diff --git a/crates/app/src/domain.rs b/crates/app/src/domain.rs index c35ad6a..ca36b5f 100644 --- a/crates/app/src/domain.rs +++ b/crates/app/src/domain.rs @@ -32,6 +32,35 @@ impl IconStyle { } } +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum AppearanceMode { + Light, + Dark, + #[default] + #[serde(other)] + Auto, +} + +impl AppearanceMode { + pub fn index(self) -> i32 { + match self { + Self::Auto => 0, + Self::Light => 1, + Self::Dark => 2, + } + } + + pub fn from_index(index: i32) -> Option { + match index { + 0 => Some(Self::Auto), + 1 => Some(Self::Light), + 2 => Some(Self::Dark), + _ => None, + } + } +} + #[derive(Clone, Deserialize, Serialize)] pub struct Server { pub name: String, @@ -54,6 +83,8 @@ pub struct Preferences { pub pull_status: String, #[serde(default)] pub icon_style: IconStyle, + #[serde(default)] + pub appearance: AppearanceMode, } impl Default for Preferences { @@ -65,6 +96,7 @@ impl Default for Preferences { issue_status: open_status(), pull_status: open_status(), icon_style: IconStyle::default(), + appearance: AppearanceMode::default(), } } } diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index d2324d4..4ba6f09 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -41,6 +41,8 @@ fn main() -> Result<(), Box> { ui.set_pull_filter(state.preferences.pull_status.clone().into()); ui.global::() .set_style(state.preferences.icon_style.index()); + ui.global::() + .set_mode(state.preferences.appearance.index()); } if let Some(error) = startup_error { ui.set_error(error.into()); @@ -231,6 +233,30 @@ fn wire_callbacks(ui: &AppWindow, state: Arc>) { } }); + ui.on_choose_appearance({ + let weak = ui.as_weak(); + let state = state.clone(); + move |index| { + let Some(appearance) = AppearanceMode::from_index(index) else { + return; + }; + let result = { + let mut state = state.lock().unwrap(); + let previous = state.preferences.appearance; + state.preferences.appearance = appearance; + save_preferences(&state.preferences).inspect_err(|_| { + state.preferences.appearance = previous; + }) + }; + if let Some(ui) = weak.upgrade() { + match result { + Ok(()) => ui.global::().set_mode(index), + Err(error) => ui.set_error(error.into()), + } + } + } + }); + ui.on_show_add_server({ let weak = ui.as_weak(); move || { diff --git a/crates/app/src/storage.rs b/crates/app/src/storage.rs index 46752da..52a7d19 100644 --- a/crates/app/src/storage.rs +++ b/crates/app/src/storage.rs @@ -100,6 +100,10 @@ mod tests { Preferences::default().icon_style, crate::domain::IconStyle::Outline ); + assert_eq!( + Preferences::default().appearance, + crate::domain::AppearanceMode::Auto + ); assert_eq!( serde_json::from_str::(r#"{"icon_style":"duotone"}"#) .unwrap() @@ -117,5 +121,22 @@ mod tests { Some(crate::domain::IconStyle::Bold) ); assert_eq!(crate::domain::IconStyle::from_index(3), None); + assert_eq!( + serde_json::from_str::(r#"{"appearance":"dark"}"#) + .unwrap() + .appearance, + crate::domain::AppearanceMode::Dark + ); + assert_eq!( + serde_json::from_str::(r#"{"appearance":"future-mode"}"#) + .unwrap() + .appearance, + crate::domain::AppearanceMode::Auto + ); + assert_eq!( + crate::domain::AppearanceMode::from_index(1), + Some(crate::domain::AppearanceMode::Light) + ); + assert_eq!(crate::domain::AppearanceMode::from_index(3), None); } } diff --git a/crates/app/src/ui.rs b/crates/app/src/ui.rs index 6b63175..c1adf51 100644 --- a/crates/app/src/ui.rs +++ b/crates/app/src/ui.rs @@ -1,5 +1,5 @@ slint::slint! { - import { Button, ComboBox, LineEdit, ListView, ScrollView, Spinner } from "std-widgets.slint"; + import { Button, ComboBox, LineEdit, ListView, Palette, ScrollView, Spinner } from "std-widgets.slint"; export struct ServerRow { name: string, url: string } export struct RepositoryRow { @@ -30,10 +30,34 @@ slint::slint! { in-out property style: 0; } + export global AppTheme { + in-out property mode: 0; + private property system-background: Palette.background; + out property dark: mode == 2 || (mode == 0 && system-background.to-oklch().lightness < 0.5); + out property accent: dark ? #0a84ff : #0879e1; + out property accent-soft: dark ? #17324d : #e5f2fd; + out property page: dark ? #1c1c1e : #f2f2f7; + out property surface: dark ? #2c2c2e : #ffffff; + out property surface-muted: dark ? #3a3a3c : #f5f5f8; + out property header: dark ? #242426 : #f8f8fa; + out property toolbar: dark ? #242426 : #fbfbfc; + out property border: dark ? #48484a : #dedee3; + out property separator: dark ? #3a3a3c : #d8d8dd; + out property text: dark ? #f2f2f7 : #17171a; + out property text-secondary: dark ? #c7c7cc : #5f5f66; + out property text-tertiary: dark ? #8e8e93 : #85858c; + out property code-background: dark ? #242426 : #e9e9ed; + out property quote-background: dark ? #242426 : #f6f6f8; + out property loading-overlay: dark ? #00000088 : #ffffff88; + changed mode => { + Palette.color-scheme = mode == 1 ? ColorScheme.light : mode == 2 ? ColorScheme.dark : ColorScheme.unknown; + } + } + component AppIcon inherits Rectangle { in property kind; in property style: IconTheme.style; - in property icon-color: #0879e1; + in property icon-color: AppTheme.accent; background: transparent; private property duotone: root.style == 1; @@ -134,7 +158,7 @@ slint::slint! { if root.has-badge: Path { width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24; commands: "M18 13.5 A4.5 4.5 0 1 1 17.99 13.5 Z"; - fill: root.style == 0 ? #ffffff : root.badge-color; + fill: root.style == 0 ? AppTheme.surface : root.badge-color; stroke: root.style == 0 ? root.badge-color : #ffffff; stroke-width: root.style == 2 ? 1.5px : 1.2px; } @@ -151,13 +175,30 @@ slint::slint! { in property value; in property label; callback clicked(); - background: IconTheme.style == root.value ? #e6f2fd : #f5f5f8; - border-color: IconTheme.style == root.value ? #0879e1 : #dedee3; + background: IconTheme.style == root.value ? AppTheme.accent-soft : AppTheme.surface-muted; + border-color: IconTheme.style == root.value ? AppTheme.accent : AppTheme.border; border-width: IconTheme.style == root.value ? 2px : 1px; border-radius: 11px; TouchArea { clicked => { root.clicked(); } } AppIcon { x: (parent.width - 30px) / 2; y: 10px; width: 30px; height: 30px; kind: "repositories"; style: root.value; } - Text { y: 48px; width: 100%; text: root.label; font-size: 12px; font-weight: IconTheme.style == root.value ? 650 : 500; color: #303036; horizontal-alignment: center; } + Text { y: 48px; width: 100%; text: root.label; font-size: 12px; font-weight: IconTheme.style == root.value ? 650 : 500; color: AppTheme.text; horizontal-alignment: center; } + } + + component AppearanceChoice inherits Rectangle { + in property value; + in property label; + callback clicked(); + background: AppTheme.mode == root.value ? AppTheme.accent-soft : AppTheme.surface-muted; + border-color: AppTheme.mode == root.value ? AppTheme.accent : AppTheme.border; + border-width: AppTheme.mode == root.value ? 2px : 1px; + border-radius: 11px; + TouchArea { clicked => { root.clicked(); } } + Text { + width: 100%; height: 100%; text: root.label; + font-size: 14px; font-weight: AppTheme.mode == root.value ? 650 : 500; + color: AppTheme.mode == root.value ? AppTheme.accent : AppTheme.text; + horizontal-alignment: center; vertical-alignment: center; + } } component Header inherits Rectangle { @@ -168,8 +209,8 @@ slint::slint! { callback filter(); height: 58px; width: 100%; - background: #f8f8fa; - border-color: #dedee3; + background: AppTheme.header; + border-color: AppTheme.border; border-width: 0px; if can_back: TouchArea { @@ -185,7 +226,7 @@ slint::slint! { vertical-alignment: center; font-size: 18px; font-weight: 600; - color: #151518; + color: AppTheme.text; overflow: elide; } if can_filter: TouchArea { @@ -193,7 +234,7 @@ slint::slint! { clicked => { root.filter(); } AppIcon { x: 17px; y: 17px; width: 24px; height: 24px; kind: "filter"; } } - Rectangle { y: parent.height - 1px; height: 1px; background: #dedee3; } + Rectangle { y: parent.height - 1px; height: 1px; background: AppTheme.border; } } component EdgeBack inherits SwipeGestureHandler { @@ -209,8 +250,8 @@ slint::slint! { in property detail; alignment: center; spacing: 8px; - Text { text: root.title; font-size: 21px; font-weight: 600; horizontal-alignment: center; color: #1c1c1e; } - Text { text: root.detail; font-size: 15px; horizontal-alignment: center; wrap: word-wrap; color: #69696f; } + Text { text: root.title; font-size: 21px; font-weight: 600; horizontal-alignment: center; color: AppTheme.text; } + Text { text: root.detail; font-size: 15px; horizontal-alignment: center; wrap: word-wrap; color: AppTheme.text-secondary; } } component MarkdownContent inherits VerticalLayout { @@ -219,17 +260,17 @@ slint::slint! { for block in root.blocks: Rectangle { height: block.kind == "rule" ? 9px : rendered.preferred-height + (block.kind == "code" || block.kind == "table" ? 16px : 0px); - background: block.kind == "code" || block.kind == "table" ? #e9e9ed : block.kind == "quote" ? #f6f6f8 : transparent; + background: block.kind == "code" || block.kind == "table" ? AppTheme.code-background : block.kind == "quote" ? AppTheme.quote-background : transparent; border-radius: block.kind == "code" || block.kind == "table" ? 7px : 0px; - if block.kind == "quote": Rectangle { width: 3px; height: 100%; background: #9a9aa1; border-radius: 2px; } - if block.kind == "rule": Rectangle { y: 4px; width: 100%; height: 1px; background: #d2d2d7; } + if block.kind == "quote": Rectangle { width: 3px; height: 100%; background: AppTheme.text-tertiary; border-radius: 2px; } + if block.kind == "rule": Rectangle { y: 4px; width: 100%; height: 1px; background: AppTheme.separator; } rendered := StyledText { x: block.kind == "code" || block.kind == "table" ? 8px : block.kind == "quote" ? 11px : 0px; y: block.kind == "code" || block.kind == "table" ? 8px : 0px; width: parent.width - self.x - (block.kind == "code" || block.kind == "table" ? 8px : 0px); text: block.text; - default-color: #252529; - link-color: #0879e1; + default-color: AppTheme.text; + link-color: AppTheme.accent; default-font-family: block.kind == "code" || block.kind == "table" ? "monospace" : ""; default-font-size: block.kind == "heading1" ? 24px : block.kind == "heading2" ? 21px : block.kind == "heading3" ? 19px : block.kind == "heading4" ? 17px : 16px; } @@ -239,14 +280,14 @@ slint::slint! { component CommentCard inherits Rectangle { in property comment; height: content.preferred-height + 24px; - background: #ffffff; + background: AppTheme.surface; border-radius: 10px; content := VerticalLayout { x: 12px; y: 10px; width: parent.width - 24px; spacing: 5px; - Text { text: root.comment.author; font-size: 13px; font-weight: 600; color: #252529; } + Text { text: root.comment.author; font-size: 13px; font-weight: 600; color: AppTheme.text; } MarkdownContent { width: parent.width; blocks: root.comment.body; } - Text { text: root.comment.meta; font-size: 11px; color: #85858b; } + Text { text: root.comment.meta; font-size: 11px; color: AppTheme.text-tertiary; } } } @@ -254,13 +295,13 @@ slint::slint! { in property file; callback clicked(); height: 66px; - background: #ffffff; - border-color: #dedee3; + background: AppTheme.surface; + border-color: AppTheme.border; border-width: 1px; border-radius: 10px; TouchArea { clicked => { root.clicked(); } } - Text { x: 13px; y: 10px; width: parent.width - 26px; text: root.file.path; font-size: 14px; font-weight: 600; color: #232327; overflow: elide; } - Text { x: 13px; y: 38px; width: parent.width - 26px; text: root.file.status; font-size: 12px; color: #74747a; } + Text { x: 13px; y: 10px; width: parent.width - 26px; text: root.file.path; font-size: 14px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 13px; y: 38px; width: parent.width - 26px; text: root.file.status; font-size: 12px; color: AppTheme.text-secondary; } } component TabButton inherits Rectangle { @@ -276,11 +317,11 @@ slint::slint! { x: (parent.width - root.icon-size) / 2; y: (root.icon-area-height - root.icon-size) / 2; width: root.icon-size; height: root.icon-size; - kind: root.kind; icon-color: root.active ? #0879e1 : #77777d; + kind: root.kind; icon-color: root.active ? AppTheme.accent : AppTheme.text-tertiary; } Text { y: root.icon-area-height; height: parent.height - self.y; width: 100%; text: root.label; - color: root.active ? #0879e1 : #77777d; font-size: 9px; + color: root.active ? AppTheme.accent : AppTheme.text-tertiary; font-size: 9px; font-weight: root.active ? 600 : 400; horizontal-alignment: center; vertical-alignment: center; } @@ -296,8 +337,8 @@ slint::slint! { @children if self.swiping: Rectangle { x: (parent.width - 142px) / 2; y: 8px; width: 142px; height: 32px; - background: #e9e9ee; border-radius: 16px; - Text { text: "Release to refresh"; font-size: 12px; color: #55555b; horizontal-alignment: center; vertical-alignment: center; } + background: AppTheme.surface-muted; border-radius: 16px; + Text { text: "Release to refresh"; font-size: 12px; color: AppTheme.text-secondary; horizontal-alignment: center; vertical-alignment: center; } } } @@ -305,7 +346,7 @@ slint::slint! { title: "Gotcha"; preferred-width: 390px; preferred-height: 844px; - background: #f2f2f7; + background: AppTheme.page; private property toolbar-height: 68px; in-out property tab: "home"; @@ -374,13 +415,14 @@ slint::slint! { callback select_activity(string, string, string, int, string); callback choose_filter(string); callback choose_icon_style(int); + callback choose_appearance(int); callback back(); if tab == "home": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: root.has_active_server ? root.home_server : "Home"; can_back: false; } if !root.has_active_server: EmptyState { @@ -391,13 +433,13 @@ slint::slint! { if root.has_active_server: Rectangle { x: 16px; y: 72px; width: parent.width - 32px; height: 118px; - background: #ffffff; border-radius: 12px; - Text { x: 13px; y: 10px; text: "Activity · last 12 months"; font-size: 13px; font-weight: 600; color: #38383d; } - Text { x: 13px; y: 86px; text: root.contribution_count + " contributions"; font-size: 12px; color: #74747a; } + background: AppTheme.surface; border-radius: 12px; + Text { x: 13px; y: 10px; text: "Activity · last 12 months"; font-size: 13px; font-weight: 600; color: AppTheme.text; } + Text { x: 13px; y: 86px; text: root.contribution_count + " contributions"; font-size: 12px; color: AppTheme.text-secondary; } for cell in root.heat_cells: Rectangle { x: 13px + cell.week * 6.7px; y: 39px + cell.day * 6.2px; width: 5.2px; height: 5.2px; border-radius: 1px; - background: cell.level == 0 ? #e5e5e9 : cell.level == 1 ? #b8d9f4 : cell.level == 2 ? #72b5e8 : cell.level == 3 ? #278bd4 : #0969b7; + background: cell.level == 0 ? (AppTheme.dark ? #3a3a3c : #e5e5e9) : cell.level == 1 ? #b8d9f4 : cell.level == 2 ? #72b5e8 : cell.level == 3 ? #278bd4 : #0969b7; } } @@ -418,19 +460,19 @@ slint::slint! { scrolled => { root.home_at_top = self.viewport-y >= 0px; } for activity in root.activities: Rectangle { height: 94px; - background: #ffffff; border-radius: 12px; + background: AppTheme.surface; border-radius: 12px; TouchArea { enabled: activity.target != ""; clicked => { root.select_activity(activity.target, activity.owner, activity.repository, activity.number, activity.sha); } } Rectangle { x: 12px; y: 14px; width: 34px; height: 34px; border-radius: 17px; - background: #e5f2fd; + background: AppTheme.accent-soft; AppIcon { x: 5px; y: 5px; width: 24px; height: 24px; kind: activity.icon; } } - Text { x: 57px; y: 11px; width: parent.width - 69px; height: 24px; text: activity.title; font-size: 15px; font-weight: 600; color: #19191d; overflow: elide; } - Text { x: 57px; y: 36px; width: parent.width - 69px; height: 35px; text: activity.detail; font-size: 13px; color: #55555c; wrap: word-wrap; overflow: elide; } - Text { x: 57px; y: 72px; width: parent.width - 69px; text: activity.meta; font-size: 11px; color: #898990; overflow: elide; } + Text { x: 57px; y: 11px; width: parent.width - 69px; height: 24px; text: activity.title; font-size: 15px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 57px; y: 36px; width: parent.width - 69px; height: 35px; text: activity.detail; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; overflow: elide; } + Text { x: 57px; y: 72px; width: parent.width - 69px; text: activity.meta; font-size: 11px; color: AppTheme.text-tertiary; overflow: elide; } } } } @@ -442,27 +484,36 @@ slint::slint! { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; can_back: false; title: "Settings"; } Rectangle { x: 16px; y: 76px; width: parent.width - 32px; height: 184px; - background: #ffffff; border-radius: 12px; - Text { x: 15px; y: 13px; text: "Icon style"; font-size: 18px; font-weight: 650; color: #1d1d21; } - Text { x: 15px; y: 43px; width: parent.width - 30px; text: "Choose how navigation and activity icons look throughout Gotcha."; font-size: 13px; color: #66666d; wrap: word-wrap; } + background: AppTheme.surface; border-radius: 12px; + Text { x: 15px; y: 13px; text: "Icon style"; font-size: 18px; font-weight: 650; color: AppTheme.text; } + Text { x: 15px; y: 43px; width: parent.width - 30px; text: "Choose how navigation and activity icons look throughout Gotcha."; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } IconStyleChoice { x: 12px; y: 91px; width: (parent.width - 36px) / 3; height: 78px; value: 0; label: "Outline"; clicked => { root.choose_icon_style(0); } } IconStyleChoice { x: 18px + (parent.width - 36px) / 3; y: 91px; width: (parent.width - 36px) / 3; height: 78px; value: 1; label: "Duotone"; clicked => { root.choose_icon_style(1); } } IconStyleChoice { x: 24px + 2 * (parent.width - 36px) / 3; y: 91px; width: (parent.width - 36px) / 3; height: 78px; value: 2; label: "Bold"; clicked => { root.choose_icon_style(2); } } } + Rectangle { + x: 16px; y: 276px; width: parent.width - 32px; height: 136px; + background: AppTheme.surface; border-radius: 12px; + Text { x: 15px; y: 13px; text: "Appearance"; font-size: 18px; font-weight: 650; color: AppTheme.text; } + Text { x: 15px; y: 43px; width: parent.width - 30px; text: "Follow iOS automatically or choose a fixed light or dark appearance."; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } + AppearanceChoice { x: 12px; y: 81px; width: (parent.width - 36px) / 3; height: 42px; value: 0; label: "Auto"; clicked => { root.choose_appearance(0); } } + AppearanceChoice { x: 18px + (parent.width - 36px) / 3; y: 81px; width: (parent.width - 36px) / 3; height: 42px; value: 1; label: "Light"; clicked => { root.choose_appearance(1); } } + AppearanceChoice { x: 24px + 2 * (parent.width - 36px) / 3; y: 81px; width: (parent.width - 36px) / 3; height: 42px; value: 2; label: "Dark"; clicked => { root.choose_appearance(2); } } + } } if (tab == "issues" || tab == "repositories") && page == "servers": Rectangle { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: "Servers"; can_back: false; } if root.servers.length > 0: servers_list := ListView { property scroll_request: root.servers_scroll_request; @@ -471,14 +522,14 @@ slint::slint! { changed scroll_request => { self.viewport-y = 0px; } for server[index] in root.servers: Rectangle { height: 76px; - background: #ffffff; - border-color: #dedee3; + background: AppTheme.surface; + border-color: AppTheme.border; border-width: 1px; border-radius: 12px; TouchArea { clicked => { root.select_server(index); } } - Text { x: 16px; y: 13px; width: parent.width - 54px; text: server.name; font-size: 17px; font-weight: 600; color: #19191c; overflow: elide; } - Text { x: 16px; y: 42px; width: parent.width - 54px; text: server.url; font-size: 13px; color: #6c6c72; overflow: elide; } - AppIcon { x: parent.width - 29px; y: 26px; width: 20px; height: 20px; kind: "disclosure"; icon-color: #a0a0a6; } + Text { x: 16px; y: 13px; width: parent.width - 54px; text: server.name; font-size: 17px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 16px; y: 42px; width: parent.width - 54px; text: server.url; font-size: 13px; color: AppTheme.text-secondary; overflow: elide; } + AppIcon { x: parent.width - 29px; y: 26px; width: 20px; height: 20px; kind: "disclosure"; icon-color: AppTheme.text-tertiary; } } } Button { x: 16px; y: parent.height - 66px; width: parent.width - 32px; height: 48px; text: "Add Server"; clicked => { root.show_add_server(); } } @@ -488,15 +539,15 @@ slint::slint! { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: "Add Server"; can_back: true; back => { root.back(); } } VerticalLayout { x: 20px; y: 92px; width: parent.width - 40px; height: 260px; spacing: 10px; - Text { text: "Name"; font-size: 13px; color: #66666c; } + Text { text: "Name"; font-size: 13px; color: AppTheme.text-secondary; } server_name := LineEdit { placeholder-text: "Work"; } - Text { text: "Server URL"; font-size: 13px; color: #66666c; } + Text { text: "Server URL"; font-size: 13px; color: AppTheme.text-secondary; } server_url := LineEdit { placeholder-text: "https://gitea.example.com"; } - Text { text: "Access token"; font-size: 13px; color: #66666c; } + Text { text: "Access token"; font-size: 13px; color: AppTheme.text-secondary; } server_token := LineEdit { placeholder-text: "Token"; input-type: InputType.password; } } Button { @@ -512,7 +563,7 @@ slint::slint! { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: root.server_title; can_back: true; back => { root.back(); } } if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } if !root.loading && root.repositories.length == 0: EmptyState { @@ -531,18 +582,18 @@ slint::slint! { scrolled => { root.repositories_at_top = self.viewport-y >= 0px; } for repo in root.repositories: Rectangle { height: 104px; - background: #ffffff; - border-color: #dedee3; + background: AppTheme.surface; + border-color: AppTheme.border; border-width: 1px; border-radius: 12px; TouchArea { clicked => { root.select_repository(repo.owner, repo.name); } } - Text { x: 15px; y: 11px; width: parent.width - 62px; text: repo.name; font-size: 17px; font-weight: 600; color: #17171a; overflow: elide; } - Text { x: 15px; y: 39px; width: parent.width - 30px; text: repo.description; font-size: 14px; color: #56565c; overflow: elide; } - Text { x: 15px; y: 71px; width: parent.width - 30px; text: repo.meta; font-size: 12px; color: #7b7b82; overflow: elide; } + Text { x: 15px; y: 11px; width: parent.width - 62px; text: repo.name; font-size: 17px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 15px; y: 39px; width: parent.width - 30px; text: repo.description; font-size: 14px; color: AppTheme.text-secondary; overflow: elide; } + Text { x: 15px; y: 71px; width: parent.width - 30px; text: repo.meta; font-size: 12px; color: AppTheme.text-tertiary; overflow: elide; } TouchArea { x: parent.width - 52px; y: 2px; width: 48px; height: 48px; clicked => { root.toggle_favorite(repo.owner, repo.name); } - AppIcon { x: 10px; y: 10px; width: 28px; height: 28px; kind: repo.favorite ? "favorite-filled" : "favorite"; icon-color: repo.favorite ? #f2a900 : #888890; } + AppIcon { x: 10px; y: 10px; width: 28px; height: 28px; kind: repo.favorite ? "favorite-filled" : "favorite"; icon-color: repo.favorite ? #f2a900 : AppTheme.text-tertiary; } } } } @@ -554,7 +605,7 @@ slint::slint! { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: root.repository_title; can_back: true; can_filter: true; back => { root.back(); } @@ -577,13 +628,13 @@ slint::slint! { scrolled => { root.issues_at_top = self.viewport-y >= 0px; } for issue in root.issues: Rectangle { height: issue.labels.length > 0 ? 138px : 112px; - background: #ffffff; - border-color: #dedee3; + background: AppTheme.surface; + border-color: AppTheme.border; border-width: 1px; border-radius: 12px; TouchArea { clicked => { root.select_issue(issue.number); } } - Text { x: 15px; y: 11px; width: parent.width - 44px; text: issue.title; font-size: 16px; font-weight: 600; color: #17171a; overflow: elide; } - Text { x: 15px; y: 39px; width: parent.width - 30px; height: 38px; text: issue.summary; font-size: 13px; color: #56565c; wrap: word-wrap; overflow: elide; } + Text { x: 15px; y: 11px; width: parent.width - 44px; text: issue.title; font-size: 16px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 15px; y: 39px; width: parent.width - 30px; height: 38px; text: issue.summary; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; overflow: elide; } if issue.labels.length > 0: HorizontalLayout { x: 15px; y: 82px; width: parent.width - 30px; height: 22px; spacing: 5px; for label in issue.labels: Rectangle { @@ -592,7 +643,7 @@ slint::slint! { Text { x: 7px; width: parent.width - 14px; text: label.name; color: label.foreground; font-size: 11px; font-weight: 600; horizontal-alignment: center; vertical-alignment: center; overflow: elide; } } } - Text { x: 15px; y: issue.labels.length > 0 ? 112px : 86px; width: parent.width - 30px; text: issue.meta; font-size: 12px; color: #7b7b82; overflow: elide; } + Text { x: 15px; y: issue.labels.length > 0 ? 112px : 86px; width: parent.width - 30px; text: issue.meta; font-size: 12px; color: AppTheme.text-tertiary; overflow: elide; } } } } @@ -603,7 +654,7 @@ slint::slint! { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: "Issue"; can_back: true; back => { root.back(); } } if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } PullToRefreshGesture { @@ -619,11 +670,11 @@ slint::slint! { width: parent.width; spacing: 12px; alignment: start; - Text { text: root.issue_title; font-size: 23px; font-weight: 650; color: #161619; wrap: word-wrap; } - Text { text: root.issue_meta; font-size: 13px; color: #707077; wrap: word-wrap; } - Rectangle { height: 1px; background: #d8d8dd; } + Text { text: root.issue_title; font-size: 23px; font-weight: 650; color: AppTheme.text; wrap: word-wrap; } + Text { text: root.issue_meta; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } + Rectangle { height: 1px; background: AppTheme.separator; } MarkdownContent { width: parent.width; blocks: root.issue_body; } - if root.comments.length > 0: Text { text: "Comments"; font-size: 17px; font-weight: 600; color: #252529; } + if root.comments.length > 0: Text { text: "Comments"; font-size: 17px; font-weight: 600; color: AppTheme.text; } for comment in root.comments: CommentCard { width: parent.width; comment: comment; } } } @@ -635,7 +686,7 @@ slint::slint! { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: "Home"; can_back: false; } EmptyState { x: 32px; width: parent.width - 64px; y: 210px; height: 190px; @@ -648,7 +699,7 @@ slint::slint! { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: "Pull Requests"; can_back: false; can_filter: true; filter => { root.filter_open = true; } @@ -668,12 +719,12 @@ slint::slint! { scrolled => { root.issues_at_top = self.viewport-y >= 0px; } for pull in root.pulls: Rectangle { height: 112px; - background: #ffffff; border-color: #dedee3; border-width: 1px; border-radius: 12px; + background: AppTheme.surface; border-color: AppTheme.border; border-width: 1px; border-radius: 12px; TouchArea { clicked => { root.select_pull(pull.owner, pull.repository, pull.number); } } - Text { x: 14px; y: 10px; width: parent.width - 28px; text: pull.repository + " #" + pull.number; font-size: 12px; color: #727278; overflow: elide; } - Text { x: 14px; y: 31px; width: parent.width - 28px; text: pull.title; font-size: 16px; font-weight: 600; color: #17171a; overflow: elide; } - Text { x: 14px; y: 57px; width: parent.width - 28px; text: pull.summary; font-size: 13px; color: #56565c; overflow: elide; } - Text { x: 14px; y: 85px; width: parent.width - 28px; text: pull.meta; font-size: 12px; color: #7b7b82; overflow: elide; } + Text { x: 14px; y: 10px; width: parent.width - 28px; text: pull.repository + " #" + pull.number; font-size: 12px; color: AppTheme.text-secondary; overflow: elide; } + Text { x: 14px; y: 31px; width: parent.width - 28px; text: pull.title; font-size: 16px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 14px; y: 57px; width: parent.width - 28px; text: pull.summary; font-size: 13px; color: AppTheme.text-secondary; overflow: elide; } + Text { x: 14px; y: 85px; width: parent.width - 28px; text: pull.meta; font-size: 12px; color: AppTheme.text-tertiary; overflow: elide; } } } } @@ -683,7 +734,7 @@ slint::slint! { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: "Pull Request"; can_back: true; back => { root.back(); } } if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } PullToRefreshGesture { @@ -695,13 +746,13 @@ slint::slint! { scrolled => { root.issue_at_top = self.viewport-y >= 0px; } VerticalLayout { width: parent.width; spacing: 12px; alignment: start; - Text { text: root.detail_title; font-size: 23px; font-weight: 650; color: #161619; wrap: word-wrap; } - Text { text: root.detail_meta; font-size: 13px; color: #707077; wrap: word-wrap; } - Rectangle { height: 1px; background: #d8d8dd; } + Text { text: root.detail_title; font-size: 23px; font-weight: 650; color: AppTheme.text; wrap: word-wrap; } + Text { text: root.detail_meta; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } + Rectangle { height: 1px; background: AppTheme.separator; } MarkdownContent { width: parent.width; blocks: root.detail_body; } - if root.files.length > 0: Text { text: root.detail_files_ref; font-size: 17px; font-weight: 600; color: #252529; } + if root.files.length > 0: Text { text: root.detail_files_ref; font-size: 17px; font-weight: 600; color: AppTheme.text; } for file in root.files: FileCard { width: parent.width; file: file; clicked => { root.select_pull_file(file.path); } } - if root.comments.length > 0: Text { text: "Comments"; font-size: 17px; font-weight: 600; color: #252529; } + if root.comments.length > 0: Text { text: "Comments"; font-size: 17px; font-weight: 600; color: AppTheme.text; } for comment in root.comments: CommentCard { width: parent.width; comment: comment; } } } @@ -713,7 +764,7 @@ slint::slint! { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: root.repository_title; can_back: true; back => { root.back(); } } ComboBox { x: 16px; y: 66px; width: parent.width - 32px; height: 42px; @@ -737,7 +788,7 @@ slint::slint! { property graph-width: commit.lanes.length > 0 ? min(72px, commit.lanes.length * 10px + 8px) : 0px; property lane-spacing: (self.graph-width - 8px) / max(1, commit.lanes.length); height: 86px; background: transparent; - Rectangle { width: 100%; height: 100%; background: #ffffff; border-color: #dedee3; border-width: 1px; border-radius: 12px; } + Rectangle { width: 100%; height: 100%; background: AppTheme.surface; border-color: AppTheme.border; border-width: 1px; border-radius: 12px; } TouchArea { clicked => { root.select_commit(commit.sha); } } for lane[index] in commit.lanes: Rectangle { x: 4px + index * commit_row.lane-spacing; @@ -774,12 +825,12 @@ slint::slint! { } if lane.node: Rectangle { x: -3px; y: 36px; width: 9px; height: 9px; border-radius: 5px; - background: lane.color; border-color: #ffffff; border-width: 2px; + background: lane.color; border-color: AppTheme.surface; border-width: 2px; } } - Text { x: parent.graph-width + 14px; y: 8px; width: parent.width - self.x - 14px; text: commit.title; font-size: 15px; font-weight: 600; color: #17171a; overflow: elide; } - Text { x: parent.graph-width + 14px; y: 33px; width: parent.width - self.x - 14px; text: commit.refs != "" ? commit.refs : commit.meta; font-size: 12px; color: commit.refs != "" ? #0879e1 : #707077; overflow: elide; } - Text { x: parent.graph-width + 14px; y: 53px; width: parent.width - self.x - 14px; text: commit.refs != "" ? commit.meta + " · " + commit.sha : commit.sha; font-size: 11px; color: #919198; overflow: elide; } + Text { x: parent.graph-width + 14px; y: 8px; width: parent.width - self.x - 14px; text: commit.title; font-size: 15px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: parent.graph-width + 14px; y: 33px; width: parent.width - self.x - 14px; text: commit.refs != "" ? commit.refs : commit.meta; font-size: 12px; color: commit.refs != "" ? AppTheme.accent : AppTheme.text-secondary; overflow: elide; } + Text { x: parent.graph-width + 14px; y: 53px; width: parent.width - self.x - 14px; text: commit.refs != "" ? commit.meta + " · " + commit.sha : commit.sha; font-size: 11px; color: AppTheme.text-tertiary; overflow: elide; } } } } @@ -790,7 +841,7 @@ slint::slint! { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: "Changed Files"; can_back: true; back => { root.back(); } } if !root.loading && root.files.length == 0: EmptyState { x: 34px; width: parent.width - 68px; y: 220px; height: 160px; @@ -808,7 +859,7 @@ slint::slint! { x: root.safe-area-insets.left; y: root.safe-area-insets.top; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: #f2f2f7; + background: AppTheme.page; Header { x: 0; y: 0; title: root.detail_title; can_back: true; back => { root.back(); } } ScrollView { x: 12px; y: 70px; width: parent.width - 24px; height: parent.height - 82px; mouse-drag-pan-enabled: true; @@ -818,13 +869,13 @@ slint::slint! { width: parent.viewport-width; for line in root.diff_lines: Rectangle { height: 20px; - background: line.kind == "addition" ? #ddf5e3 : line.kind == "removal" ? #fde2e1 : line.kind == "hunk" ? #dcecff : line.kind == "header" ? #ececf0 : #ffffff; - Text { x: 0; width: 38px; text: line.old_number; font-family: "monospace"; font-size: 11px; color: #85858c; horizontal-alignment: right; vertical-alignment: center; } - Text { x: 42px; width: 38px; text: line.new_number; font-family: "monospace"; font-size: 11px; color: #85858c; horizontal-alignment: right; vertical-alignment: center; } - Rectangle { x: 84px; width: 1px; height: 100%; background: #d5d5da; } + background: line.kind == "addition" ? (AppTheme.dark ? #173d25 : #ddf5e3) : line.kind == "removal" ? (AppTheme.dark ? #4a2020 : #fde2e1) : line.kind == "hunk" ? (AppTheme.dark ? #17324d : #dcecff) : line.kind == "header" ? AppTheme.surface-muted : AppTheme.surface; + Text { x: 0; width: 38px; text: line.old_number; font-family: "monospace"; font-size: 11px; color: AppTheme.text-tertiary; horizontal-alignment: right; vertical-alignment: center; } + Text { x: 42px; width: 38px; text: line.new_number; font-family: "monospace"; font-size: 11px; color: AppTheme.text-tertiary; horizontal-alignment: right; vertical-alignment: center; } + Rectangle { x: 84px; width: 1px; height: 100%; background: AppTheme.separator; } Text { x: 90px; width: parent.width - 94px; text: line.text; font-family: "monospace"; font-size: 11px; - color: line.kind == "addition" ? #176b2c : line.kind == "removal" ? #a12720 : line.kind == "hunk" ? #175b9e : #202024; + color: line.kind == "addition" ? (AppTheme.dark ? #7fe787 : #176b2c) : line.kind == "removal" ? (AppTheme.dark ? #ff7b72 : #a12720) : line.kind == "hunk" ? (AppTheme.dark ? #79c0ff : #175b9e) : AppTheme.text; wrap: no-wrap; overflow: clip; vertical-alignment: center; } } @@ -838,7 +889,7 @@ slint::slint! { y: root.height - root.safe-area-insets.bottom - root.toolbar-height; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; height: root.toolbar-height; - background: #fbfbfc; + background: AppTheme.toolbar; TabButton { x: 0; width: parent.width / 5; height: parent.height; kind: "home"; label: "Home"; active: root.tab == "home"; clicked => { @@ -865,17 +916,17 @@ slint::slint! { if root.loading && (root.tab == "issues" || root.tab == "repositories" || root.tab == "pulls"): Rectangle { width: 100%; height: 100%; - background: #55ffffff; + background: AppTheme.loading-overlay; TouchArea { } Spinner { width: 38px; height: 38px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; } } if root.filter_open: Rectangle { - width: 100%; height: 100%; background: #66000000; + width: 100%; height: 100%; background: #00000066; TouchArea { clicked => { root.filter_open = false; } } Rectangle { x: 28px; y: (parent.height - 190px) / 2; width: parent.width - 56px; height: 190px; - background: #ffffff; border-radius: 14px; - Text { x: 18px; y: 16px; width: parent.width - 36px; text: "Status"; font-size: 19px; font-weight: 650; color: #202024; } + background: AppTheme.surface; border-radius: 14px; + Text { x: 18px; y: 16px; width: parent.width - 36px; text: "Status"; font-size: 19px; font-weight: 650; color: AppTheme.text; } VerticalLayout { x: 18px; y: 54px; width: parent.width - 36px; height: 118px; spacing: 8px; Button { text: "Open"; clicked => { root.choose_filter("open"); root.filter_open = false; } }