Feature: details page's theme based on cover (#41)

* Using MaterialKolor & Pallete to generate MaterialTheme based on seedColor

* Fix MangaActionRow's color

* Merge ImageRequest methods into one

* add backdrop color

* support dynamic color in EnhancedSource's metadata view

* dynamic color for MetadataViewScreen

* dynamic color for all dialogs

* Remove palette buttons

* add settings preference

* Update MaterialKolor version to 1.6.1 to fix DynamicMaterialTheme & Add PalleteStyle

* split DynamicMaterialTheme content

* Missed text color in Tsumino & Cleanup & revert some unneeded changes

* Change icon color to secondary

* color for EditMangaDialog

* Custom Spinner's Dropdown style

* TextSelectionHandle color

* color for add-tag dialog

* setting for Style & Animate

* Palette does not support Hardware bitmap

* using MaterialTheme instead of DynamicMaterialTheme when no need for color animation

* reduce color on backdrop

* save/load manga's cover color & ratio with Preferences

* Trying to decode bitmap directly from response when coverCache is not available

* Update MangaCover.vibrantCoverColor map with new color

* Hide Style & Animate settings if theme-cover-based is not enable

* Only generate color if theme-cover-based is enabled

* seedColor by vibrantColor from MangaCover: setPaletteColor won't needed (almost), only when failed to load vibrantColor from MangaCover

* seedColor uses remember directly to the hashmap to recompose whenever cover is updated/changed. Drop successState.seedColor

* generate favorite manga's color every time, instead of just update ratio

* Move MangaCoverMetadata to coil
This commit is contained in:
Cuong M. Tran 2024-06-04 00:18:49 +07:00 committed by GitHub
parent acfaab6102
commit 0a7f828d77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1007 additions and 88 deletions

View file

