Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ dependencies {
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.session)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.palette)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.paging.runtime.ktx)
implementation(libs.androidx.preferences)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ class AppPreferences(context: Context) {
val pureBlackDarkMode: Preference<Boolean>
get() = preferenceStore.getBoolean("pure_black_dark_mode", false)

val accentColors: Preference<Boolean>
get() = preferenceStore.getBoolean("accent_colors", false)

val openLinksInternally: Preference<Boolean>
get() = preferenceStore.getBoolean("open_links_internally", true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ enum class AppTheme {
MONOCHROME -> R.string.theme_monochrome
}

val supportsFeedAccentColor: Boolean
get() = this == MONOCHROME || this == MONET

/** On the off chance someone has selected MONET on an older version, default them */
fun normalized(): AppTheme {
return if (this === MONET && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/com/capyreader/app/ui/articles/AccentColor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.capyreader.app.ui.articles

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.capyreader.app.ui.theme.LocalAppTheme

@Composable
fun rememberAccentColor(faviconURL: String?): Color? {
faviconURL ?: return null

val isDark = LocalAppTheme.current.isDark
var color by remember(faviconURL, isDark) { mutableStateOf<Color?>(null) }
val context = LocalContext.current

LaunchedEffect(faviconURL, isDark) {
color = FaviconColorCache.getColor(faviconURL, context, isDark)
}

return color
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ fun rememberArticleOptions(appPreferences: AppPreferences = koinInject()): Artic
val fontScale by appPreferences.articleListOptions.fontScale.stateIn(scope).collectAsState()
val shortenTitles by appPreferences.articleListOptions.shortenTitles.stateIn(scope)
.collectAsState()
val accentColors by appPreferences.accentColors.stateIn(scope).collectAsState()

return ArticleRowOptions(
showSummary = showSummary,
Expand All @@ -195,6 +196,7 @@ fun rememberArticleOptions(appPreferences: AppPreferences = koinInject()): Artic
imagePreview = imagePreview,
fontScale = fontScale,
shortenTitles = shortenTitles,
accentColors = accentColors,
)
}

Expand Down
88 changes: 78 additions & 10 deletions app/src/main/java/com/capyreader/app/ui/articles/ArticleRow.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material.icons.rounded.Star
Expand All @@ -23,6 +22,7 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
Expand Down Expand Up @@ -52,6 +52,7 @@ import coil3.compose.AsyncImage
import com.capyreader.app.R
import com.capyreader.app.common.ImagePreview
import com.capyreader.app.preferences.AppTheme
import com.capyreader.app.preferences.ThemeMode
import com.capyreader.app.ui.articles.list.ArticleActionMenu
import com.capyreader.app.ui.articles.list.ArticleListItem
import com.capyreader.app.ui.articles.list.ArticleRowSwipeBox
Expand All @@ -75,6 +76,7 @@ data class ArticleRowOptions(
val imagePreview: ImagePreview = ImagePreview.default,
val fontScale: ArticleListFontScale = ArticleListFontScale.MEDIUM,
val shortenTitles: Boolean = true,
val accentColors: Boolean = false,
val dim: Boolean = true,
)

Expand All @@ -89,14 +91,18 @@ fun ArticleRow(
options: ArticleRowOptions = ArticleRowOptions(),
) {
val imageURL = article.imageURL
val isMonochrome = LocalAppTheme.current == AppTheme.MONOCHROME
val isMonochrome = LocalAppTheme.current.value == AppTheme.MONOCHROME
val dim = article.read && options.dim
val deEmphasizeFontWeight = dim && isMonochrome
val colors = listItemColors(
selected = selected,
read = dim
)
val feedNameColor = findFeedNameColor(read = dim)
val feedNameColor = findFeedNameColor(
faviconURL = article.faviconURL,
accentColors = options.accentColors,
read = dim,
)
val haptics = LocalHapticFeedback.current
val (isArticleMenuOpen, setArticleMenuOpen) = remember { mutableStateOf(false) }
val labelsActions = LocalLabelsActions.current
Expand Down Expand Up @@ -306,25 +312,40 @@ private fun listItemColors(
read: Boolean,
): ListItemColors {
val defaults = ListItemDefaults.colors()
val isMonochrome = LocalAppTheme.current == AppTheme.MONOCHROME
val isMonochrome = LocalAppTheme.current.value == AppTheme.MONOCHROME
val dimColors = read && !isMonochrome

return ListItemDefaults.colors(
containerColor = if (selected) colorScheme.surfaceVariant else defaults.containerColor,
headlineColor = if (dimColors) defaults.disabledContentColor else defaults.headlineColor,
supportingColor = if (dimColors) defaults.disabledContentColor else defaults.supportingTextColor
headlineColor = if (dimColors) defaults.disabledContentColor else defaults.contentColor,
supportingColor = if (dimColors) defaults.disabledContentColor else defaults.supportingContentColor
)
}

@Composable
fun findFeedNameColor(read: Boolean): Color {
fun findFeedNameColor(
faviconURL: String? = null,
accentColors: Boolean = false,
read: Boolean,
): Color {
val appThemeState = LocalAppTheme.current
val showAccentColor = appThemeState.value.supportsFeedAccentColor && accentColors

if (showAccentColor) {
val accentColor = rememberAccentColor(faviconURL)

if (accentColor != null) {
return if (read) accentColor.copy(alpha = 0.5f) else accentColor
}
}

val defaults = ListItemDefaults.colors()
val isMonochrome = LocalAppTheme.current == AppTheme.MONOCHROME
val isMonochrome = appThemeState.value == AppTheme.MONOCHROME

return if (read && !isMonochrome) {
defaults.disabledHeadlineColor
defaults.disabledContentColor
} else {
defaults.overlineColor
defaults.overlineContentColor
}
}

Expand Down Expand Up @@ -593,3 +614,50 @@ fun ArticleRowPreviewPlaceholder() {
PlaceholderArticleRow(ImagePreview.LARGE)
}
}

@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
fun ArticleRowPreview_AccentBar() {
val article = Article(
id = "288",
feedID = "123",
title = "How to use the Galaxy S24's AI photo editing tool",
author = "Andrew Romero",
contentHTML = "<div>Test</div>",
imageURL = "https://example.com",
summary = "The Galaxy S24 series packs a lot of AI narrative.",
url = URL("https://9to5google.com/?p=605559"),
updatedAt = ZonedDateTime.of(2024, 2, 11, 8, 33, 0, 0, ZoneOffset.UTC),
publishedAt = ZonedDateTime.of(2024, 2, 11, 8, 33, 0, 0, ZoneOffset.UTC),
read = false,
starred = false,
feedName = "9to5Google"
)

PreviewKoinApplication {
CapyTheme(
appTheme = AppTheme.MONOCHROME,
themeMode = ThemeMode.DARK,
pureBlack = true,
) {
Column {
ArticleRow(
article = article,
index = 0,
selected = false,
onSelect = {},
currentTime = LocalDateTime.now(),
)
ArticleRow(
article = article.copy(read = true),
index = 1,
selected = false,
onSelect = {},
currentTime = LocalDateTime.now(),
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.capyreader.app.ui.articles

import android.content.Context
import androidx.collection.LruCache
import androidx.compose.ui.graphics.Color
import androidx.core.graphics.ColorUtils
import androidx.palette.graphics.Palette
import coil3.imageLoader
import coil3.request.ImageRequest
import coil3.request.allowHardware
import coil3.size.Size
import coil3.toBitmap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

object FaviconColorCache {
private data class ColorPair(val light: Color, val dark: Color)

private val cache = LruCache<String, ColorPair>(MAX_ENTRIES)

suspend fun getColor(url: String, context: Context, isDark: Boolean): Color? {
val pair = cache[url]
?: extractColors(url, context)?.also { cache.put(url, it) }
?: return null

return if (isDark) pair.dark else pair.light
}

private suspend fun extractColors(url: String, context: Context): ColorPair? {
val request = ImageRequest.Builder(context)
.data(url)
.size(Size(64, 64))
.allowHardware(false)
.build()

val bitmap = context.imageLoader.execute(request).image?.toBitmap() ?: return null
val palette = withContext(Dispatchers.Default) { Palette.from(bitmap).generate() }

val darkSwatch = palette.lightVibrantSwatch
?: palette.vibrantSwatch
?: palette.lightMutedSwatch
?: palette.dominantSwatch
?: return null

val lightSwatch = palette.darkVibrantSwatch
?: palette.vibrantSwatch
?: palette.darkMutedSwatch
?: palette.dominantSwatch
?: return null

return ColorPair(
dark = clampLightness(darkSwatch.rgb, min = MIN_DARK_LIGHTNESS),
light = clampLightness(lightSwatch.rgb, max = MAX_LIGHT_LIGHTNESS),
)
}

private fun clampLightness(
rgb: Int,
min: Float = 0f,
max: Float = 1f,
): Color {
val hsl = FloatArray(3)
ColorUtils.colorToHSL(rgb, hsl)
hsl[2] = hsl[2].coerceIn(min, max)

return Color(ColorUtils.HSLToColor(hsl))
}

private const val MAX_ENTRIES = 200
private const val MIN_DARK_LIGHTNESS = 0.8f
private const val MAX_LIGHT_LIGHTNESS = 0.35f
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ private data class DrawerColors(
private fun NavigationDrawerItemColors.mapToTheme(
selected: Boolean,
): DrawerColors {
val isMonochrome = LocalAppTheme.current == AppTheme.MONOCHROME
val isMonochrome = LocalAppTheme.current.value == AppTheme.MONOCHROME
val useMonochromeSelected = selected && isMonochrome
val unselectedTextColor = textColor(false).value
val surfaceColor = MaterialTheme.colorScheme.surface
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package com.capyreader.app.ui.settings.panels

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.clickable
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ListItem
import androidx.compose.material3.Surface
Expand All @@ -27,6 +30,7 @@ import com.capyreader.app.R
import com.capyreader.app.common.ImagePreview
import com.capyreader.app.common.RowItem
import com.capyreader.app.preferences.AppPreferences
import com.capyreader.app.preferences.AppTheme
import com.capyreader.app.preferences.ReaderImageVisibility
import com.capyreader.app.preferences.ThemeMode
import com.capyreader.app.ui.articles.ArticleListFontScale
Expand All @@ -47,12 +51,16 @@ fun DisplaySettingsPanel(
val pinArticleBars by viewModel.pinArticleBars.collectChangesWithCurrent()
val improveTalkback by viewModel.improveTalkback.collectChangesWithCurrent()
val markReadButtonPosition by viewModel.markReadButtonPosition.collectChangesWithCurrent()
val appTheme by viewModel.appPreferences.appTheme.collectChangesWithCurrent()

DisplaySettingsPanelView(
themeMode = viewModel.themeMode,
updateThemeMode = viewModel::updateThemeMode,
appTheme = appTheme,
pureBlackDarkMode = viewModel.pureBlackDarkMode,
updatePureBlackDarkMode = viewModel::updatePureBlackDarkMode,
accentColors = viewModel.accentColors,
updateAccentColors = viewModel::updateAccentColors,
appPreferences = viewModel.appPreferences,
updatePinArticleBars = viewModel::updatePinArticleBars,
pinArticleBars = pinArticleBars,
Expand Down Expand Up @@ -83,8 +91,11 @@ fun DisplaySettingsPanel(
fun DisplaySettingsPanelView(
themeMode: ThemeMode,
updateThemeMode: (ThemeMode) -> Unit,
appTheme: AppTheme = AppTheme.default,
pureBlackDarkMode: Boolean,
updatePureBlackDarkMode: (Boolean) -> Unit,
accentColors: Boolean = false,
updateAccentColors: (Boolean) -> Unit = {},
appPreferences: AppPreferences?,
updatePinArticleBars: (enable: Boolean) -> Unit,
pinArticleBars: Boolean,
Expand Down Expand Up @@ -121,6 +132,19 @@ fun DisplaySettingsPanelView(
title = stringResource(R.string.settings_pure_black_dark_mode)
)
}
AnimatedVisibility(
visible = appTheme.supportsFeedAccentColor,
enter = expandVertically(),
exit = shrinkVertically(),
) {
RowItem {
TextSwitch(
onCheckedChange = updateAccentColors,
checked = accentColors,
title = stringResource(R.string.settings_accent_colors)
)
}
}
Box(Modifier.clickable { onNavigateToUnreadBadges() }) {
ListItem(
headlineContent = {
Expand Down
Loading