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 18382b49..048a9af3 100644 --- a/SeforimApp/src/commonMain/composeResources/values/strings.xml +++ b/SeforimApp/src/commonMain/composeResources/values/strings.xml @@ -312,6 +312,13 @@ סגנון ערכת נושא קלאסי איילנדס + + צבע מבטא + מערכת + כחול + טורקיז + ירוק + זהב מדריך ההגדרה של זית 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/AccentMarkdownView.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/AccentMarkdownView.kt new file mode 100644 index 00000000..3af174db --- /dev/null +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/AccentMarkdownView.kt @@ -0,0 +1,210 @@ +@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/core/presentation/components/MainTitleBar.kt b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/components/MainTitleBar.kt index 6531f9eb..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 @@ -12,6 +12,7 @@ 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.platformtools.OperatingSystem @@ -22,7 +23,7 @@ import io.github.kdroidfilter.seforimapp.framework.platform.PlatformInfo @Composable fun DecoratedWindowScope.MainTitleBar() { TitleBar( - modifier = Modifier.newFullscreenControls(), + 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 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..49a9c894 --- /dev/null +++ b/SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/core/presentation/theme/AccentColor.kt @@ -0,0 +1,53 @@ +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) { + System, 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) + } + + /** + * 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 = resolveColor(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..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 @@ -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.resolveColor(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 + .resolveColor(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..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 @@ -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.System.name] + return try { + AccentColor.valueOf(storedValue) + } catch (_: IllegalArgumentException) { + AccentColor.System + } + } + + 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..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,48 +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.font.FontFamily 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( @@ -82,58 +71,6 @@ private fun LicenceView( ) { var isChecked by remember { mutableStateOf(false) } - val isDark = JewelTheme.isDark - - 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 markdownStyling = - remember(isDark, textStyle) { - // Make Markdown more compact: smaller block spacing and tighter heading paddings. - if (isDark) { - MarkdownStyling.dark( - baseTextStyle = textStyle, - inlinesStyling = InlinesStyling.dark(textStyle), - 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), - 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 = { @@ -142,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/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..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,174 +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.font.FontFamily +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 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 markdownStyling = - remember(isDark, textStyle) { - if (isDark) { - MarkdownStyling.dark( - baseTextStyle = textStyle, - inlinesStyling = InlinesStyling.dark(textStyle), - 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), - 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 9321ae68..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,174 +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.font.FontFamily +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 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 markdownStyling = - remember(isDark, textStyle) { - if (isDark) { - MarkdownStyling.dark( - baseTextStyle = textStyle, - inlinesStyling = InlinesStyling.dark(textStyle), - 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), - 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() } } 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..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 @@ -2,12 +2,17 @@ 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.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -15,9 +20,15 @@ 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 import io.github.kdroidfilter.seforimapp.core.presentation.utils.LocalWindowViewModelStoreOwner import io.github.kdroidfilter.seforimapp.features.settings.display.DisplaySettingsEvents @@ -32,6 +43,12 @@ 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 import seforimapp.seforimapp.generated.resources.settings_show_zmanim_widgets @@ -49,11 +66,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 +83,8 @@ private fun DisplaySettingsView( themeStyle: ThemeStyle, onEvent: (DisplaySettingsEvents) -> Unit, onThemeStyleChange: (ThemeStyle) -> Unit, + accentColor: AccentColor = AccentColor.System, + onAccentColorChange: (AccentColor) -> Unit = {}, ) { VerticallyScrollableContainer(modifier = Modifier.fillMaxSize()) { Column( @@ -74,6 +96,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 +165,104 @@ 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(10.dp), + verticalAlignment = Alignment.Top, + ) { + val swatchSize = 28.dp + AccentColor.entries.forEach { accent -> + val isSelected = selectedAccent == 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 f36a16e5..ce51fbfe 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 @@ -43,10 +44,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 +242,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, @@ -390,6 +381,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..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.3.8" +nucleus = "1.4.2" 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..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")