a lightweight, interval-based utility to combat digital strain through "Ma" (intentional pauses) for the eyes and body.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(ui): extract components and tab views to separate files

+985 -1066
+27
ui/components/atoms.slint
··· 1 + import { Theme } from "../theme.slint"; 2 + 3 + export component PaperDivider inherits Rectangle { 4 + height: 1px; 5 + background: Theme.line; 6 + } 7 + 8 + export component SettingLabel inherits VerticalLayout { 9 + in property <string> title; 10 + in property <string> description: ""; 11 + 12 + alignment: center; 13 + spacing: 3px; 14 + 15 + Text { 16 + text: root.title; 17 + font-size: 13px; 18 + font-weight: 500; 19 + color: Theme.ink; 20 + } 21 + 22 + if root.description != "": Text { 23 + text: root.description; 24 + font-size: 11.5px; 25 + color: Theme.ink-mid; 26 + } 27 + }
+89
ui/components/chips.slint
··· 1 + import { Theme } from "../theme.slint"; 2 + 3 + // Index-based chip group — for theme mode selection etc. 4 + export component ChipGroup inherits HorizontalLayout { 5 + in property <[string]> labels; 6 + in-out property <int> selected-index: 0; 7 + callback selection-changed(int); 8 + 9 + spacing: 6px; 10 + alignment: center; 11 + 12 + for label[i] in root.labels: chip-ta := TouchArea { 13 + height: 32px; 14 + min-width: 48px; 15 + clicked => { 16 + root.selected-index = i; 17 + root.selection-changed(i); 18 + } 19 + 20 + Rectangle { 21 + border-radius: 6px; 22 + background: i == root.selected-index 23 + ? Theme.btn-bg 24 + : (chip-ta.has-hover ? Theme.surface-hov : Theme.surface); 25 + border-width: 1px; 26 + border-color: i == root.selected-index ? transparent : Theme.line; 27 + animate background { duration: 120ms; } 28 + 29 + HorizontalLayout { 30 + padding-left: 12px; 31 + padding-right: 12px; 32 + alignment: center; 33 + 34 + Text { 35 + text: label; 36 + font-size: 12px; 37 + font-weight: i == root.selected-index ? 600 : 400; 38 + color: i == root.selected-index ? Theme.btn-fg : Theme.ink; 39 + vertical-alignment: center; 40 + animate color { duration: 120ms; } 41 + } 42 + } 43 + } 44 + } 45 + } 46 + 47 + // String-based chip group — for named profile selection. 48 + export component ProfileChips inherits HorizontalLayout { 49 + in property <[string]> profile-names; 50 + in-out property <string> active-profile: ""; 51 + callback selection-changed(string); 52 + 53 + spacing: 6px; 54 + alignment: center; 55 + 56 + for name in root.profile-names: chip-ta := TouchArea { 57 + height: 32px; 58 + min-width: 48px; 59 + clicked => { 60 + root.active-profile = name; 61 + root.selection-changed(name); 62 + } 63 + 64 + Rectangle { 65 + border-radius: 6px; 66 + background: name == root.active-profile 67 + ? Theme.btn-bg 68 + : (chip-ta.has-hover ? Theme.surface-hov : Theme.surface); 69 + border-width: 1px; 70 + border-color: name == root.active-profile ? transparent : Theme.line; 71 + animate background { duration: 120ms; } 72 + 73 + HorizontalLayout { 74 + padding-left: 12px; 75 + padding-right: 12px; 76 + alignment: center; 77 + 78 + Text { 79 + text: name; 80 + font-size: 12px; 81 + font-weight: name == root.active-profile ? 600 : 400; 82 + color: name == root.active-profile ? Theme.btn-fg : Theme.ink; 83 + vertical-alignment: center; 84 + animate color { duration: 120ms; } 85 + } 86 + } 87 + } 88 + } 89 + }
+38
ui/components/footer.slint
··· 1 + import { Theme } from "../theme.slint"; 2 + import { PaperPrimaryButton, PaperSecondaryButton } from "buttons.slint"; 3 + 4 + export component DialogFooter inherits VerticalLayout { 5 + spacing: 0px; 6 + 7 + callback cancel-clicked; 8 + callback save-clicked; 9 + 10 + Rectangle { 11 + height: 1px; 12 + background: Theme.line; 13 + } 14 + 15 + HorizontalLayout { 16 + padding-left: 24px; 17 + padding-right: 24px; 18 + padding-top: 12px; 19 + padding-bottom: 14px; 20 + spacing: 0px; 21 + 22 + Rectangle { horizontal-stretch: 1; } 23 + 24 + HorizontalLayout { 25 + spacing: 8px; 26 + 27 + PaperSecondaryButton { 28 + text: "Cancel"; 29 + clicked => { root.cancel-clicked(); } 30 + } 31 + 32 + PaperPrimaryButton { 33 + text: "Save"; 34 + clicked => { root.save-clicked(); } 35 + } 36 + } 37 + } 38 + }
+53
ui/components/header.slint
··· 1 + import { Theme } from "../theme.slint"; 2 + 3 + export component AppHeader inherits HorizontalLayout { 4 + padding-left: 28px; 5 + padding-right: 28px; 6 + padding-top: 18px; 7 + padding-bottom: 0px; 8 + spacing: 0px; 9 + 10 + HorizontalLayout { 11 + spacing: 10px; 12 + alignment: center; 13 + 14 + Text { 15 + text: "ioma"; 16 + font-family: "Shippori Mincho"; 17 + font-size: 22px; 18 + color: Theme.ink; 19 + vertical-alignment: center; 20 + } 21 + 22 + Text { 23 + text: "間"; 24 + font-family: "Shippori Mincho"; 25 + font-size: 11px; 26 + color: Theme.ink-lo; 27 + vertical-alignment: bottom; 28 + } 29 + } 30 + 31 + Rectangle { horizontal-stretch: 1; } 32 + 33 + // Decorative window chrome — actual close handled by WM 34 + HorizontalLayout { 35 + spacing: 6px; 36 + alignment: center; 37 + 38 + for glyph in ["−", "□", "×"]: Rectangle { 39 + width: 22px; 40 + height: 22px; 41 + border-radius: 11px; 42 + background: Theme.chrome; 43 + 44 + Text { 45 + text: glyph; 46 + font-size: 11px; 47 + color: Theme.ink-lo; 48 + horizontal-alignment: center; 49 + vertical-alignment: center; 50 + } 51 + } 52 + } 53 + }
+55
ui/components/input_fields.slint ui/components/inputs.slint
··· 150 150 } 151 151 } 152 152 } 153 + 154 + export component VolumeSlider { 155 + in-out property <float> value: 0.5; 156 + private property <length> fill-w: root.width * root.value; 157 + 158 + width: 200px; 159 + height: 16px; 160 + animate fill-w { duration: 80ms; } 161 + 162 + Rectangle { 163 + y: (parent.height - 2px) / 2; 164 + height: 2px; 165 + background: Theme.toggle-off; 166 + border-radius: 1px; 167 + } 168 + 169 + Rectangle { 170 + y: (parent.height - 2px) / 2; 171 + width: root.fill-w; 172 + height: 2px; 173 + background: Theme.accent; 174 + border-radius: 1px; 175 + } 176 + 177 + Rectangle { 178 + x: root.fill-w - 6px; 179 + y: (parent.height - 12px) / 2; 180 + width: 12px; 181 + height: 12px; 182 + border-radius: 6px; 183 + background: white; 184 + border-width: 1.5px; 185 + border-color: Theme.accent; 186 + drop-shadow-blur: 2px; 187 + drop-shadow-color: #00000020; 188 + } 189 + 190 + TouchArea { 191 + accessible-role: slider; 192 + accessible-label: "Volume"; 193 + accessible-value: "\{Math.round(root.value * 100)}%"; 194 + accessible-value-minimum: 0; 195 + accessible-value-maximum: 100; 196 + moved => { 197 + if self.pressed { 198 + root.value = max(0.0, min(1.0, self.mouse-x / parent.width)); 199 + } 200 + } 201 + pointer-event(e) => { 202 + if e.kind == PointerEventKind.down { 203 + root.value = max(0.0, min(1.0, self.mouse-x / parent.width)); 204 + } 205 + } 206 + } 207 + }
+161
ui/components/interval_card.slint
··· 1 + import { Theme } from "../theme.slint"; 2 + import { NumberField, PaperInput } from "inputs.slint"; 3 + 4 + export struct LevelEntry { 5 + work-mins: int, 6 + break-mins: int, 7 + break-extra-secs: int, 8 + label: string, 9 + } 10 + 11 + export component IntervalCard { 12 + in property <int> index; 13 + in property <int> work-mins; 14 + in property <int> break-mins; 15 + in property <int> break-secs; 16 + in property <string> label; 17 + 18 + callback remove-clicked; 19 + callback work-mins-changed(int); 20 + callback break-mins-changed(int); 21 + callback break-secs-changed(int); 22 + callback label-changed(string); 23 + 24 + Rectangle { 25 + border-radius: 8px; 26 + background: Theme.surface; 27 + border-width: 1px; 28 + border-color: Theme.line; 29 + 30 + HorizontalLayout { 31 + padding-left: 18px; 32 + padding-right: 14px; 33 + padding-top: 12px; 34 + padding-bottom: 12px; 35 + spacing: 18px; 36 + 37 + // Index column — aligns numerically with the label input row 38 + VerticalLayout { 39 + spacing: 8px; 40 + width: 20px; 41 + 42 + Text { 43 + text: root.index; 44 + font-family: "Shippori Mincho"; 45 + font-size: 28px; 46 + color: Theme.ink-hi; 47 + height: 34px; 48 + horizontal-alignment: center; 49 + vertical-alignment: bottom; 50 + } 51 + 52 + Rectangle { height: 28px; } 53 + } 54 + 55 + VerticalLayout { 56 + spacing: 8px; 57 + 58 + HorizontalLayout { 59 + spacing: 8px; 60 + 61 + PaperInput { 62 + width: 200px; 63 + text: root.label; 64 + placeholder-text: "break name"; 65 + field-label: "break label"; 66 + edited(v) => { root.label-changed(v); } 67 + } 68 + 69 + Rectangle { horizontal-stretch: 1; } 70 + } 71 + 72 + HorizontalLayout { 73 + spacing: 8px; 74 + alignment: start; 75 + 76 + Rectangle { width: 4px; } 77 + 78 + Text { 79 + text: "every"; 80 + font-size: 12px; 81 + color: Theme.ink-mid; 82 + vertical-alignment: center; 83 + } 84 + 85 + NumberField { 86 + width: 80px; 87 + value: root.work-mins; 88 + minimum: 1; 89 + maximum: 240; 90 + field-label: "work interval minutes"; 91 + edited(v) => { root.work-mins-changed(v); } 92 + } 93 + 94 + Text { 95 + text: "min · pause"; 96 + font-size: 12px; 97 + color: Theme.ink-mid; 98 + vertical-alignment: center; 99 + } 100 + 101 + NumberField { 102 + width: 72px; 103 + value: root.break-mins; 104 + minimum: 0; 105 + maximum: 120; 106 + field-label: "break minutes"; 107 + edited(v) => { root.break-mins-changed(v); } 108 + } 109 + 110 + Text { 111 + text: "min"; 112 + font-size: 12px; 113 + color: Theme.ink-mid; 114 + vertical-alignment: center; 115 + } 116 + 117 + NumberField { 118 + width: 72px; 119 + value: root.break-secs; 120 + minimum: 0; 121 + maximum: 59; 122 + field-label: "break seconds"; 123 + edited(v) => { root.break-secs-changed(v); } 124 + } 125 + 126 + Text { 127 + text: "sec"; 128 + font-size: 12px; 129 + color: Theme.ink-mid; 130 + vertical-alignment: center; 131 + } 132 + } 133 + } 134 + 135 + VerticalLayout { 136 + alignment: LayoutAlignment.stretch; 137 + 138 + ta-rm := TouchArea { 139 + width: 44px; 140 + height: 44px; 141 + accessible-role: button; 142 + accessible-item-selectable: false; 143 + accessible-label: "Remove interval"; 144 + clicked => { root.remove-clicked(); } 145 + 146 + Text { 147 + stroke-style: TextStrokeStyle.center; 148 + text: "×"; 149 + font-size: 14px; 150 + color: ta-rm.has-hover ? Theme.ink : Theme.ink-sub; 151 + horizontal-alignment: TextHorizontalAlignment.left; 152 + vertical-alignment: TextVerticalAlignment.top; 153 + x: 30px; 154 + y: 0px; 155 + animate color { duration: 120ms; } 156 + } 157 + } 158 + } 159 + } 160 + } 161 + }
-7
ui/components/widgets.slint ui/components/toggles.slint
··· 1 1 import { Theme } from "../theme.slint"; 2 2 3 - export component PaperDivider { 4 - height: 1px; 5 - Rectangle { 6 - background: Theme.line; 7 - } 8 - } 9 - 10 3 export component PaperToggle { 11 4 in-out property <bool> checked: false; 12 5 callback toggled(bool);
+43 -1059
ui/settings.slint
··· 1 - import { SpinBox, Slider, LineEdit, ScrollView } from "std-widgets.slint"; 2 1 import { Theme } from "theme.slint"; 3 - import { PaperDivider, PaperToggle } from "components/widgets.slint"; 4 - import { NumberField, PaperInput } from "components/input_fields.slint"; 5 - import { 6 - PaperButton, 7 - PaperPrimaryButton, 8 - PaperSecondaryButton, 9 - TabButton, 10 - } from "components/buttons.slint"; 2 + import { AppHeader } from "components/header.slint"; 3 + import { DialogFooter } from "components/footer.slint"; 4 + import { TabButton } from "components/buttons.slint"; 5 + import { LevelEntry } from "components/interval_card.slint"; 6 + import { RhythmTab } from "views/rhythm_tab.slint"; 7 + import { ProfileTab } from "views/profile_tab.slint"; 8 + import { AboutTab } from "views/about_tab.slint"; 11 9 import "../assets/fonts/Nunito-Regular.ttf"; 12 10 import "../assets/fonts/Nunito-Medium.ttf"; 13 11 import "../assets/fonts/Nunito-SemiBold.ttf"; ··· 15 13 import "../assets/fonts/ShipporiMincho-Regular.ttf"; 16 14 import "../assets/fonts/ShipporiMincho-Bold.ttf"; 17 15 18 - // ── Interval card (Profile tab) ────────────────────────────────────────────── 19 - component IntervalCard { 20 - in property <int> index; 21 - in property <int> work-mins; 22 - in property <int> break-mins; 23 - in property <int> break-secs; 24 - in property <string> label; 25 - 26 - callback remove-clicked; 27 - callback work-mins-changed(int); 28 - callback break-mins-changed(int); 29 - callback break-secs-changed(int); 30 - callback label-changed(string); 31 - 32 - Rectangle { 33 - border-radius: 8px; 34 - background: Theme.surface; 35 - border-width: 1px; 36 - border-color: Theme.line; 37 - 38 - HorizontalLayout { 39 - padding-left: 18px; 40 - padding-right: 14px; 41 - padding-top: 12px; 42 - padding-bottom: 12px; 43 - spacing: 18px; 44 - 45 - // Left column — mirrors right column row heights so index 46 - // stays flush with the PaperInput in row 1 47 - VerticalLayout { 48 - spacing: 8px; 49 - width: 20px; 50 - 51 - Text { 52 - text: root.index; 53 - font-family: "Shippori Mincho"; 54 - font-size: 28px; 55 - color: Theme.ink-hi; 56 - height: 34px; 57 - horizontal-alignment: center; 58 - vertical-alignment: bottom; 59 - } 60 - 61 - Rectangle { 62 - height: 28px; 63 - } 64 - } 65 - 66 - // Right column — label + interval 67 - VerticalLayout { 68 - spacing: 8px; 69 - 70 - // Row 1: label input + cancel 71 - HorizontalLayout { 72 - spacing: 8px; 73 - 74 - PaperInput { 75 - width: 200px; 76 - text: root.label; 77 - placeholder-text: "break name"; 78 - field-label: "break label"; 79 - edited(v) => { 80 - root.label-changed(v); 81 - } 82 - } 83 - 84 - Rectangle { 85 - horizontal-stretch: 1; 86 - } 87 - } 88 - 89 - // Row 2: timing, indented to align with PaperInput's inner text 90 - HorizontalLayout { 91 - spacing: 8px; 92 - alignment: start; 93 - 94 - Rectangle { 95 - width: 4px; 96 - } 97 - 98 - Text { 99 - text: "every"; 100 - font-size: 12px; 101 - color: Theme.ink-mid; 102 - vertical-alignment: center; 103 - } 104 - 105 - NumberField { 106 - width: 80px; 107 - value: root.work-mins; 108 - minimum: 1; 109 - maximum: 240; 110 - field-label: "work interval minutes"; 111 - edited(v) => { 112 - root.work-mins-changed(v); 113 - } 114 - } 115 - 116 - Text { 117 - text: "min · pause"; 118 - font-size: 12px; 119 - color: Theme.ink-mid; 120 - vertical-alignment: center; 121 - } 122 - 123 - NumberField { 124 - width: 72px; 125 - value: root.break-mins; 126 - minimum: 0; 127 - maximum: 120; 128 - field-label: "break minutes"; 129 - edited(v) => { 130 - root.break-mins-changed(v); 131 - } 132 - } 133 - 134 - Text { 135 - text: "min"; 136 - font-size: 12px; 137 - color: Theme.ink-mid; 138 - vertical-alignment: center; 139 - } 140 - 141 - NumberField { 142 - width: 72px; 143 - value: root.break-secs; 144 - minimum: 0; 145 - maximum: 59; 146 - field-label: "break seconds"; 147 - edited(v) => { 148 - root.break-secs-changed(v); 149 - } 150 - } 151 - 152 - Text { 153 - text: "sec"; 154 - font-size: 12px; 155 - color: Theme.ink-mid; 156 - vertical-alignment: center; 157 - } 158 - } 159 - } 160 - 161 - VerticalLayout { 162 - alignment: LayoutAlignment.stretch; 163 - ta-rm := TouchArea { 164 - width: 44px; 165 - height: 44px; 166 - accessible-role: button; 167 - accessible-item-selectable: false; 168 - accessible-label: "Remove interval"; 169 - clicked => { 170 - root.remove-clicked(); 171 - } 172 - 173 - Text { 174 - stroke-style: TextStrokeStyle.center; 175 - text: "×"; 176 - font-size: 14px; 177 - color: ta-rm.has-hover ? Theme.ink : Theme.ink-sub; 178 - horizontal-alignment: TextHorizontalAlignment.left; 179 - vertical-alignment: TextVerticalAlignment.top; 180 - visible: true; 181 - x: 30px; 182 - y: 0px; 183 - animate color { duration: 120ms; } 184 - } 185 - } 186 - } 187 - } 188 - } 189 - } 190 - 191 - struct LevelEntry { 192 - work-mins: int, 193 - break-mins: int, 194 - break-extra-secs: int, 195 - label: string, 196 - } 197 - 198 - // ═════════════════════════════════════════════════════════════════════════════ 199 - // Main window 200 - // ═════════════════════════════════════════════════════════════════════════════ 201 16 export component SettingsWindow inherits Window { 202 17 title: "ioma — Settings"; 203 18 preferred-width: 760px; ··· 205 20 background: Theme.bg; 206 21 default-font-family: "Nunito"; 207 22 208 - // Follow system color scheme. Set from Rust before showing the window. 209 23 in property <bool> is-dark: false; 210 - init => { 211 - Theme.dark = root.is-dark; 212 - } 213 - changed is-dark => { 214 - Theme.dark = root.is-dark; 215 - } 24 + init => { Theme.dark = root.is-dark; } 25 + changed is-dark => { Theme.dark = root.is-dark; } 216 26 217 - // ── Properties (same contract as before) ───────────────────────────────── 218 27 in-out property <bool> enforced-mode: false; 219 28 in-out property <bool> sound-enabled: true; 220 29 in-out property <float> sound-volume: 0.7; ··· 223 32 in-out property <int> idle-threshold-mins: 5; 224 33 in-out property <string> active-profile: "20·20·20"; 225 34 in-out property <[string]> profile-names: ["20·20·20", "Pomodoro", "52/17", "Focus+Micro"]; 226 - 227 35 in-out property <[LevelEntry]> levels: []; 228 36 in-out property <bool> long-break-enabled: false; 229 37 in-out property <int> long-break-after-cycles: 3; 230 38 in-out property <int> long-break-duration-mins: 30; 231 39 in-out property <int> long-break-gap-mins: 30; 232 40 in-out property <string> long-break-label: "Long rest"; 233 - 234 - // 0 = system, 1 = light, 2 = dark 235 41 in-out property <int> theme-mode: 0; 236 42 237 - // ── Callbacks (same contract as before) ────────────────────────────────── 238 43 callback save-clicked(); 239 44 callback cancel-clicked(); 240 45 callback theme-mode-changed(int); ··· 245 50 callback level-removed(int); 246 51 callback level-added(); 247 52 248 - // ── Internal state ─────────────────────────────────────────────────────── 249 53 private property <string> active-tab: "rhythm"; 250 54 251 55 VerticalLayout { 252 - // ── Header ────────────────────────────────────────────────────────── 253 - HorizontalLayout { 254 - padding-left: 28px; 255 - padding-right: 28px; 256 - padding-top: 18px; 257 - padding-bottom: 0px; 258 - spacing: 0px; 56 + AppHeader { } 259 57 260 - HorizontalLayout { 261 - spacing: 10px; 262 - alignment: center; 263 - 264 - Text { 265 - text: "ioma"; 266 - font-family: "Shippori Mincho"; 267 - font-size: 22px; 268 - color: Theme.ink; 269 - vertical-alignment: center; 270 - } 271 - 272 - Text { 273 - text: "間"; 274 - font-family: "Shippori Mincho"; 275 - font-size: 11px; 276 - color: Theme.ink-lo; 277 - vertical-alignment: bottom; 278 - } 279 - } 280 - 281 - // Spacer 282 - Rectangle { 283 - horizontal-stretch: 1; 284 - } 285 - 286 - // Window controls (decorative — actual close handled by WM) 287 - HorizontalLayout { 288 - spacing: 6px; 289 - alignment: center; 290 - 291 - for glyph in ["−", "□", "×"]: Rectangle { 292 - width: 22px; 293 - height: 22px; 294 - border-radius: 11px; 295 - background: Theme.chrome; 296 - 297 - Text { 298 - text: glyph; 299 - font-size: 11px; 300 - color: Theme.ink-lo; 301 - horizontal-alignment: center; 302 - vertical-alignment: center; 303 - } 304 - } 305 - } 306 - } 307 - 308 - // ── Tab bar ────────────────────────────────────────────────────────── 309 58 HorizontalLayout { 310 59 padding-left: 28px; 311 60 padding-right: 28px; ··· 315 64 TabButton { 316 65 text: "Rhythm"; 317 66 active: root.active-tab == "rhythm"; 318 - clicked => { 319 - root.active-tab = "rhythm"; 320 - } 67 + clicked => { root.active-tab = "rhythm"; } 321 68 } 322 69 323 70 TabButton { 324 71 text: "Profile"; 325 72 active: root.active-tab == "profile"; 326 - clicked => { 327 - root.active-tab = "profile"; 328 - } 73 + clicked => { root.active-tab = "profile"; } 329 74 } 330 75 331 76 TabButton { 332 77 text: "About"; 333 78 active: root.active-tab == "about"; 334 - clicked => { 335 - root.active-tab = "about"; 336 - } 337 - } 338 - 339 - Rectangle { 340 - horizontal-stretch: 1; 341 - } 342 - } 343 - 344 - // Tab underline 345 - Rectangle { 346 - height: 1px; 347 - background: Theme.line; 348 - } 349 - 350 - // ── Tab content ────────────────────────────────────────────────────── 351 - if root.active-tab == "rhythm": VerticalLayout { 352 - padding-left: 28px; 353 - padding-right: 28px; 354 - padding-top: 20px; 355 - padding-bottom: 16px; 356 - spacing: 0px; 357 - vertical-stretch: 1; 358 - 359 - // Active profile 360 - HorizontalLayout { 361 - padding-top: 10px; 362 - padding-bottom: 14px; 363 - spacing: 24px; 364 - 365 - VerticalLayout { 366 - alignment: center; 367 - spacing: 3px; 368 - 369 - Text { 370 - text: "Active profile"; 371 - font-size: 13px; 372 - font-weight: 500; 373 - color: Theme.ink; 374 - } 375 - 376 - Text { 377 - text: "The cadence ioma follows today."; 378 - font-size: 11.5px; 379 - color: Theme.ink-mid; 380 - } 381 - } 382 - 383 - Rectangle { 384 - horizontal-stretch: 1; 385 - } 386 - 387 - // Profile chips — click to select 388 - HorizontalLayout { 389 - spacing: 6px; 390 - alignment: center; 391 - 392 - for name in root.profile-names: profile-chip-ta := TouchArea { 393 - height: 32px; 394 - min-width: 48px; 395 - clicked => { 396 - root.active-profile = name; 397 - root.profile-changed(name); 398 - } 399 - 400 - Rectangle { 401 - border-radius: 6px; 402 - background: name == root.active-profile ? Theme.btn-bg : (profile-chip-ta.has-hover ? Theme.surface-hov : Theme.surface); 403 - border-width: 1px; 404 - border-color: name == root.active-profile ? transparent : Theme.line; 405 - animate background { duration: 120ms; } 406 - 407 - HorizontalLayout { 408 - padding-left: 12px; 409 - padding-right: 12px; 410 - alignment: center; 411 - 412 - Text { 413 - text: name; 414 - font-size: 12px; 415 - font-weight: name == root.active-profile ? 600 : 400; 416 - color: name == root.active-profile ? Theme.btn-fg : Theme.ink; 417 - vertical-alignment: center; 418 - animate color { duration: 120ms; } 419 - } 420 - } 421 - } 422 - } 423 - } 424 - } 425 - 426 - PaperDivider { } 427 - 428 - // Enforced mode 429 - HorizontalLayout { 430 - padding-top: 12px; 431 - padding-bottom: 4px; 432 - spacing: 24px; 433 - 434 - VerticalLayout { 435 - alignment: center; 436 - spacing: 3px; 437 - Text { 438 - text: "Enforced mode"; 439 - font-size: 13px; 440 - font-weight: 500; 441 - color: Theme.ink; 442 - } 443 - 444 - Text { 445 - text: "Full-screen break. Emergency unlock requires a password."; 446 - font-size: 11.5px; 447 - color: Theme.ink-mid; 448 - } 449 - } 450 - 451 - Rectangle { 452 - horizontal-stretch: 1; 453 - } 454 - 455 - PaperToggle { 456 - checked <=> root.enforced-mode; 457 - } 458 - } 459 - 460 - HorizontalLayout { 461 - padding-bottom: 12px; 462 - PaperButton { 463 - text: "Set emergency unlock password…"; 464 - preferred-width: 220px; 465 - clicked => { 466 - root.set-password-clicked(); 467 - } 468 - } 469 - } 470 - 471 - PaperDivider { } 472 - 473 - // Pause on idle 474 - HorizontalLayout { 475 - padding-top: 12px; 476 - padding-bottom: 4px; 477 - spacing: 24px; 478 - 479 - VerticalLayout { 480 - alignment: center; 481 - spacing: 3px; 482 - Text { 483 - text: "Pause on idle"; 484 - font-size: 13px; 485 - font-weight: 500; 486 - color: Theme.ink; 487 - } 488 - 489 - Text { 490 - text: "If you've stepped away, ioma waits."; 491 - font-size: 11.5px; 492 - color: Theme.ink-mid; 493 - } 494 - } 495 - 496 - Rectangle { 497 - horizontal-stretch: 1; 498 - } 499 - 500 - PaperToggle { 501 - checked <=> root.idle-detection-enabled; 502 - } 503 - } 504 - 505 - HorizontalLayout { 506 - padding-bottom: 12px; 507 - spacing: 10px; 508 - alignment: start; 509 - 510 - Text { 511 - text: "threshold"; 512 - font-size: 12px; 513 - color: Theme.ink-mid; 514 - vertical-alignment: center; 515 - } 516 - 517 - SpinBox { 518 - width: 80px; 519 - height: 26px; 520 - value <=> root.idle-threshold-mins; 521 - minimum: 1; 522 - maximum: 60; 523 - accessible-label: "Idle threshold minutes"; 524 - } 525 - 526 - Text { 527 - text: "minutes"; 528 - font-size: 12px; 529 - color: Theme.ink-mid; 530 - vertical-alignment: center; 531 - } 532 - } 533 - 534 - PaperDivider { } 535 - 536 - // Chime on break start 537 - HorizontalLayout { 538 - padding-top: 12px; 539 - padding-bottom: 4px; 540 - spacing: 24px; 541 - 542 - VerticalLayout { 543 - alignment: center; 544 - spacing: 3px; 545 - Text { 546 - text: "Chime on break start"; 547 - font-size: 13px; 548 - font-weight: 500; 549 - color: Theme.ink; 550 - } 551 - 552 - Text { 553 - text: "A single, gentle tone."; 554 - font-size: 11.5px; 555 - color: Theme.ink-mid; 556 - } 557 - } 558 - 559 - Rectangle { 560 - horizontal-stretch: 1; 561 - } 562 - 563 - PaperToggle { 564 - checked <=> root.sound-enabled; 565 - } 566 - } 567 - 568 - HorizontalLayout { 569 - padding-bottom: 12px; 570 - spacing: 12px; 571 - alignment: start; 572 - 573 - Text { 574 - text: "volume"; 575 - font-size: 12px; 576 - color: Theme.ink-mid; 577 - vertical-alignment: center; 578 - } 579 - 580 - // Custom slim slider track 581 - slider-area := Rectangle { 582 - width: 200px; 583 - height: 16px; 584 - 585 - // Track 586 - Rectangle { 587 - y: (parent.height - 2px) / 2; 588 - height: 2px; 589 - background: Theme.toggle-off; 590 - border-radius: 1px; 591 - } 592 - 593 - // Fill 594 - Rectangle { 595 - y: (parent.height - 2px) / 2; 596 - width: parent.width * root.sound-volume; 597 - height: 2px; 598 - background: Theme.accent; 599 - border-radius: 1px; 600 - animate width { duration: 80ms; } 601 - } 602 - 603 - // Thumb 604 - Rectangle { 605 - x: parent.width * root.sound-volume - 6px; 606 - y: (parent.height - 12px) / 2; 607 - width: 12px; 608 - height: 12px; 609 - border-radius: 6px; 610 - background: white; 611 - border-width: 1.5px; 612 - border-color: Theme.accent; 613 - drop-shadow-blur: 2px; 614 - drop-shadow-color: #00000020; 615 - 616 - animate x { duration: 80ms; } 617 - } 618 - 619 - // Interaction 620 - TouchArea { 621 - accessible-role: slider; 622 - accessible-label: "Volume"; 623 - accessible-value: "\{Math.round(root.sound-volume * 100)}%"; 624 - accessible-value-minimum: 0; 625 - accessible-value-maximum: 100; 626 - moved => { 627 - if self.pressed { 628 - root.sound-volume = max(0.0, min(1.0, self.mouse-x / parent.width)); 629 - } 630 - } 631 - pointer-event(e) => { 632 - if e.kind == PointerEventKind.down { 633 - root.sound-volume = max(0.0, min(1.0, self.mouse-x / parent.width)); 634 - } 635 - } 636 - } 637 - } 638 - } 639 - 640 - PaperDivider { } 641 - 642 - // Launch with system 643 - HorizontalLayout { 644 - padding-top: 12px; 645 - padding-bottom: 10px; 646 - spacing: 24px; 647 - 648 - VerticalLayout { 649 - alignment: center; 650 - spacing: 3px; 651 - Text { 652 - text: "Launch with system"; 653 - font-size: 13px; 654 - font-weight: 500; 655 - color: Theme.ink; 656 - } 657 - 658 - Text { 659 - text: "ioma starts quietly at login."; 660 - font-size: 11.5px; 661 - color: Theme.ink-mid; 662 - } 663 - } 664 - 665 - Rectangle { 666 - horizontal-stretch: 1; 667 - } 668 - 669 - PaperToggle { 670 - checked <=> root.autostart; 671 - } 672 - } 673 - 674 - HorizontalLayout { 675 - padding-bottom: 4px; 676 - PaperButton { 677 - text: "Open config file location"; 678 - preferred-width: 180px; 679 - clicked => { 680 - root.open-config-dir(); 681 - } 682 - } 683 - } 684 - 685 - PaperDivider { } 686 - 687 - // Appearance 688 - HorizontalLayout { 689 - padding-top: 12px; 690 - padding-bottom: 10px; 691 - spacing: 24px; 692 - 693 - VerticalLayout { 694 - alignment: center; 695 - spacing: 3px; 696 - Text { 697 - text: "Appearance"; 698 - font-size: 13px; 699 - font-weight: 500; 700 - color: Theme.ink; 701 - } 702 - 703 - Text { 704 - text: "Follows system by default."; 705 - font-size: 11.5px; 706 - color: Theme.ink-mid; 707 - } 708 - } 709 - 710 - Rectangle { 711 - horizontal-stretch: 1; 712 - } 713 - 714 - HorizontalLayout { 715 - spacing: 6px; 716 - alignment: center; 717 - 718 - for label[i] in ["System", "Light", "Dark"]: theme-chip-ta := TouchArea { 719 - height: 32px; 720 - min-width: 48px; 721 - clicked => { 722 - root.theme-mode = i; 723 - root.theme-mode-changed(i); 724 - } 725 - 726 - Rectangle { 727 - border-radius: 6px; 728 - background: i == root.theme-mode ? Theme.btn-bg : (theme-chip-ta.has-hover ? Theme.surface-hov : Theme.surface); 729 - border-width: 1px; 730 - border-color: i == root.theme-mode ? transparent : Theme.line; 731 - animate background { duration: 120ms; } 732 - 733 - HorizontalLayout { 734 - padding-left: 12px; 735 - padding-right: 12px; 736 - alignment: center; 737 - 738 - Text { 739 - text: label; 740 - font-size: 12px; 741 - font-weight: i == root.theme-mode ? 600 : 400; 742 - color: i == root.theme-mode ? Theme.btn-fg : Theme.ink; 743 - vertical-alignment: center; 744 - animate color { duration: 120ms; } 745 - } 746 - } 747 - } 748 - } 749 - } 79 + clicked => { root.active-tab = "about"; } 750 80 } 751 81 752 - // Spacer 753 - Rectangle { 754 - vertical-stretch: 1; 755 - } 82 + Rectangle { horizontal-stretch: 1; } 756 83 } 757 84 758 - if root.active-tab == "profile": ScrollView { 759 - vertical-stretch: 1; 760 - 761 - VerticalLayout { 762 - padding-left: 28px; 763 - padding-right: 28px; 764 - padding-top: 24px; 765 - padding-bottom: 16px; 766 - spacing: 0px; 767 - 768 - Text { 769 - text: "Break cadence"; 770 - font-size: 13px; 771 - font-weight: 500; 772 - color: Theme.ink; 773 - } 774 - 775 - Text { 776 - text: "Each row is an interval. ioma cycles through them in order."; 777 - font-size: 11.5px; 778 - color: Theme.ink-mid; 779 - } 780 - 781 - Rectangle { 782 - height: 14px; 783 - } 784 - 785 - // Interval rows 786 - for level[i] in root.levels: VerticalLayout { 787 - spacing: 0px; 788 - 789 - IntervalCard { 790 - index: i + 1; 791 - work-mins: level.work-mins; 792 - break-mins: level.break-mins; 793 - break-secs: level.break-extra-secs; 794 - label: level.label; 795 - remove-clicked => { 796 - root.level-removed(i); 797 - } 798 - work-mins-changed(v) => { 799 - root.level-changed(i, { 800 - work-mins: v, 801 - break-mins: level.break-mins, 802 - break-extra-secs: level.break-extra-secs, 803 - label: level.label 804 - }); 805 - } 806 - break-mins-changed(v) => { 807 - root.level-changed(i, { 808 - work-mins: level.work-mins, 809 - break-mins: v, 810 - break-extra-secs: level.break-extra-secs, 811 - label: level.label 812 - }); 813 - } 814 - break-secs-changed(v) => { 815 - root.level-changed(i, { 816 - work-mins: level.work-mins, 817 - break-mins: level.break-mins, 818 - break-extra-secs: v, 819 - label: level.label 820 - }); 821 - } 822 - label-changed(v) => { 823 - root.level-changed(i, { 824 - work-mins: level.work-mins, 825 - break-mins: level.break-mins, 826 - break-extra-secs: level.break-extra-secs, 827 - label: v 828 - }); 829 - } 830 - } 85 + Rectangle { height: 1px; background: Theme.line; } 831 86 832 - Rectangle { 833 - height: 10px; 834 - } 835 - } 836 - 837 - // Add interval 838 - add-ta := TouchArea { 839 - height: 42px; 840 - clicked => { 841 - root.level-added(); 842 - } 843 - 844 - Rectangle { 845 - border-radius: 8px; 846 - background: add-ta.has-hover ? Theme.surface-hov : transparent; 847 - border-width: 1px; 848 - border-color: Theme.line-med; 849 - 850 - Text { 851 - text: "+ Add interval"; 852 - font-size: 12px; 853 - color: add-ta.has-hover ? Theme.ink : Theme.ink-lo; 854 - horizontal-alignment: center; 855 - vertical-alignment: center; 856 - animate color { duration: 120ms; } 857 - } 858 - } 859 - } 860 - 861 - Rectangle { 862 - height: 20px; 863 - } 864 - 865 - Rectangle { 866 - height: 1px; 867 - background: Theme.line; 868 - } 869 - 870 - Rectangle { 871 - height: 20px; 872 - } 873 - 874 - // Long rest section 875 - HorizontalLayout { 876 - spacing: 24px; 877 - 878 - VerticalLayout { 879 - alignment: center; 880 - spacing: 3px; 881 - 882 - Text { 883 - text: "Long rest"; 884 - font-size: 13px; 885 - font-weight: 500; 886 - color: Theme.ink; 887 - } 888 - 889 - Text { 890 - text: "A longer pause after several cycles."; 891 - font-size: 11.5px; 892 - color: Theme.ink-mid; 893 - } 894 - } 895 - 896 - Rectangle { 897 - horizontal-stretch: 1; 898 - } 899 - 900 - PaperToggle { 901 - checked <=> root.long-break-enabled; 902 - } 903 - } 904 - 905 - Rectangle { 906 - height: 12px; 907 - } 908 - 909 - VerticalLayout { 910 - spacing: 0px; 911 - opacity: root.long-break-enabled ? 1.0 : 0.38; 912 - animate opacity { duration: 200ms; } 913 - 914 - HorizontalLayout { 915 - spacing: 8px; 916 - alignment: start; 917 - 918 - Text { 919 - text: "after"; 920 - font-size: 12.5px; 921 - color: Theme.ink-mid; 922 - vertical-alignment: center; 923 - } 924 - 925 - NumberField { 926 - width: 80px; 927 - value <=> root.long-break-after-cycles; 928 - minimum: 2; 929 - maximum: 20; 930 - field-label: "cycles before long rest"; 931 - } 932 - 933 - Text { 934 - text: "cycles, take"; 935 - font-size: 12.5px; 936 - color: Theme.ink-mid; 937 - vertical-alignment: center; 938 - } 939 - 940 - NumberField { 941 - width: 80px; 942 - value <=> root.long-break-duration-mins; 943 - minimum: 1; 944 - maximum: 120; 945 - field-label: "long rest duration minutes"; 946 - } 947 - 948 - Text { 949 - text: "min"; 950 - font-size: 12.5px; 951 - color: Theme.ink-mid; 952 - vertical-alignment: center; 953 - } 954 - 955 - Text { 956 - text: "·"; 957 - font-size: 12.5px; 958 - color: Theme.ink-lo; 959 - vertical-alignment: center; 960 - } 961 - 962 - PaperInput { 963 - width: 110px; 964 - text <=> root.long-break-label; 965 - placeholder-text: "Long rest"; 966 - field-label: "long rest label"; 967 - } 968 - } 969 - 970 - Rectangle { 971 - height: 10px; 972 - } 973 - 974 - HorizontalLayout { 975 - spacing: 8px; 976 - alignment: start; 977 - 978 - Text { 979 - text: "Reset after"; 980 - font-size: 11.5px; 981 - color: Theme.ink-mid; 982 - vertical-alignment: center; 983 - } 984 - 985 - NumberField { 986 - width: 80px; 987 - value <=> root.long-break-gap-mins; 988 - minimum: 1; 989 - maximum: 120; 990 - field-label: "idle reset threshold minutes"; 991 - } 992 - 993 - Text { 994 - text: "min away."; 995 - font-size: 11.5px; 996 - color: Theme.ink-mid; 997 - vertical-alignment: center; 998 - } 999 - } 1000 - } 1001 - } 87 + if root.active-tab == "rhythm": RhythmTab { 88 + enforced-mode <=> root.enforced-mode; 89 + sound-enabled <=> root.sound-enabled; 90 + sound-volume <=> root.sound-volume; 91 + autostart <=> root.autostart; 92 + idle-detection-enabled <=> root.idle-detection-enabled; 93 + idle-threshold-mins <=> root.idle-threshold-mins; 94 + active-profile <=> root.active-profile; 95 + profile-names: root.profile-names; 96 + theme-mode <=> root.theme-mode; 97 + set-password-clicked => { root.set-password-clicked(); } 98 + open-config-dir => { root.open-config-dir(); } 99 + profile-changed(name) => { root.profile-changed(name); } 100 + theme-mode-changed(i) => { root.theme-mode-changed(i); } 1002 101 } 1003 102 1004 - if root.active-tab == "about": VerticalLayout { 1005 - alignment: center; 1006 - padding: 40px; 1007 - spacing: 0px; 1008 - vertical-stretch: 1; 1009 - 1010 - // Logo (larger, centered) 1011 - HorizontalLayout { 1012 - alignment: center; 1013 - Rectangle { 1014 - width: 56px; 1015 - height: 56px; 1016 - 1017 - // Left dot 1018 - Rectangle { 1019 - x: (9px - 5px) * 56 / 64; 1020 - y: (32px - 5px) * 56 / 64; 1021 - width: 10px * 56 / 64; 1022 - height: 10px * 56 / 64; 1023 - border-radius: 5px * 56 / 64; 1024 - background: Theme.ink; 1025 - } 1026 - 1027 - Rectangle { 1028 - x: (32px - 8px) * 56 / 64; 1029 - y: (32px - 8px) * 56 / 64; 1030 - width: 16px * 56 / 64; 1031 - height: 16px * 56 / 64; 1032 - border-radius: 8px * 56 / 64; 1033 - background: Theme.ink; 1034 - } 1035 - 1036 - Rectangle { 1037 - x: (55px - 5px) * 56 / 64; 1038 - y: (32px - 5px) * 56 / 64; 1039 - width: 10px * 56 / 64; 1040 - height: 10px * 56 / 64; 1041 - border-radius: 5px * 56 / 64; 1042 - background: Theme.ink; 1043 - } 1044 - 1045 - Rectangle { 1046 - x: 17px * 56 / 64; 1047 - y: (32px - 1px) * 56 / 64; 1048 - width: 6px * 56 / 64; 1049 - height: 2px * 56 / 64; 1050 - border-radius: 1px; 1051 - background: Theme.ink; 1052 - } 1053 - 1054 - Rectangle { 1055 - x: 41px * 56 / 64; 1056 - y: (32px - 1px) * 56 / 64; 1057 - width: 6px * 56 / 64; 1058 - height: 2px * 56 / 64; 1059 - border-radius: 1px; 1060 - background: Theme.ink; 1061 - } 1062 - } 1063 - } 1064 - 1065 - Rectangle { 1066 - height: 12px; 1067 - } 1068 - 1069 - Text { 1070 - text: "ioma"; 1071 - font-family: "Shippori Mincho"; 1072 - font-size: 34px; 1073 - color: Theme.ink; 1074 - horizontal-alignment: center; 1075 - } 1076 - 1077 - Rectangle { 1078 - height: 4px; 1079 - } 1080 - 1081 - Text { 1082 - text: "v0.4.2 · the space between"; 1083 - font-size: 12px; 1084 - color: Theme.ink-mid; 1085 - horizontal-alignment: center; 1086 - } 1087 - 1088 - Rectangle { 1089 - height: 28px; 1090 - } 1091 - 1092 - Text { 1093 - text: "間 (ma) is the Japanese notion of the pause between —\nthe rest in music, the silence between words.\nioma gives that pause back to your eyes and body."; 1094 - font-size: 12.5px; 1095 - color: Theme.ink-mid; 1096 - horizontal-alignment: center; 1097 - vertical-alignment: center; 1098 - wrap: word-wrap; 1099 - } 103 + if root.active-tab == "profile": ProfileTab { 104 + levels <=> root.levels; 105 + long-break-enabled <=> root.long-break-enabled; 106 + long-break-after-cycles <=> root.long-break-after-cycles; 107 + long-break-duration-mins <=> root.long-break-duration-mins; 108 + long-break-gap-mins <=> root.long-break-gap-mins; 109 + long-break-label <=> root.long-break-label; 110 + level-changed(i, entry) => { root.level-changed(i, entry); } 111 + level-removed(i) => { root.level-removed(i); } 112 + level-added => { root.level-added(); } 1100 113 } 1101 114 1102 - // ── Footer ─────────────────────────────────────────────────────────── 1103 - Rectangle { 1104 - height: 1px; 1105 - background: Theme.line; 1106 - } 1107 - 1108 - HorizontalLayout { 1109 - padding-left: 24px; 1110 - padding-right: 24px; 1111 - padding-top: 12px; 1112 - padding-bottom: 14px; 1113 - spacing: 0px; 1114 - 1115 - Rectangle { 1116 - horizontal-stretch: 1; 1117 - } 1118 - 1119 - HorizontalLayout { 1120 - spacing: 8px; 1121 - 1122 - PaperSecondaryButton { 1123 - text: "Cancel"; 1124 - clicked => { 1125 - root.cancel-clicked(); 1126 - } 1127 - } 115 + if root.active-tab == "about": AboutTab { } 1128 116 1129 - PaperPrimaryButton { 1130 - text: "Save"; 1131 - clicked => { 1132 - root.save-clicked(); 1133 - } 1134 - } 1135 - } 117 + DialogFooter { 118 + cancel-clicked => { root.cancel-clicked(); } 119 + save-clicked => { root.save-clicked(); } 1136 120 } 1137 121 } 1138 122 }
+92
ui/views/about_tab.slint
··· 1 + import { Theme } from "../theme.slint"; 2 + 3 + export component AboutTab inherits VerticalLayout { 4 + alignment: center; 5 + padding: 40px; 6 + spacing: 0px; 7 + vertical-stretch: 1; 8 + 9 + HorizontalLayout { 10 + alignment: center; 11 + 12 + Rectangle { 13 + width: 56px; 14 + height: 56px; 15 + 16 + Rectangle { 17 + x: (9px - 5px) * 56 / 64; 18 + y: (32px - 5px) * 56 / 64; 19 + width: 10px * 56 / 64; 20 + height: 10px * 56 / 64; 21 + border-radius: 5px * 56 / 64; 22 + background: Theme.ink; 23 + } 24 + 25 + Rectangle { 26 + x: (32px - 8px) * 56 / 64; 27 + y: (32px - 8px) * 56 / 64; 28 + width: 16px * 56 / 64; 29 + height: 16px * 56 / 64; 30 + border-radius: 8px * 56 / 64; 31 + background: Theme.ink; 32 + } 33 + 34 + Rectangle { 35 + x: (55px - 5px) * 56 / 64; 36 + y: (32px - 5px) * 56 / 64; 37 + width: 10px * 56 / 64; 38 + height: 10px * 56 / 64; 39 + border-radius: 5px * 56 / 64; 40 + background: Theme.ink; 41 + } 42 + 43 + Rectangle { 44 + x: 17px * 56 / 64; 45 + y: (32px - 1px) * 56 / 64; 46 + width: 6px * 56 / 64; 47 + height: 2px * 56 / 64; 48 + border-radius: 1px; 49 + background: Theme.ink; 50 + } 51 + 52 + Rectangle { 53 + x: 41px * 56 / 64; 54 + y: (32px - 1px) * 56 / 64; 55 + width: 6px * 56 / 64; 56 + height: 2px * 56 / 64; 57 + border-radius: 1px; 58 + background: Theme.ink; 59 + } 60 + } 61 + } 62 + 63 + Rectangle { height: 12px; } 64 + 65 + Text { 66 + text: "ioma"; 67 + font-family: "Shippori Mincho"; 68 + font-size: 34px; 69 + color: Theme.ink; 70 + horizontal-alignment: center; 71 + } 72 + 73 + Rectangle { height: 4px; } 74 + 75 + Text { 76 + text: "v0.4.2 · the space between"; 77 + font-size: 12px; 78 + color: Theme.ink-mid; 79 + horizontal-alignment: center; 80 + } 81 + 82 + Rectangle { height: 28px; } 83 + 84 + Text { 85 + text: "間 (ma) is the Japanese notion of the pause between —\nthe rest in music, the silence between words.\nioma gives that pause back to your eyes and body."; 86 + font-size: 12.5px; 87 + color: Theme.ink-mid; 88 + horizontal-alignment: center; 89 + vertical-alignment: center; 90 + wrap: word-wrap; 91 + } 92 + }
+224
ui/views/profile_tab.slint
··· 1 + import { ScrollView } from "std-widgets.slint"; 2 + import { Theme } from "../theme.slint"; 3 + import { PaperDivider, SettingLabel } from "../components/atoms.slint"; 4 + import { PaperToggle } from "../components/toggles.slint"; 5 + import { NumberField, PaperInput } from "../components/inputs.slint"; 6 + import { IntervalCard, LevelEntry } from "../components/interval_card.slint"; 7 + 8 + export { LevelEntry } 9 + 10 + export component ProfileTab inherits ScrollView { 11 + in-out property <[LevelEntry]> levels; 12 + in-out property <bool> long-break-enabled: false; 13 + in-out property <int> long-break-after-cycles: 3; 14 + in-out property <int> long-break-duration-mins: 30; 15 + in-out property <int> long-break-gap-mins: 30; 16 + in-out property <string> long-break-label: "Long rest"; 17 + 18 + callback level-changed(int, LevelEntry); 19 + callback level-removed(int); 20 + callback level-added; 21 + 22 + vertical-stretch: 1; 23 + 24 + VerticalLayout { 25 + padding-left: 28px; 26 + padding-right: 28px; 27 + padding-top: 24px; 28 + padding-bottom: 16px; 29 + spacing: 0px; 30 + 31 + Text { 32 + text: "Break cadence"; 33 + font-size: 13px; 34 + font-weight: 500; 35 + color: Theme.ink; 36 + } 37 + 38 + Text { 39 + text: "Each row is an interval. ioma cycles through them in order."; 40 + font-size: 11.5px; 41 + color: Theme.ink-mid; 42 + } 43 + 44 + Rectangle { height: 14px; } 45 + 46 + for level[i] in root.levels: VerticalLayout { 47 + spacing: 0px; 48 + 49 + IntervalCard { 50 + index: i + 1; 51 + work-mins: level.work-mins; 52 + break-mins: level.break-mins; 53 + break-secs: level.break-extra-secs; 54 + label: level.label; 55 + remove-clicked => { root.level-removed(i); } 56 + work-mins-changed(v) => { 57 + root.level-changed(i, { 58 + work-mins: v, 59 + break-mins: level.break-mins, 60 + break-extra-secs: level.break-extra-secs, 61 + label: level.label 62 + }); 63 + } 64 + break-mins-changed(v) => { 65 + root.level-changed(i, { 66 + work-mins: level.work-mins, 67 + break-mins: v, 68 + break-extra-secs: level.break-extra-secs, 69 + label: level.label 70 + }); 71 + } 72 + break-secs-changed(v) => { 73 + root.level-changed(i, { 74 + work-mins: level.work-mins, 75 + break-mins: level.break-mins, 76 + break-extra-secs: v, 77 + label: level.label 78 + }); 79 + } 80 + label-changed(v) => { 81 + root.level-changed(i, { 82 + work-mins: level.work-mins, 83 + break-mins: level.break-mins, 84 + break-extra-secs: level.break-extra-secs, 85 + label: v 86 + }); 87 + } 88 + } 89 + 90 + Rectangle { height: 10px; } 91 + } 92 + 93 + add-ta := TouchArea { 94 + height: 42px; 95 + clicked => { root.level-added(); } 96 + 97 + Rectangle { 98 + border-radius: 8px; 99 + background: add-ta.has-hover ? Theme.surface-hov : transparent; 100 + border-width: 1px; 101 + border-color: Theme.line-med; 102 + 103 + Text { 104 + text: "+ Add interval"; 105 + font-size: 12px; 106 + color: add-ta.has-hover ? Theme.ink : Theme.ink-lo; 107 + horizontal-alignment: center; 108 + vertical-alignment: center; 109 + animate color { duration: 120ms; } 110 + } 111 + } 112 + } 113 + 114 + Rectangle { height: 20px; } 115 + PaperDivider { } 116 + Rectangle { height: 20px; } 117 + 118 + HorizontalLayout { 119 + spacing: 24px; 120 + 121 + SettingLabel { 122 + title: "Long rest"; 123 + description: "A longer pause after several cycles."; 124 + } 125 + 126 + Rectangle { horizontal-stretch: 1; } 127 + 128 + PaperToggle { checked <=> root.long-break-enabled; } 129 + } 130 + 131 + Rectangle { height: 12px; } 132 + 133 + VerticalLayout { 134 + spacing: 0px; 135 + opacity: root.long-break-enabled ? 1.0 : 0.38; 136 + animate opacity { duration: 200ms; } 137 + 138 + HorizontalLayout { 139 + spacing: 8px; 140 + alignment: start; 141 + 142 + Text { 143 + text: "after"; 144 + font-size: 12.5px; 145 + color: Theme.ink-mid; 146 + vertical-alignment: center; 147 + } 148 + 149 + NumberField { 150 + width: 80px; 151 + value <=> root.long-break-after-cycles; 152 + minimum: 2; 153 + maximum: 20; 154 + field-label: "cycles before long rest"; 155 + } 156 + 157 + Text { 158 + text: "cycles, take"; 159 + font-size: 12.5px; 160 + color: Theme.ink-mid; 161 + vertical-alignment: center; 162 + } 163 + 164 + NumberField { 165 + width: 80px; 166 + value <=> root.long-break-duration-mins; 167 + minimum: 1; 168 + maximum: 120; 169 + field-label: "long rest duration minutes"; 170 + } 171 + 172 + Text { 173 + text: "min"; 174 + font-size: 12.5px; 175 + color: Theme.ink-mid; 176 + vertical-alignment: center; 177 + } 178 + 179 + Text { 180 + text: "·"; 181 + font-size: 12.5px; 182 + color: Theme.ink-lo; 183 + vertical-alignment: center; 184 + } 185 + 186 + PaperInput { 187 + width: 110px; 188 + text <=> root.long-break-label; 189 + placeholder-text: "Long rest"; 190 + field-label: "long rest label"; 191 + } 192 + } 193 + 194 + Rectangle { height: 10px; } 195 + 196 + HorizontalLayout { 197 + spacing: 8px; 198 + alignment: start; 199 + 200 + Text { 201 + text: "Reset after"; 202 + font-size: 11.5px; 203 + color: Theme.ink-mid; 204 + vertical-alignment: center; 205 + } 206 + 207 + NumberField { 208 + width: 80px; 209 + value <=> root.long-break-gap-mins; 210 + minimum: 1; 211 + maximum: 120; 212 + field-label: "idle reset threshold minutes"; 213 + } 214 + 215 + Text { 216 + text: "min away."; 217 + font-size: 11.5px; 218 + color: Theme.ink-mid; 219 + vertical-alignment: center; 220 + } 221 + } 222 + } 223 + } 224 + }
+203
ui/views/rhythm_tab.slint
··· 1 + import { Theme } from "../theme.slint"; 2 + import { SettingLabel, PaperDivider } from "../components/atoms.slint"; 3 + import { PaperButton } from "../components/buttons.slint"; 4 + import { ChipGroup, ProfileChips } from "../components/chips.slint"; 5 + import { NumberField, VolumeSlider } from "../components/inputs.slint"; 6 + import { PaperToggle } from "../components/toggles.slint"; 7 + 8 + export component RhythmTab inherits VerticalLayout { 9 + in-out property <bool> enforced-mode: false; 10 + in-out property <bool> sound-enabled: true; 11 + in-out property <float> sound-volume: 0.7; 12 + in-out property <bool> autostart: false; 13 + in-out property <bool> idle-detection-enabled: true; 14 + in-out property <int> idle-threshold-mins: 5; 15 + in-out property <string> active-profile: ""; 16 + in property <[string]> profile-names; 17 + in-out property <int> theme-mode: 0; 18 + 19 + callback set-password-clicked; 20 + callback open-config-dir; 21 + callback profile-changed(string); 22 + callback theme-mode-changed(int); 23 + 24 + padding-left: 28px; 25 + padding-right: 28px; 26 + padding-top: 20px; 27 + padding-bottom: 16px; 28 + spacing: 0px; 29 + vertical-stretch: 1; 30 + 31 + HorizontalLayout { 32 + padding-top: 10px; 33 + padding-bottom: 14px; 34 + spacing: 24px; 35 + 36 + SettingLabel { 37 + title: "Active profile"; 38 + description: "The cadence ioma follows today."; 39 + } 40 + 41 + Rectangle { horizontal-stretch: 1; } 42 + 43 + ProfileChips { 44 + profile-names: root.profile-names; 45 + active-profile <=> root.active-profile; 46 + selection-changed(name) => { root.profile-changed(name); } 47 + } 48 + } 49 + 50 + PaperDivider { } 51 + 52 + HorizontalLayout { 53 + padding-top: 12px; 54 + padding-bottom: 4px; 55 + spacing: 24px; 56 + 57 + SettingLabel { 58 + title: "Enforced mode"; 59 + description: "Full-screen break. Emergency unlock requires a password."; 60 + } 61 + 62 + Rectangle { horizontal-stretch: 1; } 63 + 64 + PaperToggle { checked <=> root.enforced-mode; } 65 + } 66 + 67 + HorizontalLayout { 68 + padding-bottom: 12px; 69 + 70 + PaperButton { 71 + text: "Set emergency unlock password…"; 72 + preferred-width: 220px; 73 + clicked => { root.set-password-clicked(); } 74 + } 75 + } 76 + 77 + PaperDivider { } 78 + 79 + HorizontalLayout { 80 + padding-top: 12px; 81 + padding-bottom: 4px; 82 + spacing: 24px; 83 + 84 + SettingLabel { 85 + title: "Pause on idle"; 86 + description: "If you've stepped away, ioma waits."; 87 + } 88 + 89 + Rectangle { horizontal-stretch: 1; } 90 + 91 + PaperToggle { checked <=> root.idle-detection-enabled; } 92 + } 93 + 94 + HorizontalLayout { 95 + padding-bottom: 12px; 96 + spacing: 10px; 97 + alignment: start; 98 + 99 + Text { 100 + text: "threshold"; 101 + font-size: 12px; 102 + color: Theme.ink-mid; 103 + vertical-alignment: center; 104 + } 105 + 106 + NumberField { 107 + width: 80px; 108 + value <=> root.idle-threshold-mins; 109 + minimum: 1; 110 + maximum: 60; 111 + field-label: "Idle threshold minutes"; 112 + } 113 + 114 + Text { 115 + text: "minutes"; 116 + font-size: 12px; 117 + color: Theme.ink-mid; 118 + vertical-alignment: center; 119 + } 120 + } 121 + 122 + PaperDivider { } 123 + 124 + HorizontalLayout { 125 + padding-top: 12px; 126 + padding-bottom: 4px; 127 + spacing: 24px; 128 + 129 + SettingLabel { 130 + title: "Chime on break start"; 131 + description: "A single, gentle tone."; 132 + } 133 + 134 + Rectangle { horizontal-stretch: 1; } 135 + 136 + PaperToggle { checked <=> root.sound-enabled; } 137 + } 138 + 139 + HorizontalLayout { 140 + padding-bottom: 12px; 141 + spacing: 12px; 142 + alignment: start; 143 + 144 + Text { 145 + text: "volume"; 146 + font-size: 12px; 147 + color: Theme.ink-mid; 148 + vertical-alignment: center; 149 + } 150 + 151 + VolumeSlider { value <=> root.sound-volume; } 152 + } 153 + 154 + PaperDivider { } 155 + 156 + HorizontalLayout { 157 + padding-top: 12px; 158 + padding-bottom: 10px; 159 + spacing: 24px; 160 + 161 + SettingLabel { 162 + title: "Launch with system"; 163 + description: "ioma starts quietly at login."; 164 + } 165 + 166 + Rectangle { horizontal-stretch: 1; } 167 + 168 + PaperToggle { checked <=> root.autostart; } 169 + } 170 + 171 + HorizontalLayout { 172 + padding-bottom: 4px; 173 + 174 + PaperButton { 175 + text: "Open config file location"; 176 + preferred-width: 180px; 177 + clicked => { root.open-config-dir(); } 178 + } 179 + } 180 + 181 + PaperDivider { } 182 + 183 + HorizontalLayout { 184 + padding-top: 12px; 185 + padding-bottom: 10px; 186 + spacing: 24px; 187 + 188 + SettingLabel { 189 + title: "Appearance"; 190 + description: "Follows system by default."; 191 + } 192 + 193 + Rectangle { horizontal-stretch: 1; } 194 + 195 + ChipGroup { 196 + labels: ["System", "Light", "Dark"]; 197 + selected-index <=> root.theme-mode; 198 + selection-changed(i) => { root.theme-mode-changed(i); } 199 + } 200 + } 201 + 202 + Rectangle { vertical-stretch: 1; } 203 + }