@ -256,7 +256,8 @@ dependencies {
implementation(libs.swipe)
implementation(libs.compose.webview)
implementation(libs.compose.grid)
implementation(libs.palette.ktx)
implementation(libs.material.kolor)
implementation(libs.google.api.services.drive)
implementation(libs.google.api.client.oauth)

View file

@ -1,6 +1,7 @@
package eu.kanade.domain.ui
import android.os.Build
import com.materialkolor.PaletteStyle
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode
@ -28,6 +29,12 @@ class UiPreferences(
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
fun detailsPageThemeCoverBased() = preferenceStore.getBoolean("pref_details_page_theme_cover_based_key", true)
fun themeCoverBasedStyle() = preferenceStore.getEnum("pref_theme_cover_based_style_key", PaletteStyle.Vibrant)
fun themeCoverBasedAnimate() = preferenceStore.getBoolean("pref_theme_cover_based_animate_key", false)
fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
fun dateFormat() = preferenceStore.getString("app_date_format", "")

View file

@ -74,6 +74,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.materialkolor.ktx.blend
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga
@ -129,6 +130,7 @@ fun MangaInfoBox(
brush = Brush.verticalGradient(colors = backdropGradientColors),
)
}
.background(MaterialTheme.colorScheme.inversePrimary.copy(alpha = 0.2f))
.blur(4.dp)
.alpha(0.2f),
)
@ -241,7 +243,7 @@ fun MangaActionRow(
MangaActionButton(
title = stringResource(MR.strings.action_web_view),
icon = Icons.Outlined.Public,
color = defaultActionButtonColor,
color = MaterialTheme.colorScheme.primary,
onClick = onWebViewClicked,
onLongClick = onWebViewLongClicked,
)
@ -251,7 +253,7 @@ fun MangaActionRow(
MangaActionButton(
title = stringResource(SYMR.strings.merge),
icon = Icons.AutoMirrored.Outlined.CallMerge,
color = defaultActionButtonColor,
color = MaterialTheme.colorScheme.primary,
onClick = onMergeClicked,
)
}

View file

@ -6,10 +6,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.ActivityCompat
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.materialkolor.PaletteStyle
import eu.kanade.core.preference.asState
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode
@ -42,6 +45,7 @@ object SettingsAppearanceScreen : SearchableSettings {
return listOf(
getThemeGroup(uiPreferences = uiPreferences),
getDetailsPageThemeGroup(uiPreferences = uiPreferences),
getDisplayGroup(uiPreferences = uiPreferences),
// SY -->
getNavbarGroup(uiPreferences = uiPreferences),
@ -100,6 +104,38 @@ object SettingsAppearanceScreen : SearchableSettings {
)
}
@Composable
private fun getDetailsPageThemeGroup(
uiPreferences: UiPreferences,
): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val detailsPageThemeCoverBased by remember {
Injekt.get<UiPreferences>().detailsPageThemeCoverBased().asState(scope)
}
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_details_page_theme),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.detailsPageThemeCoverBased(),
title = stringResource(MR.strings.pref_details_page_theme_cover_based),
),
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.themeCoverBasedStyle(),
title = stringResource(MR.strings.pref_theme_cover_based_style),
enabled = detailsPageThemeCoverBased,
entries = PaletteStyle.entries
.associateWith { it.name }
.toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.themeCoverBasedAnimate(),
title = stringResource(MR.strings.pref_theme_cover_based_animate),
enabled = detailsPageThemeCoverBased,
),
),
)
}
@Composable
private fun getDisplayGroup(
uiPreferences: UiPreferences,

View file

@ -54,6 +54,7 @@ import eu.kanade.tachiyomi.di.SYPreferenceModule
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
import eu.kanade.tachiyomi.data.coil.MangaCoverMetadata
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
@ -167,6 +168,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
MangaCoverMetadata.load()
// Updates widget update
with(WidgetManager(Injekt.get(), Injekt.get())) {
init(ProcessLifecycleOwner.get().lifecycleScope)

View file

@ -12,16 +12,22 @@ import coil3.fetch.SourceFetchResult
import coil3.getOrDefault
import coil3.request.Options
import com.hippo.unifile.UniFile
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER_KEY
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import logcat.LogPriority
import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.http.HTTP_NOT_MODIFIED
import okio.BufferedSource
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import okio.Source
@ -31,7 +37,10 @@ import okio.source
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.manga.model.asMangaCover
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
@ -48,7 +57,8 @@ import java.io.IOException
*/
@Suppress("LongParameterList")
class MangaCoverFetcher(
private val url: String?,
private val mangaCover: MangaCover,
private val url: String? = mangaCover.url,
private val isLibraryManga: Boolean,
private val options: Options,
private val coverFileLazy: Lazy<File?>,
@ -59,9 +69,15 @@ class MangaCoverFetcher(
private val imageLoader: ImageLoader,
) : Fetcher {
private val uiPreferences: UiPreferences = Injekt.get()
private val fileScope = CoroutineScope(Job() + Dispatchers.IO)
private val diskCacheKey: String
get() = diskCacheKeyLazy.value
/**
* Called each time a cover is displayed
*/
override suspend fun fetch(): FetchResult {
// Use custom cover if exists
val useCustomCover = options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)
@ -83,6 +99,7 @@ class MangaCoverFetcher(
}
private fun fileLoader(file: File): FetchResult {
setRatioAndColorsInScope(mangaCover, ogFile = file)
return SourceFetchResult(
source = ImageSource(
file = file.toOkioPath(),
@ -95,6 +112,7 @@ class MangaCoverFetcher(
}
private fun fileUriLoader(uri: String): FetchResult {
setRatioAndColorsInScope(mangaCover)
val source = UniFile.fromUri(options.context, uri.toUri())!!
.openInputStream()
.source()
@ -128,6 +146,7 @@ class MangaCoverFetcher(
}
// Read from snapshot
setRatioAndColorsInScope(mangaCover, bufferedSource = snapshot.toImageSource().source())
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
@ -148,6 +167,7 @@ class MangaCoverFetcher(
// Read from disk cache
snapshot = writeToDiskCache(response)
if (snapshot != null) {
setRatioAndColorsInScope(mangaCover, bufferedSource = snapshot.toImageSource().source())
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
@ -155,6 +175,13 @@ class MangaCoverFetcher(
)
}
setRatioAndColorsInScope(
mangaCover,
bufferedSource = ImageSource(
source = responseBody.source(),
fileSystem = FileSystem.SYSTEM,
).source(),
)
// Read from response if cache is unused or unusable
return SourceFetchResult(
source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM),
@ -293,6 +320,27 @@ class MangaCoverFetcher(
}
}
/**
* [setRatioAndColorsInScope] is called whenever a cover is loaded with [MangaCoverFetcher.fetch]
*
* @param bufferedSource if not null then it will load bitmap from [BufferedSource], regardless of [ogFile]
* @param ogFile if not null then it will load bitmap from [File]. If it's null then it will try to load bitmap
* from [CoverCache] using either [CoverCache.customCoverCacheDir] or [CoverCache.cacheDir]
* @param force if true (default) then it will always re-calculate ratio & color for favorite mangas.
* This is useful when a favorite manga updates/changes its cover. If false then it will only update ratio.
*/
private fun setRatioAndColorsInScope(
mangaCover: MangaCover,
bufferedSource: BufferedSource? = null,
ogFile: File? = null,
force: Boolean = true
) {
if (!uiPreferences.detailsPageThemeCoverBased().get()) return
fileScope.launch {
MangaCoverMetadata.setRatioAndColors(mangaCover, bufferedSource, ogFile, force)
}
}
private enum class Type {
File,
URI,
@ -308,7 +356,7 @@ class MangaCoverFetcher(
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
return MangaCoverFetcher(
url = data.thumbnailUrl,
mangaCover = data.asMangaCover(),
isLibraryManga = data.favorite,
options = options,
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) },
@ -330,7 +378,7 @@ class MangaCoverFetcher(
override fun create(data: MangaCover, options: Options, imageLoader: ImageLoader): Fetcher {
return MangaCoverFetcher(
url = data.url,
mangaCover = data,
isLibraryManga = data.isMangaFavorite,
options = options,
coverFileLazy = lazy { coverCache.getCoverFile(data.url) },

View file

@ -0,0 +1,134 @@
package eu.kanade.tachiyomi.data.coil
import android.graphics.BitmapFactory
import androidx.palette.graphics.Palette
import eu.kanade.tachiyomi.data.cache.CoverCache
import okio.BufferedSource
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.MangaCover
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.util.concurrent.ConcurrentHashMap
/**
* Object that holds info about a covers size ratio + dominant colors
* @author Jays2Kings
*/
object MangaCoverMetadata {
private val preferences by injectLazy<LibraryPreferences>()
private val coverCache by injectLazy<CoverCache>()
fun load() {
val ratios = preferences.coverRatios().get()
MangaCover.coverRatioMap = ConcurrentHashMap(
ratios.mapNotNull {
val splits = it.split("|")
val id = splits.firstOrNull()?.toLongOrNull()
val ratio = splits.lastOrNull()?.toFloatOrNull()
if (id != null && ratio != null) {
id to ratio
} else {
null
}
}.toMap(),
)
val colors = preferences.coverColors().get()
MangaCover.coverColorMap = ConcurrentHashMap(
colors.mapNotNull {
val splits = it.split("|")
val id = splits.firstOrNull()?.toLongOrNull()
val color = splits.getOrNull(1)?.toIntOrNull()
val textColor = splits.getOrNull(2)?.toIntOrNull()
if (id != null && color != null) {
id to (color to (textColor ?: 0))
} else {
null
}
}.toMap(),
)
}
/**
* [setRatioAndColors] generate cover's color & ratio by reading cover's bitmap from [CoverCache].
* It's called along with [MangaCoverFetcher.fetch] everytime a cover is **displayed** (anywhere).
*
* When called:
* - It removes saved colors from saved Prefs of [MangaCover.coverColorMap] if manga is not favorite.
* - If a favorite manga already restored [MangaCover.dominantCoverColors] then it
* will skip actually reading bitmap, only extract ratio. Except when [MangaCover.vibrantCoverColor]
* is not loaded then it will read bitmap & extract vibrant color.
* => always set [force] to true so it will always re-calculate ratio & color.
*
* Set [MangaCover.dominantCoverColors] for favorite manga only.
* Set [MangaCover.vibrantCoverColor] for all mangas.
*
* @param bufferedSource if not null then it will load bitmap from [BufferedSource], regardless of [ogFile]
* @param ogFile if not null then it will load bitmap from [File]. If it's null then it will try to load bitmap
* from [CoverCache] using either [CoverCache.customCoverCacheDir] or [CoverCache.cacheDir]
* @param force if true (default) then it will always re-calculate ratio & color for favorite mangas.
* This is useful when a favorite manga updates/changes its cover. If false then it will only update ratio.
*
* @author Jays2Kings
*/
fun setRatioAndColors(
mangaCover: MangaCover,
bufferedSource: BufferedSource? = null,
ogFile: File? = null,
force: Boolean = true,
) {
if (!mangaCover.isMangaFavorite) {
mangaCover.remove()
}
val file = ogFile
?: coverCache.getCustomCoverFile(mangaCover.mangaId).takeIf { it.exists() }
?: coverCache.getCoverFile(mangaCover.url)
val options = BitmapFactory.Options()
val hasVibrantColor = if (mangaCover.isMangaFavorite) mangaCover.vibrantCoverColor != null else true
// If dominantCoverColors is not null, it means that color is restored from Prefs
// and also has vibrantCoverColor (e.g. new color caused by updated cover)
val updateRatioOnly = mangaCover.dominantCoverColors != null && hasVibrantColor && !force
if (updateRatioOnly) {
// Just trying to update ratio without actual reading bitmap (bitmap will be null)
// This is often when open a favorite manga
options.inJustDecodeBounds = true
} else {
options.inSampleSize = 4
}
val bitmap = when {
bufferedSource != null -> BitmapFactory.decodeStream(bufferedSource.inputStream(), null, options)
// if the file exists and the there was still an error then the file is corrupted
file?.exists() == true -> BitmapFactory.decodeFile(file.path, options)
else -> { return }
}
if (bitmap != null) {
Palette.from(bitmap).generate {
if (it == null) return@generate
if (mangaCover.isMangaFavorite) {
it.dominantSwatch?.let { swatch ->
mangaCover.dominantCoverColors = swatch.rgb to swatch.titleTextColor
}
}
val color = it.getBestColor() ?: return@generate
mangaCover.vibrantCoverColor = color
}
}
if (mangaCover.isMangaFavorite && options.outWidth != -1 && options.outHeight != -1) {
mangaCover.ratio = options.outWidth / options.outHeight.toFloat()
}
}
fun MangaCover.remove() {
MangaCover.coverRatioMap.remove(mangaId)
MangaCover.coverColorMap.remove(mangaId)
}
fun savePrefs() {
val mapCopy = MangaCover.coverRatioMap.toMap()
preferences.coverRatios().set(mapCopy.map { "${it.key}|${it.value}" }.toSet())
val mapColorCopy = MangaCover.coverColorMap.toMap()
preferences.coverColors().set(mapColorCopy.map { "${it.key}|${it.value.first}|${it.value.second}" }.toSet())
}
}

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.coil
import androidx.palette.graphics.Palette
import coil3.Extras
import coil3.getExtra
import coil3.request.ImageRequest
@ -42,3 +43,42 @@ val Options.customDecoder: Boolean
get() = getExtra(customDecoderKey)
private val customDecoderKey = Extras.Key(default = false)
/**
* Calculate the best [Palette.Swatch] from [Palette]
* @author Jays2Kings
*/
fun Palette.getBestColor(): Int? {
// How big is the vibrant color
val vibPopulation = vibrantSwatch?.population ?: -1
// Saturation of the most dominant color
val domSat = dominantSwatch?.hsl?.get(1) ?: 0f
// Brightness of the most dominant color
val domLum = dominantSwatch?.hsl?.get(2) ?: -1f
// How big is the muted color
val mutedPopulation = mutedSwatch?.population ?: -1
// Saturation of the muted color
val mutedSat = mutedSwatch?.hsl?.get(1) ?: 0f
// If muted color is 3 times bigger than vibrant color then minimum acceptable saturation
// for muted color is lower (more likely to use it even if it's not colorful)
val mutedSatMinAcceptable = if (mutedPopulation > vibPopulation * 3f) 0.1f else 0.25f
val dominantIsColorful = domSat >= .25f
val dominantBrightnessJustRight = domLum <= .8f && domLum > .2f
val vibrantIsConsiderableBigEnough = vibPopulation >= mutedPopulation * 0.75f
val mutedIsBig = mutedPopulation > vibPopulation * 1.5f
val mutedIsNotTooBoring = mutedSat > mutedSatMinAcceptable
return when {
dominantIsColorful && dominantBrightnessJustRight -> dominantSwatch
// use vibrant color even if it's only 0.75 times smaller than muted color
vibrantIsConsiderableBigEnough -> vibrantSwatch
// use muted color if it's 1.5 times bigger than vibrant color and colorful enough (above the limit)
mutedIsBig && mutedIsNotTooBoring -> mutedSwatch
// return major vibrant color variant with more favor of vibrant color (size x3)
else -> listOfNotNull(vibrantSwatch, lightVibrantSwatch, darkVibrantSwatch)
.maxByOrNull {
if (it === vibrantSwatch) vibPopulation * 3 else it.population
}
}?.rgb
}

View file

@ -74,6 +74,7 @@ import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
import eu.kanade.tachiyomi.data.coil.MangaCoverMetadata
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
@ -337,6 +338,11 @@ class MainActivity : BaseActivity() {
// SY -->
}
override fun onPause() {
super.onPause()
MangaCoverMetadata.savePrefs()
}
override fun onProvideAssistContent(outContent: AssistContent) {
super.onProvideAssistContent(outContent)
when (val screen = navigator?.lastItem) {

View file

@ -1,13 +1,22 @@
package eu.kanade.tachiyomi.ui.manga
import android.content.Context
import android.content.res.ColorStateList
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -17,6 +26,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.view.children
@ -31,8 +41,13 @@ import eu.kanade.tachiyomi.databinding.EditMangaDialogBinding
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
import exh.ui.metadata.adapters.MetadataUIUtil.getResourceColor
import eu.kanade.tachiyomi.widget.materialdialogs.binding
import eu.kanade.tachiyomi.widget.materialdialogs.dismissDialog
import eu.kanade.tachiyomi.widget.materialdialogs.setColors
import eu.kanade.tachiyomi.widget.materialdialogs.setNegativeButton
import eu.kanade.tachiyomi.widget.materialdialogs.setPositiveButton
import eu.kanade.tachiyomi.widget.materialdialogs.setTextEdit
import eu.kanade.tachiyomi.widget.materialdialogs.setTitle
import exh.util.dropBlank
import exh.util.trimOrNull
import kotlinx.coroutines.CoroutineScope
@ -42,6 +57,7 @@ import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.source.local.isLocal
import timber.log.Timber
@Composable
fun EditMangaDialog(
@ -61,6 +77,18 @@ fun EditMangaDialog(
var binding by remember {
mutableStateOf<EditMangaDialogBinding?>(null)
}
val colors = EditMangaDialogColors(
textColor = MaterialTheme.colorScheme.onSurfaceVariant.toArgb(),
textHighlightColor = MaterialTheme.colorScheme.outline.toArgb(),
iconColor = MaterialTheme.colorScheme.primary.toArgb(),
tagColor = MaterialTheme.colorScheme.outlineVariant.toArgb(),
tagFocusColor = MaterialTheme.colorScheme.outline.toArgb(),
tagTextColor = MaterialTheme.colorScheme.onSurfaceVariant.toArgb(),
btnTextColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
btnBgColor = MaterialTheme.colorScheme.surfaceTint.toArgb(),
dropdownBgColor = MaterialTheme.colorScheme.surfaceVariant.toArgb(),
dialogBgColor = MaterialTheme.colorScheme.surfaceContainerHigh.toArgb(),
)
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
@ -109,7 +137,13 @@ fun EditMangaDialog(
EditMangaDialogBinding.inflate(LayoutInflater.from(factoryContext))
.also { binding = it }
.apply {
onViewCreated(manga, factoryContext, this, scope)
onViewCreated(
manga,
factoryContext,
this,
scope,
colors,
)
}
.root
},
@ -120,10 +154,29 @@ fun EditMangaDialog(
)
}
private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope) {
class EditMangaDialogColors(
@ColorInt val textColor: Int,
@ColorInt val textHighlightColor: Int,
@ColorInt val iconColor: Int,
@ColorInt val tagColor: Int,
@ColorInt val tagFocusColor: Int,
@ColorInt val tagTextColor: Int,
@ColorInt val btnTextColor: Int,
@ColorInt val btnBgColor: Int,
@ColorInt val dropdownBgColor: Int,
@ColorInt val dialogBgColor: Int,
)
private fun onViewCreated(
manga: Manga,
context: Context,
binding: EditMangaDialogBinding,
scope: CoroutineScope,
colors: EditMangaDialogColors,
) {
loadCover(manga, binding)
val statusAdapter: ArrayAdapter<String> = ArrayAdapter(
val statusAdapter = SpinnerAdapter(
context,
android.R.layout.simple_spinner_dropdown_item,
listOf(
@ -135,6 +188,7 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
MR.strings.cancelled,
MR.strings.on_hiatus,
).map { context.stringResource(it) },
colors,
)
binding.status.adapter = statusAdapter
@ -153,6 +207,9 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
)
}
// Set Spinner's dropdown caret color
binding.status.backgroundTintList = ColorStateList.valueOf(colors.iconColor)
if (manga.isLocal()) {
if (manga.title != manga.url) {
binding.title.setText(manga.title)
@ -163,7 +220,7 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
binding.mangaArtist.setText(manga.artist.orEmpty())
binding.thumbnailUrl.setText(manga.thumbnailUrl.orEmpty())
binding.mangaDescription.setText(manga.description.orEmpty())
binding.mangaGenresTags.setChips(manga.genre.orEmpty().dropBlank(), scope)
binding.mangaGenresTags.setChips(manga.genre.orEmpty().dropBlank(), scope, colors)
} else {
if (manga.title != manga.ogTitle) {
binding.title.append(manga.title)
@ -180,7 +237,7 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
if (manga.description != manga.ogDescription) {
binding.mangaDescription.append(manga.description.orEmpty())
}
binding.mangaGenresTags.setChips(manga.genre.orEmpty().dropBlank(), scope)
binding.mangaGenresTags.setChips(manga.genre.orEmpty().dropBlank(), scope, colors)
binding.title.hint = context.stringResource(SYMR.strings.title_hint, manga.ogTitle)
@ -189,27 +246,76 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
binding.mangaDescription.hint =
context.stringResource(
SYMR.strings.description_hint,
manga.ogDescription?.takeIf { it.isNotBlank() }?.replace("\n", " ")?.chop(20) ?: ""
manga.ogDescription?.takeIf { it.isNotBlank() }?.replace("\n", " ")?.chop(20) ?: "",
)
binding.thumbnailUrl.hint =
context.stringResource(
SYMR.strings.thumbnail_url_hint,
manga.ogThumbnailUrl?.let {
it.chop(40) + if (it.length > 46) "." + it.substringAfterLast(".").chop(6) else ""
} ?: ""
} ?: "",
)
}
binding.mangaGenresTags.clearFocus()
binding.resetTags.setOnClickListener { resetTags(manga, binding, scope) }
binding.resetInfo.setOnClickListener { resetInfo(manga, binding, scope) }
listOf(
binding.title,
binding.mangaAuthor,
binding.mangaArtist,
binding.thumbnailUrl,
binding.mangaDescription,
).forEach {
it.setTextColor(colors.textColor)
it.highlightColor = colors.textHighlightColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.textSelectHandle?.let { drawable ->
drawable.setTint(colors.iconColor)
it.setTextSelectHandle(drawable)
}
it.textSelectHandleLeft?.let { drawable ->
drawable.setTint(colors.iconColor)
it.setTextSelectHandleLeft(drawable)
}
it.textSelectHandleRight?.let { drawable ->
drawable.setTint(colors.iconColor)
it.setTextSelectHandleRight(drawable)
}
}
}
listOf(
binding.titleOutline,
binding.mangaAuthorOutline,
binding.mangaArtistOutline,
binding.thumbnailUrlOutline,
binding.mangaDescriptionOutline,
).forEach {
it.boxStrokeColor = colors.iconColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.cursorColor = ColorStateList.valueOf(colors.iconColor)
}
}
binding.resetTags.setTextColor(colors.btnTextColor)
binding.resetTags.setBackgroundColor(colors.btnBgColor)
binding.resetInfo.setTextColor(colors.btnTextColor)
binding.resetInfo.setBackgroundColor(colors.btnBgColor)
binding.resetTags.setOnClickListener { resetTags(manga, binding, scope, colors) }
binding.resetInfo.setOnClickListener { resetInfo(manga, binding, scope, colors) }
}
private fun resetTags(manga: Manga, binding: EditMangaDialogBinding, scope: CoroutineScope) {
private fun resetTags(
manga: Manga,
binding: EditMangaDialogBinding,
scope: CoroutineScope,
colors: EditMangaDialogColors,
) {
if (manga.genre.isNullOrEmpty() || manga.isLocal()) {
binding.mangaGenresTags.setChips(emptyList(), scope)
binding.mangaGenresTags.setChips(emptyList(), scope, colors)
} else {
binding.mangaGenresTags.setChips(manga.ogGenre.orEmpty(), scope)
binding.mangaGenresTags.setChips(manga.ogGenre.orEmpty(), scope, colors)
}
}
@ -219,52 +325,88 @@ private fun loadCover(manga: Manga, binding: EditMangaDialogBinding) {
}
}
private fun resetInfo(manga: Manga, binding: EditMangaDialogBinding, scope: CoroutineScope) {
private fun resetInfo(
manga: Manga,
binding: EditMangaDialogBinding,
scope: CoroutineScope,
colors: EditMangaDialogColors,
) {
binding.title.setText("")
binding.mangaAuthor.setText("")
binding.mangaArtist.setText("")
binding.thumbnailUrl.setText("")
binding.mangaDescription.setText("")
resetTags(manga, binding, scope)
resetTags(manga, binding, scope, colors)
}
private fun ChipGroup.setChips(items: List<String>, scope: CoroutineScope) {
private fun ChipGroup.setChips(
items: List<String>,
scope: CoroutineScope,
colors: EditMangaDialogColors,
) {
removeAllViews()
val colorStateList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_focused),
intArrayOf(android.R.attr.state_pressed),
intArrayOf(-android.R.attr.state_active),
),
intArrayOf(
colors.tagFocusColor,
colors.tagFocusColor,
colors.tagColor,
),
)
items.asSequence().map { item ->
Chip(context).apply {
text = item
setTextColor(colors.tagTextColor)
isCloseIconVisible = true
closeIcon?.setTint(context.getResourceColor(R.attr.colorAccent))
closeIcon?.setTint(colors.iconColor)
setOnCloseIconClickListener {
removeView(this)
}
chipBackgroundColor = colorStateList
}
}.forEach {
addView(it)
}
val addTagChip = Chip(context).apply {
setText(SYMR.strings.add_tag.getString(context))
text = SYMR.strings.add_tag.getString(context)
setTextColor(colors.tagTextColor)
chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_add_24dp)?.apply {
isChipIconVisible = true
setTint(context.getResourceColor(R.attr.colorAccent))
setTint(colors.iconColor)
}
chipBackgroundColor = colorStateList
setOnClickListener {
var newTag: String? = null
MaterialAlertDialogBuilder(context)
var dialog: AlertDialog? = null
val builder = MaterialAlertDialogBuilder(context)
val binding = builder.binding(context)
.setTitle(SYMR.strings.add_tag.getString(context))
.setTextInput {
newTag = it.trimOrNull()
.setPositiveButton(MR.strings.action_ok.getString(context)) {
dialog?.dismissDialog()
val newTag = it.trimOrNull()
if (newTag != null) setChips(items + listOfNotNull(newTag), scope, colors)
}
.setPositiveButton(MR.strings.action_ok.getString(context)) { _, _ ->
if (newTag != null) setChips(items + listOfNotNull(newTag), scope)
.setNegativeButton(MR.strings.action_cancel.getString(context)) {
dialog?.dismissDialog()
}
.setNegativeButton(MR.strings.action_cancel.getString(context), null)
.show()
.setTextEdit()
.setColors(colors)
dialog = builder.create()
dialog.setView(binding.root)
dialog.show()
}
}
addView(addTagChip)
@ -277,3 +419,48 @@ private fun ChipGroup.getTextStrings(): List<String> = children.mapNotNull {
null
}
}.toList()
private class SpinnerAdapter(
context: Context,
@LayoutRes val resource: Int,
objects: List<String>,
val colors: EditMangaDialogColors,
) : ArrayAdapter<String>(context, resource, objects) {
private val mInflater = LayoutInflater.from(context)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return createViewFromResource(mInflater, position, convertView, parent, resource)
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return createViewFromResource(mInflater, position, convertView, parent, resource)
}
private fun createViewFromResource(
inflater: LayoutInflater,
position: Int,
convertView: View?,
parent: ViewGroup,
resource: Int,
): View {
val text: TextView
val view = convertView ?: inflater.inflate(resource, parent, false)
try {
// If no custom field is assigned, assume the whole resource is a TextView
text = view as TextView
} catch (e: ClassCastException) {
Timber.e("You must supply a resource ID for a TextView")
throw IllegalStateException("ArrayAdapter requires the resource ID to be a TextView", e)
}
val item: String? = getItem(position)
if (item != null) text.text = item
text.setTextColor(colors.textColor)
text.setBackgroundColor(colors.dropdownBgColor)
return view
}
}

View file

@ -4,7 +4,9 @@ import android.content.Context
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -12,8 +14,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
@ -23,8 +27,11 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.materialkolor.DynamicMaterialTheme
import com.materialkolor.rememberDynamicColorScheme
import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
import eu.kanade.presentation.manga.ChapterSettingsDialog
@ -116,10 +123,7 @@ class MangaScreen(
return
}
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val scope = rememberCoroutineScope()
val screenModel =
rememberScreenModel { MangaScreenModel(context, mangaId, fromSource, smartSearchConfig != null) }
@ -131,6 +135,40 @@ class MangaScreen(
}
val successState = state as MangaScreenModel.State.Success
val seedColorState = rememberUpdatedState(newValue = successState.seedColor)
val uiPreferences = remember { Injekt.get<UiPreferences>() }
if (uiPreferences.themeCoverBasedAnimate().get()) {
DynamicMaterialTheme(
seedColor = seedColorState.value ?: MaterialTheme.colorScheme.primary,
useDarkTheme = isSystemInDarkTheme(),
style = uiPreferences.themeCoverBasedStyle().get(),
animate = uiPreferences.themeCoverBasedAnimate().get(),
content = { MaterialThemeContent(context, screenModel, successState) },
)
} else {
val colorScheme = rememberDynamicColorScheme(
seedColor = seedColorState.value ?: MaterialTheme.colorScheme.primary,
isDark = isSystemInDarkTheme(),
style = uiPreferences.themeCoverBasedStyle().get(),
)
MaterialTheme(
colorScheme = colorScheme,
content = { MaterialThemeContent(context, screenModel, successState) },
)
}
}
@Composable
fun MaterialThemeContent(
context: Context,
screenModel: MangaScreenModel,
successState: MangaScreenModel.State.Success,
) {
val navigator = LocalNavigator.currentOrThrow
val haptic = LocalHapticFeedback.current
val scope = rememberCoroutineScope()
val isHttpSource = remember { successState.source is HttpSource }
// KMK -->
@ -162,7 +200,6 @@ class MangaScreen(
.launchIn(this)
}
// SY <--
MangaScreen(
state = successState,
snackbarHostState = screenModel.snackbarHostState,
@ -229,8 +266,8 @@ class MangaScreen(
onMergedSettingsClicked = screenModel::showEditMergedSettingsDialog,
onMergeClicked = { openSmartSearch(navigator, successState.manga) },
onMergeWithAnotherClicked = { mergeWithAnother(navigator, context, successState.manga, screenModel::smartSearchMerge) },
onOpenPagePreview = {
openPagePreview(context, successState.chapters.minByOrNull { it.chapter.sourceOrder }?.chapter, it)
onOpenPagePreview = { page ->
openPagePreview(context, successState.chapters.minByOrNull { it.chapter.sourceOrder }?.chapter, page)
},
onMorePreviewsClicked = { openMorePagePreviews(navigator, successState.manga) },
// SY <--
@ -289,6 +326,7 @@ class MangaScreen(
},
)
}
is MangaScreenModel.Dialog.DeleteChapters -> {
DeleteChaptersDialog(
onDismissRequest = onDismissRequest,
@ -311,6 +349,7 @@ class MangaScreen(
},
)
}
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
onDismissRequest = onDismissRequest,
manga = successState.manga,
@ -324,6 +363,7 @@ class MangaScreen(
scanlatorFilterActive = successState.scanlatorFilterActive,
onScanlatorFilterClicked = { showScanlatorsDialog = true },
)
MangaScreenModel.Dialog.TrackSheet -> {
NavigatorAdaptiveSheet(
screen = TrackInfoDialogHomeScreen(
@ -335,6 +375,7 @@ class MangaScreen(
onDismissRequest = onDismissRequest,
)
}
MangaScreenModel.Dialog.FullCover -> {
val sm = rememberScreenModel { MangaCoverScreenModel(successState.manga.id) }
val manga by sm.state.collectAsState()
@ -361,6 +402,7 @@ class MangaScreen(
LoadingScreen(Modifier.systemBarsPadding())
}
}
is MangaScreenModel.Dialog.SetFetchInterval -> {
SetIntervalDialog(
interval = dialog.manga.fetchInterval,
@ -378,6 +420,7 @@ class MangaScreen(
onPositiveClick = screenModel::updateMangaInfo,
)
}
is MangaScreenModel.Dialog.EditMergedSettings -> {
EditMergedSettingsDialog(
mergedData = dialog.mergedData,
@ -402,14 +445,19 @@ class MangaScreen(
when (bulkFavoriteState.dialog) {
is BulkFavoriteScreenModel.Dialog.AddDuplicateManga ->
AddDuplicateMangaDialog(bulkFavoriteScreenModel)
is BulkFavoriteScreenModel.Dialog.RemoveManga ->
RemoveMangaDialog(bulkFavoriteScreenModel)
is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory ->
ChangeMangaCategoryDialog(bulkFavoriteScreenModel)
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory ->
ChangeMangasCategoryDialog(bulkFavoriteScreenModel)
is BulkFavoriteScreenModel.Dialog.AllowDuplicate ->
AllowDuplicateDialog(bulkFavoriteScreenModel)
else -> {}
}
// KMK <--
@ -539,8 +587,8 @@ class MangaScreen(
// SY <--
}
private fun openMetadataViewer(navigator: Navigator, manga: Manga) {
navigator.push(MetadataViewScreen(manga.id, manga.source))
private fun openMetadataViewer(navigator: Navigator, manga: Manga, seedColor: Color? = null) {
navigator.push(MetadataViewScreen(manga.id, manga.source, seedColor))
}
private fun openMergedMangaWebview(context: Context, navigator: Navigator, mergedMangaData: MergedMangaData) {

View file

@ -9,9 +9,16 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.util.fastAny
import androidx.palette.graphics.Palette
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import coil3.Image
import coil3.executeBlocking
import coil3.imageLoader
import coil3.request.ImageRequest
import coil3.request.allowHardware
import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators
@ -57,8 +64,10 @@ import eu.kanade.tachiyomi.ui.manga.RelatedManga.Companion.sorted
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.chapter.getNextUnread
import eu.kanade.tachiyomi.data.coil.getBestColor
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.system.getBitmapOrNull
import eu.kanade.tachiyomi.util.system.toast
import exh.debug.DebugToggles
import exh.eh.EHentaiUpdateHelper
@ -100,6 +109,7 @@ import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.lang.withUIContext
@ -133,10 +143,12 @@ import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.manga.interactor.UpdateMergedSettings
import tachiyomi.domain.manga.model.CustomMangaInfo
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate
import tachiyomi.domain.manga.model.MergedMangaReference
import tachiyomi.domain.manga.model.applyFilter
import tachiyomi.domain.manga.model.asMangaCover
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.service.SourceManager
@ -242,8 +254,6 @@ class MangaScreenModel(
val redirectFlow: MutableSharedFlow<EXHRedirect> = MutableSharedFlow()
data class EXHRedirect(val mangaId: Long)
var dedupe: Boolean = true
// EXH <--
private data class CombineState(
@ -388,6 +398,7 @@ class MangaScreenModel(
screenModelScope.launchIO {
val manga = getMangaAndChapters.awaitManga(mangaId)
// SY -->
val chapters = (if (manga.source == MERGED_SOURCE_ID) getMergedChaptersByMangaId.await(mangaId, applyScanlatorFilter = true) else getMangaAndChapters.awaitChapters(mangaId, applyScanlatorFilter = true))
.toChapterListItems(manga, null)
@ -470,6 +481,76 @@ class MangaScreenModel(
}
}
/**
* Get the color of the manga cover by loading cover with ImageRequest directly from network.
*/
private suspend fun setPaletteColor(model: Any, method: ImageRequestType = ImageRequestType.Enqueue) {
if (model is ImageRequest && model.defined.sizeResolver != null) return
val imageRequestBuilder = if (model is ImageRequest) {
model.newBuilder()
} else {
ImageRequest.Builder(context).data(model)
}
.allowHardware(false)
val generatePalette: (Image) -> Unit = { image ->
val bitmap = image.asDrawable(context.resources).getBitmapOrNull()
if (bitmap != null) {
Palette.from(bitmap).generate { palette ->
screenModelScope.launchUI {
palette?.getBestColor()?.let { vibrantColor ->
when (model) {
is Manga -> model.asMangaCover().vibrantCoverColor = vibrantColor
is MangaCover -> model.vibrantCoverColor = vibrantColor
}
}
}
}
}
}
when (method) {
ImageRequestType.Enqueue -> {
context.imageLoader.enqueue(
imageRequestBuilder
.target(
onSuccess = generatePalette,
onError = {
// TODO: handle error
// val file = coverCache.getCoverFile(manga!!)
// if (file.exists()) {
// file.delete()
// setPaletteColor()
// }
},
)
.build()
)
}
ImageRequestType.Execute -> {
context.imageLoader.execute(imageRequestBuilder.build())
.image?.let { image ->
generatePalette(image)
}
}
ImageRequestType.ExecuteBlocking -> {
context.imageLoader.executeBlocking(imageRequestBuilder.build())
.image?.let { image ->
generatePalette(image)
}
}
ImageRequestType.IOContext -> {
withIOContext {
context.imageLoader.execute(imageRequestBuilder.build())
.image?.let { image ->
generatePalette(image)
}
}
}
}
}
// KMK -->
private suspend fun syncTrackers() {
if (!trackPreferences.autoSyncReadChapters().get()) return
@ -703,8 +784,8 @@ class MangaScreenModel(
mergedManga = networkToLocalManga.await(mergedManga)
getCategories.await(originalMangaId)
.let { it ->
setMangaCategories.await(mergedManga.id, it.map { it.id })
.let { categories ->
setMangaCategories.await(mergedManga.id, categories.map { it.id })
}
val originalMangaReference = MergedMangaReference(
@ -1796,7 +1877,9 @@ class MangaScreenModel(
val relatedMangaCollection: List<RelatedManga>? = null,
// KMK <--
) : State {
// KMK ->>
// KMK -->
val seedColor: Color? = MangaCover.vibrantCoverColorMap[manga.id]?.let { Color(it) }
/**
* a value of null will be treated as still loading, so if all searching were failed and won't update
* 'relatedMangaCollection` then we should return empty list
@ -1978,3 +2061,10 @@ sealed interface RelatedManga {
}
}
// KMK <--
enum class ImageRequestType {
IOContext,
Enqueue,
Execute,
ExecuteBlocking,
}

View file

@ -15,7 +15,7 @@ import java.io.InputStream
import java.time.Instant
/**
* Call before updating [Manga.thumbnail_url] to ensure old cover can be cleared from cache
* Call before updating [Manga.thumbnailUrl] to ensure old cover can be cleared from cache
*/
fun Manga.prepUpdateCover(coverCache: CoverCache, remoteManga: SManga, refreshSameUrl: Boolean): Manga {
// Never refresh covers if the new url is null, as the current url has possibly become invalid

View file

@ -1,29 +1,135 @@
package eu.kanade.tachiyomi.widget.materialdialogs
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.os.Build
import android.view.LayoutInflater
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService
import androidx.core.widget.doAfterTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.databinding.DialogStubTextinputBinding
import eu.kanade.tachiyomi.ui.manga.EditMangaDialogColors
fun MaterialAlertDialogBuilder.setTextInput(
hint: String? = null,
prefill: String? = null,
onTextChanged: (String) -> Unit,
): MaterialAlertDialogBuilder {
val binding = DialogStubTextinputBinding.inflate(LayoutInflater.from(context))
binding.textField.hint = hint
binding.textField.editText?.apply {
@Suppress("UnusedReceiverParameter")
fun MaterialAlertDialogBuilder.binding(context: Context): DialogStubTextinputBinding {
return DialogStubTextinputBinding.inflate(LayoutInflater.from(context))
}
fun AlertDialog.dismissDialog() {
if (isShowing) {
dismiss()
}
}
fun DialogStubTextinputBinding.setTitle(title: String): DialogStubTextinputBinding {
alertTitle.text = title
return this
}
fun DialogStubTextinputBinding.setPositiveButton(text: String, onClick: (String) -> Unit): DialogStubTextinputBinding {
positiveButton.text = text
positiveButton.setOnClickListener {
val textEdit = textField.editText
onClick(textEdit?.text?.toString() ?: "")
}
return this
}
fun DialogStubTextinputBinding.setNegativeButton(text: String, onClick: () -> Unit): DialogStubTextinputBinding {
negativeButton.text = text
negativeButton.setOnClickListener {
onClick()
}
return this
}
fun DialogStubTextinputBinding.setHint(hint: String? = null): DialogStubTextinputBinding {
textField.hint = hint
return this
}
fun DialogStubTextinputBinding.setTextEdit(prefill: String? = null): DialogStubTextinputBinding {
textField.editText?.apply {
setText(prefill, TextView.BufferType.EDITABLE)
doAfterTextChanged {
onTextChanged(it?.toString() ?: "")
}
post {
requestFocusFromTouch()
context.getSystemService<InputMethodManager>()?.showSoftInput(this, 0)
}
}
return setView(binding.root)
return this
}
fun DialogStubTextinputBinding.setColors(colors: EditMangaDialogColors): DialogStubTextinputBinding {
textField.boxStrokeColor = colors.iconColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
textField.cursorColor = ColorStateList.valueOf(colors.iconColor)
}
textField.editText?.apply {
setTextColor(colors.textColor)
highlightColor = colors.textHighlightColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
textSelectHandle?.let { drawable ->
drawable.setTint(colors.iconColor)
setTextSelectHandle(drawable)
}
textSelectHandleLeft?.let { drawable ->
drawable.setTint(colors.iconColor)
setTextSelectHandleLeft(drawable)
}
textSelectHandleRight?.let { drawable ->
drawable.setTint(colors.iconColor)
setTextSelectHandleRight(drawable)
}
}
}
alertTitle.setTextColor(colors.textColor)
positiveButton.setTextColor(colors.iconColor)
negativeButton.setTextColor(colors.iconColor)
root.background = RoundedCornerDrawable(color = colors.dialogBgColor)
return this
}
class RoundedCornerDrawable(val color: Int, private val cornerRadius: Float = 72f) : Drawable() {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
override fun draw(canvas: Canvas) {
val bounds = bounds
paint.color = color
canvas.drawRoundRect(
RectF(bounds.left.toFloat(), bounds.top.toFloat(), bounds.right.toFloat(), bounds.bottom.toFloat()),
cornerRadius,
cornerRadius,
paint,
)
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
}
@Deprecated("Deprecated in Java", ReplaceWith("PixelFormat.TRANSLUCENT", "android.graphics.PixelFormat"))
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
}
}

View file

@ -1,5 +1,6 @@
package exh.ui.metadata
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
@ -16,11 +17,14 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.materialkolor.DynamicMaterialTheme
import com.materialkolor.PaletteStyle
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.util.system.copyToClipboard
@ -33,7 +37,11 @@ import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.clickableNoIndication
import tachiyomi.presentation.core.util.plus
class MetadataViewScreen(private val mangaId: Long, private val sourceId: Long) : Screen() {
class MetadataViewScreen(
private val mangaId: Long,
private val sourceId: Long,
private val seedColor: Color? = null,
) : Screen() {
@Composable
override fun Content() {
@ -41,7 +49,9 @@ class MetadataViewScreen(private val mangaId: Long, private val sourceId: Long)
val navigator = LocalNavigator.currentOrThrow
val state by screenModel.state.collectAsState()
Scaffold(
@Composable
fun content() = Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = screenModel.manga.collectAsState().value?.title,
@ -96,5 +106,13 @@ class MetadataViewScreen(private val mangaId: Long, private val sourceId: Long)
}
}
}
DynamicMaterialTheme(
seedColor = seedColor ?: MaterialTheme.colorScheme.primary,
useDarkTheme = isSystemInDarkTheme(),
style = PaletteStyle.Vibrant,
animate = true,
content = { content() }
)
}
}

View file

@ -1,10 +1,13 @@
package exh.ui.metadata.adapters
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import eu.kanade.tachiyomi.R
@ -20,8 +23,15 @@ import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
@Composable
fun EHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
fun EHentaiDescription(
state: State.Success,
openMetadataViewer: () -> Unit,
search: (String) -> Unit,
) {
val context = LocalContext.current
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
val iconColor = MaterialTheme.colorScheme.secondary.toArgb()
val ratingBarSecondaryColor = MaterialTheme.colorScheme.outlineVariant.toArgb()
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { factoryContext ->
@ -40,20 +50,26 @@ fun EHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit, sea
}
?: meta.genre
?: context.stringResource(MR.strings.unknown)
binding.genre.setTextColor(textColor)
binding.visible.text = context.stringResource(SYMR.strings.is_visible, meta.visible ?: context.stringResource(MR.strings.unknown))
binding.visible.setTextColor(textColor)
binding.favorites.text = (meta.favorites ?: 0).toString()
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp)
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp, iconColor)
binding.favorites.setTextColor(textColor)
binding.uploader.text = meta.uploader ?: context.stringResource(MR.strings.unknown)
binding.uploader.setTextColor(textColor)
binding.size.text = MetadataUtil.humanReadableByteCount(meta.size ?: 0, true)
binding.size.bindDrawable(context, R.drawable.ic_outline_sd_card_24)
binding.size.bindDrawable(context, R.drawable.ic_outline_sd_card_24, iconColor)
binding.size.setTextColor(textColor)
val length = meta.length ?: 0
binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, length, length)
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24)
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor)
binding.pages.setTextColor(textColor)
val language = meta.language ?: context.stringResource(MR.strings.unknown)
binding.language.text = if (meta.translated == true) {
@ -61,13 +77,18 @@ fun EHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit, sea
} else {
language
}
binding.language.setTextColor(textColor)
val ratingFloat = meta.averageRating?.toFloat()
binding.ratingBar.rating = ratingFloat ?: 0F
@SuppressLint("SetTextI18n")
binding.rating.text = (ratingFloat ?: 0F).toString() + " - " + MetadataUIUtil.getRatingString(context, ratingFloat?.times(2))
binding.ratingBar.supportProgressTintList = ColorStateList.valueOf(iconColor)
binding.ratingBar.supportSecondaryProgressTintList = ColorStateList.valueOf(ratingBarSecondaryColor)
binding.rating.setTextColor(textColor)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
binding.moreInfo.setTextColor(textColor)
listOf(
binding.favorites,

View file

@ -2,8 +2,10 @@ package exh.ui.metadata.adapters
import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import eu.kanade.tachiyomi.R
@ -18,6 +20,8 @@ import tachiyomi.i18n.MR
@Composable
fun EightMusesDescription(state: State.Success, openMetadataViewer: () -> Unit) {
val context = LocalContext.current
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { factoryContext ->
@ -29,8 +33,10 @@ fun EightMusesDescription(state: State.Success, openMetadataViewer: () -> Unit)
val binding = DescriptionAdapter8mBinding.bind(it)
binding.title.text = meta.title ?: context.stringResource(MR.strings.unknown)
binding.title.setTextColor(textColor)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
binding.moreInfo.setTextColor(textColor)
binding.title.setOnLongClickListener {
context.copyToClipboard(
@ -39,6 +45,7 @@ fun EightMusesDescription(state: State.Success, openMetadataViewer: () -> Unit)
)
true
}
binding.title.setTextColor(textColor)
binding.moreInfo.setOnClickListener {
openMetadataViewer()

View file

@ -2,8 +2,10 @@ package exh.ui.metadata.adapters
import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import eu.kanade.tachiyomi.R
@ -18,6 +20,8 @@ import tachiyomi.i18n.sy.SYMR
@Composable
fun HBrowseDescription(state: State.Success, openMetadataViewer: () -> Unit) {
val context = LocalContext.current
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { factoryContext ->
@ -29,9 +33,11 @@ fun HBrowseDescription(state: State.Success, openMetadataViewer: () -> Unit) {
val binding = DescriptionAdapterHbBinding.bind(it)
binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, meta.length ?: 0, meta.length ?: 0)
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24)
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor)
binding.pages.setTextColor(textColor)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
binding.moreInfo.setTextColor(textColor)
binding.pages.setOnLongClickListener {
context.copyToClipboard(

View file

@ -1,10 +1,13 @@
package exh.ui.metadata.adapters
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible
@ -20,6 +23,9 @@ import kotlin.math.round
@Composable
fun MangaDexDescription(state: State.Success, openMetadataViewer: () -> Unit) {
val context = LocalContext.current
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
val ratingBarSecondaryColor = MaterialTheme.colorScheme.outlineVariant.toArgb()
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { factoryContext ->
@ -37,8 +43,12 @@ fun MangaDexDescription(state: State.Success, openMetadataViewer: () -> Unit) {
binding.rating.text = (round((ratingFloat ?: 0F) * 100.0) / 100.0).toString() + " - " + getRatingString(context, ratingFloat)
binding.rating.isVisible = ratingFloat != null
binding.ratingBar.isVisible = ratingFloat != null
binding.ratingBar.supportProgressTintList = ColorStateList.valueOf(iconColor)
binding.ratingBar.supportSecondaryProgressTintList = ColorStateList.valueOf(ratingBarSecondaryColor)
binding.rating.setTextColor(textColor)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
binding.moreInfo.setTextColor(textColor)
binding.rating.setOnLongClickListener {
context.copyToClipboard(

View file

@ -61,9 +61,13 @@ object MetadataUIUtil {
genreColor.color to context.stringResource(stringId)
}
fun TextView.bindDrawable(context: Context, @DrawableRes drawable: Int) {
fun TextView.bindDrawable(
context: Context,
@DrawableRes drawable: Int,
@ColorInt color: Int = context.getResourceColor(R.attr.colorAccent),
) {
ContextCompat.getDrawable(context, drawable)?.apply {
setTint(context.getResourceColor(R.attr.colorAccent))
setTint(color)
setBounds(0, 0, 20.dpToPx, 20.dpToPx)
setCompoundDrawables(this, null, null, null)
}

View file

@ -3,8 +3,10 @@ package exh.ui.metadata.adapters
import android.annotation.SuppressLint
import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import eu.kanade.tachiyomi.R
@ -25,6 +27,8 @@ import java.time.ZonedDateTime
@Composable
fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) {
val context = LocalContext.current
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { factoryContext ->
@ -45,11 +49,13 @@ fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) {
it.second
} ?: categoriesString ?: context.stringResource(MR.strings.unknown)
}
binding.genre.setTextColor(textColor)
meta.favoritesCount?.let {
if (it == 0L) return@let
binding.favorites.text = it.toString()
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp)
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp, iconColor)
binding.favorites.setTextColor(textColor)
}
binding.whenPosted.text = MetadataUtil.EX_DATE_FORMAT
@ -57,18 +63,22 @@ fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) {
ZonedDateTime
.ofInstant(Instant.ofEpochSecond(meta.uploadDate ?: 0), ZoneId.systemDefault())
)
binding.whenPosted.setTextColor(textColor)
binding.pages.text = context.pluralStringResource(
SYMR.plurals.num_pages,
meta.pageImageTypes.size,
meta.pageImageTypes.size,
)
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24)
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor)
binding.pages.setTextColor(textColor)
@SuppressLint("SetTextI18n")
binding.id.text = "#" + (meta.nhId ?: 0)
binding.id.setTextColor(textColor)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
binding.moreInfo.setTextColor(textColor)
listOf(
binding.favorites,

View file

@ -1,10 +1,13 @@
package exh.ui.metadata.adapters
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import eu.kanade.tachiyomi.R
@ -22,6 +25,9 @@ import kotlin.math.round
@Composable
fun PururinDescription(state: State.Success, openMetadataViewer: () -> Unit) {
val context = LocalContext.current
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
val ratingBarSecondaryColor = MaterialTheme.colorScheme.outlineVariant.toArgb()
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { factoryContext ->
@ -38,21 +44,29 @@ fun PururinDescription(state: State.Success, openMetadataViewer: () -> Unit) {
it.second
} ?: genre?.name ?: context.stringResource(MR.strings.unknown)
}
binding.genre.setTextColor(textColor)
binding.uploader.text = meta.uploaderDisp ?: meta.uploader.orEmpty()
binding.uploader.setTextColor(textColor)
binding.size.text = meta.fileSize ?: context.stringResource(MR.strings.unknown)
binding.size.bindDrawable(context, R.drawable.ic_outline_sd_card_24)
binding.size.bindDrawable(context, R.drawable.ic_outline_sd_card_24, iconColor)
binding.size.setTextColor(textColor)
binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, meta.pages ?: 0, meta.pages ?: 0)
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24)
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor)
binding.pages.setTextColor(textColor)
val ratingFloat = meta.averageRating?.toFloat()
binding.ratingBar.rating = ratingFloat ?: 0F
@SuppressLint("SetTextI18n")
binding.rating.text = (round((ratingFloat ?: 0F) * 100.0) / 100.0).toString() + " - " + MetadataUIUtil.getRatingString(context, ratingFloat?.times(2))
binding.ratingBar.supportProgressTintList = ColorStateList.valueOf(iconColor)
binding.ratingBar.supportSecondaryProgressTintList = ColorStateList.valueOf(ratingBarSecondaryColor)
binding.rating.setTextColor(textColor)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
binding.moreInfo.setTextColor(textColor)
listOf(
binding.genre,

View file

@ -1,10 +1,13 @@
package exh.ui.metadata.adapters
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import eu.kanade.tachiyomi.R
@ -23,6 +26,9 @@ import kotlin.math.round
@Composable
fun TsuminoDescription(state: State.Success, openMetadataViewer: () -> Unit) {
val context = LocalContext.current
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
val ratingBarSecondaryColor = MaterialTheme.colorScheme.outlineVariant.toArgb()
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { factoryContext ->
@ -37,22 +43,31 @@ fun TsuminoDescription(state: State.Success, openMetadataViewer: () -> Unit) {
binding.genre.setBackgroundColor(it.first)
it.second
} ?: meta.category ?: context.stringResource(MR.strings.unknown)
binding.genre.setTextColor(textColor)
binding.favorites.text = (meta.favorites ?: 0).toString()
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp)
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp, iconColor)
binding.favorites.setTextColor(textColor)
binding.whenPosted.text = TsuminoSearchMetadata.TSUMINO_DATE_FORMAT.format(Date(meta.uploadDate ?: 0))
binding.whenPosted.setTextColor(textColor)
binding.uploader.text = meta.uploader ?: context.stringResource(MR.strings.unknown)
binding.uploader.setTextColor(textColor)
binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, meta.length ?: 0, meta.length ?: 0)
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24)
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor)
binding.pages.setTextColor(textColor)
binding.ratingBar.rating = meta.averageRating ?: 0F
@SuppressLint("SetTextI18n")
binding.rating.text = (round((meta.averageRating ?: 0F) * 100.0) / 100.0).toString() + " - " + MetadataUIUtil.getRatingString(context, meta.averageRating?.times(2))
binding.ratingBar.supportProgressTintList = ColorStateList.valueOf(iconColor)
binding.ratingBar.supportSecondaryProgressTintList = ColorStateList.valueOf(ratingBarSecondaryColor)
binding.rating.setTextColor(textColor)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
binding.moreInfo.setTextColor(textColor)
listOf(
binding.favorites,

View file

@ -1,14 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp">
android:paddingVertical="8dp">
<TextView
android:id="@+id/alertTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_field"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText
android:layout_width="match_parent"
@ -16,4 +26,25 @@
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/positiveButton"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/negativeButton"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>

View file

@ -38,6 +38,7 @@
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:id="@+id/title_outline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -55,6 +56,7 @@
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:id="@+id/manga_author_outline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -72,6 +74,7 @@
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:id="@+id/manga_artist_outline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -89,6 +92,7 @@
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:id="@+id/thumbnail_url_outline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -105,6 +109,7 @@
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:id="@+id/manga_description_outline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"

View file

@ -35,6 +35,10 @@ class LibraryPreferences(
fun lastUpdatedTimestamp() = preferenceStore.getLong(Preference.appStateKey("library_update_last_timestamp"), 0L)
fun autoUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0)
fun coverRatios() = preferenceStore.getStringSet("pref_library_cover_ratios_key", emptySet())
fun coverColors() = preferenceStore.getStringSet("pref_library_cover_colors_key", emptySet())
fun autoUpdateDeviceRestrictions() = preferenceStore.getStringSet(
"library_update_restriction",
setOf(

View file

@ -2,6 +2,7 @@ package tachiyomi.domain.manga.model
import tachiyomi.domain.manga.interactor.GetCustomMangaInfo
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.ConcurrentHashMap
/**
* Contains the required data for MangaCoverFetcher
@ -22,11 +23,68 @@ data class MangaCover(
null
}
val url: String? = customThumbnailUrl ?: ogUrl
// SY <--
/**
* [vibrantCoverColor] is used to set the color theme in manga detail page.
* It contains color for all mangas, both in library or browsing.
*
* It reads/saves to a hashmap in [MangaCover.vibrantCoverColorMap] for multiple mangas.
*/
var vibrantCoverColor: Int?
get() = vibrantCoverColorMap[mangaId]
set(value) {
vibrantCoverColorMap[mangaId] = value
}
/**
* [dominantCoverColors] is used to set cover/text's color in Library (Favorite) grid view.
* It contains only color for in-library (favorite) mangas.
*
* It reads/saves to a hashmap in [MangaCover.coverColorMap].
*
* Format: <first: cover color, second: text color>.
*
* Set in *[MangaCoverMetadata.setRatioAndColors]* whenever browsing meets a favorite manga
* by loading from *[CoverCache]*.
*
* Get in *[CommonMangaItem.MangaCompactGridItem]*, *[CommonMangaItem.MangaComfortableGridItem]* and
* *[CommonMangaItem.MangaListItem]*
*/
@Suppress("KDocUnresolvedReference")
var dominantCoverColors: Pair<Int, Int>?
get() = coverColorMap[mangaId]
set(value) {
value ?: return
coverColorMap[mangaId] = value.first to value.second
}
var ratio: Float?
get() = coverRatioMap[mangaId]
set(value) {
value ?: return
coverRatioMap[mangaId] = value
}
companion object {
private val getCustomMangaInfo: GetCustomMangaInfo by injectLazy()
/**
* [vibrantCoverColorMap] store color generated while browsing library.
* It always empty at beginning each time app starts, then add more color while browsing.
*/
val vibrantCoverColorMap: HashMap<Long, Int?> = hashMapOf()
/**
* [coverColorMap] stores favorite manga's cover & text's color as a joined string in Prefs.
* They will be loaded each time *[App]* is initialized with *[MangaCoverMetadata.load]*.
*
* They will be saved back when *[MainActivity.onPause]* is triggered.
*/
@Suppress("KDocUnresolvedReference")
var coverColorMap = ConcurrentHashMap<Long, Pair<Int, Int>>()
var coverRatioMap = ConcurrentHashMap<Long, Float>()
}
// SY <--
}
fun Manga.asMangaCover(): MangaCover {

View file

@ -10,6 +10,8 @@ sqlite = "2.4.0"
voyager = "1.0.0"
detekt = "1.23.6"
detektCompose = "0.3.12"
paletteKtxVersion = "1.0.0"
materialKolorVersion = "1.6.1"
[libraries]
desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
@ -68,6 +70,8 @@ insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.2.0"
compose-webview = "io.github.kevinnzou:compose-webview:0.33.6"
compose-grid = "io.woong.compose.grid:grid:1.2.2"
palette-ktx = { group = "androidx.palette", name = "palette-ktx", version.ref = "paletteKtxVersion" }
material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolorVersion" }
swipe = "me.saket.swipe:swipe:1.3.0"

View file

@ -240,6 +240,10 @@
<!-- "Today" instead of "2023-12-31" -->
<string name="pref_relative_format_summary">\"%1$s\" instead of \"%2$s\"</string>
<string name="pref_date_format">Date format</string>
<string name="pref_details_page_theme">Details page</string>
<string name="pref_details_page_theme_cover_based">Details page theme based on cover</string>
<string name="pref_theme_cover_based_style">Theme style</string>
<string name="pref_theme_cover_based_animate">Theme animate</string>
<string name="pref_manage_notifications">Manage notifications</string>
<string name="pref_app_language">App language</string>