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:
parent
acfaab6102
commit
0a7f828d77
29 changed files with 1007 additions and 88 deletions
|
|
@ -256,7 +256,8 @@ dependencies {
|
|||
implementation(libs.swipe)
|
||||
implementation(libs.compose.webview)
|
||||
implementation(libs.compose.grid)
|
||||
|
||||
implementation(libs.palette.ktx)
|
||||
implementation(libs.material.kolor)
|
||||
|
||||
implementation(libs.google.api.services.drive)
|
||||
implementation(libs.google.api.client.oauth)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package eu.kanade.domain.ui
|
||||
|
||||
import android.os.Build
|
||||
import com.materialkolor.PaletteStyle
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.domain.ui.model.TabletUiMode
|
||||
import eu.kanade.domain.ui.model.ThemeMode
|
||||
|
|
@ -28,6 +29,12 @@ class UiPreferences(
|
|||
|
||||
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
||||
|
||||
fun detailsPageThemeCoverBased() = preferenceStore.getBoolean("pref_details_page_theme_cover_based_key", true)
|
||||
|
||||
fun themeCoverBasedStyle() = preferenceStore.getEnum("pref_theme_cover_based_style_key", PaletteStyle.Vibrant)
|
||||
|
||||
fun themeCoverBasedAnimate() = preferenceStore.getBoolean("pref_theme_cover_based_animate_key", false)
|
||||
|
||||
fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
|
||||
|
||||
fun dateFormat() = preferenceStore.getString("app_date_format", "")
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.materialkolor.ktx.blend
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
|
@ -129,6 +130,7 @@ fun MangaInfoBox(
|
|||
brush = Brush.verticalGradient(colors = backdropGradientColors),
|
||||
)
|
||||
}
|
||||
.background(MaterialTheme.colorScheme.inversePrimary.copy(alpha = 0.2f))
|
||||
.blur(4.dp)
|
||||
.alpha(0.2f),
|
||||
)
|
||||
|
|
@ -241,7 +243,7 @@ fun MangaActionRow(
|
|||
MangaActionButton(
|
||||
title = stringResource(MR.strings.action_web_view),
|
||||
icon = Icons.Outlined.Public,
|
||||
color = defaultActionButtonColor,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
onClick = onWebViewClicked,
|
||||
onLongClick = onWebViewLongClicked,
|
||||
)
|
||||
|
|
@ -251,7 +253,7 @@ fun MangaActionRow(
|
|||
MangaActionButton(
|
||||
title = stringResource(SYMR.strings.merge),
|
||||
icon = Icons.AutoMirrored.Outlined.CallMerge,
|
||||
color = defaultActionButtonColor,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
onClick = onMergeClicked,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,13 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.app.ActivityCompat
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.materialkolor.PaletteStyle
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.TabletUiMode
|
||||
import eu.kanade.domain.ui.model.ThemeMode
|
||||
|
|
@ -42,6 +45,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
|
||||
return listOf(
|
||||
getThemeGroup(uiPreferences = uiPreferences),
|
||||
getDetailsPageThemeGroup(uiPreferences = uiPreferences),
|
||||
getDisplayGroup(uiPreferences = uiPreferences),
|
||||
// SY -->
|
||||
getNavbarGroup(uiPreferences = uiPreferences),
|
||||
|
|
@ -100,6 +104,38 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDetailsPageThemeGroup(
|
||||
uiPreferences: UiPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val scope = rememberCoroutineScope()
|
||||
val detailsPageThemeCoverBased by remember {
|
||||
Injekt.get<UiPreferences>().detailsPageThemeCoverBased().asState(scope)
|
||||
}
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_details_page_theme),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = uiPreferences.detailsPageThemeCoverBased(),
|
||||
title = stringResource(MR.strings.pref_details_page_theme_cover_based),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.themeCoverBasedStyle(),
|
||||
title = stringResource(MR.strings.pref_theme_cover_based_style),
|
||||
enabled = detailsPageThemeCoverBased,
|
||||
entries = PaletteStyle.entries
|
||||
.associateWith { it.name }
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = uiPreferences.themeCoverBasedAnimate(),
|
||||
title = stringResource(MR.strings.pref_theme_cover_based_animate),
|
||||
enabled = detailsPageThemeCoverBased,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDisplayGroup(
|
||||
uiPreferences: UiPreferences,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import eu.kanade.tachiyomi.di.SYPreferenceModule
|
|||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverMetadata
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
|
|
@ -167,6 +168,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||
|
||||
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
|
||||
|
||||
MangaCoverMetadata.load()
|
||||
|
||||
// Updates widget update
|
||||
with(WidgetManager(Injekt.get(), Injekt.get())) {
|
||||
init(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
|
|
|
|||
|
|
@ -12,16 +12,22 @@ import coil3.fetch.SourceFetchResult
|
|||
import coil3.getOrDefault
|
||||
import coil3.request.Options
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER_KEY
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.http.HTTP_NOT_MODIFIED
|
||||
import okio.BufferedSource
|
||||
import okio.FileSystem
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import okio.Source
|
||||
|
|
@ -31,7 +37,10 @@ import okio.source
|
|||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
import tachiyomi.domain.manga.model.asMangaCover
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
|
@ -48,7 +57,8 @@ import java.io.IOException
|
|||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class MangaCoverFetcher(
|
||||
private val url: String?,
|
||||
private val mangaCover: MangaCover,
|
||||
private val url: String? = mangaCover.url,
|
||||
private val isLibraryManga: Boolean,
|
||||
private val options: Options,
|
||||
private val coverFileLazy: Lazy<File?>,
|
||||
|
|
@ -59,9 +69,15 @@ class MangaCoverFetcher(
|
|||
private val imageLoader: ImageLoader,
|
||||
) : Fetcher {
|
||||
|
||||
private val uiPreferences: UiPreferences = Injekt.get()
|
||||
private val fileScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
|
||||
private val diskCacheKey: String
|
||||
get() = diskCacheKeyLazy.value
|
||||
|
||||
/**
|
||||
* Called each time a cover is displayed
|
||||
*/
|
||||
override suspend fun fetch(): FetchResult {
|
||||
// Use custom cover if exists
|
||||
val useCustomCover = options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)
|
||||
|
|
@ -83,6 +99,7 @@ class MangaCoverFetcher(
|
|||
}
|
||||
|
||||
private fun fileLoader(file: File): FetchResult {
|
||||
setRatioAndColorsInScope(mangaCover, ogFile = file)
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(
|
||||
file = file.toOkioPath(),
|
||||
|
|
@ -95,6 +112,7 @@ class MangaCoverFetcher(
|
|||
}
|
||||
|
||||
private fun fileUriLoader(uri: String): FetchResult {
|
||||
setRatioAndColorsInScope(mangaCover)
|
||||
val source = UniFile.fromUri(options.context, uri.toUri())!!
|
||||
.openInputStream()
|
||||
.source()
|
||||
|
|
@ -128,6 +146,7 @@ class MangaCoverFetcher(
|
|||
}
|
||||
|
||||
// Read from snapshot
|
||||
setRatioAndColorsInScope(mangaCover, bufferedSource = snapshot.toImageSource().source())
|
||||
return SourceFetchResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
|
|
@ -148,6 +167,7 @@ class MangaCoverFetcher(
|
|||
// Read from disk cache
|
||||
snapshot = writeToDiskCache(response)
|
||||
if (snapshot != null) {
|
||||
setRatioAndColorsInScope(mangaCover, bufferedSource = snapshot.toImageSource().source())
|
||||
return SourceFetchResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
|
|
@ -155,6 +175,13 @@ class MangaCoverFetcher(
|
|||
)
|
||||
}
|
||||
|
||||
setRatioAndColorsInScope(
|
||||
mangaCover,
|
||||
bufferedSource = ImageSource(
|
||||
source = responseBody.source(),
|
||||
fileSystem = FileSystem.SYSTEM,
|
||||
).source(),
|
||||
)
|
||||
// Read from response if cache is unused or unusable
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM),
|
||||
|
|
@ -293,6 +320,27 @@ class MangaCoverFetcher(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [setRatioAndColorsInScope] is called whenever a cover is loaded with [MangaCoverFetcher.fetch]
|
||||
*
|
||||
* @param bufferedSource if not null then it will load bitmap from [BufferedSource], regardless of [ogFile]
|
||||
* @param ogFile if not null then it will load bitmap from [File]. If it's null then it will try to load bitmap
|
||||
* from [CoverCache] using either [CoverCache.customCoverCacheDir] or [CoverCache.cacheDir]
|
||||
* @param force if true (default) then it will always re-calculate ratio & color for favorite mangas.
|
||||
* This is useful when a favorite manga updates/changes its cover. If false then it will only update ratio.
|
||||
*/
|
||||
private fun setRatioAndColorsInScope(
|
||||
mangaCover: MangaCover,
|
||||
bufferedSource: BufferedSource? = null,
|
||||
ogFile: File? = null,
|
||||
force: Boolean = true
|
||||
) {
|
||||
if (!uiPreferences.detailsPageThemeCoverBased().get()) return
|
||||
fileScope.launch {
|
||||
MangaCoverMetadata.setRatioAndColors(mangaCover, bufferedSource, ogFile, force)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Type {
|
||||
File,
|
||||
URI,
|
||||
|
|
@ -308,7 +356,7 @@ class MangaCoverFetcher(
|
|||
|
||||
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return MangaCoverFetcher(
|
||||
url = data.thumbnailUrl,
|
||||
mangaCover = data.asMangaCover(),
|
||||
isLibraryManga = data.favorite,
|
||||
options = options,
|
||||
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) },
|
||||
|
|
@ -330,7 +378,7 @@ class MangaCoverFetcher(
|
|||
|
||||
override fun create(data: MangaCover, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return MangaCoverFetcher(
|
||||
url = data.url,
|
||||
mangaCover = data,
|
||||
isLibraryManga = data.isMangaFavorite,
|
||||
options = options,
|
||||
coverFileLazy = lazy { coverCache.getCoverFile(data.url) },
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import androidx.palette.graphics.Palette
|
||||
import coil3.Extras
|
||||
import coil3.getExtra
|
||||
import coil3.request.ImageRequest
|
||||
|
|
@ -42,3 +43,42 @@ val Options.customDecoder: Boolean
|
|||
get() = getExtra(customDecoderKey)
|
||||
|
||||
private val customDecoderKey = Extras.Key(default = false)
|
||||
|
||||
/**
|
||||
* Calculate the best [Palette.Swatch] from [Palette]
|
||||
* @author Jays2Kings
|
||||
*/
|
||||
fun Palette.getBestColor(): Int? {
|
||||
// How big is the vibrant color
|
||||
val vibPopulation = vibrantSwatch?.population ?: -1
|
||||
// Saturation of the most dominant color
|
||||
val domSat = dominantSwatch?.hsl?.get(1) ?: 0f
|
||||
// Brightness of the most dominant color
|
||||
val domLum = dominantSwatch?.hsl?.get(2) ?: -1f
|
||||
// How big is the muted color
|
||||
val mutedPopulation = mutedSwatch?.population ?: -1
|
||||
// Saturation of the muted color
|
||||
val mutedSat = mutedSwatch?.hsl?.get(1) ?: 0f
|
||||
// If muted color is 3 times bigger than vibrant color then minimum acceptable saturation
|
||||
// for muted color is lower (more likely to use it even if it's not colorful)
|
||||
val mutedSatMinAcceptable = if (mutedPopulation > vibPopulation * 3f) 0.1f else 0.25f
|
||||
|
||||
val dominantIsColorful = domSat >= .25f
|
||||
val dominantBrightnessJustRight = domLum <= .8f && domLum > .2f
|
||||
val vibrantIsConsiderableBigEnough = vibPopulation >= mutedPopulation * 0.75f
|
||||
val mutedIsBig = mutedPopulation > vibPopulation * 1.5f
|
||||
val mutedIsNotTooBoring = mutedSat > mutedSatMinAcceptable
|
||||
|
||||
return when {
|
||||
dominantIsColorful && dominantBrightnessJustRight -> dominantSwatch
|
||||
// use vibrant color even if it's only 0.75 times smaller than muted color
|
||||
vibrantIsConsiderableBigEnough -> vibrantSwatch
|
||||
// use muted color if it's 1.5 times bigger than vibrant color and colorful enough (above the limit)
|
||||
mutedIsBig && mutedIsNotTooBoring -> mutedSwatch
|
||||
// return major vibrant color variant with more favor of vibrant color (size x3)
|
||||
else -> listOfNotNull(vibrantSwatch, lightVibrantSwatch, darkVibrantSwatch)
|
||||
.maxByOrNull {
|
||||
if (it === vibrantSwatch) vibPopulation * 3 else it.population
|
||||
}
|
||||
}?.rgb
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ import eu.kanade.tachiyomi.ui.home.HomeScreen
|
|||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
|
||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverMetadata
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
|
||||
|
|
@ -337,6 +338,11 @@ class MainActivity : BaseActivity() {
|
|||
// SY -->
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
MangaCoverMetadata.savePrefs()
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
when (val screen = navigator?.lastItem) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
package eu.kanade.tachiyomi.ui.manga
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -17,6 +26,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
|
|
@ -31,8 +41,13 @@ import eu.kanade.tachiyomi.databinding.EditMangaDialogBinding
|
|||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.chop
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
|
||||
import exh.ui.metadata.adapters.MetadataUIUtil.getResourceColor
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.binding
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.dismissDialog
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setColors
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setNegativeButton
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setPositiveButton
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setTextEdit
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setTitle
|
||||
import exh.util.dropBlank
|
||||
import exh.util.trimOrNull
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -42,6 +57,7 @@ import tachiyomi.i18n.MR
|
|||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.source.local.isLocal
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
fun EditMangaDialog(
|
||||
|
|
@ -61,6 +77,18 @@ fun EditMangaDialog(
|
|||
var binding by remember {
|
||||
mutableStateOf<EditMangaDialogBinding?>(null)
|
||||
}
|
||||
val colors = EditMangaDialogColors(
|
||||
textColor = MaterialTheme.colorScheme.onSurfaceVariant.toArgb(),
|
||||
textHighlightColor = MaterialTheme.colorScheme.outline.toArgb(),
|
||||
iconColor = MaterialTheme.colorScheme.primary.toArgb(),
|
||||
tagColor = MaterialTheme.colorScheme.outlineVariant.toArgb(),
|
||||
tagFocusColor = MaterialTheme.colorScheme.outline.toArgb(),
|
||||
tagTextColor = MaterialTheme.colorScheme.onSurfaceVariant.toArgb(),
|
||||
btnTextColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
|
||||
btnBgColor = MaterialTheme.colorScheme.surfaceTint.toArgb(),
|
||||
dropdownBgColor = MaterialTheme.colorScheme.surfaceVariant.toArgb(),
|
||||
dialogBgColor = MaterialTheme.colorScheme.surfaceContainerHigh.toArgb(),
|
||||
)
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
|
|
@ -109,7 +137,13 @@ fun EditMangaDialog(
|
|||
EditMangaDialogBinding.inflate(LayoutInflater.from(factoryContext))
|
||||
.also { binding = it }
|
||||
.apply {
|
||||
onViewCreated(manga, factoryContext, this, scope)
|
||||
onViewCreated(
|
||||
manga,
|
||||
factoryContext,
|
||||
this,
|
||||
scope,
|
||||
colors,
|
||||
)
|
||||
}
|
||||
.root
|
||||
},
|
||||
|
|
@ -120,10 +154,29 @@ fun EditMangaDialog(
|
|||
)
|
||||
}
|
||||
|
||||
private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDialogBinding, scope: CoroutineScope) {
|
||||
class EditMangaDialogColors(
|
||||
@ColorInt val textColor: Int,
|
||||
@ColorInt val textHighlightColor: Int,
|
||||
@ColorInt val iconColor: Int,
|
||||
@ColorInt val tagColor: Int,
|
||||
@ColorInt val tagFocusColor: Int,
|
||||
@ColorInt val tagTextColor: Int,
|
||||
@ColorInt val btnTextColor: Int,
|
||||
@ColorInt val btnBgColor: Int,
|
||||
@ColorInt val dropdownBgColor: Int,
|
||||
@ColorInt val dialogBgColor: Int,
|
||||
)
|
||||
|
||||
private fun onViewCreated(
|
||||
manga: Manga,
|
||||
context: Context,
|
||||
binding: EditMangaDialogBinding,
|
||||
scope: CoroutineScope,
|
||||
colors: EditMangaDialogColors,
|
||||
) {
|
||||
loadCover(manga, binding)
|
||||
|
||||
val statusAdapter: ArrayAdapter<String> = ArrayAdapter(
|
||||
val statusAdapter = SpinnerAdapter(
|
||||
context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
listOf(
|
||||
|
|
@ -135,6 +188,7 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
|
|||
MR.strings.cancelled,
|
||||
MR.strings.on_hiatus,
|
||||
).map { context.stringResource(it) },
|
||||
colors,
|
||||
)
|
||||
|
||||
binding.status.adapter = statusAdapter
|
||||
|
|
@ -153,6 +207,9 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
|
|||
)
|
||||
}
|
||||
|
||||
// Set Spinner's dropdown caret color
|
||||
binding.status.backgroundTintList = ColorStateList.valueOf(colors.iconColor)
|
||||
|
||||
if (manga.isLocal()) {
|
||||
if (manga.title != manga.url) {
|
||||
binding.title.setText(manga.title)
|
||||
|
|
@ -163,7 +220,7 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
|
|||
binding.mangaArtist.setText(manga.artist.orEmpty())
|
||||
binding.thumbnailUrl.setText(manga.thumbnailUrl.orEmpty())
|
||||
binding.mangaDescription.setText(manga.description.orEmpty())
|
||||
binding.mangaGenresTags.setChips(manga.genre.orEmpty().dropBlank(), scope)
|
||||
binding.mangaGenresTags.setChips(manga.genre.orEmpty().dropBlank(), scope, colors)
|
||||
} else {
|
||||
if (manga.title != manga.ogTitle) {
|
||||
binding.title.append(manga.title)
|
||||
|
|
@ -180,7 +237,7 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
|
|||
if (manga.description != manga.ogDescription) {
|
||||
binding.mangaDescription.append(manga.description.orEmpty())
|
||||
}
|
||||
binding.mangaGenresTags.setChips(manga.genre.orEmpty().dropBlank(), scope)
|
||||
binding.mangaGenresTags.setChips(manga.genre.orEmpty().dropBlank(), scope, colors)
|
||||
|
||||
binding.title.hint = context.stringResource(SYMR.strings.title_hint, manga.ogTitle)
|
||||
|
||||
|
|
@ -189,27 +246,76 @@ private fun onViewCreated(manga: Manga, context: Context, binding: EditMangaDial
|
|||
binding.mangaDescription.hint =
|
||||
context.stringResource(
|
||||
SYMR.strings.description_hint,
|
||||
manga.ogDescription?.takeIf { it.isNotBlank() }?.replace("\n", " ")?.chop(20) ?: ""
|
||||
manga.ogDescription?.takeIf { it.isNotBlank() }?.replace("\n", " ")?.chop(20) ?: "",
|
||||
)
|
||||
binding.thumbnailUrl.hint =
|
||||
context.stringResource(
|
||||
SYMR.strings.thumbnail_url_hint,
|
||||
manga.ogThumbnailUrl?.let {
|
||||
it.chop(40) + if (it.length > 46) "." + it.substringAfterLast(".").chop(6) else ""
|
||||
} ?: ""
|
||||
} ?: "",
|
||||
)
|
||||
}
|
||||
binding.mangaGenresTags.clearFocus()
|
||||
|
||||
binding.resetTags.setOnClickListener { resetTags(manga, binding, scope) }
|
||||
binding.resetInfo.setOnClickListener { resetInfo(manga, binding, scope) }
|
||||
listOf(
|
||||
binding.title,
|
||||
binding.mangaAuthor,
|
||||
binding.mangaArtist,
|
||||
binding.thumbnailUrl,
|
||||
binding.mangaDescription,
|
||||
).forEach {
|
||||
it.setTextColor(colors.textColor)
|
||||
it.highlightColor = colors.textHighlightColor
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
it.textSelectHandle?.let { drawable ->
|
||||
drawable.setTint(colors.iconColor)
|
||||
it.setTextSelectHandle(drawable)
|
||||
}
|
||||
it.textSelectHandleLeft?.let { drawable ->
|
||||
drawable.setTint(colors.iconColor)
|
||||
it.setTextSelectHandleLeft(drawable)
|
||||
}
|
||||
it.textSelectHandleRight?.let { drawable ->
|
||||
drawable.setTint(colors.iconColor)
|
||||
it.setTextSelectHandleRight(drawable)
|
||||
}
|
||||
}
|
||||
}
|
||||
listOf(
|
||||
binding.titleOutline,
|
||||
binding.mangaAuthorOutline,
|
||||
binding.mangaArtistOutline,
|
||||
binding.thumbnailUrlOutline,
|
||||
binding.mangaDescriptionOutline,
|
||||
).forEach {
|
||||
it.boxStrokeColor = colors.iconColor
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
it.cursorColor = ColorStateList.valueOf(colors.iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
binding.resetTags.setTextColor(colors.btnTextColor)
|
||||
binding.resetTags.setBackgroundColor(colors.btnBgColor)
|
||||
binding.resetInfo.setTextColor(colors.btnTextColor)
|
||||
binding.resetInfo.setBackgroundColor(colors.btnBgColor)
|
||||
|
||||
binding.resetTags.setOnClickListener { resetTags(manga, binding, scope, colors) }
|
||||
binding.resetInfo.setOnClickListener { resetInfo(manga, binding, scope, colors) }
|
||||
}
|
||||
|
||||
private fun resetTags(manga: Manga, binding: EditMangaDialogBinding, scope: CoroutineScope) {
|
||||
private fun resetTags(
|
||||
manga: Manga,
|
||||
binding: EditMangaDialogBinding,
|
||||
scope: CoroutineScope,
|
||||
colors: EditMangaDialogColors,
|
||||
) {
|
||||
if (manga.genre.isNullOrEmpty() || manga.isLocal()) {
|
||||
binding.mangaGenresTags.setChips(emptyList(), scope)
|
||||
binding.mangaGenresTags.setChips(emptyList(), scope, colors)
|
||||
} else {
|
||||
binding.mangaGenresTags.setChips(manga.ogGenre.orEmpty(), scope)
|
||||
binding.mangaGenresTags.setChips(manga.ogGenre.orEmpty(), scope, colors)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -219,52 +325,88 @@ private fun loadCover(manga: Manga, binding: EditMangaDialogBinding) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun resetInfo(manga: Manga, binding: EditMangaDialogBinding, scope: CoroutineScope) {
|
||||
private fun resetInfo(
|
||||
manga: Manga,
|
||||
binding: EditMangaDialogBinding,
|
||||
scope: CoroutineScope,
|
||||
colors: EditMangaDialogColors,
|
||||
) {
|
||||
binding.title.setText("")
|
||||
binding.mangaAuthor.setText("")
|
||||
binding.mangaArtist.setText("")
|
||||
binding.thumbnailUrl.setText("")
|
||||
binding.mangaDescription.setText("")
|
||||
resetTags(manga, binding, scope)
|
||||
resetTags(manga, binding, scope, colors)
|
||||
}
|
||||
|
||||
private fun ChipGroup.setChips(items: List<String>, scope: CoroutineScope) {
|
||||
private fun ChipGroup.setChips(
|
||||
items: List<String>,
|
||||
scope: CoroutineScope,
|
||||
colors: EditMangaDialogColors,
|
||||
) {
|
||||
removeAllViews()
|
||||
|
||||
val colorStateList = ColorStateList(
|
||||
arrayOf(
|
||||
intArrayOf(android.R.attr.state_focused),
|
||||
intArrayOf(android.R.attr.state_pressed),
|
||||
intArrayOf(-android.R.attr.state_active),
|
||||
),
|
||||
intArrayOf(
|
||||
colors.tagFocusColor,
|
||||
colors.tagFocusColor,
|
||||
colors.tagColor,
|
||||
),
|
||||
)
|
||||
|
||||
items.asSequence().map { item ->
|
||||
Chip(context).apply {
|
||||
text = item
|
||||
setTextColor(colors.tagTextColor)
|
||||
|
||||
isCloseIconVisible = true
|
||||
closeIcon?.setTint(context.getResourceColor(R.attr.colorAccent))
|
||||
closeIcon?.setTint(colors.iconColor)
|
||||
setOnCloseIconClickListener {
|
||||
removeView(this)
|
||||
}
|
||||
|
||||
chipBackgroundColor = colorStateList
|
||||
}
|
||||
}.forEach {
|
||||
addView(it)
|
||||
}
|
||||
|
||||
val addTagChip = Chip(context).apply {
|
||||
setText(SYMR.strings.add_tag.getString(context))
|
||||
text = SYMR.strings.add_tag.getString(context)
|
||||
setTextColor(colors.tagTextColor)
|
||||
|
||||
chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_add_24dp)?.apply {
|
||||
isChipIconVisible = true
|
||||
setTint(context.getResourceColor(R.attr.colorAccent))
|
||||
setTint(colors.iconColor)
|
||||
}
|
||||
|
||||
chipBackgroundColor = colorStateList
|
||||
|
||||
setOnClickListener {
|
||||
var newTag: String? = null
|
||||
MaterialAlertDialogBuilder(context)
|
||||
var dialog: AlertDialog? = null
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
val binding = builder.binding(context)
|
||||
.setTitle(SYMR.strings.add_tag.getString(context))
|
||||
.setTextInput {
|
||||
newTag = it.trimOrNull()
|
||||
.setPositiveButton(MR.strings.action_ok.getString(context)) {
|
||||
dialog?.dismissDialog()
|
||||
val newTag = it.trimOrNull()
|
||||
if (newTag != null) setChips(items + listOfNotNull(newTag), scope, colors)
|
||||
}
|
||||
.setPositiveButton(MR.strings.action_ok.getString(context)) { _, _ ->
|
||||
if (newTag != null) setChips(items + listOfNotNull(newTag), scope)
|
||||
.setNegativeButton(MR.strings.action_cancel.getString(context)) {
|
||||
dialog?.dismissDialog()
|
||||
}
|
||||
.setNegativeButton(MR.strings.action_cancel.getString(context), null)
|
||||
.show()
|
||||
.setTextEdit()
|
||||
.setColors(colors)
|
||||
|
||||
dialog = builder.create()
|
||||
dialog.setView(binding.root)
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
addView(addTagChip)
|
||||
|
|
@ -277,3 +419,48 @@ private fun ChipGroup.getTextStrings(): List<String> = children.mapNotNull {
|
|||
null
|
||||
}
|
||||
}.toList()
|
||||
|
||||
private class SpinnerAdapter(
|
||||
context: Context,
|
||||
@LayoutRes val resource: Int,
|
||||
objects: List<String>,
|
||||
val colors: EditMangaDialogColors,
|
||||
) : ArrayAdapter<String>(context, resource, objects) {
|
||||
private val mInflater = LayoutInflater.from(context)
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
return createViewFromResource(mInflater, position, convertView, parent, resource)
|
||||
}
|
||||
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
return createViewFromResource(mInflater, position, convertView, parent, resource)
|
||||
}
|
||||
|
||||
private fun createViewFromResource(
|
||||
inflater: LayoutInflater,
|
||||
position: Int,
|
||||
convertView: View?,
|
||||
parent: ViewGroup,
|
||||
resource: Int,
|
||||
): View {
|
||||
val text: TextView
|
||||
|
||||
val view = convertView ?: inflater.inflate(resource, parent, false)
|
||||
|
||||
try {
|
||||
// If no custom field is assigned, assume the whole resource is a TextView
|
||||
text = view as TextView
|
||||
} catch (e: ClassCastException) {
|
||||
Timber.e("You must supply a resource ID for a TextView")
|
||||
throw IllegalStateException("ArrayAdapter requires the resource ID to be a TextView", e)
|
||||
}
|
||||
|
||||
val item: String? = getItem(position)
|
||||
if (item != null) text.text = item
|
||||
|
||||
text.setTextColor(colors.textColor)
|
||||
text.setBackgroundColor(colors.dropdownBgColor)
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
|
|
@ -12,8 +14,10 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
|
|
@ -23,8 +27,11 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
|||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.materialkolor.DynamicMaterialTheme
|
||||
import com.materialkolor.rememberDynamicColorScheme
|
||||
import eu.kanade.domain.manga.model.hasCustomCover
|
||||
import eu.kanade.domain.manga.model.toSManga
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.category.components.ChangeCategoryDialog
|
||||
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
|
||||
import eu.kanade.presentation.manga.ChapterSettingsDialog
|
||||
|
|
@ -116,10 +123,7 @@ class MangaScreen(
|
|||
return
|
||||
}
|
||||
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val screenModel =
|
||||
rememberScreenModel { MangaScreenModel(context, mangaId, fromSource, smartSearchConfig != null) }
|
||||
|
||||
|
|
@ -131,6 +135,40 @@ class MangaScreen(
|
|||
}
|
||||
|
||||
val successState = state as MangaScreenModel.State.Success
|
||||
val seedColorState = rememberUpdatedState(newValue = successState.seedColor)
|
||||
val uiPreferences = remember { Injekt.get<UiPreferences>() }
|
||||
|
||||
if (uiPreferences.themeCoverBasedAnimate().get()) {
|
||||
DynamicMaterialTheme(
|
||||
seedColor = seedColorState.value ?: MaterialTheme.colorScheme.primary,
|
||||
useDarkTheme = isSystemInDarkTheme(),
|
||||
style = uiPreferences.themeCoverBasedStyle().get(),
|
||||
animate = uiPreferences.themeCoverBasedAnimate().get(),
|
||||
content = { MaterialThemeContent(context, screenModel, successState) },
|
||||
)
|
||||
} else {
|
||||
val colorScheme = rememberDynamicColorScheme(
|
||||
seedColor = seedColorState.value ?: MaterialTheme.colorScheme.primary,
|
||||
isDark = isSystemInDarkTheme(),
|
||||
style = uiPreferences.themeCoverBasedStyle().get(),
|
||||
)
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
content = { MaterialThemeContent(context, screenModel, successState) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MaterialThemeContent(
|
||||
context: Context,
|
||||
screenModel: MangaScreenModel,
|
||||
successState: MangaScreenModel.State.Success,
|
||||
) {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val isHttpSource = remember { successState.source is HttpSource }
|
||||
|
||||
// KMK -->
|
||||
|
|
@ -162,7 +200,6 @@ class MangaScreen(
|
|||
.launchIn(this)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
MangaScreen(
|
||||
state = successState,
|
||||
snackbarHostState = screenModel.snackbarHostState,
|
||||
|
|
@ -229,8 +266,8 @@ class MangaScreen(
|
|||
onMergedSettingsClicked = screenModel::showEditMergedSettingsDialog,
|
||||
onMergeClicked = { openSmartSearch(navigator, successState.manga) },
|
||||
onMergeWithAnotherClicked = { mergeWithAnother(navigator, context, successState.manga, screenModel::smartSearchMerge) },
|
||||
onOpenPagePreview = {
|
||||
openPagePreview(context, successState.chapters.minByOrNull { it.chapter.sourceOrder }?.chapter, it)
|
||||
onOpenPagePreview = { page ->
|
||||
openPagePreview(context, successState.chapters.minByOrNull { it.chapter.sourceOrder }?.chapter, page)
|
||||
},
|
||||
onMorePreviewsClicked = { openMorePagePreviews(navigator, successState.manga) },
|
||||
// SY <--
|
||||
|
|
@ -289,6 +326,7 @@ class MangaScreen(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
is MangaScreenModel.Dialog.DeleteChapters -> {
|
||||
DeleteChaptersDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
|
@ -311,6 +349,7 @@ class MangaScreen(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
manga = successState.manga,
|
||||
|
|
@ -324,6 +363,7 @@ class MangaScreen(
|
|||
scanlatorFilterActive = successState.scanlatorFilterActive,
|
||||
onScanlatorFilterClicked = { showScanlatorsDialog = true },
|
||||
)
|
||||
|
||||
MangaScreenModel.Dialog.TrackSheet -> {
|
||||
NavigatorAdaptiveSheet(
|
||||
screen = TrackInfoDialogHomeScreen(
|
||||
|
|
@ -335,6 +375,7 @@ class MangaScreen(
|
|||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
MangaScreenModel.Dialog.FullCover -> {
|
||||
val sm = rememberScreenModel { MangaCoverScreenModel(successState.manga.id) }
|
||||
val manga by sm.state.collectAsState()
|
||||
|
|
@ -361,6 +402,7 @@ class MangaScreen(
|
|||
LoadingScreen(Modifier.systemBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
is MangaScreenModel.Dialog.SetFetchInterval -> {
|
||||
SetIntervalDialog(
|
||||
interval = dialog.manga.fetchInterval,
|
||||
|
|
@ -378,6 +420,7 @@ class MangaScreen(
|
|||
onPositiveClick = screenModel::updateMangaInfo,
|
||||
)
|
||||
}
|
||||
|
||||
is MangaScreenModel.Dialog.EditMergedSettings -> {
|
||||
EditMergedSettingsDialog(
|
||||
mergedData = dialog.mergedData,
|
||||
|
|
@ -402,14 +445,19 @@ class MangaScreen(
|
|||
when (bulkFavoriteState.dialog) {
|
||||
is BulkFavoriteScreenModel.Dialog.AddDuplicateManga ->
|
||||
AddDuplicateMangaDialog(bulkFavoriteScreenModel)
|
||||
|
||||
is BulkFavoriteScreenModel.Dialog.RemoveManga ->
|
||||
RemoveMangaDialog(bulkFavoriteScreenModel)
|
||||
|
||||
is BulkFavoriteScreenModel.Dialog.ChangeMangaCategory ->
|
||||
ChangeMangaCategoryDialog(bulkFavoriteScreenModel)
|
||||
|
||||
is BulkFavoriteScreenModel.Dialog.ChangeMangasCategory ->
|
||||
ChangeMangasCategoryDialog(bulkFavoriteScreenModel)
|
||||
|
||||
is BulkFavoriteScreenModel.Dialog.AllowDuplicate ->
|
||||
AllowDuplicateDialog(bulkFavoriteScreenModel)
|
||||
|
||||
else -> {}
|
||||
}
|
||||
// KMK <--
|
||||
|
|
@ -539,8 +587,8 @@ class MangaScreen(
|
|||
// SY <--
|
||||
}
|
||||
|
||||
private fun openMetadataViewer(navigator: Navigator, manga: Manga) {
|
||||
navigator.push(MetadataViewScreen(manga.id, manga.source))
|
||||
private fun openMetadataViewer(navigator: Navigator, manga: Manga, seedColor: Color? = null) {
|
||||
navigator.push(MetadataViewScreen(manga.id, manga.source, seedColor))
|
||||
}
|
||||
|
||||
private fun openMergedMangaWebview(context: Context, navigator: Navigator, mergedMangaData: MergedMangaData) {
|
||||
|
|
|
|||
|
|
@ -9,9 +9,16 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.palette.graphics.Palette
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import coil3.Image
|
||||
import coil3.executeBlocking
|
||||
import coil3.imageLoader
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.allowHardware
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.core.util.addOrRemove
|
||||
import eu.kanade.core.util.insertSeparators
|
||||
|
|
@ -57,8 +64,10 @@ import eu.kanade.tachiyomi.ui.manga.RelatedManga.Companion.sorted
|
|||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.util.chapter.getNextUnread
|
||||
import eu.kanade.tachiyomi.data.coil.getBestColor
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
import eu.kanade.tachiyomi.util.system.getBitmapOrNull
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.debug.DebugToggles
|
||||
import exh.eh.EHentaiUpdateHelper
|
||||
|
|
@ -100,6 +109,7 @@ import tachiyomi.core.common.preference.TriState
|
|||
import tachiyomi.core.common.preference.mapAsCheckboxState
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.common.util.lang.launchUI
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
|
|
@ -133,10 +143,12 @@ import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
|||
import tachiyomi.domain.manga.interactor.UpdateMergedSettings
|
||||
import tachiyomi.domain.manga.model.CustomMangaInfo
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate
|
||||
import tachiyomi.domain.manga.model.MergedMangaReference
|
||||
import tachiyomi.domain.manga.model.applyFilter
|
||||
import tachiyomi.domain.manga.model.asMangaCover
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.source.model.StubSource
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
|
|
@ -242,8 +254,6 @@ class MangaScreenModel(
|
|||
val redirectFlow: MutableSharedFlow<EXHRedirect> = MutableSharedFlow()
|
||||
|
||||
data class EXHRedirect(val mangaId: Long)
|
||||
|
||||
var dedupe: Boolean = true
|
||||
// EXH <--
|
||||
|
||||
private data class CombineState(
|
||||
|
|
@ -388,6 +398,7 @@ class MangaScreenModel(
|
|||
|
||||
screenModelScope.launchIO {
|
||||
val manga = getMangaAndChapters.awaitManga(mangaId)
|
||||
|
||||
// SY -->
|
||||
val chapters = (if (manga.source == MERGED_SOURCE_ID) getMergedChaptersByMangaId.await(mangaId, applyScanlatorFilter = true) else getMangaAndChapters.awaitChapters(mangaId, applyScanlatorFilter = true))
|
||||
.toChapterListItems(manga, null)
|
||||
|
|
@ -470,6 +481,76 @@ class MangaScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color of the manga cover by loading cover with ImageRequest directly from network.
|
||||
*/
|
||||
private suspend fun setPaletteColor(model: Any, method: ImageRequestType = ImageRequestType.Enqueue) {
|
||||
if (model is ImageRequest && model.defined.sizeResolver != null) return
|
||||
|
||||
val imageRequestBuilder = if (model is ImageRequest) {
|
||||
model.newBuilder()
|
||||
} else {
|
||||
ImageRequest.Builder(context).data(model)
|
||||
}
|
||||
.allowHardware(false)
|
||||
|
||||
val generatePalette: (Image) -> Unit = { image ->
|
||||
val bitmap = image.asDrawable(context.resources).getBitmapOrNull()
|
||||
if (bitmap != null) {
|
||||
Palette.from(bitmap).generate { palette ->
|
||||
screenModelScope.launchUI {
|
||||
palette?.getBestColor()?.let { vibrantColor ->
|
||||
when (model) {
|
||||
is Manga -> model.asMangaCover().vibrantCoverColor = vibrantColor
|
||||
is MangaCover -> model.vibrantCoverColor = vibrantColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (method) {
|
||||
ImageRequestType.Enqueue -> {
|
||||
context.imageLoader.enqueue(
|
||||
imageRequestBuilder
|
||||
.target(
|
||||
onSuccess = generatePalette,
|
||||
onError = {
|
||||
// TODO: handle error
|
||||
// val file = coverCache.getCoverFile(manga!!)
|
||||
// if (file.exists()) {
|
||||
// file.delete()
|
||||
// setPaletteColor()
|
||||
// }
|
||||
},
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
ImageRequestType.Execute -> {
|
||||
context.imageLoader.execute(imageRequestBuilder.build())
|
||||
.image?.let { image ->
|
||||
generatePalette(image)
|
||||
}
|
||||
}
|
||||
ImageRequestType.ExecuteBlocking -> {
|
||||
context.imageLoader.executeBlocking(imageRequestBuilder.build())
|
||||
.image?.let { image ->
|
||||
generatePalette(image)
|
||||
}
|
||||
}
|
||||
ImageRequestType.IOContext -> {
|
||||
withIOContext {
|
||||
context.imageLoader.execute(imageRequestBuilder.build())
|
||||
.image?.let { image ->
|
||||
generatePalette(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
private suspend fun syncTrackers() {
|
||||
if (!trackPreferences.autoSyncReadChapters().get()) return
|
||||
|
|
@ -703,8 +784,8 @@ class MangaScreenModel(
|
|||
mergedManga = networkToLocalManga.await(mergedManga)
|
||||
|
||||
getCategories.await(originalMangaId)
|
||||
.let { it ->
|
||||
setMangaCategories.await(mergedManga.id, it.map { it.id })
|
||||
.let { categories ->
|
||||
setMangaCategories.await(mergedManga.id, categories.map { it.id })
|
||||
}
|
||||
|
||||
val originalMangaReference = MergedMangaReference(
|
||||
|
|
@ -1796,7 +1877,9 @@ class MangaScreenModel(
|
|||
val relatedMangaCollection: List<RelatedManga>? = null,
|
||||
// KMK <--
|
||||
) : State {
|
||||
// KMK ->>
|
||||
// KMK -->
|
||||
val seedColor: Color? = MangaCover.vibrantCoverColorMap[manga.id]?.let { Color(it) }
|
||||
|
||||
/**
|
||||
* a value of null will be treated as still loading, so if all searching were failed and won't update
|
||||
* 'relatedMangaCollection` then we should return empty list
|
||||
|
|
@ -1978,3 +2061,10 @@ sealed interface RelatedManga {
|
|||
}
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
enum class ImageRequestType {
|
||||
IOContext,
|
||||
Enqueue,
|
||||
Execute,
|
||||
ExecuteBlocking,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import java.io.InputStream
|
|||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Call before updating [Manga.thumbnail_url] to ensure old cover can be cleared from cache
|
||||
* Call before updating [Manga.thumbnailUrl] to ensure old cover can be cleared from cache
|
||||
*/
|
||||
fun Manga.prepUpdateCover(coverCache: CoverCache, remoteManga: SManga, refreshSameUrl: Boolean): Manga {
|
||||
// Never refresh covers if the new url is null, as the current url has possibly become invalid
|
||||
|
|
|
|||
|
|
@ -1,29 +1,135 @@
|
|||
package eu.kanade.tachiyomi.widget.materialdialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.databinding.DialogStubTextinputBinding
|
||||
import eu.kanade.tachiyomi.ui.manga.EditMangaDialogColors
|
||||
|
||||
fun MaterialAlertDialogBuilder.setTextInput(
|
||||
hint: String? = null,
|
||||
prefill: String? = null,
|
||||
onTextChanged: (String) -> Unit,
|
||||
): MaterialAlertDialogBuilder {
|
||||
val binding = DialogStubTextinputBinding.inflate(LayoutInflater.from(context))
|
||||
binding.textField.hint = hint
|
||||
binding.textField.editText?.apply {
|
||||
@Suppress("UnusedReceiverParameter")
|
||||
fun MaterialAlertDialogBuilder.binding(context: Context): DialogStubTextinputBinding {
|
||||
return DialogStubTextinputBinding.inflate(LayoutInflater.from(context))
|
||||
}
|
||||
|
||||
fun AlertDialog.dismissDialog() {
|
||||
if (isShowing) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
fun DialogStubTextinputBinding.setTitle(title: String): DialogStubTextinputBinding {
|
||||
alertTitle.text = title
|
||||
return this
|
||||
}
|
||||
|
||||
fun DialogStubTextinputBinding.setPositiveButton(text: String, onClick: (String) -> Unit): DialogStubTextinputBinding {
|
||||
positiveButton.text = text
|
||||
|
||||
positiveButton.setOnClickListener {
|
||||
val textEdit = textField.editText
|
||||
onClick(textEdit?.text?.toString() ?: "")
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun DialogStubTextinputBinding.setNegativeButton(text: String, onClick: () -> Unit): DialogStubTextinputBinding {
|
||||
negativeButton.text = text
|
||||
|
||||
negativeButton.setOnClickListener {
|
||||
onClick()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun DialogStubTextinputBinding.setHint(hint: String? = null): DialogStubTextinputBinding {
|
||||
textField.hint = hint
|
||||
return this
|
||||
}
|
||||
|
||||
fun DialogStubTextinputBinding.setTextEdit(prefill: String? = null): DialogStubTextinputBinding {
|
||||
textField.editText?.apply {
|
||||
setText(prefill, TextView.BufferType.EDITABLE)
|
||||
doAfterTextChanged {
|
||||
onTextChanged(it?.toString() ?: "")
|
||||
}
|
||||
post {
|
||||
requestFocusFromTouch()
|
||||
context.getSystemService<InputMethodManager>()?.showSoftInput(this, 0)
|
||||
}
|
||||
}
|
||||
return setView(binding.root)
|
||||
return this
|
||||
}
|
||||
|
||||
fun DialogStubTextinputBinding.setColors(colors: EditMangaDialogColors): DialogStubTextinputBinding {
|
||||
textField.boxStrokeColor = colors.iconColor
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
textField.cursorColor = ColorStateList.valueOf(colors.iconColor)
|
||||
}
|
||||
|
||||
textField.editText?.apply {
|
||||
setTextColor(colors.textColor)
|
||||
highlightColor = colors.textHighlightColor
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
textSelectHandle?.let { drawable ->
|
||||
drawable.setTint(colors.iconColor)
|
||||
setTextSelectHandle(drawable)
|
||||
}
|
||||
textSelectHandleLeft?.let { drawable ->
|
||||
drawable.setTint(colors.iconColor)
|
||||
setTextSelectHandleLeft(drawable)
|
||||
}
|
||||
textSelectHandleRight?.let { drawable ->
|
||||
drawable.setTint(colors.iconColor)
|
||||
setTextSelectHandleRight(drawable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alertTitle.setTextColor(colors.textColor)
|
||||
positiveButton.setTextColor(colors.iconColor)
|
||||
negativeButton.setTextColor(colors.iconColor)
|
||||
|
||||
root.background = RoundedCornerDrawable(color = colors.dialogBgColor)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
class RoundedCornerDrawable(val color: Int, private val cornerRadius: Float = 72f) : Drawable() {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val bounds = bounds
|
||||
paint.color = color
|
||||
canvas.drawRoundRect(
|
||||
RectF(bounds.left.toFloat(), bounds.top.toFloat(), bounds.right.toFloat(), bounds.bottom.toFloat()),
|
||||
cornerRadius,
|
||||
cornerRadius,
|
||||
paint,
|
||||
)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paint.alpha = alpha
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java", ReplaceWith("PixelFormat.TRANSLUCENT", "android.graphics.PixelFormat"))
|
||||
override fun getOpacity(): Int {
|
||||
return PixelFormat.TRANSLUCENT
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
paint.colorFilter = colorFilter
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package exh.ui.metadata
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
|
|
@ -16,11 +17,14 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.materialkolor.DynamicMaterialTheme
|
||||
import com.materialkolor.PaletteStyle
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
|
|
@ -33,7 +37,11 @@ import tachiyomi.presentation.core.screens.LoadingScreen
|
|||
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
class MetadataViewScreen(private val mangaId: Long, private val sourceId: Long) : Screen() {
|
||||
class MetadataViewScreen(
|
||||
private val mangaId: Long,
|
||||
private val sourceId: Long,
|
||||
private val seedColor: Color? = null,
|
||||
) : Screen() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
|
|
@ -41,7 +49,9 @@ class MetadataViewScreen(private val mangaId: Long, private val sourceId: Long)
|
|||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val state by screenModel.state.collectAsState()
|
||||
Scaffold(
|
||||
|
||||
@Composable
|
||||
fun content() = Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = screenModel.manga.collectAsState().value?.title,
|
||||
|
|
@ -96,5 +106,13 @@ class MetadataViewScreen(private val mangaId: Long, private val sourceId: Long)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
DynamicMaterialTheme(
|
||||
seedColor = seedColor ?: MaterialTheme.colorScheme.primary,
|
||||
useDarkTheme = isSystemInDarkTheme(),
|
||||
style = PaletteStyle.Vibrant,
|
||||
animate = true,
|
||||
content = { content() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package exh.ui.metadata.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.LayoutInflater
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
|
@ -20,8 +23,15 @@ import tachiyomi.i18n.MR
|
|||
import tachiyomi.i18n.sy.SYMR
|
||||
|
||||
@Composable
|
||||
fun EHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
|
||||
fun EHentaiDescription(
|
||||
state: State.Success,
|
||||
openMetadataViewer: () -> Unit,
|
||||
search: (String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
|
||||
val iconColor = MaterialTheme.colorScheme.secondary.toArgb()
|
||||
val ratingBarSecondaryColor = MaterialTheme.colorScheme.outlineVariant.toArgb()
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
factory = { factoryContext ->
|
||||
|
|
@ -40,20 +50,26 @@ fun EHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit, sea
|
|||
}
|
||||
?: meta.genre
|
||||
?: context.stringResource(MR.strings.unknown)
|
||||
binding.genre.setTextColor(textColor)
|
||||
|
||||
binding.visible.text = context.stringResource(SYMR.strings.is_visible, meta.visible ?: context.stringResource(MR.strings.unknown))
|
||||
binding.visible.setTextColor(textColor)
|
||||
|
||||
binding.favorites.text = (meta.favorites ?: 0).toString()
|
||||
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp)
|
||||
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp, iconColor)
|
||||
binding.favorites.setTextColor(textColor)
|
||||
|
||||
binding.uploader.text = meta.uploader ?: context.stringResource(MR.strings.unknown)
|
||||
binding.uploader.setTextColor(textColor)
|
||||
|
||||
binding.size.text = MetadataUtil.humanReadableByteCount(meta.size ?: 0, true)
|
||||
binding.size.bindDrawable(context, R.drawable.ic_outline_sd_card_24)
|
||||
binding.size.bindDrawable(context, R.drawable.ic_outline_sd_card_24, iconColor)
|
||||
binding.size.setTextColor(textColor)
|
||||
|
||||
val length = meta.length ?: 0
|
||||
binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, length, length)
|
||||
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24)
|
||||
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor)
|
||||
binding.pages.setTextColor(textColor)
|
||||
|
||||
val language = meta.language ?: context.stringResource(MR.strings.unknown)
|
||||
binding.language.text = if (meta.translated == true) {
|
||||
|
|
@ -61,13 +77,18 @@ fun EHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit, sea
|
|||
} else {
|
||||
language
|
||||
}
|
||||
binding.language.setTextColor(textColor)
|
||||
|
||||
val ratingFloat = meta.averageRating?.toFloat()
|
||||
binding.ratingBar.rating = ratingFloat ?: 0F
|
||||
@SuppressLint("SetTextI18n")
|
||||
binding.rating.text = (ratingFloat ?: 0F).toString() + " - " + MetadataUIUtil.getRatingString(context, ratingFloat?.times(2))
|
||||
binding.ratingBar.supportProgressTintList = ColorStateList.valueOf(iconColor)
|
||||
binding.ratingBar.supportSecondaryProgressTintList = ColorStateList.valueOf(ratingBarSecondaryColor)
|
||||
binding.rating.setTextColor(textColor)
|
||||
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
|
||||
binding.moreInfo.setTextColor(textColor)
|
||||
|
||||
listOf(
|
||||
binding.favorites,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package exh.ui.metadata.adapters
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
|
@ -18,6 +20,8 @@ import tachiyomi.i18n.MR
|
|||
@Composable
|
||||
fun EightMusesDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
|
||||
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
factory = { factoryContext ->
|
||||
|
|
@ -29,8 +33,10 @@ fun EightMusesDescription(state: State.Success, openMetadataViewer: () -> Unit)
|
|||
val binding = DescriptionAdapter8mBinding.bind(it)
|
||||
|
||||
binding.title.text = meta.title ?: context.stringResource(MR.strings.unknown)
|
||||
binding.title.setTextColor(textColor)
|
||||
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
|
||||
binding.moreInfo.setTextColor(textColor)
|
||||
|
||||
binding.title.setOnLongClickListener {
|
||||
context.copyToClipboard(
|
||||
|
|
@ -39,6 +45,7 @@ fun EightMusesDescription(state: State.Success, openMetadataViewer: () -> Unit)
|
|||
)
|
||||
true
|
||||
}
|
||||
binding.title.setTextColor(textColor)
|
||||
|
||||
binding.moreInfo.setOnClickListener {
|
||||
openMetadataViewer()
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package exh.ui.metadata.adapters
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
|
@ -18,6 +20,8 @@ import tachiyomi.i18n.sy.SYMR
|
|||
@Composable
|
||||
fun HBrowseDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
|
||||
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
factory = { factoryContext ->
|
||||
|
|
@ -29,9 +33,11 @@ fun HBrowseDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
|||
val binding = DescriptionAdapterHbBinding.bind(it)
|
||||
|
||||
binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, meta.length ?: 0, meta.length ?: 0)
|
||||
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24)
|
||||
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor)
|
||||
binding.pages.setTextColor(textColor)
|
||||
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
|
||||
binding.moreInfo.setTextColor(textColor)
|
||||
|
||||
binding.pages.setOnLongClickListener {
|
||||
context.copyToClipboard(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package exh.ui.metadata.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.LayoutInflater
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.isVisible
|
||||
|
|
@ -20,6 +23,9 @@ import kotlin.math.round
|
|||
@Composable
|
||||
fun MangaDexDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
|
||||
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
|
||||
val ratingBarSecondaryColor = MaterialTheme.colorScheme.outlineVariant.toArgb()
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
factory = { factoryContext ->
|
||||
|
|
@ -37,8 +43,12 @@ fun MangaDexDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
|||
binding.rating.text = (round((ratingFloat ?: 0F) * 100.0) / 100.0).toString() + " - " + getRatingString(context, ratingFloat)
|
||||
binding.rating.isVisible = ratingFloat != null
|
||||
binding.ratingBar.isVisible = ratingFloat != null
|
||||
binding.ratingBar.supportProgressTintList = ColorStateList.valueOf(iconColor)
|
||||
binding.ratingBar.supportSecondaryProgressTintList = ColorStateList.valueOf(ratingBarSecondaryColor)
|
||||
binding.rating.setTextColor(textColor)
|
||||
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
|
||||
binding.moreInfo.setTextColor(textColor)
|
||||
|
||||
binding.rating.setOnLongClickListener {
|
||||
context.copyToClipboard(
|
||||
|
|
|
|||
|
|
@ -61,9 +61,13 @@ object MetadataUIUtil {
|
|||
genreColor.color to context.stringResource(stringId)
|
||||
}
|
||||
|
||||
fun TextView.bindDrawable(context: Context, @DrawableRes drawable: Int) {
|
||||
fun TextView.bindDrawable(
|
||||
context: Context,
|
||||
@DrawableRes drawable: Int,
|
||||
@ColorInt color: Int = context.getResourceColor(R.attr.colorAccent),
|
||||
) {
|
||||
ContextCompat.getDrawable(context, drawable)?.apply {
|
||||
setTint(context.getResourceColor(R.attr.colorAccent))
|
||||
setTint(color)
|
||||
setBounds(0, 0, 20.dpToPx, 20.dpToPx)
|
||||
setCompoundDrawables(this, null, null, null)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package exh.ui.metadata.adapters
|
|||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
|
@ -25,6 +27,8 @@ import java.time.ZonedDateTime
|
|||
@Composable
|
||||
fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
|
||||
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
factory = { factoryContext ->
|
||||
|
|
@ -45,11 +49,13 @@ fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
|||
it.second
|
||||
} ?: categoriesString ?: context.stringResource(MR.strings.unknown)
|
||||
}
|
||||
binding.genre.setTextColor(textColor)
|
||||
|
||||
meta.favoritesCount?.let {
|
||||
if (it == 0L) return@let
|
||||
binding.favorites.text = it.toString()
|
||||
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp)
|
||||
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp, iconColor)
|
||||
binding.favorites.setTextColor(textColor)
|
||||
}
|
||||
|
||||
binding.whenPosted.text = MetadataUtil.EX_DATE_FORMAT
|
||||
|
|
@ -57,18 +63,22 @@ fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
|||
ZonedDateTime
|
||||
.ofInstant(Instant.ofEpochSecond(meta.uploadDate ?: 0), ZoneId.systemDefault())
|
||||
)
|
||||
binding.whenPosted.setTextColor(textColor)
|
||||
|
||||
binding.pages.text = context.pluralStringResource(
|
||||
SYMR.plurals.num_pages,
|
||||
meta.pageImageTypes.size,
|
||||
meta.pageImageTypes.size,
|
||||
)
|
||||
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24)
|
||||
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor)
|
||||
binding.pages.setTextColor(textColor)
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
binding.id.text = "#" + (meta.nhId ?: 0)
|
||||
binding.id.setTextColor(textColor)
|
||||
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
|
||||
binding.moreInfo.setTextColor(textColor)
|
||||
|
||||
listOf(
|
||||
binding.favorites,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package exh.ui.metadata.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.LayoutInflater
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
|
@ -22,6 +25,9 @@ import kotlin.math.round
|
|||
@Composable
|
||||
fun PururinDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
|
||||
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
|
||||
val ratingBarSecondaryColor = MaterialTheme.colorScheme.outlineVariant.toArgb()
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
factory = { factoryContext ->
|
||||
|
|
@ -38,21 +44,29 @@ fun PururinDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
|||
it.second
|
||||
} ?: genre?.name ?: context.stringResource(MR.strings.unknown)
|
||||
}
|
||||
binding.genre.setTextColor(textColor)
|
||||
|
||||
binding.uploader.text = meta.uploaderDisp ?: meta.uploader.orEmpty()
|
||||
binding.uploader.setTextColor(textColor)
|
||||
|
||||
binding.size.text = meta.fileSize ?: context.stringResource(MR.strings.unknown)
|
||||
binding.size.bindDrawable(context, R.drawable.ic_outline_sd_card_24)
|
||||
binding.size.bindDrawable(context, R.drawable.ic_outline_sd_card_24, iconColor)
|
||||
binding.size.setTextColor(textColor)
|
||||
|
||||
binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, meta.pages ?: 0, meta.pages ?: 0)
|
||||
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24)
|
||||
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor)
|
||||
binding.pages.setTextColor(textColor)
|
||||
|
||||
val ratingFloat = meta.averageRating?.toFloat()
|
||||
binding.ratingBar.rating = ratingFloat ?: 0F
|
||||
@SuppressLint("SetTextI18n")
|
||||
binding.rating.text = (round((ratingFloat ?: 0F) * 100.0) / 100.0).toString() + " - " + MetadataUIUtil.getRatingString(context, ratingFloat?.times(2))
|
||||
binding.ratingBar.supportProgressTintList = ColorStateList.valueOf(iconColor)
|
||||
binding.ratingBar.supportSecondaryProgressTintList = ColorStateList.valueOf(ratingBarSecondaryColor)
|
||||
binding.rating.setTextColor(textColor)
|
||||
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
|
||||
binding.moreInfo.setTextColor(textColor)
|
||||
|
||||
listOf(
|
||||
binding.genre,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package exh.ui.metadata.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.LayoutInflater
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
|
@ -23,6 +26,9 @@ import kotlin.math.round
|
|||
@Composable
|
||||
fun TsuminoDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
|
||||
val iconColor = MaterialTheme.colorScheme.primary.toArgb()
|
||||
val ratingBarSecondaryColor = MaterialTheme.colorScheme.outlineVariant.toArgb()
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
factory = { factoryContext ->
|
||||
|
|
@ -37,22 +43,31 @@ fun TsuminoDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
|||
binding.genre.setBackgroundColor(it.first)
|
||||
it.second
|
||||
} ?: meta.category ?: context.stringResource(MR.strings.unknown)
|
||||
binding.genre.setTextColor(textColor)
|
||||
|
||||
binding.favorites.text = (meta.favorites ?: 0).toString()
|
||||
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp)
|
||||
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp, iconColor)
|
||||
binding.favorites.setTextColor(textColor)
|
||||
|
||||
binding.whenPosted.text = TsuminoSearchMetadata.TSUMINO_DATE_FORMAT.format(Date(meta.uploadDate ?: 0))
|
||||
binding.whenPosted.setTextColor(textColor)
|
||||
|
||||
binding.uploader.text = meta.uploader ?: context.stringResource(MR.strings.unknown)
|
||||
binding.uploader.setTextColor(textColor)
|
||||
|
||||
binding.pages.text = context.pluralStringResource(SYMR.plurals.num_pages, meta.length ?: 0, meta.length ?: 0)
|
||||
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24)
|
||||
binding.pages.bindDrawable(context, R.drawable.ic_baseline_menu_book_24, iconColor)
|
||||
binding.pages.setTextColor(textColor)
|
||||
|
||||
binding.ratingBar.rating = meta.averageRating ?: 0F
|
||||
@SuppressLint("SetTextI18n")
|
||||
binding.rating.text = (round((meta.averageRating ?: 0F) * 100.0) / 100.0).toString() + " - " + MetadataUIUtil.getRatingString(context, meta.averageRating?.times(2))
|
||||
binding.ratingBar.supportProgressTintList = ColorStateList.valueOf(iconColor)
|
||||
binding.ratingBar.supportSecondaryProgressTintList = ColorStateList.valueOf(ratingBarSecondaryColor)
|
||||
binding.rating.setTextColor(textColor)
|
||||
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp, iconColor)
|
||||
binding.moreInfo.setTextColor(textColor)
|
||||
|
||||
listOf(
|
||||
binding.favorites,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:paddingVertical="16dp">
|
||||
android:paddingVertical="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/alertTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="24sp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/text_field"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -16,4 +26,25 @@
|
|||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</FrameLayout>
|
||||
<LinearLayout
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end">
|
||||
|
||||
<Button
|
||||
android:id="@+id/positiveButton"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/negativeButton"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:id="@+id/title_outline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
|
@ -55,6 +56,7 @@
|
|||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:id="@+id/manga_author_outline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
|
@ -72,6 +74,7 @@
|
|||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:id="@+id/manga_artist_outline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
|
@ -89,6 +92,7 @@
|
|||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:id="@+id/thumbnail_url_outline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
|
@ -105,6 +109,7 @@
|
|||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:id="@+id/manga_description_outline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ class LibraryPreferences(
|
|||
fun lastUpdatedTimestamp() = preferenceStore.getLong(Preference.appStateKey("library_update_last_timestamp"), 0L)
|
||||
fun autoUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0)
|
||||
|
||||
fun coverRatios() = preferenceStore.getStringSet("pref_library_cover_ratios_key", emptySet())
|
||||
|
||||
fun coverColors() = preferenceStore.getStringSet("pref_library_cover_colors_key", emptySet())
|
||||
|
||||
fun autoUpdateDeviceRestrictions() = preferenceStore.getStringSet(
|
||||
"library_update_restriction",
|
||||
setOf(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package tachiyomi.domain.manga.model
|
|||
|
||||
import tachiyomi.domain.manga.interactor.GetCustomMangaInfo
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Contains the required data for MangaCoverFetcher
|
||||
|
|
@ -22,11 +23,68 @@ data class MangaCover(
|
|||
null
|
||||
}
|
||||
val url: String? = customThumbnailUrl ?: ogUrl
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* [vibrantCoverColor] is used to set the color theme in manga detail page.
|
||||
* It contains color for all mangas, both in library or browsing.
|
||||
*
|
||||
* It reads/saves to a hashmap in [MangaCover.vibrantCoverColorMap] for multiple mangas.
|
||||
*/
|
||||
var vibrantCoverColor: Int?
|
||||
get() = vibrantCoverColorMap[mangaId]
|
||||
set(value) {
|
||||
vibrantCoverColorMap[mangaId] = value
|
||||
}
|
||||
|
||||
/**
|
||||
* [dominantCoverColors] is used to set cover/text's color in Library (Favorite) grid view.
|
||||
* It contains only color for in-library (favorite) mangas.
|
||||
*
|
||||
* It reads/saves to a hashmap in [MangaCover.coverColorMap].
|
||||
*
|
||||
* Format: <first: cover color, second: text color>.
|
||||
*
|
||||
* Set in *[MangaCoverMetadata.setRatioAndColors]* whenever browsing meets a favorite manga
|
||||
* by loading from *[CoverCache]*.
|
||||
*
|
||||
* Get in *[CommonMangaItem.MangaCompactGridItem]*, *[CommonMangaItem.MangaComfortableGridItem]* and
|
||||
* *[CommonMangaItem.MangaListItem]*
|
||||
*/
|
||||
@Suppress("KDocUnresolvedReference")
|
||||
var dominantCoverColors: Pair<Int, Int>?
|
||||
get() = coverColorMap[mangaId]
|
||||
set(value) {
|
||||
value ?: return
|
||||
coverColorMap[mangaId] = value.first to value.second
|
||||
}
|
||||
|
||||
var ratio: Float?
|
||||
get() = coverRatioMap[mangaId]
|
||||
set(value) {
|
||||
value ?: return
|
||||
coverRatioMap[mangaId] = value
|
||||
}
|
||||
companion object {
|
||||
private val getCustomMangaInfo: GetCustomMangaInfo by injectLazy()
|
||||
|
||||
/**
|
||||
* [vibrantCoverColorMap] store color generated while browsing library.
|
||||
* It always empty at beginning each time app starts, then add more color while browsing.
|
||||
*/
|
||||
val vibrantCoverColorMap: HashMap<Long, Int?> = hashMapOf()
|
||||
|
||||
/**
|
||||
* [coverColorMap] stores favorite manga's cover & text's color as a joined string in Prefs.
|
||||
* They will be loaded each time *[App]* is initialized with *[MangaCoverMetadata.load]*.
|
||||
*
|
||||
* They will be saved back when *[MainActivity.onPause]* is triggered.
|
||||
*/
|
||||
@Suppress("KDocUnresolvedReference")
|
||||
var coverColorMap = ConcurrentHashMap<Long, Pair<Int, Int>>()
|
||||
|
||||
var coverRatioMap = ConcurrentHashMap<Long, Float>()
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
fun Manga.asMangaCover(): MangaCover {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ sqlite = "2.4.0"
|
|||
voyager = "1.0.0"
|
||||
detekt = "1.23.6"
|
||||
detektCompose = "0.3.12"
|
||||
paletteKtxVersion = "1.0.0"
|
||||
materialKolorVersion = "1.6.1"
|
||||
|
||||
[libraries]
|
||||
desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
|
||||
|
|
@ -68,6 +70,8 @@ insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
|||
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.2.0"
|
||||
compose-webview = "io.github.kevinnzou:compose-webview:0.33.6"
|
||||
compose-grid = "io.woong.compose.grid:grid:1.2.2"
|
||||
palette-ktx = { group = "androidx.palette", name = "palette-ktx", version.ref = "paletteKtxVersion" }
|
||||
material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolorVersion" }
|
||||
|
||||
swipe = "me.saket.swipe:swipe:1.3.0"
|
||||
|
||||
|
|
|
|||
|
|
@ -240,6 +240,10 @@
|
|||
<!-- "Today" instead of "2023-12-31" -->
|
||||
<string name="pref_relative_format_summary">\"%1$s\" instead of \"%2$s\"</string>
|
||||
<string name="pref_date_format">Date format</string>
|
||||
<string name="pref_details_page_theme">Details page</string>
|
||||
<string name="pref_details_page_theme_cover_based">Details page theme based on cover</string>
|
||||
<string name="pref_theme_cover_based_style">Theme style</string>
|
||||
<string name="pref_theme_cover_based_animate">Theme animate</string>
|
||||
|
||||
<string name="pref_manage_notifications">Manage notifications</string>
|
||||
<string name="pref_app_language">App language</string>
|
||||
|
|
|
|||
Loading…
Reference in a new issue