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.swipe)
implementation(libs.compose.webview) implementation(libs.compose.webview)
implementation(libs.compose.grid) implementation(libs.compose.grid)
implementation(libs.palette.ktx)
implementation(libs.material.kolor)
implementation(libs.google.api.services.drive) implementation(libs.google.api.services.drive)
implementation(libs.google.api.client.oauth) implementation(libs.google.api.client.oauth)

View file

@ -1,6 +1,7 @@
package eu.kanade.domain.ui package eu.kanade.domain.ui
import android.os.Build import android.os.Build
import com.materialkolor.PaletteStyle
import eu.kanade.domain.ui.model.AppTheme import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.domain.ui.model.TabletUiMode import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode import eu.kanade.domain.ui.model.ThemeMode
@ -28,6 +29,12 @@ class UiPreferences(
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false) 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 relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
fun dateFormat() = preferenceStore.getString("app_date_format", "") 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.materialkolor.ktx.blend
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
@ -129,6 +130,7 @@ fun MangaInfoBox(
brush = Brush.verticalGradient(colors = backdropGradientColors), brush = Brush.verticalGradient(colors = backdropGradientColors),
) )
} }
.background(MaterialTheme.colorScheme.inversePrimary.copy(alpha = 0.2f))
.blur(4.dp) .blur(4.dp)
.alpha(0.2f), .alpha(0.2f),
) )
@ -241,7 +243,7 @@ fun MangaActionRow(
MangaActionButton( MangaActionButton(
title = stringResource(MR.strings.action_web_view), title = stringResource(MR.strings.action_web_view),
icon = Icons.Outlined.Public, icon = Icons.Outlined.Public,
color = defaultActionButtonColor, color = MaterialTheme.colorScheme.primary,
onClick = onWebViewClicked, onClick = onWebViewClicked,
onLongClick = onWebViewLongClicked, onLongClick = onWebViewLongClicked,
) )
@ -251,7 +253,7 @@ fun MangaActionRow(
MangaActionButton( MangaActionButton(
title = stringResource(SYMR.strings.merge), title = stringResource(SYMR.strings.merge),
icon = Icons.AutoMirrored.Outlined.CallMerge, icon = Icons.AutoMirrored.Outlined.CallMerge,
color = defaultActionButtonColor, color = MaterialTheme.colorScheme.primary,
onClick = onMergeClicked, onClick = onMergeClicked,
) )
} }

View file

