feat: add selectable icon styles

This commit is contained in:
Georg Bauer
2026-07-30 18:13:42 +02:00
parent 8c383e3672
commit d8a63c00f5
5 changed files with 326 additions and 66 deletions

View File

@@ -3,6 +3,35 @@ use std::collections::BTreeSet;
use gotcha_gitea::models; use gotcha_gitea::models;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum IconStyle {
Duotone,
Bold,
#[default]
#[serde(other)]
Outline,
}
impl IconStyle {
pub fn index(self) -> i32 {
match self {
Self::Outline => 0,
Self::Duotone => 1,
Self::Bold => 2,
}
}
pub fn from_index(index: i32) -> Option<Self> {
match index {
0 => Some(Self::Outline),
1 => Some(Self::Duotone),
2 => Some(Self::Bold),
_ => None,
}
}
}
#[derive(Clone, Deserialize, Serialize)] #[derive(Clone, Deserialize, Serialize)]
pub struct Server { pub struct Server {
pub name: String, pub name: String,
@@ -23,6 +52,8 @@ pub struct Preferences {
pub issue_status: String, pub issue_status: String,
#[serde(default = "open_status")] #[serde(default = "open_status")]
pub pull_status: String, pub pull_status: String,
#[serde(default)]
pub icon_style: IconStyle,
} }
impl Default for Preferences { impl Default for Preferences {
@@ -33,6 +64,7 @@ impl Default for Preferences {
last_server: None, last_server: None,
issue_status: open_status(), issue_status: open_status(),
pull_status: open_status(), pull_status: open_status(),
icon_style: IconStyle::default(),
} }
} }
} }

View File

