Merge remote-tracking branch 'origin/devel' into devel
This commit is contained in:
@@ -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<BackupFile> = 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"
|
||||
}
|
||||
|
||||
@@ -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<Manga>.distinctById() = distinctBy { it.id }
|
||||
@JvmName("chaptersIds")
|
||||
fun Collection<MangaChapter>.ids() = mapToSet { it.id }
|
||||
|
||||
fun Collection<MangaChapter>.findById(id: Long) = find { x -> x.id == id }
|
||||
|
||||
fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
|
||||
if (size <= 1) {
|
||||
return size
|
||||
@@ -84,10 +82,6 @@ val Demographic.titleResId: Int
|
||||
Demographic.NONE -> R.string.none
|
||||
}
|
||||
|
||||
fun Manga.findChapter(id: Long): MangaChapter? {
|
||||
return chapters?.findById(id)
|
||||
}
|
||||
|
||||
fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
||||
val ch = chapters
|
||||
if (ch.isNullOrEmpty()) {
|
||||
@@ -136,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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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<SortOrder>
|
||||
get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.POPULARITY)
|
||||
|
||||
@@ -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 <T : Any> Flow<T?>.firstNotNullOrNull(): T? = firstOrNull { x -> x !
|
||||
|
||||
fun <T> Flow<Flow<T>>.flattenLatest() = flatMapLatest { it }
|
||||
|
||||
fun <T> SuspendLazy<T>.asFlow() = flow { emit(tryGet()) }
|
||||
fun <T> SuspendLazy<T>.asFlow() = flow { emit(runCatchingCancellable { get() }) }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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<FragmentPagesBinding>(),
|
||||
OnListItemClickListener<PageThumbnail> {
|
||||
OnListItemClickListener<PageThumbnail>, 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<PagesViewModel>()
|
||||
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<ListModel>) {
|
||||
val adapter = thumbnailsAdapter ?: return
|
||||
if (adapter.itemCount == 0) {
|
||||
@@ -172,6 +232,18 @@ class PagesFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectSelectedPages(): Set<ReaderPage> {
|
||||
val checkedIds = selectionController?.peekCheckedIds() ?: return emptySet()
|
||||
val items = thumbnailsAdapter?.items ?: return emptySet()
|
||||
val result = ArraySet<ReaderPage>(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) {
|
||||
|
||||
@@ -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<Collection<Uri>> {
|
||||
|
||||
override suspend fun emit(value: Collection<Uri>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -10,12 +11,17 @@ import kotlinx.coroutines.plus
|
||||
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 +38,7 @@ class PagesViewModel @Inject constructor(
|
||||
val thumbnails = MutableStateFlow<List<ListModel>>(emptyList())
|
||||
val isLoadingUp = MutableStateFlow(false)
|
||||
val isLoadingDown = MutableStateFlow(false)
|
||||
val onPageSaved = MutableEventFlow<Collection<Uri>>()
|
||||
|
||||
val gridScale = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
@@ -73,6 +80,25 @@ class PagesViewModel @Inject constructor(
|
||||
loadingNextJob = loadPrevNextChapter(isNext = true)
|
||||
}
|
||||
|
||||
fun savePages(
|
||||
pageSaveHelper: PageSaveHelper,
|
||||
pages: Set<ReaderPage>,
|
||||
) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val manga = state.requireValue().details.toManga()
|
||||
val tasks = pages.map {
|
||||
PageSaveHelper.Task(
|
||||
manga = manga,
|
||||
chapter = manga.requireChapterById(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 {
|
||||
|
||||
@@ -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<Array<ParcelableManga>>(DownloadDialogFragment.ARG_MANGA).map {
|
||||
it.manga
|
||||
}
|
||||
private val mangaDetails = SuspendLazy {
|
||||
private val mangaDetails = suspendLazy {
|
||||
coroutineScope {
|
||||
manga.map { m ->
|
||||
async { m.getDetails() }
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Set<ListFilterOption>>(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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -67,17 +67,14 @@ class PageSaveHelper @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun save(tasks: Set<Task>): Uri? = when (tasks.size) {
|
||||
0 -> null
|
||||
1 -> saveImpl(tasks.first())
|
||||
else -> {
|
||||
saveImpl(tasks)
|
||||
null
|
||||
}
|
||||
suspend fun save(tasks: Collection<Task>): Collection<Uri> = 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<Task>) {
|
||||
val pageLoader = pageLoaderProvider.get()
|
||||
private suspend fun saveImpl(tasks: Collection<Task>): Collection<Uri> {
|
||||
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<Uri>(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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -30,7 +30,6 @@ 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.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
@@ -111,7 +110,7 @@ class ReaderViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
val readerMode = MutableStateFlow<ReaderMode?>(null)
|
||||
val onPageSaved = MutableEventFlow<Uri?>()
|
||||
val onPageSaved = MutableEventFlow<Collection<Uri>>()
|
||||
val onShowToast = MutableEventFlow<Int>()
|
||||
val uiState = MutableStateFlow<ReaderUiState?>(null)
|
||||
|
||||
@@ -261,8 +260,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.requireChapterById(state.chapterId),
|
||||
pageNumber = state.page + 1,
|
||||
page = checkNotNull(getCurrentPage()) { "Cannot find current page" },
|
||||
)
|
||||
val dest = pageSaveHelper.save(setOf(task))
|
||||
@@ -497,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String>(RestoreDialogFragment.ARG_FILE)
|
||||
?.toUriOrNull() ?: throw FileNotFoundException()
|
||||
val contentResolver = context.contentResolver
|
||||
|
||||
12
app/src/main/res/menu/mode_pages.xml
Normal file
12
app/src/main/res/menu/mode_pages.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
android:icon="@drawable/ic_save"
|
||||
android:title="@string/save"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
</menu>
|
||||
@@ -57,6 +57,7 @@
|
||||
<string name="_s_deleted_from_local_storage">\"%s\" deleted from local storage</string>
|
||||
<string name="save_page">Save page</string>
|
||||
<string name="page_saved">Page saved</string>
|
||||
<string name="pages_saved">Pages saved</string>
|
||||
<string name="share_image">Share image</string>
|
||||
<string name="_import">Import</string>
|
||||
<string name="delete">Delete</string>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user