Page image picker; ability to use manga page as custom cover

This commit is contained in:
Koitharu
2025-06-05 21:02:49 +03:00
parent 5d1afab071
commit 60dca5f8c3
15 changed files with 616 additions and 22 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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