From bb6f7b1e9f099235b3e3fca09d461838c09bdad5 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 9 Nov 2024 09:57:15 +0200 Subject: [PATCH 1/3] Fix external backup crashes --- .../core/backup/ExternalBackupStorage.kt | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/ExternalBackupStorage.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/ExternalBackupStorage.kt index 9cf5b4cae..ccef8b64d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/ExternalBackupStorage.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/ExternalBackupStorage.kt @@ -2,16 +2,18 @@ package org.koitharu.kotatsu.core.backup import android.content.Context import android.net.Uri +import androidx.annotation.CheckResult import androidx.documentfile.provider.DocumentFile import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible -import okio.IOException import okio.buffer import okio.sink import okio.source import org.jetbrains.annotations.Blocking import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import java.io.File import javax.inject.Inject @@ -21,7 +23,7 @@ class ExternalBackupStorage @Inject constructor( ) { suspend fun list(): List = runInterruptible(Dispatchers.IO) { - getRoot().listFiles().mapNotNull { + getRootOrThrow().listFiles().mapNotNull { if (it.isFile && it.canRead()) { BackupFile( uri = it.uri, @@ -35,8 +37,14 @@ class ExternalBackupStorage @Inject constructor( }.sortedDescending() } + suspend fun listOrNull() = runCatchingCancellable { + list() + }.onFailure { e -> + e.printStackTraceDebug() + }.getOrNull() + suspend fun put(file: File): Uri = runInterruptible(Dispatchers.IO) { - val out = checkNotNull(getRoot().createFile("application/zip", file.nameWithoutExtension)) { + val out = checkNotNull(getRootOrThrow().createFile("application/zip", file.nameWithoutExtension)) { "Cannot create target backup file" } checkNotNull(context.contentResolver.openOutputStream(out.uri, "wt")).sink().use { sink -> @@ -47,25 +55,30 @@ class ExternalBackupStorage @Inject constructor( out.uri } + @CheckResult suspend fun delete(victim: BackupFile) = runInterruptible(Dispatchers.IO) { - val df = checkNotNull(DocumentFile.fromSingleUri(context, victim.uri)) { - "${victim.uri} cannot be resolved to the DocumentFile" - } - if (!df.delete()) { - throw IOException("Cannot delete ${df.uri}") - } + val df = DocumentFile.fromSingleUri(context, victim.uri) + df != null && df.delete() } - suspend fun getLastBackupDate() = list().maxByOrNull { it.dateTime }?.dateTime + suspend fun getLastBackupDate() = listOrNull()?.maxOfOrNull { it.dateTime } - suspend fun trim(maxCount: Int) { - list().drop(maxCount).forEach { - delete(it) + suspend fun trim(maxCount: Int): Boolean { + val list = listOrNull() + if (list == null || list.size <= maxCount) { + return false } + var result = false + for (i in maxCount until list.size) { + if (delete(list[i])) { + result = true + } + } + return result } @Blocking - private fun getRoot(): DocumentFile { + private fun getRootOrThrow(): DocumentFile { val uri = checkNotNull(settings.periodicalBackupDirectory) { "Backup directory is not specified" } From 635839065dfdb27ac27d2ae4dd1cd13079b1cc46 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 9 Nov 2024 11:44:51 +0200 Subject: [PATCH 2/3] Batch pages saving --- .../org/koitharu/kotatsu/core/model/Manga.kt | 4 + .../details/ui/pager/pages/PagesFragment.kt | 74 ++++++++++++++++++- .../ui/pager/pages/PagesSavedObserver.kt | 28 +++++++ .../pager/pages/PagesSelectionDecoration.kt | 16 ++++ .../details/ui/pager/pages/PagesViewModel.kt | 27 +++++++ .../kotatsu/list/ui/MangaListFragment.kt | 4 +- .../kotatsu/reader/ui/PageSaveHelper.kt | 24 +++--- .../kotatsu/reader/ui/ReaderActivity.kt | 14 +--- .../kotatsu/reader/ui/ReaderViewModel.kt | 7 +- app/src/main/res/menu/mode_pages.xml | 12 +++ app/src/main/res/values/strings.xml | 1 + 11 files changed, 183 insertions(+), 28 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesSavedObserver.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesSelectionDecoration.kt create mode 100644 app/src/main/res/menu/mode_pages.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index 74bcb3a30..217d9bad0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -88,6 +88,10 @@ fun Manga.findChapter(id: Long): MangaChapter? { return chapters?.findById(id) } +fun Manga.requireChapter(id: Long): MangaChapter = checkNotNull(findChapter(id)) { + "Chapter $id not found" +} + fun Manga.getPreferredBranch(history: MangaHistory?): String? { val ch = chapters if (ch.isNullOrEmpty()) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesFragment.kt index 0edc18907..eadeec114 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesFragment.kt @@ -2,8 +2,13 @@ package org.koitharu.kotatsu.details.ui.pager.pages import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.appcompat.view.ActionMode +import androidx.collection.ArraySet import androidx.core.graphics.Insets import androidx.core.view.isInvisible import androidx.core.view.isVisible @@ -20,10 +25,12 @@ import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener +import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback import org.koitharu.kotatsu.core.util.ext.dismissParentDialog +import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate import org.koitharu.kotatsu.core.util.ext.findParentCallback import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent @@ -34,16 +41,18 @@ import org.koitharu.kotatsu.list.ui.GridSpanResolver import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.reader.ui.PageSaveHelper import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback import org.koitharu.kotatsu.reader.ui.ReaderState +import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import javax.inject.Inject import kotlin.math.roundToInt @AndroidEntryPoint class PagesFragment : BaseFragment(), - OnListItemClickListener { + OnListItemClickListener, ListSelectionController.Callback { @Inject lateinit var coil: ImageLoader @@ -51,17 +60,23 @@ class PagesFragment : @Inject lateinit var settings: AppSettings + @Inject + lateinit var pageSaveHelperFactory: PageSaveHelper.Factory + private val parentViewModel by ChaptersPagesViewModel.ActivityVMLazy(this) private val viewModel by viewModels() + private lateinit var pageSaveHelper: PageSaveHelper private var thumbnailsAdapter: PageThumbnailAdapter? = null private var spanResolver: GridSpanResolver? = null private var scrollListener: ScrollListener? = null + private var selectionController: ListSelectionController? = null private val spanSizeLookup = SpanSizeLookup() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + pageSaveHelper = pageSaveHelperFactory.create(this) combine( parentViewModel.mangaDetails, parentViewModel.readingState, @@ -83,6 +98,12 @@ class PagesFragment : override fun onViewBindingCreated(binding: FragmentPagesBinding, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) spanResolver = GridSpanResolver(binding.root.resources) + selectionController = ListSelectionController( + appCompatDelegate = checkNotNull(findAppCompatDelegate()), + decoration = PagesSelectionDecoration(binding.root.context), + registryOwner = this, + callback = this, + ) thumbnailsAdapter = PageThumbnailAdapter( coil = coil, lifecycleOwner = viewLifecycleOwner, @@ -91,6 +112,7 @@ class PagesFragment : viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) // before rv initialization with(binding.recyclerView) { addItemDecoration(TypedListSpacingDecoration(context, false)) + checkNotNull(selectionController).attachToRecyclerView(this) adapter = thumbnailsAdapter setHasFixedSize(true) PagerNestedScrollHelper(this).bind(viewLifecycleOwner) @@ -103,6 +125,7 @@ class PagesFragment : } parentViewModel.isChaptersEmpty.observe(viewLifecycleOwner, ::onNoChaptersChanged) viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged) + viewModel.onPageSaved.observeEvent(this, PagesSavedObserver(binding.recyclerView)) viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) viewModel.isLoading.observe(viewLifecycleOwner) { binding.progressBar.showOrHide(it) } viewModel.isLoadingUp.observe(viewLifecycleOwner) { binding.progressBarTop.showOrHide(it) } @@ -113,6 +136,7 @@ class PagesFragment : spanResolver = null scrollListener = null thumbnailsAdapter = null + selectionController = null spanSizeLookup.invalidateCache() super.onDestroyView() } @@ -120,6 +144,9 @@ class PagesFragment : override fun onWindowInsetsChanged(insets: Insets) = Unit override fun onItemClick(item: PageThumbnail, view: View) { + if (selectionController?.onItemClick(item.page.id) == true) { + return + } val listener = findParentCallback(ReaderNavigationCallback::class.java) if (listener != null && listener.onPageSelected(item.page)) { dismissParentDialog() @@ -133,6 +160,39 @@ class PagesFragment : } } + override fun onItemLongClick(item: PageThumbnail, view: View): Boolean { + return selectionController?.onItemLongClick(view, item.page.id) ?: false + } + + override fun onItemContextClick(item: PageThumbnail, view: View): Boolean { + return selectionController?.onItemContextClick(view, item.page.id) ?: false + } + + override fun onSelectionChanged(controller: ListSelectionController, count: Int) { + viewBinding?.recyclerView?.invalidateItemDecorations() + } + + override fun onCreateActionMode( + controller: ListSelectionController, + menuInflater: MenuInflater, + menu: Menu, + ): Boolean { + menuInflater.inflate(R.menu.mode_pages, menu) + return true + } + + override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode?, item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_save -> { + viewModel.savePages(pageSaveHelper, collectSelectedPages()) + mode?.finish() + true + } + + else -> false + } + } + private suspend fun onThumbnailsChanged(list: List) { val adapter = thumbnailsAdapter ?: return if (adapter.itemCount == 0) { @@ -172,6 +232,18 @@ class PagesFragment : } } + private fun collectSelectedPages(): Set { + val checkedIds = selectionController?.peekCheckedIds() ?: return emptySet() + val items = thumbnailsAdapter?.items ?: return emptySet() + val result = ArraySet(checkedIds.size) + for (item in items) { + if (item is PageThumbnail && item.page.id in checkedIds) { + result.add(item.page) + } + } + return result + } + private inner class ScrollListener : BoundsScrollListener(3, 3) { override fun onScrolledToStart(recyclerView: RecyclerView) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesSavedObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesSavedObserver.kt new file mode 100644 index 000000000..71ebf88ee --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesSavedObserver.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.details.ui.pager.pages + +import android.net.Uri +import android.view.View +import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.flow.FlowCollector +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ShareHelper + +class PagesSavedObserver( + private val snackbarHost: View, +) : FlowCollector> { + + override suspend fun emit(value: Collection) { + val msg = when (value.size) { + 0 -> R.string.nothing_found + 1 -> R.string.page_saved + else -> R.string.pages_saved + } + val snackbar = Snackbar.make(snackbarHost, msg, Snackbar.LENGTH_LONG) + value.singleOrNull()?.let { uri -> + snackbar.setAction(R.string.share) { + ShareHelper(snackbarHost.context).shareImage(uri) + } + } + snackbar.show() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesSelectionDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesSelectionDecoration.kt new file mode 100644 index 000000000..1dc95fecb --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesSelectionDecoration.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.details.ui.pager.pages + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.core.util.ext.getItem +import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration + +class PagesSelectionDecoration(context: Context) : MangaSelectionDecoration(context) { + + override fun getItemId(parent: RecyclerView, child: View): Long { + val holder = parent.getChildViewHolder(child) ?: return RecyclerView.NO_ID + val item = holder.getItem(PageThumbnail::class.java) ?: return RecyclerView.NO_ID + return item.page.id + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt index ca0aba9ca..9b2de230b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.details.ui.pager.pages +import android.net.Uri import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -7,15 +8,21 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.plus +import org.koitharu.kotatsu.core.model.requireChapter import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.observeAsStateFlow 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.firstNotNull +import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.reader.domain.ChaptersLoader +import org.koitharu.kotatsu.reader.ui.PageSaveHelper import org.koitharu.kotatsu.reader.ui.ReaderState +import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import javax.inject.Inject @HiltViewModel @@ -32,6 +39,7 @@ class PagesViewModel @Inject constructor( val thumbnails = MutableStateFlow>(emptyList()) val isLoadingUp = MutableStateFlow(false) val isLoadingDown = MutableStateFlow(false) + val onPageSaved = MutableEventFlow>() val gridScale = settings.observeAsStateFlow( scope = viewModelScope + Dispatchers.Default, @@ -73,6 +81,25 @@ class PagesViewModel @Inject constructor( loadingNextJob = loadPrevNextChapter(isNext = true) } + fun savePages( + pageSaveHelper: PageSaveHelper, + pages: Set, + ) { + launchLoadingJob(Dispatchers.Default) { + val manga = state.requireValue().details.toManga() + val tasks = pages.map { + PageSaveHelper.Task( + manga = manga, + chapter = manga.requireChapter(it.chapterId), + pageNumber = it.index + 1, + page = it.toMangaPage(), + ) + } + val dest = pageSaveHelper.save(tasks) + onPageSaved.call(dest) + } + } + private suspend fun doInit(state: State) { chaptersLoader.init(state.details) val initialChapterId = state.readerState?.chapterId?.takeIf { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 0cf0a0bc9..2c1cd08b2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -115,9 +115,9 @@ abstract class MangaListFragment : with(binding.recyclerView) { setHasFixedSize(true) adapter = listAdapter - checkNotNull(selectionController).attachToRecyclerView(binding.recyclerView) + checkNotNull(selectionController).attachToRecyclerView(this) addItemDecoration(TypedListSpacingDecoration(context, false)) - addOnScrollListener(paginationListener!!) + addOnScrollListener(checkNotNull(paginationListener)) fastScroller.setFastScrollListener(this@MangaListFragment) } with(binding.swipeRefreshLayout) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt index 059f4a824..d44c258ca 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt @@ -67,17 +67,14 @@ class PageSaveHelper @AssistedInject constructor( } } - suspend fun save(tasks: Set): Uri? = when (tasks.size) { - 0 -> null - 1 -> saveImpl(tasks.first()) - else -> { - saveImpl(tasks) - null - } + suspend fun save(tasks: Collection): Collection = when (tasks.size) { + 0 -> emptySet() + 1 -> setOf(saveImpl(tasks.first())) + else -> saveImpl(tasks) } private suspend fun saveImpl(task: Task): Uri { - val pageLoader = pageLoaderProvider.get() + val pageLoader = getPageLoader() val pageUrl = pageLoader.getPageUrl(task.page).toUri() val pageUri = pageLoader.loadPage(task.page, force = false) val proposedName = task.getFileBaseName() + "." + getPageExtension(pageUrl, pageUri) @@ -89,13 +86,14 @@ class PageSaveHelper @AssistedInject constructor( return destination } - private suspend fun saveImpl(tasks: Collection) { - val pageLoader = pageLoaderProvider.get() + private suspend fun saveImpl(tasks: Collection): Collection { + val pageLoader = getPageLoader() val destinationDir = getDefaultFileUri(null) ?: run { val defaultUri = settings.getPagesSaveDir(context)?.uri DocumentFile.fromTreeUri(context, pickDirectoryRequest.launchAndAwait(defaultUri)) } ?: throw IOException("Cannot get destination directory") + val result = ArrayList(tasks.size) for (task in tasks) { val pageUrl = pageLoader.getPageUrl(task.page).toUri() val pageUri = pageLoader.loadPage(task.page, force = false) @@ -106,7 +104,9 @@ class PageSaveHelper @AssistedInject constructor( } val destination = destinationDir.createFile(mime, proposedName.substringBeforeLast('.')) copyImpl(pageUri, destination?.uri ?: throw IOException("Cannot create destination file")) + result.add(destination.uri) } + return result } private suspend fun getPageExtension(url: Uri, fileUri: Uri): String { @@ -143,6 +143,10 @@ class PageSaveHelper @AssistedInject constructor( } } + private suspend fun getPageLoader() = withContext(Dispatchers.Main.immediate) { + pageLoaderProvider.get() + } + private fun getDefaultFileUri(proposedName: String?): DocumentFile? { if (settings.isPagesSavingAskEnabled) { return null diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index e59e4d019..4adbad36a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -53,6 +53,7 @@ import org.koitharu.kotatsu.core.util.ext.setValueRounded import org.koitharu.kotatsu.core.util.ext.zipWithPrevious import org.koitharu.kotatsu.databinding.ActivityReaderBinding import org.koitharu.kotatsu.details.ui.DetailsActivity +import org.koitharu.kotatsu.details.ui.pager.pages.PagesSavedObserver import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.reader.data.TapGridSettings @@ -143,7 +144,7 @@ class ReaderActivity : ), ) viewModel.readerMode.observe(this, Lifecycle.State.STARTED, this::onInitReader) - viewModel.onPageSaved.observeEvent(this, this::onPageSaved) + viewModel.onPageSaved.observeEvent(this, PagesSavedObserver(viewBinding.container)) viewModel.uiState.zipWithPrevious().observe(this, this::onUiStateChanged) viewModel.isLoading.observe(this, this::onLoadingStateChanged) viewModel.content.observe(this) { @@ -289,17 +290,6 @@ class ReaderActivity : readerManager.setDoubleReaderMode(isEnabled) } - private fun onPageSaved(uri: Uri?) { - val snackbar = Snackbar.make(viewBinding.container, R.string.page_saved, Snackbar.LENGTH_LONG) - if (uri != null) { - snackbar.setAction(R.string.share) { - ShareHelper(this).shareImage(uri) - } - } - snackbar.setAnchorView(viewBinding.appbarBottom) - snackbar.show() - } - private fun setKeepScreenOn(isKeep: Boolean) { if (isKeep) { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index c1a5c70f3..e4a41f77d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -32,6 +32,7 @@ import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.core.model.findChapter import org.koitharu.kotatsu.core.model.getPreferredBranch +import org.koitharu.kotatsu.core.model.requireChapter import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaIntent @@ -111,7 +112,7 @@ class ReaderViewModel @Inject constructor( } val readerMode = MutableStateFlow(null) - val onPageSaved = MutableEventFlow() + val onPageSaved = MutableEventFlow>() val onShowToast = MutableEventFlow() val uiState = MutableStateFlow(null) @@ -261,8 +262,8 @@ class ReaderViewModel @Inject constructor( val currentManga = manga.requireValue() val task = PageSaveHelper.Task( manga = currentManga, - chapter = checkNotNull(currentManga.findChapter(state.chapterId)), - pageNumber = state.page, + chapter = currentManga.requireChapter(state.chapterId), + pageNumber = state.page + 1, page = checkNotNull(getCurrentPage()) { "Cannot find current page" }, ) val dest = pageSaveHelper.save(setOf(task)) diff --git a/app/src/main/res/menu/mode_pages.xml b/app/src/main/res/menu/mode_pages.xml new file mode 100644 index 000000000..3263d5f88 --- /dev/null +++ b/app/src/main/res/menu/mode_pages.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e40a23fa8..6eb2d8e4a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,6 +57,7 @@ \"%s\" deleted from local storage Save page Page saved + Pages saved Share image Import Delete From a1e5d78877844c8ba412cef523f1c98708622f04 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 10 Nov 2024 10:56:35 +0200 Subject: [PATCH 3/3] Update parsers --- .../org/koitharu/kotatsu/core/model/Manga.kt | 18 +----------------- .../core/parser/ParserMangaRepository.kt | 5 +++-- .../parser/external/ExternalMangaRepository.kt | 4 ++-- .../org/koitharu/kotatsu/core/util/ext/Flow.kt | 5 +++-- .../details/domain/ProgressUpdateUseCase.kt | 5 ++--- .../details/domain/ReadingTimeUseCase.kt | 2 +- .../details/service/MangaPrefetchService.kt | 2 +- .../kotatsu/details/ui/DetailsViewModel.kt | 2 +- .../details/ui/adapter/ChapterGridItemAD.kt | 3 +-- .../details/ui/adapter/ChaptersAdapter.kt | 3 +-- .../details/ui/model/ChapterListItem.kt | 3 +-- .../details/ui/pager/pages/PagesViewModel.kt | 3 +-- .../ui/dialog/DownloadDialogViewModel.kt | 4 ++-- .../download/ui/list/DownloadsViewModel.kt | 3 +-- .../kotatsu/filter/ui/FilterCoordinator.kt | 4 ++-- .../kotatsu/history/data/HistoryRepository.kt | 2 +- .../list/domain/MangaListQuickFilter.kt | 7 ++++--- .../koitharu/kotatsu/local/data/PagesCache.kt | 6 +++--- .../local/domain/DeleteReadChaptersUseCase.kt | 6 +++--- .../main/domain/CoverRestoreInterceptor.kt | 2 +- .../reader/domain/DetectReaderModeUseCase.kt | 3 +-- .../kotatsu/reader/ui/ReaderViewModel.kt | 6 ++---- .../reader/ui/config/ImageServerDelegate.kt | 15 ++++++++------- .../scrobbling/common/domain/Scrobbler.kt | 2 +- .../settings/backup/RestoreViewModel.kt | 4 ++-- gradle/libs.versions.toml | 4 ++-- 26 files changed, 51 insertions(+), 72 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index 217d9bad0..a077ee19a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -17,7 +17,7 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaState -import org.koitharu.kotatsu.parsers.util.formatSimple +import org.koitharu.kotatsu.parsers.util.findById import org.koitharu.kotatsu.parsers.util.mapToSet import com.google.android.material.R as materialR @@ -29,8 +29,6 @@ fun Collection.distinctById() = distinctBy { it.id } @JvmName("chaptersIds") fun Collection.ids() = mapToSet { it.id } -fun Collection.findById(id: Long) = find { x -> x.id == id } - fun Collection.countChaptersByBranch(): Int { if (size <= 1) { return size @@ -84,14 +82,6 @@ val Demographic.titleResId: Int Demographic.NONE -> R.string.none } -fun Manga.findChapter(id: Long): MangaChapter? { - return chapters?.findById(id) -} - -fun Manga.requireChapter(id: Long): MangaChapter = checkNotNull(findChapter(id)) { - "Chapter $id not found" -} - fun Manga.getPreferredBranch(history: MangaHistory?): String? { val ch = chapters if (ch.isNullOrEmpty()) { @@ -140,12 +130,6 @@ val Manga.appUrl: Uri .appendQueryParameter("url", url) .build() -fun MangaChapter.formatNumber(): String? = if (number > 0f) { - number.formatSimple() -} else { - null -} - fun Manga.chaptersCount(): Int { if (chapters.isNullOrEmpty()) { return 0 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/ParserMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/ParserMangaRepository.kt index 12f4cfcee..1fd673365 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/ParserMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/ParserMangaRepository.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.core.parser +import kotlinx.coroutines.Dispatchers import okhttp3.Interceptor import okhttp3.Response import org.koitharu.kotatsu.core.cache.MemoryContentCache @@ -17,9 +18,9 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.domain import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy class ParserMangaRepository( private val parser: MangaParser, @@ -27,7 +28,7 @@ class ParserMangaRepository( cache: MemoryContentCache, ) : CachingMangaRepository(cache), Interceptor { - private val filterOptionsLazy = SuspendLazy { + private val filterOptionsLazy = suspendLazy(Dispatchers.Default) { mirrorSwitchInterceptor.withMirrorSwitching { parser.getFilterOptions() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt index dece02536..999725e70 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt @@ -13,7 +13,7 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.parsers.util.SuspendLazy +import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import java.util.EnumSet class ExternalMangaRepository( @@ -32,7 +32,7 @@ class ExternalMangaRepository( }.getOrNull() } - private val filterOptions = SuspendLazy(contentSource::getListFilterOptions) + private val filterOptions = suspendLazy(initializer = contentSource::getListFilterOptions) override val sortOrders: Set get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.POPULARITY) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt index b5d922e70..cd4a04054 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt @@ -17,7 +17,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.transformWhile -import org.koitharu.kotatsu.parsers.util.SuspendLazy +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.suspendlazy.SuspendLazy import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -133,4 +134,4 @@ suspend fun Flow.firstNotNullOrNull(): T? = firstOrNull { x -> x ! fun Flow>.flattenLatest() = flatMapLatest { it } -fun SuspendLazy.asFlow() = flow { emit(tryGet()) } +fun SuspendLazy.asFlow() = flow { emit(runCatchingCancellable { get() }) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ProgressUpdateUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ProgressUpdateUseCase.kt index 32900a9fb..47d56695e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ProgressUpdateUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ProgressUpdateUseCase.kt @@ -1,7 +1,6 @@ package org.koitharu.kotatsu.details.domain import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.core.model.findChapter import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.parser.MangaRepository @@ -33,8 +32,8 @@ class ProgressUpdateUseCase @Inject constructor( } else { seed } - val chapter = details.findChapter(history.chapterId) ?: return PROGRESS_NONE - val chapters = details.getChapters(chapter.branch) ?: return PROGRESS_NONE + val chapter = details.findChapterById(history.chapterId) ?: return PROGRESS_NONE + val chapters = details.getChapters(chapter.branch) val chaptersCount = chapters.size if (chaptersCount == 0) { return PROGRESS_NONE diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ReadingTimeUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ReadingTimeUseCase.kt index 1e7b3ef0e..893d70677 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ReadingTimeUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/ReadingTimeUseCase.kt @@ -1,10 +1,10 @@ package org.koitharu.kotatsu.details.domain import org.koitharu.kotatsu.core.model.MangaHistory -import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.ReadingTime +import org.koitharu.kotatsu.parsers.util.findById import org.koitharu.kotatsu.stats.data.StatsRepository import java.util.concurrent.TimeUnit import javax.inject.Inject diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt index bf0d17f9e..2e37c1257 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt @@ -6,7 +6,6 @@ import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.EntryPointAccessors import org.koitharu.kotatsu.core.cache.MemoryContentCache import org.koitharu.kotatsu.core.model.LocalMangaSource -import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.parcelable.ParcelableChapter import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga @@ -19,6 +18,7 @@ import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.findById import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import javax.inject.Inject diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index ca798edbe..772647591 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus import org.koitharu.kotatsu.R import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository -import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.prefs.AppSettings @@ -47,6 +46,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.findById import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterGridItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterGridItemAD.kt index 5cb2d5794..92e13f385 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterGridItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterGridItemAD.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.details.ui.adapter import android.graphics.Typeface import androidx.core.view.isVisible import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import org.koitharu.kotatsu.core.model.formatNumber import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList @@ -22,7 +21,7 @@ fun chapterGridItemAD( bind { payloads -> if (payloads.isEmpty()) { - binding.textViewTitle.text = item.chapter.formatNumber() ?: "?" + binding.textViewTitle.text = item.chapter.numberString() ?: "?" } binding.imageViewNew.isVisible = item.isNew binding.imageViewCurrent.isVisible = item.isCurrent diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt index ae83503c1..0f74f83aa 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt @@ -1,7 +1,6 @@ package org.koitharu.kotatsu.details.ui.adapter import android.content.Context -import org.koitharu.kotatsu.core.model.formatNumber import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller @@ -33,7 +32,7 @@ class ChaptersAdapter( findHeader(position)?.getText(context) } else { val chapter = (items.getOrNull(position) as? ChapterListItem)?.chapter ?: return null - if (chapter.number > 0) chapter.formatNumber() else null + chapter.numberString() } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt index ec3a1c547..f0085001b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.details.ui.model import android.text.format.DateUtils import org.jsoup.internal.StringUtil.StringJoiner -import org.koitharu.kotatsu.core.model.formatNumber import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.MangaChapter import kotlin.experimental.and @@ -53,7 +52,7 @@ data class ChapterListItem( private fun buildDescription(): String { val joiner = StringJoiner(" • ") - chapter.formatNumber()?.let { + chapter.numberString()?.let { joiner.add("#").append(it) } uploadDate?.let { date -> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt index 9b2de230b..9840fc57f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/PagesViewModel.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.plus -import org.koitharu.kotatsu.core.model.requireChapter import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.ui.BaseViewModel @@ -90,7 +89,7 @@ class PagesViewModel @Inject constructor( val tasks = pages.map { PageSaveHelper.Task( manga = manga, - chapter = manga.requireChapter(it.chapterId), + chapter = manga.requireChapterById(it.chapterId), pageNumber = it.index + 1, page = it.toMangaPage(), ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt index a385a0515..efc8a4ccd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt @@ -29,9 +29,9 @@ import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import org.koitharu.kotatsu.settings.storage.DirectoryModel import javax.inject.Inject @@ -50,7 +50,7 @@ class DownloadDialogViewModel @Inject constructor( val manga = savedStateHandle.require>(DownloadDialogFragment.ARG_MANGA).map { it.manga } - private val mangaDetails = SuspendLazy { + private val mangaDetails = suspendLazy { coroutineScope { manga.map { m -> async { m.getDetails() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt index 33db4f1c7..44795ee5b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.plus import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.model.formatNumber import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.ui.BaseViewModel @@ -308,7 +307,7 @@ class DownloadsViewModel @Inject constructor( return chapters.mapNotNullTo(ArrayList(size)) { if (chapterIds == null || it.id in chapterIds) { DownloadChapter( - number = it.formatNumber(), + number = it.numberString(), name = it.name, isDownloaded = it.id in localChapters, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index ec95fea68..d01f44e23 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -35,8 +35,8 @@ import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.YEAR_MIN -import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.ifZero +import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.search.domain.MangaSearchRepository import java.util.Calendar @@ -59,7 +59,7 @@ class FilterCoordinator @Inject constructor( private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder) private val availableSortOrders = repository.sortOrders - private val filterOptions = SuspendLazy { repository.getFilterOptions() } + private val filterOptions = suspendLazy { repository.getFilterOptions() } val capabilities = repository.filterCapabilities val mangaSource: MangaSource diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt index e0879081b..9176e66c5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt @@ -14,7 +14,6 @@ import org.koitharu.kotatsu.core.db.entity.toMangaList import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.db.entity.toMangaTagsList import org.koitharu.kotatsu.core.model.MangaHistory -import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.model.toMangaSources @@ -30,6 +29,7 @@ import org.koitharu.kotatsu.list.domain.ReadingProgress import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.util.findById import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt index 8568c4c20..bb1631667 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt @@ -6,14 +6,15 @@ import kotlinx.coroutines.flow.asStateFlow import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.list.ui.model.QuickFilter -import org.koitharu.kotatsu.parsers.util.SuspendLazy +import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull +import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy abstract class MangaListQuickFilter( private val settings: AppSettings, ) : QuickFilterListener { private val appliedFilter = MutableStateFlow>(emptySet()) - private val availableFilterOptions = SuspendLazy { + private val availableFilterOptions = suspendLazy { getAvailableFilterOptions() } @@ -50,7 +51,7 @@ abstract class MangaListQuickFilter( if (!settings.isQuickFilterEnabled) { return null } - val availableOptions = availableFilterOptions.tryGet().getOrNull()?.map { option -> + val availableOptions = availableFilterOptions.getOrNull()?.map { option -> ChipsView.ChipModel( title = option.titleText, titleResId = option.titleResId, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt index dfc86c51d..3b8a9dd11 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt @@ -22,8 +22,8 @@ import org.koitharu.kotatsu.core.util.ext.subdir import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.takeIfWriteable import org.koitharu.kotatsu.core.util.ext.writeAllCancellable -import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import java.io.File import java.util.UUID import javax.inject.Inject @@ -32,13 +32,13 @@ import javax.inject.Singleton @Singleton class PagesCache @Inject constructor(@ApplicationContext context: Context) { - private val cacheDir = SuspendLazy { + private val cacheDir = suspendLazy { val dirs = context.externalCacheDirs + context.cacheDir dirs.firstNotNullOf { it?.subdir(CacheDir.PAGES.dir)?.takeIfWriteable() } } - private val lruCache = SuspendLazy { + private val lruCache = suspendLazy { val dir = cacheDir.get() val availableSize = (getAvailableSize() * 0.8).toLong() val size = SIZE_DEFAULT.coerceAtMost(availableSize).coerceAtLeast(SIZE_MIN) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteReadChaptersUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteReadChaptersUseCase.kt index d19b724ba..e4b1c71c1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteReadChaptersUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteReadChaptersUseCase.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.fold import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.ids import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.parser.MangaRepository @@ -18,6 +17,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.util.findById import org.koitharu.kotatsu.parsers.util.recoverCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import javax.inject.Inject @@ -77,8 +77,8 @@ class DeleteReadChaptersUseCase @Inject constructor( return null } val branch = (chapters.findById(history.chapterId) ?: return null).branch - val filteredChapters = manga.manga.getChapters(branch)?.takeWhile { it.id != history.chapterId } - return if (filteredChapters.isNullOrEmpty()) { + val filteredChapters = manga.manga.getChapters(branch).takeWhile { it.id != history.chapterId } + return if (filteredChapters.isEmpty()) { null } else { DeletionTask( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt index 5e2a2e8cd..733105cea 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt @@ -6,7 +6,6 @@ import coil3.request.ErrorResult import coil3.request.ImageResult import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository -import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository @@ -15,6 +14,7 @@ import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.mangaKey import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.findById import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import java.util.Collections import javax.inject.Inject diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt index 2eceea6d5..da6db5fee 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt @@ -7,7 +7,6 @@ import androidx.core.net.toFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import okhttp3.OkHttpClient -import org.koitharu.kotatsu.core.model.findChapter import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.parser.MangaDataRepository @@ -40,7 +39,7 @@ class DetectReaderModeUseCase @Inject constructor( if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) { return defaultMode } - val chapter = state?.let { manga.findChapter(it.chapterId) } + val chapter = state?.let { manga.findChapterById(it.chapterId) } ?: manga.chapters?.firstOrNull() ?: error("There are no chapters in this manga") val repo = mangaRepositoryFactory.create(manga.source) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index e4a41f77d..8697dbfac 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -30,9 +30,7 @@ import kotlinx.coroutines.plus import org.koitharu.kotatsu.R import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository -import org.koitharu.kotatsu.core.model.findChapter import org.koitharu.kotatsu.core.model.getPreferredBranch -import org.koitharu.kotatsu.core.model.requireChapter import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaIntent @@ -262,7 +260,7 @@ class ReaderViewModel @Inject constructor( val currentManga = manga.requireValue() val task = PageSaveHelper.Task( manga = currentManga, - chapter = currentManga.requireChapter(state.chapterId), + chapter = currentManga.requireChapterById(state.chapterId), pageNumber = state.page + 1, page = checkNotNull(getCurrentPage()) { "Cannot find current page" }, ) @@ -498,7 +496,7 @@ class ReaderViewModel @Inject constructor( val history = historyRepository.getOne(manga) val preselectedBranch = selectedBranch.value val result = if (history != null) { - if (preselectedBranch != null && preselectedBranch != manga.findChapter(history.chapterId)?.branch) { + if (preselectedBranch != null && preselectedBranch != manga.findChapterById(history.chapterId)?.branch) { null } else { ReaderState(history) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt index b4460ab8c..7e498e8e4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt @@ -11,7 +11,8 @@ import org.koitharu.kotatsu.core.parser.ParserMangaRepository import org.koitharu.kotatsu.core.util.ext.mapToArray import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.util.SuspendLazy +import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull +import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import kotlin.coroutines.resume class ImageServerDelegate( @@ -19,30 +20,30 @@ class ImageServerDelegate( private val mangaSource: MangaSource?, ) { - private val repositoryLazy = SuspendLazy { + private val repositoryLazy = suspendLazy { mangaRepositoryFactory.create(checkNotNull(mangaSource)) as ParserMangaRepository } suspend fun isAvailable() = withContext(Dispatchers.Default) { - repositoryLazy.tryGet().map { repository -> + repositoryLazy.getOrNull()?.let { repository -> repository.getConfigKeys().any { it is ConfigKey.PreferredImageServer } - }.getOrDefault(false) + } == true } suspend fun getValue(): String? = withContext(Dispatchers.Default) { - repositoryLazy.tryGet().map { repository -> + repositoryLazy.getOrNull()?.let { repository -> val key = repository.getConfigKeys().firstNotNullOfOrNull { it as? ConfigKey.PreferredImageServer } if (key != null) { key.presetValues[repository.getConfig()[key]] } else { null } - }.getOrNull() + } } suspend fun showDialog(context: Context): Boolean { val repository = withContext(Dispatchers.Default) { - repositoryLazy.tryGet().getOrNull() + repositoryLazy.getOrNull() } ?: return false val key = repository.getConfigKeys().firstNotNullOfOrNull { it as? ConfigKey.PreferredImageServer diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt index e0bc73f37..e95e993e8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt @@ -11,12 +11,12 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.util.ext.findKeyByValue import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.sanitize import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.findById import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt index 3af75946f..6aa77ded3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt @@ -16,7 +16,7 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.toUriOrNull -import org.koitharu.kotatsu.parsers.util.SuspendLazy +import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import java.io.File import java.io.FileNotFoundException import java.util.Date @@ -31,7 +31,7 @@ class RestoreViewModel @Inject constructor( @ApplicationContext context: Context, ) : BaseViewModel() { - private val backupInput = SuspendLazy { + private val backupInput = suspendLazy { val uri = savedStateHandle.get(RestoreDialogFragment.ARG_FILE) ?.toUriOrNull() ?: throw FileNotFoundException() val contentResolver = context.contentResolver diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f7d9e9e68..4872b916a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ material = "1.12.0" moshi = "1.15.1" okhttp = "4.12.0" okio = "3.9.1" -parsers = "f610ae6412" +parsers = "8b4bac3cc2" preference = "1.2.1" recyclerview = "1.3.2" room = "2.6.1" @@ -37,7 +37,7 @@ runner = "1.6.2" rules = "1.6.1" ssiv = "d1d10a6975" swiperefreshlayout = "1.1.0" -kspPlugin = "2.0.21-1.0.26" +kspPlugin = "2.0.21-1.0.27" transition = "1.5.1" viewpager2 = "1.1.0" webkit = "1.12.1"