Compare commits

..

No commits in common. "a64ed277f660420313d28601f55c3a5d5e8f4d03" and "365f77056ef9c8d52bf28fa9879ee75fd39002cb" have entirely different histories.

13 changed files with 1261 additions and 1823 deletions

2754
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,18 @@
[package] [package]
name = "aliveline" name = "aliveline"
version = "0.3.0" version = "0.2.1"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
chrono = "0.4.42" chrono = "0.4.42"
serde = "1.0.219" serde = "1.0.219"
slint = "1.15.1" slint = "1.12.1"
toml = "0.9.5" toml = "0.9.5"
[build-dependencies] [build-dependencies]
slint-build = "1.15.1" slint-build = "1.12.1"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
[features]
light = []
dark = []

View file

@ -34,22 +34,12 @@ finished = true
_Note: if event is not finished yet, it may have_ `end = start`. _Note: if event is not finished yet, it may have_ `end = start`.
## Building ## Building
Requirements:
### Requirements
- Rust toolchain - Rust toolchain
- Slint dependencies (see [Platforms](https://docs.slint.dev/latest/docs/slint/guide/platforms/desktop/) and [Backends & Renderers](https://docs.slint.dev/latest/docs/slint/guide/backends-and-renderers/backends_and_renderers/)) - Slint dependencies (see [Platforms](https://docs.slint.dev/latest/docs/slint/guide/platforms/desktop/) and [Backends & Renderers](https://docs.slint.dev/latest/docs/slint/guide/backends-and-renderers/backends_and_renderers/))
### Feature flags Instructions:
By default Aliveline compiles with theme autodetection, provided by Slint, which sometimes does not work on Linux. Just run `cargo build --release` and the resulting binary can be located at `target/release/aliveline[.exe]` if compilation succeeds.
You can use these flags to compile Aliveline with selected theme:
- `light`
- `dark`
### Instructions
Run `cargo build --release`
Pass features in build command with `-F foo` or `--feature foo`
Resulting binary will be located at `target/release/aliveline[.exe]`
If compilation fails, double check that you have all required dependencies. If it still fails, file an issue on [Codeberg](https://codeberg.org/2ndbeam/aliveline/issues), including logs and system info. If compilation fails, double check that you have all required dependencies. If it still fails, file an issue on [Codeberg](https://codeberg.org/2ndbeam/aliveline/issues), including logs and system info.
## Usage ## Usage

View file

@ -1,18 +1,3 @@
fn main() { fn main() {
slint_build::compile("ui/app-window.slint").expect("Slint build failed");
let mut style = String::from("cosmic");
#[cfg(all(feature = "dark", feature = "light"))]
compile_error!("Features \"dark\" and \"light\" are mutually exclusive");
if cfg!(feature = "dark") {
style.push_str("-dark");
} else if cfg!(feature = "light") {
style.push_str("-light");
}
let config = slint_build::CompilerConfiguration::new()
.with_style(style);
slint_build::compile_with_config("ui/app-window.slint", config).expect("Slint build failed");
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

View file

@ -8,6 +8,9 @@ use chrono::{Datelike, Timelike};
use slint::{Color, Model, ModelRc, SharedString, ToSharedString, VecModel, Weak}; use slint::{Color, Model, ModelRc, SharedString, ToSharedString, VecModel, Weak};
use toml::value::{Date as TomlDate, Time}; use toml::value::{Date as TomlDate, Time};
const DEFAULT_WINDOW_WIDTH: f32 = 800.;
const DEFAULT_WINDOW_HEIGHT: f32 = 600.;
slint::include_modules!(); slint::include_modules!();
impl From<Event> for TimelineEvent { impl From<Event> for TimelineEvent {
@ -57,9 +60,7 @@ fn load_log(ui_weak: Weak<AppWindow>, log: Arc<Mutex<Log>>) {
.collect(); .collect();
let in_progress = events.iter().any(|event| !event.finished); let in_progress = events.iter().any(|event| !event.finished);
let model: ModelRc<TimelineEvent> = Rc::new(VecModel::from(events)).into(); let model: ModelRc<TimelineEvent> = Rc::new(VecModel::from(events)).into();
let mut state = ui.get_record_state(); ui.set_record_events(model);
state.events = model;
ui.set_record_state(state);
ui.set_in_progress(in_progress); ui.set_in_progress(in_progress);
ui.invoke_get_previous_event(); ui.invoke_get_previous_event();
} }
@ -108,6 +109,7 @@ fn main() -> Result<(), Box<dyn Error>> {
load_colors(ui_weak, config_arc); load_colors(ui_weak, config_arc);
ui.invoke_update_record_offset(offset as i32); ui.invoke_update_record_offset(offset as i32);
ui.invoke_update_window_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT);
ui.on_fetch_log({ ui.on_fetch_log({
let config = config.clone(); let config = config.clone();
@ -125,9 +127,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.map(|event| TimelineEvent::from((*event).clone())) .map(|event| TimelineEvent::from((*event).clone()))
.collect(); .collect();
let model: ModelRc<TimelineEvent> = Rc::new(VecModel::from(events)).into(); let model: ModelRc<TimelineEvent> = Rc::new(VecModel::from(events)).into();
let mut state = ui.get_review_state(); ui.set_review_events(model);
state.events = model;
ui.set_review_state(state);
} }
}); });
@ -151,14 +151,10 @@ fn main() -> Result<(), Box<dyn Error>> {
.map(|h| h.parse::<i32>().unwrap()) .map(|h| h.parse::<i32>().unwrap())
.unwrap(); .unwrap();
if is_record { if is_record {
let mut state = ui.get_record_state(); ui.set_record_visible_time(hours * 3600);
state.visible_time = hours * 3600;
ui.set_record_state(state);
} else { } else {
let mut state = ui.get_review_state(); ui.set_review_visible_time(hours * 3600);
state.visible_time = hours * 3600; }
ui.set_review_state(state);
};
} }
}); });
@ -168,16 +164,17 @@ fn main() -> Result<(), Box<dyn Error>> {
move |event_name: SharedString| { move |event_name: SharedString| {
let ui = ui_weak.unwrap(); let ui = ui_weak.unwrap();
let state = ui.get_record_state(); let events_rc = ui.get_record_events();
let events = state.events.as_any() let events = events_rc.as_any()
.downcast_ref::<VecModel<TimelineEvent>>() .downcast_ref::<VecModel<TimelineEvent>>()
.unwrap(); .unwrap();
let offset = ui.get_record_offset();
let event = TimelineEvent { let event = TimelineEvent {
duration: 0, duration: 0,
finished: false, finished: false,
label: event_name.clone(), label: event_name.clone(),
start: state.offset, start: offset,
color_id: color_id_from_name(event_name.to_string()) color_id: color_id_from_name(event_name.to_string())
}; };
@ -197,10 +194,11 @@ fn main() -> Result<(), Box<dyn Error>> {
let log = writing_log.clone(); let log = writing_log.clone();
move || { move || {
let ui = ui_weak.unwrap(); let ui = ui_weak.unwrap();
let state = ui.get_record_state(); let events_rc = ui.get_record_events();
let events = state.events.as_any() let events = events_rc.as_any()
.downcast_ref::<VecModel<TimelineEvent>>() .downcast_ref::<VecModel<TimelineEvent>>()
.unwrap(); .unwrap();
let offset = ui.get_record_offset();
let event_id = events.iter() let event_id = events.iter()
.position(|data| !data.finished) .position(|data| !data.finished)
@ -208,7 +206,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let event = events.row_data(event_id) let event = events.row_data(event_id)
.expect("stop-event called without unfinished events"); .expect("stop-event called without unfinished events");
let new_event = TimelineEvent { let new_event = TimelineEvent {
duration: state.offset - event.start, duration: offset - event.start,
finished: true, finished: true,
label: event.label.clone(), label: event.label.clone(),
start: event.start, start: event.start,

View file

@ -1,107 +1,62 @@
import { TabWidget, VerticalBox, ComboBox } from "std-widgets.slint"; import { TabWidget } from "std-widgets.slint";
import { RecordWidget } from "record.slint"; import { RecordWidget } from "record.slint";
import { ReviewWidget } from "review.slint"; import { ReviewWidget } from "review.slint";
import { TimelineEvent, Timeline, TimelineState } from "timeline.slint"; import { TimelineEvent } from "timeline.slint";
import { Const } from "global.slint";
export { Palette } from "theme.slint"; export { Palette } from "theme.slint";
export component AppWindow inherits Window { export component AppWindow inherits Window {
callback update-record-offset(int);
callback save-log;
callback new-day-started();
update-record-offset(new-offset) => {
record-state.offset = new-offset;
}
preferred-width: 800px;
preferred-height: 600px;
max-width: 2147483647px;
max-height: 2147483647px;
callback start-new-event <=> record.start-new-event; callback start-new-event <=> record.start-new-event;
callback stop-event <=> record.stop-event; callback stop-event <=> record.stop-event;
callback chain-event <=> record.chain-event; callback chain-event <=> record.chain-event;
callback new-day-started <=> record.new-day-started;
callback get-previous-event <=> record.get-previous-event; callback get-previous-event <=> record.get-previous-event;
callback update-record-offset(int);
callback update-window-size(length, length);
callback save-log;
callback fetch-log <=> review.fetch-log; callback fetch-log <=> review.fetch-log;
callback update-visible-time(bool, string); callback update-visible-time(bool, string);
in-out property record-state <=> record.state; update-record-offset(new-offset) => {
record.offset = new-offset;
}
in-out property review-state <=> review.state; update-window-size(width, height) => {
self.width = width;
self.height = height;
}
in-out property record-events <=> record.events;
in-out property record-offset <=> record.offset;
in-out property record-visible-time <=> record.visible-time;
in-out property in-progress <=> record.in-progress; in-out property in-progress <=> record.in-progress;
in property previous-event-name <=> record.previous-event-name; in property previous-event-name <=> record.previous-event-name;
in-out property review-events <=> review.events;
in-out property review-offset <=> review.offset;
in-out property review-visible-time <=> review.visible-time;
property<[string]> combo-spans: ["1 Hour", "4 Hours", "8 Hours", "24 Hours"]; property<[string]> combo-spans: ["1 Hour", "4 Hours", "8 Hours", "24 Hours"];
property<bool> minimized: false;
property<bool> in-record-mode: true;
init => {
record-state.visible-time = 3600;
review-state.visible-time = 3600;
}
Timer {
interval: 1s;
running: true;
triggered => {
if (record-state.offset >= Const.max-offset) {
root.new-day-started();
record-state.offset = 0;
return;
}
record-state.offset += 1;
}
}
title: "Aliveline"; title: "Aliveline";
VerticalLayout { TabWidget {
width: 100%; Tab {
height: 100%; title: "Record";
tl := Timeline {
preferred-height: 100%;
min-height: 50px;
state: in-record-mode ? record-state : review-state;
clicked => {
minimized = !minimized;
}
}
spacing: minimized ? 0 : 8px;
record := RecordWidget { record := RecordWidget {
combo-spans: combo-spans; combo-spans: combo-spans;
update-visible-time(time) => { update-visible-time(time) => {
root.update-visible-time(true, time); root.update-visible-time(true, time);
} }
minimized: minimized || !in-record-mode;
} }
}
Tab {
title: "Review";
review := ReviewWidget { review := ReviewWidget {
combo-spans: combo-spans; combo-spans: combo-spans;
update-visible-time(time) => { update-visible-time(time) => {
root.update-visible-time(false, time) root.update-visible-time(false, time)
} }
minimized: minimized;
is-active: !in-record-mode;
}
if !minimized: HorizontalLayout {
padding-left: 8px;
padding-right: 8px;
padding-bottom: 8px;
spacing: 16px;
Text {
text: "Mode:";
font-size: 24px;
horizontal-alignment: right;
}
ComboBox {
model: ["Record", "Review"];
current-index: in-record-mode ? 0 : 1;
selected(current-value) => {
in-record-mode = current-value == "Record";
}
} }
} }
} }

View file

@ -1,21 +0,0 @@
export global TimeString {
pure function pad-mh(seconds: int, param: int) -> string {
if seconds / param < 10 {
return "0\{floor(seconds / param)}";
}
return "\{floor(seconds / param)}";
}
pure function pad-s(seconds: int) -> string {
if mod(seconds, 60) < 10 {
return "0\{mod(seconds, 60)}";
}
return "\{mod(seconds, 60)}";
}
public pure function from(seconds: int) -> string {
return "\{pad-mh(seconds, 3600)}:\{pad-mh(mod(seconds, 3600), 60)}:\{pad-s(seconds)}";
}
}
export global Const {
out property <int> max-offset: 24 * 3600 - 1;
}

View file

@ -1,25 +1,33 @@
import { VerticalBox, LineEdit, Button, ComboBox } from "std-widgets.slint"; import { VerticalBox, LineEdit, Button, ComboBox } from "std-widgets.slint";
import { Timeline, TimelineState } from "timeline.slint"; import { Timeline } from "timeline.slint";
export component RecordWidget inherits VerticalLayout { export component RecordWidget inherits VerticalBox {
callback new-day-started <=> tl.new-day-started;
callback update-visible-time(string); callback update-visible-time(string);
callback start-new-event(string); callback start-new-event(string);
callback chain-event(string); callback chain-event(string);
callback stop-event; callback stop-event;
callback get-previous-event(); callback get-previous-event();
in-out property visible-time <=> tl.visible-time;
in-out property<TimelineState> state; in-out property updating <=> tl.updating;
in-out property offset <=> tl.offset;
in-out property events <=> tl.events;
in property<[string]> combo-spans: []; in property<[string]> combo-spans: [];
in-out property<bool> in-progress: false; in-out property<bool> in-progress: false;
property<string> event-name: ""; property<string> event-name: "";
in property<string> previous-event-name: ""; in property<string> previous-event-name: "";
property<bool> minimized: false;
property<int> combo-index: 0; property<int> combo-index: 0;
in-out property <bool> minimized; tl := Timeline {
preferred-height: 100%;
updating: true;
clicked => {
minimized = !minimized;
}
}
if !minimized: GridLayout { if !minimized: GridLayout {
spacing-vertical: 8px; spacing-vertical: 8px;
spacing-horizontal: 16px; spacing-horizontal: 16px;
padding: 8px;
le := LineEdit { le := LineEdit {
placeholder-text: "Event name"; placeholder-text: "Event name";
text: event-name; text: event-name;
@ -35,7 +43,6 @@ export component RecordWidget inherits VerticalLayout {
text: in-progress ? "Stop" : "Start"; text: in-progress ? "Stop" : "Start";
row: 1; row: 1;
colspan: 2; colspan: 2;
primary: true;
clicked => { clicked => {
if in-progress { if in-progress {
root.stop-event(); root.stop-event();
@ -49,7 +56,7 @@ export component RecordWidget inherits VerticalLayout {
Button { Button {
text: "Chain"; text: "Chain";
enabled: in-progress; enabled: in-progress;
col: 2; col: 3;
row: 1; row: 1;
colspan: 2; colspan: 2;
clicked => { clicked => {
@ -60,7 +67,7 @@ export component RecordWidget inherits VerticalLayout {
Button { Button {
text: previous-event-name == "" ? "Chain previous event (None)" : "Chain previous event (\{previous-event-name})"; text: previous-event-name == "" ? "Chain previous event (None)" : "Chain previous event (\{previous-event-name})";
enabled: in-progress && previous-event-name != ""; enabled: in-progress && previous-event-name != "";
col: 4; col: 5;
row: 1; row: 1;
colspan: 2; colspan: 2;
clicked => { clicked => {
@ -70,7 +77,7 @@ export component RecordWidget inherits VerticalLayout {
} }
} }
Text { Text {
text: "Span: "; text: "Span:";
font-size: 24px; font-size: 24px;
row: 2; row: 2;
colspan: 3; colspan: 3;

View file

@ -1,62 +1,65 @@
import { VerticalBox, LineEdit, Button, DatePickerPopup, ComboBox, Slider } from "std-widgets.slint"; import { VerticalBox, LineEdit, Button, DatePickerPopup, ComboBox, Slider } from "std-widgets.slint";
import { Timeline, TimelineState } from "timeline.slint"; import { Timeline } from "timeline.slint";
import { Const } from "global.slint";
export component ReviewWidget inherits VerticalLayout { export component ReviewWidget inherits VerticalBox {
callback update-visible-time(string); callback update-visible-time(string);
callback fetch-log(int, int, int); callback fetch-log(int, int, int);
property<int> max-offset: 24 * 3600;
property<int> current-year; property<int> current-year;
property<int> current-month; property<int> current-month;
property<int> current-day; property<int> current-day;
in property<[string]> combo-spans: []; in property<[string]> combo-spans: [];
in-out property<TimelineState> state; in-out property visible-time <=> tl.visible-time;
in-out property<bool> minimized; in-out property offset <=> tl.offset;
in-out property<bool> is-active; in-out property events <=> tl.events;
if is-active: VerticalLayout { tl := Timeline {
spacing: 8px; updating: false;
Slider {
minimum: state.visible-time;
maximum: Const.max-offset;
value: state.offset;
changed(value) => {
state.offset = value;
} }
} GridLayout {
if !minimized: GridLayout { spacing-vertical: 8px;
spacing-horizontal: 16px; spacing-horizontal: 16px;
padding: 8px; Slider {
minimum: visible-time;
maximum: tl.max-offset;
value: offset;
row: 0;
colspan: 2;
changed(value) => {
offset = value;
}
}
Text { Text {
text: "Day: \{current-day}/\{current-month}/\{current-year}"; text: "Day: \{current-day}/\{current-month}/\{current-year}";
font-size: 24px; font-size: 32px;
horizontal-alignment: right; horizontal-alignment: right;
row: 1;
} }
Button { Button {
primary: true;
text: "Select"; text: "Select";
clicked => { clicked => {
date-picker.show() date-picker.show()
} }
row: 1;
col: 1; col: 1;
} }
Text { Text {
text: "Span: "; text: "Span: ";
font-size: 24px; font-size: 24px;
row: 1; row: 2;
horizontal-alignment: right; horizontal-alignment: right;
} }
ComboBox { ComboBox {
model: combo-spans; model: combo-spans;
current-index: 0; current-index: 0;
row: 1; row: 2;
col: 1; col: 1;
selected(current-value) => { selected(current-value) => {
root.update-visible-time(current-value); root.update-visible-time(current-value);
} }
} }
} }
}
date-picker := DatePickerPopup { date-picker := DatePickerPopup {
x: (root.width - self.width) / 2; x: (root.width - self.width) / 2;
y: (root.height - self.height) / 2; y: (root.height - self.height) / 2;

View file

@ -1,28 +1,55 @@
import { Palette } from "theme.slint"; import { Palette } from "theme.slint";
import { TimeString, Const } from "global.slint";
export struct TimelineEvent { export struct TimelineEvent {
start: int, start: int,
duration: int, duration: int,
finished: bool, finished: bool,
label: string, label: string,
color-id: int, color-id: int
} }
export struct TimelineState { global TimeString {
visible-time: int, pure function pad-mh(seconds: int, param: int) -> string {
offset: int, if seconds / param < 10 {
events: [TimelineEvent], return "0\{floor(seconds / param)}";
}
return "\{floor(seconds / param)}";
}
pure function pad-s(seconds: int) -> string {
if mod(seconds, 60) < 10 {
return "0\{mod(seconds, 60)}";
}
return "\{mod(seconds, 60)}";
}
public pure function from(seconds: int) -> string {
return "\{pad-mh(seconds, 3600)}:\{pad-mh(mod(seconds, 3600), 60)}:\{pad-s(seconds)}";
}
} }
export component Timeline inherits Rectangle { export component Timeline inherits Rectangle {
callback new-day-started;
callback clicked <=> ta.clicked; callback clicked <=> ta.clicked;
background: Palette.background; background: Palette.background;
in-out property<TimelineState> state; in-out property<bool> updating: true;
property<int> visible-offset: max(state.offset, state.visible-time); in-out property<[TimelineEvent]> events: [];
out property<int> max-offset: Const.max-offset; in-out property<int> visible-time: 3600;
property<int> visible-offset: max(offset, visible-time);
in-out property<int> offset: 0;
out property<int> max-offset: 24 * 3600 - 1;
timer := Timer {
interval: 1s;
running: updating;
triggered => {
if (offset >= max-offset) {
root.new-day-started();
offset = 0;
return;
}
offset += 1;
}
}
ta := TouchArea { ta := TouchArea {
preferred-width: 100%; preferred-width: 100%;
@ -45,7 +72,7 @@ export component Timeline inherits Rectangle {
Text { Text {
x: 0; x: 0;
y: parent.height - self.height; y: parent.height - self.height;
text: TimeString.from(visible-offset - state.visible-time); text: TimeString.from(visible-offset - visible-time);
color: Palette.background-text; color: Palette.background-text;
} }
@ -56,9 +83,9 @@ export component Timeline inherits Rectangle {
color: Palette.background-text; color: Palette.background-text;
} }
for event in state.events: timeline-event := Rectangle { for event in events: timeline-event := Rectangle {
property<length> real-x: ((state.visible-time - (visible-offset - event.start)) / state.visible-time) * parent.width; property<length> real-x: ((visible-time - (visible-offset - event.start)) / visible-time) * parent.width;
property<length> real-width: event.duration / state.visible-time * parent.width + min(real-x, 0); property<length> real-width: event.duration / visible-time * parent.width + min(real-x, 0);
x: max(real-x, 0); x: max(real-x, 0);
y: parent.height / 4; y: parent.height / 4;
z: 1; z: 1;
@ -83,7 +110,7 @@ export component Timeline inherits Rectangle {
y: root.height - self.height - timeline-event.height; y: root.height - self.height - timeline-event.height;
text: timeline-event.x == timeline-event.real-x ? text: timeline-event.x == timeline-event.real-x ?
TimeString.from(event.start) : TimeString.from(event.start) :
TimeString.from(visible-offset - state.visible-time); TimeString.from(visible-offset - visible-time);
visible: timeline-event.visible && visible: timeline-event.visible &&
(self.width * 2 < timeline-event.width || (self.width * 2 < timeline-event.width ||
(!end-txt.visible && self.width < timeline-event.width)); (!end-txt.visible && self.width < timeline-event.width));