Compare commits

..

No commits in common. "master" and "v0.2.1" have entirely different histories.

22 changed files with 1500 additions and 2148 deletions

2763
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"
toml = "1.1.2"
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

5
TODO Normal file
View file

@ -0,0 +1,5 @@
feat:
implement instants
command line interface
proper config path handling
more coloring options

View file

@ -1,17 +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");
}

View file

@ -1,50 +1,53 @@
# Default Aliveline config
# This is the default config for Aliveline.
# Note: All colors are of format 0xAARRGGBB
# Paths may be relative to config directory or absolute
[paths]
logs = "logs"
# Path where logs are saved. May be relative to config dir or absolute.
log_path = "logs"
# Colors may be defined either as alias (string) or as 0xAARRGGBB (integer)
# Colors used for events. For now Aliveline expects to have exactly 16 colors, but this is subject to change in future.
event_colors = [
0xff_97f9f9,
0xff_a4def9,
0xff_c1e0f7,
0xff_cfbae1,
0xff_c59fc9,
0xff_4e3d42,
0xff_c9d5b5,
0xff_2d82b7,
0xff_556f44,
0xff_772e25,
0xff_c44536,
0xff_7c6a0a,
0xff_babd8d,
0xff_ffdac6,
0xff_fa9500,
0xff_eb6424
]
# Aliases must be declared here, otherwise fallback color is used
[colors.aliases]
background = 0xFF_808080
timeline = 0xFF_A9A9A9
black = 0xFF_000000
white = 0xFF_FFFFFF
"very aggressive pink color to use with fallback so you definitely notice it" = 0xFF_FF00E7
# Colors used for event colors. Aliveline expects it to have same size as events.
text_colors = [
0xff_000000,
0xff_000000,
0xff_000000,
0xff_000000,
0xff_000000,
0xff_ffffff,
0xff_000000,
0xff_000000,
0xff_000000,
0xff_ffffff,
0xff_000000,
0xff_000000,
0xff_000000,
0xff_000000,
0xff_000000,
0xff_000000
]
[colors]
# Timeline background
background = "background"
# Timeline foreground
timeline = "timeline"
# Background text (timestamps, event names, etc.)
text = "black"
# Used when alias was not found
fallback = "very aggressive pink color to use with fallback so you definitely notice it"
# Event colors are chosen pseudorandomly from this array, respecting each color's priority
# Event color consists of:
# - Background color (default: black)
# - Text color (default: same as colors.text)
# - Priority (default: 1)
# Full example: { background = "color", text = "color", priority = 1337 }
events = [
{ background = 0xff_97f9f9 },
{ background = 0xff_a4def9 },
{ background = 0xff_c1e0f7 },
{ background = 0xff_cfbae1 },
{ background = 0xff_c59fc9 },
{ background = 0xff_4e3d42, text = "white" },
{ background = 0xff_c9d5b5 },
{ background = 0xff_2d82b7 },
{ background = 0xff_556f44 },
{ background = 0xff_772e25, text = "white" },
{ background = 0xff_c44536 },
{ background = 0xff_7c6a0a },
{ background = 0xff_babd8d },
{ background = 0xff_ffdac6 },
{ background = 0xff_fa9500 },
{ background = 0xff_eb6424 },
]
# Color behind the timeline
background = 0xFF_808080
# Color of the base timeline
timeline = 0xFF_a9a9a9
# Color of background text (timestamps, event names, etc.)
background_text = 0xFF_000000

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

104
src/config.rs Normal file
View file

