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]
name = "aliveline"
version = "0.3.0"
version = "0.2.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = "0.4.42"
serde = "1.0.219"
slint = "1.15.1"
slint = "1.12.1"
toml = "0.9.5"
[build-dependencies]
slint-build = "1.15.1"
slint-build = "1.12.1"
[profile.release]
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`.
## Building
### Requirements
Requirements:
- 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/))
### Feature flags
By default Aliveline compiles with theme autodetection, provided by Slint, which sometimes does not work on Linux.
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]`
Instructions:
Just run `cargo build --release` and the resulting binary can be located at `target/release/aliveline[.exe]` if compilation succeeds.
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

View file

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

View file

@ -1,106 +1,61 @@
import { TabWidget, VerticalBox, ComboBox } from "std-widgets.slint";
import { TabWidget } from "std-widgets.slint";
import { RecordWidget } from "record.slint";
import { ReviewWidget } from "review.slint";
import { TimelineEvent, Timeline, TimelineState } from "timeline.slint";
import { Const } from "global.slint";
import { TimelineEvent } from "timeline.slint";
export { Palette } from "theme.slint";
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 stop-event <=> record.stop-event;
callback chain-event <=> record.chain-event;
callback new-day-started <=> record.new-day-started;
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 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 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<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";
VerticalLayout {
width: 100%;
height: 100%;
tl := Timeline {
preferred-height: 100%;
min-height: 50px;
state: in-record-mode ? record-state : review-state;
clicked => {
minimized = !minimized;
TabWidget {
Tab {
title: "Record";
record := RecordWidget {
combo-spans: combo-spans;
update-visible-time(time) => {
root.update-visible-time(true, time);
}
}
}
spacing: minimized ? 0 : 8px;
record := RecordWidget {
combo-spans: combo-spans;
update-visible-time(time) => {
root.update-visible-time(true, 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";
Tab {
title: "Review";
review := ReviewWidget {
combo-spans: combo-spans;
update-visible-time(time) => {
root.update-visible-time(false, time)
}
}
}

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

View file

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

View file

@ -1,28 +1,55 @@
import { Palette } from "theme.slint";
import { TimeString, Const } from "global.slint";
export struct TimelineEvent {
start: int,
duration: int,
finished: bool,
label: string,
color-id: int,
color-id: int
}
export struct TimelineState {
visible-time: int,
offset: int,
events: [TimelineEvent],
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 component Timeline inherits Rectangle {
callback new-day-started;
callback clicked <=> ta.clicked;
background: Palette.background;
in-out property<TimelineState> state;
property<int> visible-offset: max(state.offset, state.visible-time);
out property<int> max-offset: Const.max-offset;
in-out property<bool> updating: true;
in-out property<[TimelineEvent]> events: [];
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 {
preferred-width: 100%;
@ -45,7 +72,7 @@ export component Timeline inherits Rectangle {
Text {
x: 0;
y: parent.height - self.height;
text: TimeString.from(visible-offset - state.visible-time);
text: TimeString.from(visible-offset - visible-time);
color: Palette.background-text;
}
@ -56,9 +83,9 @@ export component Timeline inherits Rectangle {
color: Palette.background-text;
}
for event in state.events: timeline-event := Rectangle {
property<length> real-x: ((state.visible-time - (visible-offset - event.start)) / state.visible-time) * parent.width;
property<length> real-width: event.duration / state.visible-time * parent.width + min(real-x, 0);
for event in events: timeline-event := Rectangle {
property<length> real-x: ((visible-time - (visible-offset - event.start)) / visible-time) * parent.width;
property<length> real-width: event.duration / visible-time * parent.width + min(real-x, 0);
x: max(real-x, 0);
y: parent.height / 4;
z: 1;
@ -83,7 +110,7 @@ export component Timeline inherits Rectangle {
y: root.height - self.height - timeline-event.height;
text: timeline-event.x == timeline-event.real-x ?
TimeString.from(event.start) :
TimeString.from(visible-offset - state.visible-time);
TimeString.from(visible-offset - visible-time);
visible: timeline-event.visible &&
(self.width * 2 < timeline-event.width ||
(!end-txt.visible && self.width < timeline-event.width));