mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Android moves to Material 3 (navigation bar, app bars, list items, bottom sheets, pager) with a real back stack. iOS hides the tab bar in exercises so the bottom action is reachable, aligns content, and uses system colours so themes and dark mode work. Both pick a readable accent tint. Desktop keeps the VLSM table inside the window and gives statistics the same header rhythm as other pages. Co-authored-by: Cursor <cursoragent@cursor.com>
199 lines
8.9 KiB
Swift
199 lines
8.9 KiB
Swift
import SwiftUI
|
|
|
|
enum ThemeId: Int, CaseIterable {
|
|
case catppuccin, nord, dracula, rosePine, ocean, gruvbox, solarized, everforest, monokai, oneDark
|
|
}
|
|
|
|
enum ColorMode {
|
|
case dark, light
|
|
}
|
|
|
|
enum UiLang {
|
|
case cs, en
|
|
}
|
|
|
|
let themeNames = [
|
|
"Catppuccin", "Nord", "Dracula", "Rose Pine", "Ocean",
|
|
"Gruvbox", "Solarized", "Everforest", "Monokai", "One Dark",
|
|
]
|
|
|
|
struct Palette {
|
|
let crust, base, mantle, surface0, surface1, surface2: Color
|
|
let overlay, text, subtext: Color
|
|
let accent, accent2, accent3: Color
|
|
let success, success2, warning, error: Color
|
|
let onAccent, node, lockedBg, lockedBorder, rail: Color
|
|
let finishDark, finishLight, finishStroke: Color
|
|
/// Accent that stays readable as text and icon tint on the theme's background.
|
|
let tint: Color
|
|
}
|
|
|
|
private func hx(_ v: UInt32) -> Color {
|
|
Color(
|
|
red: Double((v >> 16) & 0xFF) / 255,
|
|
green: Double((v >> 8) & 0xFF) / 255,
|
|
blue: Double(v & 0xFF) / 255
|
|
)
|
|
}
|
|
|
|
private func luminance(_ v: UInt32) -> Double {
|
|
func channel(_ c: UInt32) -> Double {
|
|
let s = Double(c) / 255
|
|
return s <= 0.03928 ? s / 12.92 : pow((s + 0.055) / 1.055, 2.4)
|
|
}
|
|
return 0.2126 * channel((v >> 16) & 0xFF) + 0.7152 * channel((v >> 8) & 0xFF) + 0.0722 * channel(v & 0xFF)
|
|
}
|
|
|
|
private func contrast(_ a: UInt32, _ b: UInt32) -> Double {
|
|
let la = luminance(a), lb = luminance(b)
|
|
return (max(la, lb) + 0.05) / (min(la, lb) + 0.05)
|
|
}
|
|
|
|
/// Pick the first accent with enough contrast against the background; yellow on
|
|
/// white is unreadable as tab labels and link text.
|
|
private func readableTint(_ c: [UInt32]) -> UInt32 {
|
|
let background = c[1]
|
|
for candidate in [c[9], c[10], c[11]] where contrast(candidate, background) >= 3.0 {
|
|
return candidate
|
|
}
|
|
return c[7]
|
|
}
|
|
|
|
private func pal(_ c: [UInt32]) -> Palette {
|
|
Palette(
|
|
crust: hx(c[0]), base: hx(c[1]), mantle: hx(c[2]), surface0: hx(c[3]),
|
|
surface1: hx(c[4]), surface2: hx(c[5]), overlay: hx(c[6]), text: hx(c[7]),
|
|
subtext: hx(c[8]), accent: hx(c[9]), accent2: hx(c[10]), accent3: hx(c[11]),
|
|
success: hx(c[12]), success2: hx(c[13]), warning: hx(c[14]), error: hx(c[15]),
|
|
onAccent: hx(c[16]), node: hx(c[17]), lockedBg: hx(c[18]),
|
|
lockedBorder: hx(c[19]), rail: hx(c[20]), finishDark: hx(c[21]),
|
|
finishLight: hx(c[22]), finishStroke: hx(c[23]),
|
|
tint: hx(readableTint(c))
|
|
)
|
|
}
|
|
|
|
func themePalette(_ id: ThemeId, _ mode: ColorMode) -> Palette {
|
|
let light = mode == .light
|
|
switch id {
|
|
case .catppuccin:
|
|
return light ? pal([
|
|
0xf3eee6, 0xfaf7f2, 0xffffff, 0xffffff, 0xf0ebe3, 0xe5dfd4,
|
|
0x8a8378, 0x1a1a1a, 0x5c574f, 0xffcc00, 0x0071e3, 0x2997ff,
|
|
0x1f8a4c, 0x1a7f4a, 0xe6a800, 0xd32f2f, 0x1a1a1a,
|
|
0xffffff, 0xf0ebe3, 0xd5cfc4, 0xe5dfd4, 0xf3eee6, 0xd5cfc4, 0x8a8378,
|
|
]) : pal([
|
|
0x000000, 0x000000, 0x111111, 0x1d1d1f, 0x2c2c2e, 0x3a3a3c,
|
|
0x86868b, 0xf5f5f7, 0xa1a1a6, 0xffd60a, 0x2997ff, 0x64d2ff,
|
|
0x30d158, 0x64d2ff, 0xffd60a, 0xff453a, 0x1d1d1f,
|
|
0x1d1d1f, 0x111111, 0x3a3a3c, 0x2c2c2e, 0x000000, 0x3a3a3c, 0x86868b,
|
|
])
|
|
case .nord:
|
|
return light ? pal([
|
|
0xd8dee9, 0xeceff4, 0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xb0b8c8,
|
|
0x7b88a1, 0x2e3440, 0x4c566a, 0x5e81ac, 0x81a1c1, 0x5e81ac,
|
|
0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0xeceff4,
|
|
0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xd8dee9, 0xd8dee9, 0xc7ceda, 0x7b88a1,
|
|
]) : pal([
|
|
0x2e3440, 0x3b4252, 0x2e3440, 0x434c5e, 0x4c566a, 0x616e88,
|
|
0x7b88a1, 0xeceff4, 0xd8dee9, 0x88c0d0, 0x81a1c1, 0x5e81ac,
|
|
0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0x2e3440,
|
|
0x434c5e, 0x3b4252, 0x4c566a, 0x2e3440, 0x2e3440, 0x434c5e, 0x4c566a,
|
|
])
|
|
case .dracula:
|
|
return light ? pal([
|
|
0xd5d7e0, 0xf8f8f2, 0xefefea, 0xe2e2d8, 0xd0d0c4, 0xbfbfb2,
|
|
0x9a9a8c, 0x282a36, 0x44475a, 0x7c3aed, 0xd946a6, 0x0891b2,
|
|
0x16a34a, 0x0e7490, 0xca8a04, 0xdc2626, 0xf8f8f2,
|
|
0xefefea, 0xe2e2d8, 0xd0d0c4, 0xe2e2d8, 0xd5d7e0, 0xd0d0c4, 0x9a9a8c,
|
|
]) : pal([
|
|
0x191a21, 0x282a36, 0x21222c, 0x44475a, 0x6272a4, 0x707eb0,
|
|
0x6272a4, 0xf8f8f2, 0xbfbfb2, 0xbd93f9, 0xff79c6, 0x8be9fd,
|
|
0x50fa7b, 0x8be9fd, 0xf1fa8c, 0xff5555, 0x282a36,
|
|
0x383a4a, 0x2f3240, 0x44475a, 0x21222c, 0x191a21, 0x383a4a, 0x6272a4,
|
|
])
|
|
case .rosePine:
|
|
return light ? pal([
|
|
0xf2e9e1, 0xfaf4ed, 0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xcecacd,
|
|
0x9893a5, 0x575279, 0x797593, 0x907aa9, 0xd7827e, 0x56949f,
|
|
0x286983, 0x56949f, 0xea9d34, 0xb4637a, 0xfaf4ed,
|
|
0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xf2e9e1, 0xf2e9e1, 0xdfdad9, 0x9893a5,
|
|
]) : pal([
|
|
0x191724, 0x1f1d2e, 0x1f1d2e, 0x26233a, 0x403d52, 0x524f67,
|
|
0x6e6a86, 0xe0def4, 0x908caa, 0xc4a7e7, 0xebbcba, 0x9ccfd8,
|
|
0x31748f, 0x9ccfd8, 0xf6c177, 0xeb6f92, 0x191724,
|
|
0x26233a, 0x21202e, 0x403d52, 0x191724, 0x191724, 0x26233a, 0x524f67,
|
|
])
|
|
case .ocean:
|
|
return light ? pal([
|
|
0xd6dae8, 0xe1e2ec, 0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0x9aa0b5,
|
|
0x7a8199, 0x2e3446, 0x4a5168, 0x2e7de9, 0x7847bd, 0x007197,
|
|
0x387068, 0x007197, 0x8c6c3e, 0xc64343, 0xe1e2ec,
|
|
0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0xc8c9d6, 0xd6dae8, 0xb4b6c5, 0x7a8199,
|
|
]) : pal([
|
|
0x16161e, 0x1a1b26, 0x16161e, 0x24283b, 0x414868, 0x565f89,
|
|
0x565f89, 0xc0caf5, 0xa9b1d6, 0x7aa2f7, 0xbb9af7, 0x7dcfff,
|
|
0x9ece6a, 0x73daca, 0xe0af68, 0xf7768e, 0x1a1b26,
|
|
0x24283b, 0x1f2335, 0x3b4261, 0x16161e, 0x16161e, 0x24283b, 0x414868,
|
|
])
|
|
case .gruvbox:
|
|
return light ? pal([
|
|
0xebdbb2, 0xfbf1c7, 0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xbdae93,
|
|
0x928374, 0x3c3836, 0x504945, 0xd65d0e, 0xb16286, 0x076678,
|
|
0x79740e, 0x427b58, 0xb57614, 0x9d0006, 0xfbf1c7,
|
|
0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xebdbb2, 0xebdbb2, 0xd5c4a1, 0x928374,
|
|
]) : pal([
|
|
0x1d2021, 0x282828, 0x1d2021, 0x3c3836, 0x504945, 0x665c54,
|
|
0x928374, 0xebdbb2, 0xa89984, 0xfe8019, 0xd3869b, 0x83a598,
|
|
0xb8bb26, 0x8ec07c, 0xfabd2f, 0xfb4934, 0x1d2021,
|
|
0x3c3836, 0x32302f, 0x504945, 0x1d2021, 0x1d2021, 0x3c3836, 0x504945,
|
|
])
|
|
case .solarized:
|
|
return light ? pal([
|
|
0xeee8d5, 0xfdf6e3, 0xeee8d5, 0xeee8d5, 0xe2dbc6, 0xc9c0a6,
|
|
0x657b83, 0x586e75, 0x657b83, 0x268bd2, 0x6c71c4, 0x2aa198,
|
|
0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0xfdf6e3,
|
|
0xeee8d5, 0xe6dfc8, 0x93a1a1, 0xeee8d5, 0xeee8d5, 0x93a1a1, 0x586e75,
|
|
]) : pal([
|
|
0x002b36, 0x073642, 0x002b36, 0x586e75, 0x657b83, 0x839496,
|
|
0x93a1a1, 0xfdf6e3, 0x93a1a1, 0x268bd2, 0x6c71c4, 0x2aa198,
|
|
0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0x002b36,
|
|
0x073642, 0x002b36, 0x586e75, 0x002b36, 0x002b36, 0x073642, 0x586e75,
|
|
])
|
|
case .everforest:
|
|
return light ? pal([
|
|
0xe6e2cc, 0xfdf6e3, 0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xa6b0a0,
|
|
0x939f91, 0x5c6a72, 0x829181, 0x8da101, 0xdf69ba, 0x35a77c,
|
|
0x8da101, 0x35a77c, 0xdfa000, 0xf85552, 0xfdf6e3,
|
|
0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xe6e2cc, 0xe6e2cc, 0xe0dcc7, 0x939f91,
|
|
]) : pal([
|
|
0x1e2326, 0x272e33, 0x2d353b, 0x374145, 0x4f5b58, 0x7a8478,
|
|
0x859289, 0xd3c6aa, 0xa7c080, 0xa7c080, 0xd699b6, 0x7fbbb3,
|
|
0xa7c080, 0x83c092, 0xdbbc7f, 0xe67e80, 0x272e33,
|
|
0x374145, 0x2d353b, 0x4f5b58, 0x1e2326, 0x1e2326, 0x374145, 0x4f5b58,
|
|
])
|
|
case .monokai:
|
|
return light ? pal([
|
|
0xe6e3d4, 0xfaf8f0, 0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xb8b49e,
|
|
0x8a8674, 0x2d2a2e, 0x5b5854, 0xe14775, 0x7c5cbf, 0x1c7d9b,
|
|
0x5b8c2a, 0x1c7d9b, 0xb08900, 0xe14775, 0xfaf8f0,
|
|
0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xe6e3d4, 0xe6e3d4, 0xd6d2c0, 0x8a8674,
|
|
]) : pal([
|
|
0x1d1e19, 0x272822, 0x1e1f1a, 0x3e3d32, 0x49483e, 0x75715e,
|
|
0x75715e, 0xf8f8f2, 0xa59f85, 0xf92672, 0xae81ff, 0x66d9ef,
|
|
0xa6e22e, 0xa1efe4, 0xe6db74, 0xf92672, 0x272822,
|
|
0x3e3d32, 0x2e2f29, 0x49483e, 0x1d1e19, 0x1d1e19, 0x3e3d32, 0x49483e,
|
|
])
|
|
case .oneDark:
|
|
return light ? pal([
|
|
0xe5e5e6, 0xfafafa, 0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7,
|
|
0xa0a1a7, 0x383a42, 0x696c77, 0xa626a4, 0x4078f2, 0x0184bc,
|
|
0x50a14f, 0x0184bc, 0xc18401, 0xe45649, 0xfafafa,
|
|
0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xe5e5e6, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7,
|
|
]) : pal([
|
|
0x21252b, 0x282c34, 0x21252b, 0x3b4048, 0x4b5263, 0x5c6370,
|
|
0x5c6370, 0xabb2bf, 0x7f848e, 0xc678dd, 0x61afef, 0x56b6c2,
|
|
0x98c379, 0x56b6c2, 0xe5c07b, 0xe06c75, 0x282c34,
|
|
0x3b4048, 0x2c313a, 0x4b5263, 0x21252b, 0x21252b, 0x3b4048, 0x4b5263,
|
|
])
|
|
}
|
|
}
|