Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba7bc67b6c | |||
| a64ed277f6 | |||
| 4bca060ea9 | |||
| 9b21d107c7 | |||
| d08e172f34 | |||
| d4e924b258 | |||
| 1a1f6dde83 | |||
| 365f77056e |
22 changed files with 2150 additions and 1502 deletions
2741
Cargo.lock
generated
2741
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
|
@ -1,18 +1,20 @@
|
|||
[package]
|
||||
name = "aliveline"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
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.12.1"
|
||||
toml = "0.9.5"
|
||||
slint = "1.15.1"
|
||||
toml = "1.1.2"
|
||||
|
||||
[build-dependencies]
|
||||
slint-build = "1.12.1"
|
||||
slint-build = "1.15.1"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
||||
[features]
|
||||
light = []
|
||||
dark = []
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -34,12 +34,22 @@ 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/))
|
||||
|
||||
Instructions:
|
||||
Just run `cargo build --release` and the resulting binary can be located at `target/release/aliveline[.exe]` if compilation succeeds.
|
||||
### 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]`
|
||||
|
||||
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
5
TODO
|
|
@ -1,5 +0,0 @@
|
|||
feat:
|
||||
implement instants
|
||||
command line interface
|
||||
proper config path handling
|
||||
more coloring options
|
||||
16
build.rs
16
build.rs
|
|
@ -1,3 +1,17 @@
|
|||
fn main() {
|
||||
slint_build::compile("ui/app-window.slint").expect("Slint build failed");
|
||||
let mut style = String::from("cosmic");
|
||||
|
||||
#[cfg(all(feature = "dark", feature = "light"))]
|
||||
compile_error!("Features \"dark\" and \"light\" are mutually exclusive");
|
||||
|
||||
if cfg!(feature = "dark") {
|
||||
style.push_str("-dark");
|
||||
} else if cfg!(feature = "light") {
|
||||
style.push_str("-light");
|
||||
}
|
||||
|
||||
let config = slint_build::CompilerConfiguration::new()
|
||||
.with_style(style);
|
||||
|
||||
slint_build::compile_with_config("ui/app-window.slint", config).expect("Slint build failed");
|
||||
}
|
||||
|
|
|
|||
93
config.toml
93
config.toml
|
|
@ -1,53 +1,50 @@
|
|||
# This is the default config for Aliveline.
|
||||
# Note: All colors are of format 0xAARRGGBB
|
||||
# Default Aliveline config
|
||||
|
||||
# Path where logs are saved. May be relative to config dir or absolute.
|
||||
log_path = "logs"
|
||||
# Paths may be relative to config directory or absolute
|
||||
[paths]
|
||||
logs = "logs"
|
||||
|
||||
# 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
|
||||
]
|
||||
# Colors may be defined either as alias (string) or as 0xAARRGGBB (integer)
|
||||
|
||||
# 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
|
||||
]
|
||||
# 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]
|
||||
# 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
|
||||
# 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 },
|
||||
]
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 122 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 28 KiB |
104
src/config.rs
104
src/config.rs
|
|
@ -1,104 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
99
src/config/color.rs
Normal file
99
src/config/color.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
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!()
|
||||
}
|
||||
}
|
||||
53
src/config/default.rs
Normal file
53
src/config/default.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
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()
|
||||
}
|
||||
57
src/config/mod.rs
Normal file
57
src/config/mod.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
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 std::{hash::{DefaultHasher, Hash, Hasher}, path::PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub mod config;
|
||||
pub mod log;
|
||||
|
|
@ -45,9 +45,3 @@ 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 }
|
||||
|
|
|
|||
14
src/log.rs
14
src/log.rs
|
|
@ -46,12 +46,12 @@ impl Log {
|
|||
}
|
||||
|
||||
fn get_log_dir(config: &Config) -> PathBuf {
|
||||
if config.log_path.is_relative() {
|
||||
if config.paths.logs.is_relative() {
|
||||
let mut path = config.conf_path.clone();
|
||||
path.push(&config.log_path);
|
||||
path.push(&config.paths.logs);
|
||||
return path;
|
||||
} else {
|
||||
return config.log_path.clone();
|
||||
return config.paths.logs.clone();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,14 +77,14 @@ impl Event {
|
|||
let start = Time {
|
||||
hour: (start / 3600) as u8,
|
||||
minute: ((start / 3600) / 60) as u8,
|
||||
second: (start % 60) as u8,
|
||||
nanosecond: 0
|
||||
second: Some((start % 60) as u8),
|
||||
nanosecond: None,
|
||||
};
|
||||
let end = Time {
|
||||
hour: (end / 3600) as u8,
|
||||
minute: ((end % 3600) / 60) as u8,
|
||||
second: (end % 60) as u8,
|
||||
nanosecond: 0
|
||||
second: Some((end % 60) as u8),
|
||||
nanosecond: None,
|
||||
};
|
||||
Event { name, start, end, finished }
|
||||
}
|
||||
|
|
|
|||
112
src/main.rs
112
src/main.rs
|
|
@ -3,30 +3,40 @@
|
|||
|
||||
use std::{error::Error, rc::Rc, sync::{Arc, Mutex}};
|
||||
|
||||
use aliveline::{color_id_from_name, config::Config, load_config, log::{Event, Log}};
|
||||
use aliveline::{config::{Config, color::Colors}, 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 From<Event> for TimelineEvent {
|
||||
fn from(event: Event) -> Self {
|
||||
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 {
|
||||
let start = (event.start.hour as i32) * 3600
|
||||
+ (event.start.minute as i32) * 60
|
||||
+ (event.start.second as i32);
|
||||
+ (event.start.second.unwrap() as i32);
|
||||
let end = (event.end.hour as i32) * 3600
|
||||
+ (event.end.minute as i32) * 60
|
||||
+ (event.end.second as i32);
|
||||
+ (event.end.second.unwrap() as i32);
|
||||
let (background_color, text_color) = TimelineEvent::colors(colors, event.name.as_str());
|
||||
|
||||
TimelineEvent {
|
||||
start,
|
||||
duration: end - start,
|
||||
label: event.name.to_shared_string(),
|
||||
finished: event.finished,
|
||||
color_id: color_id_from_name(event.name)
|
||||
background_color,
|
||||
text_color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,31 +46,33 @@ impl From<TimelineEvent> for Event {
|
|||
let start = Time {
|
||||
hour: (event.start / 3600) as u8,
|
||||
minute: ((event.start % 3600) / 60) as u8,
|
||||
second: (event.start % 60) as u8,
|
||||
nanosecond: 0
|
||||
second: Some((event.start % 60) as u8),
|
||||
nanosecond: None,
|
||||
};
|
||||
let endsecs = event.start + event.duration;
|
||||
let end = Time {
|
||||
hour: (endsecs / 3600) as u8,
|
||||
minute: ((endsecs % 3600) / 60) as u8,
|
||||
second: (endsecs % 60) as u8,
|
||||
nanosecond: 0
|
||||
second: Some((endsecs % 60) as u8),
|
||||
nanosecond: None,
|
||||
};
|
||||
Event { start, end, name: event.label.to_string(), finished: event.finished }
|
||||
}
|
||||
}
|
||||
|
||||
fn load_log(ui_weak: Weak<AppWindow>, log: Arc<Mutex<Log>>) {
|
||||
fn load_log(ui_weak: Weak<AppWindow>, log: Arc<Mutex<Log>>, config: Arc<Config>) {
|
||||
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).clone()))
|
||||
.map(|event| TimelineEvent::from_event(event.clone(), &config.colors))
|
||||
.collect();
|
||||
let in_progress = events.iter().any(|event| !event.finished);
|
||||
let model: ModelRc<TimelineEvent> = Rc::new(VecModel::from(events)).into();
|
||||
ui.set_record_events(model);
|
||||
let mut state = ui.get_record_state();
|
||||
state.events = model;
|
||||
ui.set_record_state(state);
|
||||
ui.set_in_progress(in_progress);
|
||||
ui.invoke_get_previous_event();
|
||||
}
|
||||
|
|
@ -68,21 +80,9 @@ fn load_log(ui_weak: Weak<AppWindow>, log: Arc<Mutex<Log>>) {
|
|||
fn load_colors(ui_weak: Weak<AppWindow>, config: Arc<Config>) {
|
||||
let ui = ui_weak.unwrap();
|
||||
let pal = ui.global::<Palette>();
|
||||
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);
|
||||
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));
|
||||
}
|
||||
|
||||
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();
|
||||
load_log(ui_weak, log);
|
||||
let config_arc = config.clone();
|
||||
load_log(ui_weak, log, config_arc);
|
||||
|
||||
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,10 +124,12 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
let events: Vec<TimelineEvent> = Log::load_from(&config, date)
|
||||
.events
|
||||
.iter()
|
||||
.map(|event| TimelineEvent::from((*event).clone()))
|
||||
.map(|event| TimelineEvent::from_event(event.clone(), &config.colors))
|
||||
.collect();
|
||||
let model: ModelRc<TimelineEvent> = Rc::new(VecModel::from(events)).into();
|
||||
ui.set_review_events(model);
|
||||
let mut state = ui.get_review_state();
|
||||
state.events = model;
|
||||
ui.set_review_state(state);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -151,31 +153,38 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
.map(|h| h.parse::<i32>().unwrap())
|
||||
.unwrap();
|
||||
if is_record {
|
||||
ui.set_record_visible_time(hours * 3600);
|
||||
let mut state = ui.get_record_state();
|
||||
state.visible_time = hours * 3600;
|
||||
ui.set_record_state(state);
|
||||
} else {
|
||||
ui.set_review_visible_time(hours * 3600);
|
||||
}
|
||||
let mut state = ui.get_review_state();
|
||||
state.visible_time = hours * 3600;
|
||||
ui.set_review_state(state);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
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 events_rc = ui.get_record_events();
|
||||
let events = events_rc.as_any()
|
||||
let state = ui.get_record_state();
|
||||
let events = state.events.as_any()
|
||||
.downcast_ref::<VecModel<TimelineEvent>>()
|
||||
.unwrap();
|
||||
let offset = ui.get_record_offset();
|
||||
|
||||
let (background_color, text_color) = TimelineEvent::colors(&config.colors, event_name.as_str());
|
||||
|
||||
let event = TimelineEvent {
|
||||
duration: 0,
|
||||
finished: false,
|
||||
label: event_name.clone(),
|
||||
start: offset,
|
||||
color_id: color_id_from_name(event_name.to_string())
|
||||
start: state.offset,
|
||||
background_color,
|
||||
text_color,
|
||||
};
|
||||
|
||||
{
|
||||
|
|
@ -192,25 +201,29 @@ 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 events_rc = ui.get_record_events();
|
||||
let events = events_rc.as_any()
|
||||
let state = ui.get_record_state();
|
||||
let events = state.events.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: offset - event.start,
|
||||
duration: state.offset - event.start,
|
||||
finished: true,
|
||||
label: event.label.clone(),
|
||||
start: event.start,
|
||||
color_id: color_id_from_name(event.label.to_string())
|
||||
background_color,
|
||||
text_color,
|
||||
};
|
||||
|
||||
{
|
||||
|
|
@ -261,6 +274,7 @@ 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();
|
||||
|
||||
|
|
@ -295,7 +309,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
}
|
||||
}
|
||||
|
||||
load_log(ui.as_weak(), log.clone());
|
||||
load_log(ui.as_weak(), log.clone(), config.clone());
|
||||
ui.invoke_save_log();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,62 +1,107 @@
|
|||
import { TabWidget } from "std-widgets.slint";
|
||||
import { TabWidget, VerticalBox, ComboBox } from "std-widgets.slint";
|
||||
import { RecordWidget } from "record.slint";
|
||||
import { ReviewWidget } from "review.slint";
|
||||
import { TimelineEvent } from "timeline.slint";
|
||||
import { TimelineEvent, Timeline, TimelineState } from "timeline.slint";
|
||||
import { Const } from "global.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);
|
||||
|
||||
update-record-offset(new-offset) => {
|
||||
record.offset = new-offset;
|
||||
}
|
||||
in-out property record-state <=> record.state;
|
||||
|
||||
update-window-size(width, height) => {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
in-out property review-state <=> review.state;
|
||||
|
||||
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";
|
||||
TabWidget {
|
||||
Tab {
|
||||
title: "Record";
|
||||
VerticalLayout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
tl := Timeline {
|
||||
preferred-height: 100%;
|
||||
min-height: 50px;
|
||||
state: in-record-mode ? record-state : review-state;
|
||||
clicked => {
|
||||
minimized = !minimized;
|
||||
}
|
||||
}
|
||||
spacing: minimized ? 0 : 8px;
|
||||
record := RecordWidget {
|
||||
combo-spans: combo-spans;
|
||||
update-visible-time(time) => {
|
||||
root.update-visible-time(true, time);
|
||||
}
|
||||
minimized: minimized || !in-record-mode;
|
||||
}
|
||||
}
|
||||
Tab {
|
||||
title: "Review";
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
ui/global.slint
Normal file
21
ui/global.slint
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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,33 +1,25 @@
|
|||
import { VerticalBox, LineEdit, Button, ComboBox } from "std-widgets.slint";
|
||||
import { Timeline } from "timeline.slint";
|
||||
import { Timeline, TimelineState } from "timeline.slint";
|
||||
|
||||
export component RecordWidget inherits VerticalBox {
|
||||
callback new-day-started <=> tl.new-day-started;
|
||||
export component RecordWidget inherits VerticalLayout {
|
||||
callback update-visible-time(string);
|
||||
callback start-new-event(string);
|
||||
callback chain-event(string);
|
||||
callback stop-event;
|
||||
callback get-previous-event();
|
||||
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-out property<TimelineState> state;
|
||||
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;
|
||||
tl := Timeline {
|
||||
preferred-height: 100%;
|
||||
updating: true;
|
||||
clicked => {
|
||||
minimized = !minimized;
|
||||
}
|
||||
}
|
||||
in-out property <bool> minimized;
|
||||
|
||||
if !minimized: GridLayout {
|
||||
spacing-vertical: 8px;
|
||||
spacing-horizontal: 16px;
|
||||
padding: 8px;
|
||||
le := LineEdit {
|
||||
placeholder-text: "Event name";
|
||||
text: event-name;
|
||||
|
|
@ -43,6 +35,7 @@ export component RecordWidget inherits VerticalBox {
|
|||
text: in-progress ? "Stop" : "Start";
|
||||
row: 1;
|
||||
colspan: 2;
|
||||
primary: true;
|
||||
clicked => {
|
||||
if in-progress {
|
||||
root.stop-event();
|
||||
|
|
@ -56,7 +49,7 @@ export component RecordWidget inherits VerticalBox {
|
|||
Button {
|
||||
text: "Chain";
|
||||
enabled: in-progress;
|
||||
col: 3;
|
||||
col: 2;
|
||||
row: 1;
|
||||
colspan: 2;
|
||||
clicked => {
|
||||
|
|
@ -67,7 +60,7 @@ export component RecordWidget inherits VerticalBox {
|
|||
Button {
|
||||
text: previous-event-name == "" ? "Chain previous event (None)" : "Chain previous event (\{previous-event-name})";
|
||||
enabled: in-progress && previous-event-name != "";
|
||||
col: 5;
|
||||
col: 4;
|
||||
row: 1;
|
||||
colspan: 2;
|
||||
clicked => {
|
||||
|
|
|
|||
|
|
@ -1,65 +1,62 @@
|
|||
import { VerticalBox, LineEdit, Button, DatePickerPopup, ComboBox, Slider } from "std-widgets.slint";
|
||||
import { Timeline } from "timeline.slint";
|
||||
import { Timeline, TimelineState } from "timeline.slint";
|
||||
import { Const } from "global.slint";
|
||||
|
||||
export component ReviewWidget inherits VerticalBox {
|
||||
export component ReviewWidget inherits VerticalLayout {
|
||||
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 visible-time <=> tl.visible-time;
|
||||
in-out property offset <=> tl.offset;
|
||||
in-out property events <=> tl.events;
|
||||
in-out property<TimelineState> state;
|
||||
in-out property<bool> minimized;
|
||||
in-out property<bool> is-active;
|
||||
|
||||
tl := Timeline {
|
||||
updating: false;
|
||||
}
|
||||
GridLayout {
|
||||
spacing-vertical: 8px;
|
||||
spacing-horizontal: 16px;
|
||||
if is-active: VerticalLayout {
|
||||
spacing: 8px;
|
||||
Slider {
|
||||
minimum: visible-time;
|
||||
maximum: tl.max-offset;
|
||||
value: offset;
|
||||
row: 0;
|
||||
colspan: 2;
|
||||
minimum: state.visible-time;
|
||||
maximum: Const.max-offset;
|
||||
value: state.offset;
|
||||
changed(value) => {
|
||||
offset = value;
|
||||
state.offset = value;
|
||||
}
|
||||
}
|
||||
if !minimized: GridLayout {
|
||||
spacing-horizontal: 16px;
|
||||
padding: 8px;
|
||||
Text {
|
||||
text: "Day: \{current-day}/\{current-month}/\{current-year}";
|
||||
font-size: 32px;
|
||||
font-size: 24px;
|
||||
horizontal-alignment: right;
|
||||
row: 1;
|
||||
}
|
||||
Button {
|
||||
primary: true;
|
||||
text: "Select";
|
||||
clicked => {
|
||||
date-picker.show()
|
||||
}
|
||||
row: 1;
|
||||
col: 1;
|
||||
}
|
||||
Text {
|
||||
text: "Span: ";
|
||||
font-size: 24px;
|
||||
row: 2;
|
||||
row: 1;
|
||||
horizontal-alignment: right;
|
||||
}
|
||||
ComboBox {
|
||||
model: combo-spans;
|
||||
current-index: 0;
|
||||
row: 2;
|
||||
row: 1;
|
||||
col: 1;
|
||||
selected(current-value) => {
|
||||
root.update-visible-time(current-value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
date-picker := DatePickerPopup {
|
||||
x: (root.width - self.width) / 2;
|
||||
y: (root.height - self.height) / 2;
|
||||
|
|
|
|||
|
|
@ -2,42 +2,4 @@ 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
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +1,29 @@
|
|||
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
|
||||
background-color: color,
|
||||
text-color: color,
|
||||
}
|
||||
|
||||
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 struct TimelineState {
|
||||
visible-time: int,
|
||||
offset: int,
|
||||
events: [TimelineEvent],
|
||||
}
|
||||
|
||||
export component Timeline inherits Rectangle {
|
||||
callback new-day-started;
|
||||
callback clicked <=> ta.clicked;
|
||||
|
||||
background: Palette.background;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
in-out property<TimelineState> state;
|
||||
property<int> visible-offset: max(state.offset, state.visible-time);
|
||||
out property<int> max-offset: Const.max-offset;
|
||||
|
||||
ta := TouchArea {
|
||||
preferred-width: 100%;
|
||||
|
|
@ -72,7 +46,7 @@ export component Timeline inherits Rectangle {
|
|||
Text {
|
||||
x: 0;
|
||||
y: parent.height - self.height;
|
||||
text: TimeString.from(visible-offset - visible-time);
|
||||
text: TimeString.from(visible-offset - state.visible-time);
|
||||
color: Palette.background-text;
|
||||
}
|
||||
|
||||
|
|
@ -83,9 +57,9 @@ export component Timeline inherits Rectangle {
|
|||
color: Palette.background-text;
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
x: max(real-x, 0);
|
||||
y: parent.height / 4;
|
||||
z: 1;
|
||||
|
|
@ -96,7 +70,7 @@ export component Timeline inherits Rectangle {
|
|||
visible: self.width > 0 && self.real-x < parent.width;
|
||||
border-color: black;
|
||||
border-width: 1px;
|
||||
background: Palette.event-colors[event.color-id];
|
||||
background: event.background-color;
|
||||
|
||||
Text {
|
||||
x: 0;
|
||||
|
|
@ -110,11 +84,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 - visible-time);
|
||||
TimeString.from(visible-offset - state.visible-time);
|
||||
visible: timeline-event.visible &&
|
||||
(self.width * 2 < timeline-event.width ||
|
||||
(!end-txt.visible && self.width < timeline-event.width));
|
||||
color: Palette.event-text[event.color-id];
|
||||
color: event.text-color;
|
||||
}
|
||||
end-txt := Text {
|
||||
x: timeline-event.width - self.width;
|
||||
|
|
@ -123,7 +97,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: Palette.event-text[event.color-id];
|
||||
color: event.text-color;
|
||||
}
|
||||
}
|
||||
@children
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue