Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4e0e5be726
|
|||
|
|
f18eca52af
|
||
|
808fd13ad0
|
|||
|
88c8dc4761
|
|||
|
a7eba67a97
|
|||
|
c27586231a
|
|||
|
db3db4637c
|
|||
|
bb2294f248
|
|||
|
afd56c02e6
|
|||
| dcf1ffc976 | |||
|
|
91b7028b1a | ||
|
|
734c217c03 | ||
|
|
de18c6eb71 | ||
|
|
034be6b44e | ||
|
|
995ff5a764 | ||
|
|
102bec04d6 | ||
| d05d807614 | |||
|
bffd75f4d9
|
|||
|
bdaf3da7e0
|
|||
|
353d856bf5
|
|||
| fca9ba98cd | |||
| 5df76fd881 |
@@ -16,8 +16,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 603
|
||||
versionName = '6.5-b1'
|
||||
versionCode = 606
|
||||
versionName = '6.5'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
@@ -82,7 +82,7 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:d6b578acb0') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:42ecf8a958') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
|
||||
@@ -3,17 +3,20 @@ package org.koitharu.kotatsu.core.backup
|
||||
import org.json.JSONArray
|
||||
|
||||
class BackupEntry(
|
||||
val name: String,
|
||||
val name: Name,
|
||||
val data: JSONArray
|
||||
) {
|
||||
|
||||
companion object Names {
|
||||
enum class Name(
|
||||
val key: String,
|
||||
) {
|
||||
|
||||
const val INDEX = "index"
|
||||
const val HISTORY = "history"
|
||||
const val CATEGORIES = "categories"
|
||||
const val FAVOURITES = "favourites"
|
||||
const val SETTINGS = "settings"
|
||||
const val BOOKMARKS = "bookmarks"
|
||||
INDEX("index"),
|
||||
HISTORY("history"),
|
||||
CATEGORIES("categories"),
|
||||
FAVOURITES("favourites"),
|
||||
SETTINGS("settings"),
|
||||
BOOKMARKS("bookmarks"),
|
||||
SOURCES("sources"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
|
||||
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val PAGE_SIZE = 10
|
||||
@@ -20,7 +22,7 @@ class BackupRepository @Inject constructor(
|
||||
|
||||
suspend fun dumpHistory(): BackupEntry {
|
||||
var offset = 0
|
||||
val entry = BackupEntry(BackupEntry.HISTORY, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.HISTORY, JSONArray())
|
||||
while (true) {
|
||||
val history = db.getHistoryDao().findAll(offset, PAGE_SIZE)
|
||||
if (history.isEmpty()) {
|
||||
@@ -41,7 +43,7 @@ class BackupRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun dumpCategories(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.CATEGORIES, JSONArray())
|
||||
val categories = db.getFavouriteCategoriesDao().findAll()
|
||||
for (item in categories) {
|
||||
entry.data.put(JsonSerializer(item).toJson())
|
||||
@@ -51,7 +53,7 @@ class BackupRepository @Inject constructor(
|
||||
|
||||
suspend fun dumpFavourites(): BackupEntry {
|
||||
var offset = 0
|
||||
val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.FAVOURITES, JSONArray())
|
||||
while (true) {
|
||||
val favourites = db.getFavouritesDao().findAll(offset, PAGE_SIZE)
|
||||
if (favourites.isEmpty()) {
|
||||
@@ -72,7 +74,7 @@ class BackupRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun dumpBookmarks(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.BOOKMARKS, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.BOOKMARKS, JSONArray())
|
||||
val all = db.getBookmarksDao().findAll()
|
||||
for ((m, b) in all) {
|
||||
val json = JSONObject()
|
||||
@@ -90,7 +92,7 @@ class BackupRepository @Inject constructor(
|
||||
}
|
||||
|
||||
fun dumpSettings(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.SETTINGS, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.SETTINGS, JSONArray())
|
||||
val settingsDump = settings.getAllValues().toMutableMap()
|
||||
settingsDump.remove(AppSettings.KEY_APP_PASSWORD)
|
||||
settingsDump.remove(AppSettings.KEY_PROXY_PASSWORD)
|
||||
@@ -101,8 +103,18 @@ class BackupRepository @Inject constructor(
|
||||
return entry
|
||||
}
|
||||
|
||||
suspend fun dumpSources(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.Name.SOURCES, JSONArray())
|
||||
val all = db.getSourcesDao().findAll()
|
||||
for (source in all) {
|
||||
val json = JsonSerializer(source).toJson()
|
||||
entry.data.put(json)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
fun createIndex(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
|
||||
val entry = BackupEntry(BackupEntry.Name.INDEX, JSONArray())
|
||||
val json = JSONObject()
|
||||
json.put("app_id", BuildConfig.APPLICATION_ID)
|
||||
json.put("app_version", BuildConfig.VERSION_CODE)
|
||||
@@ -111,6 +123,11 @@ class BackupRepository @Inject constructor(
|
||||
return entry
|
||||
}
|
||||
|
||||
fun getBackupDate(entry: BackupEntry?): Date? {
|
||||
val timestamp = entry?.data?.optJSONObject(0)?.getLongOrDefault("created_at", 0) ?: 0
|
||||
return if (timestamp == 0L) null else Date(timestamp)
|
||||
}
|
||||
|
||||
suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
@@ -184,6 +201,17 @@ class BackupRepository @Inject constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun restoreSources(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
val source = JsonDeserializer(item).toMangaSourceEntity()
|
||||
result += runCatchingCancellable {
|
||||
db.getSourcesDao().upsert(source)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun restoreSettings(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
|
||||
@@ -1,25 +1,44 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.Closeable
|
||||
import org.json.JSONArray
|
||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import java.io.File
|
||||
import java.util.EnumSet
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class BackupZipInput(val file: File) : Closeable {
|
||||
|
||||
private val zipFile = ZipFile(file)
|
||||
|
||||
suspend fun getEntry(name: String): BackupEntry? = runInterruptible(Dispatchers.IO) {
|
||||
val entry = zipFile.getEntry(name) ?: return@runInterruptible null
|
||||
suspend fun getEntry(name: BackupEntry.Name): BackupEntry? = runInterruptible(Dispatchers.IO) {
|
||||
val entry = zipFile.getEntry(name.key) ?: return@runInterruptible null
|
||||
val json = zipFile.getInputStream(entry).use {
|
||||
JSONArray(it.bufferedReader().readText())
|
||||
}
|
||||
BackupEntry(name, json)
|
||||
}
|
||||
|
||||
suspend fun entries(): Set<BackupEntry.Name> = runInterruptible(Dispatchers.IO) {
|
||||
zipFile.entries().toList().mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) { ze ->
|
||||
BackupEntry.Name.entries.find { it.key == ze.name }
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
zipFile.close()
|
||||
}
|
||||
|
||||
fun cleanupAsync() {
|
||||
processLifecycleScope.launch(Dispatchers.IO, CoroutineStart.ATOMIC) {
|
||||
runCatching {
|
||||
close()
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class BackupZipOutput(val file: File) : Closeable {
|
||||
private val output = ZipOutput(file, Deflater.BEST_COMPRESSION)
|
||||
|
||||
suspend fun put(entry: BackupEntry) = runInterruptible(Dispatchers.IO) {
|
||||
output.put(entry.name, entry.data.toString(2))
|
||||
output.put(entry.name.key, entry.data.toString(2))
|
||||
}
|
||||
|
||||
suspend fun finish() = runInterruptible(Dispatchers.IO) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.backup
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
@@ -78,6 +79,12 @@ class JsonDeserializer(private val json: JSONObject) {
|
||||
percent = json.getDouble("percent").toFloat(),
|
||||
)
|
||||
|
||||
fun toMangaSourceEntity() = MangaSourceEntity(
|
||||
source = json.getString("source"),
|
||||
isEnabled = json.getBoolean("enabled"),
|
||||
sortKey = json.getInt("sort_key"),
|
||||
)
|
||||
|
||||
fun toMap(): Map<String, Any?> {
|
||||
val map = mutableMapOf<String, Any?>()
|
||||
val keys = json.keys()
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.backup
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
@@ -82,6 +83,14 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
||||
},
|
||||
)
|
||||
|
||||
constructor(e: MangaSourceEntity) : this(
|
||||
JSONObject().apply {
|
||||
put("source", e.source)
|
||||
put("enabled", e.isEnabled)
|
||||
put("sort_key", e.sortKey)
|
||||
},
|
||||
)
|
||||
|
||||
constructor(m: Map<String, *>) : this(
|
||||
JSONObject(m),
|
||||
)
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.core.view.children
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipDrawable
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.theme.overlay.MaterialThemeOverlay
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.castOrNull
|
||||
|
||||
@@ -42,9 +41,10 @@ class ChipsView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
init {
|
||||
chipStyle = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0).use {
|
||||
it.getResourceId(R.styleable.ChipsView_chipStyle, R.style.Widget_Kotatsu_Chip)
|
||||
}
|
||||
val ta = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0)
|
||||
chipStyle = ta.getResourceId(R.styleable.ChipsView_chipStyle, R.style.Widget_Kotatsu_Chip)
|
||||
ta.recycle()
|
||||
|
||||
if (isInEditMode) {
|
||||
setChips(
|
||||
List(5) {
|
||||
@@ -103,14 +103,12 @@ class ChipsView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun addChip(): Chip {
|
||||
val themedContext = MaterialThemeOverlay.wrap(context, null, 0, chipStyle)
|
||||
val chip = Chip(themedContext, null)
|
||||
val drawable = ChipDrawable.createFromAttributes(themedContext, null, 0, chipStyle)
|
||||
val chip = Chip(context)
|
||||
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
|
||||
chip.setChipDrawable(drawable)
|
||||
chip.isCheckedIconVisible = true
|
||||
chip.isChipIconVisible = false
|
||||
chip.setCheckedIconResource(R.drawable.ic_check)
|
||||
// chip.checkedIconTint = chip.ic
|
||||
chip.isCloseIconVisible = onChipCloseClickListener != null
|
||||
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
||||
chip.setEnsureMinTouchTargetSize(false)
|
||||
|
||||
@@ -50,7 +50,7 @@ private fun getVolumePathBeforeAndroid11(volumeId: String, context: Context): St
|
||||
val length = ArrayReflect.getLength(checkNotNull(result))
|
||||
(0 until length).firstNotNullOfOrNull { i ->
|
||||
val storageVolumeElement = ArrayReflect.get(result, i)
|
||||
val uuid = getUuid.invoke(storageVolumeElement) as String
|
||||
val uuid = getUuid.invoke(storageVolumeElement) as String?
|
||||
val primary = isPrimary.invoke(storageVolumeElement) as Boolean
|
||||
when {
|
||||
primary && volumeId == PRIMARY_VOLUME_NAME -> getPath.invoke(storageVolumeElement) as String
|
||||
|
||||
@@ -91,7 +91,7 @@ class MangaPrefetchService : CoroutineIntentService() {
|
||||
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||
intent.action = ACTION_PREFETCH_DETAILS
|
||||
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga))
|
||||
context.startService(intent)
|
||||
tryStart(context, intent)
|
||||
}
|
||||
|
||||
fun prefetchPages(context: Context, chapter: MangaChapter) {
|
||||
@@ -99,19 +99,14 @@ class MangaPrefetchService : CoroutineIntentService() {
|
||||
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||
intent.action = ACTION_PREFETCH_PAGES
|
||||
intent.putExtra(EXTRA_CHAPTER, ParcelableChapter(chapter))
|
||||
try {
|
||||
context.startService(intent)
|
||||
} catch (e: IllegalStateException) {
|
||||
// probably app is in background
|
||||
e.printStackTraceDebug()
|
||||
}
|
||||
tryStart(context, intent)
|
||||
}
|
||||
|
||||
fun prefetchLast(context: Context) {
|
||||
if (!isPrefetchAvailable(context, null)) return
|
||||
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||
intent.action = ACTION_PREFETCH_LAST
|
||||
context.startService(intent)
|
||||
tryStart(context, intent)
|
||||
}
|
||||
|
||||
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
|
||||
@@ -127,5 +122,14 @@ class MangaPrefetchService : CoroutineIntentService() {
|
||||
)
|
||||
return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled
|
||||
}
|
||||
|
||||
private fun tryStart(context: Context, intent: Intent) {
|
||||
try {
|
||||
context.startService(intent)
|
||||
} catch (e: IllegalStateException) {
|
||||
// probably app is in background
|
||||
e.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +65,7 @@ fun categoryAD(
|
||||
binding.imageViewEdit.setOnClickListener(eventListener)
|
||||
binding.imageViewHandle.setOnTouchListener(eventListener)
|
||||
|
||||
bind { payloads ->
|
||||
if (payloads.isNotEmpty()) {
|
||||
return@bind
|
||||
}
|
||||
bind {
|
||||
binding.textViewTitle.text = item.category.title
|
||||
binding.textViewSubtitle.text = if (item.mangaCount == 0) {
|
||||
getString(R.string.empty)
|
||||
|
||||
@@ -15,13 +15,13 @@ fun categoriesHeaderAD() = adapterDelegateViewBinding<CategoriesHeaderItem, List
|
||||
|
||||
val onClickListener = View.OnClickListener { v ->
|
||||
val intent = when (v.id) {
|
||||
R.id.button_create -> FavouritesCategoryEditActivity.newIntent(v.context)
|
||||
R.id.button_manage -> FavouriteCategoriesActivity.newIntent(v.context)
|
||||
R.id.chip_create -> FavouritesCategoryEditActivity.newIntent(v.context)
|
||||
R.id.chip_manage -> FavouriteCategoriesActivity.newIntent(v.context)
|
||||
else -> return@OnClickListener
|
||||
}
|
||||
v.context.startActivity(intent)
|
||||
}
|
||||
|
||||
binding.buttonCreate.setOnClickListener(onClickListener)
|
||||
binding.buttonManage.setOnClickListener(onClickListener)
|
||||
binding.chipCreate.setOnClickListener(onClickListener)
|
||||
binding.chipManage.setOnClickListener(onClickListener)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.chip.Chip
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -39,8 +40,11 @@ class FilterSheetFragment :
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.scrollView.scrollIndicators = 0
|
||||
if (dialog == null) {
|
||||
binding.layoutBody.updatePadding(top = binding.layoutBody.paddingBottom)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.scrollView.scrollIndicators = 0
|
||||
}
|
||||
}
|
||||
val filter = requireFilter()
|
||||
filter.filterSortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.koitharu.kotatsu.filter.ui.FilterOwner
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import javax.inject.Inject
|
||||
@@ -56,8 +57,10 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
|
||||
binding.textViewAuthor.setOnClickListener(this)
|
||||
binding.imageViewCover.setOnClickListener(this)
|
||||
binding.buttonOpen.setOnClickListener(this)
|
||||
binding.buttonRead.setOnClickListener(this)
|
||||
|
||||
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||
viewModel.footer.observe(viewLifecycleOwner, ::onFooterUpdated)
|
||||
viewModel.tagsChips.observe(viewLifecycleOwner, ::onTagsChipsChanged)
|
||||
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
|
||||
}
|
||||
@@ -70,6 +73,14 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
|
||||
DetailsActivity.newIntent(v.context, manga),
|
||||
)
|
||||
|
||||
R.id.button_read -> {
|
||||
startActivity(
|
||||
ReaderActivity.IntentBuilder(v.context)
|
||||
.manga(manga)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.textView_author -> startActivity(
|
||||
SearchActivity.newIntent(
|
||||
context = v.context,
|
||||
@@ -118,6 +129,43 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFooterUpdated(footer: PreviewViewModel.FooterInfo?) {
|
||||
with(requireViewBinding()) {
|
||||
toolbarBottom.isVisible = footer != null
|
||||
if (footer == null) {
|
||||
return
|
||||
}
|
||||
toolbarBottom.title = when {
|
||||
footer.isInProgress() -> {
|
||||
getString(R.string.chapter_d_of_d, footer.currentChapter, footer.totalChapters)
|
||||
}
|
||||
|
||||
footer.totalChapters > 0 -> {
|
||||
resources.getQuantityString(R.plurals.chapters, footer.totalChapters, footer.totalChapters)
|
||||
}
|
||||
|
||||
else -> {
|
||||
getString(R.string.no_chapters)
|
||||
}
|
||||
}
|
||||
buttonRead.isEnabled = footer.totalChapters > 0
|
||||
buttonRead.setIconResource(
|
||||
when {
|
||||
footer.isIncognito -> R.drawable.ic_incognito
|
||||
footer.isInProgress() -> R.drawable.ic_play
|
||||
else -> R.drawable.ic_read
|
||||
},
|
||||
)
|
||||
buttonRead.setText(
|
||||
if (footer.isInProgress()) {
|
||||
R.string._continue
|
||||
} else {
|
||||
R.string.read
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDescriptionChanged(description: CharSequence?) {
|
||||
val tv = viewBinding?.textViewDescription ?: return
|
||||
when {
|
||||
|
||||
@@ -13,11 +13,14 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
@@ -25,6 +28,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -33,6 +37,7 @@ class PreviewViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val extraProvider: ListExtraProvider,
|
||||
private val repositoryFactory: MangaRepository.Factory,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val imageGetter: Html.ImageGetter,
|
||||
) : BaseViewModel() {
|
||||
|
||||
@@ -40,6 +45,26 @@ class PreviewViewModel @Inject constructor(
|
||||
savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga,
|
||||
)
|
||||
|
||||
val footer = combine(
|
||||
manga,
|
||||
historyRepository.observeOne(manga.value.id),
|
||||
manga.flatMapLatest { historyRepository.observeShouldSkip(it) }.distinctUntilChanged(),
|
||||
) { m, history, incognito ->
|
||||
if (m.chapters == null) {
|
||||
return@combine null
|
||||
}
|
||||
val b = m.getPreferredBranch(history)
|
||||
val chapters = m.getChapters(b).orEmpty()
|
||||
FooterInfo(
|
||||
branch = b,
|
||||
currentChapter = history?.chapterId?.let {
|
||||
chapters.indexOfFirst { x -> x.id == it }
|
||||
} ?: -1,
|
||||
totalChapters = chapters.size,
|
||||
isIncognito = incognito,
|
||||
)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, null)
|
||||
|
||||
val description = manga
|
||||
.distinctUntilChangedBy { it.description.orEmpty() }
|
||||
.transformLatest {
|
||||
@@ -82,4 +107,14 @@ class PreviewViewModel @Inject constructor(
|
||||
}
|
||||
return spannable.trim()
|
||||
}
|
||||
|
||||
data class FooterInfo(
|
||||
val branch: String?,
|
||||
val currentChapter: Int,
|
||||
val totalChapters: Int,
|
||||
val isIncognito: Boolean,
|
||||
) {
|
||||
|
||||
fun isInProgress() = currentChapter >= 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ class LocalMangaRepository @Inject constructor(
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
suspend fun findSavedManga(remoteManga: Manga): LocalManga? {
|
||||
suspend fun findSavedManga(remoteManga: Manga): LocalManga? = runCatchingCancellable {
|
||||
// fast path
|
||||
LocalMangaInput.find(storageManager.getReadableDirs(), remoteManga)?.let {
|
||||
return it.getManga()
|
||||
@@ -155,7 +155,9 @@ class LocalMangaRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
}.firstOrNull()?.getManga()
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage) = page.url
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.toCamelCase
|
||||
import java.io.File
|
||||
import java.util.TreeMap
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
/**
|
||||
@@ -49,8 +50,15 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
||||
url = mangaUri,
|
||||
coverUrl = cover,
|
||||
largeCoverUrl = cover,
|
||||
chapters = info.chapters?.mapIndexed { i, c ->
|
||||
c.copy(url = chapterFiles[i].toUri().toString(), source = MangaSource.LOCAL)
|
||||
chapters = info.chapters?.mapIndexedNotNull { i, c ->
|
||||
val fileName = index.getChapterFileName(c.id)
|
||||
val file = if (fileName != null) {
|
||||
chapterFiles[fileName]
|
||||
} else {
|
||||
// old downloads
|
||||
chapterFiles.values.elementAtOrNull(i)
|
||||
} ?: return@mapIndexedNotNull null
|
||||
c.copy(url = file.toUri().toString(), source = MangaSource.LOCAL)
|
||||
},
|
||||
) ?: Manga(
|
||||
id = root.absolutePath.longHashCode(),
|
||||
@@ -59,7 +67,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
||||
publicUrl = mangaUri,
|
||||
source = MangaSource.LOCAL,
|
||||
coverUrl = findFirstImageEntry().orEmpty(),
|
||||
chapters = chapterFiles.mapIndexed { i, f ->
|
||||
chapters = chapterFiles.values.mapIndexed { i, f ->
|
||||
MangaChapter(
|
||||
id = "$i${f.name}".longHashCode(),
|
||||
name = f.nameWithoutExtension.toHumanReadable(),
|
||||
@@ -120,9 +128,9 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
||||
|
||||
private fun String.toHumanReadable() = replace("_", " ").toCamelCase()
|
||||
|
||||
private fun getChaptersFiles(): List<File> = root.walkCompat()
|
||||
private fun getChaptersFiles() = root.walkCompat()
|
||||
.filter { it.hasCbzExtension() }
|
||||
.toListSorted(compareBy(AlphanumComparator()) { it.name })
|
||||
.associateByTo(TreeMap(AlphanumComparator())) { it.name }
|
||||
|
||||
private fun findFirstImageEntry(): String? {
|
||||
return root.walkCompat().firstOrNull { hasImageExtension(it) }?.toUri()?.toString()
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
||||
import java.io.File
|
||||
|
||||
@@ -54,7 +55,8 @@ sealed class LocalMangaInput(
|
||||
zip.isFile -> LocalMangaZipInput(zip)
|
||||
else -> null
|
||||
}
|
||||
if (input?.getMangaInfo()?.id == manga.id) {
|
||||
val info = runCatchingCancellable { input?.getMangaInfo() }.getOrNull()
|
||||
if (info?.id == manga.id) {
|
||||
send(input)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.Closeable
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
||||
import java.io.File
|
||||
|
||||
@@ -86,7 +88,11 @@ sealed class LocalMangaOutput(
|
||||
}
|
||||
|
||||
private suspend fun canWriteTo(file: File, manga: Manga): Boolean {
|
||||
val info = LocalMangaInput.of(file).getMangaInfo() ?: return false
|
||||
val info = runCatchingCancellable {
|
||||
LocalMangaInput.of(file).getMangaInfo()
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrNull() ?: return false
|
||||
return info.id == manga.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import org.koitharu.kotatsu.history.ui.HistoryListFragment
|
||||
import org.koitharu.kotatsu.local.ui.LocalStorageCleanupWorker
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
@@ -66,7 +67,6 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.about.AppUpdateDialog
|
||||
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -142,7 +142,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
viewModel.counters.observe(this, ::onCountersChanged)
|
||||
viewModel.appUpdate.observe(this, MenuInvalidator(this))
|
||||
viewModel.onFirstStart.observeEvent(this) {
|
||||
OnboardDialogFragment.show(supportFragmentManager)
|
||||
WelcomeSheet.show(supportFragmentManager)
|
||||
}
|
||||
viewModel.isIncognitoMode.observe(this) {
|
||||
adjustSearchUI(isSearchOpened(), false)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.koitharu.kotatsu.main.ui.welcome
|
||||
|
||||
import android.accounts.AccountManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.view.isGone
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
import org.koitharu.kotatsu.databinding.SheetWelcomeBinding
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
|
||||
import java.util.Locale
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipClickListener, View.OnClickListener,
|
||||
ActivityResultCallback<Uri?> {
|
||||
|
||||
private val viewModel by viewModels<WelcomeViewModel>()
|
||||
|
||||
private val backupSelectCall = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument(),
|
||||
this,
|
||||
)
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetWelcomeBinding {
|
||||
return SheetWelcomeBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetWelcomeBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.textViewWelcomeTitle.isGone = resources.getBoolean(R.bool.is_tablet)
|
||||
binding.chipsLocales.onChipClickListener = this
|
||||
binding.chipsType.onChipClickListener = this
|
||||
binding.chipBackup.setOnClickListener(this)
|
||||
binding.chipSync.setOnClickListener(this)
|
||||
|
||||
viewModel.locales.observe(viewLifecycleOwner, ::onLocalesChanged)
|
||||
viewModel.types.observe(viewLifecycleOwner, ::onTypesChanged)
|
||||
}
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is ContentType -> viewModel.setTypeChecked(data, chip.isChecked)
|
||||
is Locale? -> viewModel.setLocaleChecked(data, chip.isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.chip_backup -> {
|
||||
if (!backupSelectCall.tryLaunch(arrayOf("*/*"))) {
|
||||
Snackbar.make(
|
||||
v, R.string.operation_not_supported, Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
R.id.chip_sync -> {
|
||||
val am = AccountManager.get(v.context)
|
||||
val accountType = getString(R.string.account_type_sync)
|
||||
am.addAccount(accountType, accountType, null, null, requireActivity(), null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: Uri?) {
|
||||
if (result != null) {
|
||||
RestoreDialogFragment.show(parentFragmentManager, result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLocalesChanged(value: FilterProperty<Locale?>) {
|
||||
val chips = viewBinding?.chipsLocales ?: return
|
||||
chips.setChips(
|
||||
value.availableItems.map {
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = it?.getDisplayLanguage(it)?.toTitleCase(it) ?: getString(R.string.various_languages),
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = it in value.selectedItems,
|
||||
data = it,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun onTypesChanged(value: FilterProperty<ContentType>) {
|
||||
val chips = viewBinding?.chipsType ?: return
|
||||
chips.setChips(
|
||||
value.availableItems.map {
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = getString(it.titleResId),
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = it in value.selectedItems,
|
||||
data = it,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "WelcomeSheet"
|
||||
|
||||
fun show(fm: FragmentManager) = WelcomeSheet().showDistinct(fm, TAG)
|
||||
|
||||
fun dismiss(fm: FragmentManager): Boolean {
|
||||
val sheet = fm.findFragmentByTag(TAG) as? WelcomeSheet ?: return false
|
||||
sheet.dismissAllowingStateLoss()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.koitharu.kotatsu.main.ui.welcome
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class WelcomeViewModel @Inject constructor(
|
||||
private val repository: MangaSourcesRepository,
|
||||
@ApplicationContext context: Context,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val allSources = repository.allMangaSources
|
||||
private val localesGroups by lazy { allSources.groupBy { it.locale?.let { x -> Locale(x) } } }
|
||||
|
||||
private var updateJob: Job
|
||||
|
||||
val locales = MutableStateFlow(
|
||||
FilterProperty<Locale?>(
|
||||
availableItems = listOf(null),
|
||||
selectedItems = setOf(null),
|
||||
isLoading = true,
|
||||
error = null,
|
||||
),
|
||||
)
|
||||
|
||||
val types = MutableStateFlow(
|
||||
FilterProperty(
|
||||
availableItems = ContentType.entries.toList(),
|
||||
selectedItems = setOf(ContentType.MANGA),
|
||||
isLoading = false,
|
||||
error = null,
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
val languages = localesGroups.keys.associateBy { x -> x?.language }
|
||||
val selectedLocales = HashSet<Locale?>(2)
|
||||
selectedLocales += ConfigurationCompat.getLocales(context.resources.configuration).toList()
|
||||
.firstNotNullOfOrNull { lc -> languages[lc.language] }
|
||||
selectedLocales += null
|
||||
locales.value = locales.value.copy(
|
||||
availableItems = localesGroups.keys.sortedWithSafe(nullsFirst(LocaleComparator())),
|
||||
selectedItems = selectedLocales,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setLocaleChecked(locale: Locale?, isChecked: Boolean) {
|
||||
val snapshot = locales.value
|
||||
locales.value = snapshot.copy(
|
||||
selectedItems = if (isChecked) {
|
||||
snapshot.selectedItems + locale
|
||||
} else {
|
||||
snapshot.selectedItems - locale
|
||||
},
|
||||
)
|
||||
val prevJob = updateJob
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
prevJob.join()
|
||||
commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTypeChecked(type: ContentType, isChecked: Boolean) {
|
||||
val snapshot = types.value
|
||||
types.value = snapshot.copy(
|
||||
selectedItems = if (isChecked) {
|
||||
snapshot.selectedItems + type
|
||||
} else {
|
||||
snapshot.selectedItems - type
|
||||
},
|
||||
)
|
||||
val prevJob = updateJob
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
prevJob.join()
|
||||
commit()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun commit() {
|
||||
val languages = locales.value.selectedItems.mapToSet { it?.language }
|
||||
val types = types.value.selectedItems
|
||||
val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
|
||||
x.contentType in types && x.locale in languages
|
||||
}
|
||||
repository.setSourcesEnabledExclusive(enabledSources)
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,11 @@ class WebtoonFrameLayout @JvmOverloads constructor(
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
val target: WebtoonImageView by lazy(LazyThreadSafetyMode.NONE) {
|
||||
findViewById(R.id.ssiv)
|
||||
}
|
||||
private var _target: WebtoonImageView? = null
|
||||
val target: WebtoonImageView
|
||||
get() = _target ?: findViewById<WebtoonImageView?>(R.id.ssiv).also {
|
||||
_target = it
|
||||
}
|
||||
|
||||
fun dispatchVerticalScroll(dy: Int): Int {
|
||||
if (dy == 0) {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import android.util.AttributeSet
|
||||
import androidx.core.view.ancestors
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
|
||||
private const val SCROLL_UNKNOWN = -1
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class WebtoonImageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@@ -18,7 +20,14 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
private val ct = PointF()
|
||||
|
||||
private var scrollPos = 0
|
||||
private var scrollRange = SCROLL_UNKNOWN
|
||||
private var debugPaint: Paint? = null
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
if (BuildConfig.DEBUG) {
|
||||
drawDebug(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
fun scrollBy(delta: Int) {
|
||||
val maxScroll = getScrollRange()
|
||||
@@ -41,14 +50,14 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
fun getScroll() = scrollPos
|
||||
|
||||
fun getScrollRange(): Int {
|
||||
if (scrollRange == SCROLL_UNKNOWN) {
|
||||
computeScrollRange()
|
||||
if (!isReady) {
|
||||
return 0
|
||||
}
|
||||
return scrollRange.coerceAtLeast(0)
|
||||
val totalHeight = (sHeight * width / sWidth.toFloat()).roundToInt()
|
||||
return (totalHeight - height).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
scrollRange = SCROLL_UNKNOWN
|
||||
scrollPos = 0
|
||||
super.recycle()
|
||||
}
|
||||
@@ -91,8 +100,6 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
override fun onDownsamplingChanged() {
|
||||
super.onDownsamplingChanged()
|
||||
adjustScale()
|
||||
computeScrollRange()
|
||||
ancestors.firstNotNullOfOrNull { it as? WebtoonRecyclerView }?.updateChildrenScroll()
|
||||
}
|
||||
|
||||
override fun onReady() {
|
||||
@@ -102,13 +109,10 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
if (oldh == h || oldw == 0 || oldh == 0 || scrollRange == SCROLL_UNKNOWN) return
|
||||
|
||||
computeScrollRange()
|
||||
val container = ancestors.firstNotNullOfOrNull { it as? WebtoonFrameLayout } ?: return
|
||||
val parentHeight = parentHeight()
|
||||
if (scrollPos != 0 && container.bottom < parentHeight) {
|
||||
scrollTo(scrollRange)
|
||||
if (oldh != h && oldw != 0 && oldh != 0 && isReady) {
|
||||
ancestors.firstNotNullOfOrNull { it as? WebtoonRecyclerView }?.updateChildrenScroll()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,14 +124,6 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
setScaleAndCenter(minScale, ct)
|
||||
}
|
||||
|
||||
private fun computeScrollRange() {
|
||||
if (!isReady) {
|
||||
return
|
||||
}
|
||||
val totalHeight = (sHeight * minScale).toIntUp()
|
||||
scrollRange = (totalHeight - height).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
private fun adjustScale() {
|
||||
minScale = width / sWidth.toFloat()
|
||||
maxScale = minScale
|
||||
@@ -137,4 +133,18 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
private fun parentHeight(): Int {
|
||||
return ancestors.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0
|
||||
}
|
||||
|
||||
private fun drawDebug(canvas: Canvas) {
|
||||
val paint = debugPaint ?: Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = android.graphics.Color.RED
|
||||
strokeWidth = context.resources.resolveDp(2f)
|
||||
textAlign = android.graphics.Paint.Align.LEFT
|
||||
textSize = context.resources.resolveDp(14f)
|
||||
debugPaint = this
|
||||
}
|
||||
paint.style = Paint.Style.STROKE
|
||||
canvas.drawRect(1f, 1f, width.toFloat() - 1f, height.toFloat() - 1f, paint)
|
||||
paint.style = Paint.Style.FILL
|
||||
canvas.drawText("${getScroll()} / ${getScrollRange()}", 100f, 100f, paint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.ViewCompat.TYPE_TOUCH
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.iterator
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
|
||||
import java.util.LinkedList
|
||||
import java.util.WeakHashMap
|
||||
@@ -18,6 +17,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
|
||||
private var onPageScrollListeners: MutableList<OnPageScrollListener>? = null
|
||||
private val detachedViews = WeakHashMap<View, Unit>()
|
||||
private var isFixingScroll: Boolean = false
|
||||
|
||||
override fun onChildDetachedFromWindow(child: View) {
|
||||
super.onChildDetachedFromWindow(child)
|
||||
@@ -56,6 +56,13 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
return consumedY != 0 || dy == 0
|
||||
}
|
||||
|
||||
override fun onScrollStateChanged(state: Int) {
|
||||
super.onScrollStateChanged(state)
|
||||
if (state == SCROLL_STATE_IDLE) {
|
||||
updateChildrenScroll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun consumeVerticalScroll(dy: Int): Int {
|
||||
if (childCount == 0) {
|
||||
return 0
|
||||
@@ -106,16 +113,12 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun notifyScrollChanged(dy: Int) {
|
||||
updateChildrenScroll()
|
||||
val listeners = onPageScrollListeners
|
||||
if (listeners.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val centerPosition = findCenterViewPosition()
|
||||
listeners.forEach { it.dispatchScroll(this, dy, centerPosition) }
|
||||
if (BuildConfig.DEBUG) {
|
||||
validateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
fun relayoutChildren() {
|
||||
@@ -128,29 +131,35 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun updateChildrenScroll() {
|
||||
forEach { child ->
|
||||
if (isFixingScroll) {
|
||||
return
|
||||
}
|
||||
isFixingScroll = true
|
||||
for (child in this) {
|
||||
val ssiv = (child as WebtoonFrameLayout).target
|
||||
when {
|
||||
child.top < 0 -> ssiv.scrollTo(ssiv.getScrollRange())
|
||||
child.top > 0 -> ssiv.scrollTo(0)
|
||||
else -> ssiv.scrollBy(0)
|
||||
if (adjustScroll(child, ssiv)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
isFixingScroll = false
|
||||
}
|
||||
|
||||
private fun validateLayout() {
|
||||
forEach { child ->
|
||||
val ssiv = (child as WebtoonFrameLayout).target
|
||||
val scroll = ssiv.getScroll()
|
||||
val assertion = when {
|
||||
child.top < 0 -> scroll == ssiv.getScrollRange()
|
||||
child.top > 0 -> scroll == 0
|
||||
else -> true
|
||||
}
|
||||
if (!assertion) {
|
||||
Toast.makeText(context, "Scroll = $scroll for view with top: ${child.top}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
private fun adjustScroll(child: View, ssiv: WebtoonImageView): Boolean = when {
|
||||
child.bottom < height && ssiv.getScroll() < ssiv.getScrollRange() -> {
|
||||
val distance = minOf(height - child.bottom, ssiv.getScrollRange() - ssiv.getScroll())
|
||||
scrollBy(0, -distance)
|
||||
ssiv.scrollBy(distance)
|
||||
true
|
||||
}
|
||||
|
||||
child.top > 0 && ssiv.getScroll() > 0 -> {
|
||||
val distance = minOf(child.top, ssiv.getScroll())
|
||||
scrollBy(0, distance)
|
||||
ssiv.scrollBy(-distance)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
abstract class OnPageScrollListener {
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import org.koitharu.kotatsu.R
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionTagsBinding
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||
|
||||
fun searchSuggestionTagsAD(
|
||||
listener: SearchSuggestionListener,
|
||||
) = adapterDelegate<SearchSuggestionItem.Tags, SearchSuggestionItem>(R.layout.item_search_suggestion_tags) {
|
||||
) = adapterDelegateViewBinding<SearchSuggestionItem.Tags, SearchSuggestionItem, ItemSearchSuggestionTagsBinding>(
|
||||
{ layoutInflater, parent -> ItemSearchSuggestionTagsBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
val chipGroup = itemView as ChipsView
|
||||
|
||||
chipGroup.onChipClickListener = ChipsView.OnChipClickListener { _, data ->
|
||||
binding.chipsGenres.onChipClickListener = ChipsView.OnChipClickListener { _, data ->
|
||||
listener.onTagClick(data as? MangaTag ?: return@OnChipClickListener)
|
||||
}
|
||||
|
||||
bind {
|
||||
chipGroup.setChips(item.tags)
|
||||
binding.chipsGenres.setChips(item.tags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,11 @@ class AppBackupAgent : BackupAgent() {
|
||||
mtime: Long
|
||||
) {
|
||||
if (destination?.name?.endsWith(".bk.zip") == true) {
|
||||
restoreBackupFile(data.fileDescriptor, size, BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)))
|
||||
restoreBackupFile(
|
||||
data.fileDescriptor,
|
||||
size,
|
||||
BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)),
|
||||
)
|
||||
destination.delete()
|
||||
} else {
|
||||
super.onRestoreFile(data, size, destination, type, mode, mtime)
|
||||
@@ -69,6 +73,7 @@ class AppBackupAgent : BackupAgent() {
|
||||
backup.put(repository.dumpCategories())
|
||||
backup.put(repository.dumpFavourites())
|
||||
backup.put(repository.dumpBookmarks())
|
||||
backup.put(repository.dumpSources())
|
||||
backup.put(repository.dumpSettings())
|
||||
backup.finish()
|
||||
backup.file
|
||||
@@ -86,11 +91,12 @@ class AppBackupAgent : BackupAgent() {
|
||||
val backup = BackupZipInput(tempFile)
|
||||
try {
|
||||
runBlocking {
|
||||
backup.getEntry(BackupEntry.HISTORY)?.let { repository.restoreHistory(it) }
|
||||
backup.getEntry(BackupEntry.CATEGORIES)?.let { repository.restoreCategories(it) }
|
||||
backup.getEntry(BackupEntry.FAVOURITES)?.let { repository.restoreFavourites(it) }
|
||||
backup.getEntry(BackupEntry.BOOKMARKS)?.let { repository.restoreBookmarks(it) }
|
||||
backup.getEntry(BackupEntry.SETTINGS)?.let { repository.restoreSettings(it) }
|
||||
backup.getEntry(BackupEntry.Name.HISTORY)?.let { repository.restoreHistory(it) }
|
||||
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let { repository.restoreCategories(it) }
|
||||
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let { repository.restoreFavourites(it) }
|
||||
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let { repository.restoreBookmarks(it) }
|
||||
backup.getEntry(BackupEntry.Name.SOURCES)?.let { repository.restoreSources(it) }
|
||||
backup.getEntry(BackupEntry.Name.SETTINGS)?.let { repository.restoreSettings(it) }
|
||||
}
|
||||
} finally {
|
||||
backup.close()
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.setChecked
|
||||
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_CHECKED_CHANGED
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
|
||||
class BackupEntriesAdapter(
|
||||
clickListener: OnListItemClickListener<BackupEntryModel>,
|
||||
) : BaseListAdapter<BackupEntryModel>() {
|
||||
|
||||
init {
|
||||
addDelegate(ListItemType.NAV_ITEM, backupEntryAD(clickListener))
|
||||
}
|
||||
}
|
||||
|
||||
private fun backupEntryAD(
|
||||
clickListener: OnListItemClickListener<BackupEntryModel>,
|
||||
) = adapterDelegateViewBinding<BackupEntryModel, BackupEntryModel, ItemCheckableMultipleBinding>(
|
||||
{ layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
binding.root.setOnClickListener { v ->
|
||||
clickListener.onItemClick(item, v)
|
||||
}
|
||||
|
||||
bind { payloads ->
|
||||
with(binding.root) {
|
||||
setText(item.titleResId)
|
||||
setChecked(item.isChecked, PAYLOAD_CHECKED_CHANGED in payloads)
|
||||
isEnabled = item.isEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.backup.BackupEntry
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
data class BackupEntryModel(
|
||||
val name: BackupEntry.Name,
|
||||
val isChecked: Boolean,
|
||||
val isEnabled: Boolean,
|
||||
) : ListModel {
|
||||
|
||||
@get:StringRes
|
||||
val titleResId: Int
|
||||
get() = when (name) {
|
||||
BackupEntry.Name.INDEX -> 0 // should not appear here
|
||||
BackupEntry.Name.HISTORY -> R.string.history
|
||||
BackupEntry.Name.CATEGORIES -> R.string.favourites_categories
|
||||
BackupEntry.Name.FAVOURITES -> R.string.favourites
|
||||
BackupEntry.Name.SETTINGS -> R.string.settings
|
||||
BackupEntry.Name.BOOKMARKS -> R.string.bookmarks
|
||||
BackupEntry.Name.SOURCES -> R.string.remote_sources
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is BackupEntryModel && other.name == name
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? {
|
||||
if (previousState !is BackupEntryModel) {
|
||||
return null
|
||||
}
|
||||
return if (previousState.isEnabled != isEnabled) {
|
||||
ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED
|
||||
} else if (previousState.isChecked != isChecked) {
|
||||
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
||||
} else {
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,21 +24,25 @@ class BackupViewModel @Inject constructor(
|
||||
init {
|
||||
launchLoadingJob {
|
||||
val file = BackupZipOutput(context).use { backup ->
|
||||
val step = 1f / 6f
|
||||
backup.put(repository.createIndex())
|
||||
|
||||
progress.value = 0f
|
||||
backup.put(repository.dumpHistory())
|
||||
|
||||
progress.value = 0.2f
|
||||
progress.value += step
|
||||
backup.put(repository.dumpCategories())
|
||||
|
||||
progress.value = 0.4f
|
||||
progress.value += step
|
||||
backup.put(repository.dumpFavourites())
|
||||
|
||||
progress.value = 0.6f
|
||||
progress.value += step
|
||||
backup.put(repository.dumpBookmarks())
|
||||
|
||||
progress.value = 0.8f
|
||||
progress.value += step
|
||||
backup.put(repository.dumpSources())
|
||||
|
||||
progress.value += step
|
||||
backup.put(repository.dumpSettings())
|
||||
|
||||
backup.finish()
|
||||
|
||||
@@ -42,6 +42,7 @@ class PeriodicalBackupWorker @AssistedInject constructor(
|
||||
backup.put(repository.dumpCategories())
|
||||
backup.put(repository.dumpFavourites())
|
||||
backup.put(repository.dumpBookmarks())
|
||||
backup.put(repository.dumpSources())
|
||||
backup.put(repository.dumpSettings())
|
||||
backup.finish()
|
||||
backup.file
|
||||
|
||||
@@ -3,40 +3,58 @@ package org.koitharu.kotatsu.settings.backup
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.backup.CompositeResult
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.DialogProgressBinding
|
||||
import org.koitharu.kotatsu.databinding.DialogRestoreBinding
|
||||
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnListItemClickListener<BackupEntryModel>,
|
||||
View.OnClickListener {
|
||||
|
||||
private val viewModel: RestoreViewModel by viewModels()
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
) = DialogProgressBinding.inflate(inflater, container, false)
|
||||
) = DialogRestoreBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewBindingCreated(binding: DialogProgressBinding, savedInstanceState: Bundle?) {
|
||||
override fun onViewBindingCreated(binding: DialogRestoreBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.textViewTitle.setText(R.string.restore_backup)
|
||||
binding.textViewSubtitle.setText(R.string.preparing_)
|
||||
|
||||
val adapter = BackupEntriesAdapter(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.buttonCancel.setOnClickListener(this)
|
||||
binding.buttonRestore.setOnClickListener(this)
|
||||
viewModel.availableEntries.observe(viewLifecycleOwner, adapter)
|
||||
viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged)
|
||||
viewModel.onRestoreDone.observeEvent(viewLifecycleOwner, this::onRestoreDone)
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, this::onError)
|
||||
combine(
|
||||
viewModel.isLoading,
|
||||
viewModel.availableEntries,
|
||||
viewModel.backupDate,
|
||||
::Triple,
|
||||
).observe(viewLifecycleOwner, this::onLoadingChanged)
|
||||
}
|
||||
|
||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
||||
@@ -44,6 +62,40 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
.setCancelable(false)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_cancel -> dismiss()
|
||||
R.id.button_restore -> viewModel.restore()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: BackupEntryModel, view: View) {
|
||||
viewModel.onItemClick(item)
|
||||
}
|
||||
|
||||
private fun onLoadingChanged(value: Triple<Boolean, List<BackupEntryModel>, Date?>) {
|
||||
val (isLoading, entries, backupDate) = value
|
||||
val hasEntries = entries.isNotEmpty()
|
||||
with(requireViewBinding()) {
|
||||
progressBar.isVisible = isLoading
|
||||
recyclerView.isGone = isLoading
|
||||
textViewSubtitle.textAndVisible =
|
||||
when {
|
||||
!isLoading -> backupDate?.formatBackupDate()
|
||||
hasEntries -> getString(R.string.processing_)
|
||||
else -> getString(R.string.loading_)
|
||||
}
|
||||
buttonRestore.isEnabled = !isLoading && entries.any { it.isChecked }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Date.formatBackupDate(): String {
|
||||
return getString(
|
||||
R.string.backup_date_,
|
||||
SimpleDateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(this),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setNegativeButton(R.string.close, null)
|
||||
@@ -89,6 +141,9 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
}
|
||||
builder.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
if (!result.isEmpty && !result.isAllFailed) {
|
||||
WelcomeSheet.dismiss(parentFragmentManager)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,12 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.Date
|
||||
import java.util.EnumMap
|
||||
import java.util.EnumSet
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -26,57 +30,127 @@ class RestoreViewModel @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val backupInput = SuspendLazy {
|
||||
val uri = savedStateHandle.get<String>(RestoreDialogFragment.ARG_FILE)
|
||||
?.toUriOrNull() ?: throw FileNotFoundException()
|
||||
val contentResolver = context.contentResolver
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
val tempFile = File.createTempFile("backup_", ".tmp")
|
||||
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
BackupZipInput(tempFile)
|
||||
}
|
||||
}
|
||||
|
||||
val progress = MutableStateFlow(-1f)
|
||||
val onRestoreDone = MutableEventFlow<CompositeResult>()
|
||||
|
||||
val availableEntries = MutableStateFlow<List<BackupEntryModel>>(emptyList())
|
||||
val backupDate = MutableStateFlow<Date?>(null)
|
||||
|
||||
init {
|
||||
launchLoadingJob {
|
||||
val uri = savedStateHandle.get<String>(RestoreDialogFragment.ARG_FILE)
|
||||
?.toUriOrNull() ?: throw FileNotFoundException()
|
||||
val contentResolver = context.contentResolver
|
||||
|
||||
val backup = runInterruptible(Dispatchers.IO) {
|
||||
val tempFile = File.createTempFile("backup_", ".tmp")
|
||||
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val backup = backupInput.get()
|
||||
val entries = backup.entries()
|
||||
availableEntries.value = BackupEntry.Name.entries.mapNotNull { entry ->
|
||||
if (entry == BackupEntry.Name.INDEX || entry !in entries) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
BackupZipInput(tempFile)
|
||||
BackupEntryModel(
|
||||
name = entry,
|
||||
isChecked = true,
|
||||
isEnabled = true,
|
||||
)
|
||||
}
|
||||
try {
|
||||
val result = CompositeResult()
|
||||
backupDate.value = repository.getBackupDate(backup.getEntry(BackupEntry.Name.INDEX))
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 0f
|
||||
backup.getEntry(BackupEntry.HISTORY)?.let {
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
backupInput.peek()?.cleanupAsync()
|
||||
}
|
||||
|
||||
fun onItemClick(item: BackupEntryModel) {
|
||||
val map = availableEntries.value.associateByTo(EnumMap(BackupEntry.Name::class.java)) { it.name }
|
||||
map[item.name] = item.copy(isChecked = !item.isChecked)
|
||||
map.validate()
|
||||
availableEntries.value = map.values.sortedBy { it.name.ordinal }
|
||||
}
|
||||
|
||||
fun restore() {
|
||||
launchLoadingJob {
|
||||
val backup = backupInput.get()
|
||||
val checkedItems = availableEntries.value.mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) {
|
||||
if (it.isChecked) it.name else null
|
||||
}
|
||||
val result = CompositeResult()
|
||||
val step = 1f / 6f
|
||||
|
||||
progress.value = 0f
|
||||
if (BackupEntry.Name.HISTORY in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.HISTORY)?.let {
|
||||
result += repository.restoreHistory(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 0.2f
|
||||
backup.getEntry(BackupEntry.CATEGORIES)?.let {
|
||||
progress.value += step
|
||||
if (BackupEntry.Name.CATEGORIES in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let {
|
||||
result += repository.restoreCategories(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 0.4f
|
||||
backup.getEntry(BackupEntry.FAVOURITES)?.let {
|
||||
progress.value += step
|
||||
if (BackupEntry.Name.FAVOURITES in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let {
|
||||
result += repository.restoreFavourites(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 0.6f
|
||||
backup.getEntry(BackupEntry.BOOKMARKS)?.let {
|
||||
progress.value += step
|
||||
if (BackupEntry.Name.BOOKMARKS in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let {
|
||||
result += repository.restoreBookmarks(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 0.8f
|
||||
backup.getEntry(BackupEntry.SETTINGS)?.let {
|
||||
progress.value += step
|
||||
if (BackupEntry.Name.SOURCES in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.SOURCES)?.let {
|
||||
result += repository.restoreSources(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value += step
|
||||
if (BackupEntry.Name.SETTINGS in checkedItems) {
|
||||
backup.getEntry(BackupEntry.Name.SETTINGS)?.let {
|
||||
result += repository.restoreSettings(it)
|
||||
}
|
||||
}
|
||||
|
||||
progress.value = 1f
|
||||
onRestoreDone.call(result)
|
||||
} finally {
|
||||
backup.close()
|
||||
backup.file.delete()
|
||||
progress.value = 1f
|
||||
onRestoreDone.call(result)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for inconsistent user selection
|
||||
* Favorites cannot be restored without categories
|
||||
*/
|
||||
private fun MutableMap<BackupEntry.Name, BackupEntryModel>.validate() {
|
||||
val favorites = this[BackupEntry.Name.FAVOURITES] ?: return
|
||||
val categories = this[BackupEntry.Name.CATEGORIES]
|
||||
if (categories?.isChecked == true) {
|
||||
if (!favorites.isEnabled) {
|
||||
this[BackupEntry.Name.FAVOURITES] = favorites.copy(isEnabled = true)
|
||||
}
|
||||
} else {
|
||||
if (favorites.isEnabled) {
|
||||
this[BackupEntry.Name.FAVOURITES] = favorites.copy(isEnabled = false, isChecked = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.showAllowStateLoss
|
||||
import org.koitharu.kotatsu.databinding.DialogOnboardBinding
|
||||
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocaleListener
|
||||
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocalesAdapter
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
|
||||
@AndroidEntryPoint
|
||||
class OnboardDialogFragment :
|
||||
AlertDialogFragment<DialogOnboardBinding>(),
|
||||
DialogInterface.OnClickListener, SourceLocaleListener {
|
||||
|
||||
private val viewModel by viewModels<OnboardViewModel>()
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
) = DialogOnboardBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
||||
super.onBuildDialog(builder)
|
||||
.setPositiveButton(R.string.done, this)
|
||||
.setCancelable(false)
|
||||
builder.setTitle(R.string.welcome)
|
||||
return builder
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: DialogOnboardBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
val adapter = SourceLocalesAdapter(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.textViewTitle.setText(R.string.onboard_text)
|
||||
viewModel.list.observe(viewLifecycleOwner, adapter)
|
||||
}
|
||||
|
||||
override fun onItemCheckedChanged(item: SourceLocale, isChecked: Boolean) {
|
||||
viewModel.setItemChecked(item.key, isChecked)
|
||||
}
|
||||
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "OnboardDialog"
|
||||
|
||||
fun show(fm: FragmentManager) = OnboardDialogFragment().showAllowStateLoss(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class OnboardViewModel @Inject constructor(
|
||||
private val repository: MangaSourcesRepository,
|
||||
@ApplicationContext context: Context,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val allSources = repository.allMangaSources
|
||||
private val locales = allSources.groupBy { it.locale }
|
||||
private val selectedLocales = HashSet<String?>()
|
||||
val list = MutableStateFlow<List<SourceLocale>?>(null)
|
||||
private var updateJob: Job
|
||||
|
||||
init {
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
if (repository.isSetupRequired()) {
|
||||
selectedLocales += ConfigurationCompat.getLocales(context.resources.configuration).toList()
|
||||
.firstOrNull { lc -> lc.language in locales.keys }?.language
|
||||
selectedLocales += null
|
||||
} else {
|
||||
selectedLocales.addAll(
|
||||
repository.getEnabledSources().mapNotNullToSet { x -> x.locale },
|
||||
)
|
||||
}
|
||||
commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun setItemChecked(key: String?, isChecked: Boolean) {
|
||||
val isModified = if (isChecked) {
|
||||
selectedLocales.add(key)
|
||||
} else {
|
||||
selectedLocales.remove(key)
|
||||
}
|
||||
if (isModified) {
|
||||
val prevJob = updateJob
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
prevJob.join()
|
||||
commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun commit() {
|
||||
val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
|
||||
x.locale in selectedLocales
|
||||
}
|
||||
repository.setSourcesEnabledExclusive(enabledSources)
|
||||
list.value = locales.map { (key, srcs) ->
|
||||
val locale = key?.let { Locale(it) }
|
||||
SourceLocale(
|
||||
key = key,
|
||||
title = locale?.getDisplayLanguage(locale)?.toTitleCase(locale),
|
||||
summary = srcs.joinToString { it.title },
|
||||
isChecked = key in selectedLocales,
|
||||
)
|
||||
}.sortedWithSafe(SourceLocaleComparator())
|
||||
}
|
||||
|
||||
private class SourceLocaleComparator : Comparator<SourceLocale> {
|
||||
|
||||
private val delegate = nullsFirst(LocaleComparator())
|
||||
|
||||
override fun compare(a: SourceLocale, b: SourceLocale): Int {
|
||||
val localeA = a.key?.let { Locale(it) }
|
||||
val localeB = b.key?.let { Locale(it) }
|
||||
return delegate.compare(localeA, localeB)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard.adapter
|
||||
|
||||
import android.widget.CompoundButton
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.setChecked
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ItemSourceLocaleBinding
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
|
||||
fun sourceLocaleAD(
|
||||
listener: SourceLocaleListener,
|
||||
) = adapterDelegateViewBinding<SourceLocale, SourceLocale, ItemSourceLocaleBinding>(
|
||||
{ inflater, parent -> ItemSourceLocaleBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
val checkedChangeListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
|
||||
listener.onItemCheckedChanged(item, isChecked)
|
||||
}
|
||||
binding.switchToggle.setOnCheckedChangeListener(checkedChangeListener)
|
||||
|
||||
bind { payloads ->
|
||||
binding.textViewTitle.text = item.title ?: getString(R.string.different_languages)
|
||||
binding.textViewDescription.textAndVisible = item.summary
|
||||
binding.switchToggle.setOnCheckedChangeListener(null)
|
||||
binding.switchToggle.setChecked(item.isChecked, payloads.isNotEmpty())
|
||||
binding.switchToggle.setOnCheckedChangeListener(checkedChangeListener)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard.adapter
|
||||
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
|
||||
interface SourceLocaleListener {
|
||||
|
||||
fun onItemCheckedChanged(item: SourceLocale, isChecked: Boolean)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard.adapter
|
||||
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
|
||||
class SourceLocalesAdapter(
|
||||
listener: SourceLocaleListener,
|
||||
) : BaseListAdapter<SourceLocale>() {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(sourceLocaleAD(listener))
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.onboard.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import java.util.Locale
|
||||
|
||||
data class SourceLocale(
|
||||
val key: String?,
|
||||
val title: String?,
|
||||
val summary: String?,
|
||||
val isChecked: Boolean,
|
||||
) : ListModel, Comparable<SourceLocale> {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is SourceLocale && key == other.key
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? {
|
||||
return if (previousState is SourceLocale && previousState.isChecked != isChecked) {
|
||||
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
||||
} else {
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun compareTo(other: SourceLocale): Int {
|
||||
return when {
|
||||
this === other -> 0
|
||||
key == Locale.getDefault().language -> -2
|
||||
key == null -> 1
|
||||
other.key == null -> -1
|
||||
else -> compareValues(title, other.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
11
app/src/main/res/drawable/ic_welcome.xml
Normal file
11
app/src/main/res/drawable/ic_welcome.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12 2C13.1 2 14 2.9 14 4S13.1 6 12 6 10 5.1 10 4 10.9 2 12 2M15.9 8.1C15.5 7.7 14.8 7 13.5 7H11C8.2 7 6 4.8 6 2H4C4 5.2 6.1 7.8 9 8.7V22H11V16H13V22H15V10.1L19 14L20.4 12.6L15.9 8.1Z" />
|
||||
</vector>
|
||||
78
app/src/main/res/layout/dialog_restore.xml
Normal file
78
app/src/main/res/layout/dialog_restore.xml
Normal file
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="?dialogPreferredPadding">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="?dialogPreferredPadding"
|
||||
android:paddingBottom="@dimen/margin_normal"
|
||||
android:text="@string/restore_backup"
|
||||
android:textAppearance="?textAppearanceTitleLarge" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="?dialogPreferredPadding"
|
||||
android:indeterminate="true"
|
||||
android:max="100"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:scrollIndicators="top|bottom"
|
||||
android:visibility="gone"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="6"
|
||||
tools:listitem="@layout/item_checkable_multiple"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="?dialogPreferredPadding"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:text="@tools:sample/lorem[10]" />
|
||||
|
||||
<LinearLayout
|
||||
style="?buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="?dialogPreferredPadding"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_cancel"
|
||||
style="?buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@android:string/cancel" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_restore"
|
||||
style="?buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/restore" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,165 +1,197 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollIndicators="bottom">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?colorSecondaryContainer"
|
||||
android:foreground="?selectableItemBackground"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="H,13:18"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
|
||||
tools:background="@tools:sample/backgrounds/scenic[5]"
|
||||
tools:ignore="ContentDescription,UnusedAttribute" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="5"
|
||||
android:textAppearance="?attr/textAppearanceHeadlineSmall"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_open"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginEnd="16dp"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
android:paddingBottom="?actionBarSize">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_open"
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?colorSecondaryContainer"
|
||||
android:foreground="?selectableItemBackground"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="H,13:18"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
|
||||
tools:background="@tools:sample/backgrounds/scenic[5]"
|
||||
tools:ignore="ContentDescription,UnusedAttribute" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="5"
|
||||
android:textAppearance="?attr/textAppearanceHeadlineSmall"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_open"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginEnd="16dp"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_open"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/details"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_close"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_expand" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/close"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="?actionModeCloseDrawable"
|
||||
app:tint="?colorControlNormal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title"
|
||||
tools:text="@tools:sample/lorem[12]" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/list_selector"
|
||||
android:padding="2dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="?attr/colorTertiary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<RatingBar
|
||||
android:id="@+id/rating_bar"
|
||||
style="?ratingBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:isIndicator="true"
|
||||
android:max="1"
|
||||
android:numStars="5"
|
||||
android:stepSize="0.5"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_author"
|
||||
tools:rating="4" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:barrierMargin="8dp"
|
||||
app:constraint_referenced_ids="imageView_cover,rating_bar" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_tags"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:chipSpacingHorizontal="6dp"
|
||||
app:chipSpacingVertical="6dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/barrier_header" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.SelectableTextView
|
||||
android:id="@+id/textView_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:layout_marginEnd="@dimen/margin_normal"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:paddingBottom="@dimen/margin_normal"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textIsSelectable="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/chips_tags"
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?colorBackgroundFloating">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_read"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/details"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_close"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_expand" />
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
|
||||
android:enabled="false"
|
||||
android:text="@string/read"
|
||||
android:textAllCaps="false"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
tools:enabled="true"
|
||||
tools:icon="@drawable/ic_read" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/close"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="?actionModeCloseDrawable"
|
||||
app:tint="?colorControlNormal" />
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title"
|
||||
tools:text="@tools:sample/lorem[12]" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/list_selector"
|
||||
android:padding="2dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="?attr/colorTertiary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<RatingBar
|
||||
android:id="@+id/rating_bar"
|
||||
style="?ratingBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:isIndicator="true"
|
||||
android:max="1"
|
||||
android:numStars="5"
|
||||
android:stepSize="0.5"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_author"
|
||||
tools:rating="4" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:barrierMargin="8dp"
|
||||
app:constraint_referenced_ids="imageView_cover,rating_bar" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_tags"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:chipSpacingHorizontal="6dp"
|
||||
app:chipSpacingVertical="6dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/barrier_header" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.SelectableTextView
|
||||
android:id="@+id/textView_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:layout_marginEnd="@dimen/margin_normal"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:paddingBottom="@dimen/margin_normal"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textIsSelectable="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/chips_tags"
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
</FrameLayout>
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<HorizontalScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="8dp"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
android:scrollbars="none">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_create"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:text="@string/create_category"
|
||||
app:icon="@drawable/ic_add" />
|
||||
android:paddingStart="?listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||
app:singleLine="true">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_manage"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:text="@string/manage"
|
||||
app:icon="@drawable/ic_edit" />
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_create"
|
||||
style="@style/Widget.Kotatsu.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/create_category"
|
||||
app:chipIcon="@drawable/ic_add" />
|
||||
|
||||
</LinearLayout>
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_manage"
|
||||
style="@style/Widget.Kotatsu.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/manage_categories"
|
||||
app:chipIcon="@drawable/ic_edit" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
<HorizontalScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="?listPreferredItemPaddingStart"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="6dp" />
|
||||
android:scrollbars="none">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_genres"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="?listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||
app:singleLine="true" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
android:scrollIndicators="top">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
@@ -114,10 +115,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:drawablePadding="16dp"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:paddingVertical="8dp"
|
||||
android:textAppearance="?textAppearanceBodySmall"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_error_small"
|
||||
|
||||
@@ -17,18 +17,19 @@
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/text_input_layout"
|
||||
style="?textInputFilledDenseStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:startIconDrawable="?android:actionModeWebSearchDrawable"
|
||||
app:endIconMode="clear_text"
|
||||
app:endIconDrawable="@drawable/abc_ic_clear_material"
|
||||
android:hint="@string/genres_search_hint"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:hint="@string/genres_search_hint"
|
||||
app:endIconDrawable="@drawable/abc_ic_clear_material"
|
||||
app:endIconMode="clear_text"
|
||||
app:layout_constraintBottom_toTopOf="@id/recyclerView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/headerBar"
|
||||
app:layout_constraintBottom_toTopOf="@id/recyclerView">
|
||||
app:startIconDrawable="?android:actionModeWebSearchDrawable">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_search"
|
||||
@@ -36,7 +37,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionSearch|flagNoFullscreen"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text"/>
|
||||
android:inputType="text" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
117
app/src/main/res/layout/sheet_welcome.xml
Normal file
117
app/src/main/res/layout/sheet_welcome.xml
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
|
||||
android:id="@+id/headerBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/welcome" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollIndicators="top">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/margin_normal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_welcome_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:singleLine="true"
|
||||
android:text="@string/welcome"
|
||||
android:textAppearance="?textAppearanceTitleLarge"
|
||||
android:drawablePadding="16dp"
|
||||
app:drawableStartCompat="@drawable/ic_welcome" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:scrollbars="none">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
app:singleLine="true">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_backup"
|
||||
style="@style/Widget.Kotatsu.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/restore_backup"
|
||||
app:chipIcon="@drawable/ic_backup_restore" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_sync"
|
||||
style="@style/Widget.Kotatsu.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sync_auth"
|
||||
app:chipIcon="@drawable/ic_sync" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_hint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:text="@string/welcome_text"
|
||||
android:textAppearance="?textAppearanceBodyMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_locales_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:singleLine="true"
|
||||
android:text="@string/languages"
|
||||
android:textAppearance="?textAppearanceTitleSmall" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_locales"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_type_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:singleLine="true"
|
||||
android:text="@string/type"
|
||||
android:textAppearance="?textAppearanceTitleSmall" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_type"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
@@ -178,4 +178,7 @@
|
||||
<string name="system_default">الافتراضي</string>
|
||||
<string name="sign_in">تسجبل الدخول</string>
|
||||
<string name="domain">المجال</string>
|
||||
<string name="text_history_holder_primary">كل ما تقرأه سيعرض هنا</string>
|
||||
<string name="text_search_holder_secondary">حاول إعادة صياغة الكلمات.</string>
|
||||
<string name="text_empty_holder_primary">يبدو أنه فارغ…</string>
|
||||
</resources>
|
||||
@@ -538,4 +538,5 @@
|
||||
<string name="skip">Прапусціць</string>
|
||||
<string name="color_correction_apply_text">Гэтыя налады могуць прымяняцца глабальна або толькі да бягучай мангі. Пры глабальным прымяненні індывідуальныя налады не будуць перавызначаны.</string>
|
||||
<string name="grayscale">Адценні шэрага</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Можа дапамагчы з пачаткам загрузкі, калі ў вас узнікаюць з ёй праблемы</string>
|
||||
</resources>
|
||||
@@ -538,4 +538,9 @@
|
||||
<string name="error_filter_states_genre_not_supported">Esta fuente no permite filtrar por géneros ni por estados</string>
|
||||
<string name="error_filter_locale_genre_not_supported">Esta fuente no permite filtrar por géneros ni por configuración regional</string>
|
||||
<string name="genres_search_hint">Empieza a escribir el nombre del género</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Puede que te ayude a iniciar la descarga si tienes algún problema</string>
|
||||
<string name="welcome_text">Por favor, seleccione las fuentes de contenido que desea habilitar. Esto también se puede configurar más tarde en la configuración</string>
|
||||
<string name="restore">Restaurar</string>
|
||||
<string name="backup_date_">Fecha de la copia de seguridad: %s</string>
|
||||
<string name="sync_auth">Iniciar sesión en la cuenta de sincronización</string>
|
||||
</resources>
|
||||
@@ -538,4 +538,9 @@
|
||||
<string name="skip">Пропустить</string>
|
||||
<string name="color_correction_apply_text">Эти настройки можно применить глобально или только к текущей манге. Если применяется глобально, отдельные настройки не будут переопределены.</string>
|
||||
<string name="grayscale">Оттенки серого</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Может помочь с запуском загрузки, если у вас с этим возникают проблемы</string>
|
||||
<string name="sync_auth">Войти в аккаунт синхронизации</string>
|
||||
<string name="welcome_text">Пожалуйста, выберите, какие источники вы хотите включить. Это можно изменить в настройках позже</string>
|
||||
<string name="restore">Восстановить</string>
|
||||
<string name="backup_date_">Дата резервного копирования: %s</string>
|
||||
</resources>
|
||||
@@ -121,7 +121,7 @@
|
||||
<string name="text_shelf_holder_secondary">Пронађите шта да читате у одељку „Преглед“</string>
|
||||
<string name="manga_shelf">Полица</string>
|
||||
<string name="check_for_updates">Провери ажурирања</string>
|
||||
<string name="feed">Фид</string>
|
||||
<string name="feed">Новости</string>
|
||||
<string name="services">Услуге</string>
|
||||
<string name="show_on_shelf">Прикажи на полици</string>
|
||||
<string name="explore">Преглед</string>
|
||||
@@ -159,7 +159,7 @@
|
||||
<string name="frequency_every_day">Сваки дан</string>
|
||||
<string name="download_started">Преузимање је почело</string>
|
||||
<string name="categories">Категорије</string>
|
||||
<string name="tracking">Праћење</string>
|
||||
<string name="tracking">Пратим</string>
|
||||
<string name="progress">Напредак</string>
|
||||
<string name="cancel_all">Откажи све</string>
|
||||
<string name="sync_host_description">Можеш да користиш сервер за синхронизацију који се самостално хостује или подразумевани. Не мењај ово ако ниси сигуран шта радиш.</string>
|
||||
@@ -183,7 +183,7 @@
|
||||
<string name="error_multiple_states_not_supported">Овај извор манга не подржава филтрирање према више стања</string>
|
||||
<string name="order_added">Додато</string>
|
||||
<string name="source_summary_pattern">%1$s, %2$s</string>
|
||||
<string name="enable_logging">Омогући пријаву</string>
|
||||
<string name="enable_logging">Омогући дијагностику</string>
|
||||
<string name="on_device">На уређају</string>
|
||||
<string name="download_option_whole_manga">Цела манга</string>
|
||||
<string name="settings_apply_restart_required">Поново покрени апликацију да би применио ове промене</string>
|
||||
@@ -528,4 +528,15 @@
|
||||
<string name="done">Готово</string>
|
||||
<string name="error_no_space_left">Недовољно меморије на уређају</string>
|
||||
<string name="sign_in">Пријави се</string>
|
||||
<string name="error_filter_states_genre_not_supported">Овај извор не подржава филтрирање по жанровима и стањима у исто време</string>
|
||||
<string name="error_filter_locale_genre_not_supported">Овај извор не подржава филтрирање по жанровима и локалним стандардима у исто време</string>
|
||||
<string name="apply">Примени</string>
|
||||
<string name="genres_search_hint">Унеси назив жанра</string>
|
||||
<string name="globally">Глобално</string>
|
||||
<string name="downloads_settings_info">Можеш да омогућиш успоравање преузимања за сваки извор манге појединачно у поставкама извора ако имаш проблеме са блокирањем од стране сервера</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Можда ће вам помоћи да започнете преузимање ако имате проблем са преузимањем</string>
|
||||
<string name="this_manga">Ова манга</string>
|
||||
<string name="skip">Прескочи</string>
|
||||
<string name="color_correction_apply_text">Ова подешавања се могу применити глобално или само на тренутну мангу. Ако се примењују глобално, појединачна подешавања неће бити замењена.</string>
|
||||
<string name="grayscale">Сиви тон</string>
|
||||
</resources>
|
||||
@@ -369,4 +369,18 @@
|
||||
<string name="zoom_out">ซูมออก</string>
|
||||
<string name="text_search_holder_secondary">โปรดลองเรียบเรียงคำค้นหา</string>
|
||||
<string name="last_successful_backup">สำรองข้อมูลสำเร็จล่าสุด: %s</string>
|
||||
<string name="content_type_manga">มังงะ</string>
|
||||
<string name="source_summary_pattern">%1$s, %2$s</string>
|
||||
<string name="reader_optimize">ลดการใช้แรม (เบต้า)</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">อาจจะช่วยในการเริ่มต้นการดาวน์โหลดถ้าคุณมีปัญหา</string>
|
||||
<string name="this_manga">มังงะเรื่องนี้</string>
|
||||
<string name="lock_screen_rotation">ล็อคการหมุนหน้าจอ</string>
|
||||
<string name="skip">ข้าม</string>
|
||||
<string name="state_paused">หยุดชั่วคราว</string>
|
||||
<string name="content_type_other">อื่น</string>
|
||||
<string name="pages_cache">แคชหน้า</string>
|
||||
<string name="reader_info_pattern">บท. %1$d/%2$d หน้า. %3$d/%4$d</string>
|
||||
<string name="queued">เพิ่มเข้าคิวแล้ว</string>
|
||||
<string name="exit_confirmation">ยืนยันการออก</string>
|
||||
<string name="reorder">เรียงลำดับ</string>
|
||||
</resources>
|
||||
@@ -123,7 +123,7 @@
|
||||
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%1$d / %2$d açık</string>
|
||||
<string name="text_search_holder_secondary">Sorguyu yeniden biçimlendirmeyi deneyin.</string>
|
||||
<string name="text_history_holder_primary">Okuduklarınız burada görüntülenecek</string>
|
||||
<string name="text_history_holder_secondary">Keşfet menüsünde ne okuyacağınızı bulun.</string>
|
||||
<string name="text_history_holder_secondary">«Keşfet» kısmında neler okuyacağınızı bulun</string>
|
||||
<string name="text_local_holder_primary">Önce bir şey kaydedin</string>
|
||||
<string name="text_local_holder_secondary">Çevrim içi kaynaklardan kaydedin veya dosyaları içe aktarın.</string>
|
||||
<string name="manga_shelf">Raf</string>
|
||||
@@ -282,8 +282,8 @@
|
||||
<string name="canceled">İptal edilmiş</string>
|
||||
<string name="account_already_exists">Hesap zaten var</string>
|
||||
<string name="back">Geri</string>
|
||||
<string name="sync">Senkronizasyon</string>
|
||||
<string name="sync_title">Verini yedekle</string>
|
||||
<string name="sync">Eşitleme</string>
|
||||
<string name="sync_title">Verinizi eşitleyin</string>
|
||||
<string name="email_enter_hint">Devam etmek için E-Postanızı girin</string>
|
||||
<string name="clear_all_history">Tüm gecmişi temizle</string>
|
||||
<string name="last_2_hours">Son 2 saat</string>
|
||||
@@ -377,7 +377,7 @@
|
||||
<string name="enable">Etkinleştir</string>
|
||||
<string name="no_thanks">Hayır teşekkürler</string>
|
||||
<string name="clear_network_cache">İnternet geçmişini temizle</string>
|
||||
<string name="sync_settings">Eşitleme seçenekleri</string>
|
||||
<string name="sync_settings">Eşitleme ayarları</string>
|
||||
<string name="server_address">Sunucu adresi</string>
|
||||
<string name="sync_host_description">Şirket içinde barındırılan bir eşitleme sunucusu veya varsayılan bir sunucu kullanabilirsiniz. Ne yaptığınızdan emin değilseniz bunu değiştirmeyin.</string>
|
||||
<string name="mirror_switching_summary">Aynalar mevcutsa hatalarda manga kaynakları için etki alanlarını otomatik olarak değiştir</string>
|
||||
@@ -528,4 +528,19 @@
|
||||
<string name="disable_nsfw_summary">Eğer mümkünse yetişkin içerik ve NSFW kaynaklarını arama listesinden kaldır</string>
|
||||
<string name="available_d">Mevcut:%1$d</string>
|
||||
<string name="state">Durum</string>
|
||||
<string name="error_filter_states_genre_not_supported">Hem türlere hem de duruma göre filtreleme bu kaynak tarafından desteklenmiyor</string>
|
||||
<string name="error_filter_locale_genre_not_supported">Hem türlere hem de yerel ayara göre filtreleme bu kaynak tarafından desteklenmiyor</string>
|
||||
<string name="apply">Uygula</string>
|
||||
<string name="genres_search_hint">Tür adını yazmaya başlayın</string>
|
||||
<string name="globally">Genel</string>
|
||||
<string name="downloads_settings_info">Sunucu taraflı engelleme ile ilgili sorun yaşıyorsanız, kaynak ayarlarında her manga kaynağı için ayrı ayrı indirme yavaşlatmayı etkinleştirebilirsiniz</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Herhangi bir sorun yaşarsanız indirmeyi başlatmanıza yardımcı olabilir</string>
|
||||
<string name="this_manga">Bu manga</string>
|
||||
<string name="skip">Atla</string>
|
||||
<string name="color_correction_apply_text">Bu ayarlar genel olarak veya yalnızca geçerli mangaya uygulanabilir. Genel olarak uygulanırsa, bireysel ayarlar geçersiz kılınmaz.</string>
|
||||
<string name="grayscale">Gri tonlamalı</string>
|
||||
<string name="welcome_text">Lütfen hangi içerik kaynaklarını etkinleştirmek istediğinizi seçin. Bu daha sonra ayarlardan da yapılandırılabilir</string>
|
||||
<string name="sync_auth">Eşitleme hesabında oturum aç</string>
|
||||
<string name="restore">Geri yükle</string>
|
||||
<string name="backup_date_">Yedekleme tarihi: %s</string>
|
||||
</resources>
|
||||
@@ -538,4 +538,5 @@
|
||||
<string name="skip">Пропустити</string>
|
||||
<string name="color_correction_apply_text">Ці налаштування можна застосувати глобально або лише до поточної манґи. Користувацькі налаштування не будуть замінені, якщо застосовувати їх глобально.</string>
|
||||
<string name="grayscale">Відтінки сірого</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Може допомогти з початком завантаження, якщо у вас виникають проблеми з нею</string>
|
||||
</resources>
|
||||
@@ -544,4 +544,8 @@
|
||||
<string name="error_filter_states_genre_not_supported">Filtering by both genres and states is not supported by this source</string>
|
||||
<string name="genres_search_hint">Start typing the genre name</string>
|
||||
<string name="disable_battery_optimization_summary_downloads">Might help with getting the download started if you have any issues with it</string>
|
||||
<string name="welcome_text">Please select which content sources you would like to enable. This can also be configured later in settings</string>
|
||||
<string name="sync_auth">Login to sync account</string>
|
||||
<string name="restore">Restore</string>
|
||||
<string name="backup_date_">Backup date: %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -120,13 +120,11 @@
|
||||
<item name="trackCornerRadius">@dimen/mtrl_progress_indicator_full_rounded_corner_radius</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Kotatsu.Chip" parent="Widget.Material3.Chip.Suggestion">
|
||||
<style name="Widget.Kotatsu.Chip" parent="Widget.Material3.Chip.Suggestion" />
|
||||
|
||||
</style>
|
||||
<style name="Widget.Kotatsu.Chip.Filter" parent="Widget.Material3.Chip.Filter" />
|
||||
|
||||
<style name="Widget.Kotatsu.Chip.Filter" parent="Widget.Material3.Chip.Filter">
|
||||
|
||||
</style>
|
||||
<style name="Widget.Kotatsu.Chip.Assist" parent="Widget.Material3.Chip.Assist" />
|
||||
|
||||
<style name="Widget.Kotatsu.Button.More" parent="Widget.Material3.Button.TextButton">
|
||||
<item name="android:minWidth">48dp</item>
|
||||
|
||||
@@ -72,7 +72,6 @@
|
||||
<item name="actionModeCloseDrawable">@drawable/abc_ic_clear_material</item>
|
||||
<item name="actionModeWebSearchDrawable">@drawable/abc_ic_search_api_material</item>
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Kotatsu</item>
|
||||
<item name="chipStyle">@style/Widget.Kotatsu.Chip</item>
|
||||
<item name="textInputStyle">@style/Widget.Material3.TextInputLayout.OutlinedBox</item>
|
||||
<item name="toolbarStyle">@style/Widget.Material3.Toolbar</item>
|
||||
<item name="appBarLayoutStyle">@style/Widget.Kotatsu.AppBarLayout</item>
|
||||
|
||||
Reference in New Issue
Block a user