Compare commits
195 Commits
falling-sa
...
main
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 911bc97b69 | |
Savanni D'Gerinel | 019d9e7a6b | |
Savanni D'Gerinel | 8235ef0646 | |
Savanni D'Gerinel | dd861fbbd4 | |
Savanni D'Gerinel | 427c5d2a72 | |
Savanni D'Gerinel | 39391fb2fe | |
Savanni D'Gerinel | 99573ff7cf | |
Savanni D'Gerinel | 5ed39f814a | |
Savanni D'Gerinel | 82ec50f519 | |
Savanni D'Gerinel | 1601d2d806 | |
savanni | 3e297a5986 | |
Savanni D'Gerinel | b0383292fe | |
Savanni D'Gerinel | a0f037c9cd | |
Savanni D'Gerinel | 8e63e5210c | |
Savanni D'Gerinel | db34e69cdf | |
Savanni D'Gerinel | 20623284ed | |
Savanni D'Gerinel | 5d04c84437 | |
Savanni D'Gerinel | 6e26740a40 | |
Savanni D'Gerinel | a56c0d141c | |
Savanni D'Gerinel | 1bc146beaf | |
Savanni D'Gerinel | bb08064b9a | |
Savanni D'Gerinel | f226a83cf6 | |
Savanni D'Gerinel | fc70bb3955 | |
Savanni D'Gerinel | 7b50a71369 | |
Savanni D'Gerinel | 7a7548c78f | |
Savanni D'Gerinel | 9c56e988b2 | |
Savanni D'Gerinel | de35ebb644 | |
Savanni D'Gerinel | 791f2be3c5 | |
Savanni D'Gerinel | 74b7f1c6f7 | |
Savanni D'Gerinel | 9c490a84a4 | |
Savanni D'Gerinel | 724cc1a3f0 | |
Savanni D'Gerinel | 8f71760604 | |
Savanni D'Gerinel | 11abde345e | |
Savanni D'Gerinel | a5b76c8171 | |
Savanni D'Gerinel | 9b23dd5acd | |
Savanni D'Gerinel | 54225ca729 | |
Savanni D'Gerinel | 95b46de7fc | |
Savanni D'Gerinel | caaf9c57c6 | |
Savanni D'Gerinel | 81d452694d | |
Savanni D'Gerinel | 88cf32047b | |
Savanni D'Gerinel | 6cae7dbb0e | |
Savanni D'Gerinel | 80776c65d8 | |
Savanni D'Gerinel | 1c54e0832b | |
Savanni D'Gerinel | aee4528fb3 | |
Savanni D'Gerinel | 0535b6da5a | |
Savanni D'Gerinel | b55324feab | |
Savanni D'Gerinel | 50d8a9670e | |
Savanni D'Gerinel | 9cda35e766 | |
Savanni D'Gerinel | d0f461a5eb | |
Savanni D'Gerinel | 70c013218a | |
Savanni D'Gerinel | 37c7e04820 | |
Savanni D'Gerinel | 291663d4a3 | |
Savanni D'Gerinel | 2b0fc7639e | |
Savanni D'Gerinel | 80d8dedbaf | |
Savanni D'Gerinel | d7a70119c8 | |
Savanni D'Gerinel | 54c4b99ab6 | |
Savanni D'Gerinel | ef5415303b | |
Savanni D'Gerinel | 8d183d6d8c | |
Savanni D'Gerinel | 0b949111d2 | |
Savanni D'Gerinel | 6164cb3b39 | |
Savanni D'Gerinel | 22f0f9061c | |
Savanni D'Gerinel | 0bb5e62f96 | |
Savanni D'Gerinel | 06aedc34bb | |
Savanni D'Gerinel | 84b077e20c | |
Savanni D'Gerinel | fc2e88add2 | |
Savanni D'Gerinel | 15c4ae9bad | |
Savanni D'Gerinel | 7dd531b493 | |
Savanni D'Gerinel | cbfb3f2e37 | |
Savanni D'Gerinel | 9540a2c5bb | |
Savanni D'Gerinel | 6165d65977 | |
Savanni D'Gerinel | 4f8a1636c1 | |
Savanni D'Gerinel | 20b02fbd90 | |
Savanni D'Gerinel | 278ec27b4e | |
Savanni D'Gerinel | 8b7add37c1 | |
Savanni D'Gerinel | 5441a3c441 | |
Savanni D'Gerinel | b1374229f3 | |
Savanni D'Gerinel | bc5042c004 | |
Savanni D'Gerinel | 0534143d6b | |
Savanni D'Gerinel | d7f5269e15 | |
Savanni D'Gerinel | c913e9da37 | |
Savanni D'Gerinel | c50bd652f1 | |
Savanni D'Gerinel | 093e1f7f8a | |
Savanni D'Gerinel | 3c94f906a6 | |
Savanni D'Gerinel | 0aecaee760 | |
Savanni D'Gerinel | baeb458126 | |
Savanni D'Gerinel | da144a58ec | |
Savanni D'Gerinel | f09af67193 | |
Savanni D'Gerinel | 32391a46e7 | |
Savanni D'Gerinel | 0a62c96b0f | |
Savanni D'Gerinel | 78863ee709 | |
Savanni D'Gerinel | 5cdcf0499c | |
Savanni D'Gerinel | b982f2c1cc | |
Savanni D'Gerinel | 46b25cc6c5 | |
Savanni D'Gerinel | 9fbc630500 | |
Savanni D'Gerinel | b481d5d058 | |
Savanni D'Gerinel | 7a06b8cf39 | |
Savanni D'Gerinel | 3192c0a142 | |
Savanni D'Gerinel | acf7ca0c9a | |
Savanni D'Gerinel | 64138b9e90 | |
Savanni D'Gerinel | e587d269e9 | |
Savanni D'Gerinel | 57aadd7597 | |
Savanni D'Gerinel | b70d927eac | |
Savanni D'Gerinel | 3a7f204883 | |
Savanni D'Gerinel | 642351f248 | |
Savanni D'Gerinel | d9bb9d92e5 | |
Savanni D'Gerinel | 30e7bdb817 | |
Savanni D'Gerinel | 556f91b70b | |
Savanni D'Gerinel | 894575b0fb | |
Savanni D'Gerinel | d964ab0d2f | |
Savanni D'Gerinel | 74c8eb6861 | |
Savanni D'Gerinel | 3aac3b8393 | |
Savanni D'Gerinel | 295f0a0411 | |
Savanni D'Gerinel | e694ba74ca | |
Savanni D'Gerinel | 5f9cd2622a | |
Savanni D'Gerinel | 48271389ad | |
Savanni D'Gerinel | 49571b0f82 | |
Savanni D'Gerinel | 89a289a1ae | |
Savanni D'Gerinel | fe082773e3 | |
Savanni D'Gerinel | db9efbaedd | |
Savanni D'Gerinel | 1d959117aa | |
Savanni D'Gerinel | a5990a2a30 | |
Savanni D'Gerinel | bd6d5b62e3 | |
Savanni D'Gerinel | 82c1765513 | |
Savanni D'Gerinel | de54ec676f | |
Savanni D'Gerinel | 5612c89a61 | |
Savanni D'Gerinel | c3c144e035 | |
Savanni D'Gerinel | 05a6dcf3af | |
Savanni D'Gerinel | b98e0bdcea | |
Savanni D'Gerinel | 3d4a298dc1 | |
Savanni D'Gerinel | 2d7fbb9a4b | |
Savanni D'Gerinel | 3a5cb17e09 | |
Savanni D'Gerinel | f6c82cbcb0 | |
Savanni D'Gerinel | 74b00d94b1 | |
Savanni D'Gerinel | ddf83b3018 | |
Savanni D'Gerinel | a1f41a440f | |
Savanni D'Gerinel | e838f601ca | |
Savanni D'Gerinel | 1b0a90a332 | |
Savanni D'Gerinel | 4dc6e3151b | |
Savanni D'Gerinel | 79cec6e21d | |
Savanni D'Gerinel | 32cc74edfa | |
Savanni D'Gerinel | d3d3260091 | |
Savanni D'Gerinel | a1441f7bb1 | |
Savanni D'Gerinel | 9e7350b087 | |
Savanni D'Gerinel | 0032f16422 | |
Savanni D'Gerinel | a5d51dab70 | |
Savanni D'Gerinel | c24a5f515f | |
Savanni D'Gerinel | d70ca08db2 | |
Savanni D'Gerinel | 55b6327d42 | |
Savanni D'Gerinel | 1c2f40c868 | |
Savanni D'Gerinel | 1527942f9c | |
Savanni D'Gerinel | 7ba758b325 | |
Savanni D'Gerinel | aed4735209 | |
Savanni D'Gerinel | c14b20b79e | |
Savanni D'Gerinel | 56ff5527ba | |
Savanni D'Gerinel | f7f55d74fd | |
Savanni D'Gerinel | 843924afef | |
Savanni D'Gerinel | 86d7ca0b01 | |
Savanni D'Gerinel | d137ee2481 | |
Savanni D'Gerinel | 74dbb58ed9 | |
Savanni D'Gerinel | 9a18496c95 | |
Savanni D'Gerinel | 01776a534e | |
Savanni D'Gerinel | b33d17c256 | |
Savanni D'Gerinel | ab59eedef5 | |
Savanni D'Gerinel | 4dd6afeae7 | |
Savanni D'Gerinel | cb2bec4287 | |
Savanni D'Gerinel | 5a93c4fdcd | |
Savanni D'Gerinel | 6e5cbc0930 | |
Savanni D'Gerinel | 73a5ab89a3 | |
Savanni D'Gerinel | 291dc32fe5 | |
Savanni D'Gerinel | b5c42e3ac3 | |
Savanni D'Gerinel | 6394d89331 | |
Savanni D'Gerinel | 38b1e62b60 | |
Savanni D'Gerinel | 4acf034b8d | |
Savanni D'Gerinel | 1aff203afc | |
Savanni D'Gerinel | 9fc9d2b758 | |
Savanni D'Gerinel | 76f4b31466 | |
Savanni D'Gerinel | 73052a0694 | |
Savanni D'Gerinel | 2c42c35dfe | |
Savanni D'Gerinel | afe693fe10 | |
Savanni D'Gerinel | af1422d523 | |
Savanni D'Gerinel | 792e20d44b | |
Savanni D'Gerinel | 8016188b29 | |
Savanni D'Gerinel | 74df2880bb | |
Savanni D'Gerinel | 96c4201680 | |
Savanni D'Gerinel | ecdd38ebbc | |
Savanni D'Gerinel | 2fb8728856 | |
Savanni D'Gerinel | a7d43ef184 | |
Savanni D'Gerinel | 9727d35116 | |
Savanni D'Gerinel | 1d6155d9e5 | |
Savanni D'Gerinel | a8bf540517 | |
Savanni D'Gerinel | 3db870d790 | |
Savanni D'Gerinel | 24276d172b | |
Savanni D'Gerinel | 96317f5692 | |
Savanni D'Gerinel | c1e797f3ae | |
Savanni D'Gerinel | 304008c674 |
12
Cargo.toml
|
@ -2,10 +2,15 @@
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"authdb",
|
"authdb",
|
||||||
|
# "bike-lights/bike",
|
||||||
|
"bike-lights/core",
|
||||||
|
"bike-lights/simulator",
|
||||||
"changeset",
|
"changeset",
|
||||||
"config",
|
"config",
|
||||||
"config-derive",
|
"config-derive",
|
||||||
"coordinates",
|
"coordinates",
|
||||||
|
"cyberpunk",
|
||||||
|
"cyber-slides",
|
||||||
"cyberpunk-splash",
|
"cyberpunk-splash",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"emseries",
|
"emseries",
|
||||||
|
@ -16,15 +21,16 @@ members = [
|
||||||
"geo-types",
|
"geo-types",
|
||||||
"gm-control-panel",
|
"gm-control-panel",
|
||||||
"hex-grid",
|
"hex-grid",
|
||||||
|
"icon-test",
|
||||||
"ifc",
|
"ifc",
|
||||||
"kifu/core",
|
|
||||||
"kifu/gtk",
|
|
||||||
"memorycache",
|
"memorycache",
|
||||||
"nom-training",
|
"nom-training",
|
||||||
|
"otg/core",
|
||||||
|
"otg/gtk",
|
||||||
"result-extended",
|
"result-extended",
|
||||||
"screenplay",
|
"screenplay",
|
||||||
"sgf",
|
"sgf",
|
||||||
"timezone-testing",
|
"timezone-testing",
|
||||||
"tree",
|
"tree",
|
||||||
"visions/server",
|
"visions/server", "gm-dash/server", "halloween-leds"
|
||||||
]
|
]
|
||||||
|
|
30
Makefile
|
@ -1,30 +0,0 @@
|
||||||
|
|
||||||
all: test bin
|
|
||||||
|
|
||||||
test: kifu-core/test-oneshot sgf/test-oneshot
|
|
||||||
|
|
||||||
bin: kifu-gtk
|
|
||||||
|
|
||||||
kifu-core/dev:
|
|
||||||
cd kifu/core && make test
|
|
||||||
|
|
||||||
kifu-core/test:
|
|
||||||
cd kifu/core && make test
|
|
||||||
|
|
||||||
kifu-core/test-oneshot:
|
|
||||||
cd kifu/core && make test-oneshot
|
|
||||||
|
|
||||||
kifu-gtk:
|
|
||||||
cd kifu/gtk && make release
|
|
||||||
|
|
||||||
kifu-gtk/dev:
|
|
||||||
cd kifu/gtk && make dev
|
|
||||||
|
|
||||||
kifu-pwa:
|
|
||||||
cd kifu/pwa && make release
|
|
||||||
|
|
||||||
kifu-pwa/dev:
|
|
||||||
pushd kifu/pwa && make dev
|
|
||||||
|
|
||||||
kifu-pwa/server:
|
|
||||||
pushd kifu/pwa && make server
|
|
|
@ -19,6 +19,8 @@ clap = { version = "4", features = [ "derive" ] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha2 = { version = "0.10" }
|
sha2 = { version = "0.10" }
|
||||||
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite" ] }
|
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite" ] }
|
||||||
|
# sqlformat introduced a mistaken breaking change in 0.2.7
|
||||||
|
sqlformat = { version = "=0.2.6" }
|
||||||
thiserror = { version = "1" }
|
thiserror = { version = "1" }
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
[build]
|
||||||
|
target = "thumbv6m-none-eabi"
|
||||||
|
|
||||||
|
[target.thumbv6m-none-eabi]
|
||||||
|
rustflags = [
|
||||||
|
"-C", "link-arg=--nmagic",
|
||||||
|
"-C", "link-arg=-Tlink.x",
|
||||||
|
"-C", "inline-threshold=5",
|
||||||
|
"-C", "no-vectorize-loops",
|
||||||
|
]
|
||||||
|
|
||||||
|
runner = "elf2uf2-rs -d"
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "bike"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
az = { version = "1" }
|
||||||
|
cortex-m-rt = { version = "0.7.3" }
|
||||||
|
cortex-m = { version = "0.7.7" }
|
||||||
|
embedded-alloc = { version = "0.5.1" }
|
||||||
|
embedded-hal = { version = "0.2.7" }
|
||||||
|
fixed = { version = "1" }
|
||||||
|
fugit = { version = "0.3.7" }
|
||||||
|
lights-core = { path = "../core" }
|
||||||
|
panic-halt = { version = "0.2.0" }
|
||||||
|
rp-pico = { version = "0.8.0" }
|
|
@ -0,0 +1,244 @@
|
||||||
|
#![no_main]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
use az::*;
|
||||||
|
use core::cell::RefCell;
|
||||||
|
use cortex_m::delay::Delay;
|
||||||
|
use embedded_alloc::Heap;
|
||||||
|
use embedded_hal::{blocking::spi::Write, digital::v2::InputPin, digital::v2::OutputPin};
|
||||||
|
use fixed::types::I16F16;
|
||||||
|
use fugit::RateExtU32;
|
||||||
|
use lights_core::{App, BodyPattern, DashboardPattern, Event, Instant, FPS, UI};
|
||||||
|
use panic_halt as _;
|
||||||
|
use rp_pico::{
|
||||||
|
entry,
|
||||||
|
hal::{
|
||||||
|
clocks::init_clocks_and_plls,
|
||||||
|
gpio::{FunctionSio, Pin, PinId, PullDown, PullUp, SioInput, SioOutput},
|
||||||
|
pac::{CorePeripherals, Peripherals},
|
||||||
|
spi::{Enabled, Spi, SpiDevice, ValidSpiPinout},
|
||||||
|
watchdog::Watchdog,
|
||||||
|
Clock, Sio,
|
||||||
|
},
|
||||||
|
Pins,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static HEAP: Heap = Heap::empty();
|
||||||
|
|
||||||
|
const LIGHT_SCALE: I16F16 = I16F16::lit("256.0");
|
||||||
|
const DASHBOARD_BRIGHTESS: u8 = 1;
|
||||||
|
const BODY_BRIGHTNESS: u8 = 8;
|
||||||
|
|
||||||
|
struct DebouncedButton<P: PinId> {
|
||||||
|
debounce: Instant,
|
||||||
|
pin: Pin<P, FunctionSio<SioInput>, PullUp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: PinId> DebouncedButton<P> {
|
||||||
|
fn new(pin: Pin<P, FunctionSio<SioInput>, PullUp>) -> Self {
|
||||||
|
Self {
|
||||||
|
debounce: Instant((0 as u32).into()),
|
||||||
|
pin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_low(&self, time: Instant) -> bool {
|
||||||
|
if time <= self.debounce {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.pin.is_low().unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_debounce(&mut self, time: Instant) {
|
||||||
|
self.debounce = time + Instant((250 as u32).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BikeUI<
|
||||||
|
D: SpiDevice,
|
||||||
|
P: ValidSpiPinout<D>,
|
||||||
|
LeftId: PinId,
|
||||||
|
RightId: PinId,
|
||||||
|
PreviousId: PinId,
|
||||||
|
NextId: PinId,
|
||||||
|
BrakeId: PinId,
|
||||||
|
> {
|
||||||
|
spi: RefCell<Spi<Enabled, D, P, 8>>,
|
||||||
|
left_blinker_button: DebouncedButton<LeftId>,
|
||||||
|
right_blinker_button: DebouncedButton<RightId>,
|
||||||
|
previous_animation_button: DebouncedButton<PreviousId>,
|
||||||
|
next_animation_button: DebouncedButton<NextId>,
|
||||||
|
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
|
||||||
|
brake_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
D: SpiDevice,
|
||||||
|
P: ValidSpiPinout<D>,
|
||||||
|
LeftId: PinId,
|
||||||
|
RightId: PinId,
|
||||||
|
PreviousId: PinId,
|
||||||
|
NextId: PinId,
|
||||||
|
BrakeId: PinId,
|
||||||
|
> BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
|
||||||
|
{
|
||||||
|
fn new(
|
||||||
|
spi: Spi<Enabled, D, P, 8>,
|
||||||
|
left_blinker_button: Pin<LeftId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
right_blinker_button: Pin<RightId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
previous_animation_button: Pin<PreviousId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
next_animation_button: Pin<NextId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
spi: RefCell::new(spi),
|
||||||
|
left_blinker_button: DebouncedButton::new(left_blinker_button),
|
||||||
|
right_blinker_button: DebouncedButton::new(right_blinker_button),
|
||||||
|
previous_animation_button: DebouncedButton::new(previous_animation_button),
|
||||||
|
next_animation_button: DebouncedButton::new(next_animation_button),
|
||||||
|
brake_sensor,
|
||||||
|
|
||||||
|
brake_enabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
D: SpiDevice,
|
||||||
|
P: ValidSpiPinout<D>,
|
||||||
|
LeftId: PinId,
|
||||||
|
RightId: PinId,
|
||||||
|
PreviousId: PinId,
|
||||||
|
NextId: PinId,
|
||||||
|
BrakeId: PinId,
|
||||||
|
> UI for BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
|
||||||
|
{
|
||||||
|
fn check_event(&mut self, current_time: Instant) -> Option<Event> {
|
||||||
|
/*
|
||||||
|
if self.brake_sensor.is_high().unwrap_or(true) && !self.brake_enabled {
|
||||||
|
self.brake_enabled = true;
|
||||||
|
Some(Event::Brake)
|
||||||
|
} else if self.brake_sensor.is_low().unwrap_or(false) && self.brake_enabled {
|
||||||
|
self.brake_enabled = false;
|
||||||
|
Some(Event::BrakeRelease)
|
||||||
|
} else if self.left_blinker_button.is_low(current_time) {
|
||||||
|
*/
|
||||||
|
if self.left_blinker_button.is_low(current_time) {
|
||||||
|
self.left_blinker_button.set_debounce(current_time);
|
||||||
|
Some(Event::LeftBlinker)
|
||||||
|
} else if self.right_blinker_button.is_low(current_time) {
|
||||||
|
self.right_blinker_button.set_debounce(current_time);
|
||||||
|
Some(Event::RightBlinker)
|
||||||
|
} else if self.previous_animation_button.is_low(current_time) {
|
||||||
|
self.previous_animation_button.set_debounce(current_time);
|
||||||
|
Some(Event::PreviousPattern)
|
||||||
|
} else if self.next_animation_button.is_low(current_time) {
|
||||||
|
self.next_animation_button.set_debounce(current_time);
|
||||||
|
Some(Event::NextPattern)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern) {
|
||||||
|
let mut lights: [u8; 260] = [0; 260];
|
||||||
|
lights[256] = 0xff;
|
||||||
|
lights[257] = 0xff;
|
||||||
|
lights[258] = 0xff;
|
||||||
|
lights[259] = 0xff;
|
||||||
|
for (idx, rgb) in dashboard_lights.iter().enumerate() {
|
||||||
|
lights[(idx + 1) * 4 + 0] = 0xe0 + DASHBOARD_BRIGHTESS;
|
||||||
|
lights[(idx + 1) * 4 + 1] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 1) * 4 + 2] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 1) * 4 + 3] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
|
||||||
|
}
|
||||||
|
for (idx, rgb) in body_lights.iter().enumerate() {
|
||||||
|
lights[(idx + 4) * 4 + 0] = 0xe0 + BODY_BRIGHTNESS;
|
||||||
|
lights[(idx + 4) * 4 + 1] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 4) * 4 + 2] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 4) * 4 + 3] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
|
||||||
|
}
|
||||||
|
let mut spi = self.spi.borrow_mut();
|
||||||
|
spi.write(lights.as_slice());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[entry]
|
||||||
|
fn main() -> ! {
|
||||||
|
{
|
||||||
|
use core::mem::MaybeUninit;
|
||||||
|
const HEAP_SIZE: usize = 8096;
|
||||||
|
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||||
|
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pac = Peripherals::take().unwrap();
|
||||||
|
let core = CorePeripherals::take().unwrap();
|
||||||
|
let sio = Sio::new(pac.SIO);
|
||||||
|
let mut watchdog = Watchdog::new(pac.WATCHDOG);
|
||||||
|
|
||||||
|
let pins = Pins::new(
|
||||||
|
pac.IO_BANK0,
|
||||||
|
pac.PADS_BANK0,
|
||||||
|
sio.gpio_bank0,
|
||||||
|
&mut pac.RESETS,
|
||||||
|
);
|
||||||
|
|
||||||
|
let clocks = init_clocks_and_plls(
|
||||||
|
12_000_000u32,
|
||||||
|
pac.XOSC,
|
||||||
|
pac.CLOCKS,
|
||||||
|
pac.PLL_SYS,
|
||||||
|
pac.PLL_USB,
|
||||||
|
&mut pac.RESETS,
|
||||||
|
&mut watchdog,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
|
||||||
|
let mut spi_clk = pins.gpio10.into_function();
|
||||||
|
let mut spi_sdo = pins.gpio11.into_function();
|
||||||
|
let spi = Spi::<_, _, _, 8>::new(pac.SPI1, (spi_sdo, spi_clk));
|
||||||
|
let mut spi = spi.init(
|
||||||
|
&mut pac.RESETS,
|
||||||
|
clocks.peripheral_clock.freq(),
|
||||||
|
1_u32.MHz(),
|
||||||
|
embedded_hal::spi::MODE_1,
|
||||||
|
);
|
||||||
|
|
||||||
|
let left_blinker_button = pins.gpio16.into_pull_up_input();
|
||||||
|
let right_blinker_button = pins.gpio17.into_pull_up_input();
|
||||||
|
let previous_animation_button = pins.gpio27.into_pull_up_input();
|
||||||
|
let next_animation_button = pins.gpio26.into_pull_up_input();
|
||||||
|
let brake_sensor = pins.gpio18.into_pull_up_input();
|
||||||
|
|
||||||
|
let mut led_pin = pins.led.into_push_pull_output();
|
||||||
|
|
||||||
|
let ui = BikeUI::new(
|
||||||
|
spi,
|
||||||
|
left_blinker_button,
|
||||||
|
right_blinker_button,
|
||||||
|
previous_animation_button,
|
||||||
|
next_animation_button,
|
||||||
|
brake_sensor,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut app = App::new(Box::new(ui));
|
||||||
|
|
||||||
|
led_pin.set_high();
|
||||||
|
|
||||||
|
let mut time = Instant::default();
|
||||||
|
let delay_ms = 1000 / (FPS as u32);
|
||||||
|
loop {
|
||||||
|
app.tick(time);
|
||||||
|
|
||||||
|
delay.delay_ms(delay_ms);
|
||||||
|
time = time + Instant(delay_ms.into());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
$fn = 50;
|
||||||
|
threshold = 0.1;
|
||||||
|
half_threshold = threshold / 2;
|
||||||
|
bevel = 0.5;
|
||||||
|
|
||||||
|
wire_radius = 1;
|
||||||
|
|
||||||
|
wall_thickness = 2;
|
||||||
|
cutout_threshold = 1;
|
||||||
|
|
||||||
|
battery_length = 71;
|
||||||
|
battery_width = 18.75;
|
||||||
|
|
||||||
|
cell_holder_length = battery_length + wall_thickness * 2;
|
||||||
|
cell_holder_width = battery_width + wall_thickness * 2;
|
||||||
|
cell_holder_height = battery_width + wall_thickness;
|
||||||
|
|
||||||
|
battery_contact_thickness = .6;
|
||||||
|
// battery_contact_thickness = 1;
|
||||||
|
battery_contact_width = 11;
|
||||||
|
battery_contact_length = 12.8;
|
||||||
|
battery_contact_spring_height = 10.5;
|
||||||
|
battery_contact_flange_height = 1.9;
|
||||||
|
|
||||||
|
converter_width = 11.25;
|
||||||
|
converter_length = 22.25;
|
||||||
|
converter_height = 5;
|
||||||
|
|
||||||
|
|
||||||
|
include <./common.scad>;
|
||||||
|
|
||||||
|
// box(20, 10, 10);
|
||||||
|
// color("blue", 0.5) cube([10, 20, 10], center = true);
|
||||||
|
|
||||||
|
module cell_cradle(width, height) {
|
||||||
|
difference() {
|
||||||
|
translate([0, 0, -height / 2]) cube([width,
|
||||||
|
wall_thickness,
|
||||||
|
height],
|
||||||
|
center = true);
|
||||||
|
color("red", 1) translate([0, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness + cutout_threshold,
|
||||||
|
r = width / 2,
|
||||||
|
center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module cell_box() {
|
||||||
|
union() {
|
||||||
|
channel(cell_holder_length, cell_holder_width, cell_holder_height);
|
||||||
|
translate([0, -battery_length / 6, wall_thickness]) cell_cradle(cell_holder_width, cell_holder_height / 2);
|
||||||
|
translate([0, battery_length / 6, wall_thickness]) cell_cradle(cell_holder_width, cell_holder_height / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module contact_box() {
|
||||||
|
contact_thickness = battery_contact_flange_height * .75;
|
||||||
|
cutout_width = battery_contact_width * .8;
|
||||||
|
// box_thickness = contact_thickness_ + wall_thickness * 2;
|
||||||
|
// box_height = width + wall_thickness;
|
||||||
|
|
||||||
|
difference() {
|
||||||
|
box(wall_thickness * 2 + contact_thickness, cell_holder_width, cell_holder_height);
|
||||||
|
translate([0, contact_thickness, wall_thickness * 2])
|
||||||
|
cube([battery_contact_width,
|
||||||
|
wall_thickness * 2,
|
||||||
|
battery_contact_length + threshold],
|
||||||
|
center = true);
|
||||||
|
|
||||||
|
color("red", 1) translate([0,
|
||||||
|
-(wall_thickness + contact_thickness + threshold) / 2,
|
||||||
|
cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
|
||||||
|
translate([0,
|
||||||
|
-(wall_thickness + contact_thickness + threshold) / 2 - wire_radius,
|
||||||
|
0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = cell_holder_width, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
color("green", 1) translate([-cell_holder_width / 2, 0, cell_holder_height / 2])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = 5, r = contact_thickness / 2, center = true);
|
||||||
|
|
||||||
|
color("green", 1) translate([cell_holder_width / 2, 0, cell_holder_height / 2])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = 5, r = contact_thickness / 2, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module battery_slot() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
translate([0, -cell_holder_length / 2, 0]) contact_box();
|
||||||
|
translate([0, wall_thickness, 0]) cell_box();
|
||||||
|
translate([0, cell_holder_length / 2 + wall_thickness * 2, 0])
|
||||||
|
rotate([0, 0, 180])
|
||||||
|
contact_box();
|
||||||
|
}
|
||||||
|
translate([cell_holder_width / 2, 1, 0]) rotate([90, 0, 0]) cylinder(h = cell_holder_length + wall_thickness * 4 + battery_contact_flange_height * 2, r = wire_radius, center = true);
|
||||||
|
translate([-cell_holder_width / 2, 1, 0]) rotate([90, 0, 0]) cylinder(h = cell_holder_length + wall_thickness * 4 + battery_contact_flange_height * 2, r = wire_radius, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module converter_box() {
|
||||||
|
box_length = wall_thickness * 2 + converter_height;
|
||||||
|
box_width = cell_holder_width * 2 - wall_thickness;
|
||||||
|
difference() {
|
||||||
|
box(box_length, box_width, cell_holder_height);
|
||||||
|
|
||||||
|
translate([cell_holder_width - wire_radius, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = box_length, r = wire_radius, center = true);
|
||||||
|
translate([cell_holder_width - wire_radius * 2, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = wall_thickness + threshold, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([-cell_holder_width + wire_radius, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = box_length, r = wire_radius, center = true);
|
||||||
|
translate([-cell_holder_width + wire_radius * 2, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = wall_thickness + threshold, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([0, -box_length / 2, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = cell_holder_width * 2 + wall_thickness, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([-cell_holder_width * .75, (-box_length + wall_thickness) / 2, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness * 2, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([cell_holder_width * .75, (-box_length + wall_thickness) / 2, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness * 2, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
color("red", 1) translate([-box_width / 4, -(converter_height + wall_thickness) / 2, cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
color("red", 1) translate([box_width / 4, -(converter_height + wall_thickness) / 2, cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module battery_case() {
|
||||||
|
union() {
|
||||||
|
translate([-cell_holder_width / 2, 0, 0]) battery_slot();
|
||||||
|
translate([cell_holder_width / 2 - wall_thickness, 0, 0]) battery_slot();
|
||||||
|
translate([-wall_thickness / 2,
|
||||||
|
cell_holder_length / 2 + wall_thickness * 2 + battery_contact_flange_height + wall_thickness * 2 + wall_thickness / 2,
|
||||||
|
0])
|
||||||
|
converter_box();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
battery_case();
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
width = 65;
|
||||||
|
length = 75;
|
||||||
|
height = 16;
|
||||||
|
wall_thickness = 2;
|
||||||
|
guide_thickness = 1;
|
||||||
|
power_width = 21;
|
||||||
|
output_width = 37.5;
|
||||||
|
half_wall_thickness = wall_thickness / 2;
|
||||||
|
standoff_thickness = 10;
|
||||||
|
hole_diameter = 3;
|
||||||
|
// The radius of a nut in mm. However, based on my measurements, I'm not actually sure I have this right. The short height of a nut is 7.86mm. Derive from there.
|
||||||
|
nut_radius = 8.5 * cos(30) / 2;
|
||||||
|
nut_height = 2.69; // mm
|
||||||
|
screw_radius = 2;
|
||||||
|
handlebar_radius = 15;
|
||||||
|
clasp_thickness = 4;
|
||||||
|
clasp_width = 35;
|
||||||
|
circular_face_count = 48;
|
||||||
|
|
||||||
|
module hexagon(r, h) {
|
||||||
|
pi = 3.1415926;
|
||||||
|
polyhedron(
|
||||||
|
points=[
|
||||||
|
[r, 0, 0],
|
||||||
|
[r * cos(60), r * sin(60), 0],
|
||||||
|
[r * cos(120), r * sin(120), 0],
|
||||||
|
[r * cos(180), r * sin(180), 0],
|
||||||
|
[r * cos(240), r * sin(240), 0],
|
||||||
|
[r * cos(300), r * sin(300), 0],
|
||||||
|
|
||||||
|
[r, 0, h],
|
||||||
|
[r * cos(60), r * sin(60), h],
|
||||||
|
[r * cos(120), r * sin(120), h],
|
||||||
|
[r * cos(180), r * sin(180), h],
|
||||||
|
[r * cos(240), r * sin(240), h],
|
||||||
|
[r * cos(300), r * sin(300), h],
|
||||||
|
],
|
||||||
|
faces=[
|
||||||
|
[0, 1, 2, 3, 4, 5],
|
||||||
|
[11, 10, 9, 8, 7, 6],
|
||||||
|
[6, 7, 1, 0],
|
||||||
|
[7, 8, 2, 1],
|
||||||
|
[8, 9, 3, 2],
|
||||||
|
[9, 10, 4, 3],
|
||||||
|
[10, 11, 5, 4],
|
||||||
|
[11, 6, 0, 5],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nut holders are blocks that have a hole drilled through them and a hexagonal-shaped cavity. The idea is to
|
||||||
|
module nut_holder() {
|
||||||
|
difference() {
|
||||||
|
translate([-4.5, -4.5, -2]) cube([9, 9, 4]);
|
||||||
|
union() {
|
||||||
|
translate([0, 0, -1]) hexagon(nut_radius, 2);
|
||||||
|
cylinder(h = 6, r = screw_radius, center = true, $fn = 24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module screw_hole() {
|
||||||
|
union() {
|
||||||
|
translate([0, 0, 4]) cylinder(h = 2.1, r = screw_radius * 2, center = true, $fn = 24);
|
||||||
|
cylinder(h = 6, r = screw_radius, center = true, $fn = 24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module base() {
|
||||||
|
cube([width, length, wall_thickness]);
|
||||||
|
}
|
||||||
|
|
||||||
|
module face() {
|
||||||
|
union() {
|
||||||
|
cube([width, length, wall_thickness / 2]);
|
||||||
|
translate([wall_thickness, wall_thickness, wall_thickness / 2]) cube([width-wall_thickness*2, length-wall_thickness*2, wall_thickness / 2]);
|
||||||
|
translate([4.5 + wall_thickness, 4.5 + wall_thickness, 4]) nut_holder();
|
||||||
|
translate([width - 4.5 - wall_thickness, 4.5 + wall_thickness, 4]) nut_holder();
|
||||||
|
translate([width - 4.5 - wall_thickness, length - 4.5 - wall_thickness, 4]) nut_holder();
|
||||||
|
translate([4.5 + wall_thickness, length - 4.5 - wall_thickness, 4]) nut_holder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module wall(length) {
|
||||||
|
cube([length, height, wall_thickness]);
|
||||||
|
}
|
||||||
|
|
||||||
|
module power_wall() {
|
||||||
|
difference() {
|
||||||
|
wall(65);
|
||||||
|
translate([9, 2, -.5]) cube([power_width, height, wall_thickness + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module output_wall() {
|
||||||
|
difference() {
|
||||||
|
wall(65);
|
||||||
|
translate([9, 2, -.5]) cube([output_width, height, wall_thickness + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use hexagons as cutouts into which I can install a hex nut. This isn't quite right yet, but close.
|
||||||
|
// hexagon(nut_radius, 1);
|
||||||
|
|
||||||
|
// cube([standoff_thickness, standoff_thickness, 2]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
base();
|
||||||
|
rotate([90, 0, 90]) wall(75);
|
||||||
|
// translate([width - wall_thickness, 0, 0]) rotate([90, 0, 90]) wall(length);
|
||||||
|
// rotate([90, 0, 0]) power_wall();
|
||||||
|
// translate([0, length, 0]) rotate([90, 0, 0]) output_wall();
|
||||||
|
// translate([wall_thickness,
|
||||||
|
// wall_thickness,
|
||||||
|
// wall_thickness]) standoff();
|
||||||
|
// translate([width - wall_thickness - standoff_thickness,
|
||||||
|
// wall_thickness,
|
||||||
|
// wall_thickness]) standoff();
|
||||||
|
// translate([wall_thickness,
|
||||||
|
// length - wall_thickness - standoff_thickness,
|
||||||
|
// wall_thickness]) standoff();
|
||||||
|
// translate([width - wall_thickness - standoff_thickness,
|
||||||
|
// length - wall_thickness - standoff_thickness,
|
||||||
|
// wall_thickness]) standoff();
|
||||||
|
}
|
||||||
|
// translate([-half_wall_thickness, -wall_thickness - half_wall_thickness, height - half_wall_thickness]) cube([wall_thickness, length + wall_thickness * 2, wall_thickness]);
|
||||||
|
// translate([width - half_wall_thickness, -wall_thickness - half_wall_thickness, height - half_wall_thickness]) cube([wall_thickness, length + wall_thickness * 2, wall_thickness]);
|
||||||
|
// translate([-half_wall_thickness, -half_wall_thickness, height - half_wall_thickness]) rotate([0, 0, 270]) cube([wall_thickness, width + wall_thickness * 2, wall_thickness]);
|
||||||
|
// translate([-half_wall_thickness, length + half_wall_thickness, height - half_wall_thickness]) rotate([0, 0, 270]) cube([wall_thickness, width + wall_thickness * 2, wall_thickness]);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
module box() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cube([width, length, wall_thickness * 2]);
|
||||||
|
translate([0, 0, wall_thickness]) rotate([90, 0, 90]) wall(length);
|
||||||
|
translate([width - wall_thickness, 0, wall_thickness]) rotate([90, 0, 90]) wall(length);
|
||||||
|
translate([0, wall_thickness, wall_thickness]) rotate([90, 0, 0]) wall(width);
|
||||||
|
translate([0, length, wall_thickness]) rotate([90, 0, 0]) wall(width);
|
||||||
|
}
|
||||||
|
translate([4.5 + wall_thickness, 4.5 + wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||||
|
translate([width - 4.5 - wall_thickness, 4.5 + wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||||
|
translate([width - 4.5 - wall_thickness, length - 4.5 - wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||||
|
translate([4.5 + wall_thickness, length - 4.5 - wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module top_clasp() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cylinder(h = clasp_width, r = handlebar_radius + clasp_thickness, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2 + 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2 - 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-handlebar_radius-5, -10, -clasp_width / 2 + 6]) cube([6, 20, clasp_width - 12]);
|
||||||
|
}
|
||||||
|
translate([-0.5, 0, 0]) cylinder(h = clasp_width+2, r = handlebar_radius + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-0.5, -handlebar_radius - 10, -clasp_width / 2 - 1]) cube([handlebar_radius + 10, handlebar_radius * 2 + 20, clasp_width + 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module body() {
|
||||||
|
union() {
|
||||||
|
box();
|
||||||
|
translate([width / 2, length / 2, -5 - handlebar_radius]) rotate([0, 90, 90]) top_clasp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body();
|
||||||
|
translate([width + 10, 0, 0]) face();
|
|
@ -0,0 +1,21 @@
|
||||||
|
handlebar_radius = 15;
|
||||||
|
clasp_thickness = 4;
|
||||||
|
circular_face_count = 48;
|
||||||
|
clasp_width = 35;
|
||||||
|
|
||||||
|
module top_clasp() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cylinder(h = clasp_width, r = handlebar_radius + clasp_thickness, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2 + 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2 - 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-handlebar_radius-5, -10, -clasp_width / 2 + 6]) cube([6, 20, clasp_width - 12]);
|
||||||
|
}
|
||||||
|
translate([-0.5, 0, 0]) cylinder(h = clasp_width+2, r = handlebar_radius + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-0.5, -handlebar_radius - 10, -clasp_width / 2 - 1]) cube([handlebar_radius + 10, handlebar_radius * 2 + 20, clasp_width + 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
top_clasp();
|
|
@ -0,0 +1,92 @@
|
||||||
|
|
||||||
|
module hexagon(r, h) {
|
||||||
|
cylinder(r = r, h = h, center = 2, $fn = 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
module pill(length, bevel) {
|
||||||
|
hull() {
|
||||||
|
translate([0, 0, (-length / 2) + bevel]) sphere(r = bevel);
|
||||||
|
translate([0, 0, (length / 2) - bevel]) sphere(r = bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module rounded_cube(dimensions, bevel = 0) {
|
||||||
|
x = dimensions[0];
|
||||||
|
y = dimensions[1];
|
||||||
|
z = dimensions[2];
|
||||||
|
|
||||||
|
if (bevel > 0) {
|
||||||
|
hull() {
|
||||||
|
translate([-x / 2 + bevel, -y / 2 + bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||||
|
translate([ x / 2 - bevel, -y / 2 + bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||||
|
translate([ x / 2 - bevel, y / 2 - bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||||
|
translate([-x / 2 + bevel, y / 2 - bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||||
|
translate([-x / 2 + bevel, -y / 2 + bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||||
|
translate([ x / 2 - bevel, -y / 2 + bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||||
|
translate([ x / 2 - bevel, y / 2 - bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||||
|
translate([-x / 2 + bevel, y / 2 - bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cube(dimensions, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box_face(dimensions, bevel = 0) {
|
||||||
|
x = dimensions[0];
|
||||||
|
y = dimensions[1];
|
||||||
|
z = dimensions[2];
|
||||||
|
|
||||||
|
if (bevel > 0) {
|
||||||
|
translate([0, 0, z / 2])
|
||||||
|
hull() {
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([x, 0, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([x, y, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([0, y, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cube(dimensions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module channel(length, width, height, bevel) {
|
||||||
|
union() {
|
||||||
|
box_face([length, width, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([0, wall_thickness - bevel, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
box_face([length, height, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([0, width + bevel, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
box_face([length, height, wall_thickness], bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box(length, width, height, bevel = 0) {
|
||||||
|
union() {
|
||||||
|
channel(length, width, height, bevel);
|
||||||
|
|
||||||
|
translate([-bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([width, height, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([length - wall_thickness + bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([width, height, wall_thickness], bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box_side_slider(length, width, height) {
|
||||||
|
difference() {
|
||||||
|
box_face([width - wall_thickness * 2 + 4, height, wall_thickness], bevel);
|
||||||
|
translate([-1, -1, 1]) cube([4-threshold, height+2, 4-threshold]);
|
||||||
|
color("red") translate([width - wall_thickness * 2 + 1, -1, 1]) cube([4-threshold, height+2, 4-threshold]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
$fn = 50;
|
||||||
|
threshold = 0.1;
|
||||||
|
|
||||||
|
board_length = 92;
|
||||||
|
board_width = 72;
|
||||||
|
board_height = 21.5;
|
||||||
|
wall_thickness = 4;
|
||||||
|
bevel = 0.5;
|
||||||
|
|
||||||
|
hinge_radius = 2.5;
|
||||||
|
|
||||||
|
case_width = board_width + wall_thickness * 2;
|
||||||
|
case_length = board_length + wall_thickness * 2;
|
||||||
|
case_height = board_height + wall_thickness;
|
||||||
|
|
||||||
|
handlebar_radius = 15;
|
||||||
|
clasp_thickness = 4;
|
||||||
|
circular_face_count = 48;
|
||||||
|
clasp_width = 35;
|
||||||
|
|
||||||
|
include <./common.scad>;
|
||||||
|
|
||||||
|
module top_clasp() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cylinder(h = clasp_width, r = handlebar_radius + clasp_thickness, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, -clasp_width / 2 + 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([0, 0, clasp_width / 2 - 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-handlebar_radius-5, -10, -clasp_width / 2 + 6]) cube([6, 20, clasp_width - 12]);
|
||||||
|
}
|
||||||
|
translate([-0.5, 0, 0]) cylinder(h = clasp_width+2, r = handlebar_radius + 1, center = true, $fn = circular_face_count);
|
||||||
|
translate([-0.5, -handlebar_radius - 10, -clasp_width / 2 - 1]) cube([handlebar_radius + 10, handlebar_radius * 2 + 20, clasp_width + 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module hinge(length) {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cube([hinge_radius * 2, length, hinge_radius], center = true);
|
||||||
|
translate([0, 0, -1.5]) rotate([90, 0, 0]) cylinder(h = length, r = hinge_radius, center = true);
|
||||||
|
}
|
||||||
|
translate([0, threshold / 2, -1.5]) rotate([90, 0, 0]) cylinder(h = length + threshold * 2, r = 1, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module base_case(length, width, height, bevel = 0) {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
channel(length + wall_thickness / 2, width, height, bevel);
|
||||||
|
|
||||||
|
translate([-bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([width, height, wall_thickness], bevel);
|
||||||
|
|
||||||
|
// These are the sleds at the bottom of the case that should hold the lower of the two boards down
|
||||||
|
color("blue") translate([0, wall_thickness - 2, wall_thickness + 4]) cube([length - 8, 4, wall_thickness / 2]);
|
||||||
|
color("blue") translate([wall_thickness - 2, wall_thickness - 4, wall_thickness + 4]) cube([4, width, wall_thickness / 2]);
|
||||||
|
color("blue") translate([length - 25, width - wall_thickness * 3 / 2, wall_thickness + 6]) cube([16, wall_thickness, wall_thickness / 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This makes an indent at the bottom to accomodate solder joins
|
||||||
|
translate([wall_thickness + 2, wall_thickness + 2, wall_thickness / 2]) cube([length, width - wall_thickness * 2 - 4, wall_thickness / 2 + threshold]);
|
||||||
|
|
||||||
|
// This creates a cutout that lets the power plug slide in better.
|
||||||
|
translate([wall_thickness, width - wall_thickness, wall_thickness]) cube([length, 2, 6]);
|
||||||
|
|
||||||
|
// These two put in the slots that should allow the fourth wall to be slotted into place.
|
||||||
|
color("red") translate([length - 1, wall_thickness - 2, 4]) cube([2, 2, height]);
|
||||||
|
color("red") translate([length - 1, width - wall_thickness, 4]) cube([2, 2, height]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module main_case() {
|
||||||
|
hinge_length = board_length / 4;
|
||||||
|
hinge_y_offset = board_width + wall_thickness + hinge_radius;
|
||||||
|
hinge_z_offset = board_height;
|
||||||
|
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
base_case(case_length,
|
||||||
|
case_width,
|
||||||
|
case_height,
|
||||||
|
bevel);
|
||||||
|
|
||||||
|
translate([-bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([case_width, case_height, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([0, -hinge_radius - bevel + threshold, hinge_z_offset + bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
hinge(case_length / 4);
|
||||||
|
|
||||||
|
translate([case_length - hinge_length, -hinge_radius - bevel + threshold, hinge_z_offset + bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
hinge(case_length / 4);
|
||||||
|
|
||||||
|
translate([43, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("lights", size = 3);
|
||||||
|
|
||||||
|
translate([67, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("left", size = 3);
|
||||||
|
|
||||||
|
translate([55, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("right", size = 3);
|
||||||
|
// translate([case_length / 2, case_width / 2, -20]) rotate([0, 90, 0]) top_clasp();
|
||||||
|
}
|
||||||
|
|
||||||
|
translate([case_length / 2, case_width / 2, -threshold]) hexagon(4.5, 6);
|
||||||
|
|
||||||
|
# translate([8.5 + wall_thickness, case_width - wall_thickness - threshold, wall_thickness])
|
||||||
|
# cube([60, wall_thickness * 2, 7]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lamp() {
|
||||||
|
union() {
|
||||||
|
translate([0, 0, -0.5]) cube([12.9 + threshold, 8, 4], center = true);
|
||||||
|
translate([0, 0, .88]) cube([5 + threshold, 5 + threshold, 1.56], center = true);
|
||||||
|
/*
|
||||||
|
translate([0, 0, -1.56]) cube([12.9, 7.6, wall_thickness], center = true);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module button() {
|
||||||
|
union() {
|
||||||
|
cube([3.5 + threshold, 6.1 + threshold, 4 + threshold], center = true);
|
||||||
|
translate([0, 0, -0.5]) cube([1.2, 7, 3 + threshold], center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid() {
|
||||||
|
lid_width = case_width + hinge_radius * 2 + wall_thickness;
|
||||||
|
hinge_length = case_length / 4;
|
||||||
|
union() {
|
||||||
|
difference() {
|
||||||
|
rounded_cube([case_length,
|
||||||
|
lid_width,
|
||||||
|
wall_thickness],
|
||||||
|
bevel);
|
||||||
|
translate([0, lid_width / 5, 0.4]) lamp();
|
||||||
|
translate([-15, lid_width / 5, 0.4]) lamp();
|
||||||
|
translate([15, lid_width / 5, 0.4]) lamp();
|
||||||
|
translate([-30, lid_width / 5, 0]) button();
|
||||||
|
translate([30, lid_width / 5, 0]) button();
|
||||||
|
|
||||||
|
translate([0, lid_width / 5, -2]) cube([20, 7, 3], center = true);
|
||||||
|
|
||||||
|
color("black") translate([-2, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("black") translate([-17, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("black") translate([13, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("black") translate([-30, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("black") translate([30, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("black") translate([0, 10, -2]) rotate([0, 90, 0]) cylinder(h = 62, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
|
||||||
|
color("red") translate([-33, 21, -2]) rotate([0, 90, 0]) cylinder(h = 5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("red") translate([-35, 13, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h = 18, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("red") translate([33, 21, -2]) rotate([0, 90, 0]) cylinder(h = 5, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("red") translate([35, 13, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h = 18, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
color("red") translate([0, 5, -2]) rotate([0, 90, 0]) cylinder(h = 70, r = 1, center = true, $fn = circular_face_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
translate([case_length / 2 - hinge_length / 2, lid_width / 2 - wall_thickness / 2 - 0.5, -wall_thickness / 2]) rotate([0, 0, 90]) hinge(hinge_length);
|
||||||
|
translate([-case_length / 2 + hinge_length / 2, lid_width / 2 - wall_thickness / 2 - 0.5, -wall_thickness / 2]) rotate([0, 0, 90]) hinge(hinge_length);
|
||||||
|
|
||||||
|
translate([0, -lid_width / 2 + bevel, -3]) rounded_cube([20, wall_thickness / 2, 10], bevel);
|
||||||
|
color("blue") translate([-9, -lid_width / 2 + 1.5, -6]) rotate([90, 0, 0]) rotate([0, 90, 0]) linear_extrude(18) circle(1, $fn = 3);
|
||||||
|
color("blue") translate([-9, -lid_width / 2 + 1.5, -7]) rotate([90, 0, 0]) rotate([0, 90, 0]) linear_extrude(18) circle(1, $fn = 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box_side() {
|
||||||
|
box_side_slider(case_length, case_width, case_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
module case_base() {
|
||||||
|
difference() {
|
||||||
|
rounded_cube([case_length, case_width, wall_thickness + 2], bevel = 0.5);
|
||||||
|
translate([wall_thickness, 0, 2]) rounded_cube([case_length + threshold, board_width + threshold, 2 + threshold]);
|
||||||
|
|
||||||
|
// These give a screw-hole in the center which will allow the clamp to be attached
|
||||||
|
translate([0, 0, -1]) hexagon(4.5, 2);
|
||||||
|
translate([0, 0, -wall_thickness / 2]) cylinder(r = 2, h = wall_thickness + threshold, center = true);
|
||||||
|
|
||||||
|
// and now a bit of an indentation to help the clip remain in place
|
||||||
|
translate([0, 0, -4.5]) cube([clasp_width + threshold, clasp_width + threshold, wall_thickness], center = true);
|
||||||
|
|
||||||
|
// here are some grooves along the edges that can be used to piece parts together
|
||||||
|
translate([wall_thickness / 2, case_width / 2 - wall_thickness / 2, wall_thickness / 2])
|
||||||
|
cube([board_length + wall_thickness, wall_thickness / 2, wall_thickness / 2 + threshold], center = true);
|
||||||
|
translate([wall_thickness / 2, -case_width / 2 + wall_thickness / 2, wall_thickness / 2])
|
||||||
|
cube([board_length + wall_thickness, wall_thickness / 2, wall_thickness / 2 + threshold], center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
include <./control_panel.scad>
|
||||||
|
|
||||||
|
/*
|
||||||
|
difference() {
|
||||||
|
color("blue") rounded_cube([5, 5, 5], bevel = 0.5);
|
||||||
|
translate([0, 0, 1]) rounded_cube([4, 4, 4]);
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
case_base();
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
include <./control_panel.scad>
|
||||||
|
|
||||||
|
lid();
|
||||||
|
// lamp();
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
include <./control_panel.scad>
|
||||||
|
|
||||||
|
box_side();
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "lights-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
az = { version = "1" }
|
||||||
|
fixed = { version = "1" }
|
|
@ -0,0 +1,481 @@
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
use az::*;
|
||||||
|
use core::{
|
||||||
|
clone::Clone,
|
||||||
|
cmp::PartialEq,
|
||||||
|
default::Default,
|
||||||
|
ops::{Add, Sub},
|
||||||
|
option::Option,
|
||||||
|
};
|
||||||
|
use fixed::types::{I48F16, I8F8, U128F0, U16F0};
|
||||||
|
|
||||||
|
mod patterns;
|
||||||
|
pub use patterns::*;
|
||||||
|
|
||||||
|
mod types;
|
||||||
|
pub use types::{BodyPattern, DashboardPattern, RGB};
|
||||||
|
|
||||||
|
fn calculate_frames(starting_time: U128F0, now: U128F0) -> U16F0 {
|
||||||
|
let frames_128 = (now - starting_time) / U128F0::from(FPS);
|
||||||
|
(frames_128 % U128F0::from(U16F0::MAX)).cast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_slope(start: I8F8, end: I8F8, frames: U16F0) -> I8F8 {
|
||||||
|
let slope_i16f16 = (I48F16::from(end) - I48F16::from(start)) / I48F16::from(frames);
|
||||||
|
slope_i16f16.saturating_as()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linear_ease(value: I8F8, frames: U16F0, slope: I8F8) -> I8F8 {
|
||||||
|
let value_i16f16 = I48F16::from(value) + I48F16::from(frames) * I48F16::from(slope);
|
||||||
|
value_i16f16.saturating_as()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
|
pub struct Instant(pub U128F0);
|
||||||
|
|
||||||
|
impl Default for Instant {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(U128F0::from(0 as u8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add for Instant {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, r: Self) -> Self::Output {
|
||||||
|
Self(self.0 + r.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub for Instant {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, r: Self) -> Self::Output {
|
||||||
|
Self(self.0 - r.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FPS: u8 = 30;
|
||||||
|
|
||||||
|
pub trait UI {
|
||||||
|
fn check_event(&mut self, current_time: Instant) -> Option<Event>;
|
||||||
|
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Animation {
|
||||||
|
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub struct DefaultAnimation {}
|
||||||
|
|
||||||
|
impl Animation for DefaultAnimation {
|
||||||
|
fn tick(&mut self, _: Instant) -> (DashboardPattern, BodyPattern) {
|
||||||
|
(WATER_DASHBOARD, WATER_BODY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub struct Fade {
|
||||||
|
starting_dashboard: DashboardPattern,
|
||||||
|
starting_lights: BodyPattern,
|
||||||
|
|
||||||
|
start_time: Instant,
|
||||||
|
dashboard_slope: [RGB<I8F8>; 3],
|
||||||
|
body_slope: [RGB<I8F8>; 60],
|
||||||
|
frames: U16F0,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fade {
|
||||||
|
fn new(
|
||||||
|
dashboard: DashboardPattern,
|
||||||
|
lights: BodyPattern,
|
||||||
|
ending_dashboard: DashboardPattern,
|
||||||
|
ending_lights: BodyPattern,
|
||||||
|
frames: U16F0,
|
||||||
|
time: Instant,
|
||||||
|
) -> Self {
|
||||||
|
let mut dashboard_slope = [Default::default(); 3];
|
||||||
|
let mut body_slope = [Default::default(); 60];
|
||||||
|
for i in 0..3 {
|
||||||
|
let slope = RGB {
|
||||||
|
r: calculate_slope(dashboard[i].r, ending_dashboard[i].r, frames),
|
||||||
|
g: calculate_slope(dashboard[i].g, ending_dashboard[i].g, frames),
|
||||||
|
b: calculate_slope(dashboard[i].b, ending_dashboard[i].b, frames),
|
||||||
|
};
|
||||||
|
dashboard_slope[i] = slope;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..60 {
|
||||||
|
let slope = RGB {
|
||||||
|
r: calculate_slope(lights[i].r, ending_lights[i].r, frames),
|
||||||
|
g: calculate_slope(lights[i].g, ending_lights[i].g, frames),
|
||||||
|
b: calculate_slope(lights[i].b, ending_lights[i].b, frames),
|
||||||
|
};
|
||||||
|
body_slope[i] = slope;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
starting_dashboard: dashboard,
|
||||||
|
starting_lights: lights,
|
||||||
|
start_time: time,
|
||||||
|
dashboard_slope,
|
||||||
|
body_slope,
|
||||||
|
frames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation for Fade {
|
||||||
|
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern) {
|
||||||
|
let mut frames = calculate_frames(self.start_time.0, time.0);
|
||||||
|
if frames > self.frames {
|
||||||
|
frames = self.frames
|
||||||
|
}
|
||||||
|
let mut dashboard_pattern: DashboardPattern = OFF_DASHBOARD;
|
||||||
|
let mut body_pattern: BodyPattern = OFF_BODY;
|
||||||
|
|
||||||
|
for i in 0..3 {
|
||||||
|
dashboard_pattern[i].r = linear_ease(
|
||||||
|
self.starting_dashboard[i].r,
|
||||||
|
frames,
|
||||||
|
self.dashboard_slope[i].r,
|
||||||
|
);
|
||||||
|
dashboard_pattern[i].g = linear_ease(
|
||||||
|
self.starting_dashboard[i].g,
|
||||||
|
frames,
|
||||||
|
self.dashboard_slope[i].g,
|
||||||
|
);
|
||||||
|
dashboard_pattern[i].b = linear_ease(
|
||||||
|
self.starting_dashboard[i].b,
|
||||||
|
frames,
|
||||||
|
self.dashboard_slope[i].b,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..60 {
|
||||||
|
body_pattern[i].r =
|
||||||
|
linear_ease(self.starting_lights[i].r, frames, self.body_slope[i].r);
|
||||||
|
body_pattern[i].g =
|
||||||
|
linear_ease(self.starting_lights[i].g, frames, self.body_slope[i].g);
|
||||||
|
body_pattern[i].b =
|
||||||
|
linear_ease(self.starting_lights[i].b, frames, self.body_slope[i].b);
|
||||||
|
}
|
||||||
|
|
||||||
|
(dashboard_pattern, body_pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FadeDirection {
|
||||||
|
Transition,
|
||||||
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BlinkerDirection {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Blinker {
|
||||||
|
transition: Fade,
|
||||||
|
fade_in: Fade,
|
||||||
|
fade_out: Fade,
|
||||||
|
direction: FadeDirection,
|
||||||
|
|
||||||
|
start_time: Instant,
|
||||||
|
frames: U16F0,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Blinker {
|
||||||
|
fn new(
|
||||||
|
starting_dashboard: DashboardPattern,
|
||||||
|
starting_body: BodyPattern,
|
||||||
|
direction: BlinkerDirection,
|
||||||
|
time: Instant,
|
||||||
|
) -> Self {
|
||||||
|
let mut ending_dashboard = OFF_DASHBOARD.clone();
|
||||||
|
|
||||||
|
match direction {
|
||||||
|
BlinkerDirection::Left => {
|
||||||
|
ending_dashboard[0].r = LEFT_BLINKER_DASHBOARD[0].r;
|
||||||
|
ending_dashboard[0].g = LEFT_BLINKER_DASHBOARD[0].g;
|
||||||
|
ending_dashboard[0].b = LEFT_BLINKER_DASHBOARD[0].b;
|
||||||
|
}
|
||||||
|
BlinkerDirection::Right => {
|
||||||
|
ending_dashboard[2].r = RIGHT_BLINKER_DASHBOARD[2].r;
|
||||||
|
ending_dashboard[2].g = RIGHT_BLINKER_DASHBOARD[2].g;
|
||||||
|
ending_dashboard[2].b = RIGHT_BLINKER_DASHBOARD[2].b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ending_body = OFF_BODY.clone();
|
||||||
|
match direction {
|
||||||
|
BlinkerDirection::Left => {
|
||||||
|
for i in 0..30 {
|
||||||
|
ending_body[i].r = LEFT_BLINKER_BODY[i].r;
|
||||||
|
ending_body[i].g = LEFT_BLINKER_BODY[i].g;
|
||||||
|
ending_body[i].b = LEFT_BLINKER_BODY[i].b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BlinkerDirection::Right => {
|
||||||
|
for i in 30..60 {
|
||||||
|
ending_body[i].r = RIGHT_BLINKER_BODY[i].r;
|
||||||
|
ending_body[i].g = RIGHT_BLINKER_BODY[i].g;
|
||||||
|
ending_body[i].b = RIGHT_BLINKER_BODY[i].b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Blinker {
|
||||||
|
transition: Fade::new(
|
||||||
|
starting_dashboard.clone(),
|
||||||
|
starting_body.clone(),
|
||||||
|
ending_dashboard.clone(),
|
||||||
|
ending_body.clone(),
|
||||||
|
BLINKER_FRAMES,
|
||||||
|
time,
|
||||||
|
),
|
||||||
|
fade_in: Fade::new(
|
||||||
|
OFF_DASHBOARD.clone(),
|
||||||
|
OFF_BODY.clone(),
|
||||||
|
ending_dashboard.clone(),
|
||||||
|
ending_body.clone(),
|
||||||
|
BLINKER_FRAMES,
|
||||||
|
time,
|
||||||
|
),
|
||||||
|
fade_out: Fade::new(
|
||||||
|
ending_dashboard.clone(),
|
||||||
|
ending_body.clone(),
|
||||||
|
OFF_DASHBOARD.clone(),
|
||||||
|
OFF_BODY.clone(),
|
||||||
|
BLINKER_FRAMES,
|
||||||
|
time,
|
||||||
|
),
|
||||||
|
direction: FadeDirection::Transition,
|
||||||
|
start_time: time,
|
||||||
|
frames: BLINKER_FRAMES,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation for Blinker {
|
||||||
|
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern) {
|
||||||
|
let frames = calculate_frames(self.start_time.0, time.0);
|
||||||
|
if frames > self.frames {
|
||||||
|
match self.direction {
|
||||||
|
FadeDirection::Transition => {
|
||||||
|
self.direction = FadeDirection::FadeOut;
|
||||||
|
self.fade_out.start_time = time;
|
||||||
|
}
|
||||||
|
FadeDirection::FadeIn => {
|
||||||
|
self.direction = FadeDirection::FadeOut;
|
||||||
|
self.fade_out.start_time = time;
|
||||||
|
}
|
||||||
|
FadeDirection::FadeOut => {
|
||||||
|
self.direction = FadeDirection::FadeIn;
|
||||||
|
self.fade_in.start_time = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.start_time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.direction {
|
||||||
|
FadeDirection::Transition => self.transition.tick(time),
|
||||||
|
FadeDirection::FadeIn => self.fade_in.tick(time),
|
||||||
|
FadeDirection::FadeOut => self.fade_out.tick(time),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
Brake,
|
||||||
|
BrakeRelease,
|
||||||
|
LeftBlinker,
|
||||||
|
NextPattern,
|
||||||
|
PreviousPattern,
|
||||||
|
RightBlinker,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Pattern {
|
||||||
|
Water,
|
||||||
|
GayPride,
|
||||||
|
TransPride,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pattern {
|
||||||
|
fn previous(&self) -> Pattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => Pattern::TransPride,
|
||||||
|
Pattern::GayPride => Pattern::Water,
|
||||||
|
Pattern::TransPride => Pattern::GayPride,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&self) -> Pattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => Pattern::GayPride,
|
||||||
|
Pattern::GayPride => Pattern::TransPride,
|
||||||
|
Pattern::TransPride => Pattern::Water,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dashboard(&self) -> DashboardPattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => WATER_DASHBOARD,
|
||||||
|
Pattern::GayPride => PRIDE_DASHBOARD,
|
||||||
|
Pattern::TransPride => TRANS_PRIDE_DASHBOARD,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body(&self) -> BodyPattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => WATER_BODY,
|
||||||
|
Pattern::GayPride => PRIDE_BODY,
|
||||||
|
Pattern::TransPride => TRANS_PRIDE_BODY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum State {
|
||||||
|
Pattern(Pattern),
|
||||||
|
Brake,
|
||||||
|
LeftBlinker,
|
||||||
|
RightBlinker,
|
||||||
|
BrakeLeftBlinker,
|
||||||
|
BrakeRightBlinker,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
ui: Box<dyn UI>,
|
||||||
|
state: State,
|
||||||
|
home_pattern: Pattern,
|
||||||
|
current_animation: Box<dyn Animation>,
|
||||||
|
dashboard_lights: DashboardPattern,
|
||||||
|
lights: BodyPattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(ui: Box<dyn UI>) -> Self {
|
||||||
|
let pattern = Pattern::Water;
|
||||||
|
Self {
|
||||||
|
ui,
|
||||||
|
state: State::Pattern(pattern),
|
||||||
|
home_pattern: pattern,
|
||||||
|
current_animation: Box::new(Fade::new(
|
||||||
|
OFF_DASHBOARD,
|
||||||
|
OFF_BODY,
|
||||||
|
pattern.dashboard(),
|
||||||
|
pattern.body(),
|
||||||
|
DEFAULT_FRAMES,
|
||||||
|
Instant((0 as u32).into()),
|
||||||
|
)),
|
||||||
|
dashboard_lights: OFF_DASHBOARD,
|
||||||
|
lights: OFF_BODY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_animation(&mut self, time: Instant) {
|
||||||
|
match self.state {
|
||||||
|
State::Pattern(ref pattern) => {
|
||||||
|
self.current_animation = Box::new(Fade::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
pattern.dashboard(),
|
||||||
|
pattern.body(),
|
||||||
|
DEFAULT_FRAMES,
|
||||||
|
time,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
State::Brake => {
|
||||||
|
self.current_animation = Box::new(Fade::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
BRAKES_DASHBOARD,
|
||||||
|
BRAKES_BODY,
|
||||||
|
BRAKES_FRAMES,
|
||||||
|
time,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
State::LeftBlinker => {
|
||||||
|
self.current_animation = Box::new(Blinker::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
BlinkerDirection::Left,
|
||||||
|
time,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
State::RightBlinker => {
|
||||||
|
self.current_animation = Box::new(Blinker::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
BlinkerDirection::Right,
|
||||||
|
time,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
State::BrakeLeftBlinker => (),
|
||||||
|
State::BrakeRightBlinker => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_state(&mut self, event: Event) {
|
||||||
|
match event {
|
||||||
|
Event::Brake => {
|
||||||
|
if self.state == State::Brake {
|
||||||
|
self.state = State::Pattern(self.home_pattern);
|
||||||
|
} else {
|
||||||
|
self.state = State::Brake;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::BrakeRelease => self.state = State::Pattern(self.home_pattern),
|
||||||
|
Event::LeftBlinker => match self.state {
|
||||||
|
State::Brake => self.state = State::BrakeLeftBlinker,
|
||||||
|
State::BrakeLeftBlinker => self.state = State::Brake,
|
||||||
|
State::LeftBlinker => self.state = State::Pattern(self.home_pattern),
|
||||||
|
_ => self.state = State::LeftBlinker,
|
||||||
|
},
|
||||||
|
Event::NextPattern => match self.state {
|
||||||
|
State::Pattern(ref pattern) => {
|
||||||
|
self.home_pattern = pattern.next();
|
||||||
|
self.state = State::Pattern(self.home_pattern);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Event::PreviousPattern => match self.state {
|
||||||
|
State::Pattern(ref pattern) => {
|
||||||
|
self.home_pattern = pattern.previous();
|
||||||
|
self.state = State::Pattern(self.home_pattern);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Event::RightBlinker => match self.state {
|
||||||
|
State::Brake => self.state = State::BrakeRightBlinker,
|
||||||
|
State::BrakeRightBlinker => self.state = State::Brake,
|
||||||
|
State::RightBlinker => self.state = State::Pattern(self.home_pattern),
|
||||||
|
_ => self.state = State::RightBlinker,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self, time: Instant) {
|
||||||
|
match self.ui.check_event(time) {
|
||||||
|
Some(event) => {
|
||||||
|
self.update_state(event);
|
||||||
|
self.update_animation(time);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (dashboard, lights) = self.current_animation.tick(time);
|
||||||
|
self.dashboard_lights = dashboard.clone();
|
||||||
|
self.lights = lights.clone();
|
||||||
|
self.ui.update_lights(dashboard, lights);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,400 @@
|
||||||
|
use crate::{BodyPattern, DashboardPattern, RGB};
|
||||||
|
use fixed::types::{I8F8, U16F0};
|
||||||
|
|
||||||
|
pub const RGB_OFF: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0"),
|
||||||
|
g: I8F8::lit("0"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RGB_WHITE: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1"),
|
||||||
|
g: I8F8::lit("1"),
|
||||||
|
b: I8F8::lit("1"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BRAKES_RED: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1"),
|
||||||
|
g: I8F8::lit("0"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BLINKER_AMBER: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1"),
|
||||||
|
g: I8F8::lit("0.15"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_RED: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.95"),
|
||||||
|
g: I8F8::lit("0.00"),
|
||||||
|
b: I8F8::lit("0.00"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_ORANGE: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1.0"),
|
||||||
|
g: I8F8::lit("0.25"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_YELLOW: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1.0"),
|
||||||
|
g: I8F8::lit("0.85"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_GREEN: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0"),
|
||||||
|
g: I8F8::lit("0.95"),
|
||||||
|
b: I8F8::lit("0.05"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_INDIGO: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.04"),
|
||||||
|
g: I8F8::lit("0.15"),
|
||||||
|
b: I8F8::lit("0.55"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_VIOLET: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.75"),
|
||||||
|
g: I8F8::lit("0.0"),
|
||||||
|
b: I8F8::lit("0.80"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TRANS_BLUE: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.06"),
|
||||||
|
g: I8F8::lit("0.41"),
|
||||||
|
b: I8F8::lit("0.98"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TRANS_PINK: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.96"),
|
||||||
|
g: I8F8::lit("0.16"),
|
||||||
|
b: I8F8::lit("0.32"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WATER_1: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.0"),
|
||||||
|
g: I8F8::lit("0.0"),
|
||||||
|
b: I8F8::lit("0.75"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WATER_2: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.8"),
|
||||||
|
g: I8F8::lit("0.8"),
|
||||||
|
b: I8F8::lit("0.8"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WATER_3: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.00"),
|
||||||
|
g: I8F8::lit("0.75"),
|
||||||
|
b: I8F8::lit("0.75"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const OFF_DASHBOARD: DashboardPattern = [RGB_OFF; 3];
|
||||||
|
pub const OFF_BODY: BodyPattern = [RGB_OFF; 60];
|
||||||
|
|
||||||
|
pub const DEFAULT_FRAMES: U16F0 = U16F0::lit("30");
|
||||||
|
|
||||||
|
pub const WATER_DASHBOARD: DashboardPattern = [WATER_1, WATER_2, WATER_3];
|
||||||
|
|
||||||
|
pub const WATER_BODY: BodyPattern = [
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
|
||||||
|
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
WATER_3,
|
||||||
|
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
WATER_2,
|
||||||
|
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
WATER_1,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const PRIDE_DASHBOARD: DashboardPattern = [PRIDE_RED, PRIDE_GREEN, PRIDE_INDIGO];
|
||||||
|
|
||||||
|
pub const PRIDE_BODY: BodyPattern = [
|
||||||
|
// Left Side
|
||||||
|
// Red
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
// Orange
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
// Yellow
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
// Green
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
// Indigo
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
// Violet
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
// Right Side
|
||||||
|
// Violet
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
// Indigo
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
// Green
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
// Yellow
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
// Orange
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
// Red
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const TRANS_PRIDE_DASHBOARD: DashboardPattern = [TRANS_BLUE, RGB_WHITE, TRANS_PINK];
|
||||||
|
|
||||||
|
pub const TRANS_PRIDE_BODY: BodyPattern = [
|
||||||
|
// Left Side
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, RGB_WHITE, RGB_WHITE, RGB_WHITE, RGB_WHITE,
|
||||||
|
RGB_WHITE, RGB_WHITE, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE,
|
||||||
|
// Right side
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, RGB_WHITE, RGB_WHITE, RGB_WHITE, RGB_WHITE,
|
||||||
|
RGB_WHITE, RGB_WHITE, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const BRAKES_FRAMES: U16F0 = U16F0::lit("15");
|
||||||
|
|
||||||
|
pub const BRAKES_DASHBOARD: DashboardPattern = [BRAKES_RED; 3];
|
||||||
|
|
||||||
|
pub const BRAKES_BODY: BodyPattern = [BRAKES_RED; 60];
|
||||||
|
|
||||||
|
pub const BLINKER_FRAMES: U16F0 = U16F0::lit("10");
|
||||||
|
|
||||||
|
pub const LEFT_BLINKER_DASHBOARD: DashboardPattern = [BLINKER_AMBER, RGB_OFF, RGB_OFF];
|
||||||
|
|
||||||
|
pub const LEFT_BLINKER_BODY: BodyPattern = [
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const RIGHT_BLINKER_DASHBOARD: DashboardPattern = [RGB_OFF, RGB_OFF, BLINKER_AMBER];
|
||||||
|
|
||||||
|
pub const RIGHT_BLINKER_BODY: BodyPattern = [
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
];
|
|
@ -0,0 +1,17 @@
|
||||||
|
use core::default::Default;
|
||||||
|
use fixed::types::I8F8;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, Debug)]
|
||||||
|
pub struct RGB<T> {
|
||||||
|
pub r: T,
|
||||||
|
pub g: T,
|
||||||
|
pub b: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DASHBOARD_LIGHT_COUNT: usize = 3;
|
||||||
|
|
||||||
|
pub type DashboardPattern = [RGB<I8F8>; DASHBOARD_LIGHT_COUNT];
|
||||||
|
|
||||||
|
const BODY_LIGHT_COUNT: usize = 60;
|
||||||
|
|
||||||
|
pub type BodyPattern = [RGB<I8F8>; BODY_LIGHT_COUNT];
|
|
@ -1,34 +1,16 @@
|
||||||
[package]
|
[package]
|
||||||
name = "kifu-gtk"
|
name = "simulator"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
|
||||||
screenplay = []
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
||||||
cairo-rs = { version = "0.18" }
|
cairo-rs = { version = "0.18" }
|
||||||
|
fixed = { version = "1" }
|
||||||
gio = { version = "0.18" }
|
gio = { version = "0.18" }
|
||||||
glib = { version = "0.18" }
|
glib = { version = "0.18" }
|
||||||
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
|
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
|
||||||
image = { version = "0.24" }
|
lights-core = { path = "../core" }
|
||||||
kifu-core = { path = "../core" }
|
|
||||||
pango = { version = "*" }
|
pango = { version = "*" }
|
||||||
sgf = { path = "../../sgf" }
|
|
||||||
tokio = { version = "1.26", features = [ "full" ] }
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
glib-build-tools = "0.17"
|
|
||||||
|
|
||||||
# [[bin]]
|
|
||||||
# name = "kifu-gtk"
|
|
||||||
# path = "src/main.rs"
|
|
||||||
|
|
||||||
# [[bin]]
|
|
||||||
# name = "screenplay"
|
|
||||||
# path = "src/bin/screenplay.rs"
|
|
||||||
# required-features = [ "screenplay" ]
|
|
||||||
|
|
|
@ -0,0 +1,288 @@
|
||||||
|
use adw::prelude::*;
|
||||||
|
use fixed::types::{I8F8, U128F0};
|
||||||
|
use glib::{Object, Sender};
|
||||||
|
use gtk::subclass::prelude::*;
|
||||||
|
use lights_core::{
|
||||||
|
App, BodyPattern, DashboardPattern, Event, Instant, FPS, OFF_BODY, OFF_DASHBOARD, RGB, UI,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
env,
|
||||||
|
rc::Rc,
|
||||||
|
sync::mpsc::{Receiver, TryRecvError},
|
||||||
|
};
|
||||||
|
|
||||||
|
const WIDTH: i32 = 640;
|
||||||
|
const HEIGHT: i32 = 480;
|
||||||
|
|
||||||
|
pub struct Update {
|
||||||
|
dashboard: DashboardPattern,
|
||||||
|
lights: BodyPattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DashboardLightsPrivate {
|
||||||
|
lights: Rc<RefCell<DashboardPattern>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for DashboardLightsPrivate {
|
||||||
|
const NAME: &'static str = "DashboardLights";
|
||||||
|
type Type = DashboardLights;
|
||||||
|
type ParentType = gtk::DrawingArea;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
lights: Rc::new(RefCell::new(OFF_DASHBOARD)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for DashboardLightsPrivate {}
|
||||||
|
impl WidgetImpl for DashboardLightsPrivate {}
|
||||||
|
impl DrawingAreaImpl for DashboardLightsPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct DashboardLights(ObjectSubclass<DashboardLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DashboardLights {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
s.set_width_request(WIDTH);
|
||||||
|
s.set_height_request(100);
|
||||||
|
|
||||||
|
s.set_draw_func({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, context, width, _| {
|
||||||
|
let start = width as f64 / 2. - 150.;
|
||||||
|
let lights = s.imp().lights.borrow();
|
||||||
|
for i in 0..3 {
|
||||||
|
context.set_source_rgb(
|
||||||
|
lights[i].r.into(),
|
||||||
|
lights[i].g.into(),
|
||||||
|
lights[i].b.into(),
|
||||||
|
);
|
||||||
|
context.rectangle(start + 100. * i as f64, 10., 80., 80.);
|
||||||
|
let _ = context.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_lights(&self, lights: DashboardPattern) {
|
||||||
|
*self.imp().lights.borrow_mut() = lights;
|
||||||
|
self.queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BikeLightsPrivate {
|
||||||
|
lights: Rc<RefCell<BodyPattern>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for BikeLightsPrivate {
|
||||||
|
const NAME: &'static str = "BikeLights";
|
||||||
|
type Type = BikeLights;
|
||||||
|
type ParentType = gtk::DrawingArea;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
lights: Rc::new(RefCell::new(OFF_BODY)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for BikeLightsPrivate {}
|
||||||
|
impl WidgetImpl for BikeLightsPrivate {}
|
||||||
|
impl DrawingAreaImpl for BikeLightsPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct BikeLights(ObjectSubclass<BikeLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BikeLights {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
s.set_width_request(WIDTH);
|
||||||
|
s.set_height_request(640);
|
||||||
|
|
||||||
|
let center = WIDTH as f64 / 2.;
|
||||||
|
|
||||||
|
s.set_draw_func({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, context, _, _| {
|
||||||
|
let lights = s.imp().lights.borrow();
|
||||||
|
for i in 0..30 {
|
||||||
|
context.set_source_rgb(
|
||||||
|
lights[i].r.into(),
|
||||||
|
lights[i].g.into(),
|
||||||
|
lights[i].b.into(),
|
||||||
|
);
|
||||||
|
context.rectangle(center - 45., 5. + 20. * i as f64, 15., 15.);
|
||||||
|
let _ = context.fill();
|
||||||
|
}
|
||||||
|
for i in 0..30 {
|
||||||
|
context.set_source_rgb(
|
||||||
|
lights[i + 30].r.into(),
|
||||||
|
lights[i + 30].g.into(),
|
||||||
|
lights[i + 30].b.into(),
|
||||||
|
);
|
||||||
|
context.rectangle(center + 15., 5. + 20. * (30. - (i + 1) as f64), 15., 15.);
|
||||||
|
let _ = context.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_lights(&self, lights: [RGB<I8F8>; 60]) {
|
||||||
|
*self.imp().lights.borrow_mut() = lights;
|
||||||
|
self.queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GTKUI {
|
||||||
|
tx: Sender<Update>,
|
||||||
|
rx: Receiver<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UI for GTKUI {
|
||||||
|
fn check_event(&mut self, _: Instant) -> Option<Event> {
|
||||||
|
match self.rx.try_recv() {
|
||||||
|
Ok(event) => Some(event),
|
||||||
|
Err(TryRecvError::Empty) => None,
|
||||||
|
Err(TryRecvError::Disconnected) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_lights(&self, dashboard_lights: DashboardPattern, lights: BodyPattern) {
|
||||||
|
self.tx
|
||||||
|
.send(Update {
|
||||||
|
dashboard: dashboard_lights,
|
||||||
|
lights,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let adw_app = adw::Application::builder()
|
||||||
|
.application_id("com.luminescent-dreams.bike-light-simulator")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
adw_app.connect_activate(move |adw_app| {
|
||||||
|
let (update_tx, update_rx) =
|
||||||
|
gtk::glib::MainContext::channel::<Update>(gtk::glib::Priority::DEFAULT);
|
||||||
|
let (event_tx, event_rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut bike_app = App::new(Box::new(GTKUI {
|
||||||
|
tx: update_tx,
|
||||||
|
rx: event_rx,
|
||||||
|
}));
|
||||||
|
loop {
|
||||||
|
bike_app.tick(Instant(U128F0::from(
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis(),
|
||||||
|
)));
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1000 / (FPS as u64)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let window = adw::ApplicationWindow::builder()
|
||||||
|
.application(adw_app)
|
||||||
|
.default_width(WIDTH)
|
||||||
|
.default_height(HEIGHT)
|
||||||
|
.build();
|
||||||
|
let layout = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.build();
|
||||||
|
let controls = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let dashboard_lights = DashboardLights::new();
|
||||||
|
let bike_lights = BikeLights::new();
|
||||||
|
|
||||||
|
let left_button = gtk::Button::builder().label("L").build();
|
||||||
|
let brake_button = gtk::Button::builder().label("Brakes").build();
|
||||||
|
let right_button = gtk::Button::builder().label("R").build();
|
||||||
|
|
||||||
|
left_button.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::LeftBlinker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
brake_button.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::Brake);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
right_button.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::RightBlinker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
controls.append(&left_button);
|
||||||
|
controls.append(&brake_button);
|
||||||
|
controls.append(&right_button);
|
||||||
|
layout.append(&controls);
|
||||||
|
|
||||||
|
let pattern_controls = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let previous_pattern = gtk::Button::builder().label("Previous").build();
|
||||||
|
let next_pattern = gtk::Button::builder().label("Next").build();
|
||||||
|
|
||||||
|
previous_pattern.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::PreviousPattern);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
next_pattern.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::NextPattern);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pattern_controls.append(&previous_pattern);
|
||||||
|
pattern_controls.append(&next_pattern);
|
||||||
|
layout.append(&pattern_controls);
|
||||||
|
|
||||||
|
layout.append(&dashboard_lights);
|
||||||
|
layout.append(&bike_lights);
|
||||||
|
|
||||||
|
update_rx.attach(None, {
|
||||||
|
let dashboard_lights = dashboard_lights.clone();
|
||||||
|
let bike_lights = bike_lights.clone();
|
||||||
|
move |Update { dashboard, lights }| {
|
||||||
|
dashboard_lights.set_lights(dashboard);
|
||||||
|
bike_lights.set_lights(lights);
|
||||||
|
glib::ControlFlow::Continue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.set_content(Some(&layout));
|
||||||
|
window.present();
|
||||||
|
});
|
||||||
|
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
ApplicationExtManual::run_with_args(&adw_app, &args);
|
||||||
|
}
|
73
build.sh
|
@ -1,73 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
RUST_ALL_TARGETS=(
|
|
||||||
"changeset"
|
|
||||||
"config"
|
|
||||||
"config-derive"
|
|
||||||
"coordinates"
|
|
||||||
"cyberpunk-splash"
|
|
||||||
"dashboard"
|
|
||||||
"emseries"
|
|
||||||
"file-service"
|
|
||||||
"fitnesstrax"
|
|
||||||
"fluent-ergonomics"
|
|
||||||
"geo-types"
|
|
||||||
"gm-control-panel"
|
|
||||||
"hex-grid"
|
|
||||||
"ifc"
|
|
||||||
"kifu-core"
|
|
||||||
"kifu-gtk"
|
|
||||||
"memorycache"
|
|
||||||
"nom-training"
|
|
||||||
"result-extended"
|
|
||||||
"screenplay"
|
|
||||||
"sgf"
|
|
||||||
"tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
build_rust_targets() {
|
|
||||||
local CMD=$1
|
|
||||||
local TARGETS=${@/$CMD}
|
|
||||||
|
|
||||||
for target in $TARGETS; do
|
|
||||||
MODULE=$target CMD=$CMD ./builders/rust.sh
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
build_dist() {
|
|
||||||
local TARGETS=${@/$CMD}
|
|
||||||
|
|
||||||
for target in $TARGETS; do
|
|
||||||
if [ -f $target/dist.sh ]; then
|
|
||||||
build_rust_targets release ${TARGETS[*]}
|
|
||||||
cd $target && ./dist.sh
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
export CARGO=`which cargo`
|
|
||||||
|
|
||||||
if [ -z "${TARGET-}" ]; then
|
|
||||||
TARGET="all"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${CMD-}" ]; then
|
|
||||||
CMD="test release"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${CMD}" == "clean" ]; then
|
|
||||||
cargo clean
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
for cmd in $CMD; do
|
|
||||||
if [ "${CMD}" == "dist" ]; then
|
|
||||||
build_dist $TARGET
|
|
||||||
elif [ "${TARGET}" == "all" ]; then
|
|
||||||
build_rust_targets $cmd ${RUST_ALL_TARGETS[*]}
|
|
||||||
else
|
|
||||||
build_rust_targets $cmd $TARGET
|
|
||||||
fi
|
|
||||||
done
|
|
|
@ -1,41 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [ ! -z "$MODULE" ]; then
|
|
||||||
MODULE="-p $MODULE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${PARAMS-}" ]; then
|
|
||||||
PARAMS=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
case $CMD in
|
|
||||||
build)
|
|
||||||
$CARGO build $MODULE $PARAMS
|
|
||||||
;;
|
|
||||||
lint)
|
|
||||||
$CARGO clippy $MODULE $PARAMS -- -Dwarnings
|
|
||||||
;;
|
|
||||||
test)
|
|
||||||
$CARGO test $MODULE $PARAMS
|
|
||||||
;;
|
|
||||||
run)
|
|
||||||
$CARGO run $MODULE $PARAMS
|
|
||||||
;;
|
|
||||||
release)
|
|
||||||
$CARGO clippy $MODULE $PARAMS -- -Dwarnings
|
|
||||||
$CARGO build --release $MODULE $PARAMS
|
|
||||||
$CARGO test --release $MODULE $PARAMS
|
|
||||||
;;
|
|
||||||
clean)
|
|
||||||
$CARGO clean $MODULE
|
|
||||||
;;
|
|
||||||
"")
|
|
||||||
echo "No command specified. Use build | lint | test | run | release | clean"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "$CMD is unknown. Use build | lint | test | run | release | clean"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ macro_rules! define_config {
|
||||||
$($name($struct)),+
|
$($name($struct)),+
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
values: std::collections::HashMap<ConfigName, ConfigOption>,
|
values: std::collections::HashMap<ConfigName, ConfigOption>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,491 @@
|
||||||
|
{
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#addr2line@0.24.2": "1hd1i57zxgz08j6h5qrhsnm2fi0bcqvsh389fw400xm3arz2ggnz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#adler2@2.0.0": "09r6drylvgy8vv8k20lnbvwq8gp09h7smfn6h1rxsy15pgh629si",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#adler32@1.2.0": "0d7jq7jsjyhsgbhnfq5fvrlh9j0i9g1fqrl2735ibv5f75yjgqda",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#adler@1.0.2": "1zim79cvzd5yrkzl3nyfx0avijwgk9fqv3yrscdy1cc79ih02qpj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.11": "04chdfkls5xmhp1d48gnjsmglbqibizs3bpbj6rsj604m10si7g8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.3": "05mrpkvdgp5d20y2p989f187ry9diliijgwrs254fs9s1m1x6q4f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#allocator-api2@0.2.18": "0kr6lfnxvnj164j1x38g97qjlhb7akppqzvgfs0697140ixbav2w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#android-tzdata@0.1.1": "1w7ynjxrfs97xg3qlcdns4kgfpwcdv824g611fq32cag4cdr96g9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#android_system_properties@0.1.5": "04b3wrz12837j7mdczqd95b732gw5q7q66cv4yn4646lvccp57l1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#annotate-snippets@0.9.2": "07p8r6jzb7nqydq0kr5pllckqcdxlyld2g275v425axnzffpxbyc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstream@0.6.15": "09nm4qj34kiwgzczdvj14x7hgsb235g4sqsay3xsz7zqn4d5rqb4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.5": "1jy12rvgbldflnb2x7mcww9dcffw1mx22nyv6p3n7d62h0gdwizb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.1.1": "0aj22iy4pzk6mz745sfrm1ym14r0y892jhcrbs8nkj7nqx9gqdkd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle-wincon@3.0.4": "1y2pkvsrdxbcwircahb4wimans2pzmwwxad7ikdhj5lpdqdlxxsv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.8": "1cfmkza63xpn1kkz844mgjwm9miaiz4jkyczmwxzivcsypk1vv0v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.89": "1xh1vg89n56h6nqikcmgbpmkixjds33492klrp9m96xrbmhgizc6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-channel@1.9.0": "0dbdlkzlncbibd3ij6y6jmvjd0cmdn48ydcfdpfhw09njd93r5c1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-channel@2.3.1": "0skvwxj6ysfc6d7bhczz9a2550260g62bm5gl0nmjxxyn007id49",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-executor@1.13.1": "1v6w1dbvsmw6cs4dk4lxj5dvrikc6xi479wikwaab2qy3h09mjih",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-global-executor@2.4.1": "1762s45cc134d38rrv0hyp41hv4iv6nmx59vswid2p0il8rvdc85",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-io@2.3.4": "1s679l7x6ijh8zcxqn5pqgdiyshpy4xwklv86ldm1rhfjll04js4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-lock@3.4.0": "060vh45i809wcqyxzs5g69nqiqah7ydz0hpkcjys9258vqn4fvpz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-std@1.13.0": "059nbiyijwbndyrz0050skvlvzhds0dmnl0biwmxwbw055glfd66",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-task@4.7.1": "1pp3avr4ri2nbh7s6y9ws0397nkx1zymmcr14sq761ljarh3axcb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.83": "1p8q8gm4fv2fdka8hwy2w3f8df7p5inixqi7rlmbnky3wmysw73j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#atoi@2.0.0": "0a05h42fggmy7h0ajjv6m7z72l924i7igbx13hk9d8pyign9k3gj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#atomic-waker@1.1.2": "1h5av1lw56m0jf0fd3bchxq8a30xv0b4wv8s4zkp4s0i7mfvs18m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#autocfg@0.1.8": "0y4vw4l4izdxq1v0rrhvmlbqvalrqrmk60v1z0dqlgnlbzkl7phd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#autocfg@1.4.0": "09lz3by90d2hphbq56znag9v87gfpd9gb8nr82hll8z6x2nhprdc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#az@1.2.1": "0ww9k1w3al7x5qmb7f13v3s9c2pg1pdxbs8xshqy6zyrchj4qzkv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#backtrace@0.3.74": "06pfif7nwx66qf2zaanc2fcq7m64i91ki9imw9xd3bnz5hrwp0ld",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#base64@0.21.7": "0rw52yvsk75kar9wgqfwgb414kvil1gn7mqkrhn9zf1537mpsacx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#base64@0.9.3": "0hs62r35bgxslawyrn1vp9rmvrkkm76fqv0vqcwd048vs876r7a8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#base64ct@1.6.0": "0nvdba4jb8aikv60az40x2w1y96sjdq8z3yp09rwzmkhiwv1lg4c",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bindgen@0.69.5": "1240snlcfj663k04bjsg629g4wx6f83flgbjh5rzpgyagk3864r7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bit-set@0.5.3": "1wcm9vxi00ma4rcxkl3pzzjli6ihrpn9cfdi0c5b4cvga2mxs007",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bit-vec@0.6.3": "1ywqjnv60cdh1slhz67psnp422md6jdliji6alq0gmly2xm9p7rl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bit_field@0.10.2": "0qav5rpm4hqc33vmf4vc4r0mh51yjx5vmd9zhih26n9yjs3730nw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bitflags@1.3.2": "12ki6w8gn1ldq7yz9y680llwk5gmrhrzszaa17g1sbrw2r2qvwxy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bitflags@2.6.0": "1pkidwzn3hnxlsl8zizh0bncgbjnw7c41cx7bby26ncbzmiznj5h",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4": "0w9sa2ypmrsqqvc20nhwr75wbb5cjr4kkyhpjm1z1lv2kdicfy1h",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#blocking@1.6.1": "1si99l8zp7c4zq87y35ayjgc5c9b60jb8h0k14zfcs679z2l2gvh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#build_html@2.5.0": "0p4k25yk3v0wf720wl5zcghvc9ik6l7lsh3fz86cq3g7x4nbhpi2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bumpalo@3.16.0": "0b015qb4knwanbdlp1x48pkb4pm57b8gidbhhhxr900q2wb6fabr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.18.0": "1bp2s9wn0gjsaygv21nsbfpf854vl897ll6sqpfn3naaannv1fwl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0": "0jzncxyf404mwqdbspihyzpkndfgda450l0893pz5xj685cg5l0z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bytes@1.7.2": "1wzs7l57iwqmrszdpr2mmqf1b1hgvpxafc30imxhnry0zfl9m3a2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cairo-rs@0.18.5": "1qjfkcq3mrh3p01nnn71dy3kn99g21xx3j8xcdvzn8ll2pq6x8lc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cairo-sys-rs@0.18.2": "0lfsxl7ylw3phbnwmz3k58j1gnqi6kc2hdc7g3bb7f4hwnl9yp38",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cc@1.1.34": "1j9dh96lpkksmfvjfiqa5nrlswm5l6lj54m5jf7i0iik8l6lgfb7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cexpr@0.6.0": "0rl77bwhs5p979ih4r0202cn5jrfsrbgrksp40lkfz5vk1x3ib3g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cfg-expr@0.15.8": "00lgf717pmf5qd2qsxxzs815v6baqg38d6m5i6wlh235p14asryh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.0": "1za0vb97n4brpzpv8lsbnzmq5r8f2b0cpqqr0sy8h5bn751xxwds",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#chrono-tz-build@0.2.1": "03rmzd69cn7fp0fgkjr5042b3g54s2l941afjm3001ls7kqkjgj3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#chrono-tz@0.8.6": "0vlksnmpb6rd4h55245agnfhphnpslwnq9al3aw3is43dd3f16nm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.38": "009l8vc5p8750vn02z30mblg4pv2qhkbfizhfwmzc6vpy5nr67x2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clang-sys@1.8.1": "1x1r9yqss76z8xwpdanw313ss6fniwc1r7dzb5ycjn0ph53kj0hb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap@4.5.20": "1s37v23gcxkjy4800qgnkxkpliz68vslpr5sgn1xar56hmnkfzxr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.5.20": "0m6w10l2f65h3ch0d53lql6p26xxrh20ffipra9ysjsfsjmq1g0r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.5.18": "1ardb26bvcpg72q9myr7yir3a8c83gx7vxk1cccabsd9n73s1ija",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap_lex@0.7.2": "15zcrc2fa6ycdzaihxghf48180bnvzsivhf0fmah24bnnaf76qhl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cloudabi@0.0.3": "0kxcg83jlihy0phnd2g8c2c303px3l2p3pkjz357ll6llnd5pz6x",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#color_quant@1.1.0": "12q1n427h2bbmmm1mnglr57jaz2dj9apk0plcxw7nwqiai7qjyrx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.2": "1h18ph538y8yjmbpaf8li98l0ifms2xmh3rax9666c5qfjfi3zfk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#concurrent-queue@2.5.0": "0wrr3mzq2ijdkxwndhf79k952cp4zkz35ray8hvsxl96xrx1k82c",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#const-oid@0.9.6": "1y0jnqaq7p2wvspnx7qj76m7hjcqpz73qzvr9l2p9n2s51vr6if2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#convert_case@0.6.0": "1jn1pq6fp3rri88zyw6jlhwwgf6qiyc08d6gjv0qypgkl862n67c",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cookie-factory@0.3.3": "18mka6fk3843qq3jw1fdfvzyv05kx7kcmirfbs2vg2kbw9qzm1cq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cookie@0.17.0": "096c52jg9iq4lfcps2psncswv33fc30mmnaa2sbzzcfcw71kgyvy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cool_asserts@2.0.3": "1v18dg7ifx41k2f82j3gsnpm1fg9wk5s4zv7sf42c7pnad72b7zf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#core-foundation-sys@0.8.7": "12w8j73lazxmr1z0h98hf3z623kl8ms7g07jch7n4p8f9nwlhdkp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#core-foundation@0.9.4": "13zvbbj07yk3b61b8fhwfzhy35535a583irf23vlcg59j7h9bqci",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.14": "1q3qd9qkw94vs7n5i0y3zz2cqgzcxvdgyb54ryngwmjhfbgrg1k0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crc-catalog@2.4.0": "1xg7sz82w3nxp1jfn425fvn1clvbzb3zgblmxsyqpys0dckp9lqr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.4.2": "1czp7vif73b8xslr3c9yxysmh9ws2r8824qda7j47ffs9pcnjxx9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crc@3.2.1": "0dnn23x68qakzc429s1y9k9y3g8fn5v9jwi63jcz151sngby9rk9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-deque@0.8.5": "03bp38ljx4wj6vvy4fbhx41q8f585zyqix6pncz1mkz93z08qgv1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-epoch@0.9.18": "03j2np8llwf376m3fxqx859mgp9f83hj1w34153c7a9c7i5ar0jv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-queue@0.3.11": "0d8y8y3z48r9javzj67v3p2yfswd278myz1j9vzc4sp7snslc0yz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.20": "100fksq5mm1n7zj242cclkw6yf7a4a8ix3lvpfkhxvdhbda9kv12",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crunchy@0.2.2": "1dx9mypwd5mpfbbajm78xcrg5lirqk7934ik980mmaffg3hdm0bs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.6": "1cvby95a6xg7kxdz5ln3rl9xh66nz66w46mm3g56ri1z5x815yqv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#data-encoding@2.6.0": "1qnn68n4vragxaxlkqcb1r28d3hhj43wch67lm4rpxlw89wnjmp8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#deflate@0.8.6": "0x6iqlayg129w63999kz97m279m0jj4x4sm6gkqlvmp73y70yxvk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#der@0.7.9": "1h4vzjfa1lczxdf8avfj9qlwh1qianqlxdy1g5rn762qnvkzhnzm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#deranged@0.3.11": "1d1ibqqnr5qdrpw8rclwrf1myn3wf0dygl04idf4j2s49ah6yaxl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7": "14p2n6ih29x81akj097lvz7wi9b6b9hvls0lwrv7b6xwyy0s5ncy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#dimensioned@0.7.0": "09ky8s3higkf677lmyqg30hmj66gpg7hx907s6hfvbk2a9av05r5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#dimensioned@0.8.0": "15s3j4ry943xqlac63bp81sgdk9s3yilysabzww35j9ibmnaic50",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5": "1q0alair462j21iiqwrr21iabkfnb13d6x5w95lkdg21q2xrqdlp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#dotenvy@0.15.7": "16s3n973n5aqym02692i1npb079n5mb0fwql42ikmwn8wnrrbbqs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#either@1.13.0": "1w2c1mybrd7vljyxk77y9f4w9dyjrmp3yp82mk7bcm8848fazcb0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#encoding_rs@0.8.34": "0nagpi1rjqdpvakymwmnlxzq908ncg868lml5b70n08bm82fjpdl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#env_logger@0.10.2": "1005v71kay9kbz1d5907l0y7vh9qn2fqsp2yfgb8bjvin6m0bm2c",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.1": "1malmx5f4lkfvqasz319lq6gb3ddg19yzf9s8cykfsgzdmyq0hsl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#errno@0.3.9": "1fi0m0493maq1jygcf1bya9cymz2pc1mqxj26bdv7yjd37v5qk2k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#etcetera@0.8.0": "0hxrsn75dirbjhwgkdkh0pnpqrnq17ypyhjpjaypgax1hd91nv8k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#event-listener-strategy@0.5.2": "18f5ri227khkayhv3ndv7yl4rnasgwksl2jhwgafcxzr7324s88g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#event-listener@2.5.3": "1q4w3pndc518crld6zsqvvpy9lkzwahp2zgza9kbzmmqh9gif1h2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#event-listener@5.3.1": "1fkm6q4hjn61wl52xyqyyxai0x9w0ngrzi0wf1qsf8vhsadvwck0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#exr@1.72.0": "195iviimjnp1mdkqrq8hjrfkr0qavpp1p8pq5qvaksa30pv96zc8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.1.1": "19nyzdq3ha4g173364y2wijmd6jlyms8qx40daqkxsnl458jmh78",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fdeflate@0.3.5": "1axmgzpgf12yl3x9ymdslqza765la17j17ljv6a4kc143a90y2fq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#field-offset@0.3.6": "0zq5sssaa2ckmcmxxbly8qgz3sxpb8g1lwv90sdh1z74qif2gqiq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fixed@1.28.0": "0nn85j5x8yzx10q49jdzia4yp6pnasnxpnwh0p9aqr7qkfwf1il5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#flate2@1.0.34": "1w1nf2ap4q1sq1v6v951011wcvljk449ap7q7jnnjf8hvjs8kdd1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent-bundle@0.15.3": "14zl0cjn361is69pb1zry4k2zzh5nzsfv0iz05wccl00x0ga5q3z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent-langneg@0.13.0": "152yxplc11vmxkslvmaqak9x86xnavnhdqyhrh38ym37jscd0jic",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent-syntax@0.11.1": "0gd3cdvsx9ymbb8hijcsc9wyf8h1pbcbpsafg4ldba56ji30qlra",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent@0.16.1": "0njmdpwz52yjzyp55iik9k6vrixqiy7190d98pk0rgdy0x3n6x5v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#flume@0.11.0": "10girdbqn77wi802pdh55lwbmymy437k7kklnvj12aaiwaflbb2m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7": "1hc2mcqha06aibcaza94vbi81j6pr9a1bbxrxjfhc91zin8yr7iz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#foreign-types-shared@0.1.1": "0jxgzd04ra4imjv8jgkmdq59kj8fsz6w4zxsbmlai34h26225c00",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#foreign-types@0.3.2": "1cgk0vyd7r45cj769jym4a6s7vwshvd0z4bqrb92q1fwibmkkwzn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.1": "0milh8x7nl4f450s3ddhg57a3flcv6yq8hlkyk6fyr3mcb128dp1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fuchsia-cprng@0.1.1": "1fnkqrbz7ixxzsb04bsz9p0zzazanma8znfdqjvh39n14vapfvx0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31": "040vpqpqlbk099razq8lyn74m0f161zd0rp36hciqrwcg2zibzrd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31": "0gk6yrxgi5ihfanm2y431jadrll00n5ifhnpx090c2f2q1cr1wh5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-executor@0.3.31": "17vcci6mdfzx4gbk0wx64chr2f13wwwpvyf3xd5fb1gmjzcx2a0y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-intrusive@0.5.0": "0vwm08d1pli6bdaj0i7xhk3476qlx4pll6i0w03gzdnh7lh0r4qx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.31": "1ikmw1yfbgvsychmsihdkwa8a1knank2d9a8dk01mbjar9w1np4y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-lite@2.3.0": "19gk4my8zhfym6gwnpdjiyv2hw8cc098skkbkhryjdaf0yspwljj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.31": "0l1n7kqzwwmgiznn0ywdc5i24z72zvh9q1dwps54mimppi7f6bhn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31": "1xyly6naq6aqm52d5rh236snm08kw8zadydwqz8bip70s6vzlxg5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.31": "124rv4n90f5xwfsm9qw6y99755y021cmi5dhzh253s920z77s3zr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31": "10aa1ar8bgkgbr4wzxlidkqkcxf77gffyj8j7768h831pcaq784z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31": "0xh8ddbkm9jy8kc5gbvjp9a4b6rqqxvc8471yb2qaz5wm2qhgg35",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf-sys@0.18.0": "1xya543c4ffd2n7aiwwrdxsyc9casdbasafi6ixcknafckm3k61z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf@0.18.5": "1v7svvl0g7zybndmis5inaqqgi1mvcc6s1n8rkb31f5zn3qzbqah",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk4-sys@0.7.2": "1w7yvir565sjrrw828lss07749hfpfsr19jdjzwivkx36brl7ayv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk4@0.7.3": "1xiacc63p73apr033gjrb9dsk0y4yxnsljwfxbwfry41snd03nvy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.11.2": "0a7w8w0rg47nmcinnfzv443lcyb8mplwc251p1jyr5xj2yh6wzv6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7": "16lyyrzrljfq424c3n8kfwkqihlimmsg5nhshbbp48np3yjrqr45",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.15": "1mzlnrb3dgyd1fb84gvw10pyr8wdqdl4ry4sr64i1s8an66pqmn4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gif@0.11.4": "01hbw3isapzpzff8l6aw55jnaqx2bcscrbwyf3rglkbbfp397p9y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gif@0.13.1": "1whrkvdg26gp1r7f95c6800y6ijqw5y0z8rgj6xihpi136dxdciz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gimli@0.31.1": "0gvqc0ramx8szv76jhfd4dms0zyamvlg4whhiz11j34hh3dqxqh7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gio-sys@0.18.1": "1lip8z35iy9d184x2qwjxlbxi64q9cpayy7v1p5y9xdsa3w6smip",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gio@0.18.4": "0wsc6mnx057s4ailacg99dwgna38dbqli5x7a6y9rdw75x9qzz6l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.16.3": "1z73bl10zmxwrv16v4f5wcky1f3z5a2v0hknca54al4k2p5ka695",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.17.10": "05p7ab2vn8962cbchi7a6hndhvw64nqk4w5kpg5z53iizsgdfrbs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.18.0": "0p5c2ayiam5bkp9wvq9f9ihwp06nqs5j801npjlwnhrl8rpwac9l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-macros@0.18.5": "1p5cla53fcp195zp0hkqpmnn7iwmkdswhy7xh34002bw8y7j5c0b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-sys@0.18.1": "164qhsfmlzd5mhyxs8123jzbdfldwxbikfpq5cysj3lddbmy4g06",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib@0.18.5": "1r8fw0627nmn19bgk3xpmcfngx3wkn7mcpq5a8ma3risx3valg93",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glob@0.3.1": "16zca52nglanv23q5qrwd5jinw3d3as5ylya6y1pbx47vkxvrynj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gloo-timers@0.3.0": "1519157n7xppkk6pdw5w52vy1llzn5iljkqd7q1h5609jv7l7cdv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gobject-sys@0.18.0": "0i6fhp3m6vs3wkzyc22rk2cqj68qvgddxmpaai34l72da5xi4l08",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#graphene-rs@0.18.1": "00f4q1ra4haap5i7lazwhkdgnb49fs8adk2nm6ki6mjhl76jh8iv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#graphene-sys@0.18.1": "0n8zlg7z26lwpnvlqp1hjlgrs671skqwagdpm7r8i1zwx3748hfc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#grid@0.9.0": "0iswdcxggyxp9m1rz0m7bfg4xacinvn78zp2fgfp0l0079x10d06",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gsk4-sys@0.7.3": "0mbdlm9qi1hql48rr29vsj9vlqwc7gxg67wg1q19z67azwz9xg8j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gsk4@0.7.3": "0zhzs2dkgiinhgc11akpn2harq3x5n1iq21dnc4h689g3lsqx58d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gtk4-macros@0.7.2": "0bw3cchiycf7dw1bw4p8946gv38azxy05a5w0ndgcmxnz6fc8znm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gtk4-sys@0.7.3": "1f2ylskyqkjdik9fij2m46pra4jagnif5xyalbxfk3334fmc9n2l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gtk4@0.7.3": "0hh8nzglmz94v1m1h6vy8z12m6fr7ia467ry0md5fa4p7sm53sss",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#h2@0.3.26": "1s7msnfv7xprzs6xzfj5sg6p8bjcdpcqcmjjbkd345cyi1x55zl1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#half@2.4.1": "123q4zzw1x4309961i69igzd1wb7pj04aaii3kwasrz3599qrl3d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.14.5": "1wa1vy1xs3mp11bn3z9dv0jricgr6a2j0zkf1g19yz3vw4il89z5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.15.0": "1yx4xq091s7i6mw6bn77k8cp4jrpcac149xr32rg8szqsj27y20y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hashlink@0.8.4": "1xy8agkyp0llbqk9fcffc1xblayrrywlyrm2a7v93x8zygm4y2g8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#headers-core@0.2.0": "0ab469xfpd411mc3dhmjhmzrhqikzyj8a17jn5bkj9zfpy0n9xp7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#headers@0.3.9": "0w62gnwh2p1lml0zqdkrx9dp438881nhz32zrzdy61qa0a9kns06",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#heck@0.4.1": "1a7mqsnycv5z4z5vnv1k34548jzmc0ajic7c1j8jsaspnhw5ql4m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0": "1sjmpsdl8czyh9ywl3qcsfsq9a307dg4ni2vnlwgnzzqhc4y0113",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hermit-abi@0.3.9": "092hxjbjnq5fmz66grd9plxd0sh6ssg5fhgwwwqbrzgzkjwdycfj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hermit-abi@0.4.0": "1k1zwllx6nfq417hy38x4akw1ivlv68ymvnzyxs76ffgsqcskxpv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hex-string@0.1.0": "02sgrgrbp693jv0v5iga7z47y6aj93cq0ia39finby9x17fw53l4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hex@0.4.3": "0w1a4davm1lgzpamwnba907aysmlrnygbqmfis2mqjx5m552a93z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hkdf@0.12.4": "1xxxzcarz151p1b858yn5skmhyrvn8fs4ivx5km3i1kjmnr8wpvv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hmac@0.12.1": "0pmbr069sfg76z7wsssfk5ddcqd9ncp79fyz6zcm6yn115yc6jbc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#home@0.5.9": "19grxyg35rqfd802pcc9ys1q3lafzlcjcv2pl2s5q8xpyr5kblg3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#http-body@0.4.6": "1lmyjfk6bqk6k9gkn1dxq770sb78pqbqshga241hr5p995bb5skw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#http@0.2.12": "1w81s4bcbmcj9bjp7mllm8jlz6b31wzvirz8bgpzbqkpwmbvn730",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#http@1.1.0": "0n426lmcxas6h75c2cp25m933pswlrfjz10v91vc62vib2sdvf91",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#httparse@1.9.5": "0ip9v8m9lvgvq1lznl31wvn0ch1v254na7lhid9p29yx9rbx6wbx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3": "1aa9rd2sac0zhjqh24c9xvir96g188zldkx0hr6dnnlx5904cfyz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#humantime@2.1.0": "1r55pfkkf5v0ji1x6izrjwdq9v6sc7bv99xj6srywcar37xmnfls",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hyper-tls@0.5.0": "01crgy13102iagakf6q4mb75dprzr7ps1gj0l5hxm1cvm7gks66n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hyper@0.10.16": "0wwjh9p3mzvg3fss2lqz5r7ddcgl1fh9w6my2j69d6k0lbcm41ha",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hyper@0.14.30": "1jayxag79yln1nzyzx652kcy1bikgwssn6c4zrrp5v7s3pbdslm1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#iana-time-zone-haiku@0.1.2": "17r6jmj31chn7xs9698r122mapq85mfnv98bb4pg6spm0si2f67k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.61": "085jjsls330yj1fnwykfzmb2f10zp6l7w4fhq81ng81574ghhpi3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#idna@0.1.5": "0kl4gs5kaydn4v07c6ka33spm9qdh2np0x7iw7g5zd8z1c7rxw1q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#idna@0.5.0": "1xhjrcjqq0l5bpzvdgylvpkgk94panxgsirzhjnnqfdgc4a9nkb3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#image@0.23.14": "18gn2f7xp30pf9aqka877knlq308khxqiwjvsccvzaa4f9zcpzr4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#image@0.24.9": "17gnr6ifnpzvhjf6dwbl9hki8x6bji5mwcqp0048x1jm5yfi742n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#indent_write@2.2.0": "1hqjp80argdskrhd66g9sh542yxy8qi77j6rc69qd0l7l52rdzhc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#indexmap@2.6.0": "1nmrwn8lbs19gkvhxaawffzbvrpyrb5y3drcrr645x957kz0fybh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#intl-memoizer@0.5.2": "1nkvql7c7b76axv4g68di1p2m9bnxq1cbn6mlqcawf72zhhf08py",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#intl_pluralrules@7.0.2": "0wprd3h6h8nfj62d8xk71h178q7zfn3srxm787w4sawsqavsg3h7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ipnet@2.10.1": "025p9wm94q1w2l13hbbr4cbmfygly3a2ag8g5s618l2jhq4l3hnx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#iron@0.6.1": "1s4mf8395f693nhwsr0znw3j5frzn56gzllypyl50il85p50ily6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#is-terminal@0.4.13": "0jwgjjz33kkmnwai3nsdk1pz9vb6gkqvw1d1vq7bs3q48kinh7r6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#is_terminal_polyfill@1.70.1": "1kwfgglh91z33kl0w5i338mfpa3zs0hidq5j4ny4rmjwrikchhvr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#itertools@0.12.1": "0s95jbb3ndj1lvfxyq5wanc0fm0r6hg6q4ngb92qlfdxvci10ads",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.11": "0nv9cqjwzr3q58qz84dcz63ggc54yhf1yqar1m858m1kfd4g3wa9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#jpeg-decoder@0.1.22": "1wnh0bmmswpgwhgmlizz545x8334nlbmkq8imy9k224ri3am7792",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#jpeg-decoder@0.3.1": "1c1k53svpdyfhibkmm0ir5w0v3qmcmca8xr8vnnmizwf6pdagm7m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#js-sys@0.3.70": "0yp3rz7vrn9mmqdpkds426r1p9vs6i8mkxx8ryqdfadr0s2q0s0q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#kv-log-macro@1.0.7": "0zwp4bxkkp87rl7xy2dain77z977rvcry1gmr5bssdbn541v7s0d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#language-tags@0.2.2": "16hrjdpa827carq5x4b8zhas24d8kg4s16m6nmmn1kb7cr5qh7d9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0": "1zk6dqqni0193xg6iijh7i3i44sryglwgvx20spdvwk3r6sbrlmv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lazycell@1.3.0": "0m8gw7dn30i0zjjpjdyf6pc16c34nl71lpv461mix50x3p70h3c3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lebe@0.5.2": "1j2l6chx19qpa5gqcw434j83gyskq3g2cnffrbl3842ymlmpq203",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libadwaita-sys@0.5.3": "16n6xsy6jhbj0jbpz8yvql6c9b89a99v9vhdz5s37mg1inisl42y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libadwaita@0.5.3": "174pzn9dwsk8ikvrhx13vkh0zrpvb3rhg9yd2q5d2zjh0q6fgrrg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.159": "1i9xpia0hn1y8dws7all8rqng6h3lc8ymlgslnljcvm376jrf7an",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libloading@0.8.5": "194dvczq4sifwkzllfmw0qkgvilpha7m5xy90gd6i446vcpz4ya9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libm@0.2.8": "0n4hk1rs8pzw8hdfmwn96c4568s93kfxqgcqswr7sajd2diaihjf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libspa-sys@0.8.0": "07yh4i5grzbxkchg6dnxlwbdw2wm5jnd7ffbhl77jr0388b9f3dz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libspa@0.8.0": "044qs48yl0llp2dmrgwxj9y1pgfy09i6fhq661zqqb9a3fwa9wv5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libsqlite3-sys@0.27.0": "05pp60ncrmyjlxxjj187808jkvpxm06w5lvvdwwvxd2qrmnj4kng",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libyml@0.0.5": "106963pwg1gc3165bdlk8bbspmk919gk10vshhqglks3z8m700ik",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.4.14": "12gsjgbhhjwywpqcrizv80vrp7p7grsz5laqq773i33wphjsxcvq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.12": "05qvxa6g27yyva25a5ghsg85apdxkvr77yhkyhapj6r8vnf8pbq7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#log@0.3.9": "0jq23hhn5h35k7pa8r7wqnsywji6x3wn1q5q7lif5q536if8v7p1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#log@0.4.22": "093vs0wkm1rgyykk7fjbqp2lwizbixac1w52gv109p5r4jh0p9x7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#logger@0.4.0": "14xlxvkspcfnspjil0xi63qj5cybxn1hjmr5gq8m4v1g9k5p54bc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#matches@0.1.10": "1994402fq4viys7pjhzisj4wcw894l53g798kkm2y74laxk0jci5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#md-5@0.10.6": "1kvq5rnpm4fzwmyv5nmnxygdhhb2369888a06gdc9pxyrzh7x7nq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.4": "18z32bhxrax0fnjikv475z7ii718hq457qwmaryixfxsl2qrmjkq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.1": "12i17wh9a9plx869g7j4whf62xw68k5zd4k0k5nh6ys5mszid028",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime@0.2.6": "1q1s1ax1gaz8ld3513nvhidfwnik5asbs1ma3hp6inp5dn56nqms",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17": "16hkibgvb9klh0w0jk5crr5xv90l3wlf77ggymzjmvl1818vnxv8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime_guess@1.8.8": "18qcd5aa3363mb742y7lf39j7ha88pkzbv9ff2qidlsdxsjjjs91",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime_guess@2.0.5": "03jmg3yx6j39mg0kayf7w4a886dl3j15y8zs119zw01ccy74zi7p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#minimal-lexical@0.2.1": "16ppc5g84aijpri4jzv14rvcnslvlpphbszc7zzp6vfkddf4qdb8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.3.7": "0dblrhgbm0wa8jjl8cjp81akaj36yna92df4z1h9b26n3spal7br",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.4.4": "0jsfv00hl5rmx1nijn59sr9jmjd4rjnjhh4kdjy8d187iklih9d9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.7.4": "024wv14aa75cvik7005s5y2nfc8zfidddbd7g55g7sjgnzfl18mq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.8.0": "1wadxkg6a6z4lr7kskapj5d8pxlx7cp1ifw4daqnkzqjxych5n72",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mio@1.0.2": "1v1cnnn44awxbcfm4zlavwgkvbyg7gp5zzjm8mqf1apkrwflvq40",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#modifier@0.1.0": "0n3fmgli1nsskl0whrfzm1gk0rmwwl6pw1q4nb9sqqmn5h8wkxa1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#multer@2.1.0": "1hjiphaypj3phqaj5igrzcia9xfmf4rr4ddigbh8zzb96k1bvb01",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nary_tree@0.4.3": "1iqray1a716995l9mmvz5sfqrwg9a235bvrkpcn8bcqwjnwfv1pv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#native-tls@0.2.12": "0rkl65z70n7sy4d5w0qa99klg1hr43wx6kcprk4d2n9xr2r4wqd8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nix@0.27.1": "0ly0kkmij5f0sqz35lx9czlbk6zpihb7yh1bsy4irzwfd2f4xc1f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#no-std-compat@0.4.1": "132vrf710zsdp40yp1z3kgc2ss8pi0z4gmihsz3y7hl4dpd56f5r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nom@7.1.3": "0jha9901wxam390jcf5pfa0qqfrgh8li787jx2ip0yk5b8y9hwyj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-bigint-dig@0.8.4": "0lb12df24wgxxbspz4gw1sf1kdqwvpdcpwq4fdlwg4gj41c1k16w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-conv@0.1.0": "1ndiyg82q73783jq18isi71a7mjh56wxrk52rlvyx0mi5z9ibmai",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.46": "13w5g54a9184cqlbsq80rnxw4jj4s0d8wv75jsq5r2lms8gncsbr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-iter@0.1.45": "1gzm7vc5g9qsjjl3bqk9rz1h6raxhygbrcpbfl04swlh0i506a8l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-rational@0.3.2": "01sgiwny9iflyxh2xz02sak71v2isc3x608hfdpwwzxi3j5l5b0j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19": "0h984rhdkkqd4ny9cif7y2azl3xdfb7768hb9irhpsch4q3gq787",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num_cpus@1.16.0": "0hra6ihpnh06dvfvz9ipscys0xfqa9ca9hzp384d5m02ssvgqqa1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#object@0.36.5": "0gk8lhbs229c68lapq6w6qmnm4jkj48hrcw5ilfyswy514nhmpxf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.20.2": "0xb7rw1aqr7pa4z3b00y7786gyf8awx2gca3md73afy76dzgwq8j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl-macros@0.1.1": "173xxvfc63rr5ybwqwylsir0vq6xsj4kxiv4hmg4c3vscdmncj59",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl-probe@0.1.5": "1kq18qm48rvkwgcggfkqq6pm948190czqc94d6bm2sir5hq1l0gz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl-sys@0.9.103": "1mi9r5vbgqqwfa2nqlh2m0r1v5abhzjigfbi7ja0mx0xx7p8v7kz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.66": "1hfr9ffx67j455aqrmyys3c8l65ngbqrl5qi3v3fi8vhddwg8acm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pango-sys@0.18.0": "1iaxalcaaj59cl9n10svh4g50v8jrc1a36kd7n9yahx8j7ikfrs3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pango@0.18.3": "1r5ygq7036sv7w32kp8yxr6vgggd54iaavh3yckanmq4xg0px8kw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parking@2.2.1": "1fnfgmzkfpjd69v4j9x737b1k8pnn054bvzcn5dm3pkgq595d3gk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parking_lot@0.12.3": "09ws9g6245iiq8z975h8ycf818a66q3c6zv4b5h8skpm7hc1igzi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.10": "1y3cf9ld9ijf7i4igwzffcn0xl16dxyn4c5bwgjck1dkgabiyh0y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parse-zoneinfo@0.3.1": "093cs8slbd6kyfi6h12isz0mnaayf5ha8szri1xrbqj4inqhaahz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#paste@1.0.15": "02pxffpdqkapy292harq6asfjvadgp1s005fip9ljfsn9fvxgh2p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pem-rfc7468@0.7.0": "04l4852scl4zdva31c1z6jafbak0ni5pi0j38ml108zwzjdrrcw8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@1.0.1": "0cgq08v1fvr6bs5fvy390cz830lq4fak8havdasdacxcw790s09i",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.1": "0gi8wgx0dcy8rnv1kywdv98lwcx67hz0a0zwpib5v2i08r88y573",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf@0.11.2": "1p03rsw66l7naqhpgr1a34r9yzi1gv9jh16g3fsk6wrwyfwdiqmd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf@0.7.24": "066xwv4dr6056a9adlkarwp4n94kbpwngbmd47ngm3cfbyw49nmk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.11.2": "0nia6h4qfwaypvfch3pnq1nd2qj64dif4a6kai3b7rjrsf49dlz8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.7.24": "0zjiblicfm0nrmr2xxrs6pnf6zz2394wgch6dcbd8jijkq98agmh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.11.2": "1c14pjyxbcpwkdgw109f7581cc5fa3fnkzdq1ikvx7mdq9jcrr28",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.7.24": "0qi62gxk3x3whrmw5c4i71406icqk11qmpgln438p6qm7k4lqdh9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.2": "0azphb0a330ypqx3qvyffal5saqnks0xvl8rj73jlk3qxxgbkz4h",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.7.24": "18371fla0vsj7d6d5rlfb747xbr2in11ar9vgv5qna72bnhp2kr3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-project-internal@1.1.7": "133mxf5vmvnvw4idw2y2lb5bxsza2xlyfl6psjy7mz3l12nmy3rw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.14": "00nx3f04agwjlsmd3mc5rx5haibj2v8q9b52b0kwn63wcv4nz9mx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.7": "15cvflrzsgp1zbl5gv37al2r62nl8lc37xkfwf70ql3fji7gcmxy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-utils@0.1.0": "117ir7vslsl2z1a7qzhws4pd01cg2d3338c47swjyvqv2n60v1wb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#piper@0.2.4": "0rn0mjjm0cwagdkay77wgmz3sqf8fqmv9d9czm79mvr2yj8c9j4n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pipewire-sys@0.8.0": "04hiy3rl8v3j2dfzp04gr7r8l5azzqqsvqdzwa7sipdij27ii7l4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pipewire@0.8.0": "1nldg1hz4v0qr26lzdxqpvrac4zbc3pb6436sl392425bjx4brh8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pkcs1@0.7.5": "0zz4mil3nchnxljdfs2k5ab1cjqn7kq5lqp62n9qfix01zqvkzy8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pkcs8@0.10.2": "1dx7w21gvn07azszgqd3ryjhyphsrjrmq5mmz1fbxkj5g0vv4l7r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pkg-config@0.3.31": "1wk6yp2phl91795ia0lwkr3wl4a9xkrympvhqq8cxk4d75hwhglm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#plugin@0.2.6": "1q7nghkpvxxr168y2jnzh3w7qc9vfrby9n7ygy3xpj0bj71hsshs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#png@0.16.8": "1ipl44q3vy4kvx6j296vk7d4v8gvcg203lrkvvixwixq1j98fciw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#png@0.17.14": "1w130qw3cngzppxk1yp3ls2pbw3f0spbzhkbarbnlnm06imd9yaj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#polling@3.7.3": "04b5zdgz0m9ydbzcr3f9a55749gqbj0y89d0nz9nrv0x636r09yc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#powerfmt@0.2.0": "14ckj2xdpkhv3h6l5sdmb9f1d57z8hbfpdldjc2vl5givq2y77j3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ppv-lite86@0.2.20": "017ax9ssdnpww7nrl1hvqh2lzncpv04nnsibmnw9nxjnaqlpp5bp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pretty_env_logger@0.5.0": "076w9dnvcpx6d3mdbkqad8nwnsynb7c8haxmscyrz7g3vga28mw6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-crate@1.3.1": "069r1k56bvgk0f58dm5swlssfcp79im230affwk6d9ck20g04k3z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-crate@2.0.2": "092x5acqnic14cw6vacqap5kgknq3jn4c6jij9zi6j85839jc3xh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error-attr@1.0.4": "0sgq6m5jfmasmwwy8x4mjygx5l7kp8s4j60bv25ckv2j1qc41gm1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error@1.0.4": "1373bhxaf0pagd8zkyd03kkx6bchzf6g0dkwrwzsnal9z47lj9fs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.89": "0vlq56v41dsj69pnk7lil7fxvbfid50jnzdn3xnr31g05mkb0fgi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proptest@1.5.0": "13gm7mphs95cw4gbgk5qiczkmr68dvcwhp58gmiz33dq2ccm3hml",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#qoi@0.4.1": "00c0wkb112annn2wl72ixyd78mf56p4lxkhlmsggx65l3v3n8vbz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#quick-error@1.2.3": "1q6za3v78hsspisc197bg3g7rpc989qycy8ypr8ap8igv10ikl51",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.37": "1brklraw2g34bxy9y4q1nbrccn7bv36ylihv12c9vlcii55x7fdm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.3.23": "0v679h38pjjqj5h4md7v2slsvj6686qgcn7p9fbw3h43iwnk1b34",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.4.6": "14qjfv3gggzhnma20k0sc1jf8y6pplsaq7n1j9ls5c8kf2wl0a2m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.6.5": "1jl4449jcl4wgmzld6ffwqj5gwxrp8zvx8w573g1z368qg6xlwbd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5": "013l6931nn7gkc23jz5mm3qdhf93jjf0fg64nz2lp4i51qd8vbrl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.1.1": "1vxwyzs4fy1ffjc8l00fsyygpiss135irjf7nyxgq2v0lqf3lvam",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.3.1": "123x2adin558xbhvqb8w4f6syjsdkmqff8cxwhmjacpsl1ihmhg6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.3.1": "0jzdgszfa4bliigiy4hi66k7fs3gfwi2qxn8vik84ph77fwdwvvs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.4.2": "1p09ynysrq1vcdlmcqnapq4qakl2yd1ng3kxh3qscpx09k2a6cww",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.6.4": "0b4j2v4cb5krak1pv6kakv4sz6xcwbrmy2zckc32hsigbrwy82zc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_hc@0.1.0": "1i0vl8q5ddvvy0x8hf1zxny393miyzxkwqnw31ifg6p0gdy6fh3v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_isaac@0.1.1": "027flpjr4znx2csxk7gxb7vrf9c7y5mydmvg5az2afgisp4rgnfy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_jitter@0.1.4": "16z387y46bfz3csc42zxbjq89vcr1axqacncvv8qhyy93p4xarhi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_os@0.1.3": "0wahppm0s64gkr2vmhcgwc0lij37in1lgfxg5rbgqlz0l5vgcxbv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_pcg@0.1.2": "0i0bdla18a8x4jn1w0fxsbs3jg7ajllz6azmch1zw33r06dv1ydb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_xorshift@0.1.1": "0p2x8nr00hricpi2m6ca5vysiha7ybnghz79yqhhx6sl4gkfkxyb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_xorshift@0.3.0": "13vcag7gmqspzyabfl1gr9ykvxd2142q2agrj8dkyjmfqmgg4nyj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rayon-core@1.12.1": "1qpwim68ai5h0j7axa8ai8z0payaawv3id0lrgkqmapx7lx8fr8l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rayon@1.10.0": "1ylgnzwgllajalr4v00y4kj22klq2jbwllm70aha232iah0sc65l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rdrand@0.4.0": "1cjq0kwx1bk7jx3kzyciiish5gqsj7620dm43dc52sr8fzmm9037",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#redox_syscall@0.5.7": "07vpgfr6a04k0x19zqr1xdlqm6fncik3zydbdi3f5g3l5k7zwvcv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.8": "18wd530ndrmygi6xnz3sp345qi0hy2kdbsa89182nwbl6br5i1rn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.5": "0p41p3hj9ww7blnbwbj9h7rwxzxg0c1hvrdycgys8rxyhqqw859b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#regex@1.11.0": "1n5imk7yxam409ik5nagsjpwqvbg3f0g0mznd5drf549x1g0w81q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#remove_dir_all@0.5.3": "1rzqbsgkmr053bxxl04vmvsd1njyz0nxvly97aip6aa2cmb15k9s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#reqwest@0.11.27": "0qjary4hpplpgdi62d2m0xvbn6lnzckwffm0rgkm2x51023m6ryx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rsa@0.9.6": "1z0d1aavfm0v4pv8jqmqhhvvhvblla1ydzlvwykpc3mkzhj523jx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustc-demangle@0.1.24": "07zysaafgrkzy2rjgwqdj2a8qdpsm6zv6f5pgpk9x0lm40z9b6vi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustc-hash@1.1.0": "1qkc5khrmv5pqi5l5ca9p5nl5hs742cagrndhbrlk3dhlrx3zm08",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustc_version@0.4.1": "14lvdsmr5si5qbqzrajgb6vfn69k0sfygrvfvr2mps26xwi3mjyg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustix@0.38.37": "04b8f99c2g36gyggf4aphw8742k2b1vls3364n2z493whj5pijwa",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustls-pemfile@1.0.4": "1324n5bcns0rnw6vywr5agff3rwfvzphi7rmbyzwnv6glkhclx0w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rusty-fork@0.3.0": "0kxwq5c480gg6q0j3bg4zzyfh2kwmc3v2ba94jw8ncjc8mpcqgfb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.18": "17xx2s8j1lln7iackzd9p0sv546vjq71i779gphjq923vjh5pjzk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#safemem@0.3.3": "0wp0d2b2284lw11xhybhaszsczpbq1jbdklkxgifldcknmy3nw7g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#schannel@0.1.26": "1hfip5mdwqcfnmrnkrq9d8zwy6bssmf6rfm2441nk83ghbjpn8h1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#scoped-tls@1.0.1": "15524h04mafihcvfpgxd8f4bgc3k95aclz8grjkg9a0rxcvn9kz1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#scoped_threadpool@0.1.9": "1a26d3lk40s9mrf4imhbik7caahmw2jryhhb6vqv6fplbbgzal8x",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0": "0jcz9sd47zlsgcnm1hdw0664krxwb5gczlif4qngj2aif8vky54l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#security-framework-sys@2.12.0": "1dml0lp9lrvvi01s011lyss5kzzsmakaamdwsxr0431jd4l2jjpa",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#security-framework@2.11.1": "00ldclwx78dm61v7wkach9lcx76awlrv0fdgjdwch4dmy12j4yw9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#self_cell@0.10.3": "0pci3zh23b7dg6jmlxbn8k4plb7hcg5jprd1qiz0rp04p1ilskp1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#self_cell@1.0.4": "0jki9brixzzy032d799xspz1gikc5n2w81w8q8yyn8w6jxpsjsfk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#semver@1.0.23": "12wqpxfflclbq4dv8sa6gchdh92ahhwn4ci1ls22wlby3h57wsb1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde@0.9.15": "1bsla8l5xr9pp5sirkal6mngxcq6q961km88jvf339j5ff8j7dil",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.210": "0flc0z8wgax1k4j5bf2zyq48bgzyv425jkd5w0i6wbh7f8j5kqy8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.210": "07yzy4wafk79ps0hmbqmsqh5xjna4pm4q57wc847bb8gl3nh4f94",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.128": "1n43nia50ybpcfmh3gcw4lcc627qsg9nyakzwgkk9pm10xklbxbg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.8": "1q89g70azwi4ybilz5jb8prfpa575165lmrffd49vmcf76qpqq47",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_urlencoded@0.7.1": "1zgklbdaysj3230xivihs30qi5vkhigg323a9m62k8jwf4a1qjfk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_yml@0.0.12": "1p8xwz4znd6fj962y22fdvvv16gb8c0hx4iv5hjplngiidcdvqjr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sha1@0.10.6": "1fnnxlfg08xhkmwf2ahv634as30l1i3xhlhkvxflmasi5nd85gz3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sha2@0.10.8": "1j1x78zk9il95w9iv46dh9wm73r6xrgj32y6lzzw7bxws9dbfgbr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0": "0r1y6bv26c1scpxvhg2cabimrmwgbp4p3wy6syj9n0c4s3q2znhg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.2": "1cb5akgq8ajnd5spyn587srvs4n26ryq0p78nswffwhv46sf1sd9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#signature@2.2.0": "1pi9hd5vqfr3q3k49k37z06p7gs5si0in32qia4mmr1dancr6m3p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#simd-adler32@0.3.7": "1zkq40c3iajcnr5936gjp9jjh1lpzhy44p3dq3fiw75iwr1w2vfn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#siphasher@0.2.3": "1b53m53l24lyhr505lwqzrpjyq5qfnic71mynrcfvm43rybf938b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#siphasher@0.3.11": "03axamhmwsrmh0psdw3gf7c0zc4fyl5yjxfifz9qfka6yhkqid9q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#slab@0.4.9": "0rxvsgir0qw5lkycrqgb1cxsvxzjv9bmx73bk5y42svnzfba94lg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#smallvec@1.13.2": "0rsw5samawl3wsw6glrsb127rx6sh89a8wyikicw6dkdcjd1lpiw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#snowflake@1.3.0": "1wadr7bxdxbmkbqkqsvzan6q1h3mxqpxningi3ss3v9jaav7n817",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#socket2@0.5.7": "070r941wbq76xpy039an4pyiy3rfj7mp7pvibf1rcri9njq5wc6f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#spin@0.9.8": "0rvam5r0p3a6qhc18scqpvpgb3ckzyqxpgdfyjnghh8ja7byi039",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#spki@0.7.3": "17fj8k5fmx4w9mp27l970clrh5qa7r5sjdvbsln987xhb34dc7nr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlformat@0.2.6": "14470h40gn0f6jw9xxzbpwh5qy1fgvkhkfz8xjyzgi0cvf9kmfkv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-core@0.7.4": "1xiyr35dq10sf7lq00291svcj9wbaaz1ihandjmrng9a6jlmkfi4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros-core@0.7.4": "1j7k0fw7n6pgabqnj6cbp8s3rmd3yvqr4chjj878cvd1m99yycsq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros@0.7.4": "09rih250868nfkax022y5dyk24a7qfw6scjy3sgalbzb8lihx92f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-mysql@0.7.4": "066lxhb80xgb8r5m2yy3a7ydjvp0b6wsk9s7whwfa83d46817lqy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-postgres@0.7.4": "0zjp30wj4n2f25dnb32vsg6jfpa3gw6dmfd0i5pr4kw91fw4x0kw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-sqlite@0.7.4": "1ap0bb2hazbrdgd7mhnckdg9xcchx0k094di9gnhpnhlhh5fyi5j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx@0.7.4": "1ahadprvyhjraq0c5712x3kdkp1gkwfm9nikrmcml2h03bzwr8n9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#stringprep@0.1.5": "1cb3jis4h2b767csk272zw92lc6jzfzvh8d6m1cd86yqjb9z6kbv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1": "0kzvqlw8hxqb7y598w1s0hxlnmi84sg5vsipp3yg5na5d1rvba3x",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#subtle@2.6.1": "14ijxaymghbl1p0wql9cib5zlwiina7kall6w7g89csprkgbvhhk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#syn@1.0.109": "0ds2if4600bd59wsv7jjgfkayfzy3hnazs394kz6zdkmna8l3dkj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#syn@2.0.79": "147mk4sgigmvsb9l8qzj199ygf0fgb0bphwdsghn8205pz82q4w9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@0.1.2": "0q01lyj0gr9a93n10nxsn8lwbzq97jqd6b768x17c8f7v7gccir0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#system-configuration-sys@0.5.0": "1jckxvdr37bay3i9v52izgy52dg690x5xfg3hd394sv2xf4b2px7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#system-configuration@0.5.1": "1rz0r30xn7fiyqay2dvzfy56cvaa3km74hnbz2d72p97bkf3lfms",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#system-deps@6.2.2": "0j93ryw031n3h8b0nfpj5xwh3ify636xmv8kxianvlyyipmkbrd3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.12.16": "1cg3bnx1gdkdr5hac1hzxy64fhw4g7dqkd0n3dxy5lfngpr1mi31",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tempdir@0.3.7": "1n5n86zxpgd85y0mswrp5cfdisizq2rv3la906g6ipyc03xvbwhm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tempfile@3.13.0": "0nyagmbd4v5g6nzfydiihcn6l9j1w9bxgzyca5lyzgnhcbyckwph",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#termcolor@1.4.1": "0mappjh3fj3p2nmrg4y7qv94rchwi9mzmgmfflr8p2awdj7lyy86",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@1.0.64": "1hvzmjx9iamln854l74qyhs0jl2pg3hhqzpqm9p8gszmf9v4x408",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.64": "114s8lmssxl0c2480s671am88vzlasbaikxbvfv8pyqrq6mzh2nm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tiff@0.6.1": "0ds48vs919ccxa3fv1www7788pzkvpg434ilqkq7sjb5dmqg8lws",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tiff@0.9.1": "0ghyxlz566dzc3scvgmzys11dhq2ri77kb8sznjakijlxby104xs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.2": "1wx3qizcihw6z151hywfzzyd1y5dl804ydyxci6qm07vbakpr4pg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time-macros@0.2.18": "1kqwxvfh2jkpg38fy673d6danh1bhcmmbsmffww3mphgail2l99z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time@0.1.45": "0nl0pzv9yf56djy8y5dx25nka5pr2q1ivlandb3d24pksgx7ly8v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time@0.3.36": "11g8hdpahgrf1wwl2rpsg5nxq3aj7ri6xr672v4qcij6cgjqizax",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.6": "0bxqaw7z8r2kzngxlzlgvld1r6jbnwyylyvyjbv1q71rvgaga5wi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tinyvec@1.8.0": "0f5rf6a2wzyv6w4jmfga9iw7rp9fp5gf4d604xgjsf3d9wgqhpj4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tinyvec_macros@0.1.1": "081gag86208sc3y6sdkshgw3vysm5d34p431dzw0bshz66ncng0z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-macros@2.4.0": "0lnpg14h1v3fh2jvnc8cz7cjf0m7z1xgkwfpcyy632g829imjgb9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-native-tls@0.3.1": "1wkfg6zn85zckmv4im7mv20ca6b1vmlib5xwz9p7g19wjfmpdbmv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-stream@0.1.16": "1wc65gprcsyzqlr0k091glswy96kph90i32gffi4ksyh03hnqkjg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-tungstenite@0.21.0": "0f5wj0crsx74rlll97lhw0wk6y12nhdnqvmnjx002hjn08fmcfy8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.12": "0spc0g4irbnf2flgag22gfii87avqzibwfm0si0d1g0k9ijw7rv1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio@1.40.0": "166rllhfkyqp0fs7sxn6crv74iizi4wzd3cvxkcpmlk52qip1c72",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml@0.8.2": "0g9ysjaqvm2mv8q85xpqfn7hi710hj24sd56k49wyddvvyq8lp8q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.6.3": "0jsy7v8bdvmzsci6imj8fzgd255fmy5fzp6zsri14yrry7i77nkw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.19.15": "08bl7rp5g6jwmfpad9s8jpw8wjrciadpnbaswgywpr9hv9qbfnqv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.20.2": "0f7k5svmxw98fhi28jpcyv7ldr2s3c867pjbji65bdxjpd44svir",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3": "1hzfkvkci33ra94xjx64vv3pp0sq346w06fpkcdwjcid7zhvdycd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.27": "1rvb5dn9z6d0xdj14r403z0af0bbaqhg02hq4jc97g5wds6lqw1l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.32": "0m5aglin3cdwxpvbg6kz0r9r0k31j48n0kcfwsp6l49z26k3svf0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.40": "1vv48dac9zgj9650pg2b4d0j3w6f3x9gbggf43scq5hrlysklln3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#traitobject@0.1.0": "0yb0n8822mr59j200fyr2fxgzzgqljyxflx9y8bdy3rlaqngilgg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#try-lock@0.2.5": "0jqijrrvm1pyq34zn1jmy2vihd4jcrjlvsh4alkjahhssjnsn8g4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tungstenite@0.21.0": "1qaphb5kgwgid19p64grhv2b9kxy7f1059yy92l9kwrlx90sdwcy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#type-map@0.5.0": "17qaga12nkankr7hi2mv43f4lnc78hg480kz6j9zmy4g0h28ddny",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typeable@0.1.2": "11w8dywgnm32hb291izjvh4zjd037ccnkk77ahk63l913zwzc40l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typemap@0.3.3": "1xm1gbvz9qisj1l6d36hrl9pw8imr8ngs6qyanjnsad3h0yfcfv5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typenum@1.17.0": "09dqxv69m9lj9zvv6xw5vxaqx15ps0vxyy5myg33i0kbqvq0pzs2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typeshare-annotation@1.0.4": "0kx38ah6638pkqq5cac7nmvbg6x43v7fj5jgibla4lj8fv1dc5d6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typeshare@1.0.3": "11riglm8incm0vq7ciyd907w1sc6frfn7h7ab0yp8bkcnycp7w84",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unarray@0.1.4": "154smf048k84prsdgh09nkm2n0w0336v84jd4zikyn6v6jrqbspa",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unic-langid-impl@0.9.5": "1rckyn5wqd5h8jxhbzlbbagr459zkzg822r4k5n30jaryv0j4m0a",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unic-langid@0.9.5": "0i2s024frmpfa68lzy8y8vnb1rz3m9v0ga13f7h2afx7f8g9vp93",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicase@1.4.2": "0cwazh4qsmm9msckjk86zc1z35xg7hjxjykrgjalzdv367w6aivz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicase@2.7.0": "12gd74j79f94k4clxpf06l99wiv4p30wjr0qm04ihqk9zgdd9lpp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-bidi@0.3.17": "14vqdsnrm3y5anj6h5zz5s32w88crraycblb88d9k23k9ns7vcas",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.13": "1zm1xylzsdfvm2a5ib9li3g5pp7qnkv4amhspydvgbmd9k6mc6z9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-normalization@0.1.24": "0mnrk809z3ix1wspcqy97ld5wxdb31f3xz6nsvg5qcv289ycjcsh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-properties@0.1.3": "1l3mbgzwz8g14xcs09p4ww3hjkjcf0i1ih13nsg72bhj8n5jl3z7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-segmentation@1.12.0": "14qla2jfx74yyb9ds3d2mpwpa4l4lzb9z57c6d2ba511458z5k7n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-width@0.1.14": "1bzn2zv0gp8xxbxbhifw778a7fc93pa6a1kj24jgg9msj07f7mkx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode_categories@0.1.1": "0kp1d7fryxxm7hqywbk88yb9d1avsam9sg76xh36k5qx2arj9v1r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unsafe-any@0.4.2": "0zwwphsqkw5qaiqmjwngnfpv9ym85qcsyj7adip9qplzjzbn00zk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#url@1.7.2": "0nim1c90mxpi9wgdw2xh8dqd72vlklwlzam436akcrhjac6pqknx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#url@2.5.2": "0v2dx50mx7xzl9454cl5qmpjnhkbahmn59gd3apyipbgyyylsy12",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#urlencoding@2.1.3": "1nj99jp37k47n0hvaz5fvz7z6jd0sb4ppvfy3nphr1zbnyixpy6s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#utf-8@0.7.6": "1a9ns3fvgird0snjkd3wbdhwd3zdpc2h5gpyybrfr6ra5pkqxk09",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2": "088807qwjq46azicqwbhlmzwrbkz7l4hpw43sdkdyyk524vdxaq6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#uuid@0.4.0": "0cdj2v6v2yy3zyisij69waksd17cyir1n58kwyk1n622105wbzkw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#uuid@0.8.2": "1dy4ldcp7rnzjy56dxh7d2sgrcvn4q77y0a8r0a48946h66zjp5w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#uuid@1.10.0": "0503gvp08dh5mnm3f0ffqgisj6x3mbs53dmnn1lm19pga43a1pw1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#value-bag@1.9.0": "00aij8p1n7vcggkb9nxpwx9g5nqzclrf7prd1wpi9c3sscvw312s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#vcpkg@0.2.15": "09i4nf5y8lig6xgj3f7fyrvzd3nlaw4znrihw8psidvv5yk4xkdc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#version-compare@0.2.0": "12y9262fhjm1wp0aj3mwhads7kv0jz8h168nn5fb8b43nwf9abl5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#version_check@0.1.5": "1pf91pvj8n6akh7w6j5ypka6aqz08b3qpzgs0ak2kjf4frkiljwi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5": "0nhhi4i5x89gm911azqbn7avs9mdacw2i3vcz3cnmz3mv4rqz4hb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wait-timeout@0.2.0": "1xpkk0j5l9pfmjfh1pi0i89invlavfrd9av5xp0zhxgb29dhy84z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#want@0.3.1": "03hbfrnvqqdchb5kgxyavb9jabwza0dmh2vw5kg0dq8rxl57d9xz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#warp@0.3.7": "07137zd13lchy5hxpspd0hs6sl19b0fv2zc1chf02nwnzw1d4y23",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasi@0.10.0+wasi-snapshot-preview1": "07y3l8mzfzzz4cj09c8y90yak4hpsi9g7pllyzpr6xvwrabka50s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasi@0.11.0+wasi-snapshot-preview1": "08z4hxwkpdpalxjps1ai9y7ihin26y9f476i53dv98v45gkqg3cw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasite@0.1.0": "0nw5h9nmcl4fyf4j5d4mfdjfgvwi1cakpi349wc4zrr59wxxinmq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-backend@0.2.93": "0yypblaf94rdgqs5xw97499xfwgs1096yx026d6h88v563d9dqwx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-futures@0.4.43": "1vf8kmaj95xn5893y1bdlav47y5niq85q5bms9pfj8d6cc7k1sb1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro-support@0.2.93": "0dp8w6jmw44srym6l752nkr3hkplyw38a2fxz5f3j1ch9p3l1hxg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro@0.2.93": "1kycd1xfx4d9xzqknvzbiqhwb5fzvjqrrn88x692q1vblj8lqp2q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-shared@0.2.93": "1104bny0hv40jfap3hp8jhs0q4ya244qcrvql39i38xlghq0lan6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen@0.2.93": "1dfr7pka5kwvky2fx82m9d060p842hc5fyyw8igryikcdb0xybm8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#web-sys@0.3.70": "1h1jspkqnrx1iybwhwhc3qq8c8fn4hy5jcf0wxjry4mxv6pymz96",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#weezl@0.1.8": "10lhndjgs6y5djpg3b420xngcr6jkmv70q8rb1qcicbily35pa2k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#whoami@1.5.2": "0vdvm6sga4v9515l6glqqfnmzp246nq66dd09cw5ri4fyn3mnb9p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi-i686-pc-windows-gnu@0.4.0": "1dmpa6mvcvzz16zg6d5vrfy4bxgg541wxrcip7cnshi06v38ffxc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi-util@0.1.9": "1fqhkcl9scd230cnfj8apfficpf5c9vhwnk4yy9xfc1sw69iq8ng",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi-x86_64-pc-windows-gnu@0.4.0": "0gqq64czqb64kskjryj8isp62m2sgvx25yyj3kpc2myh85w24bki",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi@0.3.9": "06gl025x418lchw1wxj64ycr7gha83m44cjr5sarhynd9xkrm0sw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-core@0.52.0": "1nc3qv7sy24x0nlnb32f7alzpd6f72l4p24vl65vydbyil669ark",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.48.0": "1aan23v5gs7gya1lc46hqn9mdh8yph3fhxmhxlw36pn6pqc28zb7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.52.0": "0gd3v4ji88490zgb6b5mq5zgbvwv7zx1ibn8v3x83rwcdbryaar8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.59.0": "0fw5672ziw8b3zpmnbp9pdv1famk74f1l9fcbc3zsrzdg56vqf0y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.48.5": "034ljxqshifs1lan89xwpcy1hp0lhdh4b5n0d2z4fwjx2piacbws",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.52.6": "0wwrx625nwlfp7k93r2rra568gad1mwd888h1jwnl0vfg5r4ywlv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.48.5": "1n05v7qblg1ci3i567inc7xrkmywczxrs1z3lj3rkkxw18py6f1b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.52.6": "1lrcq38cr2arvmz19v32qaggvj8bh1640mdm9c2fr877h0hn591j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.48.5": "1g5l4ry968p73g6bg6jgyvy9lb8fyhcs54067yzxpcpkf44k2dfw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.52.6": "0sfl0nysnz32yyfh773hpi49b1q700ah6y7sacmjbqjjn5xjmv09",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.48.5": "0gklnglwd9ilqx7ac3cn8hbhkraqisd0n83jxzf9837nvvkiand7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.52.6": "02zspglbykh1jh9pi7gn8g1f97jh1rrccni9ivmrfbl0mgamm6wf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnullvm@0.52.6": "0rpdx1537mw6slcpqa0rm3qixmsb79nbhqy5fsm3q2q9ik9m5vhf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.48.5": "01m4rik437dl9rdf0ndnm2syh10hizvq0dajdkv2fjqcywrw4mcg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.52.6": "0rkcqmp4zzmfvrrrx01260q3xkpzi6fzi2x2pgdcdry50ny4h294",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.48.5": "13kiqqcvz2vnyxzydjh73hwgigsdr2z1xpzx313kxll34nyhmm2k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.52.6": "0y0sifqcb56a56mvn7xjgs8g43p33mfqkd8wj1yhrgxzma05qyhl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.48.5": "1k24810wfbgz8k48c2yknqjmiigmql6kk3knmddkv8k8g1v54yqb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.52.6": "03gda7zjx1qh8k9nnlgb7m3w3s1xkysg55hkd1wjch8pqhyv5m94",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.48.5": "0f4mdp895kkjh9zv8dxvn4pc10xr7839lf5pa9l0193i2pkgr57d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.52.6": "1v7rb5cibyzx8vak29pdrk8nx9hycsjs4w0jgms08qk49jl6v7sq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winnow@0.5.40": "0xk8maai7gyxda673mmw3pj1hdizy5fpi7287vaywykkk19sk4zm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winreg@0.50.0": "1cddmp929k882mdh6i9f2as848f13qqna6czwsqzkh1pqnr5fkjj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#yansi-term@0.1.2": "1w8vjlvxba6yvidqdvxddx3crl6z66h39qxj8xi6aqayw2nk0p7y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.7.35": "0gnf2ap2y92nwdalzz3x7142f2b83sni66l39vxp2ijd6j080kzs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.7.35": "1w36q7b9il2flg0qskapgi9ymgg7p985vniqd09vi0mwib8lz6qv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.1": "1pjdrmjwmszpxfd7r860jx54cyk94qk59x13sc307cvr5256glyf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zune-inflate@0.2.54": "00kg24jh3zqa3i6rg6yksnb71bch9yi1casqydl00s7nw8pk7avk"
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "cyber-slides"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-std = "1.13.0"
|
||||||
|
cairo-rs = "0.18"
|
||||||
|
cyberpunk = { path = "../cyberpunk" }
|
||||||
|
gio = "0.18"
|
||||||
|
glib = "0.18"
|
||||||
|
gtk = { version = "0.7", package = "gtk4" }
|
||||||
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
|
serde_yml = "0.0.12"
|
|
@ -0,0 +1,416 @@
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::HashMap,
|
||||||
|
fs::File,
|
||||||
|
io::Read,
|
||||||
|
ops::Index,
|
||||||
|
path::Path,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use cairo::{Context, Rectangle};
|
||||||
|
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, Text};
|
||||||
|
use glib::{GString, Object};
|
||||||
|
use gtk::{
|
||||||
|
glib::{self, Propagation},
|
||||||
|
prelude::*,
|
||||||
|
subclass::prelude::*,
|
||||||
|
EventControllerKey,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
const FPS: u64 = 60;
|
||||||
|
const PURPLE: (f64, f64, f64) = (0.7, 0., 1.);
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum Position {
|
||||||
|
Top,
|
||||||
|
Middle,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
struct Step {
|
||||||
|
text: String,
|
||||||
|
position: Position,
|
||||||
|
transition: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
struct Script(Vec<Step>);
|
||||||
|
|
||||||
|
impl Script {
|
||||||
|
fn from_file(path: &Path) -> Result<Script, serde_yml::Error> {
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let mut f = File::open(path).unwrap();
|
||||||
|
f.read_to_end(&mut buf).unwrap();
|
||||||
|
let script = serde_yml::from_slice(&buf)?;
|
||||||
|
Ok(Self(script))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a Step> {
|
||||||
|
self.0.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Script {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for Script {
|
||||||
|
type Output = Step;
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
|
&self.0[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Fade {
|
||||||
|
text: String,
|
||||||
|
position: Position,
|
||||||
|
duration: Duration,
|
||||||
|
|
||||||
|
start_time: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Animation {
|
||||||
|
fn position(&self) -> Position;
|
||||||
|
|
||||||
|
fn tick(&self, now: Instant, context: &Context, width: f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation for Fade {
|
||||||
|
fn position(&self) -> Position {
|
||||||
|
self.position.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(&self, now: Instant, context: &Context, width: f64) {
|
||||||
|
let total_frames = self.duration.as_secs() * FPS;
|
||||||
|
let alpha_rate: f64 = 1. / total_frames as f64;
|
||||||
|
|
||||||
|
let frames = (now - self.start_time).as_secs_f64() * FPS as f64;
|
||||||
|
let alpha = alpha_rate * frames as f64;
|
||||||
|
|
||||||
|
let text_display = Text::new(self.text.clone(), context, 64., width);
|
||||||
|
let _ = context.move_to(0., text_display.extents().height());
|
||||||
|
let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha);
|
||||||
|
text_display.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CrossFade {
|
||||||
|
old_text: String,
|
||||||
|
new_text: String,
|
||||||
|
position: Position,
|
||||||
|
duration: Duration,
|
||||||
|
|
||||||
|
start_time: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation for CrossFade {
|
||||||
|
fn position(&self) -> Position {
|
||||||
|
self.position.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(&self, now: Instant, context: &Context, width: f64) {
|
||||||
|
let total_frames = self.duration.as_secs() * FPS;
|
||||||
|
let alpha_rate: f64 = 1. / total_frames as f64;
|
||||||
|
|
||||||
|
let frames = (now - self.start_time).as_secs_f64() * FPS as f64;
|
||||||
|
let alpha = alpha_rate * frames as f64;
|
||||||
|
|
||||||
|
let text_display = Text::new(self.old_text.clone(), context, 64., width);
|
||||||
|
let _ = context.move_to(0., text_display.extents().height());
|
||||||
|
let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, 1. - alpha);
|
||||||
|
text_display.draw();
|
||||||
|
|
||||||
|
let text_display = Text::new(self.new_text.clone(), context, 64., width);
|
||||||
|
let _ = context.move_to(0., text_display.extents().height());
|
||||||
|
let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha);
|
||||||
|
text_display.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CyberScreenState {
|
||||||
|
script: Script,
|
||||||
|
idx: Option<usize>,
|
||||||
|
top: Option<Step>,
|
||||||
|
middle: Option<Step>,
|
||||||
|
bottom: Option<Step>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CyberScreenState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
script: Script(vec![]),
|
||||||
|
idx: None,
|
||||||
|
top: None,
|
||||||
|
middle: None,
|
||||||
|
bottom: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CyberScreenState {
|
||||||
|
fn new(script: Script) -> CyberScreenState {
|
||||||
|
let mut s = CyberScreenState::default();
|
||||||
|
s.script = script;
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_page(&mut self) -> Box<dyn Animation> {
|
||||||
|
let idx = match self.idx {
|
||||||
|
None => 0,
|
||||||
|
Some(idx) => {
|
||||||
|
if idx < self.script.len() {
|
||||||
|
idx + 1
|
||||||
|
} else {
|
||||||
|
idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.idx = Some(idx);
|
||||||
|
let step = self.script[idx].clone();
|
||||||
|
|
||||||
|
let (old, new) = match step.position {
|
||||||
|
Position::Top => {
|
||||||
|
let old = self.top.replace(step.clone());
|
||||||
|
(old, step)
|
||||||
|
}
|
||||||
|
Position::Middle => {
|
||||||
|
let old = self.middle.replace(step.clone());
|
||||||
|
(old, step)
|
||||||
|
}
|
||||||
|
Position::Bottom => {
|
||||||
|
let old = self.bottom.replace(step.clone());
|
||||||
|
(old, step)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match old {
|
||||||
|
Some(old) => Box::new(CrossFade {
|
||||||
|
old_text: old.text.clone(),
|
||||||
|
new_text: new.text.clone(),
|
||||||
|
position: new.position,
|
||||||
|
duration: new.transition,
|
||||||
|
start_time: Instant::now(),
|
||||||
|
}),
|
||||||
|
None => Box::new(Fade {
|
||||||
|
text: new.text.clone(),
|
||||||
|
position: new.position,
|
||||||
|
duration: new.transition,
|
||||||
|
start_time: Instant::now(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CyberScreenPrivate {
|
||||||
|
state: Rc<RefCell<CyberScreenState>>,
|
||||||
|
// For crossfading to work, I have to detect that there is an old animation in a position, and
|
||||||
|
// replace it with the new one.
|
||||||
|
animations: Rc<RefCell<HashMap<Position, Box<dyn Animation>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for CyberScreenPrivate {
|
||||||
|
const NAME: &'static str = "CyberScreen";
|
||||||
|
type Type = CyberScreen;
|
||||||
|
type ParentType = gtk::DrawingArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for CyberScreenPrivate {}
|
||||||
|
impl WidgetImpl for CyberScreenPrivate {}
|
||||||
|
impl DrawingAreaImpl for CyberScreenPrivate {}
|
||||||
|
|
||||||
|
impl CyberScreenPrivate {
|
||||||
|
fn set_script(&self, script: Script) {
|
||||||
|
*self.state.borrow_mut() = CyberScreenState::new(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_page(&self) {
|
||||||
|
let transition = self.state.borrow_mut().next_page();
|
||||||
|
self.animations
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(transition.position(), transition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct CyberScreen(ObjectSubclass<CyberScreenPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CyberScreen {
|
||||||
|
fn new(script: Script) -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.imp().set_script(script);
|
||||||
|
|
||||||
|
s.set_draw_func({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, context, width, height| {
|
||||||
|
let now = Instant::now();
|
||||||
|
let _ = context.set_source_rgb(0., 0., 0.);
|
||||||
|
let _ = context.paint();
|
||||||
|
|
||||||
|
let pen = GlowPen::new(width, height, 2., 8., (0.7, 0., 1.));
|
||||||
|
AsymLineCutout {
|
||||||
|
orientation: gtk::Orientation::Horizontal,
|
||||||
|
start_x: 25.,
|
||||||
|
start_y: height as f64 / 7.,
|
||||||
|
start_length: width as f64 / 3.,
|
||||||
|
cutout_length: width as f64 / 3. - 100.,
|
||||||
|
height: 50.,
|
||||||
|
end_length: width as f64 / 3. - 50.,
|
||||||
|
invert: false,
|
||||||
|
}
|
||||||
|
.draw(&pen);
|
||||||
|
pen.stroke();
|
||||||
|
|
||||||
|
AsymLine {
|
||||||
|
orientation: gtk::Orientation::Horizontal,
|
||||||
|
start_x: width as f64 / 4.,
|
||||||
|
start_y: height as f64 * 6. / 7.,
|
||||||
|
start_length: width as f64 * 2. / 3. - 25.,
|
||||||
|
height: 50.,
|
||||||
|
end_length: 0.,
|
||||||
|
invert: false,
|
||||||
|
}
|
||||||
|
.draw(&pen);
|
||||||
|
pen.stroke();
|
||||||
|
|
||||||
|
let tracery = pen.finish();
|
||||||
|
let _ = context.set_source(tracery);
|
||||||
|
let _ = context.paint();
|
||||||
|
|
||||||
|
let mut animations = s.imp().animations.borrow_mut();
|
||||||
|
|
||||||
|
let lr_margin = 50.;
|
||||||
|
let max_width = width as f64 - lr_margin * 2.;
|
||||||
|
let region_height = height as f64 / 5.;
|
||||||
|
|
||||||
|
if let Some(animation) = animations.get(&Position::Top) {
|
||||||
|
let y = height as f64 * 1. / 5.;
|
||||||
|
let surface = context
|
||||||
|
.target()
|
||||||
|
.create_for_rectangle(Rectangle::new(20., y, max_width, region_height))
|
||||||
|
.unwrap();
|
||||||
|
let ctx = Context::new(&surface).unwrap();
|
||||||
|
animation.tick(now, &ctx, max_width);
|
||||||
|
}
|
||||||
|
if let Some(animation) = animations.get(&Position::Middle) {
|
||||||
|
let y = height as f64 * 2. / 5.;
|
||||||
|
let surface = context
|
||||||
|
.target()
|
||||||
|
.create_for_rectangle(Rectangle::new(20., y, max_width, region_height))
|
||||||
|
.unwrap();
|
||||||
|
let ctx = Context::new(&surface).unwrap();
|
||||||
|
animation.tick(now, &ctx, max_width);
|
||||||
|
}
|
||||||
|
if let Some(animation) = animations.get(&Position::Bottom) {
|
||||||
|
let y = height as f64 * 3. / 5.;
|
||||||
|
let surface = context
|
||||||
|
.target()
|
||||||
|
.create_for_rectangle(Rectangle::new(20., y, max_width, region_height))
|
||||||
|
.unwrap();
|
||||||
|
let ctx = Context::new(&surface).unwrap();
|
||||||
|
animation.tick(now, &ctx, max_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_page(&self) {
|
||||||
|
self.imp().next_page();
|
||||||
|
self.queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let script = Arc::new(RwLock::new(Script::default()));
|
||||||
|
let app = gtk::Application::builder()
|
||||||
|
.application_id("com.luminescent-dreams.cyberpunk-slideshow")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
app.add_main_option(
|
||||||
|
"script",
|
||||||
|
glib::char::Char::from(b's'),
|
||||||
|
glib::OptionFlags::IN_MAIN,
|
||||||
|
glib::OptionArg::String,
|
||||||
|
"",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.connect_handle_local_options({
|
||||||
|
let script = script.clone();
|
||||||
|
move |_, options| {
|
||||||
|
if let Some(script_path) = options.lookup::<String>("script").unwrap() {
|
||||||
|
let mut script = script.write().unwrap();
|
||||||
|
*script = Script::from_file(Path::new(&script_path)).unwrap();
|
||||||
|
-1
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.connect_activate(move |app| {
|
||||||
|
let window = gtk::ApplicationWindow::new(app);
|
||||||
|
let screen = CyberScreen::new(script.read().unwrap().clone());
|
||||||
|
|
||||||
|
let events = EventControllerKey::new();
|
||||||
|
|
||||||
|
events.connect_key_released({
|
||||||
|
let app = app.clone();
|
||||||
|
let window = window.clone();
|
||||||
|
let screen = screen.clone();
|
||||||
|
move |_, key, _, _| {
|
||||||
|
let name = key
|
||||||
|
.name()
|
||||||
|
.map(|s| s.as_str().to_owned())
|
||||||
|
.unwrap_or("".to_owned());
|
||||||
|
match name.as_ref() {
|
||||||
|
"Right" => screen.next_page(),
|
||||||
|
"q" => app.quit(),
|
||||||
|
"Escape" => window.unfullscreen(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.add_controller(events);
|
||||||
|
|
||||||
|
window.set_child(Some(&screen));
|
||||||
|
window.set_width_request(800);
|
||||||
|
window.set_height_request(600);
|
||||||
|
window.present();
|
||||||
|
|
||||||
|
window.connect_maximized_notify(|window| {
|
||||||
|
window.fullscreen();
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = glib::spawn_future_local({
|
||||||
|
let screen = screen.clone();
|
||||||
|
async move {
|
||||||
|
loop {
|
||||||
|
screen.queue_draw();
|
||||||
|
async_std::task::sleep(Duration::from_millis(1000 / FPS)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.run();
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cairo-rs = { version = "0.18" }
|
cairo-rs = { version = "0.18" }
|
||||||
|
cyberpunk = { path = "../cyberpunk" }
|
||||||
gio = { version = "0.18" }
|
gio = { version = "0.18" }
|
||||||
glib = { version = "0.18" }
|
glib = { version = "0.18" }
|
||||||
gtk = { version = "0.7", package = "gtk4" }
|
gtk = { version = "0.7", package = "gtk4" }
|
||||||
|
|
|
@ -2,6 +2,7 @@ use cairo::{
|
||||||
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern,
|
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern,
|
||||||
TextExtents,
|
TextExtents,
|
||||||
};
|
};
|
||||||
|
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, SlashMeter};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*, EventControllerKey};
|
use gtk::{prelude::*, subclass::prelude::*, EventControllerKey};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -171,7 +172,7 @@ impl SplashPrivate {
|
||||||
start_y: extents.height() + 10.,
|
start_y: extents.height() + 10.,
|
||||||
start_length: 0.,
|
start_length: 0.,
|
||||||
height: extents.height() / 2.,
|
height: extents.height() / 2.,
|
||||||
total_length: extents.width() + extents.height() / 2.,
|
end_length: 0.,
|
||||||
invert: false,
|
invert: false,
|
||||||
}
|
}
|
||||||
.draw(&pen);
|
.draw(&pen);
|
||||||
|
@ -183,7 +184,7 @@ impl SplashPrivate {
|
||||||
start_y: extents.height() + 60.,
|
start_y: extents.height() + 60.,
|
||||||
start_length: extents.width(),
|
start_length: extents.width(),
|
||||||
height: extents.height() / 2.,
|
height: extents.height() / 2.,
|
||||||
total_length: extents.width() + extents.height() / 2.,
|
end_length: 0.,
|
||||||
invert: false,
|
invert: false,
|
||||||
}
|
}
|
||||||
.draw(&pen);
|
.draw(&pen);
|
||||||
|
@ -208,7 +209,7 @@ impl SplashPrivate {
|
||||||
start_x: 20.,
|
start_x: 20.,
|
||||||
start_y: center_y - 20. - title_height / 2.,
|
start_y: center_y - 20. - title_height / 2.,
|
||||||
start_length,
|
start_length,
|
||||||
total_length: *self.width.borrow() as f64 - 120.,
|
end_length: *self.width.borrow() as f64 - 120. - start_length,
|
||||||
cutout_length: title_width,
|
cutout_length: title_width,
|
||||||
height: title_height,
|
height: title_height,
|
||||||
invert: false,
|
invert: false,
|
||||||
|
@ -243,7 +244,7 @@ impl SplashPrivate {
|
||||||
start_y: *self.height.borrow() as f64 / 2. + 100.,
|
start_y: *self.height.borrow() as f64 / 2. + 100.,
|
||||||
start_length: 400.,
|
start_length: 400.,
|
||||||
height: 50.,
|
height: 50.,
|
||||||
total_length: 650.,
|
end_length: 0.,
|
||||||
invert: true,
|
invert: true,
|
||||||
}
|
}
|
||||||
.draw(&pen);
|
.draw(&pen);
|
||||||
|
@ -258,7 +259,7 @@ impl SplashPrivate {
|
||||||
start_y: *self.height.borrow() as f64 / 2. + 200.,
|
start_y: *self.height.borrow() as f64 / 2. + 200.,
|
||||||
start_length: 600.,
|
start_length: 600.,
|
||||||
height: 50.,
|
height: 50.,
|
||||||
total_length: 650.,
|
end_length: 0.,
|
||||||
invert: false,
|
invert: false,
|
||||||
}
|
}
|
||||||
.draw(&pen);
|
.draw(&pen);
|
||||||
|
@ -419,212 +420,6 @@ impl Splash {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AsymLineCutout {
|
|
||||||
orientation: gtk::Orientation,
|
|
||||||
start_x: f64,
|
|
||||||
start_y: f64,
|
|
||||||
start_length: f64,
|
|
||||||
total_length: f64,
|
|
||||||
cutout_length: f64,
|
|
||||||
height: f64,
|
|
||||||
invert: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsymLineCutout {
|
|
||||||
fn draw(&self, pen: &impl Pen) {
|
|
||||||
let dodge = if self.invert {
|
|
||||||
self.height
|
|
||||||
} else {
|
|
||||||
-self.height
|
|
||||||
};
|
|
||||||
match self.orientation {
|
|
||||||
gtk::Orientation::Horizontal => {
|
|
||||||
pen.move_to(self.start_x, self.start_y);
|
|
||||||
pen.line_to(self.start_x + self.start_length, self.start_y);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + self.start_length + self.height,
|
|
||||||
self.start_y + dodge,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + self.start_length + self.height + self.cutout_length,
|
|
||||||
self.start_y + dodge,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x
|
|
||||||
+ self.start_length
|
|
||||||
+ self.height
|
|
||||||
+ self.cutout_length
|
|
||||||
+ (self.height / 2.),
|
|
||||||
self.start_y + dodge / 2.,
|
|
||||||
);
|
|
||||||
pen.line_to(self.total_length, self.start_y + dodge / 2.);
|
|
||||||
}
|
|
||||||
gtk::Orientation::Vertical => {
|
|
||||||
pen.move_to(self.start_x, self.start_y);
|
|
||||||
pen.line_to(self.start_x, self.start_y + self.start_length);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + dodge,
|
|
||||||
self.start_y + self.start_length + self.height,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + dodge,
|
|
||||||
self.start_y + self.start_length + self.height + self.cutout_length,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + dodge / 2.,
|
|
||||||
self.start_y
|
|
||||||
+ self.start_length
|
|
||||||
+ self.height
|
|
||||||
+ self.cutout_length
|
|
||||||
+ (self.height / 2.),
|
|
||||||
);
|
|
||||||
pen.line_to(self.start_x + dodge / 2., self.total_length);
|
|
||||||
}
|
|
||||||
_ => panic!("unknown orientation"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AsymLine {
|
|
||||||
orientation: gtk::Orientation,
|
|
||||||
start_x: f64,
|
|
||||||
start_y: f64,
|
|
||||||
start_length: f64,
|
|
||||||
height: f64,
|
|
||||||
total_length: f64,
|
|
||||||
invert: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsymLine {
|
|
||||||
fn draw(&self, pen: &impl Pen) {
|
|
||||||
let dodge = if self.invert {
|
|
||||||
self.height
|
|
||||||
} else {
|
|
||||||
-self.height
|
|
||||||
};
|
|
||||||
match self.orientation {
|
|
||||||
gtk::Orientation::Horizontal => {
|
|
||||||
pen.move_to(self.start_x, self.start_y);
|
|
||||||
pen.line_to(self.start_x + self.start_length, self.start_y);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + self.start_length + self.height,
|
|
||||||
self.start_y + dodge,
|
|
||||||
);
|
|
||||||
pen.line_to(self.start_x + self.total_length, self.start_y + dodge);
|
|
||||||
}
|
|
||||||
gtk::Orientation::Vertical => {}
|
|
||||||
_ => panic!("unknown orientation"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SlashMeter {
|
|
||||||
orientation: gtk::Orientation,
|
|
||||||
start_x: f64,
|
|
||||||
start_y: f64,
|
|
||||||
count: u8,
|
|
||||||
fill_count: u8,
|
|
||||||
height: f64,
|
|
||||||
length: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SlashMeter {
|
|
||||||
fn draw(&self, context: &Context) {
|
|
||||||
match self.orientation {
|
|
||||||
gtk::Orientation::Horizontal => {
|
|
||||||
let angle: f64 = 0.8;
|
|
||||||
let run = self.height / angle.tan();
|
|
||||||
let width = self.length / (self.count as f64 * 2.);
|
|
||||||
|
|
||||||
for c in 0..self.count {
|
|
||||||
context.set_line_width(1.);
|
|
||||||
|
|
||||||
let start_x = self.start_x + c as f64 * width * 2.;
|
|
||||||
context.move_to(start_x, self.start_y);
|
|
||||||
context.line_to(start_x + run, self.start_y - self.height);
|
|
||||||
context.line_to(start_x + run + width, self.start_y - self.height);
|
|
||||||
context.line_to(start_x + width, self.start_y);
|
|
||||||
context.line_to(start_x, self.start_y);
|
|
||||||
if c < self.fill_count {
|
|
||||||
let _ = context.fill();
|
|
||||||
} else {
|
|
||||||
let _ = context.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gtk::Orientation::Vertical => {}
|
|
||||||
_ => panic!("unknown orientation"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait Pen {
|
|
||||||
fn move_to(&self, x: f64, y: f64);
|
|
||||||
fn line_to(&self, x: f64, y: f64);
|
|
||||||
fn stroke(&self);
|
|
||||||
|
|
||||||
fn finish(self) -> Pattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GlowPen {
|
|
||||||
blur_context: Context,
|
|
||||||
draw_context: Context,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GlowPen {
|
|
||||||
fn new(
|
|
||||||
width: i32,
|
|
||||||
height: i32,
|
|
||||||
line_width: f64,
|
|
||||||
blur_line_width: f64,
|
|
||||||
color: (f64, f64, f64),
|
|
||||||
) -> Self {
|
|
||||||
let blur_context =
|
|
||||||
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
|
||||||
blur_context.set_line_width(blur_line_width);
|
|
||||||
blur_context.set_source_rgba(color.0, color.1, color.2, 0.5);
|
|
||||||
blur_context.push_group();
|
|
||||||
blur_context.set_line_cap(LineCap::Round);
|
|
||||||
|
|
||||||
let draw_context =
|
|
||||||
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
|
||||||
draw_context.set_line_width(line_width);
|
|
||||||
draw_context.set_source_rgb(color.0, color.1, color.2);
|
|
||||||
draw_context.push_group();
|
|
||||||
draw_context.set_line_cap(LineCap::Round);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
blur_context,
|
|
||||||
draw_context,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Pen for GlowPen {
|
|
||||||
fn move_to(&self, x: f64, y: f64) {
|
|
||||||
self.blur_context.move_to(x, y);
|
|
||||||
self.draw_context.move_to(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn line_to(&self, x: f64, y: f64) {
|
|
||||||
self.blur_context.line_to(x, y);
|
|
||||||
self.draw_context.line_to(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stroke(&self) {
|
|
||||||
self.blur_context.stroke().expect("to draw the blur line");
|
|
||||||
self.draw_context
|
|
||||||
.stroke()
|
|
||||||
.expect("to draw the regular line");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(self) -> Pattern {
|
|
||||||
let foreground = self.draw_context.pop_group().unwrap();
|
|
||||||
self.blur_context.set_source(foreground).unwrap();
|
|
||||||
self.blur_context.paint().unwrap();
|
|
||||||
self.blur_context.pop_group().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let app = gtk::Application::builder()
|
let app = gtk::Application::builder()
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "cyberpunk"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cairo-rs = { version = "0.18" }
|
||||||
|
gio = { version = "0.18" }
|
||||||
|
glib = { version = "0.18" }
|
||||||
|
gtk = { version = "0.7", package = "gtk4" }
|
|
@ -0,0 +1,301 @@
|
||||||
|
use cairo::{
|
||||||
|
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, Pattern,
|
||||||
|
TextExtents,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct AsymLineCutout {
|
||||||
|
pub orientation: gtk::Orientation,
|
||||||
|
pub start_x: f64,
|
||||||
|
pub start_y: f64,
|
||||||
|
pub start_length: f64,
|
||||||
|
pub cutout_length: f64,
|
||||||
|
pub end_length: f64,
|
||||||
|
pub height: f64,
|
||||||
|
pub invert: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsymLineCutout {
|
||||||
|
pub fn draw(&self, pen: &impl Pen) {
|
||||||
|
let dodge = if self.invert {
|
||||||
|
self.height
|
||||||
|
} else {
|
||||||
|
-self.height
|
||||||
|
};
|
||||||
|
match self.orientation {
|
||||||
|
gtk::Orientation::Horizontal => {
|
||||||
|
pen.move_to(self.start_x, self.start_y);
|
||||||
|
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height + self.cutout_length,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.),
|
||||||
|
self.start_y + dodge / 2.,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.)
|
||||||
|
+ self.end_length,
|
||||||
|
self.start_y + dodge / 2.,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
gtk::Orientation::Vertical => {
|
||||||
|
pen.move_to(self.start_x, self.start_y);
|
||||||
|
pen.line_to(self.start_x, self.start_y + self.start_length);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge,
|
||||||
|
self.start_y + self.start_length + self.height,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge,
|
||||||
|
self.start_y + self.start_length + self.height + self.cutout_length,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge / 2.,
|
||||||
|
self.start_y
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.),
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge / 2.,
|
||||||
|
self.start_y
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.)
|
||||||
|
+ self.end_length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => panic!("unknown orientation"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represents an asymetrical line that starts at one location, then a 45-degree angle and then
|
||||||
|
// another line afterwards.
|
||||||
|
pub struct AsymLine {
|
||||||
|
// Will this be drawn left-to-right or up-to-down?
|
||||||
|
pub orientation: gtk::Orientation,
|
||||||
|
|
||||||
|
// Starting address
|
||||||
|
pub start_x: f64,
|
||||||
|
pub start_y: f64,
|
||||||
|
|
||||||
|
// Length of the first segment
|
||||||
|
pub start_length: f64,
|
||||||
|
|
||||||
|
// Height to dodge over to the next section
|
||||||
|
pub height: f64,
|
||||||
|
|
||||||
|
// Total length of the entire line.
|
||||||
|
pub end_length: f64,
|
||||||
|
|
||||||
|
// When normal, the angle dodge is upwards. When inverted, the angle dodge is downwards.
|
||||||
|
pub invert: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsymLine {
|
||||||
|
pub fn draw(&self, pen: &impl Pen) {
|
||||||
|
let dodge = if self.invert {
|
||||||
|
self.height
|
||||||
|
} else {
|
||||||
|
-self.height
|
||||||
|
};
|
||||||
|
match self.orientation {
|
||||||
|
gtk::Orientation::Horizontal => {
|
||||||
|
pen.move_to(self.start_x, self.start_y);
|
||||||
|
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height + self.end_length,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
gtk::Orientation::Vertical => {}
|
||||||
|
_ => panic!("unknown orientation"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SlashMeter {
|
||||||
|
pub orientation: gtk::Orientation,
|
||||||
|
pub start_x: f64,
|
||||||
|
pub start_y: f64,
|
||||||
|
pub count: u8,
|
||||||
|
pub fill_count: u8,
|
||||||
|
pub height: f64,
|
||||||
|
pub length: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlashMeter {
|
||||||
|
pub fn draw(&self, context: &Context) {
|
||||||
|
match self.orientation {
|
||||||
|
gtk::Orientation::Horizontal => {
|
||||||
|
let angle: f64 = 0.8;
|
||||||
|
let run = self.height / angle.tan();
|
||||||
|
let width = self.length / (self.count as f64 * 2.);
|
||||||
|
|
||||||
|
for c in 0..self.count {
|
||||||
|
context.set_line_width(1.);
|
||||||
|
|
||||||
|
let start_x = self.start_x + c as f64 * width * 2.;
|
||||||
|
context.move_to(start_x, self.start_y);
|
||||||
|
context.line_to(start_x + run, self.start_y - self.height);
|
||||||
|
context.line_to(start_x + run + width, self.start_y - self.height);
|
||||||
|
context.line_to(start_x + width, self.start_y);
|
||||||
|
context.line_to(start_x, self.start_y);
|
||||||
|
if c < self.fill_count {
|
||||||
|
let _ = context.fill();
|
||||||
|
} else {
|
||||||
|
let _ = context.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gtk::Orientation::Vertical => {}
|
||||||
|
_ => panic!("unknown orientation"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a pen for drawing a pattern. This is good for complex patterns that may require
|
||||||
|
/// multiple identical steps.
|
||||||
|
pub trait Pen {
|
||||||
|
/// Move the pen to a location.
|
||||||
|
fn move_to(&self, x: f64, y: f64);
|
||||||
|
|
||||||
|
/// Draw a line from the current location to the specified destination.
|
||||||
|
fn line_to(&self, x: f64, y: f64);
|
||||||
|
|
||||||
|
/// Instantiate the line.
|
||||||
|
fn stroke(&self);
|
||||||
|
|
||||||
|
/// Convert all of the drawing into a pattern that can be painted to a drawing context.
|
||||||
|
fn finish(self) -> Pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GlowPen {
|
||||||
|
blur_context: Context,
|
||||||
|
draw_context: Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlowPen {
|
||||||
|
pub fn new(
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
line_width: f64,
|
||||||
|
blur_line_width: f64,
|
||||||
|
color: (f64, f64, f64),
|
||||||
|
) -> Self {
|
||||||
|
let blur_context =
|
||||||
|
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
||||||
|
blur_context.set_line_width(blur_line_width);
|
||||||
|
blur_context.set_source_rgba(color.0, color.1, color.2, 0.5);
|
||||||
|
blur_context.push_group();
|
||||||
|
blur_context.set_line_cap(LineCap::Round);
|
||||||
|
|
||||||
|
let draw_context =
|
||||||
|
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
||||||
|
draw_context.set_line_width(line_width);
|
||||||
|
draw_context.set_source_rgb(color.0, color.1, color.2);
|
||||||
|
draw_context.push_group();
|
||||||
|
draw_context.set_line_cap(LineCap::Round);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
blur_context,
|
||||||
|
draw_context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pen for GlowPen {
|
||||||
|
fn move_to(&self, x: f64, y: f64) {
|
||||||
|
self.blur_context.move_to(x, y);
|
||||||
|
self.draw_context.move_to(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_to(&self, x: f64, y: f64) {
|
||||||
|
self.blur_context.line_to(x, y);
|
||||||
|
self.draw_context.line_to(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stroke(&self) {
|
||||||
|
self.blur_context.stroke().expect("to draw the blur line");
|
||||||
|
self.draw_context
|
||||||
|
.stroke()
|
||||||
|
.expect("to draw the regular line");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> Pattern {
|
||||||
|
let foreground = self.draw_context.pop_group().unwrap();
|
||||||
|
self.blur_context.set_source(foreground).unwrap();
|
||||||
|
self.blur_context.paint().unwrap();
|
||||||
|
self.blur_context.pop_group().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Text<'a> {
|
||||||
|
content: Vec<String>,
|
||||||
|
context: &'a Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Text<'a> {
|
||||||
|
pub fn new(content: String, context: &'a Context, size: f64, width: f64) -> Self {
|
||||||
|
context.select_font_face("Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold);
|
||||||
|
context.set_font_size(size);
|
||||||
|
|
||||||
|
let lines = word_wrap(content, context, width);
|
||||||
|
|
||||||
|
Self { content: lines, context }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extents(&self) -> TextExtents {
|
||||||
|
self.context.text_extents(&self.content[0]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&self) {
|
||||||
|
let mut baseline = 0.;
|
||||||
|
for line in self.content.iter() {
|
||||||
|
baseline += self.context.text_extents(line).unwrap().height() + 10.;
|
||||||
|
self.context.move_to(0., baseline);
|
||||||
|
let _ = self.context.show_text(&line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word_wrap(content: String, context: &Context, max_width: f64) -> Vec<String> {
|
||||||
|
let mut lines = vec![];
|
||||||
|
let words: Vec<&str> = content.split_whitespace().collect();
|
||||||
|
let mut start: usize = 0;
|
||||||
|
let mut line = String::new();
|
||||||
|
|
||||||
|
for idx in 0..words.len() + 1 {
|
||||||
|
line = words[start..idx].join(" ");
|
||||||
|
let extents = context.text_extents(&line).unwrap();
|
||||||
|
if extents.width() > max_width {
|
||||||
|
let line = words[start..idx-1].join(" ");
|
||||||
|
start = idx-1;
|
||||||
|
lines.push(line.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if line.len() > 0 {
|
||||||
|
lines.push(line);
|
||||||
|
}
|
||||||
|
lines
|
||||||
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "dashboard"
|
name = "dashboard"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
||||||
|
async-std = { version = "1.13" }
|
||||||
cairo-rs = { version = "0.18" }
|
cairo-rs = { version = "0.18" }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
fluent-ergonomics = { path = "../fluent-ergonomics/" }
|
fluent-ergonomics = { path = "../fluent-ergonomics/" }
|
||||||
|
@ -17,13 +18,11 @@ gio = { version = "0.18" }
|
||||||
glib = { version = "0.18" }
|
glib = { version = "0.18" }
|
||||||
gdk = { version = "0.7", package = "gdk4" }
|
gdk = { version = "0.7", package = "gdk4" }
|
||||||
gtk = { version = "0.7", package = "gtk4" }
|
gtk = { version = "0.7", package = "gtk4" }
|
||||||
ifc = { path = "../ifc/" }
|
|
||||||
lazy_static = { version = "1.4" }
|
lazy_static = { version = "1.4" }
|
||||||
memorycache = { path = "../memorycache/" }
|
memorycache = { path = "../memorycache/" }
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
serde_derive = { version = "1" }
|
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
serde = { version = "1" }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
unic-langid = { version = "0.9" }
|
unic-langid = { version = "0.9" }
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,10 @@ impl ApplicationWindow {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let date_label = Date::default();
|
let date_label = Date::default();
|
||||||
layout.append(&date_label);
|
let header = adw::HeaderBar::builder()
|
||||||
|
.title_widget(&date_label)
|
||||||
|
.build();
|
||||||
|
layout.append(&header);
|
||||||
|
|
||||||
let events = Events::default();
|
let events = Events::default();
|
||||||
layout.append(&events);
|
layout.append(&events);
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
use chrono::Datelike;
|
|
||||||
use glib::Object;
|
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
|
||||||
use ifc::IFC;
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
pub struct DatePrivate {
|
pub struct DatePrivate {
|
||||||
date: Rc<RefCell<IFC>>,
|
date: Rc<RefCell<NaiveDate>>,
|
||||||
label: Rc<RefCell<gtk::Label>>,
|
label: Rc<RefCell<gtk::Label>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DatePrivate {
|
impl Default for DatePrivate {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let date = chrono::Local::now().date_naive();
|
let date = chrono::Local::now().date_naive();
|
||||||
let year = date.year();
|
|
||||||
let date = date.with_year(year + 10000).unwrap();
|
|
||||||
Self {
|
Self {
|
||||||
date: Rc::new(RefCell::new(IFC::from(date))),
|
date: Rc::new(RefCell::new(date)),
|
||||||
label: Rc::new(RefCell::new(gtk::Label::new(None))),
|
label: Rc::new(RefCell::new(gtk::Label::new(None))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,19 +50,16 @@ impl Default for Date {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Date {
|
impl Date {
|
||||||
pub fn update_date(&self, date: IFC) {
|
pub fn update_date(&self, date: NaiveDate) {
|
||||||
*self.imp().date.borrow_mut() = date;
|
*self.imp().date.borrow_mut() = date;
|
||||||
self.redraw();
|
self.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redraw(&self) {
|
fn redraw(&self) {
|
||||||
let date = self.imp().date.borrow().clone();
|
let date = self.imp().date.borrow();
|
||||||
self.imp().label.borrow_mut().set_text(&format!(
|
self.imp()
|
||||||
"{:?}, {:?} {}, {}",
|
.label
|
||||||
date.weekday(),
|
.borrow_mut()
|
||||||
date.month(),
|
.set_text(&date.format("%Y %B %d").to_string());
|
||||||
date.day(),
|
|
||||||
date.year()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ use crate::{
|
||||||
};
|
};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use ifc::IFC;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
@ -59,19 +58,19 @@ impl Events {
|
||||||
pub fn set_events(&self, events: YearlyEvents, next_event: solstices::Event) {
|
pub fn set_events(&self, events: YearlyEvents, next_event: solstices::Event) {
|
||||||
self.imp()
|
self.imp()
|
||||||
.spring_equinox
|
.spring_equinox
|
||||||
.update_date(IFC::from(events.spring_equinox.date_naive()));
|
.update_date(events.spring_equinox.date_naive());
|
||||||
|
|
||||||
self.imp()
|
self.imp()
|
||||||
.summer_solstice
|
.summer_solstice
|
||||||
.update_date(IFC::from(events.summer_solstice.date_naive()));
|
.update_date(events.summer_solstice.date_naive());
|
||||||
|
|
||||||
self.imp()
|
self.imp()
|
||||||
.autumn_equinox
|
.autumn_equinox
|
||||||
.update_date(IFC::from(events.autumn_equinox.date_naive()));
|
.update_date(events.autumn_equinox.date_naive());
|
||||||
|
|
||||||
self.imp()
|
self.imp()
|
||||||
.winter_solstice
|
.winter_solstice
|
||||||
.update_date(IFC::from(events.winter_solstice.date_naive()));
|
.update_date(events.winter_solstice.date_naive());
|
||||||
|
|
||||||
self.imp().spring_equinox.remove_css_class("highlight");
|
self.imp().spring_equinox.remove_css_class("highlight");
|
||||||
self.imp().summer_solstice.remove_css_class("highlight");
|
self.imp().summer_solstice.remove_css_class("highlight");
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use chrono::{Datelike, Local, Utc};
|
|
||||||
use geo_types::{Latitude, Longitude};
|
|
||||||
use glib::Sender;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use ifc::IFC;
|
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use async_std::channel::Sender;
|
||||||
|
use chrono::{Datelike, Local, Utc};
|
||||||
|
use geo_types::{Latitude, Longitude};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
mod app_window;
|
mod app_window;
|
||||||
use app_window::ApplicationWindow;
|
use app_window::ApplicationWindow;
|
||||||
|
|
||||||
|
@ -102,14 +102,17 @@ pub fn main() {
|
||||||
|
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let state = State {
|
let state = State {
|
||||||
date: IFC::from(now.date_naive().with_year(now.year() + 10000).unwrap()),
|
date: now.date_naive(),
|
||||||
next_event: EVENTS.next_event(now.with_timezone(&Utc)).unwrap(),
|
next_event: EVENTS.next_event(now.with_timezone(&Utc)).unwrap(),
|
||||||
events: EVENTS.yearly_events(now.year()).unwrap(),
|
events: EVENTS.yearly_events(now.year()).unwrap(),
|
||||||
transit: Some(transit),
|
transit: Some(transit),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref gtk_tx) = *core.tx.read().unwrap() {
|
let gtk_tx = core.tx.read().unwrap().clone();
|
||||||
let _ = gtk_tx.send(Message::Refresh(state.clone()));
|
|
||||||
|
if let Some(gtk_tx) = gtk_tx {
|
||||||
|
let state = state.clone();
|
||||||
|
let _ = gtk_tx.send(Message::Refresh(state)).await;
|
||||||
std::thread::sleep(std::time::Duration::from_secs(60));
|
std::thread::sleep(std::time::Duration::from_secs(60));
|
||||||
} else {
|
} else {
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
@ -119,21 +122,17 @@ pub fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.connect_activate(move |app| {
|
app.connect_activate(move |app| {
|
||||||
let (gtk_tx, gtk_rx) =
|
let (gtk_tx, gtk_rx) = async_std::channel::unbounded();
|
||||||
gtk::glib::MainContext::channel::<Message>(gtk::glib::Priority::DEFAULT);
|
|
||||||
|
|
||||||
*core.tx.write().unwrap() = Some(gtk_tx);
|
*core.tx.write().unwrap() = Some(gtk_tx);
|
||||||
|
|
||||||
let window = ApplicationWindow::new(app);
|
let window = ApplicationWindow::new(app);
|
||||||
window.window.present();
|
window.window.present();
|
||||||
|
|
||||||
gtk_rx.attach(None, {
|
glib::spawn_future_local(async move {
|
||||||
let window = window.clone();
|
loop {
|
||||||
move |msg| {
|
let Message::Refresh(state) = gtk_rx.recv().await.unwrap();
|
||||||
let Message::Refresh(state) = msg;
|
window.update_state(state);
|
||||||
ApplicationWindow::update_state(&window, state);
|
|
||||||
|
|
||||||
glib::ControlFlow::Continue
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
// http://astropixels.com/ephemeris/soleq2001.html
|
// http://astropixels.com/ephemeris/soleq2001.html
|
||||||
const SOLSTICE_TEXT: &str = "
|
const SOLSTICE_TEXT: &str = "
|
||||||
|
|
|
@ -2,11 +2,11 @@ use crate::{
|
||||||
solstices::{Event, YearlyEvents},
|
solstices::{Event, YearlyEvents},
|
||||||
soluna_client::SunMoon,
|
soluna_client::SunMoon,
|
||||||
};
|
};
|
||||||
use ifc::IFC;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub date: IFC,
|
pub date: NaiveDate,
|
||||||
pub next_event: Event,
|
pub next_event: Event,
|
||||||
pub events: YearlyEvents,
|
pub events: YearlyEvents,
|
||||||
pub transit: Option<SunMoon>,
|
pub transit: Option<SunMoon>,
|
||||||
|
|
|
@ -1,198 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020-2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
||||||
|
|
||||||
This file is part of the Luminescent Dreams Tools.
|
|
||||||
|
|
||||||
Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
extern crate chrono;
|
|
||||||
extern crate chrono_tz;
|
|
||||||
|
|
||||||
use chrono::SecondsFormat;
|
|
||||||
use chrono_tz::Etc::UTC;
|
|
||||||
use serde::de::{self, Deserialize, Deserializer, Visitor};
|
|
||||||
use serde::ser::{Serialize, Serializer};
|
|
||||||
use std::{fmt, str::FromStr};
|
|
||||||
|
|
||||||
/// This is a wrapper around date time objects, using timezones from the chroon-tz database and
|
|
||||||
/// providing string representation and parsing of the form "<RFC3339> <Timezone Name>", i.e.,
|
|
||||||
/// "2019-05-15T14:30:00Z US/Central". The to_string method, and serde serialization will
|
|
||||||
/// produce a string of this format. The parser will accept an RFC3339-only string of the forms
|
|
||||||
/// "2019-05-15T14:30:00Z", "2019-05-15T14:30:00+00:00", and also an "RFC3339 Timezone Name"
|
|
||||||
/// string.
|
|
||||||
///
|
|
||||||
/// The function here is to generate as close to unambiguous time/date strings, (for earth's
|
|
||||||
/// gravitational frame of reference), as possible. Clumping together the time, offset from UTC,
|
|
||||||
/// and the named time zone allows future parsers to know the exact interpretation of the time in
|
|
||||||
/// the frame of reference of the original recording.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct DateTimeTz(pub chrono::DateTime<chrono_tz::Tz>);
|
|
||||||
|
|
||||||
impl fmt::Display for DateTimeTz {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
|
||||||
if self.0.timezone() == UTC {
|
|
||||||
write!(f, "{}", self.0.to_rfc3339_opts(SecondsFormat::Secs, true))
|
|
||||||
} else {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{} {}",
|
|
||||||
self.0
|
|
||||||
.with_timezone(&chrono_tz::Etc::UTC)
|
|
||||||
.to_rfc3339_opts(SecondsFormat::Secs, true,),
|
|
||||||
self.0.timezone().name()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DateTimeTz {
|
|
||||||
pub fn map<F>(&self, f: F) -> DateTimeTz
|
|
||||||
where
|
|
||||||
F: FnOnce(chrono::DateTime<chrono_tz::Tz>) -> chrono::DateTime<chrono_tz::Tz>,
|
|
||||||
{
|
|
||||||
DateTimeTz(f(self.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for DateTimeTz {
|
|
||||||
type Err = chrono::ParseError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let v: Vec<&str> = s.split_terminator(' ').collect();
|
|
||||||
if v.len() == 2 {
|
|
||||||
let tz = v[1].parse::<chrono_tz::Tz>().unwrap();
|
|
||||||
chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&tz)))
|
|
||||||
} else {
|
|
||||||
chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&UTC)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<chrono::DateTime<chrono_tz::Tz>> for DateTimeTz {
|
|
||||||
fn from(dt: chrono::DateTime<chrono_tz::Tz>) -> DateTimeTz {
|
|
||||||
DateTimeTz(dt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DateTimeTzVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for DateTimeTzVisitor {
|
|
||||||
type Value = DateTimeTz;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
formatter.write_str("a string date time representation that can be parsed")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
|
|
||||||
DateTimeTz::from_str(s).or(Err(E::custom(
|
|
||||||
"string is not a parsable datetime representation".to_owned(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for DateTimeTz {
|
|
||||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
||||||
serializer.serialize_str(&self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for DateTimeTz {
|
|
||||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
|
||||||
deserializer.deserialize_str(DateTimeTzVisitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
extern crate serde_json;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use chrono::TimeZone;
|
|
||||||
use chrono_tz::America::Phoenix;
|
|
||||||
use chrono_tz::Etc::UTC;
|
|
||||||
use chrono_tz::US::{Arizona, Central};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_creates_timestamp_with_z() {
|
|
||||||
let t = DateTimeTz(UTC.with_ymd_and_hms(2019, 5, 15, 12, 0, 0).unwrap());
|
|
||||||
assert_eq!(t.to_string(), "2019-05-15T12:00:00Z");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_parses_utc_rfc3339_z() {
|
|
||||||
let t = DateTimeTz::from_str("2019-05-15T12:00:00Z").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
t,
|
|
||||||
DateTimeTz(UTC.with_ymd_and_hms(2019, 5, 15, 12, 0, 0).unwrap())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_parses_rfc3339_with_offset() {
|
|
||||||
let t = DateTimeTz::from_str("2019-05-15T12:00:00-06:00").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
t,
|
|
||||||
DateTimeTz(UTC.with_ymd_and_hms(2019, 5, 15, 18, 0, 0).unwrap())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_parses_rfc3339_with_tz() {
|
|
||||||
let t = DateTimeTz::from_str("2019-06-15T19:00:00Z US/Arizona").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
t,
|
|
||||||
DateTimeTz(UTC.with_ymd_and_hms(2019, 6, 15, 19, 0, 0).unwrap())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
t,
|
|
||||||
DateTimeTz(Arizona.with_ymd_and_hms(2019, 6, 15, 12, 0, 0).unwrap())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
t,
|
|
||||||
DateTimeTz(Central.with_ymd_and_hms(2019, 6, 15, 14, 0, 0).unwrap())
|
|
||||||
);
|
|
||||||
assert_eq!(t.to_string(), "2019-06-15T19:00:00Z US/Arizona");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct DemoStruct {
|
|
||||||
id: String,
|
|
||||||
dt: DateTimeTz,
|
|
||||||
}
|
|
||||||
|
|
||||||
// I used Arizona here specifically because large parts of Arizona do not honor DST, and so
|
|
||||||
// that adds in more ambiguity of the -0700 offset with Pacific time.
|
|
||||||
#[test]
|
|
||||||
fn it_json_serializes() {
|
|
||||||
let t = DateTimeTz::from_str("2019-06-15T19:00:00Z America/Phoenix").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::to_string(&t).unwrap(),
|
|
||||||
"\"2019-06-15T19:00:00Z America/Phoenix\""
|
|
||||||
);
|
|
||||||
|
|
||||||
let demo = DemoStruct {
|
|
||||||
id: String::from("abcdefg"),
|
|
||||||
dt: t,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::to_string(&demo).unwrap(),
|
|
||||||
"{\"id\":\"abcdefg\",\"dt\":\"2019-06-15T19:00:00Z America/Phoenix\"}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_json_parses() {
|
|
||||||
let t =
|
|
||||||
serde_json::from_str::<DateTimeTz>("\"2019-06-15T19:00:00Z America/Phoenix\"").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
t,
|
|
||||||
DateTimeTz(Phoenix.with_ymd_and_hms(2019, 6, 15, 12, 0, 0).unwrap())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -110,7 +110,7 @@ where
|
||||||
.map_err(EmseriesReadError::JSONParseError)
|
.map_err(EmseriesReadError::JSONParseError)
|
||||||
.and_then(Record::try_from)
|
.and_then(Record::try_from)
|
||||||
{
|
{
|
||||||
Ok(record) => records.insert(record.id.clone(), record.clone()),
|
Ok(record) => records.insert(record.id, record.clone()),
|
||||||
Err(EmseriesReadError::RecordDeleted(id)) => records.remove(&id),
|
Err(EmseriesReadError::RecordDeleted(id)) => records.remove(&id),
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
|
@ -124,19 +124,16 @@ where
|
||||||
/// Put a new record into the database. A unique id will be assigned to the record and
|
/// Put a new record into the database. A unique id will be assigned to the record and
|
||||||
/// returned.
|
/// returned.
|
||||||
pub fn put(&mut self, entry: T) -> Result<RecordId, EmseriesWriteError> {
|
pub fn put(&mut self, entry: T) -> Result<RecordId, EmseriesWriteError> {
|
||||||
let uuid = RecordId::default();
|
let id = RecordId::default();
|
||||||
let record = Record {
|
let record = Record { id, data: entry };
|
||||||
id: uuid.clone(),
|
|
||||||
data: entry,
|
|
||||||
};
|
|
||||||
self.update(record)?;
|
self.update(record)?;
|
||||||
Ok(uuid)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update an existing record. The [RecordId] of the record passed into this function must match
|
/// Update an existing record. The [RecordId] of the record passed into this function must match
|
||||||
/// the [RecordId] of a record already in the database.
|
/// the [RecordId] of a record already in the database.
|
||||||
pub fn update(&mut self, record: Record<T>) -> Result<(), EmseriesWriteError> {
|
pub fn update(&mut self, record: Record<T>) -> Result<(), EmseriesWriteError> {
|
||||||
self.records.insert(record.id.clone(), record.clone());
|
self.records.insert(record.id, record.clone());
|
||||||
let write_res = match serde_json::to_string(&RecordOnDisk {
|
let write_res = match serde_json::to_string(&RecordOnDisk {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
data: Some(record.data),
|
data: Some(record.data),
|
||||||
|
@ -166,7 +163,7 @@ where
|
||||||
self.records.remove(uuid);
|
self.records.remove(uuid);
|
||||||
|
|
||||||
let rec: RecordOnDisk<T> = RecordOnDisk {
|
let rec: RecordOnDisk<T> = RecordOnDisk {
|
||||||
id: uuid.clone(),
|
id: *uuid,
|
||||||
data: None,
|
data: None,
|
||||||
};
|
};
|
||||||
match serde_json::to_string(&rec) {
|
match serde_json::to_string(&rec) {
|
||||||
|
|
|
@ -120,7 +120,7 @@ pub trait Recordable {
|
||||||
/// Uniquely identifies a record.
|
/// Uniquely identifies a record.
|
||||||
///
|
///
|
||||||
/// This is a wrapper around a basic uuid with some extra convenience methods.
|
/// This is a wrapper around a basic uuid with some extra convenience methods.
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct RecordId(Uuid);
|
pub struct RecordId(Uuid);
|
||||||
|
|
||||||
impl Default for RecordId {
|
impl Default for RecordId {
|
||||||
|
@ -166,6 +166,17 @@ impl<T: Clone + Recordable> Record<T> {
|
||||||
pub fn timestamp(&self) -> Timestamp {
|
pub fn timestamp(&self) -> Timestamp {
|
||||||
self.data.timestamp()
|
self.data.timestamp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn map<Map, U>(self, map: Map) -> Record<U>
|
||||||
|
where
|
||||||
|
Map: Fn(T) -> U,
|
||||||
|
U: Clone + Recordable,
|
||||||
|
{
|
||||||
|
Record {
|
||||||
|
id: self.id,
|
||||||
|
data: map(self.data),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -190,7 +201,7 @@ mod test {
|
||||||
|
|
||||||
impl Recordable for WeightRecord {
|
impl Recordable for WeightRecord {
|
||||||
fn timestamp(&self) -> Timestamp {
|
fn timestamp(&self) -> Timestamp {
|
||||||
Timestamp::Date(self.date.clone())
|
Timestamp::Date(self.date)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tags(&self) -> Vec<String> {
|
fn tags(&self) -> Vec<String> {
|
||||||
|
|
|
@ -20,7 +20,7 @@ extern crate emseries;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use chrono::{format::Fixed, prelude::*};
|
use chrono::{prelude::*};
|
||||||
use chrono_tz::Etc::UTC;
|
use chrono_tz::Etc::UTC;
|
||||||
use dimensioned::si::{Kilogram, Meter, Second, M, S};
|
use dimensioned::si::{Kilogram, Meter, Second, M, S};
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ mod test {
|
||||||
|
|
||||||
impl Recordable for BikeTrip {
|
impl Recordable for BikeTrip {
|
||||||
fn timestamp(&self) -> Timestamp {
|
fn timestamp(&self) -> Timestamp {
|
||||||
Timestamp::DateTime(self.datetime.clone())
|
Timestamp::DateTime(self.datetime)
|
||||||
}
|
}
|
||||||
fn tags(&self) -> Vec<String> {
|
fn tags(&self) -> Vec<String> {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
|
@ -99,7 +99,7 @@ mod test {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_test<T>(test: T) -> ()
|
fn run_test<T>(test: T)
|
||||||
where
|
where
|
||||||
T: FnOnce(tempfile::TempPath),
|
T: FnOnce(tempfile::TempPath),
|
||||||
{
|
{
|
||||||
|
@ -108,7 +108,7 @@ mod test {
|
||||||
test(tmp_path);
|
test(tmp_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run<T>(test: T) -> ()
|
fn run<T>(test: T)
|
||||||
where
|
where
|
||||||
T: FnOnce(Series<BikeTrip>),
|
T: FnOnce(Series<BikeTrip>),
|
||||||
{
|
{
|
||||||
|
@ -280,8 +280,7 @@ mod test {
|
||||||
UTC.with_ymd_and_hms(2011, 11, 04, 0, 0, 0)
|
UTC.with_ymd_and_hms(2011, 11, 04, 0, 0, 0)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
)
|
),
|
||||||
.into(),
|
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|l, r| l.timestamp().cmp(&r.timestamp()),
|
|l, r| l.timestamp().cmp(&r.timestamp()),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "fitnesstrax"
|
name = "fitnesstrax"
|
||||||
version = "0.3.0"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
@ -8,6 +8,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
adw = { version = "0.5", package = "libadwaita", features = [ "v1_4" ] }
|
adw = { version = "0.5", package = "libadwaita", features = [ "v1_4" ] }
|
||||||
async-channel = { version = "2.1" }
|
async-channel = { version = "2.1" }
|
||||||
|
async-trait = { version = "0.1" }
|
||||||
chrono = { version = "0.4" }
|
chrono = { version = "0.4" }
|
||||||
chrono-tz = { version = "0.8" }
|
chrono-tz = { version = "0.8" }
|
||||||
dimensioned = { version = "0.8", features = [ "serde" ] }
|
dimensioned = { version = "0.8", features = [ "serde" ] }
|
||||||
|
@ -15,6 +16,7 @@ emseries = { path = "../../emseries" }
|
||||||
ft-core = { path = "../core" }
|
ft-core = { path = "../core" }
|
||||||
gio = { version = "0.18" }
|
gio = { version = "0.18" }
|
||||||
glib = { version = "0.18" }
|
glib = { version = "0.18" }
|
||||||
|
gdk = { version = "0.7", package = "gdk4" }
|
||||||
gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] }
|
gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] }
|
||||||
thiserror = { version = "1.0" }
|
thiserror = { version = "1.0" }
|
||||||
tokio = { version = "1.34", features = [ "full" ] }
|
tokio = { version = "1.34", features = [ "full" ] }
|
||||||
|
|
|
@ -3,4 +3,16 @@
|
||||||
<gresource prefix="/com/luminescent-dreams/fitnesstrax/">
|
<gresource prefix="/com/luminescent-dreams/fitnesstrax/">
|
||||||
<file>style.css</file>
|
<file>style.css</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
|
|
||||||
|
<gresource prefix="/com/luminescent-dreams/fitnesstrax/icons/scalable/actions">
|
||||||
|
<file preprocess="xml-stripblanks">cycling-symbolic.svg</file>
|
||||||
|
</gresource>
|
||||||
|
|
||||||
|
<gresource prefix="/com/luminescent-dreams/fitnesstrax/icons/scalable/actions">
|
||||||
|
<file preprocess="xml-stripblanks">running-symbolic.svg</file>
|
||||||
|
</gresource>
|
||||||
|
|
||||||
|
<gresource prefix="/com/luminescent-dreams/fitnesstrax/icons/scalable/actions/">
|
||||||
|
<file preprocess="xml-stripblanks">walking-symbolic.svg</file>
|
||||||
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{ gtkNativeInputs }:
|
||||||
|
attrs: {
|
||||||
|
nativeBuildInputs = gtkNativeInputs;
|
||||||
|
postInstall = ''
|
||||||
|
install -Dt $out/share/applications resources/fitnesstrax.desktop
|
||||||
|
install -Dt $out/gsettings-schemas/${attrs.crateName}-${attrs.version}/glib-2.0/schemas resources/com.luminescent-dreams.fitnesstrax.gschema.xml
|
||||||
|
glib-compile-schemas $out/gsettings-schemas/${attrs.crateName}-${attrs.version}/glib-2.0/schemas
|
||||||
|
'';
|
||||||
|
preFixup = ''
|
||||||
|
gappsWrapperArgs+=(
|
||||||
|
--prefix XDG_DATA_DIRS : $out/gsettings-schemas/${attrs.crateName}-${attrs.version}
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16px" viewBox="0 0 16 16" width="16px"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix color-interpolation-filters="sRGB" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="b"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.5"/></g></mask><clipPath id="c"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="d"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.7"/></g></mask><clipPath id="e"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="f"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.35"/></g></mask><clipPath id="g"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><path d="m 9.5 2 c -0.828125 0 -1.5 0.671875 -1.5 1.5 s 0.671875 1.5 1.5 1.5 s 1.5 -0.671875 1.5 -1.5 s -0.671875 -1.5 -1.5 -1.5 z m 0 0"/><path d="m 4.285156 13 c 0 0.703125 -0.582031 1.285156 -1.285156 1.285156 s -1.285156 -0.582031 -1.285156 -1.285156 s 0.582031 -1.285156 1.285156 -1.285156 s 1.285156 0.582031 1.285156 1.285156 z m -4.285156 0 c 0 1.675781 1.324219 3 3 3 s 3 -1.324219 3 -3 s -1.324219 -3 -3 -3 s -3 1.324219 -3 3 z m 0 0"/><path d="m 8.992188 13.007812 v -3.003906 c 0 -0.359375 -0.1875 -0.6875 -0.5 -0.867187 l -2.558594 -1.476563 l 0.363281 1.363282 l 1.671875 -2.890626 l -1.367188 0.363282 l 0.910157 0.527344 l -0.40625 -0.4375 c 0.773437 1.621093 1.96875 1.933593 1.96875 1.933593 s 0.578125 0.242188 1.9375 0.429688 c 0.546875 0.074219 1.050781 -0.304688 1.128906 -0.851563 c 0.074219 -0.550781 -0.308594 -1.054687 -0.855469 -1.128906 c -1.179687 -0.164062 -1.601562 -0.355469 -1.601562 -0.355469 s -0.425782 -0.164062 -0.769532 -0.886719 c -0.089843 -0.183593 -0.226562 -0.335937 -0.402343 -0.4375 l -0.910157 -0.523437 c -0.476562 -0.277344 -1.089843 -0.113281 -1.363281 0.367187 l -1.671875 2.890626 c -0.277344 0.480468 -0.113281 1.089843 0.367188 1.367187 l 2.558594 1.480469 l -0.5 -0.867188 v 3.003906 c 0 0.550782 0.449218 1 1 1 c 0.554687 0 1 -0.449218 1 -1 z m 0 0"/><path d="m 14.285156 13 c 0 0.703125 -0.582031 1.285156 -1.285156 1.285156 s -1.285156 -0.582031 -1.285156 -1.285156 s 0.582031 -1.285156 1.285156 -1.285156 s 1.285156 0.582031 1.285156 1.285156 z m -4.285156 0 c 0 1.675781 1.324219 3 3 3 s 3 -1.324219 3 -3 s -1.324219 -3 -3 -3 s -3 1.324219 -3 3 z m 0 0"/><g mask="url(#b)"><g clip-path="url(#c)" transform="matrix(1 0 0 1 -600 -120)"><path d="m 550 182 c -0.351562 0.003906 -0.695312 0.101562 -1 0.28125 v 3.4375 c 0.304688 0.179688 0.648438 0.277344 1 0.28125 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 c -0.339844 0 -0.679688 0.058594 -1 0.175781 v 6.824219 h 4 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#d)"><g clip-path="url(#e)" transform="matrix(1 0 0 1 -600 -120)"><path d="m 569 182 v 4 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 v 7 h 3 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#f)"><g clip-path="url(#g)" transform="matrix(1 0 0 1 -600 -120)"><path d="m 573 182.269531 v 3.449219 c 0.613281 -0.355469 0.996094 -1.007812 1 -1.71875 c 0 -0.714844 -0.382812 -1.375 -1 -1.730469 z m 0 4.90625 v 6.824219 h 2 v -4 c 0 -1.269531 -0.800781 -2.402344 -2 -2.824219 z m 0 0"/></g></g></svg>
|
After Width: | Height: | Size: 3.3 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16px" viewBox="0 0 16 16" width="16px"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix color-interpolation-filters="sRGB" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="b"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.5"/></g></mask><clipPath id="c"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="d"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.7"/></g></mask><clipPath id="e"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="f"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.35"/></g></mask><clipPath id="g"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><path d="m 8.5 0 c -0.828125 0 -1.5 0.671875 -1.5 1.5 s 0.671875 1.5 1.5 1.5 s 1.5 -0.671875 1.5 -1.5 s -0.671875 -1.5 -1.5 -1.5 z m -2.5 4 c -0.117188 0 -0.230469 0.027344 -0.335938 0.082031 l -2 1 c -0.144531 0.070313 -0.261718 0.1875 -0.332031 0.332031 l -1 2 c -0.1875 0.371094 -0.039062 0.820313 0.332031 1.007813 c 0.371094 0.183594 0.820313 0.035156 1.003907 -0.335937 l 0.890625 -1.777344 l 1.5625 -0.773438 c -0.042969 0.074219 -0.726563 2.835938 -0.726563 2.835938 c -0.230469 0.949218 0.398438 1.523437 0.398438 1.523437 l 3.351562 2.703125 l 0.90625 2.71875 c 0.175781 0.523438 0.742188 0.808594 1.265625 0.632813 c 0.523438 -0.175781 0.808594 -0.742188 0.632813 -1.265625 l -1 -3 c -0.0625 -0.183594 -0.171875 -0.34375 -0.324219 -0.464844 l -2 -1.597656 l 0.679688 -2.714844 l 0.25 0.625 c 0.085937 0.222656 0.28125 0.390625 0.515624 0.449219 l 2 0.5 c 0.402344 0.097656 0.808594 -0.144531 0.910157 -0.546875 c 0.097656 -0.40625 -0.144531 -0.8125 -0.546875 -0.910156 l -1.628906 -0.40625 l -0.855469 -2.144532 c -0.117188 -0.285156 -0.390625 -0.472656 -0.699219 -0.472656 z m -1.164062 6.328125 l -0.710938 2.128906 l -1.832031 1.835938 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 s 1.023437 0.390625 1.414062 0 l 2 -2 c 0.109375 -0.109375 0.191407 -0.242187 0.242188 -0.390625 l 0.542969 -1.628906 z m 0 0"/><g mask="url(#b)"><g clip-path="url(#c)" transform="matrix(1 0 0 1 -620 -120)"><path d="m 550 182 c -0.351562 0.003906 -0.695312 0.101562 -1 0.28125 v 3.4375 c 0.304688 0.179688 0.648438 0.277344 1 0.28125 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 c -0.339844 0 -0.679688 0.058594 -1 0.175781 v 6.824219 h 4 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#d)"><g clip-path="url(#e)" transform="matrix(1 0 0 1 -620 -120)"><path d="m 569 182 v 4 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 v 7 h 3 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#f)"><g clip-path="url(#g)" transform="matrix(1 0 0 1 -620 -120)"><path d="m 573 182.269531 v 3.449219 c 0.613281 -0.355469 0.996094 -1.007812 1 -1.71875 c 0 -0.714844 -0.382812 -1.375 -1 -1.730469 z m 0 4.90625 v 6.824219 h 2 v -4 c 0 -1.269531 -0.800781 -2.402344 -2 -2.824219 z m 0 0"/></g></g></svg>
|
After Width: | Height: | Size: 3.0 KiB |
|
@ -3,7 +3,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome__title {
|
.welcome__title {
|
||||||
font-size: larger;
|
font-size: x-large;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,22 +11,57 @@
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome__footer {}
|
|
||||||
|
|
||||||
.historical {
|
.historical {
|
||||||
margin: 32px;
|
margin: 32px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-range-picker {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
.date-range-picker > box:not(:last-child) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
.date-range-picker__date-field {
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range-picker__search-button {
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range-picker__range-button {
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-field__year {
|
||||||
|
margin: 0px 4px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-field__month {
|
||||||
|
margin: 0px 4px 0px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-field__day {
|
||||||
|
margin: 0px 0px 0px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.day-summary {
|
.day-summary {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-summary__date {
|
.day-summary > *:not(:last-child) {
|
||||||
font-size: larger;
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.day-summary__date {
|
||||||
|
font-size: x-large;
|
||||||
|
}
|
||||||
|
|
||||||
.day-summary__weight {
|
.day-summary__weight {
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
@ -40,3 +75,11 @@
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.about__content {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about label {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16px" viewBox="0 0 16 16" width="16px"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix color-interpolation-filters="sRGB" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="b"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.5"/></g></mask><clipPath id="c"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="d"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.7"/></g></mask><clipPath id="e"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="f"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.35"/></g></mask><clipPath id="g"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><path d="m 9.5 1.5 c 0 0.828125 -0.671875 1.5 -1.5 1.5 s -1.5 -0.671875 -1.5 -1.5 s 0.671875 -1.5 1.5 -1.5 s 1.5 0.671875 1.5 1.5 z m 0 0"/><path d="m 7 4 c -0.550781 0 -1 0.449219 -1 1 v 4 c 0 0.265625 0.105469 0.519531 0.292969 0.707031 l 0.445312 0.449219 l -2.59375 4.328125 c -0.285156 0.476563 -0.132812 1.089844 0.34375 1.375 c 0.472657 0.28125 1.085938 0.128906 1.367188 -0.34375 l 2.34375 -3.902344 l 0.925781 0.929688 l 0.925781 2.773437 c 0.082031 0.25 0.265625 0.460938 0.5 0.578125 c 0.238281 0.121094 0.515625 0.140625 0.765625 0.054688 c 0.25 -0.082031 0.460938 -0.265625 0.578125 -0.5 c 0.121094 -0.238281 0.140625 -0.515625 0.054688 -0.765625 l -1 -3 c -0.050781 -0.148438 -0.132813 -0.28125 -0.242188 -0.390625 l -1.707031 -1.707031 v -4.585938 c 0 -0.550781 -0.449219 -1 -1 -1 z m 0 0"/><path d="m 6 4 c -0.101562 0 -0.207031 0.019531 -0.300781 0.0625 c 0 0 -2.113281 0.847656 -2.199219 2.90625 v 0.03125 v 2.25 c 0 0.414062 0.335938 0.75 0.75 0.75 s 0.75 -0.335938 0.75 -0.75 v -2.21875 c 0.039062 -0.894531 1.050781 -1.449219 1.207031 -1.53125 h 2.332031 l 1.042969 2.085938 c 0.097657 0.195312 0.273438 0.339843 0.488281 0.394531 l 2 0.5 c 0.191407 0.046875 0.394532 0.015625 0.566407 -0.085938 c 0.171875 -0.101562 0.292969 -0.269531 0.34375 -0.460937 c 0.046875 -0.195313 0.015625 -0.398438 -0.085938 -0.570313 c -0.101562 -0.171875 -0.269531 -0.292969 -0.464843 -0.34375 l -1.664063 -0.414062 l -1.097656 -2.191407 c -0.125 -0.253906 -0.382813 -0.414062 -0.667969 -0.414062 z m 0 0"/><g mask="url(#b)"><g clip-path="url(#c)" transform="matrix(1 0 0 1 -620 -100)"><path d="m 550 182 c -0.351562 0.003906 -0.695312 0.101562 -1 0.28125 v 3.4375 c 0.304688 0.179688 0.648438 0.277344 1 0.28125 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 c -0.339844 0 -0.679688 0.058594 -1 0.175781 v 6.824219 h 4 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#d)"><g clip-path="url(#e)" transform="matrix(1 0 0 1 -620 -100)"><path d="m 569 182 v 4 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 v 7 h 3 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#f)"><g clip-path="url(#g)" transform="matrix(1 0 0 1 -620 -100)"><path d="m 573 182.269531 v 3.449219 c 0.613281 -0.355469 0.996094 -1.007812 1 -1.71875 c 0 -0.714844 -0.382812 -1.375 -1 -1.730469 z m 0 4.90625 v 6.824219 h 2 v -4 c 0 -1.269531 -0.800781 -2.402344 -2 -2.824219 z m 0 0"/></g></g></svg>
|
After Width: | Height: | Size: 3.2 KiB |
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 - 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
|
This file is part of FitnessTrax.
|
||||||
|
|
||||||
|
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||||
|
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct AboutWindowPrivate {}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for AboutWindowPrivate {
|
||||||
|
const NAME: &'static str = "AboutWindow";
|
||||||
|
type Type = AboutWindow;
|
||||||
|
type ParentType = gtk::Window;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for AboutWindowPrivate {}
|
||||||
|
impl WidgetImpl for AboutWindowPrivate {}
|
||||||
|
impl WindowImpl for AboutWindowPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct AboutWindow(ObjectSubclass<AboutWindowPrivate>) @extends gtk::Window, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AboutWindow {
|
||||||
|
fn default() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.set_width_request(600);
|
||||||
|
s.set_height_request(700);
|
||||||
|
s.add_css_class("about");
|
||||||
|
|
||||||
|
s.set_title(Some("About Fitnesstrax"));
|
||||||
|
let copyright = gtk::Label::builder()
|
||||||
|
.label("Copyright 2023-2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>")
|
||||||
|
.halign(gtk::Align::Start)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let gtk_rs_thanks = gtk::Label::builder()
|
||||||
|
.label("I owe a huge debt of gratitude to the GTK-RS project (https://gtk-rs.org/), which makes it possible for me to write this application to begin with. Further, I owe a particular debt to Julian Hofer and his book, GUI development with Rust and GTK 4 (https://gtk-rs.org/gtk4-rs/stable/latest/book/). Without this book, I would have continued to stumble around writing bad user interfaces with even worse code.")
|
||||||
|
.halign(gtk::Align::Start).wrap(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let dependencies = gtk::Label::builder()
|
||||||
|
.label("This application depends on many libraries, most of which are licensed under the BSD-3 or GPL-3 licenses.")
|
||||||
|
.halign(gtk::Align::Start).wrap(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let content = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.css_classes(["about__content"])
|
||||||
|
.build();
|
||||||
|
content.append(©right);
|
||||||
|
content.append(>k_rs_thanks);
|
||||||
|
content.append(&dependencies);
|
||||||
|
|
||||||
|
let scroller = gtk::ScrolledWindow::builder()
|
||||||
|
.child(&content)
|
||||||
|
.hexpand(true)
|
||||||
|
.vexpand(true)
|
||||||
|
.hscrollbar_policy(gtk::PolicyType::Never)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
s.set_child(Some(&scroller));
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use emseries::{time_range, Record, RecordId, Series, Timestamp};
|
use emseries::{time_range, Record, RecordId, Series, Timestamp};
|
||||||
use ft_core::TraxRecord;
|
use ft_core::TraxRecord;
|
||||||
|
@ -34,6 +35,32 @@ pub enum AppError {
|
||||||
Unhandled,
|
Unhandled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ReadError {
|
||||||
|
#[error("no database loaded")]
|
||||||
|
NoDatabase,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum WriteError {
|
||||||
|
#[error("no database loaded")]
|
||||||
|
NoDatabase,
|
||||||
|
#[error("unhandled error")]
|
||||||
|
Unhandled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait RecordProvider: Send + Sync {
|
||||||
|
async fn records(
|
||||||
|
&self,
|
||||||
|
start: NaiveDate,
|
||||||
|
end: NaiveDate,
|
||||||
|
) -> Result<Vec<Record<TraxRecord>>, ReadError>;
|
||||||
|
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError>;
|
||||||
|
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError>;
|
||||||
|
async fn delete_record(&self, id: RecordId) -> Result<(), WriteError>;
|
||||||
|
}
|
||||||
|
|
||||||
/// The real, headless application. This is where all of the logic will reside.
|
/// The real, headless application. This is where all of the logic will reside.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
|
@ -57,11 +84,30 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn records(
|
pub async fn open_db(&self, path: PathBuf) -> Result<(), AppError> {
|
||||||
|
let db_ref = self.database.clone();
|
||||||
|
self.runtime
|
||||||
|
.spawn_blocking(move || {
|
||||||
|
let db = Series::open(path).map_err(|_| AppError::FailedToOpenDatabase)?;
|
||||||
|
*db_ref.write().unwrap() = Some(db);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn database_is_open(&self) -> bool {
|
||||||
|
self.database.read().unwrap().is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RecordProvider for App {
|
||||||
|
async fn records(
|
||||||
&self,
|
&self,
|
||||||
start: NaiveDate,
|
start: NaiveDate,
|
||||||
end: NaiveDate,
|
end: NaiveDate,
|
||||||
) -> Result<Vec<Record<TraxRecord>>, AppError> {
|
) -> Result<Vec<Record<TraxRecord>>, ReadError> {
|
||||||
let db = self.database.clone();
|
let db = self.database.clone();
|
||||||
self.runtime
|
self.runtime
|
||||||
.spawn_blocking(move || {
|
.spawn_blocking(move || {
|
||||||
|
@ -77,14 +123,14 @@ impl App {
|
||||||
.collect::<Vec<Record<TraxRecord>>>();
|
.collect::<Vec<Record<TraxRecord>>>();
|
||||||
Ok(records)
|
Ok(records)
|
||||||
} else {
|
} else {
|
||||||
Err(AppError::NoDatabase)
|
Err(ReadError::NoDatabase)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn put_record(&self, record: TraxRecord) -> Result<RecordId, AppError> {
|
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
|
||||||
let db = self.database.clone();
|
let db = self.database.clone();
|
||||||
self.runtime
|
self.runtime
|
||||||
.spawn_blocking(move || {
|
.spawn_blocking(move || {
|
||||||
|
@ -97,10 +143,10 @@ impl App {
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map_err(|_| AppError::Unhandled)
|
.map_err(|_| WriteError::Unhandled)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), AppError> {
|
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> {
|
||||||
let db = self.database.clone();
|
let db = self.database.clone();
|
||||||
self.runtime
|
self.runtime
|
||||||
.spawn_blocking(move || {
|
.spawn_blocking(move || {
|
||||||
|
@ -112,18 +158,10 @@ impl App {
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map_err(|_| AppError::Unhandled)
|
.map_err(|_| WriteError::Unhandled)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open_db(&self, path: PathBuf) -> Result<(), AppError> {
|
async fn delete_record(&self, _id: RecordId) -> Result<(), WriteError> {
|
||||||
let db_ref = self.database.clone();
|
unimplemented!()
|
||||||
self.runtime
|
|
||||||
.spawn_blocking(move || {
|
|
||||||
let db = Series::open(path).map_err(|_| AppError::FailedToOpenDatabase)?;
|
|
||||||
*db_ref.write().unwrap() = Some(db);
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,13 @@ You should have received a copy of the GNU General Public License along with Fit
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
|
types::DayInterval,
|
||||||
view_models::DayDetailViewModel,
|
view_models::DayDetailViewModel,
|
||||||
views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView},
|
views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView},
|
||||||
};
|
};
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use chrono::{Duration, Local};
|
use chrono::{Duration, Local};
|
||||||
use emseries::Record;
|
|
||||||
use ft_core::TraxRecord;
|
|
||||||
use gio::resources_lookup_data;
|
use gio::resources_lookup_data;
|
||||||
use gtk::STYLE_PROVIDER_PRIORITY_USER;
|
use gtk::STYLE_PROVIDER_PRIORITY_USER;
|
||||||
use std::{cell::RefCell, path::PathBuf, rc::Rc};
|
use std::{cell::RefCell, path::PathBuf, rc::Rc};
|
||||||
|
@ -54,8 +54,11 @@ impl AppWindow {
|
||||||
let window = adw::ApplicationWindow::builder()
|
let window = adw::ApplicationWindow::builder()
|
||||||
.application(adw_app)
|
.application(adw_app)
|
||||||
.width_request(800)
|
.width_request(800)
|
||||||
.height_request(600)
|
.height_request(746)
|
||||||
.build();
|
.build();
|
||||||
|
window.connect_destroy(|s| {
|
||||||
|
let _ = gtk::prelude::WidgetExt::activate_action(s, "app.quit", None);
|
||||||
|
});
|
||||||
|
|
||||||
let stylesheet = String::from_utf8(
|
let stylesheet = String::from_utf8(
|
||||||
resources_lookup_data(
|
resources_lookup_data(
|
||||||
|
@ -83,10 +86,23 @@ impl AppWindow {
|
||||||
|
|
||||||
let initial_view = View::Placeholder(PlaceholderView::default().upcast());
|
let initial_view = View::Placeholder(PlaceholderView::default().upcast());
|
||||||
|
|
||||||
|
let header_bar = adw::HeaderBar::new();
|
||||||
|
|
||||||
|
let main_menu = gio::Menu::new();
|
||||||
|
main_menu.append(Some("About"), Some("app.about"));
|
||||||
|
main_menu.append(Some("Quit"), Some("app.quit"));
|
||||||
|
let main_menu_button = gtk::MenuButton::builder()
|
||||||
|
.icon_name("open-menu")
|
||||||
|
.direction(gtk::ArrowType::Down)
|
||||||
|
.halign(gtk::Align::End)
|
||||||
|
.menu_model(&main_menu)
|
||||||
|
.build();
|
||||||
|
header_bar.pack_end(&main_menu_button);
|
||||||
|
|
||||||
layout.append(&initial_view.widget());
|
layout.append(&initial_view.widget());
|
||||||
|
|
||||||
let nav_layout = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
let nav_layout = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
nav_layout.append(&adw::HeaderBar::new());
|
nav_layout.append(&header_bar);
|
||||||
nav_layout.append(&layout);
|
nav_layout.append(&layout);
|
||||||
navigation.push(
|
navigation.push(
|
||||||
&adw::NavigationPage::builder()
|
&adw::NavigationPage::builder()
|
||||||
|
@ -99,10 +115,6 @@ impl AppWindow {
|
||||||
window.set_content(Some(&navigation));
|
window.set_content(Some(&navigation));
|
||||||
window.present();
|
window.present();
|
||||||
|
|
||||||
let gesture = gtk::GestureClick::new();
|
|
||||||
gesture.connect_released(|_, _, _, _| println!("detected gesture"));
|
|
||||||
layout.add_controller(gesture);
|
|
||||||
|
|
||||||
let s = Self {
|
let s = Self {
|
||||||
app: ft_app,
|
app: ft_app,
|
||||||
layout,
|
layout,
|
||||||
|
@ -133,25 +145,31 @@ impl AppWindow {
|
||||||
self.swap_main(view);
|
self.swap_main(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_historical_view(&self, records: Vec<Record<TraxRecord>>) {
|
fn show_historical_view(&self, interval: DayInterval) {
|
||||||
let view = View::Historical(HistoricalView::new(self.app.clone(), records, {
|
let on_select_day = {
|
||||||
let s = self.clone();
|
let s = self.clone();
|
||||||
Rc::new(move |date, records| {
|
move |date| {
|
||||||
|
let s = s.clone();
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
let view_model = DayDetailViewModel::new(date, s.app.clone()).await.unwrap();
|
||||||
let layout = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
let layout = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
layout.append(&adw::HeaderBar::new());
|
layout.append(&adw::HeaderBar::new());
|
||||||
// layout.append(&DayDetailView::new(date, records, s.app.clone()));
|
// layout.append(&DayDetailView::new(date, records, s.app.clone()));
|
||||||
layout.append(&DayDetailView::new(DayDetailViewModel::new(
|
layout.append(&DayDetailView::new(view_model));
|
||||||
date,
|
|
||||||
records,
|
|
||||||
s.app.clone(),
|
|
||||||
)));
|
|
||||||
let page = &adw::NavigationPage::builder()
|
let page = &adw::NavigationPage::builder()
|
||||||
.title(date.format("%Y-%m-%d").to_string())
|
.title(date.format("%Y-%m-%d").to_string())
|
||||||
.child(&layout)
|
.child(&layout)
|
||||||
.build();
|
.build();
|
||||||
s.navigation.push(page);
|
s.navigation.push(page);
|
||||||
})
|
});
|
||||||
}));
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let view = View::Historical(HistoricalView::new(
|
||||||
|
self.app.clone(),
|
||||||
|
interval,
|
||||||
|
Rc::new(on_select_day),
|
||||||
|
));
|
||||||
self.swap_main(view);
|
self.swap_main(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,11 +177,12 @@ impl AppWindow {
|
||||||
glib::spawn_future_local({
|
glib::spawn_future_local({
|
||||||
let s = self.clone();
|
let s = self.clone();
|
||||||
async move {
|
async move {
|
||||||
|
if s.app.database_is_open() {
|
||||||
let end = Local::now().date_naive();
|
let end = Local::now().date_naive();
|
||||||
let start = end - Duration::days(7);
|
let start = end - Duration::days(7);
|
||||||
match s.app.records(start, end).await {
|
s.show_historical_view(DayInterval { start, end });
|
||||||
Ok(records) => s.show_historical_view(records),
|
} else {
|
||||||
Err(_) => s.show_welcome_view(),
|
s.show_welcome_view();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -181,6 +200,7 @@ impl AppWindow {
|
||||||
self.layout.append(¤t_widget.widget());
|
self.layout.append(¤t_widget.widget());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
fn on_apply_config(&self, path: PathBuf) {
|
fn on_apply_config(&self, path: PathBuf) {
|
||||||
glib::spawn_future_local({
|
glib::spawn_future_local({
|
||||||
let s = self.clone();
|
let s = self.clone();
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
|
This file is part of FitnessTrax.
|
||||||
|
|
||||||
|
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||||
|
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::{components::{i32_field_builder, TextEntry, month_field_builder}, types::ParseError};
|
||||||
|
use chrono::{Datelike, Local};
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
pub struct DateFieldPrivate {
|
||||||
|
date: Rc<RefCell<chrono::NaiveDate>>,
|
||||||
|
year: TextEntry<i32>,
|
||||||
|
month: TextEntry<u32>,
|
||||||
|
day: TextEntry<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for DateFieldPrivate {
|
||||||
|
const NAME: &'static str = "DateField";
|
||||||
|
type Type = DateField;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
let date = Rc::new(RefCell::new(Local::now().date_naive()));
|
||||||
|
|
||||||
|
let year = i32_field_builder()
|
||||||
|
.with_value(date.borrow().year())
|
||||||
|
.with_on_update(
|
||||||
|
{
|
||||||
|
let date = date.clone();
|
||||||
|
move |value| {
|
||||||
|
if let Some(year) = value {
|
||||||
|
let mut date = date.borrow_mut();
|
||||||
|
if let Some(new_date) = date.with_year(year) {
|
||||||
|
*date = new_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_length(4)
|
||||||
|
.with_css_classes(vec!["date-field__year".to_owned()]).build();
|
||||||
|
|
||||||
|
let month = month_field_builder()
|
||||||
|
.with_value(date.borrow().month())
|
||||||
|
.with_on_update(
|
||||||
|
{
|
||||||
|
let date = date.clone();
|
||||||
|
move |value| {
|
||||||
|
if let Some(month) = value {
|
||||||
|
let mut date = date.borrow_mut();
|
||||||
|
if let Some(new_date) = date.with_month(month) {
|
||||||
|
*date = new_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_css_classes(vec!["date-field__month".to_owned()])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
/* Modify this so that it enforces the number of days per month */
|
||||||
|
let day = TextEntry::builder()
|
||||||
|
.with_placeholder("day".to_owned())
|
||||||
|
.with_value(date.borrow().day())
|
||||||
|
.with_renderer(|v| format!("{}", v))
|
||||||
|
.with_parser(|v| v.parse::<u32>().map_err(|_| ParseError))
|
||||||
|
.with_on_update({
|
||||||
|
let date = date.clone();
|
||||||
|
move |value| {
|
||||||
|
if let Some(day) = value {
|
||||||
|
let mut date = date.borrow_mut();
|
||||||
|
if let Some(new_date) = date.with_day(day) {
|
||||||
|
*date = new_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_css_classes(vec!["date-field__day".to_owned()])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
date,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for DateFieldPrivate {}
|
||||||
|
impl WidgetImpl for DateFieldPrivate {}
|
||||||
|
impl BoxImpl for DateFieldPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct DateField(ObjectSubclass<DateFieldPrivate>) @extends gtk::Box, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Render a date in the format 2006 Jan 01. The entire date is editable. When the user moves to one part of the date, it will be erased and replaced with a grey placeholder.
|
||||||
|
*/
|
||||||
|
impl DateField {
|
||||||
|
pub fn new(date: chrono::NaiveDate) -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.add_css_class("date-field");
|
||||||
|
|
||||||
|
s.append(&s.imp().year.widget());
|
||||||
|
s.append(>k::Label::new(Some("-")));
|
||||||
|
s.append(&s.imp().month.widget());
|
||||||
|
s.append(>k::Label::new(Some("-")));
|
||||||
|
s.append(&s.imp().day.widget());
|
||||||
|
|
||||||
|
s.set_date(date);
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_date(&self, date: chrono::NaiveDate) {
|
||||||
|
self.imp().year.set_value(Some(date.year()));
|
||||||
|
self.imp().month.set_value(Some(date.month()));
|
||||||
|
self.imp().day.set_value(Some(date.day()));
|
||||||
|
|
||||||
|
*self.imp().date.borrow_mut() = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn date(&self) -> chrono::NaiveDate {
|
||||||
|
*self.imp().date.borrow()
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
// use crate::gtk_init::gtk_init;
|
||||||
|
|
||||||
|
// Enabling this test pushes tests on the TextField into an infinite loop. That likely indicates a bad interaction within the TextField itself, and that is going to need to be fixed.
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn it_allows_valid_dates() {
|
||||||
|
let reference = chrono::NaiveDate::from_ymd_opt(2006, 01, 02).unwrap();
|
||||||
|
let field = DateField::new(reference);
|
||||||
|
field.imp().year.set_value(Some(2023));
|
||||||
|
field.imp().month.set_value(Some(10));
|
||||||
|
field.imp().day.set_value(Some(13));
|
||||||
|
// assert!(field.is_valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn it_disallows_out_of_range_months() {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn it_allows_days_within_range_for_month() {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
|
This file is part of FitnessTrax.
|
||||||
|
|
||||||
|
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||||
|
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::{components::DateField, types::DayInterval};
|
||||||
|
use chrono::{Duration, Local, Months};
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
type OnSearch = dyn Fn(DayInterval) + 'static;
|
||||||
|
|
||||||
|
pub struct DateRangePickerPrivate {
|
||||||
|
start: DateField,
|
||||||
|
end: DateField,
|
||||||
|
|
||||||
|
on_search: RefCell<Box<OnSearch>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for DateRangePickerPrivate {
|
||||||
|
const NAME: &'static str = "DateRangePicker";
|
||||||
|
type Type = DateRangePicker;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
let default_date = Local::now().date_naive();
|
||||||
|
let start = DateField::new(default_date);
|
||||||
|
start.add_css_class("date-range-picker__date-field");
|
||||||
|
let end = DateField::new(default_date);
|
||||||
|
end.add_css_class("date-range-picker__date-field");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
on_search: RefCell::new(Box::new(|_| {})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for DateRangePickerPrivate {}
|
||||||
|
impl WidgetImpl for DateRangePickerPrivate {}
|
||||||
|
impl BoxImpl for DateRangePickerPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct DateRangePicker(ObjectSubclass<DateRangePickerPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DateRangePicker {
|
||||||
|
pub fn connect_on_search<OnSearch>(&self, f: OnSearch)
|
||||||
|
where
|
||||||
|
OnSearch: Fn(DayInterval) + 'static,
|
||||||
|
{
|
||||||
|
*self.imp().on_search.borrow_mut() = Box::new(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_interval(&self, start: chrono::NaiveDate, end: chrono::NaiveDate) {
|
||||||
|
self.imp().start.set_date(start);
|
||||||
|
self.imp().end.set_date(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interval(&self) -> DayInterval {
|
||||||
|
DayInterval {
|
||||||
|
start: self.imp().start.date(),
|
||||||
|
end: self.imp().end.date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DateRangePicker {
|
||||||
|
fn default() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.set_orientation(gtk::Orientation::Vertical);
|
||||||
|
s.add_css_class("date-range-picker");
|
||||||
|
|
||||||
|
let search_button = gtk::Button::builder()
|
||||||
|
.css_classes(["date-range-picker__search-button"])
|
||||||
|
.label("Search")
|
||||||
|
.build();
|
||||||
|
search_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| (s.imp().on_search.borrow())(s.interval())
|
||||||
|
});
|
||||||
|
|
||||||
|
let last_week_button = gtk::Button::builder()
|
||||||
|
.css_classes(["date-range-picker__range-button"])
|
||||||
|
.label("week")
|
||||||
|
.build();
|
||||||
|
last_week_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| {
|
||||||
|
let end = Local::now().date_naive();
|
||||||
|
let start = end - Duration::days(7);
|
||||||
|
s.set_interval(start, end);
|
||||||
|
(s.imp().on_search.borrow())(s.interval());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let two_weeks_button = gtk::Button::builder()
|
||||||
|
.css_classes(["date-range-picker__range-button"])
|
||||||
|
.label("two weeks")
|
||||||
|
.build();
|
||||||
|
two_weeks_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| {
|
||||||
|
let end = Local::now().date_naive();
|
||||||
|
let start = end - Duration::days(14);
|
||||||
|
s.set_interval(start, end);
|
||||||
|
(s.imp().on_search.borrow())(s.interval());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let last_month_button = gtk::Button::builder()
|
||||||
|
.css_classes(["date-range-picker__range-button"])
|
||||||
|
.label("month")
|
||||||
|
.build();
|
||||||
|
last_month_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| {
|
||||||
|
let end = Local::now().date_naive();
|
||||||
|
let start = end - Months::new(1);
|
||||||
|
s.set_interval(start, end);
|
||||||
|
(s.imp().on_search.borrow())(s.interval());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let six_months_button = gtk::Button::builder()
|
||||||
|
.css_classes(["date-range-picker__range-button"])
|
||||||
|
.label("six months")
|
||||||
|
.build();
|
||||||
|
six_months_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| {
|
||||||
|
let end = Local::now().date_naive();
|
||||||
|
let start = end - Months::new(6);
|
||||||
|
s.set_interval(start, end);
|
||||||
|
(s.imp().on_search.borrow())(s.interval());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let last_year_button = gtk::Button::builder()
|
||||||
|
.css_classes(["date-range-picker__range-button"])
|
||||||
|
.label("year")
|
||||||
|
.build();
|
||||||
|
last_year_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| {
|
||||||
|
let end = Local::now().date_naive();
|
||||||
|
let start = end - Months::new(12);
|
||||||
|
s.set_interval(start, end);
|
||||||
|
(s.imp().on_search.borrow())(s.interval());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let date_row = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
date_row.append(&s.imp().start);
|
||||||
|
date_row.append(>k::Label::new(Some("to")));
|
||||||
|
date_row.append(&s.imp().end);
|
||||||
|
date_row.append(&search_button);
|
||||||
|
|
||||||
|
let quick_picker = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
quick_picker.append(&last_week_button);
|
||||||
|
quick_picker.append(&two_weeks_button);
|
||||||
|
quick_picker.append(&last_month_button);
|
||||||
|
quick_picker.append(&six_months_button);
|
||||||
|
quick_picker.append(&last_year_button);
|
||||||
|
|
||||||
|
s.append(&date_row);
|
||||||
|
s.append(&quick_picker);
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,12 +17,19 @@ You should have received a copy of the GNU General Public License along with Fit
|
||||||
// use chrono::NaiveDate;
|
// use chrono::NaiveDate;
|
||||||
// use ft_core::TraxRecord;
|
// use ft_core::TraxRecord;
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{steps_editor, weight_field, ActionGroup, Steps, WeightLabel},
|
components::{
|
||||||
|
steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, WeightLabel,
|
||||||
|
},
|
||||||
|
types::{DistanceFormatter, DurationFormatter, WeightFormatter},
|
||||||
view_models::DayDetailViewModel,
|
view_models::DayDetailViewModel,
|
||||||
};
|
};
|
||||||
|
use emseries::{Record, RecordId};
|
||||||
|
use ft_core::{TimeDistanceActivity, TraxRecord, TIME_DISTANCE_ACTIVITIES};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use std::cell::RefCell;
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use super::{time_distance::TimeDistanceEdit, time_distance_detail};
|
||||||
|
|
||||||
pub struct DaySummaryPrivate {
|
pub struct DaySummaryPrivate {
|
||||||
date: gtk::Label,
|
date: gtk::Label,
|
||||||
|
@ -77,27 +84,36 @@ impl DaySummary {
|
||||||
|
|
||||||
let row = gtk::Box::builder().build();
|
let row = gtk::Box::builder().build();
|
||||||
|
|
||||||
let label = gtk::Label::builder()
|
let weight_label = gtk::Label::builder()
|
||||||
.halign(gtk::Align::Start)
|
.halign(gtk::Align::Start)
|
||||||
.css_classes(["day-summary__weight"])
|
.css_classes(["day-summary__weight"])
|
||||||
.build();
|
.build();
|
||||||
if let Some(w) = view_model.weight() {
|
if let Some(w) = view_model.weight() {
|
||||||
label.set_label(&w.to_string())
|
weight_label.set_label(&w.to_string())
|
||||||
}
|
}
|
||||||
row.append(&label);
|
|
||||||
|
|
||||||
self.append(&label);
|
let steps_label = gtk::Label::builder()
|
||||||
|
|
||||||
let label = gtk::Label::builder()
|
|
||||||
.halign(gtk::Align::Start)
|
.halign(gtk::Align::Start)
|
||||||
.css_classes(["day-summary__weight"])
|
.css_classes(["day-summary__steps"])
|
||||||
.build();
|
.build();
|
||||||
if let Some(s) = view_model.steps() {
|
if let Some(s) = view_model.steps() {
|
||||||
label.set_label(&format!("{} steps", s));
|
steps_label.set_label(&format!("{} steps", s));
|
||||||
}
|
}
|
||||||
row.append(&label);
|
|
||||||
|
|
||||||
|
row.append(&weight_label);
|
||||||
|
row.append(&steps_label);
|
||||||
self.append(&row);
|
self.append(&row);
|
||||||
|
|
||||||
|
for activity in TIME_DISTANCE_ACTIVITIES {
|
||||||
|
let summary = view_model.time_distance_summary(activity);
|
||||||
|
if let Some(label) = time_distance_summary(
|
||||||
|
activity,
|
||||||
|
DistanceFormatter::from(summary.0),
|
||||||
|
DurationFormatter::from(summary.1),
|
||||||
|
) {
|
||||||
|
self.append(&label);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,35 +150,10 @@ impl DayDetail {
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
|
||||||
let click_controller = gtk::GestureClick::new();
|
|
||||||
click_controller.connect_released({
|
|
||||||
let s = s.clone();
|
|
||||||
move |_, _, _, _| {
|
|
||||||
println!("clicked outside of focusable entity");
|
|
||||||
if let Some(widget) = s.focus_child().and_downcast_ref::<WeightView>() {
|
|
||||||
println!("focused child is the weight view");
|
|
||||||
widget.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
s.add_controller(click_controller);
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
let weight_record = records.iter().find_map(|record| match record {
|
|
||||||
Record {
|
|
||||||
id,
|
|
||||||
data: ft_core::TraxRecord::Weight(record),
|
|
||||||
} => Some((id.clone(), record.clone())),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
let top_row = gtk::Box::builder()
|
let top_row = gtk::Box::builder()
|
||||||
.orientation(gtk::Orientation::Horizontal)
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
.build();
|
.build();
|
||||||
let weight_view = WeightLabel::new(view_model.weight());
|
let weight_view = WeightLabel::new(view_model.weight().map(WeightFormatter::from));
|
||||||
top_row.append(&weight_view.widget());
|
top_row.append(&weight_view.widget());
|
||||||
|
|
||||||
let steps_view = Steps::new(view_model.steps());
|
let steps_view = Steps::new(view_model.steps());
|
||||||
|
@ -170,51 +161,10 @@ impl DayDetail {
|
||||||
|
|
||||||
s.append(&top_row);
|
s.append(&top_row);
|
||||||
|
|
||||||
/*
|
let records = view_model.time_distance_records();
|
||||||
records.into_iter().for_each(|record| {
|
for emseries::Record { data, .. } in records {
|
||||||
let record_view = match record {
|
s.append(&time_distance_detail(data));
|
||||||
Record {
|
|
||||||
data: ft_core::TraxRecord::BikeRide(record),
|
|
||||||
..
|
|
||||||
} => Some(
|
|
||||||
TimeDistanceView::new(ft_core::RecordType::BikeRide, record)
|
|
||||||
.upcast::<gtk::Widget>(),
|
|
||||||
),
|
|
||||||
Record {
|
|
||||||
data: ft_core::TraxRecord::Row(record),
|
|
||||||
..
|
|
||||||
} => Some(
|
|
||||||
TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::<gtk::Widget>(),
|
|
||||||
),
|
|
||||||
Record {
|
|
||||||
data: ft_core::TraxRecord::Run(record),
|
|
||||||
..
|
|
||||||
} => Some(
|
|
||||||
TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::<gtk::Widget>(),
|
|
||||||
),
|
|
||||||
Record {
|
|
||||||
data: ft_core::TraxRecord::Swim(record),
|
|
||||||
..
|
|
||||||
} => Some(
|
|
||||||
TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::<gtk::Widget>(),
|
|
||||||
),
|
|
||||||
Record {
|
|
||||||
data: ft_core::TraxRecord::Walk(record),
|
|
||||||
..
|
|
||||||
} => Some(
|
|
||||||
TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::<gtk::Widget>(),
|
|
||||||
),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(record_view) = record_view {
|
|
||||||
record_view.add_css_class("day-detail");
|
|
||||||
record_view.set_halign(gtk::Align::Start);
|
|
||||||
|
|
||||||
s.append(&record_view);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
@ -222,12 +172,22 @@ impl DayDetail {
|
||||||
|
|
||||||
pub struct DayEditPrivate {
|
pub struct DayEditPrivate {
|
||||||
on_finished: RefCell<Box<dyn Fn()>>,
|
on_finished: RefCell<Box<dyn Fn()>>,
|
||||||
|
#[allow(unused)]
|
||||||
|
workout_rows: RefCell<gtk::Box>,
|
||||||
|
view_model: RefCell<Option<DayDetailViewModel>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DayEditPrivate {
|
impl Default for DayEditPrivate {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
on_finished: RefCell::new(Box::new(|| {})),
|
on_finished: RefCell::new(Box::new(|| {})),
|
||||||
|
workout_rows: RefCell::new(
|
||||||
|
gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.hexpand(true)
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
view_model: RefCell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,45 +215,121 @@ impl DayEdit {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
s.set_orientation(gtk::Orientation::Vertical);
|
s.set_orientation(gtk::Orientation::Vertical);
|
||||||
s.set_hexpand(true);
|
s.set_hexpand(true);
|
||||||
|
|
||||||
*s.imp().on_finished.borrow_mut() = Box::new(on_finished);
|
*s.imp().on_finished.borrow_mut() = Box::new(on_finished);
|
||||||
|
*s.imp().view_model.borrow_mut() = Some(view_model.clone());
|
||||||
|
|
||||||
s.append(
|
let workout_buttons = workout_buttons(view_model.clone(), {
|
||||||
&ActionGroup::builder()
|
let s = s.clone();
|
||||||
|
move |workout| s.add_row(workout)
|
||||||
|
});
|
||||||
|
|
||||||
|
view_model
|
||||||
|
.records()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map({
|
||||||
|
let s = s.clone();
|
||||||
|
move |record| match record.data {
|
||||||
|
TraxRecord::TimeDistance(workout) => Some(TimeDistanceEdit::new(workout, {
|
||||||
|
let s = s.clone();
|
||||||
|
move |data| {
|
||||||
|
s.update_workout(record.id, data);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.for_each(|row| s.imp().workout_rows.borrow().append(&row));
|
||||||
|
|
||||||
|
s.append(&control_buttons(&s, &view_model));
|
||||||
|
s.append(&weight_and_steps_row(&view_model));
|
||||||
|
s.append(&*s.imp().workout_rows.borrow());
|
||||||
|
s.append(&workout_buttons);
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&self) {
|
||||||
|
glib::spawn_future_local({
|
||||||
|
let s = self.clone();
|
||||||
|
async move {
|
||||||
|
let view_model = {
|
||||||
|
let view_model = s.imp().view_model.borrow();
|
||||||
|
view_model
|
||||||
|
.as_ref()
|
||||||
|
.expect("DayEdit has not been initialized with the view model")
|
||||||
|
.clone()
|
||||||
|
};
|
||||||
|
let _ = view_model.async_save().await;
|
||||||
|
(s.imp().on_finished.borrow())()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_row(&self, workout: Record<TraxRecord>) {
|
||||||
|
let workout_rows = self.imp().workout_rows.borrow();
|
||||||
|
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match workout.data {
|
||||||
|
TraxRecord::TimeDistance(r) => workout_rows.append(&TimeDistanceEdit::new(r, {
|
||||||
|
let s = self.clone();
|
||||||
|
move |data| {
|
||||||
|
println!("update workout callback on workout: {:?}", workout.id);
|
||||||
|
s.update_workout(workout.id, data)
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_workout(&self, id: RecordId, data: ft_core::TimeDistance) {
|
||||||
|
if let Some(ref view_model) = *self.imp().view_model.borrow() {
|
||||||
|
let record = Record {
|
||||||
|
id,
|
||||||
|
data: TraxRecord::TimeDistance(data),
|
||||||
|
};
|
||||||
|
view_model.update_record(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup {
|
||||||
|
ActionGroup::builder()
|
||||||
.primary_action("Save", {
|
.primary_action("Save", {
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
let view_model = view_model.clone();
|
move || s.finish()
|
||||||
move || {
|
|
||||||
view_model.save();
|
|
||||||
s.finish();
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.secondary_action("Cancel", {
|
.secondary_action("Cancel", {
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
let view_model = view_model.clone();
|
let view_model = view_model.clone();
|
||||||
move || {
|
move || {
|
||||||
view_model.revert();
|
let s = s.clone();
|
||||||
|
let view_model = view_model.clone();
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
view_model.revert().await;
|
||||||
s.finish();
|
s.finish();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build(),
|
.build()
|
||||||
);
|
}
|
||||||
|
|
||||||
let top_row = gtk::Box::builder()
|
fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box {
|
||||||
|
let row = gtk::Box::builder()
|
||||||
.orientation(gtk::Orientation::Horizontal)
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
.build();
|
.build();
|
||||||
top_row.append(
|
row.append(
|
||||||
&weight_field(view_model.weight(), {
|
&weight_field(view_model.weight().map(WeightFormatter::from), {
|
||||||
let view_model = view_model.clone();
|
let view_model = view_model.clone();
|
||||||
move |w| match w {
|
move |w| match w {
|
||||||
Some(w) => view_model.set_weight(w),
|
Some(w) => view_model.set_weight(*w),
|
||||||
None => eprintln!("have not implemented record delete"),
|
None => eprintln!("have not implemented record delete"),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.widget(),
|
.widget(),
|
||||||
);
|
);
|
||||||
|
|
||||||
top_row.append(
|
row.append(
|
||||||
&steps_editor(view_model.steps(), {
|
&steps_editor(view_model.steps(), {
|
||||||
let view_model = view_model.clone();
|
let view_model = view_model.clone();
|
||||||
move |s| match s {
|
move |s| match s {
|
||||||
|
@ -303,12 +339,62 @@ impl DayEdit {
|
||||||
})
|
})
|
||||||
.widget(),
|
.widget(),
|
||||||
);
|
);
|
||||||
s.append(&top_row);
|
|
||||||
|
|
||||||
s
|
row
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(&self) {
|
fn workout_buttons<AddRow>(view_model: DayDetailViewModel, add_row: AddRow) -> gtk::Box
|
||||||
(self.imp().on_finished.borrow())()
|
where
|
||||||
|
AddRow: Fn(Record<TraxRecord>) + 'static,
|
||||||
|
{
|
||||||
|
let add_row = Rc::new(add_row);
|
||||||
|
|
||||||
|
let layout = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
for (activity, icon, label) in [
|
||||||
|
(
|
||||||
|
TimeDistanceActivity::Biking,
|
||||||
|
"cycling-symbolic",
|
||||||
|
"Bike Ride",
|
||||||
|
),
|
||||||
|
(TimeDistanceActivity::Rowing, "rowing-symbolic", "Rowing"),
|
||||||
|
(TimeDistanceActivity::Running, "running-symbolic", "Run"),
|
||||||
|
(TimeDistanceActivity::Swimming, "swimming-symbolic", "Swim"),
|
||||||
|
(TimeDistanceActivity::Walking, "walking-symbolic", "Walk"),
|
||||||
|
] {
|
||||||
|
let button = workout_button(activity, icon, label, view_model.clone(), {
|
||||||
|
let add_row = add_row.clone();
|
||||||
|
move |record| add_row(record)
|
||||||
|
});
|
||||||
|
layout.append(&button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layout
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workout_button<AddRow>(
|
||||||
|
activity: TimeDistanceActivity,
|
||||||
|
_icon: &str,
|
||||||
|
label: &str,
|
||||||
|
view_model: DayDetailViewModel,
|
||||||
|
add_row: AddRow,
|
||||||
|
) -> gtk::Button
|
||||||
|
where
|
||||||
|
AddRow: Fn(Record<TraxRecord>) + 'static,
|
||||||
|
{
|
||||||
|
let button = gtk::Button::builder()
|
||||||
|
.label(label)
|
||||||
|
.width_request(64)
|
||||||
|
.height_request(64)
|
||||||
|
.build();
|
||||||
|
button.connect_clicked({
|
||||||
|
let view_model = view_model.clone();
|
||||||
|
move |_| {
|
||||||
|
let workout = view_model.new_time_distance(activity);
|
||||||
|
add_row(workout.map(TraxRecord::TimeDistance));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
button
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY W
|
||||||
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
General Public License for more details.
|
General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not,
|
||||||
|
see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
mod action_group;
|
mod action_group;
|
||||||
|
@ -20,6 +21,12 @@ pub use action_group::ActionGroup;
|
||||||
mod day;
|
mod day;
|
||||||
pub use day::{DayDetail, DayEdit, DaySummary};
|
pub use day::{DayDetail, DayEdit, DaySummary};
|
||||||
|
|
||||||
|
mod date_field;
|
||||||
|
pub use date_field::DateField;
|
||||||
|
|
||||||
|
mod date_range;
|
||||||
|
pub use date_range::DateRangePicker;
|
||||||
|
|
||||||
mod singleton;
|
mod singleton;
|
||||||
pub use singleton::{Singleton, SingletonImpl};
|
pub use singleton::{Singleton, SingletonImpl};
|
||||||
|
|
||||||
|
@ -27,10 +34,10 @@ mod steps;
|
||||||
pub use steps::{steps_editor, Steps};
|
pub use steps::{steps_editor, Steps};
|
||||||
|
|
||||||
mod text_entry;
|
mod text_entry;
|
||||||
pub use text_entry::{weight_field, TextEntry};
|
pub use text_entry::{distance_field, duration_field, time_field, weight_field, i32_field_builder, month_field_builder, TextEntry};
|
||||||
|
|
||||||
mod time_distance;
|
mod time_distance;
|
||||||
pub use time_distance::TimeDistanceView;
|
pub use time_distance::{time_distance_detail, time_distance_summary};
|
||||||
|
|
||||||
mod weight;
|
mod weight;
|
||||||
pub use weight::WeightLabel;
|
pub use weight::WeightLabel;
|
||||||
|
|
|
@ -46,11 +46,15 @@ pub fn steps_editor<OnUpdate>(value: Option<u32>, on_update: OnUpdate) -> TextEn
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<u32>) + 'static,
|
OnUpdate: Fn(Option<u32>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
let text_entry = TextEntry::builder()
|
||||||
"0",
|
.with_placeholder( "0".to_owned())
|
||||||
value,
|
.with_renderer(|v| format!("{}", v))
|
||||||
|v| format!("{}", v),
|
.with_parser(|v| v.parse::<u32>().map_err(|_| ParseError))
|
||||||
move |v| v.parse::<u32>().map_err(|_| ParseError),
|
.with_on_update(on_update);
|
||||||
on_update,
|
|
||||||
)
|
if let Some(time) = value {
|
||||||
|
text_entry.with_value(time)
|
||||||
|
} else {
|
||||||
|
text_entry
|
||||||
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,14 +20,16 @@ use crate::types::{
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
|
pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
|
||||||
type OnUpdate<T> = dyn Fn(Option<T>);
|
pub type OnUpdate<T> = dyn Fn(Option<T>);
|
||||||
|
|
||||||
|
// TextEntry is not a proper widget because I was never able to figure out how to do a type parameterization on a GTK widget.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TextEntry<T: Clone + std::fmt::Debug> {
|
pub struct TextEntry<T: Clone + std::fmt::Debug> {
|
||||||
value: Rc<RefCell<Option<T>>>,
|
value: Rc<RefCell<Option<T>>>,
|
||||||
|
|
||||||
widget: gtk::Entry,
|
widget: gtk::Entry,
|
||||||
|
renderer: Rc<dyn Fn(&T) -> String>,
|
||||||
parser: Rc<Parser<T>>,
|
parser: Rc<Parser<T>>,
|
||||||
on_update: Rc<OnUpdate<T>>,
|
on_update: Rc<OnUpdate<T>>,
|
||||||
}
|
}
|
||||||
|
@ -44,28 +46,20 @@ impl<T: Clone + std::fmt::Debug> std::fmt::Debug for TextEntry<T> {
|
||||||
|
|
||||||
// I do not understand why the data should be 'static.
|
// I do not understand why the data should be 'static.
|
||||||
impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
|
impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
|
||||||
pub fn new<R, V, U>(
|
fn from_builder(builder: TextEntryBuilder<T>) -> TextEntry<T> {
|
||||||
placeholder: &str,
|
let widget = gtk::Entry::builder()
|
||||||
value: Option<T>,
|
.placeholder_text(builder.placeholder)
|
||||||
renderer: R,
|
.build();
|
||||||
parser: V,
|
if let Some(ref v) = builder.value {
|
||||||
on_update: U,
|
widget.set_text(&(builder.renderer)(v))
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
R: Fn(&T) -> String + 'static,
|
|
||||||
V: Fn(&str) -> Result<T, ParseError> + 'static,
|
|
||||||
U: Fn(Option<T>) + 'static,
|
|
||||||
{
|
|
||||||
let widget = gtk::Entry::builder().placeholder_text(placeholder).build();
|
|
||||||
if let Some(ref v) = value {
|
|
||||||
widget.set_text(&renderer(v))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let s = Self {
|
let s = Self {
|
||||||
value: Rc::new(RefCell::new(value)),
|
value: Rc::new(RefCell::new(builder.value)),
|
||||||
widget,
|
widget,
|
||||||
parser: Rc::new(parser),
|
renderer: Rc::new(builder.renderer),
|
||||||
on_update: Rc::new(on_update),
|
parser: Rc::new(builder.parser),
|
||||||
|
on_update: Rc::new(builder.on_update),
|
||||||
};
|
};
|
||||||
|
|
||||||
s.widget.buffer().connect_text_notify({
|
s.widget.buffer().connect_text_notify({
|
||||||
|
@ -73,9 +67,27 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
|
||||||
move |buffer| s.handle_text_change(buffer)
|
move |buffer| s.handle_text_change(buffer)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(length) = builder.length {
|
||||||
|
s.widget.set_max_length(length.try_into().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// let classes: Vec<&str> = builder.css_classes.iter(|v| v.as_ref()).collect();
|
||||||
|
let classes: Vec<&str> = builder.css_classes.iter().map(AsRef::as_ref).collect();
|
||||||
|
s.widget.set_css_classes(&classes);
|
||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn builder() -> TextEntryBuilder<T> {
|
||||||
|
TextEntryBuilder::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_value(&self, val: Option<T>) {
|
||||||
|
if let Some(ref v) = val {
|
||||||
|
self.widget.set_text(&(self.renderer)(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_text_change(&self, buffer: >k::EntryBuffer) {
|
fn handle_text_change(&self, buffer: >k::EntryBuffer) {
|
||||||
if buffer.text().is_empty() {
|
if buffer.text().is_empty() {
|
||||||
*self.value.borrow_mut() = None;
|
*self.value.borrow_mut() = None;
|
||||||
|
@ -106,6 +118,85 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TextEntryBuilder<T: Clone + std::fmt::Debug + 'static> {
|
||||||
|
placeholder: String,
|
||||||
|
value: Option<T>,
|
||||||
|
length: Option<usize>,
|
||||||
|
css_classes: Vec<String>,
|
||||||
|
renderer: Box<dyn Fn(&T) -> String>,
|
||||||
|
parser: Box<Parser<T>>,
|
||||||
|
on_update: Box<OnUpdate<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + std::fmt::Debug + 'static> Default for TextEntryBuilder<T> {
|
||||||
|
fn default() -> TextEntryBuilder<T> {
|
||||||
|
TextEntryBuilder {
|
||||||
|
placeholder: "".to_owned(),
|
||||||
|
value: None,
|
||||||
|
length: None,
|
||||||
|
css_classes: vec![],
|
||||||
|
renderer: Box::new(|_| "".to_owned()),
|
||||||
|
parser: Box::new(|_| Err(ParseError)),
|
||||||
|
on_update: Box::new(|_| {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + std::fmt::Debug + 'static> TextEntryBuilder<T> {
|
||||||
|
pub fn build(self) -> TextEntry<T> {
|
||||||
|
TextEntry::from_builder(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_placeholder(self, placeholder: String) -> Self {
|
||||||
|
Self {
|
||||||
|
placeholder,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_value(self, value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
value: Some(value),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_length(self, length: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
length: Some(length),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_css_classes(self, classes: Vec<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
css_classes: classes,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_renderer(self, renderer: impl Fn(&T) -> String + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
renderer: Box::new(renderer),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_parser(self, parser: impl Fn(&str) -> Result<T, ParseError> + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
parser: Box::new(parser),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_on_update(self, on_update: impl Fn(Option<T>) + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
on_update: Box::new(on_update),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn time_field<OnUpdate>(
|
pub fn time_field<OnUpdate>(
|
||||||
value: Option<TimeFormatter>,
|
value: Option<TimeFormatter>,
|
||||||
on_update: OnUpdate,
|
on_update: OnUpdate,
|
||||||
|
@ -113,13 +204,18 @@ pub fn time_field<OnUpdate>(
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<TimeFormatter>) + 'static,
|
OnUpdate: Fn(Option<TimeFormatter>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
let text_entry = TextEntry::builder()
|
||||||
"HH:MM",
|
.with_placeholder("HH:MM".to_owned())
|
||||||
value,
|
.with_renderer(|val: &TimeFormatter| val.format(FormatOption::Abbreviated))
|
||||||
|val| val.format(FormatOption::Abbreviated),
|
.with_parser(TimeFormatter::parse)
|
||||||
TimeFormatter::parse,
|
.with_on_update(on_update);
|
||||||
on_update,
|
|
||||||
)
|
if let Some(time) = value {
|
||||||
|
text_entry.with_value(time)
|
||||||
|
} else {
|
||||||
|
text_entry
|
||||||
|
}
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn distance_field<OnUpdate>(
|
pub fn distance_field<OnUpdate>(
|
||||||
|
@ -129,13 +225,18 @@ pub fn distance_field<OnUpdate>(
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<DistanceFormatter>) + 'static,
|
OnUpdate: Fn(Option<DistanceFormatter>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
let text_entry = TextEntry::builder()
|
||||||
"0 km",
|
.with_placeholder("0 km".to_owned())
|
||||||
value,
|
.with_renderer(|val: &DistanceFormatter| val.format(FormatOption::Abbreviated))
|
||||||
|val| val.format(FormatOption::Abbreviated),
|
.with_parser(DistanceFormatter::parse)
|
||||||
DistanceFormatter::parse,
|
.with_on_update(on_update);
|
||||||
on_update,
|
|
||||||
)
|
if let Some(distance) = value {
|
||||||
|
text_entry.with_value(distance)
|
||||||
|
} else {
|
||||||
|
text_entry
|
||||||
|
}
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn duration_field<OnUpdate>(
|
pub fn duration_field<OnUpdate>(
|
||||||
|
@ -145,13 +246,18 @@ pub fn duration_field<OnUpdate>(
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<DurationFormatter>) + 'static,
|
OnUpdate: Fn(Option<DurationFormatter>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
let text_entry = TextEntry::builder()
|
||||||
"0 m",
|
.with_placeholder("0 m".to_owned())
|
||||||
value,
|
.with_renderer(|val: &DurationFormatter| val.format(FormatOption::Abbreviated))
|
||||||
|val| val.format(FormatOption::Abbreviated),
|
.with_parser(DurationFormatter::parse)
|
||||||
DurationFormatter::parse,
|
.with_on_update(on_update);
|
||||||
on_update,
|
|
||||||
)
|
if let Some(duration) = value {
|
||||||
|
text_entry.with_value(duration)
|
||||||
|
} else {
|
||||||
|
text_entry
|
||||||
|
}
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
pub fn weight_field<OnUpdate>(
|
pub fn weight_field<OnUpdate>(
|
||||||
weight: Option<WeightFormatter>,
|
weight: Option<WeightFormatter>,
|
||||||
|
@ -160,13 +266,39 @@ pub fn weight_field<OnUpdate>(
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<WeightFormatter>) + 'static,
|
OnUpdate: Fn(Option<WeightFormatter>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
let text_entry = TextEntry::builder()
|
||||||
"0 kg",
|
.with_placeholder("0 kg".to_owned())
|
||||||
weight,
|
.with_renderer(|val: &WeightFormatter| val.format(FormatOption::Abbreviated))
|
||||||
|val| val.format(FormatOption::Abbreviated),
|
.with_parser(WeightFormatter::parse)
|
||||||
WeightFormatter::parse,
|
.with_on_update(on_update);
|
||||||
on_update,
|
if let Some(weight) = weight {
|
||||||
)
|
text_entry.with_value(weight)
|
||||||
|
} else {
|
||||||
|
text_entry
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn i32_field_builder() -> TextEntryBuilder<i32>
|
||||||
|
{
|
||||||
|
TextEntry::builder()
|
||||||
|
.with_placeholder("0".to_owned())
|
||||||
|
.with_renderer(|val| format!("{}", val))
|
||||||
|
.with_parser(|v| v.parse::<i32>().map_err(|_| ParseError))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn month_field_builder() -> TextEntryBuilder<u32>
|
||||||
|
{
|
||||||
|
TextEntry::builder()
|
||||||
|
.with_placeholder("0".to_owned())
|
||||||
|
.with_renderer(|val| format!("{}", val))
|
||||||
|
.with_parser(|v| {
|
||||||
|
let val = v.parse::<u32>().map_err(|_| ParseError)?;
|
||||||
|
if val == 0 || val > 12 {
|
||||||
|
return Err(ParseError);
|
||||||
|
}
|
||||||
|
Ok(val)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -177,16 +309,15 @@ mod test {
|
||||||
fn setup_u32_entry() -> (Rc<RefCell<Option<u32>>>, TextEntry<u32>) {
|
fn setup_u32_entry() -> (Rc<RefCell<Option<u32>>>, TextEntry<u32>) {
|
||||||
let current_value = Rc::new(RefCell::new(None));
|
let current_value = Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
let entry = TextEntry::new(
|
let entry = TextEntry::builder()
|
||||||
"step count",
|
.with_placeholder("step count".to_owned())
|
||||||
None,
|
.with_renderer(|steps| format!("{}", steps))
|
||||||
|steps| format!("{}", steps),
|
.with_parser(|v| v.parse::<u32>().map_err(|_| ParseError))
|
||||||
|v| v.parse::<u32>().map_err(|_| ParseError),
|
.with_on_update({
|
||||||
{
|
|
||||||
let current_value = current_value.clone();
|
let current_value = current_value.clone();
|
||||||
move |v| *current_value.borrow_mut() = v
|
move |v| *current_value.borrow_mut() = v
|
||||||
},
|
})
|
||||||
);
|
.build();
|
||||||
|
|
||||||
(current_value, entry)
|
(current_value, entry)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
Copyright 2023-2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
This file is part of FitnessTrax.
|
This file is part of FitnessTrax.
|
||||||
|
|
||||||
|
@ -14,41 +14,49 @@ General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// use crate::components::{EditView, ParseError, TextEntry};
|
use crate::{
|
||||||
// use chrono::{Local, NaiveDate};
|
components::{distance_field, duration_field, time_field},
|
||||||
// use dimensioned::si;
|
types::{DistanceFormatter, DurationFormatter, FormatOption, TimeFormatter},
|
||||||
use ft_core::{RecordType, TimeDistance};
|
};
|
||||||
|
use dimensioned::si;
|
||||||
|
use ft_core::{TimeDistance, TimeDistanceActivity, TIME_DISTANCE_ACTIVITIES};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use std::cell::RefCell;
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
#[derive(Default)]
|
pub fn time_distance_summary(
|
||||||
pub struct TimeDistanceViewPrivate {
|
activity: TimeDistanceActivity,
|
||||||
#[allow(unused)]
|
distance: DistanceFormatter,
|
||||||
record: RefCell<Option<TimeDistance>>,
|
duration: DurationFormatter,
|
||||||
|
) -> Option<gtk::Label> {
|
||||||
|
let text = match (*distance > si::M, *duration > si::S) {
|
||||||
|
(true, true) => Some(format!(
|
||||||
|
"{} of {:?} in {}",
|
||||||
|
distance.format(FormatOption::Full),
|
||||||
|
activity,
|
||||||
|
duration.format(FormatOption::Full)
|
||||||
|
)),
|
||||||
|
(true, false) => Some(format!(
|
||||||
|
"{} of {:?}",
|
||||||
|
distance.format(FormatOption::Full),
|
||||||
|
activity
|
||||||
|
)),
|
||||||
|
(false, true) => Some(format!(
|
||||||
|
"{} of {:?}",
|
||||||
|
duration.format(FormatOption::Full),
|
||||||
|
activity
|
||||||
|
)),
|
||||||
|
(false, false) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
text.map(|text| gtk::Label::builder().halign(gtk::Align::Start).label(&text).build())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box {
|
||||||
impl ObjectSubclass for TimeDistanceViewPrivate {
|
let layout = gtk::Box::builder()
|
||||||
const NAME: &'static str = "TimeDistanceView";
|
.orientation(gtk::Orientation::Vertical)
|
||||||
type Type = TimeDistanceView;
|
.hexpand(true)
|
||||||
type ParentType = gtk::Box;
|
.build();
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectImpl for TimeDistanceViewPrivate {}
|
|
||||||
impl WidgetImpl for TimeDistanceViewPrivate {}
|
|
||||||
impl BoxImpl for TimeDistanceViewPrivate {}
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct TimeDistanceView(ObjectSubclass<TimeDistanceViewPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimeDistanceView {
|
|
||||||
pub fn new(type_: RecordType, record: TimeDistance) -> Self {
|
|
||||||
let s: Self = Object::builder().build();
|
|
||||||
s.set_orientation(gtk::Orientation::Vertical);
|
|
||||||
s.set_hexpand(true);
|
|
||||||
|
|
||||||
let first_row = gtk::Box::builder().homogeneous(true).build();
|
let first_row = gtk::Box::builder().homogeneous(true).build();
|
||||||
|
|
||||||
first_row.append(
|
first_row.append(
|
||||||
|
@ -61,7 +69,7 @@ impl TimeDistanceView {
|
||||||
first_row.append(
|
first_row.append(
|
||||||
>k::Label::builder()
|
>k::Label::builder()
|
||||||
.halign(gtk::Align::Start)
|
.halign(gtk::Align::Start)
|
||||||
.label(format!("{:?}", type_))
|
.label(format!("{:?}", record.activity))
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -71,7 +79,7 @@ impl TimeDistanceView {
|
||||||
.label(
|
.label(
|
||||||
record
|
record
|
||||||
.distance
|
.distance
|
||||||
.map(|dist| format!("{}", dist))
|
.map(|dist| DistanceFormatter::from(dist).format(FormatOption::Abbreviated))
|
||||||
.unwrap_or("".to_owned()),
|
.unwrap_or("".to_owned()),
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
|
@ -83,15 +91,17 @@ impl TimeDistanceView {
|
||||||
.label(
|
.label(
|
||||||
record
|
record
|
||||||
.duration
|
.duration
|
||||||
.map(|duration| format!("{}", duration))
|
.map(|duration| {
|
||||||
|
DurationFormatter::from(duration).format(FormatOption::Abbreviated)
|
||||||
|
})
|
||||||
.unwrap_or("".to_owned()),
|
.unwrap_or("".to_owned()),
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
s.append(&first_row);
|
layout.append(&first_row);
|
||||||
|
|
||||||
s.append(
|
layout.append(
|
||||||
>k::Label::builder()
|
>k::Label::builder()
|
||||||
.halign(gtk::Align::Start)
|
.halign(gtk::Align::Start)
|
||||||
.label(
|
.label(
|
||||||
|
@ -103,6 +113,159 @@ impl TimeDistanceView {
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
layout
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnUpdate = Rc<RefCell<Box<dyn Fn(TimeDistance)>>>;
|
||||||
|
|
||||||
|
pub struct TimeDistanceEditPrivate {
|
||||||
|
#[allow(unused)]
|
||||||
|
workout: RefCell<ft_core::TimeDistance>,
|
||||||
|
on_update: OnUpdate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TimeDistanceEditPrivate {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
workout: RefCell::new(TimeDistance {
|
||||||
|
datetime: chrono::Utc::now().into(),
|
||||||
|
activity: TimeDistanceActivity::Biking,
|
||||||
|
duration: None,
|
||||||
|
distance: None,
|
||||||
|
comments: None,
|
||||||
|
}),
|
||||||
|
on_update: Rc::new(RefCell::new(Box::new(|_| {}))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for TimeDistanceEditPrivate {
|
||||||
|
const NAME: &'static str = "TimeDistanceEdit";
|
||||||
|
type Type = TimeDistanceEdit;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for TimeDistanceEditPrivate {}
|
||||||
|
impl WidgetImpl for TimeDistanceEditPrivate {}
|
||||||
|
impl BoxImpl for TimeDistanceEditPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct TimeDistanceEdit(ObjectSubclass<TimeDistanceEditPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TimeDistanceEdit {
|
||||||
|
fn default() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.set_orientation(gtk::Orientation::Vertical);
|
||||||
|
s.set_hexpand(true);
|
||||||
|
s.set_css_classes(&["time-distance-edit"]);
|
||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TimeDistanceEdit {
|
||||||
|
pub fn new<OnUpdate>(workout: TimeDistance, on_update: OnUpdate) -> Self
|
||||||
|
where
|
||||||
|
OnUpdate: Fn(TimeDistance) + 'static,
|
||||||
|
{
|
||||||
|
let s = Self::default();
|
||||||
|
|
||||||
|
*s.imp().workout.borrow_mut() = workout.clone();
|
||||||
|
*s.imp().on_update.borrow_mut() = Box::new(on_update);
|
||||||
|
|
||||||
|
let details_row = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
details_row.append(
|
||||||
|
&time_field(
|
||||||
|
Some(TimeFormatter::from(workout.datetime.naive_local().time())),
|
||||||
|
{
|
||||||
|
let s = s.clone();
|
||||||
|
move |t| s.update_time(t)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.widget(),
|
||||||
|
);
|
||||||
|
details_row.append(&s.activity_menu(workout.activity));
|
||||||
|
details_row.append(
|
||||||
|
&distance_field(workout.distance.map(DistanceFormatter::from), {
|
||||||
|
let s = s.clone();
|
||||||
|
move |d| s.update_distance(d)
|
||||||
|
})
|
||||||
|
.widget(),
|
||||||
|
);
|
||||||
|
details_row.append(
|
||||||
|
&duration_field(workout.duration.map(DurationFormatter::from), {
|
||||||
|
let s = s.clone();
|
||||||
|
move |d| s.update_duration(d)
|
||||||
|
})
|
||||||
|
.widget(),
|
||||||
|
);
|
||||||
|
s.append(&details_row);
|
||||||
|
s.append(>k::Entry::new());
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_time(&self, time: Option<TimeFormatter>) {
|
||||||
|
if let Some(time_formatter) = time {
|
||||||
|
let mut workout = self.imp().workout.borrow_mut();
|
||||||
|
let tz = workout.datetime.timezone();
|
||||||
|
let new_time = workout
|
||||||
|
.datetime
|
||||||
|
.date_naive()
|
||||||
|
.and_time(*time_formatter)
|
||||||
|
.and_local_timezone(tz)
|
||||||
|
.unwrap()
|
||||||
|
.fixed_offset();
|
||||||
|
workout.datetime = new_time;
|
||||||
|
(self.imp().on_update.borrow())(workout.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_workout_type(&self, type_: TimeDistanceActivity) {
|
||||||
|
let mut workout = self.imp().workout.borrow_mut();
|
||||||
|
workout.activity = type_;
|
||||||
|
(self.imp().on_update.borrow())(workout.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_distance(&self, distance: Option<DistanceFormatter>) {
|
||||||
|
let mut workout = self.imp().workout.borrow_mut();
|
||||||
|
workout.distance = distance.map(|d| *d);
|
||||||
|
(self.imp().on_update.borrow())(workout.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_duration(&self, duration: Option<DurationFormatter>) {
|
||||||
|
let mut workout = self.imp().workout.borrow_mut();
|
||||||
|
workout.duration = duration.map(|d| *d);
|
||||||
|
(self.imp().on_update.borrow())(workout.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activity_menu(&self, selected: TimeDistanceActivity) -> gtk::DropDown {
|
||||||
|
let options = TIME_DISTANCE_ACTIVITIES
|
||||||
|
.iter()
|
||||||
|
.map(|item| format!("{:?}", item))
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
let options = options.iter().map(|o| o.as_ref()).collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
let selected_idx = TIME_DISTANCE_ACTIVITIES
|
||||||
|
.iter()
|
||||||
|
.position(|&v| v == selected)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let menu = gtk::DropDown::from_strings(&options);
|
||||||
|
menu.set_selected(selected_idx as u32);
|
||||||
|
menu.connect_selected_item_notify({
|
||||||
|
let s = self.clone();
|
||||||
|
move |menu| {
|
||||||
|
let new_item = TIME_DISTANCE_ACTIVITIES[menu.selected() as usize];
|
||||||
|
s.update_workout_type(new_item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
menu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,10 +14,7 @@ General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::{
|
use crate::types::{FormatOption, WeightFormatter};
|
||||||
components::TextEntry,
|
|
||||||
types::{FormatOption, WeightFormatter},
|
|
||||||
};
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
||||||
pub struct WeightLabel {
|
pub struct WeightLabel {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
Copyright 2023 - 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
This file is part of FitnessTrax.
|
This file is part of FitnessTrax.
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
mod about;
|
||||||
mod app;
|
mod app;
|
||||||
mod app_window;
|
mod app_window;
|
||||||
mod components;
|
mod components;
|
||||||
|
@ -25,6 +26,7 @@ mod views;
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use app_window::AppWindow;
|
use app_window::AppWindow;
|
||||||
|
use gio::ActionEntry;
|
||||||
use std::{env, path::PathBuf};
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev";
|
const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev";
|
||||||
|
@ -32,6 +34,29 @@ const APP_ID_PROD: &str = "com.luminescent-dreams.fitnesstrax";
|
||||||
|
|
||||||
const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/fitnesstrax/";
|
const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/fitnesstrax/";
|
||||||
|
|
||||||
|
fn setup_app_about_action(app: &adw::Application) {
|
||||||
|
let action = ActionEntry::builder("about")
|
||||||
|
.activate(|_app: &adw::Application, _, _| {
|
||||||
|
let window = about::AboutWindow::default();
|
||||||
|
window.present();
|
||||||
|
}).build();
|
||||||
|
app.add_action_entries([action]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets up an application-global action, `app.quit`, which will terminate the application.
|
||||||
|
fn setup_app_close_action(app: &adw::Application) {
|
||||||
|
let action = ActionEntry::builder("quit")
|
||||||
|
.activate(|app: &adw::Application, _, _| {
|
||||||
|
// right now, stopping the application is dirt simple. But we could use this
|
||||||
|
// block to add extra code that does additional shutdown steps if we ever want
|
||||||
|
// some states that shouldn't be discarded.
|
||||||
|
app.quit();
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
app.add_action_entries([action]);
|
||||||
|
app.set_accels_for_action("app.quit", &["<Ctrl>Q"]);
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// I still don't fully understand gio resources. resources_register_include! is convenient
|
// I still don't fully understand gio resources. resources_register_include! is convenient
|
||||||
// because I don't have to deal with filesystem locations at runtime. However, I think other
|
// because I don't have to deal with filesystem locations at runtime. However, I think other
|
||||||
|
@ -62,6 +87,12 @@ fn main() {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
adw_app.connect_activate(move |adw_app| {
|
adw_app.connect_activate(move |adw_app| {
|
||||||
|
let icon_theme = gtk::IconTheme::for_display(&gdk::Display::default().unwrap());
|
||||||
|
icon_theme.add_resource_path(&(RESOURCE_BASE_PATH.to_owned() + "/icons/scalable/actions"));
|
||||||
|
|
||||||
|
setup_app_about_action(adw_app);
|
||||||
|
setup_app_close_action(adw_app);
|
||||||
|
|
||||||
AppWindow::new(app_id, RESOURCE_BASE_PATH, adw_app, ft_app.clone());
|
AppWindow::new(app_id, RESOURCE_BASE_PATH, adw_app, ft_app.clone());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ impl Iterator for DayIterator {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum FormatOption {
|
pub enum FormatOption {
|
||||||
Abbreviated,
|
Abbreviated,
|
||||||
|
#[allow(unused)]
|
||||||
Full,
|
Full,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +61,7 @@ pub enum FormatOption {
|
||||||
pub struct TimeFormatter(chrono::NaiveTime);
|
pub struct TimeFormatter(chrono::NaiveTime);
|
||||||
|
|
||||||
impl TimeFormatter {
|
impl TimeFormatter {
|
||||||
|
#[allow(unused)]
|
||||||
pub fn format(&self, option: FormatOption) -> String {
|
pub fn format(&self, option: FormatOption) -> String {
|
||||||
match option {
|
match option {
|
||||||
FormatOption::Abbreviated => self.0.format("%H:%M"),
|
FormatOption::Abbreviated => self.0.format("%H:%M"),
|
||||||
|
@ -68,6 +70,7 @@ impl TimeFormatter {
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
pub fn parse(s: &str) -> Result<TimeFormatter, ParseError> {
|
pub fn parse(s: &str) -> Result<TimeFormatter, ParseError> {
|
||||||
let parts = s
|
let parts = s
|
||||||
.split(':')
|
.split(':')
|
||||||
|
@ -76,12 +79,12 @@ impl TimeFormatter {
|
||||||
match parts.len() {
|
match parts.len() {
|
||||||
0 => Err(ParseError),
|
0 => Err(ParseError),
|
||||||
1 => Err(ParseError),
|
1 => Err(ParseError),
|
||||||
2 => Ok(TimeFormatter(
|
2 => chrono::NaiveTime::from_hms_opt(parts[0], parts[1], 0)
|
||||||
chrono::NaiveTime::from_hms_opt(parts[0], parts[1], 0).unwrap(),
|
.map(|v| TimeFormatter(v))
|
||||||
)),
|
.ok_or(ParseError),
|
||||||
3 => Ok(TimeFormatter(
|
3 => chrono::NaiveTime::from_hms_opt(parts[0], parts[1], parts[2])
|
||||||
chrono::NaiveTime::from_hms_opt(parts[0], parts[1], parts[2]).unwrap(),
|
.map(|v| TimeFormatter(v))
|
||||||
)),
|
.ok_or(ParseError),
|
||||||
_ => Err(ParseError),
|
_ => Err(ParseError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,6 +107,7 @@ impl From<chrono::NaiveTime> for TimeFormatter {
|
||||||
pub struct WeightFormatter(si::Kilogram<f64>);
|
pub struct WeightFormatter(si::Kilogram<f64>);
|
||||||
|
|
||||||
impl WeightFormatter {
|
impl WeightFormatter {
|
||||||
|
#[allow(unused)]
|
||||||
pub fn format(&self, option: FormatOption) -> String {
|
pub fn format(&self, option: FormatOption) -> String {
|
||||||
match option {
|
match option {
|
||||||
FormatOption::Abbreviated => format!("{} kg", self.0.value_unsafe),
|
FormatOption::Abbreviated => format!("{} kg", self.0.value_unsafe),
|
||||||
|
@ -111,6 +115,7 @@ impl WeightFormatter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
pub fn parse(s: &str) -> Result<WeightFormatter, ParseError> {
|
pub fn parse(s: &str) -> Result<WeightFormatter, ParseError> {
|
||||||
s.parse::<f64>()
|
s.parse::<f64>()
|
||||||
.map(|w| WeightFormatter(w * si::KG))
|
.map(|w| WeightFormatter(w * si::KG))
|
||||||
|
@ -149,6 +154,7 @@ impl From<si::Kilogram<f64>> for WeightFormatter {
|
||||||
pub struct DistanceFormatter(si::Meter<f64>);
|
pub struct DistanceFormatter(si::Meter<f64>);
|
||||||
|
|
||||||
impl DistanceFormatter {
|
impl DistanceFormatter {
|
||||||
|
#[allow(unused)]
|
||||||
pub fn format(&self, option: FormatOption) -> String {
|
pub fn format(&self, option: FormatOption) -> String {
|
||||||
match option {
|
match option {
|
||||||
FormatOption::Abbreviated => format!("{} km", self.0.value_unsafe / 1000.),
|
FormatOption::Abbreviated => format!("{} km", self.0.value_unsafe / 1000.),
|
||||||
|
@ -156,6 +162,7 @@ impl DistanceFormatter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
pub fn parse(s: &str) -> Result<DistanceFormatter, ParseError> {
|
pub fn parse(s: &str) -> Result<DistanceFormatter, ParseError> {
|
||||||
let value = s.parse::<f64>().map_err(|_| ParseError)?;
|
let value = s.parse::<f64>().map_err(|_| ParseError)?;
|
||||||
Ok(DistanceFormatter(value * 1000. * si::M))
|
Ok(DistanceFormatter(value * 1000. * si::M))
|
||||||
|
@ -193,6 +200,7 @@ impl From<si::Meter<f64>> for DistanceFormatter {
|
||||||
pub struct DurationFormatter(si::Second<f64>);
|
pub struct DurationFormatter(si::Second<f64>);
|
||||||
|
|
||||||
impl DurationFormatter {
|
impl DurationFormatter {
|
||||||
|
#[allow(unused)]
|
||||||
pub fn format(&self, option: FormatOption) -> String {
|
pub fn format(&self, option: FormatOption) -> String {
|
||||||
let (hours, minutes) = self.hours_and_minutes();
|
let (hours, minutes) = self.hours_and_minutes();
|
||||||
let (h, m) = match option {
|
let (h, m) = match option {
|
||||||
|
@ -206,11 +214,13 @@ impl DurationFormatter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
pub fn parse(s: &str) -> Result<DurationFormatter, ParseError> {
|
pub fn parse(s: &str) -> Result<DurationFormatter, ParseError> {
|
||||||
let value = s.parse::<f64>().map_err(|_| ParseError)?;
|
let value = s.parse::<f64>().map_err(|_| ParseError)?;
|
||||||
Ok(DurationFormatter(value * 60. * si::S))
|
Ok(DurationFormatter(value * 60. * si::S))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
fn hours_and_minutes(&self) -> (i64, i64) {
|
fn hours_and_minutes(&self) -> (i64, i64) {
|
||||||
let minutes: i64 = (self.0.value_unsafe / 60.).round() as i64;
|
let minutes: i64 = (self.0.value_unsafe / 60.).round() as i64;
|
||||||
let hours: i64 = minutes / 60;
|
let hours: i64 = minutes / 60;
|
||||||
|
@ -246,14 +256,6 @@ impl From<si::Second<f64>> for DurationFormatter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
fn take_digits(s: String) -> String {
|
|
||||||
s.chars()
|
|
||||||
.take_while(|t| t.is_ascii_digit())
|
|
||||||
.collect::<String>()
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -14,44 +14,64 @@ General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::{app::App, types::WeightFormatter};
|
use crate::app::{ReadError, RecordProvider};
|
||||||
|
use dimensioned::si;
|
||||||
use emseries::{Record, RecordId, Recordable};
|
use emseries::{Record, RecordId, Recordable};
|
||||||
use ft_core::TraxRecord;
|
use ft_core::{TimeDistance, TimeDistanceActivity, TraxRecord};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// These are actually a used imports. Clippy isn't detecting their use, probably because of complexity around the async trait macros.
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use crate::app::WriteError;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum RecordState<T: Clone + Recordable> {
|
enum RecordState<T: Clone + Recordable> {
|
||||||
Original(Record<T>),
|
Original(Record<T>),
|
||||||
New(T),
|
New(Record<T>),
|
||||||
Updated(Record<T>),
|
Updated(Record<T>),
|
||||||
#[allow(unused)]
|
|
||||||
Deleted(Record<T>),
|
Deleted(Record<T>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Clone + emseries::Recordable> RecordState<T> {
|
impl<T: Clone + emseries::Recordable> RecordState<T> {
|
||||||
#[allow(unused)]
|
fn exists(&self) -> bool {
|
||||||
fn id(&self) -> Option<&RecordId> {
|
|
||||||
match self {
|
match self {
|
||||||
RecordState::Original(ref r) => Some(&r.id),
|
RecordState::Original(_) => true,
|
||||||
RecordState::New(ref r) => None,
|
RecordState::New(_) => true,
|
||||||
RecordState::Updated(ref r) => Some(&r.id),
|
RecordState::Updated(_) => true,
|
||||||
RecordState::Deleted(ref r) => Some(&r.id),
|
RecordState::Deleted(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_value(self, value: T) -> RecordState<T> {
|
#[allow(unused)]
|
||||||
|
fn data(&self) -> Option<&Record<T>> {
|
||||||
match self {
|
match self {
|
||||||
RecordState::Original(r) => RecordState::Updated(Record { data: value, ..r }),
|
RecordState::Original(ref r) => Some(r),
|
||||||
RecordState::New(_) => RecordState::New(value),
|
RecordState::New(ref r) => None,
|
||||||
RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }),
|
RecordState::Updated(ref r) => Some(r),
|
||||||
RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }),
|
RecordState::Deleted(ref r) => Some(r),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_value(&mut self, value: T) {
|
||||||
|
*self = match self {
|
||||||
|
RecordState::Original(r) => RecordState::Updated(Record { data: value, ..*r }),
|
||||||
|
RecordState::New(r) => RecordState::New(Record { data: value, ..*r }),
|
||||||
|
RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..*r }),
|
||||||
|
RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..*r }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_value(mut self, value: T) -> RecordState<T> {
|
||||||
|
self.set_value(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
fn with_delete(self) -> Option<RecordState<T>> {
|
fn with_delete(self) -> Option<RecordState<T>> {
|
||||||
match self {
|
match self {
|
||||||
|
@ -68,19 +88,27 @@ impl<T: Clone + emseries::Recordable> Deref for RecordState<T> {
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
match self {
|
match self {
|
||||||
RecordState::Original(ref r) => &r.data,
|
RecordState::Original(ref r) => &r.data,
|
||||||
RecordState::New(ref r) => r,
|
RecordState::New(ref r) => &r.data,
|
||||||
RecordState::Updated(ref r) => &r.data,
|
RecordState::Updated(ref r) => &r.data,
|
||||||
RecordState::Deleted(ref r) => &r.data,
|
RecordState::Deleted(ref r) => &r.data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
impl<T: Clone + emseries::Recordable> std::ops::DerefMut for RecordState<T> {
|
||||||
struct DayDetailViewModelInner {}
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
match self {
|
||||||
|
RecordState::Original(ref mut r) => &mut r.data,
|
||||||
|
RecordState::New(ref mut r) => &mut r.data,
|
||||||
|
RecordState::Updated(ref mut r) => &mut r.data,
|
||||||
|
RecordState::Deleted(ref mut r) => &mut r.data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone)]
|
||||||
pub struct DayDetailViewModel {
|
pub struct DayDetailViewModel {
|
||||||
app: Option<App>,
|
provider: Arc<dyn RecordProvider>,
|
||||||
pub date: chrono::NaiveDate,
|
pub date: chrono::NaiveDate,
|
||||||
weight: Arc<RwLock<Option<RecordState<ft_core::Weight>>>>,
|
weight: Arc<RwLock<Option<RecordState<ft_core::Weight>>>>,
|
||||||
steps: Arc<RwLock<Option<RecordState<ft_core::Steps>>>>,
|
steps: Arc<RwLock<Option<RecordState<ft_core::Steps>>>>,
|
||||||
|
@ -88,58 +116,38 @@ pub struct DayDetailViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DayDetailViewModel {
|
impl DayDetailViewModel {
|
||||||
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>, app: App) -> Self {
|
pub async fn new(
|
||||||
let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
|
date: chrono::NaiveDate,
|
||||||
records.into_iter().partition(|r| r.data.is_weight());
|
provider: impl RecordProvider + 'static,
|
||||||
let (step_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
|
) -> Result<Self, ReadError> {
|
||||||
records.into_iter().partition(|r| r.data.is_steps());
|
let s = Self {
|
||||||
Self {
|
provider: Arc::new(provider),
|
||||||
app: Some(app),
|
|
||||||
date,
|
date,
|
||||||
weight: Arc::new(RwLock::new(
|
weight: Arc::new(RwLock::new(None)),
|
||||||
weight_records
|
steps: Arc::new(RwLock::new(None)),
|
||||||
.first()
|
records: Arc::new(RwLock::new(HashMap::new())),
|
||||||
.and_then(|r| match r.data {
|
};
|
||||||
TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())),
|
s.populate_records().await;
|
||||||
_ => None,
|
Ok(s)
|
||||||
})
|
|
||||||
.map(|(id, w)| RecordState::Original(Record { id, data: w })),
|
|
||||||
)),
|
|
||||||
steps: Arc::new(RwLock::new(
|
|
||||||
step_records
|
|
||||||
.first()
|
|
||||||
.and_then(|r| match r.data {
|
|
||||||
TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.map(|(id, w)| RecordState::Original(Record { id, data: w })),
|
|
||||||
)),
|
|
||||||
|
|
||||||
records: Arc::new(RwLock::new(
|
|
||||||
records
|
|
||||||
.into_iter()
|
|
||||||
.map(|r| (r.id.clone(), RecordState::Original(r)))
|
|
||||||
.collect::<HashMap<RecordId, RecordState<TraxRecord>>>(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn weight(&self) -> Option<WeightFormatter> {
|
pub fn weight(&self) -> Option<si::Kilogram<f64>> {
|
||||||
(*self.weight.read().unwrap())
|
(*self.weight.read().unwrap()).as_ref().map(|w| w.weight)
|
||||||
.as_ref()
|
|
||||||
.map(|w| WeightFormatter::from(w.weight))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_weight(&self, new_weight: WeightFormatter) {
|
pub fn set_weight(&self, new_weight: si::Kilogram<f64>) {
|
||||||
let mut record = self.weight.write().unwrap();
|
let mut record = self.weight.write().unwrap();
|
||||||
let new_record = match *record {
|
let new_record = match *record {
|
||||||
Some(ref rstate) => rstate.clone().with_value(ft_core::Weight {
|
Some(ref rstate) => rstate.clone().with_value(ft_core::Weight {
|
||||||
date: self.date,
|
date: self.date,
|
||||||
weight: *new_weight,
|
weight: new_weight,
|
||||||
}),
|
}),
|
||||||
None => RecordState::New(ft_core::Weight {
|
None => RecordState::New(Record {
|
||||||
|
id: RecordId::default(),
|
||||||
|
data: ft_core::Weight {
|
||||||
date: self.date,
|
date: self.date,
|
||||||
weight: *new_weight,
|
weight: new_weight,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
*record = Some(new_record);
|
*record = Some(new_record);
|
||||||
|
@ -156,27 +164,140 @@ impl DayDetailViewModel {
|
||||||
date: self.date,
|
date: self.date,
|
||||||
count: new_count,
|
count: new_count,
|
||||||
}),
|
}),
|
||||||
None => RecordState::New(ft_core::Steps {
|
None => RecordState::New(Record {
|
||||||
|
id: RecordId::default(),
|
||||||
|
data: ft_core::Steps {
|
||||||
date: self.date,
|
date: self.date,
|
||||||
count: new_count,
|
count: new_count,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
*record = Some(new_record);
|
*record = Some(new_record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_time_distance(&self, activity: TimeDistanceActivity) -> Record<TimeDistance> {
|
||||||
|
let now = chrono::Local::now();
|
||||||
|
let base_time = now.time();
|
||||||
|
let tz = now.timezone();
|
||||||
|
let datetime = self
|
||||||
|
.date
|
||||||
|
.clone()
|
||||||
|
.and_time(base_time)
|
||||||
|
.and_local_timezone(tz)
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let id = RecordId::default();
|
||||||
|
let workout = TimeDistance {
|
||||||
|
datetime,
|
||||||
|
activity,
|
||||||
|
distance: None,
|
||||||
|
duration: None,
|
||||||
|
comments: None,
|
||||||
|
};
|
||||||
|
let tr = TraxRecord::from(workout.clone());
|
||||||
|
self.records
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.insert(id, RecordState::New(Record { id, data: tr }));
|
||||||
|
Record { id, data: workout }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_distance_records(&self) -> Vec<Record<TimeDistance>> {
|
||||||
|
self.records
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, record)| record.exists())
|
||||||
|
.filter_map(|(id, record_state)| match **record_state {
|
||||||
|
TraxRecord::TimeDistance(ref workout) => Some(Record {
|
||||||
|
id: *id,
|
||||||
|
data: workout.clone(),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_distance_summary(
|
||||||
|
&self,
|
||||||
|
activity: TimeDistanceActivity,
|
||||||
|
) -> (si::Meter<f64>, si::Second<f64>) {
|
||||||
|
self.time_distance_records()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|rec| rec.data.activity == activity)
|
||||||
|
.fold(
|
||||||
|
(0. * si::M, 0. * si::S),
|
||||||
|
|(distance, duration), workout| match (workout.data.distance, workout.data.duration)
|
||||||
|
{
|
||||||
|
(Some(distance_), Some(duration_)) => {
|
||||||
|
(distance + distance_, duration + duration_)
|
||||||
|
}
|
||||||
|
(Some(distance_), None) => (distance + distance_, duration),
|
||||||
|
(None, Some(duration_)) => (distance, duration + duration_),
|
||||||
|
(None, None) => (distance, duration),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_record(&self, update: Record<TraxRecord>) {
|
||||||
|
let mut records = self.records.write().unwrap();
|
||||||
|
records
|
||||||
|
.entry(update.id)
|
||||||
|
.and_modify(|record| record.set_value(update.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn records(&self) -> Vec<Record<TraxRecord>> {
|
||||||
|
let read_lock = self.records.read().unwrap();
|
||||||
|
read_lock
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(_, record_state)| record_state.data())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<Record<TraxRecord>>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn get_record(&self, id: &RecordId) -> Option<Record<TraxRecord>> {
|
||||||
|
let record_set = self.records.read().unwrap();
|
||||||
|
record_set.get(id).map(|record| Record {
|
||||||
|
id: *id,
|
||||||
|
data: (**record).clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_record(&self, id: RecordId) {
|
||||||
|
let mut record_set = self.records.write().unwrap();
|
||||||
|
let updated_record = match record_set.remove(&id) {
|
||||||
|
Some(RecordState::Original(r)) => Some(RecordState::Deleted(r)),
|
||||||
|
Some(RecordState::New(_)) => None,
|
||||||
|
Some(RecordState::Updated(r)) => Some(RecordState::Deleted(r)),
|
||||||
|
Some(RecordState::Deleted(r)) => Some(RecordState::Deleted(r)),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
if let Some(updated_record) = updated_record {
|
||||||
|
record_set.insert(id, updated_record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn save(&self) {
|
pub fn save(&self) {
|
||||||
glib::spawn_future({
|
|
||||||
let s = self.clone();
|
let s = self.clone();
|
||||||
async move {
|
|
||||||
if let Some(app) = s.app {
|
glib::spawn_future(async move { s.async_save().await });
|
||||||
let weight_record = s.weight.read().unwrap().clone();
|
}
|
||||||
|
|
||||||
|
pub async fn async_save(&self) {
|
||||||
|
let weight_record = self.weight.read().unwrap().clone();
|
||||||
match weight_record {
|
match weight_record {
|
||||||
Some(RecordState::New(weight)) => {
|
Some(RecordState::New(data)) => {
|
||||||
let _ = app.put_record(TraxRecord::Weight(weight)).await;
|
let _ = self
|
||||||
|
.provider
|
||||||
|
.put_record(TraxRecord::Weight(data.data))
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
Some(RecordState::Original(_)) => {}
|
Some(RecordState::Original(_)) => {}
|
||||||
Some(RecordState::Updated(weight)) => {
|
Some(RecordState::Updated(weight)) => {
|
||||||
let _ = app
|
let _ = self
|
||||||
|
.provider
|
||||||
.update_record(Record {
|
.update_record(Record {
|
||||||
id: weight.id,
|
id: weight.id,
|
||||||
data: TraxRecord::Weight(weight.data),
|
data: TraxRecord::Weight(weight.data),
|
||||||
|
@ -187,14 +308,15 @@ impl DayDetailViewModel {
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let steps_record = s.steps.read().unwrap().clone();
|
let steps_record = self.steps.read().unwrap().clone();
|
||||||
match steps_record {
|
match steps_record {
|
||||||
Some(RecordState::New(steps)) => {
|
Some(RecordState::New(data)) => {
|
||||||
let _ = app.put_record(TraxRecord::Steps(steps)).await;
|
let _ = self.provider.put_record(TraxRecord::Steps(data.data)).await;
|
||||||
}
|
}
|
||||||
Some(RecordState::Original(_)) => {}
|
Some(RecordState::Original(_)) => {}
|
||||||
Some(RecordState::Updated(steps)) => {
|
Some(RecordState::Updated(steps)) => {
|
||||||
let _ = app
|
let _ = self
|
||||||
|
.provider
|
||||||
.update_record(Record {
|
.update_record(Record {
|
||||||
id: steps.id,
|
id: steps.id,
|
||||||
data: TraxRecord::Steps(steps.data),
|
data: TraxRecord::Steps(steps.data),
|
||||||
|
@ -205,7 +327,7 @@ impl DayDetailViewModel {
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let records = s
|
let records = self
|
||||||
.records
|
.records
|
||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -214,23 +336,322 @@ impl DayDetailViewModel {
|
||||||
.collect::<Vec<RecordState<TraxRecord>>>();
|
.collect::<Vec<RecordState<TraxRecord>>>();
|
||||||
|
|
||||||
for record in records {
|
for record in records {
|
||||||
|
println!("saving record: {:?}", record);
|
||||||
match record {
|
match record {
|
||||||
RecordState::New(data) => {
|
RecordState::New(data) => {
|
||||||
let _ = app.put_record(data).await;
|
let _ = self.provider.put_record(data.data).await;
|
||||||
}
|
}
|
||||||
RecordState::Original(_) => {}
|
RecordState::Original(_) => {}
|
||||||
RecordState::Updated(r) => {
|
RecordState::Updated(r) => {
|
||||||
let _ = app.update_record(r.clone()).await;
|
let _ = self.provider.update_record(r.clone()).await;
|
||||||
}
|
}
|
||||||
RecordState::Deleted(_) => unimplemented!(),
|
RecordState::Deleted(r) => {
|
||||||
|
let _ = self.provider.delete_record(r.id).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
self.populate_records().await;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn revert(&self) {
|
pub async fn revert(&self) {
|
||||||
unimplemented!();
|
self.populate_records().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn populate_records(&self) {
|
||||||
|
let records = self.provider.records(self.date, self.date).await.unwrap();
|
||||||
|
|
||||||
|
let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
|
||||||
|
records.into_iter().partition(|r| r.data.is_weight());
|
||||||
|
let (step_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
|
||||||
|
records.into_iter().partition(|r| r.data.is_steps());
|
||||||
|
|
||||||
|
*self.weight.write().unwrap() = weight_records
|
||||||
|
.first()
|
||||||
|
.and_then(|r| match r.data {
|
||||||
|
TraxRecord::Weight(ref w) => Some((r.id, w.clone())),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(|(id, w)| RecordState::Original(Record { id, data: w }));
|
||||||
|
|
||||||
|
*self.steps.write().unwrap() = step_records
|
||||||
|
.first()
|
||||||
|
.and_then(|r| match r.data {
|
||||||
|
TraxRecord::Steps(ref w) => Some((r.id, w.clone())),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(|(id, w)| RecordState::Original(Record { id, data: w }));
|
||||||
|
|
||||||
|
*self.records.write().unwrap() = records
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| (r.id, RecordState::Original(r)))
|
||||||
|
.collect::<HashMap<RecordId, RecordState<TraxRecord>>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::{DateTime, FixedOffset};
|
||||||
|
use dimensioned::si;
|
||||||
|
use emseries::Record;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct MockProvider {
|
||||||
|
records: Arc<RwLock<HashMap<RecordId, Record<TraxRecord>>>>,
|
||||||
|
|
||||||
|
put_records: Arc<RwLock<Vec<Record<TraxRecord>>>>,
|
||||||
|
updated_records: Arc<RwLock<Vec<Record<TraxRecord>>>>,
|
||||||
|
deleted_records: Arc<RwLock<Vec<RecordId>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockProvider {
|
||||||
|
fn new(records: Vec<Record<TraxRecord>>) -> Self {
|
||||||
|
let record_map = records
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| (r.id, r))
|
||||||
|
.collect::<HashMap<RecordId, Record<TraxRecord>>>();
|
||||||
|
Self {
|
||||||
|
records: Arc::new(RwLock::new(record_map)),
|
||||||
|
put_records: Arc::new(RwLock::new(vec![])),
|
||||||
|
updated_records: Arc::new(RwLock::new(vec![])),
|
||||||
|
deleted_records: Arc::new(RwLock::new(vec![])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RecordProvider for MockProvider {
|
||||||
|
async fn records(
|
||||||
|
&self,
|
||||||
|
start: NaiveDate,
|
||||||
|
end: NaiveDate,
|
||||||
|
) -> Result<Vec<Record<TraxRecord>>, ReadError> {
|
||||||
|
let start = emseries::Timestamp::Date(start);
|
||||||
|
let end = emseries::Timestamp::Date(end);
|
||||||
|
Ok(self
|
||||||
|
.records
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|(_, r)| r)
|
||||||
|
.filter(|r| r.timestamp() >= start && r.timestamp() <= end)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<Record<TraxRecord>>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
|
||||||
|
let id = RecordId::default();
|
||||||
|
let record = Record {
|
||||||
|
id: id,
|
||||||
|
data: record,
|
||||||
|
};
|
||||||
|
self.put_records.write().unwrap().push(record.clone());
|
||||||
|
self.records.write().unwrap().insert(id, record);
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> {
|
||||||
|
println!("updated record: {:?}", record);
|
||||||
|
self.updated_records.write().unwrap().push(record.clone());
|
||||||
|
self.records.write().unwrap().insert(record.id, record);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> {
|
||||||
|
self.deleted_records.write().unwrap().push(id);
|
||||||
|
let _ = self.records.write().unwrap().remove(&id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_empty_view_model() -> (DayDetailViewModel, MockProvider) {
|
||||||
|
let provider = MockProvider::new(vec![]);
|
||||||
|
|
||||||
|
let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap();
|
||||||
|
let model = DayDetailViewModel::new(oct_13, provider.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
(model, provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_view_model() -> (DayDetailViewModel, MockProvider) {
|
||||||
|
let oct_12 = chrono::NaiveDate::from_ymd_opt(2023, 10, 12).unwrap();
|
||||||
|
let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap();
|
||||||
|
let oct_13_am: DateTime<FixedOffset> = oct_13
|
||||||
|
.clone()
|
||||||
|
.and_hms_opt(3, 28, 0)
|
||||||
|
.unwrap()
|
||||||
|
.and_utc()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(5 * 3600).unwrap());
|
||||||
|
let provider = MockProvider::new(vec![
|
||||||
|
Record {
|
||||||
|
id: RecordId::default(),
|
||||||
|
data: TraxRecord::Weight(ft_core::Weight {
|
||||||
|
date: oct_12,
|
||||||
|
weight: 93. * si::KG,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Record {
|
||||||
|
id: RecordId::default(),
|
||||||
|
data: TraxRecord::Weight(ft_core::Weight {
|
||||||
|
date: oct_13,
|
||||||
|
weight: 95. * si::KG,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Record {
|
||||||
|
id: RecordId::default(),
|
||||||
|
data: TraxRecord::Steps(ft_core::Steps {
|
||||||
|
date: oct_13,
|
||||||
|
count: 2500,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Record {
|
||||||
|
id: RecordId::default(),
|
||||||
|
data: TraxRecord::TimeDistance(ft_core::TimeDistance {
|
||||||
|
datetime: oct_13_am.clone(),
|
||||||
|
activity: TimeDistanceActivity::Biking,
|
||||||
|
distance: Some(15000. * si::M),
|
||||||
|
duration: Some(3600. * si::S),
|
||||||
|
comments: Some("somecomments present".to_owned()),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
let model = DayDetailViewModel::new(oct_13, provider.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
(model, provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_honors_only_the_first_weight_and_step_record() {
|
||||||
|
let (view_model, _provider) = create_view_model().await;
|
||||||
|
assert_eq!(view_model.weight(), Some(95. * si::KG));
|
||||||
|
assert_eq!(view_model.steps(), Some(2500));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_can_create_a_weight_and_stepcount() {
|
||||||
|
let (view_model, provider) = create_empty_view_model().await;
|
||||||
|
assert_eq!(view_model.weight(), None);
|
||||||
|
assert_eq!(view_model.steps(), None);
|
||||||
|
|
||||||
|
view_model.set_weight(95. * si::KG);
|
||||||
|
view_model.set_steps(250);
|
||||||
|
|
||||||
|
assert_eq!(view_model.weight(), Some(95. * si::KG));
|
||||||
|
assert_eq!(view_model.steps(), Some(250));
|
||||||
|
|
||||||
|
view_model.set_weight(93. * si::KG);
|
||||||
|
view_model.set_steps(255);
|
||||||
|
|
||||||
|
assert_eq!(view_model.weight(), Some(93. * si::KG));
|
||||||
|
assert_eq!(view_model.steps(), Some(255));
|
||||||
|
|
||||||
|
view_model.async_save().await;
|
||||||
|
|
||||||
|
println!("provider: {:?}", provider);
|
||||||
|
assert_eq!(provider.put_records.read().unwrap().len(), 2);
|
||||||
|
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
|
||||||
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_can_construct_new_records() {
|
||||||
|
let (view_model, provider) = create_empty_view_model().await;
|
||||||
|
assert_eq!(
|
||||||
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
|
(0. * si::M, 0. * si::S)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut record = view_model.new_time_distance(TimeDistanceActivity::Biking);
|
||||||
|
record.data.duration = Some(60. * si::S);
|
||||||
|
view_model.async_save().await;
|
||||||
|
|
||||||
|
assert_eq!(provider.put_records.read().unwrap().len(), 1);
|
||||||
|
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
|
||||||
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_can_update_a_new_record_before_saving() {
|
||||||
|
let (view_model, provider) = create_empty_view_model().await;
|
||||||
|
assert_eq!(
|
||||||
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
|
(0. * si::M, 0. * si::S)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut record = view_model.new_time_distance(TimeDistanceActivity::Biking);
|
||||||
|
record.data.duration = Some(60. * si::S);
|
||||||
|
let record = record.map(TraxRecord::TimeDistance);
|
||||||
|
view_model.update_record(record.clone());
|
||||||
|
assert_eq!(view_model.get_record(&record.id), Some(record));
|
||||||
|
assert_eq!(
|
||||||
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
|
(0. * si::M, 60. * si::S)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
view_model.time_distance_summary(TimeDistanceActivity::Running),
|
||||||
|
(0. * si::M, 0. * si::S)
|
||||||
|
);
|
||||||
|
view_model.async_save().await;
|
||||||
|
|
||||||
|
assert_eq!(provider.put_records.read().unwrap().len(), 1);
|
||||||
|
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
|
||||||
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_can_update_an_existing_record() {
|
||||||
|
let (view_model, provider) = create_view_model().await;
|
||||||
|
let mut workout = view_model.time_distance_records().first().cloned().unwrap();
|
||||||
|
|
||||||
|
workout.data.duration = Some(1800. * si::S);
|
||||||
|
view_model.update_record(workout.map(TraxRecord::TimeDistance));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
|
(15000. * si::M, 1800. * si::S)
|
||||||
|
);
|
||||||
|
|
||||||
|
view_model.async_save().await;
|
||||||
|
|
||||||
|
assert_eq!(provider.put_records.read().unwrap().len(), 0);
|
||||||
|
assert_eq!(provider.updated_records.read().unwrap().len(), 1);
|
||||||
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_can_remove_a_new_record() {
|
||||||
|
let (view_model, provider) = create_empty_view_model().await;
|
||||||
|
assert_eq!(
|
||||||
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
|
(0. * si::M, 0. * si::S)
|
||||||
|
);
|
||||||
|
|
||||||
|
let record = view_model.new_time_distance(TimeDistanceActivity::Biking);
|
||||||
|
view_model.remove_record(record.id);
|
||||||
|
view_model.save();
|
||||||
|
|
||||||
|
assert_eq!(provider.put_records.read().unwrap().len(), 0);
|
||||||
|
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
|
||||||
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_can_delete_an_existing_record() {
|
||||||
|
let (view_model, provider) = create_view_model().await;
|
||||||
|
let workout = view_model.time_distance_records().first().cloned().unwrap();
|
||||||
|
|
||||||
|
view_model.remove_record(workout.id);
|
||||||
|
assert_eq!(
|
||||||
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
|
(0. * si::M, 0. * si::S)
|
||||||
|
);
|
||||||
|
view_model.async_save().await;
|
||||||
|
|
||||||
|
assert_eq!(provider.put_records.read().unwrap().len(), 0);
|
||||||
|
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
|
||||||
|
assert_eq!(provider.deleted_records.read().unwrap().len(), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ use std::cell::RefCell;
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DayDetailViewPrivate {
|
pub struct DayDetailViewPrivate {
|
||||||
container: Singleton,
|
container: Singleton,
|
||||||
view_model: RefCell<DayDetailViewModel>,
|
view_model: RefCell<Option<DayDetailViewModel>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
|
@ -47,7 +47,7 @@ glib::wrapper! {
|
||||||
impl DayDetailView {
|
impl DayDetailView {
|
||||||
pub fn new(view_model: DayDetailViewModel) -> Self {
|
pub fn new(view_model: DayDetailViewModel) -> Self {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
*s.imp().view_model.borrow_mut() = view_model;
|
*s.imp().view_model.borrow_mut() = Some(view_model);
|
||||||
|
|
||||||
s.append(&s.imp().container);
|
s.append(&s.imp().container);
|
||||||
|
|
||||||
|
@ -57,18 +57,26 @@ impl DayDetailView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self) {
|
fn view(&self) {
|
||||||
self.imp()
|
let view_model = self.imp().view_model.borrow();
|
||||||
.container
|
let view_model = view_model
|
||||||
.swap(&DayDetail::new(self.imp().view_model.borrow().clone(), {
|
.as_ref()
|
||||||
|
.expect("DayDetailView has not been initialized with a view_model")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
self.imp().container.swap(&DayDetail::new(view_model, {
|
||||||
let s = self.clone();
|
let s = self.clone();
|
||||||
move || s.edit()
|
move || s.edit()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn edit(&self) {
|
fn edit(&self) {
|
||||||
self.imp()
|
let view_model = self.imp().view_model.borrow();
|
||||||
.container
|
let view_model = view_model
|
||||||
.swap(&DayEdit::new(self.imp().view_model.borrow().clone(), {
|
.as_ref()
|
||||||
|
.expect("DayDetailView has not been initialized with a view_model")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
self.imp().container.swap(&DayEdit::new(view_model, {
|
||||||
let s = self.clone();
|
let s = self.clone();
|
||||||
move || s.view()
|
move || s.view()
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -15,22 +15,22 @@ You should have received a copy of the GNU General Public License along with Fit
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::App, components::DaySummary, types::DayInterval, view_models::DayDetailViewModel,
|
app::App,
|
||||||
|
components::{DateRangePicker, DaySummary},
|
||||||
|
types::DayInterval,
|
||||||
|
view_models::DayDetailViewModel,
|
||||||
};
|
};
|
||||||
use chrono::NaiveDate;
|
|
||||||
use emseries::Record;
|
|
||||||
use ft_core::TraxRecord;
|
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
/// The historical view will show a window into the main database. It will show some version of
|
/// The historical view will show a window into the main database. It will show some version of
|
||||||
/// daily summaries, daily details, and will provide all functions the user may need for editing
|
/// daily summaries, daily details, and will provide all functions the user may need for editing
|
||||||
/// records.
|
/// records.
|
||||||
pub struct HistoricalViewPrivate {
|
pub struct HistoricalViewPrivate {
|
||||||
app: Rc<RefCell<Option<App>>>,
|
app: Rc<RefCell<Option<App>>>,
|
||||||
time_window: Rc<RefCell<DayInterval>>,
|
|
||||||
list_view: gtk::ListView,
|
list_view: gtk::ListView,
|
||||||
|
date_range_picker: DateRangePicker,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
|
@ -48,23 +48,27 @@ impl ObjectSubclass for HistoricalViewPrivate {
|
||||||
.set_child(Some(&DaySummary::new()));
|
.set_child(Some(&DaySummary::new()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let date_range_picker = DateRangePicker::default();
|
||||||
|
|
||||||
let s = Self {
|
let s = Self {
|
||||||
app: Rc::new(RefCell::new(None)),
|
app: Rc::new(RefCell::new(None)),
|
||||||
time_window: Rc::new(RefCell::new(DayInterval::default())),
|
|
||||||
list_view: gtk::ListView::builder()
|
list_view: gtk::ListView::builder()
|
||||||
.factory(&factory)
|
.factory(&factory)
|
||||||
.single_click_activate(true)
|
.single_click_activate(true)
|
||||||
|
.show_separators(true)
|
||||||
.build(),
|
.build(),
|
||||||
|
date_range_picker,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory.connect_bind({
|
factory.connect_bind({
|
||||||
let app = s.app.clone();
|
let app = s.app.clone();
|
||||||
move |_, list_item| {
|
move |_, list_item| {
|
||||||
let records = list_item
|
let date = list_item
|
||||||
.downcast_ref::<gtk::ListItem>()
|
.downcast_ref::<gtk::ListItem>()
|
||||||
.expect("should be a ListItem")
|
.expect("should be a ListItem")
|
||||||
.item()
|
.item()
|
||||||
.and_downcast::<DayRecords>()
|
.and_downcast::<Date>()
|
||||||
.expect("should be a DaySummary");
|
.expect("should be a Date");
|
||||||
|
|
||||||
let summary = list_item
|
let summary = list_item
|
||||||
.downcast_ref::<gtk::ListItem>()
|
.downcast_ref::<gtk::ListItem>()
|
||||||
|
@ -74,11 +78,12 @@ impl ObjectSubclass for HistoricalViewPrivate {
|
||||||
.expect("should be a DaySummary");
|
.expect("should be a DaySummary");
|
||||||
|
|
||||||
if let Some(app) = app.borrow().clone() {
|
if let Some(app) = app.borrow().clone() {
|
||||||
summary.set_data(DayDetailViewModel::new(
|
glib::spawn_future_local(async move {
|
||||||
records.date(),
|
let view_model = DayDetailViewModel::new(date.date(), app.clone())
|
||||||
records.records(),
|
.await
|
||||||
app.clone(),
|
.unwrap();
|
||||||
));
|
summary.set_data(view_model);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -96,13 +101,9 @@ glib::wrapper! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistoricalView {
|
impl HistoricalView {
|
||||||
pub fn new<SelectFn>(
|
pub fn new<SelectFn>(app: App, interval: DayInterval, on_select_day: Rc<SelectFn>) -> Self
|
||||||
app: App,
|
|
||||||
records: Vec<Record<TraxRecord>>,
|
|
||||||
on_select_day: Rc<SelectFn>,
|
|
||||||
) -> Self
|
|
||||||
where
|
where
|
||||||
SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static,
|
SelectFn: Fn(chrono::NaiveDate) + 'static,
|
||||||
{
|
{
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
s.set_orientation(gtk::Orientation::Vertical);
|
s.set_orientation(gtk::Orientation::Vertical);
|
||||||
|
@ -110,195 +111,72 @@ impl HistoricalView {
|
||||||
|
|
||||||
*s.imp().app.borrow_mut() = Some(app);
|
*s.imp().app.borrow_mut() = Some(app);
|
||||||
|
|
||||||
let grouped_records =
|
s.imp().date_range_picker.connect_on_search({
|
||||||
GroupedRecords::new((*s.imp().time_window.borrow()).clone()).with_data(records);
|
let s = s.clone();
|
||||||
|
move |interval| s.set_interval(interval)
|
||||||
let mut model = gio::ListStore::new::<DayRecords>();
|
});
|
||||||
model.extend(grouped_records.items());
|
s.set_interval(interval);
|
||||||
s.imp()
|
|
||||||
.list_view
|
|
||||||
.set_model(Some(>k::NoSelection::new(Some(model))));
|
|
||||||
|
|
||||||
s.imp().list_view.connect_activate({
|
s.imp().list_view.connect_activate({
|
||||||
let on_select_day = on_select_day.clone();
|
let on_select_day = on_select_day.clone();
|
||||||
move |s, idx| {
|
move |s, idx| {
|
||||||
// This gets triggered whenever the user clicks on an item on the list. What we
|
// This gets triggered whenever the user clicks on an item on the list.
|
||||||
// actually want to do here is to open a modal dialog that shows all of the details of
|
|
||||||
// the day and which allows the user to edit items within that dialog.
|
|
||||||
let item = s.model().unwrap().item(idx).unwrap();
|
let item = s.model().unwrap().item(idx).unwrap();
|
||||||
let records = item.downcast_ref::<DayRecords>().unwrap();
|
let date = item.downcast_ref::<Date>().unwrap();
|
||||||
on_select_day(records.date(), records.records());
|
on_select_day(date.date());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
s.append(&s.imp().list_view);
|
let scroller = gtk::ScrolledWindow::builder()
|
||||||
|
.child(&s.imp().list_view)
|
||||||
|
.hexpand(true)
|
||||||
|
.vexpand(true)
|
||||||
|
.hscrollbar_policy(gtk::PolicyType::Never)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
s.append(&s.imp().date_range_picker);
|
||||||
|
s.append(&scroller);
|
||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_records(&self, records: Vec<Record<TraxRecord>>) {
|
pub fn set_interval(&self, interval: DayInterval) {
|
||||||
println!("set_records: {:?}", records);
|
let mut model = gio::ListStore::new::<Date>();
|
||||||
let grouped_records =
|
let mut days = interval.days().map(Date::new).collect::<Vec<Date>>();
|
||||||
GroupedRecords::new((self.imp().time_window.borrow()).clone()).with_data(records);
|
days.reverse();
|
||||||
let mut model = gio::ListStore::new::<DayRecords>();
|
model.extend(days.into_iter());
|
||||||
model.extend(grouped_records.items());
|
|
||||||
self.imp()
|
self.imp()
|
||||||
.list_view
|
.list_view
|
||||||
.set_model(Some(>k::NoSelection::new(Some(model))));
|
.set_model(Some(>k::NoSelection::new(Some(model))));
|
||||||
}
|
self.imp().date_range_picker.set_interval(interval.start, interval.end);
|
||||||
|
|
||||||
pub fn time_window(&self) -> DayInterval {
|
|
||||||
self.imp().time_window.borrow().clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DayRecordsPrivate {
|
pub struct DatePrivate {
|
||||||
date: RefCell<chrono::NaiveDate>,
|
date: RefCell<chrono::NaiveDate>,
|
||||||
records: RefCell<Vec<Record<TraxRecord>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
impl ObjectSubclass for DayRecordsPrivate {
|
impl ObjectSubclass for DatePrivate {
|
||||||
const NAME: &'static str = "DayRecords";
|
const NAME: &'static str = "Date";
|
||||||
type Type = DayRecords;
|
type Type = Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectImpl for DayRecordsPrivate {}
|
impl ObjectImpl for DatePrivate {}
|
||||||
|
|
||||||
glib::wrapper! {
|
glib::wrapper! {
|
||||||
pub struct DayRecords(ObjectSubclass<DayRecordsPrivate>);
|
pub struct Date(ObjectSubclass<DatePrivate>);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DayRecords {
|
impl Date {
|
||||||
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>) -> Self {
|
pub fn new(date: chrono::NaiveDate) -> Self {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
*s.imp().date.borrow_mut() = date;
|
*s.imp().date.borrow_mut() = date;
|
||||||
*s.imp().records.borrow_mut() = records;
|
|
||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn date(&self) -> chrono::NaiveDate {
|
pub fn date(&self) -> chrono::NaiveDate {
|
||||||
*self.imp().date.borrow()
|
*self.imp().date.borrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn records(&self) -> Vec<Record<TraxRecord>> {
|
|
||||||
self.imp().records.borrow().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_record(&self, record: Record<TraxRecord>) {
|
|
||||||
self.imp().records.borrow_mut().push(record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This isn't feeling quite right. DayRecords is a glib object, but I'm not sure that I want to
|
|
||||||
// really be passing that around. It seems not generic enough. I feel like this whole grouped
|
|
||||||
// records thing can be made more generic.
|
|
||||||
struct GroupedRecords {
|
|
||||||
interval: DayInterval,
|
|
||||||
data: HashMap<NaiveDate, DayRecords>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GroupedRecords {
|
|
||||||
fn new(interval: DayInterval) -> Self {
|
|
||||||
let mut s = Self {
|
|
||||||
interval: interval.clone(),
|
|
||||||
data: HashMap::new(),
|
|
||||||
};
|
|
||||||
interval.days().for_each(|date| {
|
|
||||||
let _ = s.data.insert(date, DayRecords::new(date, vec![]));
|
|
||||||
});
|
|
||||||
s
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_data(mut self, records: Vec<Record<TraxRecord>>) -> Self {
|
|
||||||
records.into_iter().for_each(|record| {
|
|
||||||
self.data
|
|
||||||
.entry(record.date())
|
|
||||||
.and_modify(|entry: &mut DayRecords| (*entry).add_record(record.clone()))
|
|
||||||
.or_insert(DayRecords::new(record.date(), vec![record]));
|
|
||||||
});
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn items(&self) -> impl Iterator<Item = DayRecords> + '_ {
|
|
||||||
self.interval.days().map(|date| {
|
|
||||||
self.data
|
|
||||||
.get(&date)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or(DayRecords::new(date, vec![]))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::GroupedRecords;
|
|
||||||
use crate::types::DayInterval;
|
|
||||||
use chrono::{FixedOffset, NaiveDate, TimeZone};
|
|
||||||
use dimensioned::si::{KG, M, S};
|
|
||||||
use emseries::{Record, RecordId};
|
|
||||||
use ft_core::{Steps, TimeDistance, TraxRecord, Weight};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn groups_records() {
|
|
||||||
let records = vec![
|
|
||||||
Record {
|
|
||||||
id: RecordId::default(),
|
|
||||||
data: TraxRecord::Steps(Steps {
|
|
||||||
date: NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(),
|
|
||||||
count: 1500,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Record {
|
|
||||||
id: RecordId::default(),
|
|
||||||
data: TraxRecord::Weight(Weight {
|
|
||||||
date: NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(),
|
|
||||||
weight: 85. * KG,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Record {
|
|
||||||
id: RecordId::default(),
|
|
||||||
data: TraxRecord::Weight(Weight {
|
|
||||||
date: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(),
|
|
||||||
weight: 86. * KG,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Record {
|
|
||||||
id: RecordId::default(),
|
|
||||||
data: TraxRecord::BikeRide(TimeDistance {
|
|
||||||
datetime: FixedOffset::west_opt(10 * 60 * 60)
|
|
||||||
.unwrap()
|
|
||||||
.with_ymd_and_hms(2019, 6, 15, 12, 0, 0)
|
|
||||||
.unwrap(),
|
|
||||||
distance: Some(1000. * M),
|
|
||||||
duration: Some(150. * S),
|
|
||||||
comments: Some("Test Comments".to_owned()),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Record {
|
|
||||||
id: RecordId::default(),
|
|
||||||
data: TraxRecord::BikeRide(TimeDistance {
|
|
||||||
datetime: FixedOffset::west_opt(10 * 60 * 60)
|
|
||||||
.unwrap()
|
|
||||||
.with_ymd_and_hms(2019, 6, 15, 23, 0, 0)
|
|
||||||
.unwrap(),
|
|
||||||
distance: Some(1000. * M),
|
|
||||||
duration: Some(150. * S),
|
|
||||||
comments: Some("Test Comments".to_owned()),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let groups = GroupedRecords::new(DayInterval {
|
|
||||||
start: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(),
|
|
||||||
end: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(),
|
|
||||||
})
|
|
||||||
.with_data(records)
|
|
||||||
.data;
|
|
||||||
assert_eq!(groups.len(), 3);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ chrono-tz = { version = "0.8" }
|
||||||
dimensioned = { version = "0.8", features = [ "serde" ] }
|
dimensioned = { version = "0.8", features = [ "serde" ] }
|
||||||
emseries = { path = "../../emseries" }
|
emseries = { path = "../../emseries" }
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
|
serde_json = { version = "1" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "*"
|
tempfile = "*"
|
||||||
|
|
|
@ -0,0 +1,328 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
|
This file is part of FitnessTrax.
|
||||||
|
|
||||||
|
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||||
|
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use chrono::SecondsFormat;
|
||||||
|
use chrono_tz::Etc::UTC;
|
||||||
|
use dimensioned::si;
|
||||||
|
use emseries::{Record, RecordId, Series, Timestamp};
|
||||||
|
use ft_core::{self, DurationWorkout, DurationWorkoutActivity, SetRepActivity, TraxRecord};
|
||||||
|
use serde::{
|
||||||
|
de::{self, Visitor},
|
||||||
|
Deserialize, Deserializer, Serialize, Serializer,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
fmt,
|
||||||
|
fs::File,
|
||||||
|
io::{BufRead, BufReader, Read},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// This is a wrapper around date time objects, using timezones from the chroon-tz database and
|
||||||
|
/// providing string representation and parsing of the form "<RFC3339> <Timezone Name>", i.e.,
|
||||||
|
/// "2019-05-15T14:30:00Z US/Central". The to_string method, and serde serialization will
|
||||||
|
/// produce a string of this format. The parser will accept an RFC3339-only string of the forms
|
||||||
|
/// "2019-05-15T14:30:00Z", "2019-05-15T14:30:00+00:00", and also an "RFC3339 Timezone Name"
|
||||||
|
/// string.
|
||||||
|
///
|
||||||
|
/// The function here is to generate as close to unambiguous time/date strings, (for earth's
|
||||||
|
/// gravitational frame of reference), as possible. Clumping together the time, offset from UTC,
|
||||||
|
/// and the named time zone allows future parsers to know the exact interpretation of the time in
|
||||||
|
/// the frame of reference of the original recording.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct DateTimeTz(pub chrono::DateTime<chrono_tz::Tz>);
|
||||||
|
|
||||||
|
impl fmt::Display for DateTimeTz {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
|
if self.0.timezone() == UTC {
|
||||||
|
write!(f, "{}", self.0.to_rfc3339_opts(SecondsFormat::Secs, true))
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} {}",
|
||||||
|
self.0
|
||||||
|
.with_timezone(&chrono_tz::Etc::UTC)
|
||||||
|
.to_rfc3339_opts(SecondsFormat::Secs, true,),
|
||||||
|
self.0.timezone().name()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DateTimeTz {
|
||||||
|
pub fn map<F>(&self, f: F) -> DateTimeTz
|
||||||
|
where
|
||||||
|
F: FnOnce(chrono::DateTime<chrono_tz::Tz>) -> chrono::DateTime<chrono_tz::Tz>,
|
||||||
|
{
|
||||||
|
DateTimeTz(f(self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for DateTimeTz {
|
||||||
|
type Err = chrono::ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let v: Vec<&str> = s.split_terminator(' ').collect();
|
||||||
|
if v.len() == 2 {
|
||||||
|
let tz = v[1].parse::<chrono_tz::Tz>().unwrap();
|
||||||
|
chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&tz)))
|
||||||
|
} else {
|
||||||
|
chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&UTC)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<chrono::DateTime<chrono_tz::Tz>> for DateTimeTz {
|
||||||
|
fn from(dt: chrono::DateTime<chrono_tz::Tz>) -> DateTimeTz {
|
||||||
|
DateTimeTz(dt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DateTimeTzVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for DateTimeTzVisitor {
|
||||||
|
type Value = DateTimeTz;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a string date time representation that can be parsed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
|
||||||
|
DateTimeTz::from_str(s).or(Err(E::custom(
|
||||||
|
"string is not a parsable datetime representation".to_owned(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for DateTimeTz {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for DateTimeTz {
|
||||||
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
deserializer.deserialize_str(DateTimeTzVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
struct Steps {
|
||||||
|
date: DateTimeTz,
|
||||||
|
steps: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Steps> for ft_core::Steps {
|
||||||
|
fn from(s: Steps) -> Self {
|
||||||
|
Self {
|
||||||
|
date: s.date.0.naive_utc().date(),
|
||||||
|
count: s.steps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
struct Weight {
|
||||||
|
date: DateTimeTz,
|
||||||
|
weight: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Weight> for ft_core::Weight {
|
||||||
|
fn from(w: Weight) -> Self {
|
||||||
|
Self {
|
||||||
|
date: w.date.0.naive_utc().date(),
|
||||||
|
weight: w.weight * si::KG,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
enum TDActivity {
|
||||||
|
Cycling,
|
||||||
|
Rowing,
|
||||||
|
Running,
|
||||||
|
Swimming,
|
||||||
|
Walking,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TDActivity> for ft_core::TimeDistanceActivity {
|
||||||
|
fn from(activity: TDActivity) -> Self {
|
||||||
|
match activity {
|
||||||
|
TDActivity::Cycling => ft_core::TimeDistanceActivity::Biking,
|
||||||
|
TDActivity::Rowing => ft_core::TimeDistanceActivity::Rowing,
|
||||||
|
TDActivity::Running => ft_core::TimeDistanceActivity::Running,
|
||||||
|
TDActivity::Swimming => ft_core::TimeDistanceActivity::Swimming,
|
||||||
|
TDActivity::Walking => ft_core::TimeDistanceActivity::Walking,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
struct TimeDistance {
|
||||||
|
date: DateTimeTz,
|
||||||
|
activity: TDActivity,
|
||||||
|
comments: Option<String>,
|
||||||
|
distance: Option<f64>,
|
||||||
|
duration: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TimeDistance> for ft_core::TimeDistance {
|
||||||
|
fn from(td: TimeDistance) -> Self {
|
||||||
|
Self {
|
||||||
|
datetime: td.date.0.fixed_offset(),
|
||||||
|
activity: td.activity.into(),
|
||||||
|
comments: td.comments,
|
||||||
|
distance: td.distance.map(|d| d * si::M),
|
||||||
|
duration: td.duration.map(|d| d * si::S),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
enum SRActivity {
|
||||||
|
Pushups,
|
||||||
|
Situps,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SRActivity> for SetRepActivity {
|
||||||
|
fn from(activity: SRActivity) -> Self {
|
||||||
|
match activity {
|
||||||
|
SRActivity::Pushups => Self::Pushups,
|
||||||
|
SRActivity::Situps => Self::Situps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
struct SetRep {
|
||||||
|
date: DateTimeTz,
|
||||||
|
activity: SRActivity,
|
||||||
|
sets: Vec<u32>,
|
||||||
|
comments: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SetRep> for ft_core::SetRep {
|
||||||
|
fn from(sr: SetRep) -> Self {
|
||||||
|
Self {
|
||||||
|
date: sr.date.0.naive_utc().date(),
|
||||||
|
activity: sr.activity.into(),
|
||||||
|
sets: sr.sets,
|
||||||
|
comments: sr.comments,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
enum RDActivity {
|
||||||
|
MartialArts,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RDActivity> for DurationWorkoutActivity {
|
||||||
|
fn from(_: RDActivity) -> Self {
|
||||||
|
Self::MartialArts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
struct RepDuration {
|
||||||
|
date: DateTimeTz,
|
||||||
|
activity: RDActivity,
|
||||||
|
sets: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RepDuration> for DurationWorkout {
|
||||||
|
fn from(rd: RepDuration) -> Self {
|
||||||
|
Self {
|
||||||
|
datetime: rd.date.0.fixed_offset(),
|
||||||
|
activity: rd.activity.into(),
|
||||||
|
duration: rd.sets.into_iter().map(|d| d * si::S).next(),
|
||||||
|
comments: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
enum LegacyData {
|
||||||
|
RepDuration(RepDuration),
|
||||||
|
SetRep(SetRep),
|
||||||
|
Steps(Steps),
|
||||||
|
TimeDistance(TimeDistance),
|
||||||
|
Weight(Weight),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
struct LegacyRecord {
|
||||||
|
id: RecordId,
|
||||||
|
data: LegacyData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LegacyRecord> for Record<TraxRecord> {
|
||||||
|
fn from(record: LegacyRecord) -> Self {
|
||||||
|
match record.data {
|
||||||
|
LegacyData::RepDuration(rd) => Record {
|
||||||
|
id: record.id,
|
||||||
|
data: TraxRecord::DurationWorkout(rd.into()),
|
||||||
|
},
|
||||||
|
LegacyData::SetRep(sr) => Record {
|
||||||
|
id: record.id,
|
||||||
|
data: TraxRecord::SetRep(sr.into()),
|
||||||
|
},
|
||||||
|
LegacyData::Steps(s) => Record {
|
||||||
|
id: record.id,
|
||||||
|
data: TraxRecord::Steps(s.into()),
|
||||||
|
},
|
||||||
|
LegacyData::TimeDistance(td) => Record {
|
||||||
|
id: record.id,
|
||||||
|
data: TraxRecord::TimeDistance(td.into()),
|
||||||
|
},
|
||||||
|
LegacyData::Weight(weight) => Record {
|
||||||
|
id: record.id,
|
||||||
|
data: TraxRecord::Weight(weight.into()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args = std::env::args();
|
||||||
|
let _ = args.next().unwrap();
|
||||||
|
let input_filename = args.next().unwrap();
|
||||||
|
println!("input filename: {}", input_filename);
|
||||||
|
// let output: Series<ft_core::TraxRecord> = Series::open_file("import.fitnesstrax").unwrap();
|
||||||
|
|
||||||
|
let input_file = File::open(input_filename).unwrap();
|
||||||
|
let mut buf_reader = BufReader::new(input_file);
|
||||||
|
// let mut contents = String::new();
|
||||||
|
// buf_reader.read_(&mut contents).unwrap();
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
let res = buf_reader.read_line(&mut line);
|
||||||
|
match res {
|
||||||
|
Err(err) => {
|
||||||
|
panic!("failed after {} lines: {:?}", count, err);
|
||||||
|
}
|
||||||
|
Ok(0) => std::process::exit(0),
|
||||||
|
Ok(_) => {
|
||||||
|
let record = serde_json::from_str::<LegacyRecord>(&line).unwrap();
|
||||||
|
let record: Record<TraxRecord> = record.into();
|
||||||
|
println!("{}", serde_json::to_string(&record).unwrap());
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,8 @@
|
||||||
mod legacy;
|
mod legacy;
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use types::{RecordType, Steps, TimeDistance, TraxRecord, Weight};
|
pub use types::{
|
||||||
|
DurationWorkout, DurationWorkoutActivity, SetRep, SetRepActivity, Steps, TimeDistance,
|
||||||
|
TimeDistanceActivity, TraxRecord, Weight, DURATION_WORKOUT_ACTIVITIES, SET_REP_ACTIVITIES,
|
||||||
|
TIME_DISTANCE_ACTIVITIES,
|
||||||
|
};
|
||||||
|
|
|
@ -1,19 +1,56 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023-2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
|
This file is part of FitnessTrax.
|
||||||
|
|
||||||
|
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||||
|
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
use chrono::{DateTime, FixedOffset, NaiveDate};
|
use chrono::{DateTime, FixedOffset, NaiveDate};
|
||||||
use dimensioned::si;
|
use dimensioned::si;
|
||||||
use emseries::{Recordable, Timestamp};
|
use emseries::{Recordable, Timestamp};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum SetRepActivity {
|
||||||
|
Pushups,
|
||||||
|
Situps,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const SET_REP_ACTIVITIES: [SetRepActivity; 2] =
|
||||||
|
[SetRepActivity::Pushups, SetRepActivity::Situps];
|
||||||
|
|
||||||
/// SetRep represents workouts like pushups or situps, which involve doing a "set" of a number of
|
/// SetRep represents workouts like pushups or situps, which involve doing a "set" of a number of
|
||||||
/// actions, resting, and then doing another set.
|
/// actions, resting, and then doing another set.
|
||||||
#[allow(dead_code)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct SetRep {
|
pub struct SetRep {
|
||||||
/// I assume that a set/rep workout is only done once in a day.
|
/// I assume that a set/rep workout is only done once in a day.
|
||||||
date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
|
/// The activity involved
|
||||||
|
pub activity: SetRepActivity,
|
||||||
/// Each set entry represents the number of times that the action was performed in a set. So, a
|
/// Each set entry represents the number of times that the action was performed in a set. So, a
|
||||||
/// pushup workout that involved five sets would have five entries. Each entry would be x
|
/// pushup workout that involved five sets would have five entries. Each entry would be x
|
||||||
/// number of pushups. A viable workout would be something like [6, 6, 4, 4, 5].
|
/// number of pushups. A viable workout would be something like [6, 6, 4, 4, 5].
|
||||||
sets: Vec<u32>,
|
pub sets: Vec<u32>,
|
||||||
comments: Option<String>,
|
pub comments: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Recordable for SetRep {
|
||||||
|
fn timestamp(&self) -> Timestamp {
|
||||||
|
Timestamp::Date(self.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tags(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The number of steps one takes in a single day.
|
/// The number of steps one takes in a single day.
|
||||||
|
@ -33,6 +70,23 @@ impl Recordable for Steps {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum TimeDistanceActivity {
|
||||||
|
Biking,
|
||||||
|
Running,
|
||||||
|
Rowing,
|
||||||
|
Swimming,
|
||||||
|
Walking,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TIME_DISTANCE_ACTIVITIES: [TimeDistanceActivity; 5] = [
|
||||||
|
TimeDistanceActivity::Biking,
|
||||||
|
TimeDistanceActivity::Rowing,
|
||||||
|
TimeDistanceActivity::Running,
|
||||||
|
TimeDistanceActivity::Swimming,
|
||||||
|
TimeDistanceActivity::Walking,
|
||||||
|
];
|
||||||
|
|
||||||
/// TimeDistance represents workouts characterized by a duration and a distance travelled. These
|
/// TimeDistance represents workouts characterized by a duration and a distance travelled. These
|
||||||
/// sorts of workouts can occur many times a day, depending on how one records things. I might
|
/// sorts of workouts can occur many times a day, depending on how one records things. I might
|
||||||
/// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km
|
/// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km
|
||||||
|
@ -48,6 +102,8 @@ pub struct TimeDistance {
|
||||||
/// in the database, but we can still get a Naive Date from the DateTime, which will still read
|
/// in the database, but we can still get a Naive Date from the DateTime, which will still read
|
||||||
/// as the original day.
|
/// as the original day.
|
||||||
pub datetime: DateTime<FixedOffset>,
|
pub datetime: DateTime<FixedOffset>,
|
||||||
|
/// The activity
|
||||||
|
pub activity: TimeDistanceActivity,
|
||||||
/// The distance travelled. This is optional because such a workout makes sense even without
|
/// The distance travelled. This is optional because such a workout makes sense even without
|
||||||
/// the distance.
|
/// the distance.
|
||||||
pub distance: Option<si::Meter<f64>>,
|
pub distance: Option<si::Meter<f64>>,
|
||||||
|
@ -57,6 +113,16 @@ pub struct TimeDistance {
|
||||||
pub comments: Option<String>,
|
pub comments: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Recordable for TimeDistance {
|
||||||
|
fn timestamp(&self) -> Timestamp {
|
||||||
|
Timestamp::DateTime(self.datetime)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tags(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A singular daily weight measurement. Weight changes slowly enough that it seems unlikely to
|
/// A singular daily weight measurement. Weight changes slowly enough that it seems unlikely to
|
||||||
/// need to track more than a single weight in a day.
|
/// need to track more than a single weight in a day.
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
@ -75,42 +141,49 @@ impl Recordable for Weight {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum RecordType {
|
pub enum DurationWorkoutActivity {
|
||||||
BikeRide,
|
MartialArts,
|
||||||
Row,
|
Yoga,
|
||||||
Run,
|
}
|
||||||
Steps,
|
|
||||||
Swim,
|
pub const DURATION_WORKOUT_ACTIVITIES: [DurationWorkoutActivity; 2] = [
|
||||||
Walk,
|
DurationWorkoutActivity::MartialArts,
|
||||||
Weight,
|
DurationWorkoutActivity::Yoga,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Generic workouts for which only duration really matters. This is for things
|
||||||
|
/// such as Martial Arts or Yoga, which are activities done for an amount of
|
||||||
|
/// time, but with no other details.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct DurationWorkout {
|
||||||
|
pub datetime: DateTime<FixedOffset>,
|
||||||
|
pub activity: DurationWorkoutActivity,
|
||||||
|
pub duration: Option<si::Second<f64>>,
|
||||||
|
pub comments: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Recordable for DurationWorkout {
|
||||||
|
fn timestamp(&self) -> Timestamp {
|
||||||
|
Timestamp::DateTime(self.datetime)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tags(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The unified data structure for all records that are part of the app.
|
/// The unified data structure for all records that are part of the app.
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum TraxRecord {
|
pub enum TraxRecord {
|
||||||
BikeRide(TimeDistance),
|
DurationWorkout(DurationWorkout),
|
||||||
Row(TimeDistance),
|
SetRep(SetRep),
|
||||||
Run(TimeDistance),
|
|
||||||
Steps(Steps),
|
Steps(Steps),
|
||||||
Swim(TimeDistance),
|
TimeDistance(TimeDistance),
|
||||||
Walk(TimeDistance),
|
|
||||||
Weight(Weight),
|
Weight(Weight),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TraxRecord {
|
impl TraxRecord {
|
||||||
pub fn workout_type(&self) -> RecordType {
|
|
||||||
match self {
|
|
||||||
TraxRecord::BikeRide(_) => RecordType::BikeRide,
|
|
||||||
TraxRecord::Row(_) => RecordType::Row,
|
|
||||||
TraxRecord::Run(_) => RecordType::Run,
|
|
||||||
TraxRecord::Steps(_) => RecordType::Steps,
|
|
||||||
TraxRecord::Swim(_) => RecordType::Swim,
|
|
||||||
TraxRecord::Walk(_) => RecordType::Walk,
|
|
||||||
TraxRecord::Weight(_) => RecordType::Weight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_weight(&self) -> bool {
|
pub fn is_weight(&self) -> bool {
|
||||||
matches!(self, TraxRecord::Weight(_))
|
matches!(self, TraxRecord::Weight(_))
|
||||||
}
|
}
|
||||||
|
@ -118,18 +191,38 @@ impl TraxRecord {
|
||||||
pub fn is_steps(&self) -> bool {
|
pub fn is_steps(&self) -> bool {
|
||||||
matches!(self, TraxRecord::Steps(_))
|
matches!(self, TraxRecord::Steps(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_time_distance(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
TraxRecord::TimeDistance(TimeDistance {
|
||||||
|
activity: TimeDistanceActivity::Biking,
|
||||||
|
..
|
||||||
|
}) | TraxRecord::TimeDistance(TimeDistance {
|
||||||
|
activity: TimeDistanceActivity::Running,
|
||||||
|
..
|
||||||
|
}) | TraxRecord::TimeDistance(TimeDistance {
|
||||||
|
activity: TimeDistanceActivity::Rowing,
|
||||||
|
..
|
||||||
|
}) | TraxRecord::TimeDistance(TimeDistance {
|
||||||
|
activity: TimeDistanceActivity::Swimming,
|
||||||
|
..
|
||||||
|
}) | TraxRecord::TimeDistance(TimeDistance {
|
||||||
|
activity: TimeDistanceActivity::Walking,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Recordable for TraxRecord {
|
impl Recordable for TraxRecord {
|
||||||
fn timestamp(&self) -> Timestamp {
|
fn timestamp(&self) -> Timestamp {
|
||||||
match self {
|
match self {
|
||||||
TraxRecord::BikeRide(rec) => Timestamp::DateTime(rec.datetime),
|
TraxRecord::TimeDistance(rec) => Timestamp::DateTime(rec.datetime),
|
||||||
TraxRecord::Row(rec) => Timestamp::DateTime(rec.datetime),
|
TraxRecord::SetRep(rec) => rec.timestamp(),
|
||||||
TraxRecord::Run(rec) => Timestamp::DateTime(rec.datetime),
|
|
||||||
TraxRecord::Steps(rec) => rec.timestamp(),
|
TraxRecord::Steps(rec) => rec.timestamp(),
|
||||||
TraxRecord::Swim(rec) => Timestamp::DateTime(rec.datetime),
|
|
||||||
TraxRecord::Walk(rec) => Timestamp::DateTime(rec.datetime),
|
|
||||||
TraxRecord::Weight(rec) => rec.timestamp(),
|
TraxRecord::Weight(rec) => rec.timestamp(),
|
||||||
|
TraxRecord::DurationWorkout(rec) => rec.timestamp(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,6 +231,12 @@ impl Recordable for TraxRecord {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<TimeDistance> for TraxRecord {
|
||||||
|
fn from(td: TimeDistance) -> Self {
|
||||||
|
Self::TimeDistance(td)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
10
flake.nix
|
@ -32,6 +32,7 @@
|
||||||
pkgs.gst_all_1.gstreamer
|
pkgs.gst_all_1.gstreamer
|
||||||
pkgs.gtk4
|
pkgs.gtk4
|
||||||
pkgs.libadwaita
|
pkgs.libadwaita
|
||||||
|
pkgs.librsvg
|
||||||
pkgs.nodejs
|
pkgs.nodejs
|
||||||
pkgs.openssl
|
pkgs.openssl
|
||||||
pkgs.pipewire
|
pkgs.pipewire
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
pkgs.udev
|
pkgs.udev
|
||||||
pkgs.wasm-pack
|
pkgs.wasm-pack
|
||||||
typeshare.packages."x86_64-linux".default
|
typeshare.packages."x86_64-linux".default
|
||||||
|
pkgs.nodePackages_latest.typescript-language-server
|
||||||
];
|
];
|
||||||
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
||||||
ENV = "dev";
|
ENV = "dev";
|
||||||
|
@ -57,6 +59,7 @@
|
||||||
pkgs.pkg-config
|
pkgs.pkg-config
|
||||||
pkgs.gtk4
|
pkgs.gtk4
|
||||||
pkgs.libadwaita
|
pkgs.libadwaita
|
||||||
|
pkgs.wrapGAppsHook4
|
||||||
];
|
];
|
||||||
|
|
||||||
cargoOverrides = pkgs: pkgs.buildRustCrate.override {
|
cargoOverrides = pkgs: pkgs.buildRustCrate.override {
|
||||||
|
@ -67,7 +70,8 @@
|
||||||
libadwaita-sys = attrs: { nativeBuildInputs = gtkNativeInputs; };
|
libadwaita-sys = attrs: { nativeBuildInputs = gtkNativeInputs; };
|
||||||
|
|
||||||
dashboard = attrs: { nativeBuildInputs = gtkNativeInputs; };
|
dashboard = attrs: { nativeBuildInputs = gtkNativeInputs; };
|
||||||
fitnesstrax = attrs: { nativeBuildInputs = gtkNativeInputs; };
|
fitnesstrax = import ./fitnesstrax/app/override.nix { gtkNativeInputs = gtkNativeInputs; };
|
||||||
|
otg-gtk = import ./otg/gtk/override.nix { gtkNativeInputs = gtkNativeInputs; };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -77,18 +81,22 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
in rec {
|
in rec {
|
||||||
|
cyber-slides = cargo_nix.workspaceMembers.cyber-slides.build;
|
||||||
cyberpunk-splash = cargo_nix.workspaceMembers.cyberpunk-splash.build;
|
cyberpunk-splash = cargo_nix.workspaceMembers.cyberpunk-splash.build;
|
||||||
dashboard = cargo_nix.workspaceMembers.dashboard.build;
|
dashboard = cargo_nix.workspaceMembers.dashboard.build;
|
||||||
file-service = cargo_nix.workspaceMembers.file-service.build;
|
file-service = cargo_nix.workspaceMembers.file-service.build;
|
||||||
fitnesstrax = cargo_nix.workspaceMembers.fitnesstrax.build;
|
fitnesstrax = cargo_nix.workspaceMembers.fitnesstrax.build;
|
||||||
|
otg-gtk = cargo_nix.workspaceMembers.otg-gtk.build;
|
||||||
|
|
||||||
all = pkgs.symlinkJoin {
|
all = pkgs.symlinkJoin {
|
||||||
name = "all";
|
name = "all";
|
||||||
paths = [
|
paths = [
|
||||||
|
cyber-slides
|
||||||
cyberpunk-splash
|
cyberpunk-splash
|
||||||
dashboard
|
dashboard
|
||||||
file-service
|
file-service
|
||||||
fitnesstrax
|
fitnesstrax
|
||||||
|
otg-gtk
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "gm-dash"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pipewire = "0.8.0"
|
||||||
|
serde = { version = "1.0.209", features = ["alloc", "derive"] }
|
||||||
|
serde_json = "1.0.127"
|
||||||
|
tokio = { version = "1.39.3", features = ["full"] }
|
||||||
|
warp = "0.3.7"
|
|
@ -0,0 +1,25 @@
|
||||||
|
use pipewire::{context::Context, main_loop::MainLoop};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mainloop = MainLoop::new(None)?;
|
||||||
|
let context = Context::new(&mainloop)?;
|
||||||
|
let core = context.connect(None)?;
|
||||||
|
let registry = core.get_registry()?;
|
||||||
|
|
||||||
|
let _listener = registry
|
||||||
|
.add_listener_local()
|
||||||
|
.global(|global| {
|
||||||
|
if global.props.and_then(|p| p.get("media.class")) == Some("Audio/Sink"){
|
||||||
|
println!(
|
||||||
|
"\t{:?} {:?}",
|
||||||
|
global.props.and_then(|p| p.get("node.description")),
|
||||||
|
global.props.and_then(|p| p.get("media.class"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.register();
|
||||||
|
|
||||||
|
mainloop.run();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
use pipewire::{context::Context, main_loop::MainLoop};
|
||||||
|
use std::{
|
||||||
|
net::{Ipv6Addr, SocketAddrV6},
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
use tokio::task::spawn_blocking;
|
||||||
|
use warp::{serve, Filter};
|
||||||
|
|
||||||
|
struct State_ {
|
||||||
|
device_list: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct State {
|
||||||
|
internal: Arc<RwLock<State_>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
fn new() -> State {
|
||||||
|
let internal = State_ {
|
||||||
|
device_list: vec![],
|
||||||
|
};
|
||||||
|
State {
|
||||||
|
internal: Arc::new(RwLock::new(internal)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_audio(&self, device: String) {
|
||||||
|
let mut st = self.internal.write().unwrap();
|
||||||
|
(*st).device_list.push(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn audio_devices(&self) -> Vec<String> {
|
||||||
|
let st = self.internal.read().unwrap();
|
||||||
|
(*st).device_list.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> State {
|
||||||
|
State::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn server_main(state: State) {
|
||||||
|
let localhost: Ipv6Addr = "::1".parse().unwrap();
|
||||||
|
let server_addr = SocketAddrV6::new(localhost, 3001, 0, 0);
|
||||||
|
|
||||||
|
let root = warp::path!().map(|| "ok".to_string());
|
||||||
|
let list_output_devices = warp::path!("output_devices").map({
|
||||||
|
let state = state.clone();
|
||||||
|
move || {
|
||||||
|
let devices = state.audio_devices();
|
||||||
|
serde_json::to_string(&devices).unwrap()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let routes = root.or(list_output_devices);
|
||||||
|
|
||||||
|
serve(routes).run(server_addr).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_add_audio_device(state: State, props: &pipewire::spa::utils::dict::DictRef)
|
||||||
|
{
|
||||||
|
if props.get("media.class") == Some("Audio/Sink") {
|
||||||
|
if let Some(device_name) = props.get("node.description") {
|
||||||
|
state.add_audio(device_name.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pipewire_loop(state: State) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mainloop = MainLoop::new(None)?;
|
||||||
|
let context = Context::new(&mainloop)?;
|
||||||
|
let core = context.connect(None)?;
|
||||||
|
let registry = core.get_registry()?;
|
||||||
|
|
||||||
|
let _listener = registry
|
||||||
|
.add_listener_local()
|
||||||
|
.global({
|
||||||
|
let state = state.clone();
|
||||||
|
move |global_data| {
|
||||||
|
if let Some(props) = global_data.props {
|
||||||
|
handle_add_audio_device(state.clone(), props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.register();
|
||||||
|
|
||||||
|
mainloop.run();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pipewire_main(state: State) {
|
||||||
|
pipewire_loop(state).expect("pipewire should not error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let state = State::default();
|
||||||
|
|
||||||
|
spawn_blocking({
|
||||||
|
let state = state.clone();
|
||||||
|
move || pipewire_main(state)
|
||||||
|
});
|
||||||
|
|
||||||
|
server_main(state.clone()).await;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.\
|
||||||
|
You will also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||||
|
|
||||||
|
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||||
|
|
||||||
|
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"@types/jest": "^27.5.2",
|
||||||
|
"@types/node": "^16.18.105",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.26.1",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,43 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 9.4 KiB |
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
|
@ -0,0 +1,5 @@
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import Dashboard from './Dashboard';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<Dashboard />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
|
@ -0,0 +1,77 @@
|
||||||
|
import './Dashboard.css';
|
||||||
|
import Card from './components/Card/Card';
|
||||||
|
import Launcher from './components/Launcher/Launcher';
|
||||||
|
import Launchpad from './components/Launchpad/Launchpad';
|
||||||
|
|
||||||
|
const LightThemes = () => <Card name="Light Themes">
|
||||||
|
<Launchpad
|
||||||
|
exclusive={true}
|
||||||
|
options={[
|
||||||
|
{ title: "Dark reds" },
|
||||||
|
{ title: "Watery" },
|
||||||
|
{ title: "Sunset" },
|
||||||
|
{ title: "Darkness" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
const LightSetup = () => <div> </div>
|
||||||
|
|
||||||
|
interface LightProps {
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Light = ({ name }: LightProps) => <div> <p> {name} </p> </div>
|
||||||
|
|
||||||
|
const Tracks = () => <Card name="Tracks">
|
||||||
|
<Launchpad
|
||||||
|
exclusive={false}
|
||||||
|
options={[
|
||||||
|
{ title: "City BGM" },
|
||||||
|
{ title: "Chat on the streets" },
|
||||||
|
{ title: "Abandoned structure" },
|
||||||
|
{ title: "Water dripping" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
interface TrackProps {
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
const Track = ({ name }: TrackProps) => <Launcher title={name} />
|
||||||
|
|
||||||
|
const Presets = () => <Card name="Presets">
|
||||||
|
<Launchpad
|
||||||
|
exclusive={true}
|
||||||
|
options={[
|
||||||
|
{ title: "Gilcrest Falls day" },
|
||||||
|
{ title: "Gilcrest Falls night" },
|
||||||
|
{ title: "Empty colony" },
|
||||||
|
{ title: "Surk colony" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
interface PresetProps {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// const Scene = ({ name }: PresetProps) => <Launcher title={name} activated={false} />
|
||||||
|
|
||||||
|
const SceneEditor = () => <div> </div>
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<div className="layout">
|
||||||
|
<Presets />
|
||||||
|
<div>
|
||||||
|
<LightThemes />
|
||||||
|
<Tracks />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard;
|
|
@ -0,0 +1,78 @@
|
||||||
|
.palette {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette div {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 1em;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-purple div.item-1 {
|
||||||
|
background-color: var(--purple-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-purple div.item-2 {
|
||||||
|
background-color: var(--purple-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-purple div.item-3 {
|
||||||
|
background-color: var(--purple-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-purple div.item-4 {
|
||||||
|
background-color: var(--purple-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-purple div.item-5 {
|
||||||
|
background-color: var(--purple-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-blue div.item-1 {
|
||||||
|
background-color: var(--blue-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-blue div.item-2 {
|
||||||
|
background-color: var(--blue-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-blue div.item-3 {
|
||||||
|
background-color: var(--blue-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-blue div.item-4 {
|
||||||
|
background-color: var(--blue-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-blue div.item-5 {
|
||||||
|
background-color: var(--blue-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-grey div.item-1 {
|
||||||
|
background-color: var(--grey-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-grey div.item-2 {
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-grey div.item-3 {
|
||||||
|
background-color: var(--grey-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-grey div.item-4 {
|
||||||
|
background-color: var(--grey-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-grey div.item-5 {
|
||||||
|
background-color: var(--grey-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.design_horizontal {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import './Design.css'
|
||||||
|
import Launchpad from './components/Launchpad/Launchpad'
|
||||||
|
|
||||||
|
const PaletteGrey = () => <div className="palette palette-grey">
|
||||||
|
<div className="item-1" />
|
||||||
|
<div className="item-2" />
|
||||||
|
<div className="item-3" />
|
||||||
|
<div className="item-4" />
|
||||||
|
<div className="item-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
const PalettePurple = () => <div className="palette palette-purple">
|
||||||
|
<div className="item-1" />
|
||||||
|
<div className="item-2" />
|
||||||
|
<div className="item-3" />
|
||||||
|
<div className="item-4" />
|
||||||
|
<div className="item-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
const PaletteBlue = () => <div className="palette palette-blue">
|
||||||
|
<div className="item-1" />
|
||||||
|
<div className="item-2" />
|
||||||
|
<div className="item-3" />
|
||||||
|
<div className="item-4" />
|
||||||
|
<div className="item-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
const Launchpads = () => <div className="design_horizontal">
|
||||||
|
<Launchpad
|
||||||
|
exclusive={true}
|
||||||
|
options={[
|
||||||
|
{ title: "Grey" },
|
||||||
|
{ title: "Purple" },
|
||||||
|
{ title: "Blue" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Launchpad
|
||||||
|
exclusive={false}
|
||||||
|
flow={"vertical"}
|
||||||
|
options={[
|
||||||
|
{ title: "Grey" },
|
||||||
|
{ title: "Purple" },
|
||||||
|
{ title: "Blue" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
const Design = () => <div>
|
||||||
|
<PaletteGrey />
|
||||||
|
<PalettePurple />
|
||||||
|
<PaletteBlue />
|
||||||
|
<Launchpads />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
export default Design;
|
|
@ -0,0 +1,15 @@
|
||||||
|
.card {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: var(--spacer-l);
|
||||||
|
box-shadow: 4px 4px 4px 0px var(--shadow-1),
|
||||||
|
8px 8px 8px 0px var(--shadow-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__title {
|
||||||
|
color: var(--title-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__body {
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import './Card.css';
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Card = ({ name, children }: PropsWithChildren<CardProps>) => (
|
||||||
|
<div className="card">
|
||||||
|
<h1 className="card__title"> {name} </h1>
|
||||||
|
<div className="card__body">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Card;
|
|
@ -0,0 +1,11 @@
|
||||||
|
.activator {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: var(--spacer-m);
|
||||||
|
padding: var(--spacer-s);
|
||||||
|
box-shadow: var(--shadow-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activator_enabled {
|
||||||
|
box-shadow: var(--activator-ring);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import './Launcher.css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export interface LauncherProps {
|
||||||
|
title: string,
|
||||||
|
icon?: string,
|
||||||
|
activated?: boolean,
|
||||||
|
onSelected?: (key: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Launcher = ({ title, activated = false, onSelected = (key) => {} }: LauncherProps) => {
|
||||||
|
const classnames = activated ? "activator activator_enabled" : "activator";
|
||||||
|
console.log("classnames ", activated, classnames);
|
||||||
|
return (
|
||||||
|
<div className={classnames} onClick={() => onSelected(title)}>
|
||||||
|
<p> {title} </p>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Launcher;
|
|
@ -0,0 +1,18 @@
|
||||||
|
.launchpad {
|
||||||
|
display: flex;
|
||||||
|
border: var(--border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--shadow-depression);
|
||||||
|
margin: var(--spacer-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.launchpad_horizontal-flow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.launchpad_vertical-flow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import './Launchpad.css';
|
||||||
|
import Launcher, { LauncherProps } from '../Launcher/Launcher';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
export interface Selectable {
|
||||||
|
onSelected?: (key: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Flow = "horizontal" | "vertical";
|
||||||
|
|
||||||
|
interface LaunchpadProps {
|
||||||
|
exclusive: boolean;
|
||||||
|
flow?: Flow;
|
||||||
|
options: Array<LauncherProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exclusiveSelect = (state: { [key: string]: boolean }, targetId: string) => {
|
||||||
|
console.log("running exclusiveSelect on ", targetId);
|
||||||
|
return { [targetId]: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiSelect = (state: { [key: string]: boolean }, targetId: string) => {
|
||||||
|
if (state[targetId]) {
|
||||||
|
return { ...state, [targetId]: false };
|
||||||
|
} else {
|
||||||
|
return { ...state, [targetId]: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Launchpad = ({ flow = "horizontal", options, exclusive }: LaunchpadProps) => {
|
||||||
|
const [selected, dispatch] = React.useReducer(exclusive ? exclusiveSelect : multiSelect, {});
|
||||||
|
|
||||||
|
let classOptions = [ "launchpad" ];
|
||||||
|
|
||||||
|
if (flow === "horizontal") {
|
||||||
|
classOptions.push("launchpad_horizontal-flow");
|
||||||
|
} else {
|
||||||
|
classOptions.push("launchpad_vertical-flow");
|
||||||
|
}
|
||||||
|
|
||||||
|
let tiedOptions = options.map(option =>
|
||||||
|
<Launcher key={option.title} title={option.title} onSelected={(key: string) => dispatch(key)} activated={selected[option.title]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (<div className={classnames(classOptions)}> {tiedOptions} </div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Launchpad;
|
|
@ -0,0 +1,50 @@
|
||||||
|
:root {
|
||||||
|
--purple-1: hsl(265, 50%, 25%);
|
||||||
|
--purple-2: hsl(265, 60%, 35%);
|
||||||
|
--purple-3: hsl(265, 70%, 45%);
|
||||||
|
--purple-4: hsl(265, 80%, 55%);
|
||||||
|
--purple-5: hsl(265, 90%, 60%);
|
||||||
|
|
||||||
|
--blue-1: hsl(210, 50%, 25%);
|
||||||
|
--blue-2: hsl(210, 60%, 35%);
|
||||||
|
--blue-3: hsl(210, 70%, 45%);
|
||||||
|
--blue-4: hsl(210, 80%, 55%);
|
||||||
|
--blue-5: hsl(210, 90%, 65%);
|
||||||
|
|
||||||
|
--grey-1: hsl(210, 0%, 25%);
|
||||||
|
--grey-2: hsl(210, 0%, 40%);
|
||||||
|
--grey-3: hsl(210, 0%, 55%);
|
||||||
|
--grey-4: hsl(210, 0%, 70%);
|
||||||
|
--grey-5: hsl(210, 0%, 85%);
|
||||||
|
|
||||||
|
--title-color: var(--grey-1);
|
||||||
|
|
||||||
|
--border: 1px solid var(--purple-1);
|
||||||
|
--activator-ring: 0px 0px 8px 4px var(--blue-4);
|
||||||
|
--shadow-depression: inset 1px 1px 2px 0px var(--purple-1);
|
||||||
|
--shadow-shallow: 1px 1px 2px 0px var(--purple-1);
|
||||||
|
--shadow-deep: 2px 2px 4px 0px var(--purple-1),
|
||||||
|
4px 4px 8px 0px var(--purple-2);
|
||||||
|
|
||||||
|
--border-radius: 8px;
|
||||||
|
|
||||||
|
--spacer-xs: 2px;
|
||||||
|
--spacer-s: 4px;
|
||||||
|
--spacer-m: 8px;
|
||||||
|
--spacer-l: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: var(--grey-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import Dashboard from './Dashboard';
|
||||||
|
import Design from './Design';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(
|
||||||
|
document.getElementById('root') as HTMLElement
|
||||||
|
);
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <Dashboard />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/design",
|
||||||
|
element: <Design />,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="react-scripts" />
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ReportHandler } from 'web-vitals';
|
||||||
|
|
||||||
|
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|