Compare commits
No commits in common. "a64ed277f660420313d28601f55c3a5d5e8f4d03" and "365f77056ef9c8d52bf28fa9879ee75fd39002cb" have entirely different histories.
a64ed277f6
...
365f77056e
13 changed files with 1261 additions and 1823 deletions
2754
Cargo.lock
generated
2754
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
|
@ -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 = []
|
|
||||||
|
|
|
||||||
16
README.md
16
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
17
build.rs
17
build.rs
|
|
@ -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 |
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 25 KiB |
36
src/main.rs
36
src/main.rs
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,61 @@
|
||||||
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 {
|
record := RecordWidget {
|
||||||
preferred-height: 100%;
|
combo-spans: combo-spans;
|
||||||
min-height: 50px;
|
update-visible-time(time) => {
|
||||||
state: in-record-mode ? record-state : review-state;
|
root.update-visible-time(true, time);
|
||||||
clicked => {
|
}
|
||||||
minimized = !minimized;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
spacing: minimized ? 0 : 8px;
|
Tab {
|
||||||
record := RecordWidget {
|
title: "Review";
|
||||||
combo-spans: combo-spans;
|
review := ReviewWidget {
|
||||||
update-visible-time(time) => {
|
combo-spans: combo-spans;
|
||||||
root.update-visible-time(true, time);
|
update-visible-time(time) => {
|
||||||
}
|
root.update-visible-time(false, time)
|
||||||
minimized: minimized || !in-record-mode;
|
|
||||||
}
|
|
||||||
review := ReviewWidget {
|
|
||||||
combo-spans: combo-spans;
|
|
||||||
update-visible-time(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";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,62 @@
|
||||||
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;
|
||||||
|
}
|
||||||
|
GridLayout {
|
||||||
|
spacing-vertical: 8px;
|
||||||
|
spacing-horizontal: 16px;
|
||||||
Slider {
|
Slider {
|
||||||
minimum: state.visible-time;
|
minimum: visible-time;
|
||||||
maximum: Const.max-offset;
|
maximum: tl.max-offset;
|
||||||
value: state.offset;
|
value: offset;
|
||||||
|
row: 0;
|
||||||
|
colspan: 2;
|
||||||
changed(value) => {
|
changed(value) => {
|
||||||
state.offset = value;
|
offset = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !minimized: GridLayout {
|
Text {
|
||||||
spacing-horizontal: 16px;
|
text: "Day: \{current-day}/\{current-month}/\{current-year}";
|
||||||
padding: 8px;
|
font-size: 32px;
|
||||||
Text {
|
horizontal-alignment: right;
|
||||||
text: "Day: \{current-day}/\{current-month}/\{current-year}";
|
row: 1;
|
||||||
font-size: 24px;
|
}
|
||||||
horizontal-alignment: right;
|
Button {
|
||||||
|
text: "Select";
|
||||||
|
clicked => {
|
||||||
|
date-picker.show()
|
||||||
}
|
}
|
||||||
Button {
|
row: 1;
|
||||||
primary: true;
|
col: 1;
|
||||||
text: "Select";
|
}
|
||||||
clicked => {
|
Text {
|
||||||
date-picker.show()
|
text: "Span: ";
|
||||||
}
|
font-size: 24px;
|
||||||
col: 1;
|
row: 2;
|
||||||
}
|
horizontal-alignment: right;
|
||||||
Text {
|
}
|
||||||
text: "Span: ";
|
ComboBox {
|
||||||
font-size: 24px;
|
model: combo-spans;
|
||||||
row: 1;
|
current-index: 0;
|
||||||
horizontal-alignment: right;
|
row: 2;
|
||||||
}
|
col: 1;
|
||||||
ComboBox {
|
selected(current-value) => {
|
||||||
model: combo-spans;
|
root.update-visible-time(current-value);
|
||||||
current-index: 0;
|
|
||||||
row: 1;
|
|
||||||
col: 1;
|
|
||||||
selected(current-value) => {
|
|
||||||
root.update-visible-time(current-value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue