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
2763
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
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 = "1.1.2"
|
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
|
||||||
|
|
|
||||||
5
TODO
Normal file
5
TODO
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
feat:
|
||||||
|
implement instants
|
||||||
|
command line interface
|
||||||
|
proper config path handling
|
||||||
|
more coloring options
|
||||||
16
build.rs
16
build.rs
|
|
@ -1,17 +1,3 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut style = String::from("cosmic");
|
slint_build::compile("ui/app-window.slint").expect("Slint build failed");
|
||||||
|
|
||||||
#[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");
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
93
config.toml
93
config.toml
|
|
@ -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
|
# Path where logs are saved. May be relative to config dir or absolute.
|
||||||
[paths]
|
log_path = "logs"
|
||||||
logs = "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 used for event colors. Aliveline expects it to have same size as events.
|
||||||
[colors.aliases]
|
text_colors = [
|
||||||
background = 0xFF_808080
|
0xff_000000,
|
||||||
timeline = 0xFF_A9A9A9
|
0xff_000000,
|
||||||
black = 0xFF_000000
|
0xff_000000,
|
||||||
white = 0xFF_FFFFFF
|
0xff_000000,
|
||||||
"very aggressive pink color to use with fallback so you definitely notice it" = 0xFF_FF00E7
|
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]
|
[colors]
|
||||||
# Timeline background
|
# Color behind the timeline
|
||||||
background = "background"
|
background = 0xFF_808080
|
||||||
# Timeline foreground
|
# Color of the base timeline
|
||||||
timeline = "timeline"
|
timeline = 0xFF_a9a9a9
|
||||||
# Background text (timestamps, event names, etc.)
|
# Color of background text (timestamps, event names, etc.)
|
||||||
text = "black"
|
background_text = 0xFF_000000
|
||||||
# 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 },
|
|
||||||
]
|
|
||||||
|
|
|
||||||
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 |
104
src/config.rs
Normal file
104
src/config.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use std::path::PathBuf;
|
use std::{hash::{DefaultHasher, Hash, Hasher}, path::PathBuf};
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod log;
|
pub mod log;
|
||||||
|
|
@ -45,3 +45,9 @@ pub fn load_config() -> Config {
|
||||||
println!("Using config path ./config.toml");
|
println!("Using config path ./config.toml");
|
||||||
Config::new(PathBuf::from("./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 }
|
||||||
|
|
|
||||||
14
src/log.rs
14
src/log.rs
|
|
@ -46,12 +46,12 @@ impl Log {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_log_dir(config: &Config) -> PathBuf {
|
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();
|
let mut path = config.conf_path.clone();
|
||||||
path.push(&config.paths.logs);
|
path.push(&config.log_path);
|
||||||
return path;
|
return path;
|
||||||
} else {
|
} else {
|
||||||
return config.paths.logs.clone();
|
return config.log_path.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,14 +77,14 @@ impl Event {
|
||||||
let start = Time {
|
let start = Time {
|
||||||
hour: (start / 3600) as u8,
|
hour: (start / 3600) as u8,
|
||||||
minute: ((start / 3600) / 60) as u8,
|
minute: ((start / 3600) / 60) as u8,
|
||||||
second: Some((start % 60) as u8),
|
second: (start % 60) as u8,
|
||||||
nanosecond: None,
|
nanosecond: 0
|
||||||
};
|
};
|
||||||
let end = Time {
|
let end = Time {
|
||||||
hour: (end / 3600) as u8,
|
hour: (end / 3600) as u8,
|
||||||
minute: ((end % 3600) / 60) as u8,
|
minute: ((end % 3600) / 60) as u8,
|
||||||
second: Some((end % 60) as u8),
|
second: (end % 60) as u8,
|
||||||
nanosecond: None,
|
nanosecond: 0
|
||||||
};
|
};
|
||||||
Event { name, start, end, finished }
|
Event { name, start, end, finished }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
112
src/main.rs
112
src/main.rs
|
|
@ -3,40 +3,30 @@
|
||||||
|
|
||||||
use std::{error::Error, rc::Rc, sync::{Arc, Mutex}};
|
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 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 TimelineEvent {
|
impl From<Event> for TimelineEvent {
|
||||||
pub fn colors(colors: &Colors, text: &str) -> (Color, Color) {
|
fn from(event: Event) -> Self {
|
||||||
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 {
|
|
||||||
let start = (event.start.hour as i32) * 3600
|
let start = (event.start.hour as i32) * 3600
|
||||||
+ (event.start.minute as i32) * 60
|
+ (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
|
let end = (event.end.hour as i32) * 3600
|
||||||
+ (event.end.minute as i32) * 60
|
+ (event.end.minute as i32) * 60
|
||||||
+ (event.end.second.unwrap() as i32);
|
+ (event.end.second as i32);
|
||||||
let (background_color, text_color) = TimelineEvent::colors(colors, event.name.as_str());
|
|
||||||
|
|
||||||
TimelineEvent {
|
TimelineEvent {
|
||||||
start,
|
start,
|
||||||
duration: end - start,
|
duration: end - start,
|
||||||
label: event.name.to_shared_string(),
|
label: event.name.to_shared_string(),
|
||||||
finished: event.finished,
|
finished: event.finished,
|
||||||
background_color,
|
color_id: color_id_from_name(event.name)
|
||||||
text_color,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,33 +36,31 @@ impl From<TimelineEvent> for Event {
|
||||||
let start = Time {
|
let start = Time {
|
||||||
hour: (event.start / 3600) as u8,
|
hour: (event.start / 3600) as u8,
|
||||||
minute: ((event.start % 3600) / 60) as u8,
|
minute: ((event.start % 3600) / 60) as u8,
|
||||||
second: Some((event.start % 60) as u8),
|
second: (event.start % 60) as u8,
|
||||||
nanosecond: None,
|
nanosecond: 0
|
||||||
};
|
};
|
||||||
let endsecs = event.start + event.duration;
|
let endsecs = event.start + event.duration;
|
||||||
let end = Time {
|
let end = Time {
|
||||||
hour: (endsecs / 3600) as u8,
|
hour: (endsecs / 3600) as u8,
|
||||||
minute: ((endsecs % 3600) / 60) as u8,
|
minute: ((endsecs % 3600) / 60) as u8,
|
||||||
second: Some((endsecs % 60) as u8),
|
second: (endsecs % 60) as u8,
|
||||||
nanosecond: None,
|
nanosecond: 0
|
||||||
};
|
};
|
||||||
Event { start, end, name: event.label.to_string(), finished: event.finished }
|
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 ui = ui_weak.unwrap();
|
||||||
let log_guard = log.lock().expect("Log shouldn't be used twice");
|
let log_guard = log.lock().expect("Log shouldn't be used twice");
|
||||||
let events: Vec<TimelineEvent> = (*log_guard)
|
let events: Vec<TimelineEvent> = (*log_guard)
|
||||||
.events
|
.events
|
||||||
.iter()
|
.iter()
|
||||||
.map(|event| TimelineEvent::from_event(event.clone(), &config.colors))
|
.map(|event| TimelineEvent::from((*event).clone()))
|
||||||
.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();
|
||||||
}
|
}
|
||||||
|
|
@ -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>) {
|
fn load_colors(ui_weak: Weak<AppWindow>, config: Arc<Config>) {
|
||||||
let ui = ui_weak.unwrap();
|
let ui = ui_weak.unwrap();
|
||||||
let pal = ui.global::<Palette>();
|
let pal = ui.global::<Palette>();
|
||||||
pal.set_background(config.colors.get_color(&config.colors.background));
|
pal.set_background(Color::from_argb_encoded(config.colors.background));
|
||||||
pal.set_timeline(config.colors.get_color(&config.colors.timeline));
|
pal.set_timeline(Color::from_argb_encoded(config.colors.timeline));
|
||||||
pal.set_background_text(config.colors.get_color(&config.colors.text));
|
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>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
|
@ -102,14 +102,14 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
|
||||||
let ui_weak = ui.as_weak();
|
let ui_weak = ui.as_weak();
|
||||||
let log = writing_log.clone();
|
let log = writing_log.clone();
|
||||||
let config_arc = config.clone();
|
load_log(ui_weak, log);
|
||||||
load_log(ui_weak, log, config_arc);
|
|
||||||
|
|
||||||
let ui_weak = ui.as_weak();
|
let ui_weak = ui.as_weak();
|
||||||
let config_arc = config.clone();
|
let config_arc = config.clone();
|
||||||
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();
|
||||||
|
|
@ -124,12 +124,10 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let events: Vec<TimelineEvent> = Log::load_from(&config, date)
|
let events: Vec<TimelineEvent> = Log::load_from(&config, date)
|
||||||
.events
|
.events
|
||||||
.iter()
|
.iter()
|
||||||
.map(|event| TimelineEvent::from_event(event.clone(), &config.colors))
|
.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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -153,38 +151,31 @@ 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);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.on_start_new_event({
|
ui.on_start_new_event({
|
||||||
let ui_weak = ui.as_weak();
|
let ui_weak = ui.as_weak();
|
||||||
let log = writing_log.clone();
|
let log = writing_log.clone();
|
||||||
let config = config.clone();
|
|
||||||
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 (background_color, text_color) = TimelineEvent::colors(&config.colors, event_name.as_str());
|
|
||||||
|
|
||||||
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,
|
||||||
background_color,
|
color_id: color_id_from_name(event_name.to_string())
|
||||||
text_color,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -201,29 +192,25 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
ui.on_stop_event({
|
ui.on_stop_event({
|
||||||
let ui_weak = ui.as_weak();
|
let ui_weak = ui.as_weak();
|
||||||
let log = writing_log.clone();
|
let log = writing_log.clone();
|
||||||
let config = config.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)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
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 (background_color, text_color) = TimelineEvent::colors(&config.colors, event.label.as_str());
|
|
||||||
|
|
||||||
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,
|
||||||
background_color,
|
color_id: color_id_from_name(event.label.to_string())
|
||||||
text_color,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -274,7 +261,6 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
ui.on_new_day_started({
|
ui.on_new_day_started({
|
||||||
let ui_weak = ui.as_weak();
|
let ui_weak = ui.as_weak();
|
||||||
let log = writing_log.clone();
|
let log = writing_log.clone();
|
||||||
let config = config.clone();
|
|
||||||
move || {
|
move || {
|
||||||
let ui = ui_weak.unwrap();
|
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();
|
ui.invoke_save_log();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,42 @@ export global Palette {
|
||||||
in-out property<color> background: gray;
|
in-out property<color> background: gray;
|
||||||
in-out property<color> timeline: darkgray;
|
in-out property<color> timeline: darkgray;
|
||||||
in-out property<color> background-text: black;
|
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
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +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,
|
||||||
background-color: color,
|
color-id: int
|
||||||
text-color: color,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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%;
|
||||||
|
|
@ -46,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,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;
|
||||||
|
|
@ -70,7 +96,7 @@ export component Timeline inherits Rectangle {
|
||||||
visible: self.width > 0 && self.real-x < parent.width;
|
visible: self.width > 0 && self.real-x < parent.width;
|
||||||
border-color: black;
|
border-color: black;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
background: event.background-color;
|
background: Palette.event-colors[event.color-id];
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
x: 0;
|
x: 0;
|
||||||
|
|
@ -84,11 +110,11 @@ 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));
|
||||||
color: event.text-color;
|
color: Palette.event-text[event.color-id];
|
||||||
}
|
}
|
||||||
end-txt := Text {
|
end-txt := Text {
|
||||||
x: timeline-event.width - self.width;
|
x: timeline-event.width - self.width;
|
||||||
|
|
@ -97,7 +123,7 @@ export component Timeline inherits Rectangle {
|
||||||
TimeString.from(event.start + event.duration) :
|
TimeString.from(event.start + event.duration) :
|
||||||
TimeString.from(visible-offset);
|
TimeString.from(visible-offset);
|
||||||
visible: timeline-event.visible && timeline-event.width - self.width * 2 > 0;
|
visible: timeline-event.visible && timeline-event.width - self.width * 2 > 0;
|
||||||
color: event.text-color;
|
color: Palette.event-text[event.color-id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@children
|
@children
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue