Merge remote-tracking branch 'origin/devel' into devel

This commit is contained in:
Mac135135
2024-11-10 14:19:06 +03:00
35 changed files with 253 additions and 106 deletions

View File

@@ -2,16 +2,18 @@ package org.koitharu.kotatsu.core.backup
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.annotation.CheckResult
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okio.IOException
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import okio.source import okio.source
import org.jetbrains.annotations.Blocking import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.core.prefs.AppSettings 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 java.io.File
import javax.inject.Inject import javax.inject.Inject
@@ -21,7 +23,7 @@ class ExternalBackupStorage @Inject constructor(
) { ) {
suspend fun list(): List<BackupFile> = runInterruptible(Dispatchers.IO) { suspend fun list(): List<BackupFile> = runInterruptible(Dispatchers.IO) {
getRoot().listFiles().mapNotNull { getRootOrThrow().listFiles().mapNotNull {
if (it.isFile && it.canRead()) { if (it.isFile && it.canRead()) {
BackupFile( BackupFile(
uri = it.uri, uri = it.uri,
@@ -35,8 +37,14 @@ class ExternalBackupStorage @Inject constructor(
}.sortedDescending() }.sortedDescending()
} }
suspend fun listOrNull() = runCatchingCancellable {
list()
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrNull()
suspend fun put(file: File): Uri = runInterruptible(Dispatchers.IO) { 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" "Cannot create target backup file"
} }
checkNotNull(context.contentResolver.openOutputStream(out.uri, "wt")).sink().use { sink -> checkNotNull(context.contentResolver.openOutputStream(out.uri, "wt")).sink().use { sink ->
@@ -47,25 +55,30 @@ class ExternalBackupStorage @Inject constructor(
out.uri out.uri
} }
@CheckResult
suspend fun delete(victim: BackupFile) = runInterruptible(Dispatchers.IO) { suspend fun delete(victim: BackupFile) = runInterruptible(Dispatchers.IO) {
val df = checkNotNull(DocumentFile.fromSingleUri(context, victim.uri)) { val df = DocumentFile.fromSingleUri(context, victim.uri)
"${victim.uri} cannot be resolved to the DocumentFile" df != null && df.delete()
}
if (!df.delete()) {
throw IOException("Cannot delete ${df.uri}")
}
} }
suspend fun getLastBackupDate() = list().maxByOrNull { it.dateTime }?.dateTime suspend fun getLastBackupDate() = listOrNull()?.maxOfOrNull { it.dateTime }
suspend fun trim(maxCount: Int) { suspend fun trim(maxCount: Int): Boolean {
list().drop(maxCount).forEach { val list = listOrNull()
delete(it) 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 @Blocking
private fun getRoot(): DocumentFile { private fun getRootOrThrow(): DocumentFile {
val uri = checkNotNull(settings.periodicalBackupDirectory) { val uri = checkNotNull(settings.periodicalBackupDirectory) {
"Backup directory is not specified" "Backup directory is not specified"
} }

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaState 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 org.koitharu.kotatsu.parsers.util.mapToSet
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -29,8 +29,6 @@ fun Collection<Manga>.distinctById() = distinctBy { it.id }
@JvmName("chaptersIds") @JvmName("chaptersIds")
fun Collection<MangaChapter>.ids() = mapToSet { it.id } fun Collection<MangaChapter>.ids() = mapToSet { it.id }
fun Collection<MangaChapter>.findById(id: Long) = find { x -> x.id == id }
fun Collection<ChapterListItem>.countChaptersByBranch(): Int { fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
if (size <= 1) { if (size <= 1) {
return size return size
@@ -84,10 +82,6 @@ val Demographic.titleResId: Int
Demographic.NONE -> R.string.none Demographic.NONE -> R.string.none
} }
fun Manga.findChapter(id: Long): MangaChapter? {
return chapters?.findById(id)
}
fun Manga.getPreferredBranch(history: MangaHistory?): String? { fun Manga.getPreferredBranch(history: MangaHistory?): String? {
val ch = chapters val ch = chapters
if (ch.isNullOrEmpty()) { if (ch.isNullOrEmpty()) {
@@ -136,12 +130,6 @@ val Manga.appUrl: Uri
.appendQueryParameter("url", url) .appendQueryParameter("url", url)
.build() .build()
fun MangaChapter.formatNumber(): String? = if (number > 0f) {
number.formatSimple()
} else {
null
}
fun Manga.chaptersCount(): Int { fun Manga.chaptersCount(): Int {
if (chapters.isNullOrEmpty()) { if (chapters.isNullOrEmpty()) {
return 0 return 0

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.parser package org.koitharu.kotatsu.core.parser
import kotlinx.coroutines.Dispatchers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import org.koitharu.kotatsu.core.cache.MemoryContentCache 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.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.SortOrder 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.domain
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
class ParserMangaRepository( class ParserMangaRepository(
private val parser: MangaParser, private val parser: MangaParser,
@@ -27,7 +28,7 @@ class ParserMangaRepository(
cache: MemoryContentCache, cache: MemoryContentCache,
) : CachingMangaRepository(cache), Interceptor { ) : CachingMangaRepository(cache), Interceptor {
private val filterOptionsLazy = SuspendLazy { private val filterOptionsLazy = suspendLazy(Dispatchers.Default) {
mirrorSwitchInterceptor.withMirrorSwitching { mirrorSwitchInterceptor.withMirrorSwitching {
parser.getFilterOptions() parser.getFilterOptions()
} }

View File

@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.SortOrder 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 import java.util.EnumSet
class ExternalMangaRepository( class ExternalMangaRepository(
@@ -32,7 +32,7 @@ class ExternalMangaRepository(
}.getOrNull() }.getOrNull()
} }
private val filterOptions = SuspendLazy(contentSource::getListFilterOptions) private val filterOptions = suspendLazy(initializer = contentSource::getListFilterOptions)
override val sortOrders: Set<SortOrder> override val sortOrders: Set<SortOrder>
get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.POPULARITY) get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.POPULARITY)

View File

@@ -17,7 +17,8 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.transformWhile 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.TimeUnit
import java.util.concurrent.atomic.AtomicInteger 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> Flow<Flow<T>>.flattenLatest() = flatMapLatest { it }
fun <T> SuspendLazy<T>.asFlow() = flow { emit(tryGet()) } fun <T> SuspendLazy<T>.asFlow() = flow { emit(runCatchingCancellable { get() }) }

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.details.domain package org.koitharu.kotatsu.details.domain
import org.koitharu.kotatsu.core.db.MangaDatabase 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.model.isLocal
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -33,8 +32,8 @@ class ProgressUpdateUseCase @Inject constructor(
} else { } else {
seed seed
} }
val chapter = details.findChapter(history.chapterId) ?: return PROGRESS_NONE val chapter = details.findChapterById(history.chapterId) ?: return PROGRESS_NONE
val chapters = details.getChapters(chapter.branch) ?: return PROGRESS_NONE val chapters = details.getChapters(chapter.branch)
val chaptersCount = chapters.size val chaptersCount = chapters.size
if (chaptersCount == 0) { if (chaptersCount == 0) {
return PROGRESS_NONE return PROGRESS_NONE

View File

@@ -1,10 +1,10 @@
package org.koitharu.kotatsu.details.domain package org.koitharu.kotatsu.details.domain
import org.koitharu.kotatsu.core.model.MangaHistory 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.core.prefs.AppSettings
import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.data.ReadingTime import org.koitharu.kotatsu.details.data.ReadingTime
import org.koitharu.kotatsu.parsers.util.findById
import org.koitharu.kotatsu.stats.data.StatsRepository import org.koitharu.kotatsu.stats.data.StatsRepository
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject

View File

@@ -6,7 +6,6 @@ import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.EntryPointAccessors
import org.koitharu.kotatsu.core.cache.MemoryContentCache import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.model.LocalMangaSource 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.isLocal
import org.koitharu.kotatsu.core.model.parcelable.ParcelableChapter import org.koitharu.kotatsu.core.model.parcelable.ParcelableChapter
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.findById
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject import javax.inject.Inject

View File

@@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository 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.model.getPreferredBranch
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings 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.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga 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.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.details.ui.adapter
import android.graphics.Typeface import android.graphics.Typeface
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding 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.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
@@ -22,7 +21,7 @@ fun chapterGridItemAD(
bind { payloads -> bind { payloads ->
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
binding.textViewTitle.text = item.chapter.formatNumber() ?: "?" binding.textViewTitle.text = item.chapter.numberString() ?: "?"
} }
binding.imageViewNew.isVisible = item.isNew binding.imageViewNew.isVisible = item.isNew
binding.imageViewCurrent.isVisible = item.isCurrent binding.imageViewCurrent.isVisible = item.isCurrent

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.details.ui.adapter package org.koitharu.kotatsu.details.ui.adapter
import android.content.Context import android.content.Context
import org.koitharu.kotatsu.core.model.formatNumber
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
@@ -33,7 +32,7 @@ class ChaptersAdapter(
findHeader(position)?.getText(context) findHeader(position)?.getText(context)
} else { } else {
val chapter = (items.getOrNull(position) as? ChapterListItem)?.chapter ?: return null val chapter = (items.getOrNull(position) as? ChapterListItem)?.chapter ?: return null
if (chapter.number > 0) chapter.formatNumber() else null chapter.numberString()
} }
} }
} }

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.details.ui.model
import android.text.format.DateUtils import android.text.format.DateUtils
import org.jsoup.internal.StringUtil.StringJoiner 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.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import kotlin.experimental.and import kotlin.experimental.and
@@ -53,7 +52,7 @@ data class ChapterListItem(
private fun buildDescription(): String { private fun buildDescription(): String {
val joiner = StringJoiner("") val joiner = StringJoiner("")
chapter.formatNumber()?.let { chapter.numberString()?.let {
joiner.add("#").append(it) joiner.add("#").append(it)
} }
uploadDate?.let { date -> uploadDate?.let { date ->

View File

@@ -2,8 +2,13 @@ package org.koitharu.kotatsu.details.ui.pager.pages
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.collection.ArraySet
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible 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.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener 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.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.dismissParentDialog 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.findParentCallback
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent 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.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListModel 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.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class PagesFragment : class PagesFragment :
BaseFragment<FragmentPagesBinding>(), BaseFragment<FragmentPagesBinding>(),
OnListItemClickListener<PageThumbnail> { OnListItemClickListener<PageThumbnail>, ListSelectionController.Callback {
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
@@ -51,17 +60,23 @@ class PagesFragment :
@Inject @Inject
lateinit var settings: AppSettings lateinit var settings: AppSettings
@Inject
lateinit var pageSaveHelperFactory: PageSaveHelper.Factory
private val parentViewModel by ChaptersPagesViewModel.ActivityVMLazy(this) private val parentViewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
private val viewModel by viewModels<PagesViewModel>() private val viewModel by viewModels<PagesViewModel>()
private lateinit var pageSaveHelper: PageSaveHelper
private var thumbnailsAdapter: PageThumbnailAdapter? = null private var thumbnailsAdapter: PageThumbnailAdapter? = null
private var spanResolver: GridSpanResolver? = null private var spanResolver: GridSpanResolver? = null
private var scrollListener: ScrollListener? = null private var scrollListener: ScrollListener? = null
private var selectionController: ListSelectionController? = null
private val spanSizeLookup = SpanSizeLookup() private val spanSizeLookup = SpanSizeLookup()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
pageSaveHelper = pageSaveHelperFactory.create(this)
combine( combine(
parentViewModel.mangaDetails, parentViewModel.mangaDetails,
parentViewModel.readingState, parentViewModel.readingState,
@@ -83,6 +98,12 @@ class PagesFragment :
override fun onViewBindingCreated(binding: FragmentPagesBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentPagesBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
spanResolver = GridSpanResolver(binding.root.resources) spanResolver = GridSpanResolver(binding.root.resources)
selectionController = ListSelectionController(
appCompatDelegate = checkNotNull(findAppCompatDelegate()),
decoration = PagesSelectionDecoration(binding.root.context),
registryOwner = this,
callback = this,
)
thumbnailsAdapter = PageThumbnailAdapter( thumbnailsAdapter = PageThumbnailAdapter(
coil = coil, coil = coil,
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
@@ -91,6 +112,7 @@ class PagesFragment :
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) // before rv initialization viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) // before rv initialization
with(binding.recyclerView) { with(binding.recyclerView) {
addItemDecoration(TypedListSpacingDecoration(context, false)) addItemDecoration(TypedListSpacingDecoration(context, false))
checkNotNull(selectionController).attachToRecyclerView(this)
adapter = thumbnailsAdapter adapter = thumbnailsAdapter
setHasFixedSize(true) setHasFixedSize(true)
PagerNestedScrollHelper(this).bind(viewLifecycleOwner) PagerNestedScrollHelper(this).bind(viewLifecycleOwner)
@@ -103,6 +125,7 @@ class PagesFragment :
} }
parentViewModel.isChaptersEmpty.observe(viewLifecycleOwner, ::onNoChaptersChanged) parentViewModel.isChaptersEmpty.observe(viewLifecycleOwner, ::onNoChaptersChanged)
viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged) viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged)
viewModel.onPageSaved.observeEvent(this, PagesSavedObserver(binding.recyclerView))
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.isLoading.observe(viewLifecycleOwner) { binding.progressBar.showOrHide(it) } viewModel.isLoading.observe(viewLifecycleOwner) { binding.progressBar.showOrHide(it) }
viewModel.isLoadingUp.observe(viewLifecycleOwner) { binding.progressBarTop.showOrHide(it) } viewModel.isLoadingUp.observe(viewLifecycleOwner) { binding.progressBarTop.showOrHide(it) }
@@ -113,6 +136,7 @@ class PagesFragment :
spanResolver = null spanResolver = null
scrollListener = null scrollListener = null
thumbnailsAdapter = null thumbnailsAdapter = null
selectionController = null
spanSizeLookup.invalidateCache() spanSizeLookup.invalidateCache()
super.onDestroyView() super.onDestroyView()
} }
@@ -120,6 +144,9 @@ class PagesFragment :
override fun onWindowInsetsChanged(insets: Insets) = Unit override fun onWindowInsetsChanged(insets: Insets) = Unit
override fun onItemClick(item: PageThumbnail, view: View) { override fun onItemClick(item: PageThumbnail, view: View) {
if (selectionController?.onItemClick(item.page.id) == true) {
return
}
val listener = findParentCallback(ReaderNavigationCallback::class.java) val listener = findParentCallback(ReaderNavigationCallback::class.java)
if (listener != null && listener.onPageSelected(item.page)) { if (listener != null && listener.onPageSelected(item.page)) {
dismissParentDialog() 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>) { private suspend fun onThumbnailsChanged(list: List<ListModel>) {
val adapter = thumbnailsAdapter ?: return val adapter = thumbnailsAdapter ?: return
if (adapter.itemCount == 0) { 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) { private inner class ScrollListener : BoundsScrollListener(3, 3) {
override fun onScrolledToStart(recyclerView: RecyclerView) { override fun onScrolledToStart(recyclerView: RecyclerView) {

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.details.ui.pager.pages package org.koitharu.kotatsu.details.ui.pager.pages
import android.net.Uri
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers 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.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel 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.firstNotNull
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.reader.domain.ChaptersLoader 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.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -32,6 +38,7 @@ class PagesViewModel @Inject constructor(
val thumbnails = MutableStateFlow<List<ListModel>>(emptyList()) val thumbnails = MutableStateFlow<List<ListModel>>(emptyList())
val isLoadingUp = MutableStateFlow(false) val isLoadingUp = MutableStateFlow(false)
val isLoadingDown = MutableStateFlow(false) val isLoadingDown = MutableStateFlow(false)
val onPageSaved = MutableEventFlow<Collection<Uri>>()
val gridScale = settings.observeAsStateFlow( val gridScale = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default, scope = viewModelScope + Dispatchers.Default,
@@ -73,6 +80,25 @@ class PagesViewModel @Inject constructor(
loadingNextJob = loadPrevNextChapter(isNext = true) 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) { private suspend fun doInit(state: State) {
chaptersLoader.init(state.details) chaptersLoader.init(state.details)
val initialChapterId = state.readerState?.chapterId?.takeIf { val initialChapterId = state.readerState?.chapterId?.takeIf {

View File

@@ -29,9 +29,9 @@ import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.model.Manga 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.mapToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import org.koitharu.kotatsu.settings.storage.DirectoryModel import org.koitharu.kotatsu.settings.storage.DirectoryModel
import javax.inject.Inject import javax.inject.Inject
@@ -50,7 +50,7 @@ class DownloadDialogViewModel @Inject constructor(
val manga = savedStateHandle.require<Array<ParcelableManga>>(DownloadDialogFragment.ARG_MANGA).map { val manga = savedStateHandle.require<Array<ParcelableManga>>(DownloadDialogFragment.ARG_MANGA).map {
it.manga it.manga
} }
private val mangaDetails = SuspendLazy { private val mangaDetails = suspendLazy {
coroutineScope { coroutineScope {
manga.map { m -> manga.map { m ->
async { m.getDetails() } async { m.getDetails() }

View File

@@ -22,7 +22,6 @@ import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.R 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.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
@@ -308,7 +307,7 @@ class DownloadsViewModel @Inject constructor(
return chapters.mapNotNullTo(ArrayList(size)) { return chapters.mapNotNullTo(ArrayList(size)) {
if (chapterIds == null || it.id in chapterIds) { if (chapterIds == null || it.id in chapterIds) {
DownloadChapter( DownloadChapter(
number = it.formatNumber(), number = it.numberString(),
name = it.name, name = it.name,
isDownloaded = it.id in localChapters, isDownloaded = it.id in localChapters,
) )

View File

@@ -35,8 +35,8 @@ import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.YEAR_MIN 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.ifZero
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import java.util.Calendar import java.util.Calendar
@@ -59,7 +59,7 @@ class FilterCoordinator @Inject constructor(
private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder) private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder)
private val availableSortOrders = repository.sortOrders private val availableSortOrders = repository.sortOrders
private val filterOptions = SuspendLazy { repository.getFilterOptions() } private val filterOptions = suspendLazy { repository.getFilterOptions() }
val capabilities = repository.filterCapabilities val capabilities = repository.filterCapabilities
val mangaSource: MangaSource val mangaSource: MangaSource

View File

@@ -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.toMangaTags
import org.koitharu.kotatsu.core.db.entity.toMangaTagsList import org.koitharu.kotatsu.core.db.entity.toMangaTagsList
import org.koitharu.kotatsu.core.model.MangaHistory 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.isLocal
import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.model.toMangaSources 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag 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.parsers.util.levenshteinDistance
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble

View File

@@ -6,14 +6,15 @@ import kotlinx.coroutines.flow.asStateFlow
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.list.ui.model.QuickFilter 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( abstract class MangaListQuickFilter(
private val settings: AppSettings, private val settings: AppSettings,
) : QuickFilterListener { ) : QuickFilterListener {
private val appliedFilter = MutableStateFlow<Set<ListFilterOption>>(emptySet()) private val appliedFilter = MutableStateFlow<Set<ListFilterOption>>(emptySet())
private val availableFilterOptions = SuspendLazy { private val availableFilterOptions = suspendLazy {
getAvailableFilterOptions() getAvailableFilterOptions()
} }
@@ -50,7 +51,7 @@ abstract class MangaListQuickFilter(
if (!settings.isQuickFilterEnabled) { if (!settings.isQuickFilterEnabled) {
return null return null
} }
val availableOptions = availableFilterOptions.tryGet().getOrNull()?.map { option -> val availableOptions = availableFilterOptions.getOrNull()?.map { option ->
ChipsView.ChipModel( ChipsView.ChipModel(
title = option.titleText, title = option.titleText,
titleResId = option.titleResId, titleResId = option.titleResId,

View File

@@ -115,9 +115,9 @@ abstract class MangaListFragment :
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
adapter = listAdapter adapter = listAdapter
checkNotNull(selectionController).attachToRecyclerView(binding.recyclerView) checkNotNull(selectionController).attachToRecyclerView(this)
addItemDecoration(TypedListSpacingDecoration(context, false)) addItemDecoration(TypedListSpacingDecoration(context, false))
addOnScrollListener(paginationListener!!) addOnScrollListener(checkNotNull(paginationListener))
fastScroller.setFastScrollListener(this@MangaListFragment) fastScroller.setFastScrollListener(this@MangaListFragment)
} }
with(binding.swipeRefreshLayout) { with(binding.swipeRefreshLayout) {

View File

@@ -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.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.takeIfWriteable import org.koitharu.kotatsu.core.util.ext.takeIfWriteable
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable 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.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@@ -32,13 +32,13 @@ import javax.inject.Singleton
@Singleton @Singleton
class PagesCache @Inject constructor(@ApplicationContext context: Context) { class PagesCache @Inject constructor(@ApplicationContext context: Context) {
private val cacheDir = SuspendLazy { private val cacheDir = suspendLazy {
val dirs = context.externalCacheDirs + context.cacheDir val dirs = context.externalCacheDirs + context.cacheDir
dirs.firstNotNullOf { dirs.firstNotNullOf {
it?.subdir(CacheDir.PAGES.dir)?.takeIfWriteable() it?.subdir(CacheDir.PAGES.dir)?.takeIfWriteable()
} }
} }
private val lruCache = SuspendLazy { private val lruCache = suspendLazy {
val dir = cacheDir.get() val dir = cacheDir.get()
val availableSize = (getAvailableSize() * 0.8).toLong() val availableSize = (getAvailableSize() * 0.8).toLong()
val size = SIZE_DEFAULT.coerceAtMost(availableSize).coerceAtLeast(SIZE_MIN) val size = SIZE_DEFAULT.coerceAtMost(availableSize).coerceAtLeast(SIZE_MIN)

View File

@@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.fold import kotlinx.coroutines.flow.fold
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.ids import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaRepository 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.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter 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.recoverCatchingCancellable
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject import javax.inject.Inject
@@ -77,8 +77,8 @@ class DeleteReadChaptersUseCase @Inject constructor(
return null return null
} }
val branch = (chapters.findById(history.chapterId) ?: return null).branch val branch = (chapters.findById(history.chapterId) ?: return null).branch
val filteredChapters = manga.manga.getChapters(branch)?.takeWhile { it.id != history.chapterId } val filteredChapters = manga.manga.getChapters(branch).takeWhile { it.id != history.chapterId }
return if (filteredChapters.isNullOrEmpty()) { return if (filteredChapters.isEmpty()) {
null null
} else { } else {
DeletionTask( DeletionTask(

View File

@@ -6,7 +6,6 @@ import coil3.request.ErrorResult
import coil3.request.ImageResult import coil3.request.ImageResult
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository 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.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository 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.mangaKey
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.Manga 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.parsers.util.runCatchingCancellable
import java.util.Collections import java.util.Collections
import javax.inject.Inject import javax.inject.Inject

View File

@@ -7,7 +7,6 @@ import androidx.core.net.toFile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koitharu.kotatsu.core.model.findChapter
import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
@@ -40,7 +39,7 @@ class DetectReaderModeUseCase @Inject constructor(
if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) { if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) {
return defaultMode return defaultMode
} }
val chapter = state?.let { manga.findChapter(it.chapterId) } val chapter = state?.let { manga.findChapterById(it.chapterId) }
?: manga.chapters?.firstOrNull() ?: manga.chapters?.firstOrNull()
?: error("There are no chapters in this manga") ?: error("There are no chapters in this manga")
val repo = mangaRepositoryFactory.create(manga.source) val repo = mangaRepositoryFactory.create(manga.source)

View File

@@ -67,17 +67,14 @@ class PageSaveHelper @AssistedInject constructor(
} }
} }
suspend fun save(tasks: Set<Task>): Uri? = when (tasks.size) { suspend fun save(tasks: Collection<Task>): Collection<Uri> = when (tasks.size) {
0 -> null 0 -> emptySet()
1 -> saveImpl(tasks.first()) 1 -> setOf(saveImpl(tasks.first()))
else -> { else -> saveImpl(tasks)
saveImpl(tasks)
null
}
} }
private suspend fun saveImpl(task: Task): Uri { private suspend fun saveImpl(task: Task): Uri {
val pageLoader = pageLoaderProvider.get() val pageLoader = getPageLoader()
val pageUrl = pageLoader.getPageUrl(task.page).toUri() val pageUrl = pageLoader.getPageUrl(task.page).toUri()
val pageUri = pageLoader.loadPage(task.page, force = false) val pageUri = pageLoader.loadPage(task.page, force = false)
val proposedName = task.getFileBaseName() + "." + getPageExtension(pageUrl, pageUri) val proposedName = task.getFileBaseName() + "." + getPageExtension(pageUrl, pageUri)
@@ -89,13 +86,14 @@ class PageSaveHelper @AssistedInject constructor(
return destination return destination
} }
private suspend fun saveImpl(tasks: Collection<Task>) { private suspend fun saveImpl(tasks: Collection<Task>): Collection<Uri> {
val pageLoader = pageLoaderProvider.get() val pageLoader = getPageLoader()
val destinationDir = getDefaultFileUri(null) ?: run { val destinationDir = getDefaultFileUri(null) ?: run {
val defaultUri = settings.getPagesSaveDir(context)?.uri val defaultUri = settings.getPagesSaveDir(context)?.uri
DocumentFile.fromTreeUri(context, pickDirectoryRequest.launchAndAwait(defaultUri)) DocumentFile.fromTreeUri(context, pickDirectoryRequest.launchAndAwait(defaultUri))
} ?: throw IOException("Cannot get destination directory") } ?: throw IOException("Cannot get destination directory")
val result = ArrayList<Uri>(tasks.size)
for (task in tasks) { for (task in tasks) {
val pageUrl = pageLoader.getPageUrl(task.page).toUri() val pageUrl = pageLoader.getPageUrl(task.page).toUri()
val pageUri = pageLoader.loadPage(task.page, force = false) val pageUri = pageLoader.loadPage(task.page, force = false)
@@ -106,7 +104,9 @@ class PageSaveHelper @AssistedInject constructor(
} }
val destination = destinationDir.createFile(mime, proposedName.substringBeforeLast('.')) val destination = destinationDir.createFile(mime, proposedName.substringBeforeLast('.'))
copyImpl(pageUri, destination?.uri ?: throw IOException("Cannot create destination file")) 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 { 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? { private fun getDefaultFileUri(proposedName: String?): DocumentFile? {
if (settings.isPagesSavingAskEnabled) { if (settings.isPagesSavingAskEnabled) {
return null return null

View File

@@ -53,6 +53,7 @@ import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.core.util.ext.zipWithPrevious import org.koitharu.kotatsu.core.util.ext.zipWithPrevious
import org.koitharu.kotatsu.databinding.ActivityReaderBinding import org.koitharu.kotatsu.databinding.ActivityReaderBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.reader.data.TapGridSettings import org.koitharu.kotatsu.reader.data.TapGridSettings
@@ -143,7 +144,7 @@ class ReaderActivity :
), ),
) )
viewModel.readerMode.observe(this, Lifecycle.State.STARTED, this::onInitReader) 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.uiState.zipWithPrevious().observe(this, this::onUiStateChanged)
viewModel.isLoading.observe(this, this::onLoadingStateChanged) viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.content.observe(this) { viewModel.content.observe(this) {
@@ -289,17 +290,6 @@ class ReaderActivity :
readerManager.setDoubleReaderMode(isEnabled) 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) { private fun setKeepScreenOn(isKeep: Boolean) {
if (isKeep) { if (isKeep) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

View File

@@ -30,7 +30,6 @@ import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository 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.getPreferredBranch
import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
@@ -111,7 +110,7 @@ class ReaderViewModel @Inject constructor(
} }
val readerMode = MutableStateFlow<ReaderMode?>(null) val readerMode = MutableStateFlow<ReaderMode?>(null)
val onPageSaved = MutableEventFlow<Uri?>() val onPageSaved = MutableEventFlow<Collection<Uri>>()
val onShowToast = MutableEventFlow<Int>() val onShowToast = MutableEventFlow<Int>()
val uiState = MutableStateFlow<ReaderUiState?>(null) val uiState = MutableStateFlow<ReaderUiState?>(null)
@@ -261,8 +260,8 @@ class ReaderViewModel @Inject constructor(
val currentManga = manga.requireValue() val currentManga = manga.requireValue()
val task = PageSaveHelper.Task( val task = PageSaveHelper.Task(
manga = currentManga, manga = currentManga,
chapter = checkNotNull(currentManga.findChapter(state.chapterId)), chapter = currentManga.requireChapterById(state.chapterId),
pageNumber = state.page, pageNumber = state.page + 1,
page = checkNotNull(getCurrentPage()) { "Cannot find current page" }, page = checkNotNull(getCurrentPage()) { "Cannot find current page" },
) )
val dest = pageSaveHelper.save(setOf(task)) val dest = pageSaveHelper.save(setOf(task))
@@ -497,7 +496,7 @@ class ReaderViewModel @Inject constructor(
val history = historyRepository.getOne(manga) val history = historyRepository.getOne(manga)
val preselectedBranch = selectedBranch.value val preselectedBranch = selectedBranch.value
val result = if (history != null) { val result = if (history != null) {
if (preselectedBranch != null && preselectedBranch != manga.findChapter(history.chapterId)?.branch) { if (preselectedBranch != null && preselectedBranch != manga.findChapterById(history.chapterId)?.branch) {
null null
} else { } else {
ReaderState(history) ReaderState(history)

View File

@@ -11,7 +11,8 @@ import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.ext.mapToArray import org.koitharu.kotatsu.core.util.ext.mapToArray
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.MangaSource 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 import kotlin.coroutines.resume
class ImageServerDelegate( class ImageServerDelegate(
@@ -19,30 +20,30 @@ class ImageServerDelegate(
private val mangaSource: MangaSource?, private val mangaSource: MangaSource?,
) { ) {
private val repositoryLazy = SuspendLazy { private val repositoryLazy = suspendLazy {
mangaRepositoryFactory.create(checkNotNull(mangaSource)) as ParserMangaRepository mangaRepositoryFactory.create(checkNotNull(mangaSource)) as ParserMangaRepository
} }
suspend fun isAvailable() = withContext(Dispatchers.Default) { suspend fun isAvailable() = withContext(Dispatchers.Default) {
repositoryLazy.tryGet().map { repository -> repositoryLazy.getOrNull()?.let { repository ->
repository.getConfigKeys().any { it is ConfigKey.PreferredImageServer } repository.getConfigKeys().any { it is ConfigKey.PreferredImageServer }
}.getOrDefault(false) } == true
} }
suspend fun getValue(): String? = withContext(Dispatchers.Default) { suspend fun getValue(): String? = withContext(Dispatchers.Default) {
repositoryLazy.tryGet().map { repository -> repositoryLazy.getOrNull()?.let { repository ->
val key = repository.getConfigKeys().firstNotNullOfOrNull { it as? ConfigKey.PreferredImageServer } val key = repository.getConfigKeys().firstNotNullOfOrNull { it as? ConfigKey.PreferredImageServer }
if (key != null) { if (key != null) {
key.presetValues[repository.getConfig()[key]] key.presetValues[repository.getConfig()[key]]
} else { } else {
null null
} }
}.getOrNull() }
} }
suspend fun showDialog(context: Context): Boolean { suspend fun showDialog(context: Context): Boolean {
val repository = withContext(Dispatchers.Default) { val repository = withContext(Dispatchers.Default) {
repositoryLazy.tryGet().getOrNull() repositoryLazy.getOrNull()
} ?: return false } ?: return false
val key = repository.getConfigKeys().firstNotNullOfOrNull { val key = repository.getConfigKeys().firstNotNullOfOrNull {
it as? ConfigKey.PreferredImageServer it as? ConfigKey.PreferredImageServer

View File

@@ -11,12 +11,12 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase 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.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.findKeyByValue import org.koitharu.kotatsu.core.util.ext.findKeyByValue
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.sanitize import org.koitharu.kotatsu.core.util.ext.sanitize
import org.koitharu.kotatsu.parsers.model.Manga 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.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity

View File

@@ -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.call
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toUriOrNull 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.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.util.Date import java.util.Date
@@ -31,7 +31,7 @@ class RestoreViewModel @Inject constructor(
@ApplicationContext context: Context, @ApplicationContext context: Context,
) : BaseViewModel() { ) : BaseViewModel() {
private val backupInput = SuspendLazy { private val backupInput = suspendLazy {
val uri = savedStateHandle.get<String>(RestoreDialogFragment.ARG_FILE) val uri = savedStateHandle.get<String>(RestoreDialogFragment.ARG_FILE)
?.toUriOrNull() ?: throw FileNotFoundException() ?.toUriOrNull() ?: throw FileNotFoundException()
val contentResolver = context.contentResolver val contentResolver = context.contentResolver

View 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>

View File

@@ -57,6 +57,7 @@
<string name="_s_deleted_from_local_storage">\"%s\" deleted from local storage</string> <string name="_s_deleted_from_local_storage">\"%s\" deleted from local storage</string>
<string name="save_page">Save page</string> <string name="save_page">Save page</string>
<string name="page_saved">Page saved</string> <string name="page_saved">Page saved</string>
<string name="pages_saved">Pages saved</string>
<string name="share_image">Share image</string> <string name="share_image">Share image</string>
<string name="_import">Import</string> <string name="_import">Import</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>

View File

@@ -29,7 +29,7 @@ material = "1.12.0"
moshi = "1.15.1" moshi = "1.15.1"
okhttp = "4.12.0" okhttp = "4.12.0"
okio = "3.9.1" okio = "3.9.1"
parsers = "f610ae6412" parsers = "8b4bac3cc2"
preference = "1.2.1" preference = "1.2.1"
recyclerview = "1.3.2" recyclerview = "1.3.2"
room = "2.6.1" room = "2.6.1"
@@ -37,7 +37,7 @@ runner = "1.6.2"
rules = "1.6.1" rules = "1.6.1"
ssiv = "d1d10a6975" ssiv = "d1d10a6975"
swiperefreshlayout = "1.1.0" swiperefreshlayout = "1.1.0"
kspPlugin = "2.0.21-1.0.26" kspPlugin = "2.0.21-1.0.27"
transition = "1.5.1" transition = "1.5.1"
viewpager2 = "1.1.0" viewpager2 = "1.1.0"
webkit = "1.12.1" webkit = "1.12.1"