From 51822634d726586934aaf41ea84207b4ee88c504 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Sat, 7 Mar 2026 21:16:51 +0200 Subject: [PATCH 1/6] feat: add accent color customization to display settings Add selectable accent color presets (Default, Teal, Green, Gold) that override Jewel's primary blue across the entire UI: buttons, tabs, checkboxes, radio buttons, combo boxes, context menus, dropdowns, list items, search toggles, markdown links, and title bar gradient. Each accent provides light/dark mode variants. Default uses Jewel's built-in blue palette directly. --- .../composeResources/values/strings.xml | 2 + .../seforimapp/core/MainAppState.kt | 9 ++ .../components/SearchToggleChip.kt | 5 +- .../presentation/theme/AccentButtonColors.kt | 100 +++++++++++++++ .../core/presentation/theme/AccentColor.kt | 35 ++++++ .../theme/ClassicComponentStyling.kt | 56 ++++++++- .../theme/IslandsComponentStyling.kt | 118 +++++++++++++++++- .../core/presentation/theme/ThemeUtils.kt | 112 +++++++++++++++-- .../seforimapp/core/settings/AppSettings.kt | 16 +++ .../ui/panels/bookcontent/views/HomeView.kt | 5 +- .../onboarding/licence/LicenceScreen.kt | 24 +++- .../features/settings/SettingsWindow.kt | 4 +- .../settings/ui/AboutSettingsScreen.kt | 24 +++- .../settings/ui/ConditionsSettingsScreen.kt | 24 +++- .../settings/ui/DisplaySettingsScreen.kt | 60 +++++++++ .../io/github/kdroidfilter/seforimapp/main.kt | 12 +- 16 files changed, 561 insertions(+), 45 deletions(-) create mode 100644 SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentButtonColors.kt create mode 100644 SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentColor.kt diff --git a/SeforimApp/src/commonMain/composeResources/values/strings.xml b/SeforimApp/src/commonMain/composeResources/values/strings.xml index 18382b49..66f6a11b 100644 --- a/SeforimApp/src/commonMain/composeResources/values/strings.xml +++ b/SeforimApp/src/commonMain/composeResources/values/strings.xml @@ -312,6 +312,8 @@ סגנון ערכת נושא קלאסי איילנדס + + צבע מבטא מדריך ההגדרה של זית diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/MainAppState.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/MainAppState.kt index 92cc6b01..a592ec41 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/MainAppState.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/MainAppState.kt @@ -1,6 +1,7 @@ package io.github.kdroidfilter.seforimapp.core import androidx.compose.runtime.Stable +import io.github.kdroidfilter.seforimapp.core.presentation.theme.AccentColor import io.github.kdroidfilter.seforimapp.core.presentation.theme.IntUiThemes import io.github.kdroidfilter.seforimapp.core.presentation.theme.ThemeStyle import io.github.kdroidfilter.seforimapp.core.settings.AppSettings @@ -30,6 +31,14 @@ class MainAppState { AppSettings.setThemeStyle(style) } + private val _accentColor = MutableStateFlow(AppSettings.getAccentColor()) + val accentColor: StateFlow = _accentColor.asStateFlow() + + fun setAccentColor(accent: AccentColor) { + _accentColor.value = accent + AppSettings.setAccentColor(accent) + } + private val _showOnboarding = MutableStateFlow(null) val showOnBoarding: StateFlow = _showOnboarding.asStateFlow() diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/SearchToggleChip.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/SearchToggleChip.kt index b5628221..0fbd1fbc 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/SearchToggleChip.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/SearchToggleChip.kt @@ -75,11 +75,12 @@ fun TelescopeIconButton( withPadding: Boolean = true, enabled: Boolean = true, ) { + val accent = JewelTheme.globalColors.outlines.focused val backgroundColor by animateColorAsState( targetValue = when { - !enabled -> Color(0xFF0E639C).copy(alpha = 0.5f) - isSelected -> Color(0xFF0E639C) + !enabled -> accent.copy(alpha = 0.5f) + isSelected -> accent else -> Color.Transparent }, animationSpec = tween(200), diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentButtonColors.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentButtonColors.kt new file mode 100644 index 00000000..803ad8b9 --- /dev/null +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentButtonColors.kt @@ -0,0 +1,100 @@ +package io.github.kdroidfilter.seforimapp.core.presentation.theme + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import org.jetbrains.jewel.intui.standalone.styling.Default +import org.jetbrains.jewel.intui.standalone.styling.Undecorated +import org.jetbrains.jewel.intui.standalone.styling.dark +import org.jetbrains.jewel.intui.standalone.styling.light +import org.jetbrains.jewel.ui.component.styling.ButtonColors +import org.jetbrains.jewel.ui.component.styling.ButtonStyle +import org.jetbrains.jewel.ui.component.styling.DropdownColors +import org.jetbrains.jewel.ui.component.styling.DropdownStyle +import org.jetbrains.jewel.ui.component.styling.MenuColors +import org.jetbrains.jewel.ui.component.styling.MenuItemColors +import org.jetbrains.jewel.ui.component.styling.MenuStyle + +/** Builds a [MenuStyle] with the accent color used as the selection highlight. */ +fun accentMenuStyleLight(accent: Color): MenuStyle = + MenuStyle.light( + colors = + MenuColors.light( + itemColors = + MenuItemColors.light( + backgroundFocused = accent.copy(alpha = 0.12f), + backgroundHovered = accent.copy(alpha = 0.12f), + ), + ), + ) + +fun accentMenuStyleDark(accent: Color): MenuStyle = + MenuStyle.dark( + colors = + MenuColors.dark( + itemColors = + MenuItemColors.dark( + backgroundFocused = accent.copy(alpha = 0.15f), + backgroundHovered = accent.copy(alpha = 0.15f), + ), + ), + ) + +fun accentDropdownStyleLight(accent: Color): DropdownStyle = + DropdownStyle.Default.light( + colors = DropdownColors.Default.light(borderFocused = accent), + menuStyle = accentMenuStyleLight(accent), + ) + +fun accentDropdownStyleDark(accent: Color): DropdownStyle = + DropdownStyle.Default.dark( + colors = DropdownColors.Default.dark(borderFocused = accent), + menuStyle = accentMenuStyleDark(accent), + ) + +fun accentUndecoratedDropdownStyleLight(accent: Color): DropdownStyle = + DropdownStyle.Undecorated.light(menuStyle = accentMenuStyleLight(accent)) + +fun accentUndecoratedDropdownStyleDark(accent: Color): DropdownStyle = + DropdownStyle.Undecorated.dark(menuStyle = accentMenuStyleDark(accent)) + +/** Darkens a color by the given [factor] (0f = unchanged, 1f = black). */ +private fun Color.darken(factor: Float): Color = + Color( + red = red * (1f - factor), + green = green * (1f - factor), + blue = blue * (1f - factor), + alpha = alpha, + ) + +/** Lightens a color by the given [factor] (0f = unchanged, 1f = white). */ +private fun Color.lighten(factor: Float): Color = + Color( + red = red + (1f - red) * factor, + green = green + (1f - green) * factor, + blue = blue + (1f - blue) * factor, + alpha = alpha, + ) + +fun accentDefaultButtonStyleLight(accent: Color): ButtonStyle = + ButtonStyle.Default.light( + colors = + ButtonColors.Default.light( + background = SolidColor(accent), + backgroundFocused = SolidColor(accent), + backgroundPressed = SolidColor(accent.darken(0.15f)), + backgroundHovered = SolidColor(accent.lighten(0.10f)), + border = SolidColor(accent), + ), + ) + +fun accentDefaultButtonStyleDark(accent: Color): ButtonStyle = + ButtonStyle.Default.dark( + colors = + ButtonColors.Default.dark( + background = SolidColor(accent), + backgroundFocused = SolidColor(accent), + backgroundPressed = SolidColor(accent.darken(0.20f)), + backgroundHovered = SolidColor(accent.lighten(0.10f)), + border = SolidColor(accent), + ), + ) diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentColor.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentColor.kt new file mode 100644 index 00000000..00c20031 --- /dev/null +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentColor.kt @@ -0,0 +1,35 @@ +package io.github.kdroidfilter.seforimapp.core.presentation.theme + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme +import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme + +/** + * Predefined accent color presets for the application theme. + * [Default] uses Jewel's built-in blue; other entries provide light/dark variants. + */ +enum class AccentColor { + Default, + Teal, + Green, + Gold, + ; + + fun forMode(isDark: Boolean): Color = + when (this) { + Default -> + if (isDark) { + IntUiDarkTheme.colors.blueOrNull(6) ?: Color(0xFF3574F0) + } else { + IntUiLightTheme.colors.blueOrNull(4) ?: Color(0xFF4682FA) + } + Teal -> if (isDark) Color(0xFF2FC2B6) else Color(0xFF1A998E) + Green -> if (isDark) Color(0xFF5AB869) else Color(0xFF3D9A50) + Gold -> if (isDark) Color(0xFFD4A843) else Color(0xFFBE9117) + } + + /** Resolve for display in the settings UI. */ + @Composable + fun displayColor(isDark: Boolean): Color = forMode(isDark) +} diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ClassicComponentStyling.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ClassicComponentStyling.kt index 14592781..d9d0dd3c 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ClassicComponentStyling.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ClassicComponentStyling.kt @@ -2,11 +2,19 @@ package io.github.kdroidfilter.seforimapp.core.presentation.theme import androidx.compose.ui.graphics.Color import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme +import org.jetbrains.jewel.intui.standalone.styling.Default +import org.jetbrains.jewel.intui.standalone.styling.Editor import org.jetbrains.jewel.intui.standalone.styling.dark import org.jetbrains.jewel.intui.standalone.styling.light import org.jetbrains.jewel.intui.standalone.theme.dark import org.jetbrains.jewel.intui.standalone.theme.light import org.jetbrains.jewel.ui.ComponentStyling +import org.jetbrains.jewel.ui.component.styling.ComboBoxColors +import org.jetbrains.jewel.ui.component.styling.ComboBoxStyle +import org.jetbrains.jewel.ui.component.styling.SimpleListItemColors +import org.jetbrains.jewel.ui.component.styling.SimpleListItemStyle +import org.jetbrains.jewel.ui.component.styling.TabColors +import org.jetbrains.jewel.ui.component.styling.TabStyle import org.jetbrains.jewel.ui.component.styling.TooltipColors import org.jetbrains.jewel.ui.component.styling.TooltipStyle @@ -14,11 +22,55 @@ import org.jetbrains.jewel.ui.component.styling.TooltipStyle * Builds a [ComponentStyling] for the Classic theme with default Jewel styling * and custom tooltip colors for the light variant. */ -fun classicComponentStyling(isDark: Boolean): ComponentStyling = +fun classicComponentStyling( + isDark: Boolean, + accent: Color, +): ComponentStyling = if (isDark) { - ComponentStyling.dark() + ComponentStyling.dark( + defaultButtonStyle = accentDefaultButtonStyleDark(accent), + menuStyle = accentMenuStyleDark(accent), + dropdownStyle = accentDropdownStyleDark(accent), + undecoratedDropdownStyle = accentUndecoratedDropdownStyleDark(accent), + comboBoxStyle = + ComboBoxStyle.Default.dark( + colors = ComboBoxColors.Default.dark(borderFocused = accent), + ), + simpleListItemStyle = + SimpleListItemStyle.dark( + colors = SimpleListItemColors.dark(backgroundSelectedActive = accent.copy(alpha = 0.15f)), + ), + defaultTabStyle = + TabStyle.Default.dark( + colors = TabColors.Default.dark(underlineSelected = accent), + ), + editorTabStyle = + TabStyle.Editor.dark( + colors = TabColors.Editor.dark(underlineSelected = accent), + ), + ) } else { ComponentStyling.light( + defaultButtonStyle = accentDefaultButtonStyleLight(accent), + menuStyle = accentMenuStyleLight(accent), + dropdownStyle = accentDropdownStyleLight(accent), + undecoratedDropdownStyle = accentUndecoratedDropdownStyleLight(accent), + comboBoxStyle = + ComboBoxStyle.Default.light( + colors = ComboBoxColors.Default.light(borderFocused = accent), + ), + simpleListItemStyle = + SimpleListItemStyle.light( + colors = SimpleListItemColors.light(backgroundSelectedActive = accent.copy(alpha = 0.12f)), + ), + defaultTabStyle = + TabStyle.Default.light( + colors = TabColors.Default.light(underlineSelected = accent), + ), + editorTabStyle = + TabStyle.Editor.light( + colors = TabColors.Editor.light(underlineSelected = accent), + ), tooltipStyle = TooltipStyle.light( intUiTooltipColors = diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/IslandsComponentStyling.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/IslandsComponentStyling.kt index 9e31c38f..c209c5e0 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/IslandsComponentStyling.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/IslandsComponentStyling.kt @@ -6,12 +6,15 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme import org.jetbrains.jewel.intui.standalone.styling.Default +import org.jetbrains.jewel.intui.standalone.styling.Editor import org.jetbrains.jewel.intui.standalone.styling.Outlined +import org.jetbrains.jewel.intui.standalone.styling.Undecorated import org.jetbrains.jewel.intui.standalone.styling.dark import org.jetbrains.jewel.intui.standalone.styling.default import org.jetbrains.jewel.intui.standalone.styling.defaults import org.jetbrains.jewel.intui.standalone.styling.light import org.jetbrains.jewel.intui.standalone.styling.outlined +import org.jetbrains.jewel.intui.standalone.styling.undecorated import org.jetbrains.jewel.intui.standalone.theme.dark import org.jetbrains.jewel.intui.standalone.theme.light import org.jetbrains.jewel.ui.ComponentStyling @@ -19,8 +22,18 @@ import org.jetbrains.jewel.ui.component.styling.ButtonMetrics import org.jetbrains.jewel.ui.component.styling.ButtonStyle import org.jetbrains.jewel.ui.component.styling.CheckboxMetrics import org.jetbrains.jewel.ui.component.styling.CheckboxStyle +import org.jetbrains.jewel.ui.component.styling.ComboBoxColors import org.jetbrains.jewel.ui.component.styling.ComboBoxMetrics import org.jetbrains.jewel.ui.component.styling.ComboBoxStyle +import org.jetbrains.jewel.ui.component.styling.DropdownColors +import org.jetbrains.jewel.ui.component.styling.DropdownMetrics +import org.jetbrains.jewel.ui.component.styling.DropdownStyle +import org.jetbrains.jewel.ui.component.styling.MenuMetrics +import org.jetbrains.jewel.ui.component.styling.MenuStyle +import org.jetbrains.jewel.ui.component.styling.SimpleListItemColors +import org.jetbrains.jewel.ui.component.styling.SimpleListItemStyle +import org.jetbrains.jewel.ui.component.styling.TabColors +import org.jetbrains.jewel.ui.component.styling.TabStyle import org.jetbrains.jewel.ui.component.styling.TextAreaMetrics import org.jetbrains.jewel.ui.component.styling.TextAreaStyle import org.jetbrains.jewel.ui.component.styling.TextFieldMetrics @@ -38,12 +51,48 @@ private val checkboxCorner = CornerSize(5.dp) * on all interactive components (buttons, text fields, combo boxes, checkboxes, tooltips). */ @OptIn(ExperimentalFoundationApi::class) -fun islandsComponentStyling(isDark: Boolean): ComponentStyling = +fun islandsComponentStyling( + isDark: Boolean, + accent: Color, +): ComponentStyling = if (isDark) { ComponentStyling.dark( defaultButtonStyle = - ButtonStyle.Default.dark( - metrics = ButtonMetrics.default(cornerSize = roundedCorner), + accentDefaultButtonStyleDark(accent).let { + ButtonStyle.Default.dark( + colors = it.colors, + metrics = ButtonMetrics.default(cornerSize = roundedCorner), + ) + }, + menuStyle = + accentMenuStyleDark(accent).let { + MenuStyle.dark( + colors = it.colors, + metrics = MenuMetrics.defaults(cornerSize = roundedCorner), + ) + }, + dropdownStyle = + DropdownStyle.Default.dark( + colors = DropdownColors.Default.dark(borderFocused = accent), + metrics = DropdownMetrics.default(cornerSize = roundedCorner), + menuStyle = + accentMenuStyleDark(accent).let { + MenuStyle.dark( + colors = it.colors, + metrics = MenuMetrics.defaults(cornerSize = roundedCorner), + ) + }, + ), + undecoratedDropdownStyle = + DropdownStyle.Undecorated.dark( + metrics = DropdownMetrics.undecorated(cornerSize = roundedCorner), + menuStyle = + accentMenuStyleDark(accent).let { + MenuStyle.dark( + colors = it.colors, + metrics = MenuMetrics.defaults(cornerSize = roundedCorner), + ) + }, ), outlinedButtonStyle = ButtonStyle.Outlined.dark( @@ -59,8 +108,13 @@ fun islandsComponentStyling(isDark: Boolean): ComponentStyling = ), comboBoxStyle = ComboBoxStyle.Default.dark( + colors = ComboBoxColors.Default.dark(borderFocused = accent), metrics = ComboBoxMetrics.default(cornerSize = roundedCorner), ), + simpleListItemStyle = + SimpleListItemStyle.dark( + colors = SimpleListItemColors.dark(backgroundSelectedActive = accent.copy(alpha = 0.15f)), + ), checkboxStyle = CheckboxStyle.dark( metrics = @@ -71,6 +125,14 @@ fun islandsComponentStyling(isDark: Boolean): ComponentStyling = outlineSelectedFocusedCornerSize = checkboxCorner, ), ), + defaultTabStyle = + TabStyle.Default.dark( + colors = TabColors.Default.dark(underlineSelected = accent), + ), + editorTabStyle = + TabStyle.Editor.dark( + colors = TabColors.Editor.dark(underlineSelected = accent), + ), tooltipStyle = TooltipStyle.dark( intUiTooltipMetrics = @@ -84,8 +146,41 @@ fun islandsComponentStyling(isDark: Boolean): ComponentStyling = } else { ComponentStyling.light( defaultButtonStyle = - ButtonStyle.Default.light( - metrics = ButtonMetrics.default(cornerSize = roundedCorner), + accentDefaultButtonStyleLight(accent).let { + ButtonStyle.Default.light( + colors = it.colors, + metrics = ButtonMetrics.default(cornerSize = roundedCorner), + ) + }, + menuStyle = + accentMenuStyleLight(accent).let { + MenuStyle.light( + colors = it.colors, + metrics = MenuMetrics.defaults(cornerSize = roundedCorner), + ) + }, + dropdownStyle = + DropdownStyle.Default.light( + colors = DropdownColors.Default.light(borderFocused = accent), + metrics = DropdownMetrics.default(cornerSize = roundedCorner), + menuStyle = + accentMenuStyleLight(accent).let { + MenuStyle.light( + colors = it.colors, + metrics = MenuMetrics.defaults(cornerSize = roundedCorner), + ) + }, + ), + undecoratedDropdownStyle = + DropdownStyle.Undecorated.light( + metrics = DropdownMetrics.undecorated(cornerSize = roundedCorner), + menuStyle = + accentMenuStyleLight(accent).let { + MenuStyle.light( + colors = it.colors, + metrics = MenuMetrics.defaults(cornerSize = roundedCorner), + ) + }, ), outlinedButtonStyle = ButtonStyle.Outlined.light( @@ -101,8 +196,13 @@ fun islandsComponentStyling(isDark: Boolean): ComponentStyling = ), comboBoxStyle = ComboBoxStyle.Default.light( + colors = ComboBoxColors.Default.light(borderFocused = accent), metrics = ComboBoxMetrics.default(cornerSize = roundedCorner), ), + simpleListItemStyle = + SimpleListItemStyle.light( + colors = SimpleListItemColors.light(backgroundSelectedActive = accent.copy(alpha = 0.12f)), + ), checkboxStyle = CheckboxStyle.light( metrics = @@ -113,6 +213,14 @@ fun islandsComponentStyling(isDark: Boolean): ComponentStyling = outlineSelectedFocusedCornerSize = checkboxCorner, ), ), + defaultTabStyle = + TabStyle.Default.light( + colors = TabColors.Default.light(underlineSelected = accent), + ), + editorTabStyle = + TabStyle.Editor.light( + colors = TabColors.Editor.light(underlineSelected = accent), + ), tooltipStyle = TooltipStyle.light( intUiTooltipColors = diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ThemeUtils.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ThemeUtils.kt index 7ba0353a..2dcc203c 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ThemeUtils.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ThemeUtils.kt @@ -18,10 +18,14 @@ import org.jetbrains.jewel.foundation.GlobalColors import org.jetbrains.jewel.foundation.OutlineColors import org.jetbrains.jewel.foundation.TextColors import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.foundation.theme.ThemeIconData +import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme +import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme import org.jetbrains.jewel.intui.standalone.theme.dark import org.jetbrains.jewel.intui.standalone.theme.darkThemeDefinition import org.jetbrains.jewel.intui.standalone.theme.light import org.jetbrains.jewel.intui.standalone.theme.lightThemeDefinition +import org.jetbrains.jewel.ui.ComponentStyling import seforimapp.seforimapp.generated.resources.Res import seforimapp.seforimapp.generated.resources.notoserifhebrew @@ -96,9 +100,10 @@ object ThemeUtils { } /** - * Builds a Jewel theme definition driven by two independent axes: + * Builds a Jewel theme definition driven by three axes: * - theme mode (Light / Dark / System) — controls brightness * - theme style (Classic / Islands) — controls the color palette + * - accent color — optionally overrides the primary accent */ @Composable fun buildThemeDefinition() = @@ -106,20 +111,24 @@ object ThemeUtils { val mainAppState = LocalAppGraph.current.mainAppState val isDark = isDarkTheme() val themeStyle = mainAppState.themeStyle.collectAsState().value + val accentColor = mainAppState.accentColor.collectAsState().value + val accent = accentColor.forMode(isDark) val disabledValues = if (isDark) DisabledAppearanceValues.dark() else DisabledAppearanceValues.light() + val iconData = accentIconData(accent, isDark) when (themeStyle) { ThemeStyle.Islands -> if (isDark) { JewelTheme.darkThemeDefinition( - colors = islandsDarkGlobalColors(), + colors = islandsDarkGlobalColors(accent), + iconData = iconData, defaultTextStyle = defaultTextStyle(), disabledAppearanceValues = disabledValues, ) } else { - // Light variant of Dark Islands: standard light theme with Islands blue accent JewelTheme.lightThemeDefinition( - colors = lightIslandsGlobalColors(), + colors = lightIslandsGlobalColors(accent), + iconData = iconData, defaultTextStyle = defaultTextStyle(), disabledAppearanceValues = disabledValues, ) @@ -127,11 +136,15 @@ object ThemeUtils { ThemeStyle.Classic -> if (isDark) { JewelTheme.darkThemeDefinition( + colors = classicDarkGlobalColors(accent), + iconData = iconData, defaultTextStyle = defaultTextStyle(), disabledAppearanceValues = disabledValues, ) } else { JewelTheme.lightThemeDefinition( + colors = classicLightGlobalColors(accent), + iconData = iconData, defaultTextStyle = defaultTextStyle(), disabledAppearanceValues = disabledValues, ) @@ -139,6 +152,25 @@ object ThemeUtils { } } + /** + * Builds the [ComponentStyling] matching the current theme style and accent color. + */ + @Composable + fun buildComponentStyling(): ComponentStyling { + val mainAppState = LocalAppGraph.current.mainAppState + val isDark = isDarkTheme() + val themeStyle = mainAppState.themeStyle.collectAsState().value + val accent = + mainAppState.accentColor + .collectAsState() + .value + .forMode(isDark) + return when (themeStyle) { + ThemeStyle.Islands -> islandsComponentStyling(isDark, accent) + ThemeStyle.Classic -> classicComponentStyling(isDark, accent) + } + } + /** Returns true if the Islands style is active. */ @Composable fun isIslandsStyle(): Boolean { @@ -147,17 +179,17 @@ object ThemeUtils { } /** GlobalColors for the dark variant of the "Islands Dark" VS Code theme. */ - private fun islandsDarkGlobalColors(): GlobalColors = + private fun islandsDarkGlobalColors(accent: Color): GlobalColors = GlobalColors.dark( borders = BorderColors.dark( normal = Color(0xFF3C3F41), - focused = Color(0xFF548AF7), + focused = accent, disabled = Color(0xFF2B2D30), ), outlines = OutlineColors.dark( - focused = Color(0xFF548AF7), + focused = accent, focusedWarning = Color(0xFFE8A33E), focusedError = Color(0xFFF75464), warning = Color(0xFFE8A33E), @@ -177,14 +209,14 @@ object ThemeUtils { /** * GlobalColors for the light variant of Islands: - * standard light palette overridden with the Islands blue accent (#548AF7). + * standard light palette overridden with the accent color. * Canvas (toolwindowBackground) is slightly darker than panel to show rounded card edges. */ - private fun lightIslandsGlobalColors(): GlobalColors = + private fun lightIslandsGlobalColors(accent: Color): GlobalColors = GlobalColors.light( outlines = OutlineColors.light( - focused = Color(0xFF548AF7), + focused = accent, focusedWarning = Color(0xFFE8A33E), focusedError = Color(0xFFF75464), warning = Color(0xFFE8A33E), @@ -192,8 +224,66 @@ object ThemeUtils { ), borders = BorderColors.light( - focused = Color(0xFF548AF7), + focused = accent, ), toolwindowBackground = Color(0xFFE8E9EB), ) + + /** GlobalColors for Classic dark with a custom accent override. */ + private fun classicDarkGlobalColors(accent: Color): GlobalColors = + GlobalColors.dark( + borders = BorderColors.dark(focused = accent), + outlines = + OutlineColors.dark( + focused = accent, + focusedWarning = Color(0xFFE8A33E), + focusedError = Color(0xFFF75464), + warning = Color(0xFFE8A33E), + error = Color(0xFFF75464), + ), + ) + + /** GlobalColors for Classic light with a custom accent override. */ + private fun classicLightGlobalColors(accent: Color): GlobalColors = + GlobalColors.light( + borders = BorderColors.light(focused = accent), + outlines = + OutlineColors.light( + focused = accent, + focusedWarning = Color(0xFFE8A33E), + focusedError = Color(0xFFF75464), + warning = Color(0xFFE8A33E), + error = Color(0xFFF75464), + ), + ) + + /** + * Builds a [ThemeIconData] that patches checkbox/radio SVG colors + * to use the given accent color for their selected state. + */ + private fun accentIconData( + accent: Color, + isDark: Boolean, + ): ThemeIconData { + val hex = accent.toHexString() + val base = if (isDark) IntUiDarkTheme.iconData else IntUiLightTheme.iconData + return ThemeIconData( + iconOverrides = base.iconOverrides, + colorPalette = + base.colorPalette + + mapOf( + "Checkbox.Background.Selected" to hex, + "Checkbox.Border.Selected" to hex, + "Checkbox.Focus.Thin.Selected" to hex, + ), + selectionColorPalette = base.selectionColorPalette, + ) + } + + private fun Color.toHexString(): String { + val r = (red * 255).toInt() + val g = (green * 255).toInt() + val b = (blue * 255).toInt() + return "#%02X%02X%02X".format(r, g, b) + } } diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/settings/AppSettings.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/settings/AppSettings.kt index 8eca1bd6..e61a35cf 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/settings/AppSettings.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/settings/AppSettings.kt @@ -3,6 +3,7 @@ package io.github.kdroidfilter.seforimapp.core.settings import com.russhwolf.settings.Settings import com.russhwolf.settings.get import com.russhwolf.settings.set +import io.github.kdroidfilter.seforimapp.core.presentation.theme.AccentColor import io.github.kdroidfilter.seforimapp.core.presentation.theme.IntUiThemes import io.github.kdroidfilter.seforimapp.core.presentation.theme.ThemeStyle import kotlinx.coroutines.flow.MutableStateFlow @@ -69,6 +70,7 @@ object AppSettings { // Theme configuration private const val KEY_THEME_MODE = "theme_mode" private const val KEY_THEME_STYLE = "theme_style" + private const val KEY_ACCENT_COLOR = "accent_color" // Zmanim widgets visibility private const val KEY_SHOW_ZMANIM_WIDGETS = "show_zmanim_widgets" @@ -437,6 +439,20 @@ object AppSettings { settings[KEY_THEME_STYLE] = style.name } + // Accent color preset + fun getAccentColor(): AccentColor { + val storedValue: String = settings[KEY_ACCENT_COLOR, AccentColor.Default.name] + return try { + AccentColor.valueOf(storedValue) + } catch (_: IllegalArgumentException) { + AccentColor.Default + } + } + + fun setAccentColor(accent: AccentColor) { + settings[KEY_ACCENT_COLOR] = accent.name + } + fun setSavedSessionJson(json: String?) { if (json.isNullOrBlank()) { // Clear legacy and chunked storage diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/bookcontent/ui/panels/bookcontent/views/HomeView.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/bookcontent/ui/panels/bookcontent/views/HomeView.kt index 13e6122b..31ac5ed1 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/bookcontent/ui/panels/bookcontent/views/HomeView.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/bookcontent/ui/panels/bookcontent/views/HomeView.kt @@ -1743,8 +1743,9 @@ private fun FilterButton( onClick: () -> Unit, modifier: Modifier = Modifier, ) { + val accent = JewelTheme.globalColors.outlines.focused val backgroundColor by animateColorAsState( - targetValue = if (isSelected) Color(0xFF0E639C) else Color.Transparent, + targetValue = if (isSelected) accent else Color.Transparent, animationSpec = tween(200), label = "backgroundColor", ) @@ -1781,7 +1782,7 @@ private fun SearchLevelCard( modifier: Modifier = Modifier, ) { val shape = RoundedCornerShape(16.dp) - val backgroundColor = if (selected) Color(0xFF0E639C) else Color.Transparent + val backgroundColor = if (selected) JewelTheme.globalColors.outlines.focused else Color.Transparent val borderColor = if (selected) JewelTheme.globalColors.borders.focused else JewelTheme.globalColors.borders.disabled diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt index 4df0876d..c1c712fb 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt @@ -10,7 +10,9 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -83,6 +85,7 @@ private fun LicenceView( var isChecked by remember { mutableStateOf(false) } val isDark = JewelTheme.isDark + val accent = JewelTheme.globalColors.outlines.focused val textStyle = LocalTextStyle.current.copy( @@ -98,13 +101,22 @@ private fun LicenceView( val h2Padding = PaddingValues(top = 12.dp, bottom = 8.dp) + val linkStyle = SpanStyle(color = accent) + val linkHoveredStyle = SpanStyle(color = accent, textDecoration = TextDecoration.Underline) + val markdownStyling = - remember(isDark, textStyle) { + remember(isDark, textStyle, accent) { // Make Markdown more compact: smaller block spacing and tighter heading paddings. if (isDark) { MarkdownStyling.dark( baseTextStyle = textStyle, - inlinesStyling = InlinesStyling.dark(textStyle), + inlinesStyling = + InlinesStyling.dark( + textStyle, + link = linkStyle, + linkHovered = linkHoveredStyle, + linkVisited = linkStyle, + ), blockVerticalSpacing = 8.dp, heading = MarkdownStyling.Heading.dark( @@ -119,7 +131,13 @@ private fun LicenceView( } else { MarkdownStyling.light( baseTextStyle = textStyle, - inlinesStyling = InlinesStyling.light(textStyle), + inlinesStyling = + InlinesStyling.light( + textStyle, + link = linkStyle, + linkHovered = linkHoveredStyle, + linkVisited = linkStyle, + ), blockVerticalSpacing = 8.dp, heading = MarkdownStyling.Heading.light( diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/SettingsWindow.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/SettingsWindow.kt index af128f46..4ba41432 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/SettingsWindow.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/SettingsWindow.kt @@ -30,8 +30,6 @@ import org.jetbrains.compose.resources.stringResource import org.jetbrains.jewel.foundation.modifier.trackActivation import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme -import org.jetbrains.jewel.intui.standalone.theme.default -import org.jetbrains.jewel.ui.ComponentStyling import org.jetbrains.jewel.ui.Orientation import org.jetbrains.jewel.ui.component.* import org.jetbrains.jewel.ui.icons.AllIconsKeys @@ -52,7 +50,7 @@ private fun SettingsWindowView(onClose: () -> Unit) { NucleusDecoratedWindowTheme(isDark = isDark, titleBarStyle = ThemeUtils.buildCustomTitleBarStyle()) { IntUiTheme( theme = themeDefinition, - styling = ComponentStyling.default(), + styling = ThemeUtils.buildComponentStyling(), ) { val settingsDialogState = rememberDialogState(position = WindowPosition.Aligned(Alignment.Center), size = DpSize(700.dp, 500.dp)) diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt index 7849df45..bd25680c 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt @@ -9,7 +9,9 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -44,6 +46,7 @@ fun AboutSettingsScreen() { @Composable private fun AboutSettingsView() { val isDark = JewelTheme.isDark + val accent = JewelTheme.globalColors.outlines.focused val textStyle = LocalTextStyle.current.copy( @@ -66,12 +69,21 @@ private fun AboutSettingsView() { val h2Padding = PaddingValues(top = 12.dp, bottom = 8.dp) val h3Padding = PaddingValues(top = 10.dp, bottom = 6.dp) + val linkStyle = SpanStyle(color = accent) + val linkHoveredStyle = SpanStyle(color = accent, textDecoration = TextDecoration.Underline) + val markdownStyling = - remember(isDark, textStyle) { + remember(isDark, textStyle, accent) { if (isDark) { MarkdownStyling.dark( baseTextStyle = textStyle, - inlinesStyling = InlinesStyling.dark(textStyle), + inlinesStyling = + InlinesStyling.dark( + textStyle, + link = linkStyle, + linkHovered = linkHoveredStyle, + linkVisited = linkStyle, + ), blockVerticalSpacing = 8.dp, heading = MarkdownStyling.Heading.dark( @@ -91,7 +103,13 @@ private fun AboutSettingsView() { } else { MarkdownStyling.light( baseTextStyle = textStyle, - inlinesStyling = InlinesStyling.light(textStyle), + inlinesStyling = + InlinesStyling.light( + textStyle, + link = linkStyle, + linkHovered = linkHoveredStyle, + linkVisited = linkStyle, + ), blockVerticalSpacing = 8.dp, heading = MarkdownStyling.Heading.light( diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt index 9321ae68..615781f1 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt @@ -9,7 +9,9 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -44,6 +46,7 @@ fun ConditionsSettingsScreen() { @Composable private fun ConditionsSettingsView() { val isDark = JewelTheme.isDark + val accent = JewelTheme.globalColors.outlines.focused val textStyle = LocalTextStyle.current.copy( @@ -66,12 +69,21 @@ private fun ConditionsSettingsView() { val h2Padding = PaddingValues(top = 12.dp, bottom = 8.dp) val h3Padding = PaddingValues(top = 10.dp, bottom = 6.dp) + val linkStyle = SpanStyle(color = accent) + val linkHoveredStyle = SpanStyle(color = accent, textDecoration = TextDecoration.Underline) + val markdownStyling = - remember(isDark, textStyle) { + remember(isDark, textStyle, accent) { if (isDark) { MarkdownStyling.dark( baseTextStyle = textStyle, - inlinesStyling = InlinesStyling.dark(textStyle), + inlinesStyling = + InlinesStyling.dark( + textStyle, + link = linkStyle, + linkHovered = linkHoveredStyle, + linkVisited = linkStyle, + ), blockVerticalSpacing = 8.dp, heading = MarkdownStyling.Heading.dark( @@ -91,7 +103,13 @@ private fun ConditionsSettingsView() { } else { MarkdownStyling.light( baseTextStyle = textStyle, - inlinesStyling = InlinesStyling.light(textStyle), + inlinesStyling = + InlinesStyling.light( + textStyle, + link = linkStyle, + linkHovered = linkHoveredStyle, + linkVisited = linkStyle, + ), blockVerticalSpacing = 8.dp, heading = MarkdownStyling.Heading.light( diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt index 0180279b..b9cc2738 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt @@ -2,12 +2,16 @@ package io.github.kdroidfilter.seforimapp.features.settings.ui import androidx.compose.foundation.background import androidx.compose.foundation.border +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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -15,9 +19,11 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import dev.zacsweers.metrox.viewmodel.metroViewModel +import io.github.kdroidfilter.seforimapp.core.presentation.theme.AccentColor import io.github.kdroidfilter.seforimapp.core.presentation.theme.ThemeStyle import io.github.kdroidfilter.seforimapp.core.presentation.utils.LocalWindowViewModelStoreOwner import io.github.kdroidfilter.seforimapp.features.settings.display.DisplaySettingsEvents @@ -32,6 +38,7 @@ import org.jetbrains.jewel.ui.component.RadioButtonRow import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.VerticallyScrollableContainer import seforimapp.seforimapp.generated.resources.Res +import seforimapp.seforimapp.generated.resources.settings_accent_color_label import seforimapp.seforimapp.generated.resources.settings_compact_mode import seforimapp.seforimapp.generated.resources.settings_compact_mode_description import seforimapp.seforimapp.generated.resources.settings_show_zmanim_widgets @@ -49,11 +56,14 @@ fun DisplaySettingsScreen() { val state by viewModel.state.collectAsState() val mainAppState = LocalAppGraph.current.mainAppState val themeStyle by mainAppState.themeStyle.collectAsState() + val accentColor by mainAppState.accentColor.collectAsState() DisplaySettingsView( state = state, themeStyle = themeStyle, + accentColor = accentColor, onEvent = viewModel::onEvent, onThemeStyleChange = { mainAppState.setThemeStyle(it) }, + onAccentColorChange = { mainAppState.setAccentColor(it) }, ) } @@ -63,6 +73,8 @@ private fun DisplaySettingsView( themeStyle: ThemeStyle, onEvent: (DisplaySettingsEvents) -> Unit, onThemeStyleChange: (ThemeStyle) -> Unit, + accentColor: AccentColor = AccentColor.Default, + onAccentColorChange: (AccentColor) -> Unit = {}, ) { VerticallyScrollableContainer(modifier = Modifier.fillMaxSize()) { Column( @@ -74,6 +86,11 @@ private fun DisplaySettingsView( ) { ThemeStyleCard(themeStyle = themeStyle, onStyleChange = onThemeStyleChange) + AccentColorCard( + selectedAccent = accentColor, + onAccentChange = onAccentColorChange, + ) + SettingCard( title = Res.string.settings_show_zmanim_widgets, description = Res.string.settings_show_zmanim_widgets_description, @@ -138,6 +155,49 @@ private fun ThemeStyleCard( } } +@Composable +private fun AccentColorCard( + selectedAccent: AccentColor, + onAccentChange: (AccentColor) -> Unit, +) { + val shape = RoundedCornerShape(8.dp) + Column( + modifier = + Modifier + .fillMaxWidth() + .clip(shape) + .border(1.dp, JewelTheme.globalColors.borders.normal, shape) + .background(JewelTheme.globalColors.panelBackground) + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text(text = stringResource(Res.string.settings_accent_color_label)) + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AccentColor.entries.forEach { accent -> + val displayColor = accent.forMode(JewelTheme.isDark) + val isSelected = selectedAccent == accent + Box( + modifier = + Modifier + .size(32.dp) + .clip(CircleShape) + .background(displayColor, CircleShape) + .then( + if (isSelected) { + Modifier.border(2.5.dp, JewelTheme.globalColors.text.normal, CircleShape) + } else { + Modifier.border(1.dp, Color.Black.copy(alpha = 0.2f), CircleShape) + }, + ).clickable { onAccentChange(accent) }, + ) + } + } + } +} + @Composable @Preview private fun DisplaySettingsView_Preview() { diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt index f36a16e5..afd0ce32 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt @@ -43,10 +43,7 @@ import io.github.kdroidfilter.seforim.tabs.TabsEvents import io.github.kdroidfilter.seforimapp.core.TextSelectionStore import io.github.kdroidfilter.seforimapp.core.presentation.components.MainTitleBar import io.github.kdroidfilter.seforimapp.core.presentation.tabs.TabsContent -import io.github.kdroidfilter.seforimapp.core.presentation.theme.ThemeStyle import io.github.kdroidfilter.seforimapp.core.presentation.theme.ThemeUtils -import io.github.kdroidfilter.seforimapp.core.presentation.theme.classicComponentStyling -import io.github.kdroidfilter.seforimapp.core.presentation.theme.islandsComponentStyling import io.github.kdroidfilter.seforimapp.core.presentation.utils.LocalWindowViewModelStoreOwner import io.github.kdroidfilter.seforimapp.core.presentation.utils.processKeyShortcuts import io.github.kdroidfilter.seforimapp.core.presentation.utils.rememberWindowViewModelStoreOwner @@ -244,15 +241,8 @@ fun main() { ) { val isDark = ThemeUtils.isDarkTheme() val themeDefinition = ThemeUtils.buildThemeDefinition() - val themeStyle by mainAppState.themeStyle.collectAsState() - val customTitleBarStyle = ThemeUtils.buildCustomTitleBarStyle() - - val componentStyling = - when (themeStyle) { - ThemeStyle.Islands -> islandsComponentStyling(isDark) - ThemeStyle.Classic -> classicComponentStyling(isDark) - } + val componentStyling = ThemeUtils.buildComponentStyling() NucleusDecoratedWindowTheme( isDark = isDark, From 0bb7795ea90ce87620b54ea8b2b8c1329cfa98a5 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Sat, 7 Mar 2026 21:30:00 +0200 Subject: [PATCH 2/6] refactor: extract shared AccentMarkdownView composable Deduplicate the markdown styling+rendering pattern from AboutSettingsScreen, ConditionsSettingsScreen, and LicenceScreen into a single reusable AccentMarkdownView composable. --- .../components/AccentMarkdownView.kt | 201 +++++++++++++++++ .../onboarding/licence/LicenceScreen.kt | 203 ++++-------------- .../settings/ui/AboutSettingsScreen.kt | 183 +--------------- .../settings/ui/ConditionsSettingsScreen.kt | 183 +--------------- 4 files changed, 246 insertions(+), 524 deletions(-) create mode 100644 SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/AccentMarkdownView.kt diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/AccentMarkdownView.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/AccentMarkdownView.kt new file mode 100644 index 00000000..20d473ea --- /dev/null +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/AccentMarkdownView.kt @@ -0,0 +1,201 @@ +@file:OptIn(ExperimentalJewelApi::class) + +package io.github.kdroidfilter.seforimapp.core.presentation.components + +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +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.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.Font +import org.jetbrains.jewel.foundation.ExperimentalJewelApi +import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.foundation.theme.LocalTextStyle +import org.jetbrains.jewel.intui.markdown.standalone.ProvideMarkdownStyling +import org.jetbrains.jewel.intui.markdown.standalone.dark +import org.jetbrains.jewel.intui.markdown.standalone.light +import org.jetbrains.jewel.intui.markdown.standalone.styling.dark +import org.jetbrains.jewel.intui.markdown.standalone.styling.light +import org.jetbrains.jewel.markdown.MarkdownBlock +import org.jetbrains.jewel.markdown.extensions.autolink.AutolinkProcessorExtension +import org.jetbrains.jewel.markdown.processing.MarkdownProcessor +import org.jetbrains.jewel.markdown.rendering.InlinesStyling +import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer +import org.jetbrains.jewel.markdown.rendering.MarkdownStyling +import org.jetbrains.jewel.ui.component.VerticallyScrollableContainer +import org.jetbrains.jewel.ui.component.scrollbarContentSafePadding +import org.jetbrains.jewel.ui.typography +import seforimapp.seforimapp.generated.resources.Res +import seforimapp.seforimapp.generated.resources.notoserifhebrew +import java.awt.Desktop.getDesktop +import java.net.URI.create + +/** + * Renders a markdown resource file with accent-colored links and the app's Hebrew font. + * + * @param resourcePath path inside composeResources (e.g. "files/ABOUT.md") + * @param modifier modifier for the outer container + * @param includeH3 whether to include custom h3 heading styling (About/Conditions use it, Licence doesn't) + * @param extraItems additional items appended after the markdown blocks (e.g. a checkbox) + */ +@Composable +fun AccentMarkdownView( + resourcePath: String, + modifier: Modifier = Modifier, + includeH3: Boolean = true, + extraItems: (LazyListScope.() -> Unit)? = null, +) { + val isDark = JewelTheme.isDark + val accent = JewelTheme.globalColors.outlines.focused + + val textStyle = + LocalTextStyle.current.copy( + fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), + fontSize = 14.sp, + ) + + val h2TextStyle = + LocalTextStyle.current.copy( + fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), + fontSize = JewelTheme.typography.h2TextStyle.fontSize, + ) + + val h3TextStyle = + LocalTextStyle.current.copy( + fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), + fontSize = JewelTheme.typography.h3TextStyle.fontSize, + ) + + val h2Padding = PaddingValues(top = 12.dp, bottom = 8.dp) + val h3Padding = PaddingValues(top = 10.dp, bottom = 6.dp) + + val linkStyle = SpanStyle(color = accent) + val linkHoveredStyle = SpanStyle(color = accent, textDecoration = TextDecoration.Underline) + + val markdownStyling = + remember(isDark, textStyle, accent, includeH3) { + val inlines = + if (isDark) { + InlinesStyling.dark(textStyle, link = linkStyle, linkHovered = linkHoveredStyle, linkVisited = linkStyle) + } else { + InlinesStyling.light(textStyle, link = linkStyle, linkHovered = linkHoveredStyle, linkVisited = linkStyle) + } + + val heading = buildHeading(isDark, textStyle, h2TextStyle, h2Padding, h3TextStyle, h3Padding, includeH3) + + if (isDark) { + MarkdownStyling.dark( + baseTextStyle = textStyle, + inlinesStyling = inlines, + blockVerticalSpacing = 8.dp, + heading = heading, + ) + } else { + MarkdownStyling.light( + baseTextStyle = textStyle, + inlinesStyling = inlines, + blockVerticalSpacing = 8.dp, + heading = heading, + ) + } + } + + val processor = + remember { + MarkdownProcessor(listOf(AutolinkProcessorExtension)) + } + + val blockRenderer = + remember(markdownStyling) { + if (isDark) MarkdownBlockRenderer.dark(styling = markdownStyling) else MarkdownBlockRenderer.light(styling = markdownStyling) + } + + var blocks by remember { mutableStateOf(emptyList()) } + LaunchedEffect(resourcePath) { + val bytes = Res.readBytes(resourcePath) + blocks = processor.processMarkdownDocument(bytes.decodeToString()) + } + + ProvideMarkdownStyling(markdownStyling, blockRenderer, NoOpCodeHighlighter) { + val lazyListState = rememberLazyListState() + VerticallyScrollableContainer(lazyListState as ScrollableState) { + LazyColumn( + modifier = modifier.fillMaxSize(), + state = lazyListState, + contentPadding = + PaddingValues( + start = 8.dp, + top = 8.dp, + end = 8.dp + scrollbarContentSafePadding(), + bottom = 16.dp, + ), + verticalArrangement = Arrangement.spacedBy(markdownStyling.blockVerticalSpacing), + ) { + items(blocks) { block -> + blockRenderer.RenderBlock( + block = block, + enabled = true, + onUrlClick = { url: String -> getDesktop().browse(create(url)) }, + modifier = Modifier.fillMaxWidth(), + ) + } + extraItems?.invoke(this) + } + } + } +} + +private fun buildHeading( + isDark: Boolean, + textStyle: TextStyle, + h2TextStyle: TextStyle, + h2Padding: PaddingValues, + h3TextStyle: TextStyle, + h3Padding: PaddingValues, + includeH3: Boolean, +): MarkdownStyling.Heading = + if (isDark) { + if (includeH3) { + MarkdownStyling.Heading.dark( + baseTextStyle = textStyle, + h2 = MarkdownStyling.Heading.H2.dark(baseTextStyle = h2TextStyle, padding = h2Padding), + h3 = MarkdownStyling.Heading.H3.dark(baseTextStyle = h3TextStyle, padding = h3Padding), + ) + } else { + MarkdownStyling.Heading.dark( + baseTextStyle = textStyle, + h2 = MarkdownStyling.Heading.H2.dark(baseTextStyle = h2TextStyle, padding = h2Padding), + ) + } + } else { + if (includeH3) { + MarkdownStyling.Heading.light( + baseTextStyle = textStyle, + h2 = MarkdownStyling.Heading.H2.light(baseTextStyle = h2TextStyle, padding = h2Padding), + h3 = MarkdownStyling.Heading.H3.light(baseTextStyle = h3TextStyle, padding = h3Padding), + ) + } else { + MarkdownStyling.Heading.light( + baseTextStyle = textStyle, + h2 = MarkdownStyling.Heading.H2.light(baseTextStyle = h2TextStyle, padding = h2Padding), + ) + } + } diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt index c1c712fb..16ca57ce 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt @@ -1,50 +1,37 @@ -@file:OptIn(ExperimentalJewelApi::class) - package io.github.kdroidfilter.seforimapp.features.onboarding.licence -import androidx.compose.foundation.gestures.ScrollableState -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Arrangement +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.layout.width +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.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import io.github.kdroidfilter.seforimapp.core.presentation.components.AccentMarkdownView import io.github.kdroidfilter.seforimapp.features.onboarding.navigation.OnBoardingDestination import io.github.kdroidfilter.seforimapp.features.onboarding.navigation.ProgressBarState import io.github.kdroidfilter.seforimapp.features.onboarding.ui.components.OnBoardingScaffold import io.github.kdroidfilter.seforimapp.framework.database.DatabaseVersionManager import io.github.kdroidfilter.seforimapp.framework.database.getDatabasePath import io.github.kdroidfilter.seforimapp.theme.PreviewContainer -import org.jetbrains.compose.resources.Font import org.jetbrains.compose.resources.stringResource -import org.jetbrains.jewel.foundation.ExperimentalJewelApi -import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter -import org.jetbrains.jewel.foundation.theme.JewelTheme -import org.jetbrains.jewel.foundation.theme.LocalTextStyle -import org.jetbrains.jewel.intui.markdown.standalone.ProvideMarkdownStyling -import org.jetbrains.jewel.intui.markdown.standalone.dark -import org.jetbrains.jewel.intui.markdown.standalone.light -import org.jetbrains.jewel.intui.markdown.standalone.styling.dark -import org.jetbrains.jewel.intui.markdown.standalone.styling.light -import org.jetbrains.jewel.markdown.MarkdownBlock -import org.jetbrains.jewel.markdown.extensions.autolink.AutolinkProcessorExtension -import org.jetbrains.jewel.markdown.processing.MarkdownProcessor -import org.jetbrains.jewel.markdown.rendering.InlinesStyling -import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer -import org.jetbrains.jewel.markdown.rendering.MarkdownStyling -import org.jetbrains.jewel.ui.component.* -import org.jetbrains.jewel.ui.typography -import seforimapp.seforimapp.generated.resources.* -import java.awt.Desktop.getDesktop -import java.net.URI.create +import org.jetbrains.jewel.ui.component.Checkbox +import org.jetbrains.jewel.ui.component.DefaultButton +import org.jetbrains.jewel.ui.component.Text +import seforimapp.seforimapp.generated.resources.Res +import seforimapp.seforimapp.generated.resources.license_accept_checkbox +import seforimapp.seforimapp.generated.resources.license_screen_title +import seforimapp.seforimapp.generated.resources.next_button @Composable fun LicenceScreen( @@ -84,74 +71,6 @@ private fun LicenceView( ) { var isChecked by remember { mutableStateOf(false) } - val isDark = JewelTheme.isDark - val accent = JewelTheme.globalColors.outlines.focused - - val textStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = 14.sp, - ) - - val h2TextStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = JewelTheme.typography.h2TextStyle.fontSize, - ) - - val h2Padding = PaddingValues(top = 12.dp, bottom = 8.dp) - - val linkStyle = SpanStyle(color = accent) - val linkHoveredStyle = SpanStyle(color = accent, textDecoration = TextDecoration.Underline) - - val markdownStyling = - remember(isDark, textStyle, accent) { - // Make Markdown more compact: smaller block spacing and tighter heading paddings. - if (isDark) { - MarkdownStyling.dark( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.dark( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.dark( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.dark( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - ), - ) - } else { - MarkdownStyling.light( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.light( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.light( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.light( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - ), - ) - } - } - OnBoardingScaffold( title = stringResource(Res.string.license_screen_title), bottomAction = { @@ -160,73 +79,25 @@ private fun LicenceView( } }, ) { - var bytes by remember { mutableStateOf(ByteArray(0)) } - var blocks by remember { mutableStateOf(emptyList()) } - val processor = - remember { - MarkdownProcessor( - listOf( - AutolinkProcessorExtension, - ), - ) - } - val blockRenderer = - remember(markdownStyling) { - if (isDark) { - MarkdownBlockRenderer.dark( - styling = markdownStyling, - ) - } else { - MarkdownBlockRenderer.light( - styling = markdownStyling, - ) - } - } - - LaunchedEffect(Unit) { - bytes = Res.readBytes("files/CONDITIONS.md") - // Process the loaded markdown into renderable blocks - blocks = processor.processMarkdownDocument(bytes.decodeToString()) - } - ProvideMarkdownStyling(markdownStyling, blockRenderer, NoOpCodeHighlighter) { - val lazyListState = rememberLazyListState() - VerticallyScrollableContainer(lazyListState as ScrollableState) { - LazyColumn( - modifier = Modifier.fillMaxWidth(), - state = lazyListState, - contentPadding = - PaddingValues( - start = 8.dp, - top = 8.dp, - end = 8.dp + scrollbarContentSafePadding(), - bottom = 16.dp, - ), - verticalArrangement = Arrangement.spacedBy(markdownStyling.blockVerticalSpacing), - ) { - items(blocks) { block -> - blockRenderer.RenderBlock( - block = block, - enabled = true, - onUrlClick = { url: String -> getDesktop().browse(create(url)) }, - modifier = Modifier.fillMaxWidth(), - ) - } - - item { - Spacer(Modifier.height(8.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - ) { - Checkbox(checked = isChecked, onCheckedChange = { isChecked = it }) - Spacer(Modifier.width(2.dp)) - Text(text = stringResource(Res.string.license_accept_checkbox)) - } + AccentMarkdownView( + resourcePath = "files/CONDITIONS.md", + modifier = Modifier.fillMaxWidth(), + includeH3 = false, + extraItems = { + item { + Spacer(Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Checkbox(checked = isChecked, onCheckedChange = { isChecked = it }) + Spacer(Modifier.width(2.dp)) + Text(text = stringResource(Res.string.license_accept_checkbox)) } } - } - } + }, + ) } } diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt index bd25680c..b22f9fc1 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt @@ -1,192 +1,17 @@ -@file:OptIn(ExperimentalJewelApi::class) - package io.github.kdroidfilter.seforimapp.features.settings.ui -import androidx.compose.foundation.gestures.ScrollableState -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import io.github.kdroidfilter.seforimapp.core.presentation.components.AccentMarkdownView import io.github.kdroidfilter.seforimapp.theme.PreviewContainer -import org.jetbrains.compose.resources.Font -import org.jetbrains.jewel.foundation.ExperimentalJewelApi -import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter -import org.jetbrains.jewel.foundation.theme.JewelTheme -import org.jetbrains.jewel.foundation.theme.LocalTextStyle -import org.jetbrains.jewel.intui.markdown.standalone.ProvideMarkdownStyling -import org.jetbrains.jewel.intui.markdown.standalone.dark -import org.jetbrains.jewel.intui.markdown.standalone.light -import org.jetbrains.jewel.intui.markdown.standalone.styling.dark -import org.jetbrains.jewel.intui.markdown.standalone.styling.light -import org.jetbrains.jewel.markdown.MarkdownBlock -import org.jetbrains.jewel.markdown.extensions.autolink.AutolinkProcessorExtension -import org.jetbrains.jewel.markdown.processing.MarkdownProcessor -import org.jetbrains.jewel.markdown.rendering.InlinesStyling -import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer -import org.jetbrains.jewel.markdown.rendering.MarkdownStyling -import org.jetbrains.jewel.ui.component.* -import org.jetbrains.jewel.ui.typography -import seforimapp.seforimapp.generated.resources.* -import java.awt.Desktop.getDesktop -import java.net.URI.create @Composable fun AboutSettingsScreen() { - AboutSettingsView() -} - -@Composable -private fun AboutSettingsView() { - val isDark = JewelTheme.isDark - val accent = JewelTheme.globalColors.outlines.focused - - val textStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = 14.sp, - ) - - val h2TextStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = JewelTheme.typography.h2TextStyle.fontSize, - ) - - val h3TextStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = JewelTheme.typography.h3TextStyle.fontSize, - ) - - val h2Padding = PaddingValues(top = 12.dp, bottom = 8.dp) - val h3Padding = PaddingValues(top = 10.dp, bottom = 6.dp) - - val linkStyle = SpanStyle(color = accent) - val linkHoveredStyle = SpanStyle(color = accent, textDecoration = TextDecoration.Underline) - - val markdownStyling = - remember(isDark, textStyle, accent) { - if (isDark) { - MarkdownStyling.dark( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.dark( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.dark( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.dark( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - h3 = - MarkdownStyling.Heading.H3.dark( - baseTextStyle = h3TextStyle, - padding = h3Padding, - ), - ), - ) - } else { - MarkdownStyling.light( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.light( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.light( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.light( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - h3 = - MarkdownStyling.Heading.H3.light( - baseTextStyle = h3TextStyle, - padding = h3Padding, - ), - ), - ) - } - } - - var bytes by remember { mutableStateOf(ByteArray(0)) } - var blocks by remember { mutableStateOf(emptyList()) } - val processor = - remember { - MarkdownProcessor( - listOf( - AutolinkProcessorExtension, - ), - ) - } - val blockRenderer = - remember(markdownStyling) { - if (isDark) { - MarkdownBlockRenderer.dark( - styling = markdownStyling, - ) - } else { - MarkdownBlockRenderer.light( - styling = markdownStyling, - ) - } - } - - LaunchedEffect(Unit) { - bytes = Res.readBytes("files/ABOUT.md") - blocks = processor.processMarkdownDocument(bytes.decodeToString()) - } - - ProvideMarkdownStyling(markdownStyling, blockRenderer, NoOpCodeHighlighter) { - val lazyListState = rememberLazyListState() - VerticallyScrollableContainer(lazyListState as ScrollableState) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = lazyListState, - contentPadding = - PaddingValues( - start = 8.dp, - top = 8.dp, - end = 8.dp + scrollbarContentSafePadding(), - bottom = 16.dp, - ), - verticalArrangement = Arrangement.spacedBy(markdownStyling.blockVerticalSpacing), - ) { - items(blocks) { block -> - blockRenderer.RenderBlock( - block = block, - enabled = true, - onUrlClick = { url: String -> getDesktop().browse(create(url)) }, - modifier = Modifier.fillMaxWidth(), - ) - } - } - } - } + AccentMarkdownView(resourcePath = "files/ABOUT.md") } @Composable @Preview private fun AboutSettingsViewPreview() { - PreviewContainer { AboutSettingsView() } + PreviewContainer { AboutSettingsScreen() } } diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt index 615781f1..8e2cd8d4 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt @@ -1,192 +1,17 @@ -@file:OptIn(ExperimentalJewelApi::class) - package io.github.kdroidfilter.seforimapp.features.settings.ui -import androidx.compose.foundation.gestures.ScrollableState -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import io.github.kdroidfilter.seforimapp.core.presentation.components.AccentMarkdownView import io.github.kdroidfilter.seforimapp.theme.PreviewContainer -import org.jetbrains.compose.resources.Font -import org.jetbrains.jewel.foundation.ExperimentalJewelApi -import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter -import org.jetbrains.jewel.foundation.theme.JewelTheme -import org.jetbrains.jewel.foundation.theme.LocalTextStyle -import org.jetbrains.jewel.intui.markdown.standalone.ProvideMarkdownStyling -import org.jetbrains.jewel.intui.markdown.standalone.dark -import org.jetbrains.jewel.intui.markdown.standalone.light -import org.jetbrains.jewel.intui.markdown.standalone.styling.dark -import org.jetbrains.jewel.intui.markdown.standalone.styling.light -import org.jetbrains.jewel.markdown.MarkdownBlock -import org.jetbrains.jewel.markdown.extensions.autolink.AutolinkProcessorExtension -import org.jetbrains.jewel.markdown.processing.MarkdownProcessor -import org.jetbrains.jewel.markdown.rendering.InlinesStyling -import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer -import org.jetbrains.jewel.markdown.rendering.MarkdownStyling -import org.jetbrains.jewel.ui.component.* -import org.jetbrains.jewel.ui.typography -import seforimapp.seforimapp.generated.resources.* -import java.awt.Desktop.getDesktop -import java.net.URI.create @Composable fun ConditionsSettingsScreen() { - ConditionsSettingsView() -} - -@Composable -private fun ConditionsSettingsView() { - val isDark = JewelTheme.isDark - val accent = JewelTheme.globalColors.outlines.focused - - val textStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = 14.sp, - ) - - val h2TextStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = JewelTheme.typography.h2TextStyle.fontSize, - ) - - val h3TextStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = JewelTheme.typography.h3TextStyle.fontSize, - ) - - val h2Padding = PaddingValues(top = 12.dp, bottom = 8.dp) - val h3Padding = PaddingValues(top = 10.dp, bottom = 6.dp) - - val linkStyle = SpanStyle(color = accent) - val linkHoveredStyle = SpanStyle(color = accent, textDecoration = TextDecoration.Underline) - - val markdownStyling = - remember(isDark, textStyle, accent) { - if (isDark) { - MarkdownStyling.dark( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.dark( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.dark( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.dark( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - h3 = - MarkdownStyling.Heading.H3.dark( - baseTextStyle = h3TextStyle, - padding = h3Padding, - ), - ), - ) - } else { - MarkdownStyling.light( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.light( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.light( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.light( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - h3 = - MarkdownStyling.Heading.H3.light( - baseTextStyle = h3TextStyle, - padding = h3Padding, - ), - ), - ) - } - } - - var bytes by remember { mutableStateOf(ByteArray(0)) } - var blocks by remember { mutableStateOf(emptyList()) } - val processor = - remember { - MarkdownProcessor( - listOf( - AutolinkProcessorExtension, - ), - ) - } - val blockRenderer = - remember(markdownStyling) { - if (isDark) { - MarkdownBlockRenderer.dark( - styling = markdownStyling, - ) - } else { - MarkdownBlockRenderer.light( - styling = markdownStyling, - ) - } - } - - LaunchedEffect(Unit) { - bytes = Res.readBytes("files/CONDITIONS.md") - blocks = processor.processMarkdownDocument(bytes.decodeToString()) - } - - ProvideMarkdownStyling(markdownStyling, blockRenderer, NoOpCodeHighlighter) { - val lazyListState = rememberLazyListState() - VerticallyScrollableContainer(lazyListState as ScrollableState) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = lazyListState, - contentPadding = - PaddingValues( - start = 8.dp, - top = 8.dp, - end = 8.dp + scrollbarContentSafePadding(), - bottom = 16.dp, - ), - verticalArrangement = Arrangement.spacedBy(markdownStyling.blockVerticalSpacing), - ) { - items(blocks) { block -> - blockRenderer.RenderBlock( - block = block, - enabled = true, - onUrlClick = { url: String -> getDesktop().browse(create(url)) }, - modifier = Modifier.fillMaxWidth(), - ) - } - } - } - } + AccentMarkdownView(resourcePath = "files/CONDITIONS.md") } @Composable @Preview private fun ConditionsSettingsViewPreview() { - PreviewContainer { ConditionsSettingsView() } + PreviewContainer { ConditionsSettingsScreen() } } From 25a598544a62041597e339b21ea30b074e1b58ae Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Sat, 7 Mar 2026 21:30:00 +0200 Subject: [PATCH 3/6] refactor: extract shared AccentMarkdownView composable Deduplicate the markdown styling+rendering pattern from AboutSettingsScreen, ConditionsSettingsScreen, and LicenceScreen into a single reusable AccentMarkdownView composable. --- .../components/AccentMarkdownView.kt | 206 ++++++++++++++++++ .../onboarding/licence/LicenceScreen.kt | 203 ++++------------- .../settings/ui/AboutSettingsScreen.kt | 183 +--------------- .../settings/ui/ConditionsSettingsScreen.kt | 183 +--------------- 4 files changed, 251 insertions(+), 524 deletions(-) create mode 100644 SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/AccentMarkdownView.kt diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/AccentMarkdownView.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/AccentMarkdownView.kt new file mode 100644 index 00000000..fc343447 --- /dev/null +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/AccentMarkdownView.kt @@ -0,0 +1,206 @@ +@file:OptIn(ExperimentalJewelApi::class) + +package io.github.kdroidfilter.seforimapp.core.presentation.components + +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +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.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.Font +import org.jetbrains.jewel.foundation.ExperimentalJewelApi +import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.foundation.theme.LocalTextStyle +import org.jetbrains.jewel.intui.markdown.standalone.ProvideMarkdownStyling +import org.jetbrains.jewel.intui.markdown.standalone.dark +import org.jetbrains.jewel.intui.markdown.standalone.light +import org.jetbrains.jewel.intui.markdown.standalone.styling.dark +import org.jetbrains.jewel.intui.markdown.standalone.styling.light +import org.jetbrains.jewel.markdown.MarkdownBlock +import org.jetbrains.jewel.markdown.extensions.autolink.AutolinkProcessorExtension +import org.jetbrains.jewel.markdown.processing.MarkdownProcessor +import org.jetbrains.jewel.markdown.rendering.InlinesStyling +import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer +import org.jetbrains.jewel.markdown.rendering.MarkdownStyling +import org.jetbrains.jewel.ui.component.VerticallyScrollableContainer +import org.jetbrains.jewel.ui.component.scrollbarContentSafePadding +import org.jetbrains.jewel.ui.typography +import seforimapp.seforimapp.generated.resources.Res +import seforimapp.seforimapp.generated.resources.notoserifhebrew +import java.awt.Desktop.getDesktop +import java.net.URI.create + +/** + * Renders a markdown resource file with accent-colored links and the app's Hebrew font. + * + * @param resourcePath path inside composeResources (e.g. "files/ABOUT.md") + * @param modifier modifier for the outer container + * @param includeH3 whether to include custom h3 heading styling (About/Conditions use it, Licence doesn't) + * @param extraItems additional items appended after the markdown blocks (e.g. a checkbox) + */ +@Composable +fun AccentMarkdownView( + resourcePath: String, + modifier: Modifier = Modifier, + includeH3: Boolean = true, + extraItems: (LazyListScope.() -> Unit)? = null, +) { + val isDark = JewelTheme.isDark + val accent = JewelTheme.globalColors.outlines.focused + + val textStyle = + LocalTextStyle.current.copy( + fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), + fontSize = 14.sp, + ) + + val h2TextStyle = + LocalTextStyle.current.copy( + fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), + fontSize = JewelTheme.typography.h2TextStyle.fontSize, + ) + + val h3TextStyle = + LocalTextStyle.current.copy( + fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), + fontSize = JewelTheme.typography.h3TextStyle.fontSize, + ) + + val h2Padding = PaddingValues(top = 12.dp, bottom = 8.dp) + val h3Padding = PaddingValues(top = 10.dp, bottom = 6.dp) + + val linkStyle = SpanStyle(color = accent) + val linkHoveredStyle = SpanStyle(color = accent, textDecoration = TextDecoration.Underline) + + val markdownStyling = + remember(isDark, textStyle, accent, includeH3) { + val inlines = + if (isDark) { + InlinesStyling.dark(textStyle, link = linkStyle, linkHovered = linkHoveredStyle, linkVisited = linkStyle) + } else { + InlinesStyling.light(textStyle, link = linkStyle, linkHovered = linkHoveredStyle, linkVisited = linkStyle) + } + + val heading = buildHeading(isDark, textStyle, h2TextStyle, h2Padding, h3TextStyle, h3Padding, includeH3) + + if (isDark) { + MarkdownStyling.dark( + baseTextStyle = textStyle, + inlinesStyling = inlines, + blockVerticalSpacing = 8.dp, + heading = heading, + ) + } else { + MarkdownStyling.light( + baseTextStyle = textStyle, + inlinesStyling = inlines, + blockVerticalSpacing = 8.dp, + heading = heading, + ) + } + } + + val processor = + remember { + MarkdownProcessor(listOf(AutolinkProcessorExtension)) + } + + val blockRenderer = + remember(markdownStyling) { + if (isDark) MarkdownBlockRenderer.dark(styling = markdownStyling) else MarkdownBlockRenderer.light(styling = markdownStyling) + } + + var blocks by remember { mutableStateOf(emptyList()) } + LaunchedEffect(resourcePath) { + val bytes = Res.readBytes(resourcePath) + blocks = processor.processMarkdownDocument(bytes.decodeToString()) + } + + ProvideMarkdownStyling(markdownStyling, blockRenderer, NoOpCodeHighlighter) { + val lazyListState = rememberLazyListState() + VerticallyScrollableContainer(lazyListState as ScrollableState) { + LazyColumn( + modifier = modifier.fillMaxSize(), + state = lazyListState, + contentPadding = + PaddingValues( + start = 8.dp, + top = 8.dp, + end = 8.dp + scrollbarContentSafePadding(), + bottom = 16.dp, + ), + verticalArrangement = Arrangement.spacedBy(markdownStyling.blockVerticalSpacing), + ) { + items(blocks) { block -> + blockRenderer.RenderBlock( + block = block, + enabled = true, + onUrlClick = { url: String -> getDesktop().browse(create(url)) }, + modifier = Modifier.fillMaxWidth(), + ) + } + extraItems?.invoke(this) + } + } + } +} + +private fun buildHeading( + isDark: Boolean, + textStyle: TextStyle, + h2TextStyle: TextStyle, + h2Padding: PaddingValues, + h3TextStyle: TextStyle, + h3Padding: PaddingValues, + includeH3: Boolean, +): MarkdownStyling.Heading { + val h2 = + if (isDark) { + MarkdownStyling.Heading.H2.dark(baseTextStyle = h2TextStyle, padding = h2Padding) + } else { + MarkdownStyling.Heading.H2.light(baseTextStyle = h2TextStyle, padding = h2Padding) + } + + val h3 = + if (includeH3) { + if (isDark) { + MarkdownStyling.Heading.H3.dark(baseTextStyle = h3TextStyle, padding = h3Padding) + } else { + MarkdownStyling.Heading.H3.light(baseTextStyle = h3TextStyle, padding = h3Padding) + } + } else { + null + } + + return if (isDark) { + if (h3 != null) { + MarkdownStyling.Heading.dark(baseTextStyle = textStyle, h2 = h2, h3 = h3) + } else { + MarkdownStyling.Heading.dark(baseTextStyle = textStyle, h2 = h2) + } + } else { + if (h3 != null) { + MarkdownStyling.Heading.light(baseTextStyle = textStyle, h2 = h2, h3 = h3) + } else { + MarkdownStyling.Heading.light(baseTextStyle = textStyle, h2 = h2) + } + } +} diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt index c1c712fb..16ca57ce 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/onboarding/licence/LicenceScreen.kt @@ -1,50 +1,37 @@ -@file:OptIn(ExperimentalJewelApi::class) - package io.github.kdroidfilter.seforimapp.features.onboarding.licence -import androidx.compose.foundation.gestures.ScrollableState -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Arrangement +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.layout.width +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.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import io.github.kdroidfilter.seforimapp.core.presentation.components.AccentMarkdownView import io.github.kdroidfilter.seforimapp.features.onboarding.navigation.OnBoardingDestination import io.github.kdroidfilter.seforimapp.features.onboarding.navigation.ProgressBarState import io.github.kdroidfilter.seforimapp.features.onboarding.ui.components.OnBoardingScaffold import io.github.kdroidfilter.seforimapp.framework.database.DatabaseVersionManager import io.github.kdroidfilter.seforimapp.framework.database.getDatabasePath import io.github.kdroidfilter.seforimapp.theme.PreviewContainer -import org.jetbrains.compose.resources.Font import org.jetbrains.compose.resources.stringResource -import org.jetbrains.jewel.foundation.ExperimentalJewelApi -import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter -import org.jetbrains.jewel.foundation.theme.JewelTheme -import org.jetbrains.jewel.foundation.theme.LocalTextStyle -import org.jetbrains.jewel.intui.markdown.standalone.ProvideMarkdownStyling -import org.jetbrains.jewel.intui.markdown.standalone.dark -import org.jetbrains.jewel.intui.markdown.standalone.light -import org.jetbrains.jewel.intui.markdown.standalone.styling.dark -import org.jetbrains.jewel.intui.markdown.standalone.styling.light -import org.jetbrains.jewel.markdown.MarkdownBlock -import org.jetbrains.jewel.markdown.extensions.autolink.AutolinkProcessorExtension -import org.jetbrains.jewel.markdown.processing.MarkdownProcessor -import org.jetbrains.jewel.markdown.rendering.InlinesStyling -import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer -import org.jetbrains.jewel.markdown.rendering.MarkdownStyling -import org.jetbrains.jewel.ui.component.* -import org.jetbrains.jewel.ui.typography -import seforimapp.seforimapp.generated.resources.* -import java.awt.Desktop.getDesktop -import java.net.URI.create +import org.jetbrains.jewel.ui.component.Checkbox +import org.jetbrains.jewel.ui.component.DefaultButton +import org.jetbrains.jewel.ui.component.Text +import seforimapp.seforimapp.generated.resources.Res +import seforimapp.seforimapp.generated.resources.license_accept_checkbox +import seforimapp.seforimapp.generated.resources.license_screen_title +import seforimapp.seforimapp.generated.resources.next_button @Composable fun LicenceScreen( @@ -84,74 +71,6 @@ private fun LicenceView( ) { var isChecked by remember { mutableStateOf(false) } - val isDark = JewelTheme.isDark - val accent = JewelTheme.globalColors.outlines.focused - - val textStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = 14.sp, - ) - - val h2TextStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = JewelTheme.typography.h2TextStyle.fontSize, - ) - - val h2Padding = PaddingValues(top = 12.dp, bottom = 8.dp) - - val linkStyle = SpanStyle(color = accent) - val linkHoveredStyle = SpanStyle(color = accent, textDecoration = TextDecoration.Underline) - - val markdownStyling = - remember(isDark, textStyle, accent) { - // Make Markdown more compact: smaller block spacing and tighter heading paddings. - if (isDark) { - MarkdownStyling.dark( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.dark( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.dark( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.dark( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - ), - ) - } else { - MarkdownStyling.light( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.light( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.light( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.light( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - ), - ) - } - } - OnBoardingScaffold( title = stringResource(Res.string.license_screen_title), bottomAction = { @@ -160,73 +79,25 @@ private fun LicenceView( } }, ) { - var bytes by remember { mutableStateOf(ByteArray(0)) } - var blocks by remember { mutableStateOf(emptyList()) } - val processor = - remember { - MarkdownProcessor( - listOf( - AutolinkProcessorExtension, - ), - ) - } - val blockRenderer = - remember(markdownStyling) { - if (isDark) { - MarkdownBlockRenderer.dark( - styling = markdownStyling, - ) - } else { - MarkdownBlockRenderer.light( - styling = markdownStyling, - ) - } - } - - LaunchedEffect(Unit) { - bytes = Res.readBytes("files/CONDITIONS.md") - // Process the loaded markdown into renderable blocks - blocks = processor.processMarkdownDocument(bytes.decodeToString()) - } - ProvideMarkdownStyling(markdownStyling, blockRenderer, NoOpCodeHighlighter) { - val lazyListState = rememberLazyListState() - VerticallyScrollableContainer(lazyListState as ScrollableState) { - LazyColumn( - modifier = Modifier.fillMaxWidth(), - state = lazyListState, - contentPadding = - PaddingValues( - start = 8.dp, - top = 8.dp, - end = 8.dp + scrollbarContentSafePadding(), - bottom = 16.dp, - ), - verticalArrangement = Arrangement.spacedBy(markdownStyling.blockVerticalSpacing), - ) { - items(blocks) { block -> - blockRenderer.RenderBlock( - block = block, - enabled = true, - onUrlClick = { url: String -> getDesktop().browse(create(url)) }, - modifier = Modifier.fillMaxWidth(), - ) - } - - item { - Spacer(Modifier.height(8.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - ) { - Checkbox(checked = isChecked, onCheckedChange = { isChecked = it }) - Spacer(Modifier.width(2.dp)) - Text(text = stringResource(Res.string.license_accept_checkbox)) - } + AccentMarkdownView( + resourcePath = "files/CONDITIONS.md", + modifier = Modifier.fillMaxWidth(), + includeH3 = false, + extraItems = { + item { + Spacer(Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Checkbox(checked = isChecked, onCheckedChange = { isChecked = it }) + Spacer(Modifier.width(2.dp)) + Text(text = stringResource(Res.string.license_accept_checkbox)) } } - } - } + }, + ) } } diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt index bd25680c..b22f9fc1 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/AboutSettingsScreen.kt @@ -1,192 +1,17 @@ -@file:OptIn(ExperimentalJewelApi::class) - package io.github.kdroidfilter.seforimapp.features.settings.ui -import androidx.compose.foundation.gestures.ScrollableState -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import io.github.kdroidfilter.seforimapp.core.presentation.components.AccentMarkdownView import io.github.kdroidfilter.seforimapp.theme.PreviewContainer -import org.jetbrains.compose.resources.Font -import org.jetbrains.jewel.foundation.ExperimentalJewelApi -import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter -import org.jetbrains.jewel.foundation.theme.JewelTheme -import org.jetbrains.jewel.foundation.theme.LocalTextStyle -import org.jetbrains.jewel.intui.markdown.standalone.ProvideMarkdownStyling -import org.jetbrains.jewel.intui.markdown.standalone.dark -import org.jetbrains.jewel.intui.markdown.standalone.light -import org.jetbrains.jewel.intui.markdown.standalone.styling.dark -import org.jetbrains.jewel.intui.markdown.standalone.styling.light -import org.jetbrains.jewel.markdown.MarkdownBlock -import org.jetbrains.jewel.markdown.extensions.autolink.AutolinkProcessorExtension -import org.jetbrains.jewel.markdown.processing.MarkdownProcessor -import org.jetbrains.jewel.markdown.rendering.InlinesStyling -import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer -import org.jetbrains.jewel.markdown.rendering.MarkdownStyling -import org.jetbrains.jewel.ui.component.* -import org.jetbrains.jewel.ui.typography -import seforimapp.seforimapp.generated.resources.* -import java.awt.Desktop.getDesktop -import java.net.URI.create @Composable fun AboutSettingsScreen() { - AboutSettingsView() -} - -@Composable -private fun AboutSettingsView() { - val isDark = JewelTheme.isDark - val accent = JewelTheme.globalColors.outlines.focused - - val textStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = 14.sp, - ) - - val h2TextStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = JewelTheme.typography.h2TextStyle.fontSize, - ) - - val h3TextStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = JewelTheme.typography.h3TextStyle.fontSize, - ) - - val h2Padding = PaddingValues(top = 12.dp, bottom = 8.dp) - val h3Padding = PaddingValues(top = 10.dp, bottom = 6.dp) - - val linkStyle = SpanStyle(color = accent) - val linkHoveredStyle = SpanStyle(color = accent, textDecoration = TextDecoration.Underline) - - val markdownStyling = - remember(isDark, textStyle, accent) { - if (isDark) { - MarkdownStyling.dark( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.dark( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.dark( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.dark( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - h3 = - MarkdownStyling.Heading.H3.dark( - baseTextStyle = h3TextStyle, - padding = h3Padding, - ), - ), - ) - } else { - MarkdownStyling.light( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.light( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.light( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.light( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - h3 = - MarkdownStyling.Heading.H3.light( - baseTextStyle = h3TextStyle, - padding = h3Padding, - ), - ), - ) - } - } - - var bytes by remember { mutableStateOf(ByteArray(0)) } - var blocks by remember { mutableStateOf(emptyList()) } - val processor = - remember { - MarkdownProcessor( - listOf( - AutolinkProcessorExtension, - ), - ) - } - val blockRenderer = - remember(markdownStyling) { - if (isDark) { - MarkdownBlockRenderer.dark( - styling = markdownStyling, - ) - } else { - MarkdownBlockRenderer.light( - styling = markdownStyling, - ) - } - } - - LaunchedEffect(Unit) { - bytes = Res.readBytes("files/ABOUT.md") - blocks = processor.processMarkdownDocument(bytes.decodeToString()) - } - - ProvideMarkdownStyling(markdownStyling, blockRenderer, NoOpCodeHighlighter) { - val lazyListState = rememberLazyListState() - VerticallyScrollableContainer(lazyListState as ScrollableState) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = lazyListState, - contentPadding = - PaddingValues( - start = 8.dp, - top = 8.dp, - end = 8.dp + scrollbarContentSafePadding(), - bottom = 16.dp, - ), - verticalArrangement = Arrangement.spacedBy(markdownStyling.blockVerticalSpacing), - ) { - items(blocks) { block -> - blockRenderer.RenderBlock( - block = block, - enabled = true, - onUrlClick = { url: String -> getDesktop().browse(create(url)) }, - modifier = Modifier.fillMaxWidth(), - ) - } - } - } - } + AccentMarkdownView(resourcePath = "files/ABOUT.md") } @Composable @Preview private fun AboutSettingsViewPreview() { - PreviewContainer { AboutSettingsView() } + PreviewContainer { AboutSettingsScreen() } } diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt index 615781f1..8e2cd8d4 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/ConditionsSettingsScreen.kt @@ -1,192 +1,17 @@ -@file:OptIn(ExperimentalJewelApi::class) - package io.github.kdroidfilter.seforimapp.features.settings.ui -import androidx.compose.foundation.gestures.ScrollableState -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import io.github.kdroidfilter.seforimapp.core.presentation.components.AccentMarkdownView import io.github.kdroidfilter.seforimapp.theme.PreviewContainer -import org.jetbrains.compose.resources.Font -import org.jetbrains.jewel.foundation.ExperimentalJewelApi -import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter -import org.jetbrains.jewel.foundation.theme.JewelTheme -import org.jetbrains.jewel.foundation.theme.LocalTextStyle -import org.jetbrains.jewel.intui.markdown.standalone.ProvideMarkdownStyling -import org.jetbrains.jewel.intui.markdown.standalone.dark -import org.jetbrains.jewel.intui.markdown.standalone.light -import org.jetbrains.jewel.intui.markdown.standalone.styling.dark -import org.jetbrains.jewel.intui.markdown.standalone.styling.light -import org.jetbrains.jewel.markdown.MarkdownBlock -import org.jetbrains.jewel.markdown.extensions.autolink.AutolinkProcessorExtension -import org.jetbrains.jewel.markdown.processing.MarkdownProcessor -import org.jetbrains.jewel.markdown.rendering.InlinesStyling -import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer -import org.jetbrains.jewel.markdown.rendering.MarkdownStyling -import org.jetbrains.jewel.ui.component.* -import org.jetbrains.jewel.ui.typography -import seforimapp.seforimapp.generated.resources.* -import java.awt.Desktop.getDesktop -import java.net.URI.create @Composable fun ConditionsSettingsScreen() { - ConditionsSettingsView() -} - -@Composable -private fun ConditionsSettingsView() { - val isDark = JewelTheme.isDark - val accent = JewelTheme.globalColors.outlines.focused - - val textStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = 14.sp, - ) - - val h2TextStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = JewelTheme.typography.h2TextStyle.fontSize, - ) - - val h3TextStyle = - LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(resource = Res.font.notoserifhebrew)), - fontSize = JewelTheme.typography.h3TextStyle.fontSize, - ) - - val h2Padding = PaddingValues(top = 12.dp, bottom = 8.dp) - val h3Padding = PaddingValues(top = 10.dp, bottom = 6.dp) - - val linkStyle = SpanStyle(color = accent) - val linkHoveredStyle = SpanStyle(color = accent, textDecoration = TextDecoration.Underline) - - val markdownStyling = - remember(isDark, textStyle, accent) { - if (isDark) { - MarkdownStyling.dark( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.dark( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.dark( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.dark( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - h3 = - MarkdownStyling.Heading.H3.dark( - baseTextStyle = h3TextStyle, - padding = h3Padding, - ), - ), - ) - } else { - MarkdownStyling.light( - baseTextStyle = textStyle, - inlinesStyling = - InlinesStyling.light( - textStyle, - link = linkStyle, - linkHovered = linkHoveredStyle, - linkVisited = linkStyle, - ), - blockVerticalSpacing = 8.dp, - heading = - MarkdownStyling.Heading.light( - baseTextStyle = textStyle, - h2 = - MarkdownStyling.Heading.H2.light( - baseTextStyle = h2TextStyle, - padding = h2Padding, - ), - h3 = - MarkdownStyling.Heading.H3.light( - baseTextStyle = h3TextStyle, - padding = h3Padding, - ), - ), - ) - } - } - - var bytes by remember { mutableStateOf(ByteArray(0)) } - var blocks by remember { mutableStateOf(emptyList()) } - val processor = - remember { - MarkdownProcessor( - listOf( - AutolinkProcessorExtension, - ), - ) - } - val blockRenderer = - remember(markdownStyling) { - if (isDark) { - MarkdownBlockRenderer.dark( - styling = markdownStyling, - ) - } else { - MarkdownBlockRenderer.light( - styling = markdownStyling, - ) - } - } - - LaunchedEffect(Unit) { - bytes = Res.readBytes("files/CONDITIONS.md") - blocks = processor.processMarkdownDocument(bytes.decodeToString()) - } - - ProvideMarkdownStyling(markdownStyling, blockRenderer, NoOpCodeHighlighter) { - val lazyListState = rememberLazyListState() - VerticallyScrollableContainer(lazyListState as ScrollableState) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = lazyListState, - contentPadding = - PaddingValues( - start = 8.dp, - top = 8.dp, - end = 8.dp + scrollbarContentSafePadding(), - bottom = 16.dp, - ), - verticalArrangement = Arrangement.spacedBy(markdownStyling.blockVerticalSpacing), - ) { - items(blocks) { block -> - blockRenderer.RenderBlock( - block = block, - enabled = true, - onUrlClick = { url: String -> getDesktop().browse(create(url)) }, - modifier = Modifier.fillMaxWidth(), - ) - } - } - } - } + AccentMarkdownView(resourcePath = "files/CONDITIONS.md") } @Composable @Preview private fun ConditionsSettingsViewPreview() { - PreviewContainer { ConditionsSettingsView() } + PreviewContainer { ConditionsSettingsScreen() } } From 81295603c20409c1cfe5765d3117a48abc80582a Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Tue, 10 Mar 2026 15:49:30 +0200 Subject: [PATCH 4/6] refactor: enhance accent color customization UI Improve the accent color settings screen with better UI layout and add ProGuard rules for native image compilation. Update theme utilities and configuration to support enhanced customization options. --- SeforimApp/build.gradle.kts | 2 + SeforimApp/proguard-rules.pro | 26 +++++ .../composeResources/values/strings.xml | 5 + .../presentation/components/MainTitleBar.kt | 6 +- .../core/presentation/theme/AccentColor.kt | 22 +++- .../core/presentation/theme/ThemeUtils.kt | 4 +- .../seforimapp/core/settings/AppSettings.kt | 4 +- .../settings/ui/DisplaySettingsScreen.kt | 100 ++++++++++++++---- .../io/github/kdroidfilter/seforimapp/main.kt | 10 ++ .../native-image/reachability-metadata.json | 46 ++++++++ gradle/libs.versions.toml | 4 +- settings.gradle.kts | 2 +- 12 files changed, 203 insertions(+), 28 deletions(-) diff --git a/SeforimApp/build.gradle.kts b/SeforimApp/build.gradle.kts index f4d0df02..7351b937 100644 --- a/SeforimApp/build.gradle.kts +++ b/SeforimApp/build.gradle.kts @@ -154,9 +154,11 @@ kotlin { implementation(libs.hebrew.numerals) api(project(":jewel")) implementation(project(":earthwidget")) + implementation(libs.nucleus.system.color) implementation(libs.nucleus.decorated.window) implementation(libs.nucleus.graalvm.runtime) implementation(libs.nucleus.updater.runtime) + implementation(libs.nucleus.energy.manager) implementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") } diff --git a/SeforimApp/proguard-rules.pro b/SeforimApp/proguard-rules.pro index 414978aa..b5429672 100644 --- a/SeforimApp/proguard-rules.pro +++ b/SeforimApp/proguard-rules.pro @@ -273,7 +273,33 @@ native ; } +-keep class io.github.kdroidfilter.nucleus.energymanager.** { *; } + +# macOS +-keep class io.github.kdroidfilter.nucleus.systemcolor.mac.NativeMacSystemColorBridge { + native ; + static void onAccentColorChanged(float, float, float); + static void onContrastChanged(boolean); +} + +# Windows +-keep class io.github.kdroidfilter.nucleus.systemcolor.windows.NativeWindowsSystemColorBridge { + native ; + static void onAccentColorChanged(int, int, int); + static void onHighContrastChanged(boolean); +} + +# Linux +-keep class io.github.kdroidfilter.nucleus.systemcolor.linux.NativeLinuxSystemColorBridge { + native ; + static void onAccentColorChanged(float, float, float); + static void onHighContrastChanged(boolean); +} + +-keep class io.github.kdroidfilter.nucleus.systemcolor.** { *; } + # --- Sentry crash reporting SDK --- # Sentry uses reflection for serialization and event processing. -keep class io.sentry.** { *; } -dontwarn io.sentry.** + diff --git a/SeforimApp/src/commonMain/composeResources/values/strings.xml b/SeforimApp/src/commonMain/composeResources/values/strings.xml index 66f6a11b..048a9af3 100644 --- a/SeforimApp/src/commonMain/composeResources/values/strings.xml +++ b/SeforimApp/src/commonMain/composeResources/values/strings.xml @@ -314,6 +314,11 @@ איילנדס צבע מבטא + מערכת + כחול + טורקיז + ירוק + זהב מדריך ההגדרה של זית diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/MainTitleBar.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/MainTitleBar.kt index 6531f9eb..8174a827 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/MainTitleBar.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/MainTitleBar.kt @@ -12,8 +12,10 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.github.kdroidfilter.nucleus.window.DecoratedWindowScope import io.github.kdroidfilter.nucleus.window.TitleBar +import io.github.kdroidfilter.nucleus.window.macOSLargeCornerRadius import io.github.kdroidfilter.nucleus.window.newFullscreenControls import io.github.kdroidfilter.nucleus.window.styling.LocalTitleBarStyle +import io.github.kdroidfilter.nucleus.window.styling.TitleBarMetrics import io.github.kdroidfilter.platformtools.OperatingSystem import io.github.kdroidfilter.seforimapp.core.presentation.tabs.TabsView import io.github.kdroidfilter.seforimapp.core.presentation.theme.ThemeUtils @@ -22,8 +24,8 @@ import io.github.kdroidfilter.seforimapp.framework.platform.PlatformInfo @Composable fun DecoratedWindowScope.MainTitleBar() { TitleBar( - modifier = Modifier.newFullscreenControls(), - gradientStartColor = if (ThemeUtils.isIslandsStyle()) ThemeUtils.titleBarGradientColor() else Color.Unspecified, + modifier = Modifier.newFullscreenControls().macOSLargeCornerRadius(), + gradientStartColor = if (ThemeUtils.isIslandsStyle()) ThemeUtils.titleBarGradientColor() else Color.Unspecified, ) { // Window control buttons (close/maximize/minimize) are Compose-based on Linux and // Windows-fallback. Their total width must be subtracted from the available width so diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentColor.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentColor.kt index 00c20031..49a9c894 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentColor.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentColor.kt @@ -2,23 +2,30 @@ package io.github.kdroidfilter.seforimapp.core.presentation.theme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import io.github.kdroidfilter.nucleus.systemcolor.systemAccentColor import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme /** * Predefined accent color presets for the application theme. + * [System] uses the OS accent color (falls back to Jewel's blue when unavailable). * [Default] uses Jewel's built-in blue; other entries provide light/dark variants. */ enum class AccentColor { + System, Default, Teal, Green, Gold, ; + /** + * Returns the static color for non-System presets. + * For [System], returns the Jewel default blue (use [resolveColor] in composable contexts). + */ fun forMode(isDark: Boolean): Color = when (this) { - Default -> + System, Default -> if (isDark) { IntUiDarkTheme.colors.blueOrNull(6) ?: Color(0xFF3574F0) } else { @@ -29,7 +36,18 @@ enum class AccentColor { Gold -> if (isDark) Color(0xFFD4A843) else Color(0xFFBE9117) } + /** + * Composable-aware resolution that queries the OS accent color for [System]. + * Falls back to [forMode] when the platform doesn't provide an accent color. + */ + @Composable + fun resolveColor(isDark: Boolean): Color = + when (this) { + System -> systemAccentColor() ?: forMode(isDark) + else -> forMode(isDark) + } + /** Resolve for display in the settings UI. */ @Composable - fun displayColor(isDark: Boolean): Color = forMode(isDark) + fun displayColor(isDark: Boolean): Color = resolveColor(isDark) } diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ThemeUtils.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ThemeUtils.kt index 2dcc203c..e0f875fa 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ThemeUtils.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/ThemeUtils.kt @@ -112,7 +112,7 @@ object ThemeUtils { val isDark = isDarkTheme() val themeStyle = mainAppState.themeStyle.collectAsState().value val accentColor = mainAppState.accentColor.collectAsState().value - val accent = accentColor.forMode(isDark) + val accent = accentColor.resolveColor(isDark) val disabledValues = if (isDark) DisabledAppearanceValues.dark() else DisabledAppearanceValues.light() val iconData = accentIconData(accent, isDark) @@ -164,7 +164,7 @@ object ThemeUtils { mainAppState.accentColor .collectAsState() .value - .forMode(isDark) + .resolveColor(isDark) return when (themeStyle) { ThemeStyle.Islands -> islandsComponentStyling(isDark, accent) ThemeStyle.Classic -> classicComponentStyling(isDark, accent) diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/settings/AppSettings.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/settings/AppSettings.kt index e61a35cf..271c9404 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/settings/AppSettings.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/settings/AppSettings.kt @@ -441,11 +441,11 @@ object AppSettings { // Accent color preset fun getAccentColor(): AccentColor { - val storedValue: String = settings[KEY_ACCENT_COLOR, AccentColor.Default.name] + val storedValue: String = settings[KEY_ACCENT_COLOR, AccentColor.System.name] return try { AccentColor.valueOf(storedValue) } catch (_: IllegalArgumentException) { - AccentColor.Default + AccentColor.System } } diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt index b9cc2738..f6cb5993 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize 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.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -19,9 +20,13 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import dev.zacsweers.metrox.viewmodel.metroViewModel import io.github.kdroidfilter.seforimapp.core.presentation.theme.AccentColor import io.github.kdroidfilter.seforimapp.core.presentation.theme.ThemeStyle @@ -38,6 +43,11 @@ import org.jetbrains.jewel.ui.component.RadioButtonRow import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.VerticallyScrollableContainer import seforimapp.seforimapp.generated.resources.Res +import seforimapp.seforimapp.generated.resources.accent_color_default +import seforimapp.seforimapp.generated.resources.accent_color_gold +import seforimapp.seforimapp.generated.resources.accent_color_green +import seforimapp.seforimapp.generated.resources.accent_color_system +import seforimapp.seforimapp.generated.resources.accent_color_teal import seforimapp.seforimapp.generated.resources.settings_accent_color_label import seforimapp.seforimapp.generated.resources.settings_compact_mode import seforimapp.seforimapp.generated.resources.settings_compact_mode_description @@ -73,7 +83,7 @@ private fun DisplaySettingsView( themeStyle: ThemeStyle, onEvent: (DisplaySettingsEvents) -> Unit, onThemeStyleChange: (ThemeStyle) -> Unit, - accentColor: AccentColor = AccentColor.Default, + accentColor: AccentColor = AccentColor.System, onAccentColorChange: (AccentColor) -> Unit = {}, ) { VerticallyScrollableContainer(modifier = Modifier.fillMaxSize()) { @@ -173,31 +183,85 @@ private fun AccentColorCard( ) { Text(text = stringResource(Res.string.settings_accent_color_label)) Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.Top, ) { + val swatchSize = 28.dp AccentColor.entries.forEach { accent -> - val displayColor = accent.forMode(JewelTheme.isDark) val isSelected = selectedAccent == accent - Box( - modifier = - Modifier - .size(32.dp) - .clip(CircleShape) - .background(displayColor, CircleShape) - .then( - if (isSelected) { - Modifier.border(2.5.dp, JewelTheme.globalColors.text.normal, CircleShape) - } else { - Modifier.border(1.dp, Color.Black.copy(alpha = 0.2f), CircleShape) - }, - ).clickable { onAccentChange(accent) }, - ) + val borderModifier = + if (isSelected) { + Modifier.border(2.5.dp, JewelTheme.globalColors.text.normal, CircleShape) + } else { + Modifier.border(1.dp, Color.Black.copy(alpha = 0.2f), CircleShape) + } + Column( + modifier = Modifier.width(swatchSize), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + if (accent == AccentColor.System) { + val gradient = + Brush.linearGradient( + colors = + listOf( + Color(0xFFFF6B6B), + Color(0xFFFFD93D), + Color(0xFF6BCB77), + Color(0xFF4D96FF), + Color(0xFFA66CFF), + ), + start = Offset.Zero, + end = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY), + ) + Box( + modifier = + Modifier + .size(swatchSize) + .clip(CircleShape) + .drawBehind { drawRect(gradient) } + .then(borderModifier) + .clickable { onAccentChange(accent) }, + ) + } else { + val displayColor = accent.forMode(JewelTheme.isDark) + Box( + modifier = + Modifier + .size(swatchSize) + .clip(CircleShape) + .background(displayColor, CircleShape) + .then(borderModifier) + .clickable { onAccentChange(accent) }, + ) + } + if (isSelected) { + Text( + text = accentColorLabel(accent), + style = JewelTheme.defaultTextStyle.copy( + color = JewelTheme.globalColors.text.info, + fontSize = 10.sp, + ), + maxLines = 1, + softWrap = false, + ) + } + } } } } } +@Composable +private fun accentColorLabel(accent: AccentColor): String = + when (accent) { + AccentColor.System -> stringResource(Res.string.accent_color_system) + AccentColor.Default -> stringResource(Res.string.accent_color_default) + AccentColor.Teal -> stringResource(Res.string.accent_color_teal) + AccentColor.Green -> stringResource(Res.string.accent_color_green) + AccentColor.Gold -> stringResource(Res.string.accent_color_gold) + } + @Composable @Preview private fun DisplaySettingsView_Preview() { diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt index afd0ce32..f5769b28 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt @@ -33,6 +33,7 @@ import io.github.kdroidfilter.knotify.compose.builder.notification import io.github.kdroidfilter.nucleus.aot.runtime.AotRuntime import io.github.kdroidfilter.nucleus.core.runtime.ExecutableRuntime import io.github.kdroidfilter.nucleus.core.runtime.SingleInstanceManager +import io.github.kdroidfilter.nucleus.energymanager.EnergyManager import io.github.kdroidfilter.nucleus.graalvm.GraalVmInitializer import io.github.kdroidfilter.nucleus.window.DecoratedWindow import io.github.kdroidfilter.nucleus.window.NucleusDecoratedWindowTheme @@ -76,6 +77,8 @@ import java.awt.Toolkit import java.awt.Window import java.awt.datatransfer.StringSelection import java.awt.event.KeyEvent +import java.awt.event.WindowEvent +import java.awt.event.WindowFocusListener import java.net.URI import java.util.* @@ -380,6 +383,13 @@ fun main() { window.minimumSize = Dimension(600, 300) } MainTitleBar() + LaunchedEffect(state.isMinimized, ) { + if (state.isMinimized ) { + EnergyManager.enableEfficiencyMode() + } else { + EnergyManager.disableEfficiencyMode() + } + } // Restore previously saved session once when main window becomes active var sessionRestored by remember { mutableStateOf(false) } diff --git a/SeforimApp/src/main/resources-macos/META-INF/native-image/reachability-metadata.json b/SeforimApp/src/main/resources-macos/META-INF/native-image/reachability-metadata.json index 6397355f..5a7db44b 100644 --- a/SeforimApp/src/main/resources-macos/META-INF/native-image/reachability-metadata.json +++ b/SeforimApp/src/main/resources-macos/META-INF/native-image/reachability-metadata.json @@ -2393,6 +2393,12 @@ "name": "installToolkitThreadInJava", "parameterTypes": [ + ] + }, + { + "name": "systemColorsChanged", + "parameterTypes": [ + ] } ] @@ -4195,6 +4201,13 @@ "java.lang.String", "java.lang.Throwable" ] + }, + { + "name": "debug", + "parameterTypes": [ + "java.lang.String", + "java.lang.Throwable" + ] } ] }, @@ -4811,6 +4824,33 @@ ] } ] + }, + { + "type": "io.github.kdroidfilter.nucleus.window.utils.macos.JniMacTitleBarBridge", + "jniAccessible": true, + "methods": [ + { + "name": "onMenuBarOffsetChanged", + "parameterTypes": [ + "long", + "float" + ] + } + ] + }, + { + "type": "io.github.kdroidfilter.nucleus.systemcolor.mac.NativeMacSystemColorBridge", + "jniAccessible": true, + "methods": [ + { + "name": "onAccentColorChanged", + "parameterTypes": [ + "float", + "float", + "float" + ] + } + ] } ], "resources": [ @@ -5621,6 +5661,12 @@ }, { "glob": "vcs/vendors/github_dark.svg" + }, + { + "glob": "nucleus/native/darwin-aarch64/libnucleus_energy_manager.dylib" + }, + { + "glob": "nucleus/native/darwin-aarch64/libnucleus_systemcolor.dylib" } ], "foreign": { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e57cd1a4..971bfb54 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ hebrewNumerals = "0.2.6" jsoup = "1.22.1" jvmToolchain = "25" knotify = "0.4.3" -nucleus = "1.3.8" +nucleus = "1.4.0" koalaplotCore = "0.11.0" kotlin = "2.3.10" compose = "1.10.1" @@ -75,6 +75,8 @@ nucleus-decorated-window = { module = "io.github.kdroidfilter:nucleus.decorated- nucleus-graalvm-runtime = { module = "io.github.kdroidfilter:nucleus.graalvm-runtime", version.ref = "nucleus" } nucleus-updater-runtime = { module = "io.github.kdroidfilter:nucleus.updater-runtime", version.ref = "nucleus" } nucleus-native-ssl = { module = "io.github.kdroidfilter:nucleus.native-ssl", version.ref = "nucleus" } +nucleus-energy-manager = { module = "io.github.kdroidfilter:nucleus.energy-manager", version.ref = "nucleus"} +nucleus-system-color = { module = "io.github.kdroidfilter:nucleus.system-color", version.ref = "nucleus" } nucleus-native-http-ktor = { module = "io.github.kdroidfilter:nucleus.native-http-ktor", version.ref = "nucleus" } koalaplot-core = { module = "io.github.koalaplot:koalaplot-core", version.ref = "koalaplotCore" } kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } diff --git a/settings.gradle.kts b/settings.gradle.kts index c52e6a3e..4dc9d71f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,7 +29,7 @@ dependencyResolutionManagement { } } mavenCentral() - maven("https://jitpack.io") +// maven("https://jitpack.io") maven("https://packages.jetbrains.team/maven/p/kpm/public/") maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies/") maven("https://www.jetbrains.com/intellij-repository/releases") From 61e27eaf00ed8a83466113a4b5c687b545c1c581 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Tue, 10 Mar 2026 15:52:41 +0200 Subject: [PATCH 5/6] chore: update dependencies and cleanup Maven repositories - Updated nucleus to version 1.4.2. - Removed unused JitPack Maven repository from settings.gradle.kts. --- gradle/libs.versions.toml | 2 +- settings.gradle.kts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 971bfb54..a5dc430d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ hebrewNumerals = "0.2.6" jsoup = "1.22.1" jvmToolchain = "25" knotify = "0.4.3" -nucleus = "1.4.0" +nucleus = "1.4.2" koalaplotCore = "0.11.0" kotlin = "2.3.10" compose = "1.10.1" diff --git a/settings.gradle.kts b/settings.gradle.kts index 4dc9d71f..5b381bbb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,7 +29,6 @@ dependencyResolutionManagement { } } mavenCentral() -// maven("https://jitpack.io") maven("https://packages.jetbrains.team/maven/p/kpm/public/") maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies/") maven("https://www.jetbrains.com/intellij-repository/releases") From a4f21ac267832932872d4656ec59e68ae3209b96 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Tue, 10 Mar 2026 16:06:59 +0200 Subject: [PATCH 6/6] fix: resolve ktlint violations in MainTitleBar, DisplaySettingsScreen and main --- .../core/presentation/components/MainTitleBar.kt | 3 +-- .../features/settings/ui/DisplaySettingsScreen.kt | 9 +++++---- .../kotlin/io/github/kdroidfilter/seforimapp/main.kt | 6 ++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/MainTitleBar.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/MainTitleBar.kt index 8174a827..beb4ba89 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/MainTitleBar.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/MainTitleBar.kt @@ -15,7 +15,6 @@ import io.github.kdroidfilter.nucleus.window.TitleBar import io.github.kdroidfilter.nucleus.window.macOSLargeCornerRadius import io.github.kdroidfilter.nucleus.window.newFullscreenControls import io.github.kdroidfilter.nucleus.window.styling.LocalTitleBarStyle -import io.github.kdroidfilter.nucleus.window.styling.TitleBarMetrics import io.github.kdroidfilter.platformtools.OperatingSystem import io.github.kdroidfilter.seforimapp.core.presentation.tabs.TabsView import io.github.kdroidfilter.seforimapp.core.presentation.theme.ThemeUtils @@ -25,7 +24,7 @@ import io.github.kdroidfilter.seforimapp.framework.platform.PlatformInfo fun DecoratedWindowScope.MainTitleBar() { TitleBar( modifier = Modifier.newFullscreenControls().macOSLargeCornerRadius(), - gradientStartColor = if (ThemeUtils.isIslandsStyle()) ThemeUtils.titleBarGradientColor() else Color.Unspecified, + gradientStartColor = if (ThemeUtils.isIslandsStyle()) ThemeUtils.titleBarGradientColor() else Color.Unspecified, ) { // Window control buttons (close/maximize/minimize) are Compose-based on Linux and // Windows-fallback. Their total width must be subtracted from the available width so diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt index f6cb5993..31a2d27d 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/settings/ui/DisplaySettingsScreen.kt @@ -238,10 +238,11 @@ private fun AccentColorCard( if (isSelected) { Text( text = accentColorLabel(accent), - style = JewelTheme.defaultTextStyle.copy( - color = JewelTheme.globalColors.text.info, - fontSize = 10.sp, - ), + style = + JewelTheme.defaultTextStyle.copy( + color = JewelTheme.globalColors.text.info, + fontSize = 10.sp, + ), maxLines = 1, softWrap = false, ) diff --git a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt index f5769b28..ce51fbfe 100644 --- a/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/main.kt @@ -77,8 +77,6 @@ import java.awt.Toolkit import java.awt.Window import java.awt.datatransfer.StringSelection import java.awt.event.KeyEvent -import java.awt.event.WindowEvent -import java.awt.event.WindowFocusListener import java.net.URI import java.util.* @@ -383,8 +381,8 @@ fun main() { window.minimumSize = Dimension(600, 300) } MainTitleBar() - LaunchedEffect(state.isMinimized, ) { - if (state.isMinimized ) { + LaunchedEffect(state.isMinimized) { + if (state.isMinimized) { EnergyManager.enableEfficiencyMode() } else { EnergyManager.disableEfficiencyMode()