Batch pages saving
This commit is contained in:
@@ -88,6 +88,10 @@ fun Manga.findChapter(id: Long): MangaChapter? {
|
|||||||
return chapters?.findById(id)
|
return chapters?.findById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Manga.requireChapter(id: Long): MangaChapter = checkNotNull(findChapter(id)) {
|
||||||
|
"Chapter $id not found"
|
||||||
|
}
|
||||||
|
|
||||||
fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
||||||
val ch = chapters
|
val ch = chapters
|
||||||
if (ch.isNullOrEmpty()) {
|
if (ch.isNullOrEmpty()) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
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
|
||||||
@@ -7,15 +8,21 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
|
import org.koitharu.kotatsu.core.model.requireChapter
|
||||||
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 +39,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 +81,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.requireChapter(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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ 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.findChapter
|
||||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
||||||
|
import org.koitharu.kotatsu.core.model.requireChapter
|
||||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||||
@@ -111,7 +112,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 +262,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.requireChapter(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))
|
||||||
|
|||||||
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="_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>
|
||||||
|
|||||||
Reference in New Issue
Block a user