Page image picker; ability to use manga page as custom cover
This commit is contained in:
@@ -269,6 +269,24 @@
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.tracker.ui.debug.TrackerDebugActivity"
|
||||
android:label="@string/tracker_debug_info" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.picker.ui.PageImagePickActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/pick_manga_page">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GET_CONTENT" />
|
||||
|
||||
<category android:name="android.intent.category.OPENABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PICK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
|
||||
@@ -20,6 +20,14 @@ data class MangaDetails(
|
||||
val isLoaded: Boolean,
|
||||
) {
|
||||
|
||||
constructor(manga: Manga) : this(
|
||||
manga = manga,
|
||||
localManga = null,
|
||||
override = null,
|
||||
description = null,
|
||||
isLoaded = false,
|
||||
)
|
||||
|
||||
val id: Long
|
||||
get() = manga.id
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class DetailsViewModel @Inject constructor(
|
||||
val mangaId = intent.mangaId
|
||||
|
||||
init {
|
||||
mangaDetails.value = intent.manga?.let { MangaDetails(it, null, null, null, false) }
|
||||
mangaDetails.value = intent.manga?.let { MangaDetails(it) }
|
||||
}
|
||||
|
||||
val history = historyRepository.observeOne(mangaId)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.koitharu.kotatsu.picker.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.DialogErrorObserver
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.consume
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.ActivityPickerBinding
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.picker.ui.manga.MangaPickerFragment
|
||||
import org.koitharu.kotatsu.picker.ui.page.PagePickerFragment
|
||||
import org.koitharu.kotatsu.reader.ui.PageSaveHelper
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PageImagePickActivity : BaseActivity<ActivityPickerBinding>(),
|
||||
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<PageImagePickViewModel>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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<Manga?, Uri?>() {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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<File>()
|
||||
|
||||
fun savePageToTempFile(pageSaveHelper: PageSaveHelper, task: PageSaveHelper.Task) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val file = pageSaveHelper.saveToTempFile(task)
|
||||
onFileReady.call(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<MangaPickerViewModel>()
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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<List<ListModel>>
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<FragmentPagesBinding>(),
|
||||
OnListItemClickListener<PageThumbnail> {
|
||||
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
private val viewModel by viewModels<PagePickerViewModel>()
|
||||
|
||||
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<ListModel>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<List<ListModel>>(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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class ReaderViewModel @Inject constructor(
|
||||
init {
|
||||
selectedBranch.value = savedStateHandle.get<String>(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<ReaderMode?>(null)
|
||||
|
||||
@@ -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<ActivityOverrideEditBinding>(), View.OnClickListener {
|
||||
class OverrideConfigActivity : BaseActivity<ActivityOverrideEditBinding>(), View.OnClickListener,
|
||||
ActivityResultCallback<Uri?> {
|
||||
|
||||
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<ActivityOverrideEditBinding>(), 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<ActivityOverrideEditBinding>(), 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<ActivityOverrideEditBinding>(), View
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
R.id.button_pick_page -> {
|
||||
val manga = viewModel.data.value?.first
|
||||
pickPageLauncher.launch(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
36
app/src/main/res/layout/activity_picker.xml
Normal file
36
app/src/main/res/layout/activity_picker.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_collapseMode="pin"
|
||||
tools:title="Title" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
Reference in New Issue
Block a user