Option to override manga title and cover
parent
d542fa6bb6
commit
bd4fecc3b6
@ -0,0 +1,9 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
|
|
||||||
|
data class MangaOverride(
|
||||||
|
val coverUrl: String?,
|
||||||
|
val title: String?,
|
||||||
|
val contentRating: ContentRating?,
|
||||||
|
)
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.override
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.lifecycle
|
||||||
|
import coil3.request.target
|
||||||
|
import coil3.size.Scale
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||||
|
import org.koitharu.kotatsu.core.ui.model.MangaOverride
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.consumeAll
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
|
||||||
|
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 javax.inject.Inject
|
||||||
|
import androidx.appcompat.R as appcompatR
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class OverrideConfigActivity : BaseActivity<ActivityOverrideEditBinding>(), View.OnClickListener {
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(ActivityOverrideEditBinding.inflate(layoutInflater))
|
||||||
|
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
|
||||||
|
viewBinding.buttonDone.setOnClickListener(this)
|
||||||
|
viewBinding.buttonPickFile.setOnClickListener(this)
|
||||||
|
viewBinding.buttonPickPage.setOnClickListener(this)
|
||||||
|
viewBinding.buttonResetCover.setOnClickListener(this)
|
||||||
|
viewBinding.layoutName.setEndIconOnClickListener(this)
|
||||||
|
viewModel.data.filterNotNull().observe(this, ::onDataChanged)
|
||||||
|
viewModel.onSaved.observeEvent(this) { finishAfterTransition() }
|
||||||
|
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
|
||||||
|
viewModel.onError.observeEvent(this, ::onError)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||||
|
val typeMask = WindowInsetsCompat.Type.systemBars()
|
||||||
|
val barsInsets = insets.getInsets(typeMask)
|
||||||
|
viewBinding.root.setPadding(
|
||||||
|
barsInsets.left,
|
||||||
|
barsInsets.top,
|
||||||
|
barsInsets.right,
|
||||||
|
barsInsets.bottom,
|
||||||
|
)
|
||||||
|
return insets.consumeAll(typeMask)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
when (v.id) {
|
||||||
|
R.id.button_done -> viewModel.save(
|
||||||
|
title = viewBinding.editName.text?.toString()?.trim(),
|
||||||
|
)
|
||||||
|
|
||||||
|
materialR.id.text_input_end_icon -> viewBinding.editName.text?.clear()
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
Snackbar.make(
|
||||||
|
viewBinding.imageViewCover,
|
||||||
|
R.string.operation_not_supported,
|
||||||
|
Snackbar.LENGTH_SHORT,
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDataChanged(data: Pair<Manga, MangaOverride>) {
|
||||||
|
val (manga, override) = data
|
||||||
|
ImageRequest.Builder(this)
|
||||||
|
.target(viewBinding.imageViewCover)
|
||||||
|
.size(CoverSizeResolver(viewBinding.imageViewCover))
|
||||||
|
.scale(Scale.FILL)
|
||||||
|
.data(override.coverUrl.ifNullOrEmpty { manga.coverUrl })
|
||||||
|
.mangaSourceExtra(manga.source)
|
||||||
|
.crossfade(this)
|
||||||
|
.lifecycle(this)
|
||||||
|
.enqueueWith(coil)
|
||||||
|
viewBinding.layoutName.placeholderText = manga.title
|
||||||
|
if (viewBinding.editName.tag == null) {
|
||||||
|
viewBinding.editName.setText(override.title)
|
||||||
|
viewBinding.editName.tag = override.title
|
||||||
|
}
|
||||||
|
viewBinding.buttonResetCover.isEnabled = !override.coverUrl.isNullOrEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onError(e: Throwable) {
|
||||||
|
viewBinding.textViewError.text = e.getDisplayMessage(resources)
|
||||||
|
viewBinding.textViewError.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
|
viewBinding.buttonDone.isEnabled = !isLoading
|
||||||
|
viewBinding.editName.isEnabled = !isLoading
|
||||||
|
if (isLoading) {
|
||||||
|
viewBinding.textViewError.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.override
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
|
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.ui.model.MangaOverride
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.require
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class OverrideConfigViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
private val dataRepository: MangaDataRepository,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val manga = savedStateHandle.require<ParcelableManga>(AppRouter.KEY_MANGA).manga
|
||||||
|
|
||||||
|
val data = MutableStateFlow<Pair<Manga, MangaOverride>?>(null)
|
||||||
|
val onSaved = MutableEventFlow<Unit>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
|
data.value = manga to (dataRepository.getOverride(manga.id) ?: emptyOverride())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(title: String?) {
|
||||||
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
|
val override = checkNotNull(data.value).second.copy(
|
||||||
|
title = title,
|
||||||
|
)
|
||||||
|
dataRepository.setOverride(manga.id, override)
|
||||||
|
onSaved.call(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCover(coverUri: String?) {
|
||||||
|
val snapshot = data.value ?: return
|
||||||
|
data.value = snapshot.first to snapshot.second.copy(
|
||||||
|
coverUrl = coverUri,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emptyOverride() = MangaOverride(null, null, null)
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M13.5,7A6.5,6.5 0 0,1 20,13.5A6.5,6.5 0 0,1 13.5,20H10V18H13.5C16,18 18,16 18,13.5C18,11 16,9 13.5,9H7.83L10.91,12.09L9.5,13.5L4,8L9.5,2.5L10.92,3.91L7.83,7H13.5M6,18H8V20H6V18Z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
@ -0,0 +1,177 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
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"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_done"
|
||||||
|
style="@style/Widget.Material3.Button.UnelevatedButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
|
||||||
|
android:text="@string/save" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/scrollView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:overScrollMode="ifContentScrolls">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="@dimen/screen_padding">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/imageView_cover"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:background="?colorSecondaryContainer"
|
||||||
|
android:clipToOutline="true"
|
||||||
|
android:foreground="?selectableItemBackground"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintDimensionRatio="H,13:18"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.3"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
|
||||||
|
tools:background="@tools:sample/backgrounds/scenic[5]"
|
||||||
|
tools:ignore="ContentDescription,UnusedAttribute" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_cover_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="@dimen/screen_padding"
|
||||||
|
android:paddingHorizontal="@dimen/margin_small"
|
||||||
|
android:text="@string/change_cover"
|
||||||
|
android:textAppearance="?textAppearanceTitleSmall"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/imageView_cover" />
|
||||||
|
|
||||||
|
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
|
||||||
|
android:id="@+id/button_pick_file"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||||
|
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||||
|
android:text="@string/pick_custom_file"
|
||||||
|
android:textAppearance="?attr/textAppearanceButton"
|
||||||
|
app:drawableStartCompat="@drawable/ic_folder_file"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/textView_cover_title"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/textView_cover_title"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textView_cover_title" />
|
||||||
|
|
||||||
|
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
|
||||||
|
android:id="@+id/button_pick_page"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||||
|
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||||
|
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||||
|
android:text="@string/pick_manga_page"
|
||||||
|
android:textAppearance="?attr/textAppearanceButton"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:drawableStartCompat="@drawable/ic_grid"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/textView_cover_title"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/textView_cover_title"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/button_pick_file" />
|
||||||
|
|
||||||
|
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
|
||||||
|
android:id="@+id/button_reset_cover"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||||
|
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||||
|
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||||
|
android:text="@string/use_default_cover"
|
||||||
|
android:textAppearance="?attr/textAppearanceButton"
|
||||||
|
app:drawableStartCompat="@drawable/ic_revert"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/textView_cover_title"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/textView_cover_title"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/button_pick_page" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/barrier_cover"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:constraint_referenced_ids="imageView_cover,button_reset_cover" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/layout_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="@dimen/screen_padding"
|
||||||
|
android:layout_marginTop="@dimen/margin_normal"
|
||||||
|
app:endIconContentDescription="@string/reset"
|
||||||
|
app:endIconDrawable="@drawable/ic_revert"
|
||||||
|
app:endIconMode="custom"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/barrier_cover">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/edit_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/name"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="textCapSentences"
|
||||||
|
android:maxLength="120"
|
||||||
|
tools:text="@tools:sample/lorem[3]" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_tip"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||||
|
android:layout_marginTop="@dimen/margin_normal"
|
||||||
|
android:text="@string/manga_override_hint"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/layout_name" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_error"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="@dimen/screen_padding"
|
||||||
|
android:layout_marginTop="@dimen/margin_small"
|
||||||
|
android:textColor="?colorError"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textView_tip"
|
||||||
|
tools:text="@tools:sample/lorem[4]"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
Loading…
Reference in New Issue