@ -6,10 +6,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow 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.UiPreferences
import eu.kanade.domain.ui.model.TabletUiMode import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode import eu.kanade.domain.ui.model.ThemeMode
@ -42,6 +45,7 @@ object SettingsAppearanceScreen : SearchableSettings {
return listOf( return listOf(
getThemeGroup(uiPreferences = uiPreferences), getThemeGroup(uiPreferences = uiPreferences),
getDetailsPageThemeGroup(uiPreferences = uiPreferences),
getDisplayGroup(uiPreferences = uiPreferences), getDisplayGroup(uiPreferences = uiPreferences),
// SY --> // SY -->
getNavbarGroup(uiPreferences = uiPreferences), 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 @Composable
private fun getDisplayGroup( private fun getDisplayGroup(
uiPreferences: UiPreferences, 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.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate 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.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.animatorDurationScale
@ -167,6 +168,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get()) setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
MangaCoverMetadata.load()
// Updates widget update // Updates widget update
with(WidgetManager(Injekt.get(), Injekt.get())) { with(WidgetManager(Injekt.get(), Injekt.get())) {
init(ProcessLifecycleOwner.get().lifecycleScope) init(ProcessLifecycleOwner.get().lifecycleScope)

View file

@ -12,16 +12,22 @@ import coil3.fetch.SourceFetchResult
import coil3.getOrDefault import coil3.getOrDefault
import coil3.request.Options import coil3.request.Options
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER_KEY import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER_KEY
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.online.HttpSource 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 logcat.LogPriority
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Call import okhttp3.Call
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.http.HTTP_NOT_MODIFIED import okhttp3.internal.http.HTTP_NOT_MODIFIED
import okio.BufferedSource
import okio.FileSystem import okio.FileSystem
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import okio.Source import okio.Source
@ -31,7 +37,10 @@ import okio.source
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.manga.model.asMangaCover
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -48,7 +57,8 @@ import java.io.IOException
*/ */
@Suppress("LongParameterList") @Suppress("LongParameterList")
class MangaCoverFetcher( class MangaCoverFetcher(
private val url: String?, private val mangaCover: MangaCover,
private val url: String? = mangaCover.url,
private val isLibraryManga: Boolean, private val isLibraryManga: Boolean,
private val options: Options, private val options: Options,
private val coverFileLazy: Lazy<File?>, private val coverFileLazy: Lazy<File?>,
@ -59,9 +69,15 @@ class MangaCoverFetcher(
private val imageLoader: ImageLoader, private val imageLoader: ImageLoader,
) : Fetcher { ) : Fetcher {
private val uiPreferences: UiPreferences = Injekt.get()
private val fileScope = CoroutineScope(Job() + Dispatchers.IO)
private val diskCacheKey: String private val diskCacheKey: String
get() = diskCacheKeyLazy.value get() = diskCacheKeyLazy.value
/**
* Called each time a cover is displayed
*/
override suspend fun fetch(): FetchResult { override suspend fun fetch(): FetchResult {
// Use custom cover if exists // Use custom cover if exists
val useCustomCover = options.extras.getOrDefault(USE_CUSTOM_COVER_KEY) val useCustomCover = options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)
@ -83,6 +99,7 @@ class MangaCoverFetcher(
} }
private fun fileLoader(file: File): FetchResult { private fun fileLoader(file: File): FetchResult {
setRatioAndColorsInScope(mangaCover, ogFile = file)
return SourceFetchResult( return SourceFetchResult(
source = ImageSource( source = ImageSource(
file = file.toOkioPath(), file = file.toOkioPath(),
@ -95,6 +112,7 @@ class MangaCoverFetcher(
} }
private fun fileUriLoader(uri: String): FetchResult { private fun fileUriLoader(uri: String): FetchResult {
setRatioAndColorsInScope(mangaCover)
val source = UniFile.fromUri(options.context, uri.toUri())!! val source = UniFile.fromUri(options.context, uri.toUri())!!
.openInputStream() .openInputStream()
.source() .source()
@ -128,6 +146,7 @@ class MangaCoverFetcher(
} }
// Read from snapshot // Read from snapshot
setRatioAndColorsInScope(mangaCover, bufferedSource = snapshot.toImageSource().source())
return SourceFetchResult( return SourceFetchResult(
source = snapshot.toImageSource(), source = snapshot.toImageSource(),
mimeType = "image/*", mimeType = "image/*",
@ -148,6 +167,7 @@ class MangaCoverFetcher(
// Read from disk cache // Read from disk cache
snapshot = writeToDiskCache(response) snapshot = writeToDiskCache(response)
if (snapshot != null) { if (snapshot != null) {
setRatioAndColorsInScope(mangaCover, bufferedSource = snapshot.toImageSource().source())
return SourceFetchResult( return SourceFetchResult(
source = snapshot.toImageSource(), source = snapshot.toImageSource(),
mimeType = "image/*", 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 // Read from response if cache is unused or unusable
return SourceFetchResult( return SourceFetchResult(
source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM), 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 { private enum class Type {
File, File,
URI, URI,
@ -308,7 +356,7 @@ class MangaCoverFetcher(
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher { override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
return MangaCoverFetcher( return MangaCoverFetcher(
url = data.thumbnailUrl, mangaCover = data.asMangaCover(),
isLibraryManga = data.favorite, isLibraryManga = data.favorite,
options = options, options = options,
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) }, coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) },
@ -330,7 +378,7 @@ class MangaCoverFetcher(
override fun create(data: MangaCover, options: Options, imageLoader: ImageLoader): Fetcher { override fun create(data: MangaCover, options: Options, imageLoader: ImageLoader): Fetcher {
return MangaCoverFetcher( return MangaCoverFetcher(
url = data.url, mangaCover = data,
isLibraryManga = data.isMangaFavorite, isLibraryManga = data.isMangaFavorite,
options = options, options = options,
coverFileLazy = lazy { coverCache.getCoverFile(data.url) }, 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 package eu.kanade.tachiyomi.data.coil
import androidx.palette.graphics.Palette
import coil3.Extras import coil3.Extras
import coil3.getExtra import coil3.getExtra
import coil3.request.ImageRequest import coil3.request.ImageRequest
@ -42,3 +43,42 @@ val Options.customDecoder: Boolean
get() = getExtra(customDecoderKey) get() = getExtra(customDecoderKey)
private val customDecoderKey = Extras.Key(default = false) 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.manga.MangaScreen
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.ui.more.OnboardingScreen 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.dpToPx
import eu.kanade.tachiyomi.util.system.isDevFlavor import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
@ -337,6 +338,11 @@ class MainActivity : BaseActivity() {
// SY --> // SY -->
} }
override fun onPause() {
super.onPause()
MangaCoverMetadata.savePrefs()
}
override fun onProvideAssistContent(outContent: AssistContent) { override fun onProvideAssistContent(outContent: AssistContent) {
super.onProvideAssistContent(outContent) super.onProvideAssistContent(outContent)
when (val screen = navigator?.lastItem) { when (val screen = navigator?.lastItem) {

View file

@ -1,13 +1,22 @@
package eu.kanade.tachiyomi.ui.manga package eu.kanade.tachiyomi.ui.manga
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.os.Build
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter 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.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -17,6 +26,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.children 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.source.model.SManga
import eu.kanade.tachiyomi.util.lang.chop import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput import eu.kanade.tachiyomi.widget.materialdialogs.binding
import exh.ui.metadata.adapters.MetadataUIUtil.getResourceColor 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.dropBlank
import exh.util.trimOrNull import exh.util.trimOrNull
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -42,6 +57,7 @@ import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import timber.log.Timber
@Composable @Composable
fun EditMangaDialog( fun EditMangaDialog(
@ -61,6 +77,18 @@ fun EditMangaDialog(
var binding by remember { var binding by remember {
mutableStateOf<EditMangaDialogBinding?>(null) 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( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
confirmButton = { confirmButton = {
@ -109,7 +137,13 @@ fun EditMangaDialog(
EditMangaDialogBinding.inflate(LayoutInflater.from(factoryContext)) EditMangaDialogBinding.inflate(LayoutInflater.from(factoryContext))
.also { binding = it } .also { binding = it }
.apply { .apply {
onViewCreated(manga, factoryContext, this, scope) onViewCreated(
manga,
factoryContext,
this,
scope,
colors,
)
} }
.root .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) loadCover(manga, binding)
val statusAdapter: ArrayAdapter<String> = ArrayAdapter( val statusAdapter = SpinnerAdapter(
context, context,
android.R.layout.simple_spinner_dropdown_item, android.R.layout.simple_spinner_dropdown_item,
listOf( listOf(
@ -135,6 +188,7 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
MR.strings.cancelled, MR.strings.cancelled,
MR.strings.on_hiatus, MR.strings.on_hiatus,
).map { context.stringResource(it) }, ).map { context.stringResource(it) },
colors,
) )
binding.status.adapter = statusAdapter 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.isLocal()) {
if (manga.title != manga.url) { if (manga.title != manga.url) {
binding.title.setText(manga.title) 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.mangaArtist.setText(manga.artist.orEmpty())
binding.thumbnailUrl.setText(manga.thumbnailUrl.orEmpty()) binding.thumbnailUrl.setText(manga.thumbnailUrl.orEmpty())
binding.mangaDescription.setText(manga.description.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 { } else {
if (manga.title != manga.ogTitle) { if (manga.title != manga.ogTitle) {
binding.title.append(manga.title) binding.title.append(manga.title)
@ -180,7 +237,7 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
if (manga.description != manga.ogDescription) { if (manga.description != manga.ogDescription) {
binding.mangaDescription.append(manga.description.orEmpty()) 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) 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 = binding.mangaDescription.hint =
context.stringResource( context.stringResource(
SYMR.strings.description_hint, 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 = binding.thumbnailUrl.hint =
context.stringResource( context.stringResource(
SYMR.strings.thumbnail_url_hint, SYMR.strings.thumbnail_url_hint,
manga.ogThumbnailUrl?.let { manga.ogThumbnailUrl?.let {
it.chop(40) + if (it.length > 46) "." + it.substringAfterLast(".").chop(6) else "" it.chop(40) + if (it.length > 46) "." + it.substringAfterLast(".").chop(6) else ""
} ?: "" } ?: "",
) )
} }
binding.mangaGenresTags.clearFocus() binding.mangaGenresTags.clearFocus()
binding.resetTags.setOnClickListener { resetTags(manga, binding, scope) } listOf(
binding.resetInfo.setOnClickListener { resetInfo(manga, binding, scope) } 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()) { if (manga.genre.isNullOrEmpty() || manga.isLocal()) {
binding.mangaGenresTags.setChips(emptyList(), scope) binding.mangaGenresTags.setChips(emptyList(), scope, colors)
} else { } 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.title.setText("")
binding.mangaAuthor.setText("") binding.mangaAuthor.setText("")
binding.mangaArtist.setText("") binding.mangaArtist.setText("")
binding.thumbnailUrl.setText("") binding.thumbnailUrl.setText("")
binding.mangaDescription.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() 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 -> items.asSequence().map { item ->
Chip(context).apply { Chip(context).apply {
text = item text = item
setTextColor(colors.tagTextColor)
isCloseIconVisible = true isCloseIconVisible = true
closeIcon?.setTint(context.getResourceColor(R.attr.colorAccent)) closeIcon?.setTint(colors.iconColor)
setOnCloseIconClickListener { setOnCloseIconClickListener {
removeView(this) removeView(this)
} }
chipBackgroundColor = colorStateList
} }
}.forEach { }.forEach {
addView(it) addView(it)
} }
val addTagChip = Chip(context).apply { 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 { chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_add_24dp)?.apply {
isChipIconVisible = true isChipIconVisible = true
setTint(context.getResourceColor(R.attr.colorAccent)) setTint(colors.iconColor)
} }
chipBackgroundColor = colorStateList
setOnClickListener { setOnClickListener {
var newTag: String? = null var dialog: AlertDialog? = null
MaterialAlertDialogBuilder(context)
val builder = MaterialAlertDialogBuilder(context)
val binding = builder.binding(context)
.setTitle(SYMR.strings.add_tag.getString(context)) .setTitle(SYMR.strings.add_tag.getString(context))
.setTextInput { .setPositiveButton(MR.strings.action_ok.getString(context)) {
newTag = it.trimOrNull() dialog?.dismissDialog()
val newTag = it.trimOrNull()
if (newTag != null) setChips(items + listOfNotNull(newTag), scope, colors)
} }
.setPositiveButton(MR.strings.action_ok.getString(context)) { _, _ -> .setNegativeButton(MR.strings.action_cancel.getString(context)) {
if (newTag != null) setChips(items + listOfNotNull(newTag), scope) dialog?.dismissDialog()
} }
.setNegativeButton(MR.strings.action_cancel.getString(context), null) .setTextEdit()
.show() .setColors(colors)
dialog = builder.create()
dialog.setView(binding.root)
dialog.show()
} }
} }
addView(addTagChip) addView(addTagChip)
@ -277,3 +419,48 @@ private fun ChipGroup.getTextStrings(): List<String> = children.mapNotNull {
null null
} }
}.toList() }.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 android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -12,8 +14,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback 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.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.hasCustomCover
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.components.NavigatorAdaptiveSheet import eu.kanade.presentation.components.NavigatorAdaptiveSheet
import eu.kanade.presentation.manga.ChapterSettingsDialog import eu.kanade.presentation.manga.ChapterSettingsDialog
@ -116,10 +123,7 @@ class MangaScreen(
return return
} }
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val scope = rememberCoroutineScope()
val screenModel = val screenModel =
rememberScreenModel { MangaScreenModel(context, mangaId, fromSource, smartSearchConfig != null) } rememberScreenModel { MangaScreenModel(context, mangaId, fromSource, smartSearchConfig != null) }
@ -131,6 +135,40 @@ class MangaScreen(
} }
val successState = state as MangaScreenModel.State.Success 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 } val isHttpSource = remember { successState.source is HttpSource }
// KMK --> // KMK -->
@ -162,7 +200,6 @@ class MangaScreen(
.launchIn(this) .launchIn(this)
} }
// SY <-- // SY <--
MangaScreen( MangaScreen(
state = successState, state = successState,
snackbarHostState = screenModel.snackbarHostState, snackbarHostState = screenModel.snackbarHostState,
@ -229,8 +266,8 @@ class MangaScreen(
onMergedSettingsClicked = screenModel::showEditMergedSettingsDialog, onMergedSettingsClicked = screenModel::showEditMergedSettingsDialog,
onMergeClicked = { openSmartSearch(navigator, successState.manga) }, onMergeClicked = { openSmartSearch(navigator, successState.manga) },
onMergeWithAnotherClicked = { mergeWithAnother(navigator, context, successState.manga, screenModel::smartSearchMerge) }, onMergeWithAnotherClicked = { mergeWithAnother(navigator, context, successState.manga, screenModel::smartSearchMerge) },
onOpenPagePreview = { onOpenPagePreview = { page ->
openPagePreview(context, successState.chapters.minByOrNull { it.chapter.sourceOrder }?.chapter, it) openPagePreview(context, successState.chapters.minByOrNull { it.chapter.sourceOrder }?.chapter, page)
}, },
onMorePreviewsClicked = { openMorePagePreviews(navigator, successState.manga) }, onMorePreviewsClicked = { openMorePagePreviews(navigator, successState.manga) },
// SY <-- // SY <--
@ -289,6 +326,7 @@ class MangaScreen(
}, },
) )
} }
is MangaScreenModel.Dialog.DeleteChapters -> { is MangaScreenModel.Dialog.DeleteChapters -> {
DeleteChaptersDialog( DeleteChaptersDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -311,6 +349,7 @@ class MangaScreen(
}, },
) )
} }
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog( MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
manga = successState.manga, manga = successState.manga,
@ -324,6 +363,7 @@ class MangaScreen(
scanlatorFilterActive = successState.scanlatorFilterActive, scanlatorFilterActive = successState.scanlatorFilterActive,
onScanlatorFilterClicked = { showScanlatorsDialog = true }, onScanlatorFilterClicked = { showScanlatorsDialog = true },
) )
MangaScreenModel.Dialog.TrackSheet -> { MangaScreenModel.Dialog.TrackSheet -> {
NavigatorAdaptiveSheet( NavigatorAdaptiveSheet(
screen = TrackInfoDialogHomeScreen( screen = TrackInfoDialogHomeScreen(
@ -335,6 +375,7 @@ class MangaScreen(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
) )
} }
MangaScreenModel.Dialog.FullCover -> { MangaScreenModel.Dialog.FullCover -> {
val sm = rememberScreenModel { MangaCoverScreenModel(successState.manga.id) } val sm = rememberScreenModel { MangaCoverScreenModel(successState.manga.id) }
val manga by sm.state.collectAsState() val manga by sm.state.collectAsState()
@ -361,6 +402,7 @@ class MangaScreen(
LoadingScreen(Modifier.systemBarsPadding()) LoadingScreen(Modifier.systemBarsPadding())
} }
} }
is MangaScreenModel.Dialog.SetFetchInterval -> { is MangaScreenModel.Dialog.SetFetchInterval -> {
SetIntervalDialog( SetIntervalDialog(
interval = dialog.manga.fetchInterval, interval = dialog.manga.fetchInterval,
@ -378,6 +420,7 @@ class MangaScreen(
onPositiveClick = screenModel::updateMangaInfo, onPositiveClick = screenModel::updateMangaInfo,
) )
} }
is MangaScreenModel.Dialog.EditMergedSettings -> { is MangaScreenModel.Dialog.EditMergedSettings -> {
EditMergedSettingsDialog( EditMergedSettingsDialog(
mergedData = dialog.mergedData, mergedData = dialog.mergedData,
@ -402,14 +445,19 @@ class MangaScreen(
when (bulkFavoriteState.dialog) { when (bulkFavoriteState.dialog) {
is BulkFavoriteScreenModel.Dialog.AddDuplicateManga -> is BulkFavoriteScreenModel.Dialog.AddDuplicateManga ->
AddDuplicateMangaDialog(bulkFavoriteScreenModel) AddDuplicateMangaDialog(bulkFavoriteScreenModel)
is BulkFavoriteScreenModel.Dialog.RemoveManga -> is BulkFavoriteScreenModel.Dialog.RemoveManga ->
RemoveMangaDialog(bulkFavoriteScreenModel) RemoveMangaDialog(bulkFavoriteScreenModel)
is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory -> is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory ->
ChangeMangaCategoryDialog(bulkFavoriteScreenModel) ChangeMangaCategoryDialog(bulkFavoriteScreenModel)
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory -> is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory ->
ChangeMangasCategoryDialog(bulkFavoriteScreenModel) ChangeMangasCategoryDialog(bulkFavoriteScreenModel)
is BulkFavoriteScreenModel.Dialog.AllowDuplicate -> is BulkFavoriteScreenModel.Dialog.AllowDuplicate ->
AllowDuplicateDialog(bulkFavoriteScreenModel) AllowDuplicateDialog(bulkFavoriteScreenModel)
else -> {} else -> {}
} }
// KMK <-- // KMK <--
@ -539,8 +587,8 @@ class MangaScreen(
// SY <-- // SY <--
} }
private fun openMetadataViewer(navigator: Navigator, manga: Manga) { private fun openMetadataViewer(navigator: Navigator, manga: Manga, seedColor: Color? = null) {
navigator.push(MetadataViewScreen(manga.id, manga.source)) navigator.push(MetadataViewScreen(manga.id, manga.source, seedColor))
} }
private fun openMergedMangaWebview(context: Context, navigator: Navigator, mergedMangaData: MergedMangaData) { 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.getValue
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.palette.graphics.Palette
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope 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.preference.asState
import eu.kanade.core.util.addOrRemove import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators 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.manga.track.TrackItem
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.chapter.getNextUnread 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.removeCovers
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.system.getBitmapOrNull
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.debug.DebugToggles import exh.debug.DebugToggles
import exh.eh.EHentaiUpdateHelper import exh.eh.EHentaiUpdateHelper
@ -100,6 +109,7 @@ import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.preference.mapAsCheckboxState import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable 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.withIOContext
import tachiyomi.core.common.util.lang.withNonCancellableContext import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.lang.withUIContext 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.interactor.UpdateMergedSettings
import tachiyomi.domain.manga.model.CustomMangaInfo import tachiyomi.domain.manga.model.CustomMangaInfo
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate
import tachiyomi.domain.manga.model.MergedMangaReference import tachiyomi.domain.manga.model.MergedMangaReference
import tachiyomi.domain.manga.model.applyFilter import tachiyomi.domain.manga.model.applyFilter
import tachiyomi.domain.manga.model.asMangaCover
import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@ -242,8 +254,6 @@ class MangaScreenModel(
val redirectFlow: MutableSharedFlow<EXHRedirect> = MutableSharedFlow() val redirectFlow: MutableSharedFlow<EXHRedirect> = MutableSharedFlow()
data class EXHRedirect(val mangaId: Long) data class EXHRedirect(val mangaId: Long)
var dedupe: Boolean = true
// EXH <-- // EXH <--
private data class CombineState( private data class CombineState(
@ -388,6 +398,7 @@ class MangaScreenModel(
screenModelScope.launchIO { screenModelScope.launchIO {
val manga = getMangaAndChapters.awaitManga(mangaId) val manga = getMangaAndChapters.awaitManga(mangaId)
// SY --> // SY -->
val chapters = (if (manga.source == MERGED_SOURCE_ID) getMergedChaptersByMangaId.await(mangaId, applyScanlatorFilter = true) else getMangaAndChapters.awaitChapters(mangaId, applyScanlatorFilter = true)) val chapters = (if (manga.source == MERGED_SOURCE_ID) getMergedChaptersByMangaId.await(mangaId, applyScanlatorFilter = true) else getMangaAndChapters.awaitChapters(mangaId, applyScanlatorFilter = true))
.toChapterListItems(manga, null) .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 --> // KMK -->
private suspend fun syncTrackers() { private suspend fun syncTrackers() {
if (!trackPreferences.autoSyncReadChapters().get()) return if (!trackPreferences.autoSyncReadChapters().get()) return
@ -703,8 +784,8 @@ class MangaScreenModel(
mergedManga = networkToLocalManga.await(mergedManga) mergedManga = networkToLocalManga.await(mergedManga)
getCategories.await(originalMangaId) getCategories.await(originalMangaId)
.let { it -> .let { categories ->
setMangaCategories.await(mergedManga.id, it.map { it.id }) setMangaCategories.await(mergedManga.id, categories.map { it.id })
} }
val originalMangaReference = MergedMangaReference( val originalMangaReference = MergedMangaReference(
@ -1796,7 +1877,9 @@ class MangaScreenModel(
val relatedMangaCollection: List<RelatedManga>? = null, val relatedMangaCollection: List<RelatedManga>? = null,
// KMK <-- // KMK <--
) : State { ) : 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 * 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 * 'relatedMangaCollection` then we should return empty list
@ -1978,3 +2061,10 @@ sealed interface RelatedManga {
} }
} }
// KMK <-- // KMK <--
enum class ImageRequestType {
IOContext,
Enqueue,
Execute,
ExecuteBlocking,
}

View file

@ -15,7 +15,7 @@ import java.io.InputStream
import java.time.Instant 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 { 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 // 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 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.LayoutInflater
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.widget.doAfterTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.databinding.DialogStubTextinputBinding import eu.kanade.tachiyomi.databinding.DialogStubTextinputBinding
import eu.kanade.tachiyomi.ui.manga.EditMangaDialogColors
fun MaterialAlertDialogBuilder.setTextInput( @Suppress("UnusedReceiverParameter")
hint: String? = null, fun MaterialAlertDialogBuilder.binding(context: Context): DialogStubTextinputBinding {
prefill: String? = null, return DialogStubTextinputBinding.inflate(LayoutInflater.from(context))
onTextChanged: (String) -> Unit, }
): MaterialAlertDialogBuilder {
val binding = DialogStubTextinputBinding.inflate(LayoutInflater.from(context)) fun AlertDialog.dismissDialog() {
binding.textField.hint = hint if (isShowing) {
binding.textField.editText?.apply { 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) setText(prefill, TextView.BufferType.EDITABLE)
doAfterTextChanged {
onTextChanged(it?.toString() ?: "")
}
post { post {
requestFocusFromTouch() requestFocusFromTouch()
context.getSystemService<InputMethodManager>()?.showSoftInput(this, 0) 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 package exh.ui.metadata
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
@ -16,11 +17,14 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.materialkolor.DynamicMaterialTheme
import com.materialkolor.PaletteStyle
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.util.system.copyToClipboard 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.clickableNoIndication
import tachiyomi.presentation.core.util.plus 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 @Composable
override fun Content() { override fun Content() {
@ -41,7 +49,9 @@ class MetadataViewScreen(private val mangaId: Long, private val sourceId: Long)
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
Scaffold(
@Composable
fun content() = Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
title = screenModel.manga.collectAsState().value?.title, 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 package exh.ui.metadata.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -20,8 +23,15 @@ import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
@Composable @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 context = LocalContext.current
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
val iconColor = MaterialTheme.colorScheme.secondary.toArgb()
val ratingBarSecondaryColor = MaterialTheme.colorScheme.outlineVariant.toArgb()
AndroidView( AndroidView(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
factory = { factoryContext -> factory = { factoryContext ->
@ -40,20 +50,26 @@ fun EHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit, sea
} }
?: meta.genre ?: meta.genre
?: context.stringResource(MR.strings.unknown) ?: 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.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.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.text = meta.uploader ?: context.stringResource(MR.strings.unknown)
binding.uploader.setTextColor(textColor)
binding.size.text = MetadataUtil.humanReadableByteCount(meta.size ?: 0, true) 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 val length = meta.length ?: 0
binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, length, length) 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) val language = meta.language ?: context.stringResource(MR.strings.unknown)
binding.language.text = if (meta.translated == true) { binding.language.text = if (meta.translated == true) {
@ -61,13 +77,18 @@ fun EHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit, sea
} else { } else {
language language
} }
binding.language.setTextColor(textColor)
val ratingFloat = meta.averageRating?.toFloat() val ratingFloat = meta.averageRating?.toFloat()
binding.ratingBar.rating = ratingFloat ?: 0F binding.ratingBar.rating = ratingFloat ?: 0F
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
binding.rating.text = (ratingFloat ?: 0F).toString() + " - " + MetadataUIUtil.getRatingString(context, ratingFloat?.times(2)) 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( listOf(
binding.favorites, binding.favorites,

View file

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

View file

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

View file

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

View file

@ -61,9 +61,13 @@ object MetadataUIUtil {
genreColor.color to context.stringResource(stringId) 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 { ContextCompat.getDrawable(context, drawable)?.apply {
setTint(context.getResourceColor(R.attr.colorAccent)) setTint(color)
setBounds(0, 0, 20.dpToPx, 20.dpToPx) setBounds(0, 0, 20.dpToPx, 20.dpToPx)
setCompoundDrawables(this, null, null, null) setCompoundDrawables(this, null, null, null)
} }

View file

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

View file

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

View file

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

View file

@ -1,14 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingHorizontal="24dp" 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 <com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_field" android:id="@+id/text_field"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText <eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
@ -16,4 +26,25 @@
</com.google.android.material.textfield.TextInputLayout> </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 <com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:id="@+id/title_outline"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
@ -55,6 +56,7 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:id="@+id/manga_author_outline"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
@ -72,6 +74,7 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:id="@+id/manga_artist_outline"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
@ -89,6 +92,7 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:id="@+id/thumbnail_url_outline"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
@ -105,6 +109,7 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:id="@+id/manga_description_outline"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"

View file

@ -35,6 +35,10 @@ class LibraryPreferences(
fun lastUpdatedTimestamp() = preferenceStore.getLong(Preference.appStateKey("library_update_last_timestamp"), 0L) fun lastUpdatedTimestamp() = preferenceStore.getLong(Preference.appStateKey("library_update_last_timestamp"), 0L)
fun autoUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0) 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( fun autoUpdateDeviceRestrictions() = preferenceStore.getStringSet(
"library_update_restriction", "library_update_restriction",
setOf( setOf(

View file

@ -2,6 +2,7 @@ package tachiyomi.domain.manga.model
import tachiyomi.domain.manga.interactor.GetCustomMangaInfo import tachiyomi.domain.manga.interactor.GetCustomMangaInfo
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.concurrent.ConcurrentHashMap
/** /**
* Contains the required data for MangaCoverFetcher * Contains the required data for MangaCoverFetcher
@ -22,11 +23,68 @@ data class MangaCover(
null null
} }
val url: String? = customThumbnailUrl ?: ogUrl 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 { companion object {
private val getCustomMangaInfo: GetCustomMangaInfo by injectLazy() 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 { fun Manga.asMangaCover(): MangaCover {

View file

@ -10,6 +10,8 @@ sqlite = "2.4.0"
voyager = "1.0.0" voyager = "1.0.0"
detekt = "1.23.6" detekt = "1.23.6"
detektCompose = "0.3.12" detektCompose = "0.3.12"
paletteKtxVersion = "1.0.0"
materialKolorVersion = "1.6.1"
[libraries] [libraries]
desugar = "com.android.tools:desugar_jdk_libs:2.0.4" 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-materialmotion = "io.github.fornewid:material-motion-compose-core:1.2.0"
compose-webview = "io.github.kevinnzou:compose-webview:0.33.6" compose-webview = "io.github.kevinnzou:compose-webview:0.33.6"
compose-grid = "io.woong.compose.grid:grid:1.2.2" 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" swipe = "me.saket.swipe:swipe:1.3.0"

View file

@ -240,6 +240,10 @@
<!-- "Today" instead of "2023-12-31" --> <!-- "Today" instead of "2023-12-31" -->
<string name="pref_relative_format_summary">\"%1$s\" instead of \"%2$s\"</string> <string name="pref_relative_format_summary">\"%1$s\" instead of \"%2$s\"</string>
<string name="pref_date_format">Date format</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_manage_notifications">Manage notifications</string>
<string name="pref_app_language">App language</string> <string name="pref_app_language">App language</string>