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