@ -0,0 +1,104 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
#[serde(default)]
pub struct Colors {
pub background: u32,
pub timeline: u32,
pub background_text: u32,
}
impl Default for Colors {
fn default() -> Self {
Colors {
background: 0xff_808080,
timeline: 0xff_a9a9a9,
background_text: 0xff_000000,
}
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct Config {
/// directory, where config is located
#[serde(skip)]
pub conf_path: PathBuf,
pub log_path: PathBuf,
#[serde(default)]
pub colors: Colors,
#[serde(default)]
pub event_colors: Vec<u32>,
#[serde(default)]
pub text_colors: Vec<u32>,
}
impl Default for Config {
fn default() -> Self {
let conf_path = PathBuf::new();
let colors: Colors = Default::default();
let event_colors: Vec<u32> = vec![
0xff_97f9f9,
0xff_a4def9,
0xff_c1e0f7,
0xff_cfbae1,
0xff_c59fc9,
0xff_4e3d42,
0xff_c9d5b5,
0xff_2d82b7,
0xff_556f44,
0xff_772e25,
0xff_c44536,
0xff_7c6a0a,
0xff_babd8d,
0xff_ffdac6,
0xff_fa9500,
0xff_eb6424
];
let text_colors: Vec<u32> = vec![
0xff000000,
0xff000000,
0xff000000,
0xff000000,
0xff000000,
0xffffffff,
0xff000000,
0xff000000,
0xff000000,
0xffffffff,
0xff000000,
0xff000000,
0xff000000,
0xff000000,
0xff000000,
0xff000000
];
Config {
conf_path,
log_path: PathBuf::from("./logs"),
colors,
event_colors,
text_colors,
}
}
}
impl Config {
pub fn new(conf_path: PathBuf) -> Self {
let conf_dir: PathBuf = conf_path.parent().unwrap().into();
Config {
conf_path: conf_dir,
..Default::default()
}
}
pub fn load(path: PathBuf) -> Option<Self> {
if let Ok(toml_string) = std::fs::read_to_string(path.clone()) {
if let Ok(mut conf) = toml::from_str::<Config>(&toml_string) {
conf.conf_path = path.parent().unwrap().into();
return Some(conf);
}
}
None
}
}

View file

@ -1,99 +0,0 @@
use std::{collections::HashMap, hash::{DefaultHasher, Hash, Hasher}};
use serde::{Serialize, Deserialize};
use slint::Color;
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(untagged)]
pub enum ConfigColor {
Raw(u32),
Alias(String),
}
impl Default for ConfigColor {
fn default() -> Self {
Self::Raw(0xFF000000)
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(default)]
pub struct EventColor {
pub background: ConfigColor,
pub text: Option<ConfigColor>,
pub priority: u64,
}
impl Default for EventColor {
fn default() -> Self {
Self {
background: ConfigColor::default(),
text: None,
priority: 1,
}
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(default)]
pub struct Colors {
pub aliases: HashMap<String, u32>,
pub background: ConfigColor,
pub timeline: ConfigColor,
pub text: ConfigColor,
pub fallback: ConfigColor,
pub events: Vec<EventColor>,
}
impl Default for Colors {
fn default() -> Self {
super::default::default_config().colors.into()
}
}
#[inline(always)]
fn argb(value: u32) -> Color {
Color::from_argb_encoded(value)
}
impl Colors {
#[inline(always)]
pub fn fallback_color(&self) -> Color {
self.try_get_color(&self.fallback).unwrap_or_default()
}
pub fn try_get_color(&self, color: &ConfigColor) -> Option<Color> {
match color {
ConfigColor::Raw(color) => Some(argb(*color)),
ConfigColor::Alias(alias) => self.aliases.get(alias).map(|c| argb(*c)),
}
}
pub fn get_color(&self, color: &ConfigColor) -> Color {
self.try_get_color(color).unwrap_or_else(|| self.fallback_color())
}
pub fn event_color_for(&self, text: &str) -> &EventColor {
let priority_sum: u64 = self.events.iter().map(|e| e.priority).sum();
let mut s = DefaultHasher::new();
text.hash(&mut s);
let hash = s.finish();
let mut chosen = hash % priority_sum;
for (id, color) in self.events.iter().enumerate() {
match chosen.checked_sub(color.priority) {
Some(new_count) => {
chosen = new_count;
},
None => {
return &self.events[id];
},
}
}
unreachable!()
}
}

View file

@ -1,53 +0,0 @@
use std::collections::HashMap;
use crate::config::color::{ConfigColor, EventColor};
use super::*;
const DEFAULT_CFG: &'static str = include_str!("../../config.toml");
#[derive(Deserialize)]
pub(super) struct DefaultConfig {
pub(super) colors: DefaultColors,
pub(super) paths: DefaultPaths,
}
#[derive(Deserialize)]
pub struct DefaultColors {
pub aliases: HashMap<String, u32>,
pub background: ConfigColor,
pub timeline: ConfigColor,
pub text: ConfigColor,
pub fallback: ConfigColor,
pub events: Vec<EventColor>,
}
impl From<DefaultColors> for Colors {
fn from(value: DefaultColors) -> Self {
Self {
aliases: value.aliases,
background: value.background,
timeline: value.timeline,
text: value.text,
fallback: value.fallback,
events: value.events
}
}
}
#[derive(Deserialize)]
pub struct DefaultPaths {
pub logs: PathBuf,
}
impl From<DefaultPaths> for Paths {
fn from(value: DefaultPaths) -> Self {
Self { logs: value.logs }
}
}
pub(super) fn default_config() -> DefaultConfig {
toml::de::from_str(DEFAULT_CFG).unwrap()
}

View file

@ -1,57 +0,0 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use color::Colors;
pub mod color;
mod default;
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(default)]
pub struct Paths {
pub logs: PathBuf,
}
impl Default for Paths {
fn default() -> Self {
default::default_config().paths.into()
}
}
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, Debug)]
#[serde(default)]
pub struct Config {
/// directory, where config is located
#[serde(skip)]
pub conf_path: PathBuf,
pub colors: Colors,
pub paths: Paths,
}
impl Config {
pub fn new(conf_path: PathBuf) -> Self {
let conf_dir: PathBuf = conf_path.parent().unwrap().into();
Config {
conf_path: conf_dir,
..Default::default()
}
}
fn replace_missing_fields(&mut self) {
let default = Config::default();
if self.colors.events.len() == 0 {
self.colors.events = default.colors.events;
}
}
pub fn load(path: PathBuf) -> Option<Self> {
if let Ok(toml_string) = std::fs::read_to_string(path.clone()) {
if let Ok(mut conf) = toml::from_str::<Config>(&toml_string) {
conf.replace_missing_fields();
conf.conf_path = path.parent().unwrap().into();
return Some(conf);
}
}
None
}
}

