diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 229e26b04..df6a6534e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt index 3c732f649..c90e170a4 100644 --- a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt @@ -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", "") diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index 4eea7cf89..293632aa9 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -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, ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt index 36e03eef5..aef9beeb3 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt @@ -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().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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index e71fb0d38..b2a30d852 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -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().themeMode().get()) + MangaCoverMetadata.load() + // Updates widget update with(WidgetManager(Injekt.get(), Injekt.get())) { init(ProcessLifecycleOwner.get().lifecycleScope) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt index 11ff6a277..6d95a3860 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt @@ -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, @@ -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) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverMetadata.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverMetadata.kt new file mode 100644 index 000000000..4caff1e8b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverMetadata.kt @@ -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() + private val coverCache by injectLazy() + + 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()) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt index 7a920bf39..d908cb663 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt @@ -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 +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index ca47e2f79..19295faae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt index 780616365..cf9cda52a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt @@ -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(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 = 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, scope: CoroutineScope) { +private fun ChipGroup.setChips( + items: List, + 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 = children.mapNotNull { null } }.toList() + +private class SpinnerAdapter( + context: Context, + @LayoutRes val resource: Int, + objects: List, + val colors: EditMangaDialogColors, +) : ArrayAdapter(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 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 041b3412f..239129a6c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -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() } + + 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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index e991c63fe..6389a475b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -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 = 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? = 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, +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 50834eadb..4071a55aa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialAlertDialogBuilderExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialAlertDialogBuilderExtensions.kt index 6c6956470..43b68f4ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialAlertDialogBuilderExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialAlertDialogBuilderExtensions.kt @@ -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()?.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 + } } diff --git a/app/src/main/java/exh/ui/metadata/MetadataViewScreen.kt b/app/src/main/java/exh/ui/metadata/MetadataViewScreen.kt index 952a8d307..b0178548d 100644 --- a/app/src/main/java/exh/ui/metadata/MetadataViewScreen.kt +++ b/app/src/main/java/exh/ui/metadata/MetadataViewScreen.kt @@ -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() } + ) } } diff --git a/app/src/main/java/exh/ui/metadata/adapters/EHentaiDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/EHentaiDescriptionAdapter.kt index 49d62ac6b..5317a2171 100644 --- a/app/src/main/java/exh/ui/metadata/adapters/EHentaiDescriptionAdapter.kt +++ b/app/src/main/java/exh/ui/metadata/adapters/EHentaiDescriptionAdapter.kt @@ -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, diff --git a/app/src/main/java/exh/ui/metadata/adapters/EightMusesDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/EightMusesDescriptionAdapter.kt index 6209eee43..8eaa295ac 100644 --- a/app/src/main/java/exh/ui/metadata/adapters/EightMusesDescriptionAdapter.kt +++ b/app/src/main/java/exh/ui/metadata/adapters/EightMusesDescriptionAdapter.kt @@ -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() diff --git a/app/src/main/java/exh/ui/metadata/adapters/HBrowseDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/HBrowseDescriptionAdapter.kt index 5ed4fee59..de4d2f2bb 100644 --- a/app/src/main/java/exh/ui/metadata/adapters/HBrowseDescriptionAdapter.kt +++ b/app/src/main/java/exh/ui/metadata/adapters/HBrowseDescriptionAdapter.kt @@ -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( diff --git a/app/src/main/java/exh/ui/metadata/adapters/MangaDexDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/MangaDexDescriptionAdapter.kt index 34b00a2c3..5c7cc5b5b 100644 --- a/app/src/main/java/exh/ui/metadata/adapters/MangaDexDescriptionAdapter.kt +++ b/app/src/main/java/exh/ui/metadata/adapters/MangaDexDescriptionAdapter.kt @@ -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( diff --git a/app/src/main/java/exh/ui/metadata/adapters/MetadataUIUtil.kt b/app/src/main/java/exh/ui/metadata/adapters/MetadataUIUtil.kt index 7e8f5e7c6..ec57f8cd5 100644 --- a/app/src/main/java/exh/ui/metadata/adapters/MetadataUIUtil.kt +++ b/app/src/main/java/exh/ui/metadata/adapters/MetadataUIUtil.kt @@ -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) } diff --git a/app/src/main/java/exh/ui/metadata/adapters/NHentaiDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/NHentaiDescriptionAdapter.kt index 17d635663..85326077b 100644 --- a/app/src/main/java/exh/ui/metadata/adapters/NHentaiDescriptionAdapter.kt +++ b/app/src/main/java/exh/ui/metadata/adapters/NHentaiDescriptionAdapter.kt @@ -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, diff --git a/app/src/main/java/exh/ui/metadata/adapters/PururinDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/PururinDescriptionAdapter.kt index b2591e793..81f91262c 100644 --- a/app/src/main/java/exh/ui/metadata/adapters/PururinDescriptionAdapter.kt +++ b/app/src/main/java/exh/ui/metadata/adapters/PururinDescriptionAdapter.kt @@ -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, diff --git a/app/src/main/java/exh/ui/metadata/adapters/TsuminoDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/TsuminoDescriptionAdapter.kt index d618861b6..0830283d4 100644 --- a/app/src/main/java/exh/ui/metadata/adapters/TsuminoDescriptionAdapter.kt +++ b/app/src/main/java/exh/ui/metadata/adapters/TsuminoDescriptionAdapter.kt @@ -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, diff --git a/app/src/main/res/layout/dialog_stub_textinput.xml b/app/src/main/res/layout/dialog_stub_textinput.xml index 057f9c718..616ae6ee5 100644 --- a/app/src/main/res/layout/dialog_stub_textinput.xml +++ b/app/src/main/res/layout/dialog_stub_textinput.xml @@ -1,14 +1,24 @@ - + android:paddingVertical="8dp"> + + + android:layout_height="wrap_content" + android:layout_marginBottom="16dp"> - + + +