diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9b6ee3686..b936a49d0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -269,6 +269,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
(),
+ AppBarOwner,
+ SnackbarOwner {
+
+ @Inject
+ lateinit var pageSaveHelperFactory: PageSaveHelper.Factory
+
+ override val appBar: AppBarLayout
+ get() = viewBinding.appbar
+
+ override val snackbarHost: CoordinatorLayout
+ get() = viewBinding.root
+
+ private lateinit var pageSaveHelper: PageSaveHelper
+ private val viewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(ActivityPickerBinding.inflate(layoutInflater))
+ setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false)
+ pageSaveHelper = pageSaveHelperFactory.create(this)
+ viewModel.onError.observeEvent(this, DialogErrorObserver(viewBinding.container, null))
+ viewModel.onFileReady.observeEvent(this, ::finishWithResult)
+ viewModel.isLoading.observe(this, ::onLoadingStateChanged)
+ val fm = supportFragmentManager
+ if (fm.findFragmentById(R.id.container) == null) {
+ fm.commit {
+ setReorderingAllowed(true)
+ if (intent?.hasExtra(AppRouter.KEY_MANGA) == true) {
+ replace(R.id.container, PagePickerFragment::class.java, intent.extras)
+ } else {
+ replace(R.id.container, MangaPickerFragment::class.java, null)
+ }
+ }
+ }
+ }
+
+ override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+ val typeMask = WindowInsetsCompat.Type.systemBars()
+ val bars = insets.getInsets(typeMask)
+ viewBinding.appbar.updatePadding(
+ left = bars.left,
+ right = bars.right,
+ top = bars.top,
+ )
+ return insets.consume(v, typeMask, top = true)
+ }
+
+ fun onMangaPicked(manga: Manga) {
+ val args = Bundle(1)
+ args.putLong(AppRouter.KEY_ID, manga.id)
+ supportFragmentManager.commit {
+ setReorderingAllowed(true)
+ replace(R.id.container, PagePickerFragment::class.java, args)
+ addToBackStack(null)
+ }
+ }
+
+ fun onPagePicked(manga: Manga, page: ReaderPage) {
+ val task = PageSaveHelper.Task(
+ manga = manga,
+ chapterId = page.chapterId,
+ pageNumber = page.index + 1,
+ page = page.toMangaPage(),
+ )
+ viewModel.savePageToTempFile(pageSaveHelper, task)
+ }
+
+ private fun onLoadingStateChanged(isLoading: Boolean) {
+ viewBinding.container.isGone = isLoading
+ viewBinding.progressBar.isVisible = isLoading
+ }
+
+ private fun finishWithResult(file: File) {
+ val uri = FileProvider.getUriForFile(applicationContext, "${BuildConfig.APPLICATION_ID}.files", file)
+ val result = Intent()
+ result.setData(uri)
+ result.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ setResult(RESULT_OK, result)
+ finish()
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/PageImagePickContract.kt b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/PageImagePickContract.kt
new file mode 100644
index 000000000..8c72d5ecf
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/PageImagePickContract.kt
@@ -0,0 +1,18 @@
+package org.koitharu.kotatsu.picker.ui
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.activity.result.contract.ActivityResultContract
+import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
+import org.koitharu.kotatsu.core.nav.AppRouter
+import org.koitharu.kotatsu.parsers.model.Manga
+
+class PageImagePickContract : ActivityResultContract() {
+
+ override fun createIntent(context: Context, input: Manga?): Intent =
+ Intent(context, PageImagePickActivity::class.java)
+ .putExtra(AppRouter.KEY_MANGA, input?.let { ParcelableManga(it) })
+
+ override fun parseResult(resultCode: Int, intent: Intent?): Uri? = intent?.data
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/PageImagePickViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/PageImagePickViewModel.kt
new file mode 100644
index 000000000..b89993cf5
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/PageImagePickViewModel.kt
@@ -0,0 +1,23 @@
+package org.koitharu.kotatsu.picker.ui
+
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+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.reader.ui.PageSaveHelper
+import java.io.File
+import javax.inject.Inject
+
+@HiltViewModel
+class PageImagePickViewModel @Inject constructor() : BaseViewModel() {
+
+ val onFileReady = MutableEventFlow()
+
+ fun savePageToTempFile(pageSaveHelper: PageSaveHelper, task: PageSaveHelper.Task) {
+ launchLoadingJob(Dispatchers.Default) {
+ val file = pageSaveHelper.saveToTempFile(task)
+ onFileReady.call(file)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/manga/MangaPickerFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/manga/MangaPickerFragment.kt
new file mode 100644
index 000000000..358f47fed
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/manga/MangaPickerFragment.kt
@@ -0,0 +1,32 @@
+package org.koitharu.kotatsu.picker.ui.manga
+
+import android.view.View
+import androidx.fragment.app.viewModels
+import dagger.hilt.android.AndroidEntryPoint
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.list.ui.MangaListFragment
+import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.picker.ui.PageImagePickActivity
+
+@AndroidEntryPoint
+class MangaPickerFragment : MangaListFragment() {
+
+ override val isSwipeRefreshEnabled = false
+
+ override val viewModel by viewModels()
+
+ override fun onScrolledToEnd() = Unit
+
+ override fun onItemClick(item: Manga, view: View) {
+ (activity as PageImagePickActivity).onMangaPicked(item)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ activity?.setTitle(R.string.pick_manga_page)
+ }
+
+ override fun onItemLongClick(item: Manga, view: View): Boolean = false
+
+ override fun onItemContextClick(item: Manga, view: View): Boolean = false
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/manga/MangaPickerViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/manga/MangaPickerViewModel.kt
new file mode 100644
index 000000000..a8b692989
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/manga/MangaPickerViewModel.kt
@@ -0,0 +1,57 @@
+package org.koitharu.kotatsu.picker.ui.manga
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.plus
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.parser.MangaDataRepository
+import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
+import org.koitharu.kotatsu.history.data.HistoryRepository
+import org.koitharu.kotatsu.list.domain.MangaListMapper
+import org.koitharu.kotatsu.list.ui.MangaListViewModel
+import org.koitharu.kotatsu.list.ui.model.ListHeader
+import org.koitharu.kotatsu.list.ui.model.ListModel
+import org.koitharu.kotatsu.list.ui.model.LoadingState
+import javax.inject.Inject
+
+@HiltViewModel
+class MangaPickerViewModel @Inject constructor(
+ private val settings: AppSettings,
+ mangaDataRepository: MangaDataRepository,
+ private val historyRepository: HistoryRepository,
+ private val favouritesRepository: FavouritesRepository,
+ private val mangaListMapper: MangaListMapper,
+) : MangaListViewModel(settings, mangaDataRepository) {
+
+ override val content: StateFlow>
+ get() = flow {
+ emit(loadList())
+ }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState))
+
+ override fun onRefresh() = Unit
+
+ override fun onRetry() = Unit
+
+ private suspend fun loadList() = buildList {
+ val history = historyRepository.getList(0, Int.MAX_VALUE)
+ if (history.isNotEmpty()) {
+ add(ListHeader(R.string.history))
+ mangaListMapper.toListModelList(this, history, settings.listMode)
+ }
+ val categories = favouritesRepository.observeCategoriesForLibrary().first()
+ for (category in categories) {
+ val favorites = favouritesRepository.getManga(category.id)
+ if (favorites.isNotEmpty()) {
+ add(ListHeader(category.title))
+ mangaListMapper.toListModelList(this, favorites, settings.listMode)
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/page/PagePickerFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/page/PagePickerFragment.kt
new file mode 100644
index 000000000..a8497fa4b
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/page/PagePickerFragment.kt
@@ -0,0 +1,165 @@
+package org.koitharu.kotatsu.picker.ui.page
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import dagger.hilt.android.AndroidEntryPoint
+import org.koitharu.kotatsu.R
+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.OnListItemClickListener
+import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper
+import org.koitharu.kotatsu.core.util.ext.consumeAll
+import org.koitharu.kotatsu.core.util.ext.observe
+import org.koitharu.kotatsu.core.util.ext.observeEvent
+import org.koitharu.kotatsu.core.util.ext.showOrHide
+import org.koitharu.kotatsu.databinding.FragmentPagesBinding
+import org.koitharu.kotatsu.details.ui.pager.pages.PageThumbnail
+import org.koitharu.kotatsu.details.ui.pager.pages.PageThumbnailAdapter
+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.parsers.util.ifNullOrEmpty
+import org.koitharu.kotatsu.picker.ui.PageImagePickActivity
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class PagePickerFragment :
+ BaseFragment(),
+ OnListItemClickListener {
+
+ @Inject
+ lateinit var settings: AppSettings
+
+ private val viewModel by viewModels()
+
+ private var thumbnailsAdapter: PageThumbnailAdapter? = null
+ private var spanResolver: GridSpanResolver? = null
+ private var scrollListener: ScrollListener? = null
+
+ private val spanSizeLookup = SpanSizeLookup()
+
+ override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentPagesBinding {
+ return FragmentPagesBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewBindingCreated(binding: FragmentPagesBinding, savedInstanceState: Bundle?) {
+ super.onViewBindingCreated(binding, savedInstanceState)
+ spanResolver = GridSpanResolver(binding.root.resources)
+ thumbnailsAdapter = PageThumbnailAdapter(
+ clickListener = this@PagePickerFragment,
+ )
+ viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) // before rv initialization
+ with(binding.recyclerView) {
+ addItemDecoration(TypedListSpacingDecoration(context, false))
+ adapter = thumbnailsAdapter
+ setHasFixedSize(true)
+ PagerNestedScrollHelper(this).bind(viewLifecycleOwner)
+ addOnLayoutChangeListener(spanResolver)
+ addOnScrollListener(ScrollListener().also { scrollListener = it })
+ (layoutManager as GridLayoutManager).let {
+ it.spanSizeLookup = spanSizeLookup
+ it.spanCount = checkNotNull(spanResolver).spanCount
+ }
+ }
+ viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged)
+ viewModel.isNoChapters.observe(viewLifecycleOwner, ::onNoChaptersChanged)
+ viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
+ viewModel.isLoading.observe(viewLifecycleOwner) { binding.progressBar.showOrHide(it) }
+ viewModel.isLoadingDown.observe(viewLifecycleOwner) { binding.progressBarBottom.showOrHide(it) }
+ viewModel.manga.observe(viewLifecycleOwner, Lifecycle.State.RESUMED) {
+ activity?.title = it?.toManga()?.title.ifNullOrEmpty { getString(R.string.pick_manga_page) }
+ }
+ }
+
+ override fun onDestroyView() {
+ spanResolver = null
+ scrollListener = null
+ thumbnailsAdapter = null
+ spanSizeLookup.invalidateCache()
+ super.onDestroyView()
+ }
+
+ override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+ val typeBask = WindowInsetsCompat.Type.systemBars()
+ val barsInsets = insets.getInsets(typeBask)
+ viewBinding?.recyclerView?.setPadding(
+ barsInsets.left,
+ barsInsets.top,
+ barsInsets.right,
+ barsInsets.bottom,
+ )
+ return insets.consumeAll(typeBask)
+ }
+
+ override fun onItemClick(item: PageThumbnail, view: View) {
+ val manga = viewModel.manga.value?.toManga() ?: return
+ (activity as PageImagePickActivity).onPagePicked(manga, item.page)
+ }
+
+ override fun onItemLongClick(item: PageThumbnail, view: View): Boolean = false
+
+ override fun onItemContextClick(item: PageThumbnail, view: View): Boolean = false
+
+ private suspend fun onThumbnailsChanged(list: List) {
+ val adapter = thumbnailsAdapter ?: return
+ adapter.emit(list)
+ spanSizeLookup.invalidateCache()
+ viewBinding?.recyclerView?.let {
+ scrollListener?.postInvalidate(it)
+ }
+ }
+
+ private fun onGridScaleChanged(scale: Float) {
+ spanSizeLookup.invalidateCache()
+ spanResolver?.setGridSize(scale, requireViewBinding().recyclerView)
+ }
+
+ private fun onNoChaptersChanged(isNoChapters: Boolean) {
+ with(viewBinding ?: return) {
+ textViewHolder.isVisible = isNoChapters
+ recyclerView.isInvisible = isNoChapters
+ }
+ }
+
+ private inner class ScrollListener : BoundsScrollListener(3, 3) {
+
+ override fun onScrolledToStart(recyclerView: RecyclerView) = Unit
+
+ override fun onScrolledToEnd(recyclerView: RecyclerView) {
+ viewModel.loadNextChapter()
+ }
+ }
+
+ private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
+
+ init {
+ isSpanIndexCacheEnabled = true
+ isSpanGroupIndexCacheEnabled = true
+ }
+
+ override fun getSpanSize(position: Int): Int {
+ val total = (viewBinding?.recyclerView?.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
+ return when (thumbnailsAdapter?.getItemViewType(position)) {
+ ListItemType.PAGE_THUMB.ordinal -> 1
+ else -> total
+ }
+ }
+
+ fun invalidateCache() {
+ invalidateSpanGroupIndexCache()
+ invalidateSpanIndexCache()
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/page/PagePickerViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/page/PagePickerViewModel.kt
new file mode 100644
index 000000000..c8821652b
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/picker/ui/page/PagePickerViewModel.kt
@@ -0,0 +1,106 @@
+package org.koitharu.kotatsu.picker.ui.page
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.plus
+import org.koitharu.kotatsu.core.nav.MangaIntent
+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.firstNotNull
+import org.koitharu.kotatsu.details.data.MangaDetails
+import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
+import org.koitharu.kotatsu.details.ui.pager.pages.PageThumbnail
+import org.koitharu.kotatsu.list.ui.model.ListHeader
+import org.koitharu.kotatsu.list.ui.model.ListModel
+import org.koitharu.kotatsu.reader.domain.ChaptersLoader
+import javax.inject.Inject
+
+@HiltViewModel
+class PagePickerViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val chaptersLoader: ChaptersLoader,
+ private val detailsLoadUseCase: DetailsLoadUseCase,
+ settings: AppSettings,
+) : BaseViewModel() {
+
+ private val intent = MangaIntent(savedStateHandle)
+
+ private var loadingJob: Job? = null
+ private var loadingNextJob: Job? = null
+
+ val thumbnails = MutableStateFlow>(emptyList())
+ val isLoadingDown = MutableStateFlow(false)
+ val manga = MutableStateFlow(intent.manga?.let { MangaDetails(it) })
+
+ val isNoChapters = manga.map {
+ it != null && it.isLoaded && it.allChapters.isEmpty()
+ }
+
+ val gridScale = settings.observeAsStateFlow(
+ scope = viewModelScope + Dispatchers.Default,
+ key = AppSettings.KEY_GRID_SIZE_PAGES,
+ valueProducer = { gridSizePages / 100f },
+ )
+
+ init {
+ loadingJob = launchLoadingJob(Dispatchers.Default) {
+ doInit()
+ }
+ }
+
+ private suspend fun doInit() {
+ val details = detailsLoadUseCase.invoke(intent, force = false)
+ .onEach { manga.value = it }
+ .first { x -> x.isLoaded }
+ chaptersLoader.init(details)
+ val initialChapterId = details.allChapters.firstOrNull()?.id ?: return
+ if (!chaptersLoader.hasPages(initialChapterId)) {
+ chaptersLoader.loadSingleChapter(initialChapterId)
+ }
+ updateList()
+ }
+
+ fun loadNextChapter() {
+ if (loadingJob?.isActive == true || loadingNextJob?.isActive == true) {
+ return
+ }
+ loadingNextJob = launchJob(Dispatchers.Default) {
+ isLoadingDown.value = true
+ try {
+ val currentId = chaptersLoader.last().chapterId
+ chaptersLoader.loadPrevNextChapter(manga.firstNotNull(), currentId, isNext = true)
+ updateList()
+ } finally {
+ isLoadingDown.value = false
+ }
+ }
+ }
+
+ private fun updateList() {
+ val snapshot = chaptersLoader.snapshot()
+ val pages = buildList(snapshot.size + chaptersLoader.size + 2) {
+ var previousChapterId = 0L
+ for (page in snapshot) {
+ if (page.chapterId != previousChapterId) {
+ chaptersLoader.peekChapter(page.chapterId)?.let {
+ add(ListHeader(it))
+ }
+ previousChapterId = page.chapterId
+ }
+ this += PageThumbnail(
+ isCurrent = false,
+ page = page,
+ )
+ }
+ }
+ thumbnails.value = pages
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt
index ac34f89b3..d1d593ead 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt
@@ -35,7 +35,6 @@ import org.koitharu.kotatsu.core.util.ext.toFileNameSafe
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import java.io.File
@@ -72,6 +71,16 @@ class PageSaveHelper @AssistedInject constructor(
else -> saveImpl(tasks)
}
+ suspend fun saveToTempFile(task: Task): File {
+ 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)
+ val destination = File(checkNotNull(context.getExternalFilesDir(TEMP_DIR)), proposedName)
+ copyImpl(pageUri, destination.toUri())
+ return destination
+ }
+
private suspend fun saveImpl(task: Task): Uri {
val pageLoader = getPageLoader()
val pageUrl = pageLoader.getPageUrl(task.page).toUri()
@@ -206,5 +215,6 @@ class PageSaveHelper @AssistedInject constructor(
private const val MAX_BASENAME_LENGTH = 12
private const val EXTENSION_FALLBACK = "png"
+ private const val TEMP_DIR = "pages"
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
index 985b05a60..fba48bc93 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
@@ -108,7 +108,7 @@ class ReaderViewModel @Inject constructor(
init {
selectedBranch.value = savedStateHandle.get(ReaderIntent.EXTRA_BRANCH)
readingState.value = savedStateHandle[ReaderIntent.EXTRA_STATE]
- mangaDetails.value = intent.manga?.let { MangaDetails(it, null, null, null, false) }
+ mangaDetails.value = intent.manga?.let { MangaDetails(it) }
}
val readerMode = MutableStateFlow(null)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/override/OverrideConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/override/OverrideConfigActivity.kt
index 667823721..3b8a97495 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/override/OverrideConfigActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/override/OverrideConfigActivity.kt
@@ -1,10 +1,11 @@
package org.koitharu.kotatsu.settings.override
import android.content.Intent
+import android.net.Uri
import android.os.Bundle
import android.view.View
-import androidx.activity.result.PickVisualMediaRequest
-import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
+import androidx.activity.result.ActivityResultCallback
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
@@ -16,29 +17,23 @@ import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.model.MangaOverride
import org.koitharu.kotatsu.core.util.ext.consumeAll
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
-import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.databinding.ActivityOverrideEditBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
-import androidx.appcompat.R as appcompatR
+import org.koitharu.kotatsu.picker.ui.PageImagePickContract
import com.google.android.material.R as materialR
@AndroidEntryPoint
-class OverrideConfigActivity : BaseActivity(), View.OnClickListener {
+class OverrideConfigActivity : BaseActivity(), View.OnClickListener,
+ ActivityResultCallback {
private val viewModel: OverrideConfigViewModel by viewModels()
- private val pickCoverFileLauncher = registerForActivityResult(
- PickVisualMedia(),
- ) { uri ->
- if (uri != null) {
- contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
- viewModel.updateCover(uri.toString())
- }
- }
+ private val pickCoverFileLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument(), this)
+ private val pickPageLauncher = registerForActivityResult(PageImagePickContract(), this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -67,6 +62,15 @@ class OverrideConfigActivity : BaseActivity(), View
return insets.consumeAll(typeMask)
}
+ override fun onActivityResult(result: Uri?) {
+ if (result != null) {
+ if (result.host?.startsWith(packageName) != true) {
+ contentResolver.takePersistableUriPermission(result, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ viewModel.updateCover(result.toString())
+ }
+ }
+
override fun onClick(v: View) {
when (v.id) {
R.id.button_done -> viewModel.save(
@@ -77,11 +81,7 @@ class OverrideConfigActivity : BaseActivity(), View
R.id.button_reset_cover -> viewModel.updateCover(null)
R.id.button_pick_file -> {
- val request = PickVisualMediaRequest.Builder()
- .setMediaType(PickVisualMedia.ImageOnly)
- .setAccentColor(getThemeColor(appcompatR.attr.colorAccent).toLong())
- .build()
- if (!pickCoverFileLauncher.tryLaunch(request)) {
+ if (!pickCoverFileLauncher.tryLaunch(arrayOf("image/*"))) {
Snackbar.make(
viewBinding.imageViewCover,
R.string.operation_not_supported,
@@ -89,6 +89,11 @@ class OverrideConfigActivity : BaseActivity(), View
).show()
}
}
+
+ R.id.button_pick_page -> {
+ val manga = viewModel.data.value?.first
+ pickPageLauncher.launch(manga)
+ }
}
}
diff --git a/app/src/main/res/layout/activity_override_edit.xml b/app/src/main/res/layout/activity_override_edit.xml
index 0f32fb84d..a5176db77 100644
--- a/app/src/main/res/layout/activity_override_edit.xml
+++ b/app/src/main/res/layout/activity_override_edit.xml
@@ -83,7 +83,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/pick_manga_page"
- android:visibility="gone"
app:drawableStartCompat="@drawable/ic_grid"
app:layout_constraintEnd_toEndOf="@id/textView_cover_title"
app:layout_constraintStart_toStartOf="@id/textView_cover_title"
diff --git a/app/src/main/res/layout/activity_picker.xml b/app/src/main/res/layout/activity_picker.xml
new file mode 100644
index 000000000..3f481048e
--- /dev/null
+++ b/app/src/main/res/layout/activity_picker.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+