View file

@ -1,5 +1,5 @@
use config::Config;
use std::path::PathBuf;
use std::{hash::{DefaultHasher, Hash, Hasher}, path::PathBuf};
pub mod config;
pub mod log;
@ -45,3 +45,9 @@ pub fn load_config() -> Config {
println!("Using config path ./config.toml");
Config::new(PathBuf::from("./config.toml"))
}
/// Get random-like color id in range 0..16 by computing string hash
pub fn color_id_from_name(name: String) -> i32 {
let mut s = DefaultHasher::new();
name.hash(&mut s);
let hash = s.finish();
(hash % 16) as i32 }

View file

@ -46,12 +46,12 @@ impl Log {
}
fn get_log_dir(config: &Config) -> PathBuf {
if config.paths.logs.is_relative() {
if config.log_path.is_relative() {
let mut path = config.conf_path.clone();
path.push(&config.paths.logs);
path.push(&config.log_path);
return path;
} else {
return config.paths.logs.clone();
return config.log_path.clone();
}
}
@ -77,14 +77,14 @@ impl Event {
let start = Time {
hour: (start / 3600) as u8,
minute: ((start / 3600) / 60) as u8,
second: Some((start % 60) as u8),
nanosecond: None,
second: (start % 60) as u8,
nanosecond: 0
};
let end = Time {
hour: (end / 3600) as u8,
minute: ((end % 3600) / 60) as u8,
second: Some((end % 60) as u8),
nanosecond: None,
second: (end % 60) as u8,
nanosecond: 0
};
Event { name, start, end, finished }
}

View file

@ -3,40 +3,30 @@
use std::{error::Error, rc::Rc, sync::{Arc, Mutex}};
use aliveline::{config::{Config, color::Colors}, load_config, log::{Event, Log}};
use aliveline::{color_id_from_name, config::Config, load_config, log::{Event, Log}};
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 TimelineEvent {
pub fn colors(colors: &Colors, text: &str) -> (Color, Color) {
let event_colors = colors.event_color_for(text);
let background_color = colors.get_color(&event_colors.background);
let text_color = match &event_colors.text {
Some(color) => colors.get_color(color),
None => colors.get_color(&colors.text),
};
(background_color, text_color)
}
pub fn from_event(event: Event, colors: &Colors) -> Self {
impl From<Event> for TimelineEvent {
fn from(event: Event) -> Self {
let start = (event.start.hour as i32) * 3600
+ (event.start.minute as i32) * 60
+ (event.start.second.unwrap() as i32);
+ (event.start.second as i32);
let end = (event.end.hour as i32) * 3600
+ (event.end.minute as i32) * 60
+ (event.end.second.unwrap() as i32);
let (background_color, text_color) = TimelineEvent::colors(colors, event.name.as_str());
+ (event.end.second as i32);
TimelineEvent {
start,
duration: end - start,
label: event.name.to_shared_string(),
finished: event.finished,
background_color,
text_color,
color_id: color_id_from_name(event.name)
}
}
}
@ -46,33 +36,31 @@ impl From<TimelineEvent> for Event {
let start = Time {
hour: (event.start / 3600) as u8,
minute: ((event.start % 3600) / 60) as u8,
second: Some((event.start % 60) as u8),
nanosecond: None,
second: (event.start % 60) as u8,
nanosecond: 0
};
let endsecs = event.start + event.duration;
let end = Time {
hour: (endsecs / 3600) as u8,
minute: ((endsecs % 3600) / 60) as u8,
second: Some((endsecs % 60) as u8),
nanosecond: None,
second: (endsecs % 60) as u8,
nanosecond: 0
};
Event { start, end, name: event.label.to_string(), finished: event.finished }
}
}
fn load_log(ui_weak: Weak<AppWindow>, log: Arc<Mutex<Log>>, config: Arc<Config>) {
fn load_log(ui_weak: Weak<AppWindow>, log: Arc<Mutex<Log>>) {
let ui = ui_weak.unwrap();
let log_guard = log.lock().expect("Log shouldn't be used twice");
let events: Vec<TimelineEvent> = (*log_guard)
.events
.iter()
.map(|event| TimelineEvent::from_event(event.clone(), &config.colors))
.map(|event| TimelineEvent::from((*event).clone()))
.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();
}
@ -80,9 +68,21 @@ fn load_log(ui_weak: Weak<AppWindow>, log: Arc<Mutex<Log>>, config: Arc<Config>)
fn load_colors(ui_weak: Weak<AppWindow>, config: Arc<Config>) {
let ui = ui_weak.unwrap();
let pal = ui.global::<Palette>();
pal.set_background(config.colors.get_color(&config.colors.background));
pal.set_timeline(config.colors.get_color(&config.colors.timeline));
pal.set_background_text(config.colors.get_color(&config.colors.text));
pal.set_background(Color::from_argb_encoded(config.colors.background));
pal.set_timeline(Color::from_argb_encoded(config.colors.timeline));
pal.set_background_text(Color::from_argb_encoded(config.colors.background_text));
// This looks like war crime
let event_colors_rc: ModelRc<Color> = Rc::new(VecModel::from(
config.event_colors.iter()
.map(|value| Color::from_argb_encoded(*value)).collect::<Vec<Color>>()
)).into();
pal.set_event_colors(event_colors_rc);
let event_text_rc: ModelRc<Color> = Rc::new(VecModel::from(
config.text_colors.iter()
.map(|value| Color::from_argb_encoded(*value)).collect::<Vec<Color>>()
)).into();
pal.set_event_text(event_text_rc);
}
fn main() -> Result<(), Box<dyn Error>> {
@ -102,14 +102,14 @@ fn main() -> Result<(), Box<dyn Error>> {
let ui_weak = ui.as_weak();
let log = writing_log.clone();
let config_arc = config.clone();
load_log(ui_weak, log, config_arc);
load_log(ui_weak, log);
let ui_weak = ui.as_weak();
let config_arc = config.clone();
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();
@ -124,12 +124,10 @@ fn main() -> Result<(), Box<dyn Error>> {
let events: Vec<TimelineEvent> = Log::load_from(&config, date)
.events
.iter()
.map(|event| TimelineEvent::from_event(event.clone(), &config.colors))
.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);
}
});
@ -153,38 +151,31 @@ 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);
}
}
});
ui.on_start_new_event({
let ui_weak = ui.as_weak();
let log = writing_log.clone();
let config = config.clone();
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 (background_color, text_color) = TimelineEvent::colors(&config.colors, event_name.as_str());
let offset = ui.get_record_offset();
let event = TimelineEvent {
duration: 0,
finished: false,
label: event_name.clone(),
start: state.offset,
background_color,
text_color,
start: offset,
color_id: color_id_from_name(event_name.to_string())
};
{
@ -201,29 +192,25 @@ fn main() -> Result<(), Box<dyn Error>> {
ui.on_stop_event({
let ui_weak = ui.as_weak();
let log = writing_log.clone();
let config = config.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)
.unwrap();
let event = events.row_data(event_id)
.expect("stop-event called without unfinished events");
let (background_color, text_color) = TimelineEvent::colors(&config.colors, event.label.as_str());
let new_event = TimelineEvent {
duration: state.offset - event.start,
duration: offset - event.start,
finished: true,
label: event.label.clone(),
start: event.start,
background_color,
text_color,
color_id: color_id_from_name(event.label.to_string())
};
{
@ -274,7 +261,6 @@ fn main() -> Result<(), Box<dyn Error>> {
ui.on_new_day_started({
let ui_weak = ui.as_weak();
let log = writing_log.clone();
let config = config.clone();
move || {
let ui = ui_weak.unwrap();
@ -309,7 +295,7 @@ fn main() -> Result<(), Box<dyn Error>> {
}
}
load_log(ui.as_weak(), log.clone(), config.clone());
load_log(ui.as_weak(), log.clone());
ui.invoke_save_log();
}
});

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

@ -2,4 +2,42 @@ export global Palette {
in-out property<color> background: gray;
in-out property<color> timeline: darkgray;
in-out property<color> background-text: black;
// Note: these colors were almost randomly picked
in-out property<[color]> event-colors: [
#97f9f9,
#a4def9,
#c1e0f7,
#cfbae1,
#c59fc9,
#4e3d42,
#c9d5b5,
#2d82b7,
#556f44,
#772e25,
#c44536,
#7c6a0a,
#babd8d,
#ffdac6,
#fa9500,
#eb6424
];
in-out property <[color]> event-text: [
#000000,
#000000,
#000000,
#000000,
#000000,
#ffffff,
#000000,
#000000,
#000000,
#ffffff,
#000000,
#000000,
#000000,
#000000,
#000000,
#000000
];
}

View file

@ -1,29 +1,55 @@
import { Palette } from "theme.slint";
import { TimeString, Const } from "global.slint";
export struct TimelineEvent {
start: int,
duration: int,
finished: bool,
label: string,
background-color: color,
text-color: color,
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%;
@ -46,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;
}
@ -57,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;
@ -70,7 +96,7 @@ export component Timeline inherits Rectangle {
visible: self.width > 0 && self.real-x < parent.width;
border-color: black;
border-width: 1px;
background: event.background-color;
background: Palette.event-colors[event.color-id];
Text {
x: 0;
@ -84,11 +110,11 @@ 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));
color: event.text-color;
color: Palette.event-text[event.color-id];
}
end-txt := Text {
x: timeline-event.width - self.width;
@ -97,7 +123,7 @@ export component Timeline inherits Rectangle {
TimeString.from(event.start + event.duration) :
TimeString.from(visible-offset);
visible: timeline-event.visible && timeline-event.width - self.width * 2 > 0;
color: event.text-color;
color: Palette.event-text[event.color-id];
}
}
@children