@@ -39,6 +39,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let state = state.lock().unwrap(); let state = state.lock().unwrap();
ui.set_issue_filter(state.preferences.issue_status.clone().into()); ui.set_issue_filter(state.preferences.issue_status.clone().into());
ui.set_pull_filter(state.preferences.pull_status.clone().into()); ui.set_pull_filter(state.preferences.pull_status.clone().into());
ui.global::<IconTheme>()
.set_style(state.preferences.icon_style.index());
} }
if let Some(error) = startup_error { if let Some(error) = startup_error {
ui.set_error(error.into()); ui.set_error(error.into());
@@ -205,6 +207,30 @@ fn wire_callbacks(ui: &AppWindow, state: Arc<Mutex<State>>) {
} }
}); });
ui.on_choose_icon_style({
let weak = ui.as_weak();
let state = state.clone();
move |index| {
let Some(style) = IconStyle::from_index(index) else {
return;
};
let result = {
let mut state = state.lock().unwrap();
let previous = state.preferences.icon_style;
state.preferences.icon_style = style;
save_preferences(&state.preferences).inspect_err(|_| {
state.preferences.icon_style = previous;
})
};
if let Some(ui) = weak.upgrade() {
match result {
Ok(()) => ui.global::<IconTheme>().set_style(index),
Err(error) => ui.set_error(error.into()),
}
}
}
});
ui.on_show_add_server({ ui.on_show_add_server({
let weak = ui.as_weak(); let weak = ui.as_weak();
move || { move || {

View File

@@ -96,5 +96,26 @@ mod tests {
"https://gitea.example.com|octo/demo" "https://gitea.example.com|octo/demo"
); );
assert_eq!(Preferences::default().pull_status, "open"); assert_eq!(Preferences::default().pull_status, "open");
assert_eq!(
Preferences::default().icon_style,
crate::domain::IconStyle::Outline
);
assert_eq!(
serde_json::from_str::<Preferences>(r#"{"icon_style":"duotone"}"#)
.unwrap()
.icon_style,
crate::domain::IconStyle::Duotone
);
assert_eq!(
serde_json::from_str::<Preferences>(r#"{"icon_style":"future-style"}"#)
.unwrap()
.icon_style,
crate::domain::IconStyle::Outline
);
assert_eq!(
crate::domain::IconStyle::from_index(2),
Some(crate::domain::IconStyle::Bold)
);
assert_eq!(crate::domain::IconStyle::from_index(3), None);
} }
} }

View File

@@ -26,6 +26,140 @@ slint::slint! {
export struct DiffLine { old_number: string, new_number: string, text: string, kind: string } export struct DiffLine { old_number: string, new_number: string, text: string, kind: string }
export struct HeatCell { week: int, day: int, level: int } export struct HeatCell { week: int, day: int, level: int }
export global IconTheme {
in-out property <int> style: 0;
}
component AppIcon inherits Rectangle {
in property <string> kind;
in property <int> style: IconTheme.style;
in property <color> icon-color: #0879e1;
background: transparent;
private property <bool> duotone: root.style == 1;
private property <bool> bold: root.style == 2;
private property <bool> issue-kind: root.kind == "issues" || root.kind == "issue-open" || root.kind == "issue-close" || root.kind == "issue-reopen" || root.kind == "issue-comment";
private property <bool> repository-kind: root.kind == "repositories" || root.kind == "repo-create" || root.kind == "repo-rename" || root.kind == "repo-star" || root.kind == "repo-watch" || root.kind == "update";
private property <bool> pull-kind: root.kind == "pulls" || root.kind == "pull-open" || root.kind == "pull-merge" || root.kind == "pull-close" || root.kind == "pull-reopen" || root.kind == "pull-comment" || root.kind == "pull-approve" || root.kind == "pull-reject";
private property <bool> tag-kind: root.kind == "tag-push" || root.kind == "tag-delete";
private property <bool> branch-kind: root.kind == "branch-create" || root.kind == "branch-delete";
private property <bool> closed-shape: root.kind == "home" || root.issue-kind || root.repository-kind || root.kind == "favorite" || root.kind == "favorite-filled" || root.tag-kind || root.kind == "release";
private property <bool> has-badge: root.kind == "branch-create" || root.kind == "branch-delete" || root.kind == "push" || root.kind == "repo-create" || root.kind == "repo-rename" || root.kind == "repo-star" || root.kind == "repo-watch" || root.kind == "update" || root.kind == "issue-open" || root.kind == "issue-close" || root.kind == "issue-reopen" || root.kind == "issue-comment" || root.kind == "pull-open" || root.kind == "pull-merge" || root.kind == "pull-close" || root.kind == "pull-reopen" || root.kind == "pull-comment" || root.kind == "pull-approve" || root.kind == "pull-reject" || root.kind == "tag-push" || root.kind == "tag-delete";
private property <bool> positive: root.kind == "issue-close" || root.kind == "pull-merge" || root.kind == "pull-approve";
private property <bool> destructive: root.kind == "branch-delete" || root.kind == "pull-close" || root.kind == "pull-reject" || root.kind == "tag-delete";
private property <color> badge-color: root.positive ? #20a866 : root.destructive ? #e45b4d : root.duotone ? #26bfd1 : root.icon-color;
private property <string> shape: root.kind == "home"
? "M3 10.5 L12 3 L21 10.5 V21 H15 V14 H9 V21 H3 Z"
: root.issue-kind
? "M4 5 H20 V9 C18 9 18 15 20 15 V19 H4 V15 C6 15 6 9 4 9 Z"
: root.repository-kind
? "M6 3 H18 C19.1 3 20 3.9 20 5 V21 H6 C4.9 21 4 20.1 4 19 V5 C4 3.9 4.9 3 6 3 Z"
: root.pull-kind
? "M7 4 V20 M17 4 V11 C17 15 14 17 10 17 M7 4 H7 M17 4 H17"
: root.kind == "settings"
? "M12 3 V6 M12 18 V21 M3 12 H6 M18 12 H21 M5.6 5.6 L7.8 7.8 M16.2 16.2 L18.4 18.4 M18.4 5.6 L16.2 7.8 M7.8 16.2 L5.6 18.4"
: root.kind == "back"
? (root.bold ? "M16 4 L7 12 L16 20 Z" : "M16 4 L7 12 L16 20")
: root.kind == "filter"
? (root.bold ? "M3 5 H21 L14 13 V20 L10 22 V13 Z" : "M4 6 H20 M4 12 H20 M4 18 H20")
: root.kind == "favorite" || root.kind == "favorite-filled"
? "M12 3 L14.8 8.7 L21 9.6 L16.5 14 L17.6 20.3 L12 17.3 L6.4 20.3 L7.5 14 L3 9.6 L9.2 8.7 Z"
: root.kind == "disclosure"
? (root.bold ? "M8 4 L17 12 L8 20 Z" : "M8 4 L17 12 L8 20")
: root.branch-kind
? "M7 4 V20 M7 11 C11 11 14 9 14 5"
: root.kind == "push"
? "M4 8 H13 M4 12 H13 M4 16 H13"
: root.tag-kind
? "M3 5 V13 L11 21 L21 11 L13 3 H5 C3.9 3 3 3.9 3 5 Z"
: root.kind == "release"
? "M4 8 L12 4 L20 8 V18 L12 22 L4 18 Z"
: "M12 4 V20 M4 12 H20";
private property <string> detail: root.kind == "home"
? "M9 21 V14 H15 V21"
: root.issue-kind
? "M12 8 V13 M12 16.5 V17"
: root.repository-kind
? "M7 3 V21 M11 9 L8.5 12 L11 15 M15 9 L17.5 12 L15 15"
: root.pull-kind
? "M7 4 A1.8 1.8 0 1 1 6.99 4 M7 20 A1.8 1.8 0 1 1 6.99 20 M17 4 A1.8 1.8 0 1 1 16.99 4"
: root.kind == "settings"
? "M12 8 A4 4 0 1 1 11.99 8"
: root.kind == "filter" && !root.bold
? "M9 4 V8 M15 10 V14 M8 16 V20"
: root.tag-kind
? "M8 8 A1.5 1.5 0 1 1 7.99 8"
: root.kind == "release"
? "M12 18 V8 M9 11 L12 8 L15 11 M7 6 V3 M17 6 V3"
: "";
private property <string> badge-detail: root.kind == "branch-create" || root.kind == "repo-create" || root.kind == "issue-open" || root.kind == "pull-open"
? "M18 15.5 V20.5 M15.5 18 H20.5"
: root.kind == "issue-close" || root.kind == "pull-merge" || root.kind == "pull-approve"
? "M15.4 18 L17.2 19.8 L20.7 16.2"
: root.kind == "branch-delete" || root.kind == "tag-delete"
? "M15.3 18 H20.7"
: root.kind == "pull-close" || root.kind == "pull-reject"
? "M16 16 L20 20 M20 16 L16 20"
: root.kind == "push" || root.kind == "tag-push"
? "M18 20.5 V15.5 M15.7 17.8 L18 15.5 L20.3 17.8"
: root.kind == "repo-rename"
? "M15.3 20.6 L16 18 L19.7 14.3 L21.7 16.3 L18 20 Z"
: root.kind == "repo-star"
? "M18 14.5 L19 16.7 L21.4 17 L19.6 18.7 L20.1 21 L18 19.8 L15.9 21 L16.4 18.7 L14.6 17 L17 16.7 Z"
: root.kind == "repo-watch"
? "M14.5 18 C16.3 15.8 19.7 15.8 21.5 18 C19.7 20.2 16.3 20.2 14.5 18 M18 17 A1 1 0 1 1 17.99 17"
: root.kind == "issue-reopen" || root.kind == "pull-reopen"
? "M20.8 17 A3 3 0 1 0 20.2 20.3 M20.8 17 H18.5 M20.8 17 V19.3"
: root.kind == "issue-comment" || root.kind == "pull-comment"
? "M15.5 17 H20.5 V20 H18 L16 21 V20 H15.5 Z M17 18.5 H17.1 M19 18.5 H19.1"
: root.kind == "update"
? "M18 14.8 L18.8 17.2 L21.2 18 L18.8 18.8 L18 21.2 L17.2 18.8 L14.8 18 L17.2 17.2 Z"
: "";
Path {
width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24;
commands: root.shape;
fill: root.closed-shape ? (root.bold || root.kind == "favorite-filled" ? root.icon-color : root.duotone ? #26bfd133 : transparent) : transparent;
stroke: root.closed-shape && (root.bold || root.kind == "favorite-filled") ? transparent : root.icon-color;
stroke-width: root.bold ? 3px : root.duotone ? 2.2px : 1.8px;
stroke-line-cap: round; stroke-line-join: round;
}
if root.detail != "": Path {
width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24;
commands: root.detail; fill: transparent;
stroke: root.bold && root.closed-shape ? #ffffff : root.icon-color;
stroke-width: root.bold ? 2.4px : 1.8px;
stroke-line-cap: round; stroke-line-join: round;
}
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;
stroke: root.style == 0 ? root.badge-color : #ffffff;
stroke-width: root.style == 2 ? 1.5px : 1.2px;
}
if root.has-badge: Path {
width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24;
commands: root.badge-detail; fill: root.style == 0 ? transparent : #ffffff;
stroke: root.style == 0 ? root.badge-color : #ffffff;
stroke-width: root.bold ? 2px : 1.6px;
stroke-line-cap: round; stroke-line-join: round;
}
}
component IconStyleChoice inherits Rectangle {
in property <int> value;
in property <string> label;
callback clicked();
background: IconTheme.style == root.value ? #e6f2fd : #f5f5f8;
border-color: IconTheme.style == root.value ? #0879e1 : #dedee3;
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; }
}
component Header inherits Rectangle { component Header inherits Rectangle {
in property <string> title; in property <string> title;
in property <bool> can_back; in property <bool> can_back;
@@ -41,7 +175,7 @@ slint::slint! {
if can_back: TouchArea { if can_back: TouchArea {
x: 8px; width: 54px; x: 8px; width: 54px;
clicked => { root.back(); } clicked => { root.back(); }
Text { text: ""; color: #0879e1; font-size: 38px; vertical-alignment: center; } AppIcon { x: 7px; y: 17px; width: 24px; height: 24px; kind: "back"; }
} }
Text { Text {
x: root.can_back || root.can_filter ? 62px : 12px; x: root.can_back || root.can_filter ? 62px : 12px;
@@ -57,7 +191,7 @@ slint::slint! {
if can_filter: TouchArea { if can_filter: TouchArea {
x: parent.width - 58px; width: 58px; x: parent.width - 58px; width: 58px;
clicked => { root.filter(); } clicked => { root.filter(); }
Text { text: ""; color: #0879e1; font-size: 25px; horizontal-alignment: center; vertical-alignment: center; } 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: #dedee3; }
} }
@@ -130,20 +264,23 @@ slint::slint! {
} }
component TabButton inherits Rectangle { component TabButton inherits Rectangle {
in property <string> icon; in property <string> kind;
in property <string> label; in property <string> label;
in property <bool> active; in property <bool> active;
private property <length> icon-area-height: root.height * 3 / 4;
private property <length> icon-size: min(42px, self.icon-area-height - 6px);
callback clicked(); callback clicked();
background: transparent; background: transparent;
TouchArea { clicked => { root.clicked(); } } TouchArea { clicked => { root.clicked(); } }
Text { AppIcon {
y: 4px; height: 29px; width: 100%; text: root.icon; x: (parent.width - root.icon-size) / 2;
color: root.active ? #0879e1 : #77777d; font-size: 24px; y: (root.icon-area-height - root.icon-size) / 2;
horizontal-alignment: center; vertical-alignment: center; width: root.icon-size; height: root.icon-size;
kind: root.kind; icon-color: root.active ? #0879e1 : #77777d;
} }
Text { Text {
y: 34px; height: 18px; width: 100%; text: root.label; y: root.icon-area-height; height: parent.height - self.y; width: 100%; text: root.label;
color: root.active ? #0879e1 : #77777d; font-size: 10px; color: root.active ? #0879e1 : #77777d; font-size: 9px;
font-weight: root.active ? 600 : 400; font-weight: root.active ? 600 : 400;
horizontal-alignment: center; vertical-alignment: center; horizontal-alignment: center; vertical-alignment: center;
} }
@@ -169,6 +306,7 @@ slint::slint! {
preferred-width: 390px; preferred-width: 390px;
preferred-height: 844px; preferred-height: 844px;
background: #f2f2f7; background: #f2f2f7;
private property <length> toolbar-height: 68px;
in-out property <string> tab: "home"; in-out property <string> tab: "home";
in-out property <string> page: "servers"; in-out property <string> page: "servers";
@@ -235,12 +373,13 @@ slint::slint! {
callback select_pull_file(string); callback select_pull_file(string);
callback select_activity(string, string, string, int, string); callback select_activity(string, string, string, int, string);
callback choose_filter(string); callback choose_filter(string);
callback choose_icon_style(int);
callback back(); callback back();
if tab == "home": Rectangle { if tab == "home": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { x: 0; y: 0; title: root.has_active_server ? root.home_server : "Home"; can_back: false; } Header { x: 0; y: 0; title: root.has_active_server ? root.home_server : "Home"; can_back: false; }
@@ -287,7 +426,7 @@ slint::slint! {
Rectangle { Rectangle {
x: 12px; y: 14px; width: 34px; height: 34px; border-radius: 17px; x: 12px; y: 14px; width: 34px; height: 34px; border-radius: 17px;
background: #e5f2fd; background: #e5f2fd;
Text { text: activity.icon; color: #0879e1; font-size: 20px; horizontal-alignment: center; vertical-alignment: center; } 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: 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: 36px; width: parent.width - 69px; height: 35px; text: activity.detail; font-size: 13px; color: #55555c; wrap: word-wrap; overflow: elide; }
@@ -302,23 +441,27 @@ slint::slint! {
if tab == "settings": Rectangle { if tab == "settings": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { Header {
x: 0; y: 0; can_back: false; x: 0; y: 0; can_back: false;
title: "Settings"; title: "Settings";
} }
EmptyState { Rectangle {
x: 32px; width: parent.width - 64px; y: 220px; height: 160px; x: 16px; y: 76px; width: parent.width - 32px; height: 184px;
title: "Coming soon"; background: #ffffff; border-radius: 12px;
detail: "This section is ready for the next feature pass."; 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; }
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); } }
} }
} }
if (tab == "issues" || tab == "repositories") && page == "servers": Rectangle { if (tab == "issues" || tab == "repositories") && page == "servers": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { x: 0; y: 0; title: "Servers"; can_back: false; } Header { x: 0; y: 0; title: "Servers"; can_back: false; }
if root.servers.length > 0: servers_list := ListView { if root.servers.length > 0: servers_list := ListView {
@@ -335,7 +478,7 @@ slint::slint! {
TouchArea { clicked => { root.select_server(index); } } 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: 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; } Text { x: 16px; y: 42px; width: parent.width - 54px; text: server.url; font-size: 13px; color: #6c6c72; overflow: elide; }
Text { x: parent.width - 28px; text: ""; font-size: 27px; color: #a0a0a6; vertical-alignment: center; } AppIcon { x: parent.width - 29px; y: 26px; width: 20px; height: 20px; kind: "disclosure"; icon-color: #a0a0a6; }
} }
} }
Button { x: 16px; y: parent.height - 66px; width: parent.width - 32px; height: 48px; text: "Add Server"; clicked => { root.show_add_server(); } } Button { x: 16px; y: parent.height - 66px; width: parent.width - 32px; height: 48px; text: "Add Server"; clicked => { root.show_add_server(); } }
@@ -344,7 +487,7 @@ slint::slint! {
if (tab == "issues" || tab == "repositories") && page == "add": Rectangle { if (tab == "issues" || tab == "repositories") && page == "add": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { x: 0; y: 0; title: "Add Server"; can_back: true; back => { root.back(); } } Header { x: 0; y: 0; title: "Add Server"; can_back: true; back => { root.back(); } }
VerticalLayout { VerticalLayout {
@@ -368,7 +511,7 @@ slint::slint! {
if (tab == "issues" || tab == "repositories") && page == "repositories": Rectangle { if (tab == "issues" || tab == "repositories") && page == "repositories": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { x: 0; y: 0; title: root.server_title; can_back: true; back => { root.back(); } } 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.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; }
@@ -399,7 +542,7 @@ slint::slint! {
TouchArea { TouchArea {
x: parent.width - 52px; y: 2px; width: 48px; height: 48px; x: parent.width - 52px; y: 2px; width: 48px; height: 48px;
clicked => { root.toggle_favorite(repo.owner, repo.name); } clicked => { root.toggle_favorite(repo.owner, repo.name); }
Text { text: repo.favorite ? "" : ""; color: repo.favorite ? #f2a900 : #888890; font-size: 27px; horizontal-alignment: center; vertical-alignment: center; } AppIcon { x: 10px; y: 10px; width: 28px; height: 28px; kind: repo.favorite ? "favorite-filled" : "favorite"; icon-color: repo.favorite ? #f2a900 : #888890; }
} }
} }
} }
@@ -410,7 +553,7 @@ slint::slint! {
if tab == "issues" && page == "issues": Rectangle { if tab == "issues" && page == "issues": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { Header {
x: 0; y: 0; title: root.repository_title; can_back: true; can_filter: true; x: 0; y: 0; title: root.repository_title; can_back: true; can_filter: true;
@@ -459,7 +602,7 @@ slint::slint! {
if tab == "issues" && page == "issue": Rectangle { if tab == "issues" && page == "issue": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { x: 0; y: 0; title: "Issue"; can_back: true; back => { root.back(); } } 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; } if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; }
@@ -491,7 +634,7 @@ slint::slint! {
if tab == "pulls" && !root.has_active_server: Rectangle { if tab == "pulls" && !root.has_active_server: Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { x: 0; y: 0; title: "Home"; can_back: false; } Header { x: 0; y: 0; title: "Home"; can_back: false; }
EmptyState { EmptyState {
@@ -504,7 +647,7 @@ slint::slint! {
if tab == "pulls" && root.has_active_server && page == "pulls": Rectangle { if tab == "pulls" && root.has_active_server && page == "pulls": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { Header {
x: 0; y: 0; title: "Pull Requests"; can_back: false; can_filter: true; x: 0; y: 0; title: "Pull Requests"; can_back: false; can_filter: true;
@@ -539,7 +682,7 @@ slint::slint! {
if tab == "pulls" && page == "pull": Rectangle { if tab == "pulls" && page == "pull": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { x: 0; y: 0; title: "Pull Request"; can_back: true; back => { root.back(); } } 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; } if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; }
@@ -569,7 +712,7 @@ slint::slint! {
if tab == "repositories" && page == "commits": Rectangle { if tab == "repositories" && page == "commits": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { x: 0; y: 0; title: root.repository_title; can_back: true; back => { root.back(); } } Header { x: 0; y: 0; title: root.repository_title; can_back: true; back => { root.back(); } }
ComboBox { ComboBox {
@@ -646,7 +789,7 @@ slint::slint! {
if tab == "repositories" && page == "files": Rectangle { if tab == "repositories" && page == "files": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { x: 0; y: 0; title: "Changed Files"; can_back: true; back => { root.back(); } } Header { x: 0; y: 0; title: "Changed Files"; can_back: true; back => { root.back(); } }
if !root.loading && root.files.length == 0: EmptyState { if !root.loading && root.files.length == 0: EmptyState {
@@ -664,7 +807,7 @@ slint::slint! {
if (tab == "repositories" || tab == "pulls") && page == "diff": Rectangle { if (tab == "repositories" || tab == "pulls") && page == "diff": Rectangle {
x: root.safe-area-insets.left; y: root.safe-area-insets.top; 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; 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 - 58px; height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height;
background: #f2f2f7; background: #f2f2f7;
Header { x: 0; y: 0; title: root.detail_title; can_back: true; back => { root.back(); } } Header { x: 0; y: 0; title: root.detail_title; can_back: true; back => { root.back(); } }
ScrollView { ScrollView {
@@ -692,20 +835,19 @@ slint::slint! {
Rectangle { Rectangle {
x: root.safe-area-insets.left; x: root.safe-area-insets.left;
y: root.height - root.safe-area-insets.bottom - 58px; y: root.height - root.safe-area-insets.bottom - root.toolbar-height;
width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; width: root.width - root.safe-area-insets.left - root.safe-area-insets.right;
height: 58px; height: root.toolbar-height;
background: #fbfbfc; background: #fbfbfc;
Rectangle { width: 100%; height: 1px; background: #d4d4d8; }
TabButton { TabButton {
x: 0; width: parent.width / 5; height: parent.height; icon: ""; label: "Home"; active: root.tab == "home"; x: 0; width: parent.width / 5; height: parent.height; kind: "home"; label: "Home"; active: root.tab == "home";
clicked => { clicked => {
if root.tab == "home" { root.home_scroll_request += 1; } if root.tab == "home" { root.home_scroll_request += 1; }
root.select_tab("home"); root.select_tab("home");
} }
} }
TabButton { TabButton {
x: parent.width / 5; width: parent.width / 5; height: parent.height; icon: "!"; label: "Issues"; active: root.tab == "issues"; x: parent.width / 5; width: parent.width / 5; height: parent.height; kind: "issues"; label: "Issues"; active: root.tab == "issues";
clicked => { clicked => {
if root.tab == "issues" { if root.tab == "issues" {
if root.page == "servers" { root.servers_scroll_request += 1; } if root.page == "servers" { root.servers_scroll_request += 1; }
@@ -716,9 +858,9 @@ slint::slint! {
root.select_tab("issues"); root.select_tab("issues");
} }
} }
TabButton { x: parent.width * 2 / 5; width: parent.width / 5; height: parent.height; icon: ""; label: "Repos"; active: root.tab == "repositories"; clicked => { root.select_tab("repositories"); } } TabButton { x: parent.width * 2 / 5; width: parent.width / 5; height: parent.height; kind: "repositories"; label: "Repos"; active: root.tab == "repositories"; clicked => { root.select_tab("repositories"); } }
TabButton { x: parent.width * 3 / 5; width: parent.width / 5; height: parent.height; icon: ""; label: "Pulls"; active: root.tab == "pulls"; clicked => { root.select_tab("pulls"); } } TabButton { x: parent.width * 3 / 5; width: parent.width / 5; height: parent.height; kind: "pulls"; label: "Pulls"; active: root.tab == "pulls"; clicked => { root.select_tab("pulls"); } }
TabButton { x: parent.width * 4 / 5; width: parent.width / 5; height: parent.height; icon: ""; label: "Settings"; active: root.tab == "settings"; clicked => { root.select_tab("settings"); } } TabButton { x: parent.width * 4 / 5; width: parent.width / 5; height: parent.height; kind: "settings"; label: "Settings"; active: root.tab == "settings"; clicked => { root.select_tab("settings"); } }
} }
if root.loading && (root.tab == "issues" || root.tab == "repositories" || root.tab == "pulls"): Rectangle { if root.loading && (root.tab == "issues" || root.tab == "repositories" || root.tab == "pulls"): Rectangle {

View File

@@ -248,38 +248,62 @@ fn activity_row(activity: &models::Activity) -> ActivityRow {
.and_then(|name| name.strip_prefix("refs/heads/").or(Some(name))) .and_then(|name| name.strip_prefix("refs/heads/").or(Some(name)))
.unwrap_or("default branch"); .unwrap_or("default branch");
let (icon, title) = match activity.op_type { let (icon, title) = match activity.op_type {
Some(OpType::CommitRepo) if activity.content.as_deref().unwrap_or("").is_empty() => { Some(OpType::CommitRepo) if activity.content.as_deref().unwrap_or("").is_empty() => (
("", format!("Created branch {branch} in {repository}")) "branch-create",
} format!("Created branch {branch} in {repository}"),
),
Some(OpType::CommitRepo | OpType::MirrorSyncPush) => { Some(OpType::CommitRepo | OpType::MirrorSyncPush) => {
("", format!("Pushed to {branch} in {repository}")) ("push", format!("Pushed to {branch} in {repository}"))
} }
Some(OpType::CreateRepo) => ("", format!("Created {repository}")), Some(OpType::CreateRepo) => ("repo-create", format!("Created {repository}")),
Some(OpType::RenameRepo) => ("", format!("Renamed {repository}")), Some(OpType::RenameRepo) => ("repo-rename", format!("Renamed {repository}")),
Some(OpType::StarRepo) => ("", format!("Starred {repository}")), Some(OpType::StarRepo) => ("repo-star", format!("Starred {repository}")),
Some(OpType::WatchRepo) => ("", format!("Started watching {repository}")), Some(OpType::WatchRepo) => ("repo-watch", format!("Started watching {repository}")),
Some(OpType::CreateIssue) => ("!", format!("Opened an issue in {repository}")), Some(OpType::CreateIssue) => ("issue-open", format!("Opened an issue in {repository}")),
Some(OpType::CloseIssue) => ("", format!("Closed an issue in {repository}")), Some(OpType::CloseIssue) => ("issue-close", format!("Closed an issue in {repository}")),
Some(OpType::ReopenIssue) => ("", format!("Reopened an issue in {repository}")), Some(OpType::ReopenIssue) => ("issue-reopen", format!("Reopened an issue in {repository}")),
Some(OpType::CommentIssue) => ("", format!("Commented on an issue in {repository}")), Some(OpType::CommentIssue) => (
Some(OpType::CreatePullRequest) => ("", format!("Opened a pull request in {repository}")), "issue-comment",
Some(OpType::MergePullRequest | OpType::AutoMergePullRequest) => { format!("Commented on an issue in {repository}"),
("", format!("Merged a pull request in {repository}")) ),
Some(OpType::CreatePullRequest) => (
"pull-open",
format!("Opened a pull request in {repository}"),
),
Some(OpType::MergePullRequest | OpType::AutoMergePullRequest) => (
"pull-merge",
format!("Merged a pull request in {repository}"),
),
Some(OpType::ClosePullRequest) => (
"pull-close",
format!("Closed a pull request in {repository}"),
),
Some(OpType::ReopenPullRequest) => (
"pull-reopen",
format!("Reopened a pull request in {repository}"),
),
Some(OpType::CommentPull) => (
"pull-comment",
format!("Commented on a pull request in {repository}"),
),
Some(OpType::ApprovePullRequest) => (
"pull-approve",
format!("Approved a pull request in {repository}"),
),
Some(OpType::RejectPullRequest) => {
("pull-reject", format!("Requested changes in {repository}"))
} }
Some(OpType::ClosePullRequest) => ("×", format!("Closed a pull request in {repository}")), Some(OpType::PushTag) => ("tag-push", format!("Pushed tag {branch} in {repository}")),
Some(OpType::ReopenPullRequest) => { Some(OpType::DeleteTag) => (
("", format!("Reopened a pull request in {repository}")) "tag-delete",
} format!("Deleted tag {branch} in {repository}"),
Some(OpType::CommentPull) => ("", format!("Commented on a pull request in {repository}")), ),
Some(OpType::ApprovePullRequest) => { Some(OpType::DeleteBranch) => (
("", format!("Approved a pull request in {repository}")) "branch-delete",
} format!("Deleted branch {branch} in {repository}"),
Some(OpType::RejectPullRequest) => ("×", format!("Requested changes in {repository}")), ),
Some(OpType::PushTag) => ("", format!("Pushed tag {branch} in {repository}")), Some(OpType::PublishRelease) => ("release", format!("Published a release in {repository}")),
Some(OpType::DeleteTag) => ("", format!("Deleted tag {branch} in {repository}")), Some(_) | None => ("update", format!("Updated {repository}")),
Some(OpType::DeleteBranch) => ("", format!("Deleted branch {branch} in {repository}")),
Some(OpType::PublishRelease) => ("", format!("Published a release in {repository}")),
Some(_) | None => ("", format!("Updated {repository}")),
}; };
let target = activity::target(activity); let target = activity::target(activity);
let (target, owner, repository, number, sha) = match target { let (target, owner, repository, number, sha) = match target {
@@ -585,4 +609,19 @@ mod tests {
pull.state = Some("closed".into()); pull.state = Some("closed".into());
assert_eq!(pull_files_ref(&pull), "Files at merge commit 01234567"); assert_eq!(pull_files_ref(&pull), "Files at merge commit 01234567");
} }
#[test]
fn activity_rows_use_semantic_icons() {
let activity = models::Activity {
op_type: Some(models::activity::OpType::CreatePullRequest),
content: Some("42|feature".into()),
repo: Some(Box::new(models::Repository {
full_name: Some("octo/demo".into()),
..Default::default()
})),
..Default::default()
};
assert_eq!(activity_row(&activity).icon, "pull-open");
}
} }