Compare commits

..

2 Commits

Author SHA1 Message Date
Savanni D'Gerinel 6bf40684b9 Start trying to draw the game tree 2023-08-29 23:22:04 -04:00
Savanni D'Gerinel d85d9c4593 Start on the game review window 2023-08-29 23:22:04 -04:00
374 changed files with 10189 additions and 79595 deletions

5
.gitignore vendored
View File

@ -4,8 +4,3 @@ node_modules
dist
result
*.tgz
*.tar.gz
*.sqlite
*.sqlite-shm
*.sqlite-wal
file-service/var

View File

@ -1,3 +0,0 @@
{
"rust-analyzer.showUnlinkedFileNotification": false
}

3827
Cargo.lock generated

File diff suppressed because it is too large Load Diff

18372
Cargo.nix

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,21 @@
[workspace]
resolver = "2"
members = [
"authdb",
# "bike-lights/bike",
"bike-lights/core",
"bike-lights/simulator",
"changeset",
"config",
"config-derive",
"coordinates",
"cyberpunk",
"cyber-slides",
"cyberpunk-splash",
"dashboard",
"emseries",
"file-service",
"fitnesstrax/core",
"fitnesstrax/app",
"flow",
"fluent-ergonomics",
"geo-types",
"gm-control-panel",
"hex-grid",
"icon-test",
"ifc",
"kifu/core",
"kifu/gtk",
"memorycache",
"nom-training",
"otg/core",
"otg/gtk",
"result-extended",
"screenplay",
"sgf",
"timezone-testing",
"tree",
"visions/server",
"gm-dash/server"
]

30
Makefile Normal file
View File

@ -0,0 +1,30 @@
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

View File

@ -1,29 +0,0 @@
[package]
name = "authdb"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "authdb"
path = "src/lib.rs"
[[bin]]
name = "auth-cli"
path = "src/bin/cli.rs"
[dependencies]
base64ct = { version = "1", features = [ "alloc" ] }
clap = { version = "4", features = [ "derive" ] }
serde = { version = "1.0", features = ["derive"] }
sha2 = { version = "0.10" }
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" }
tokio = { version = "1", features = [ "full" ] }
uuid = { version = "0.4", features = [ "serde", "v4" ] }
[dev-dependencies]
cool_asserts = "*"

View File

@ -1,11 +0,0 @@
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY NOT NULL,
username TEXT NOT NULL,
token TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT NOT NULL,
user_id INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id)
);

View File

@ -1,40 +0,0 @@
use authdb::{AuthDB, Username};
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Subcommand, Debug)]
enum Commands {
AddUser { username: String },
DeleteUser { username: String },
ListUsers,
}
#[derive(Parser, Debug)]
struct Args {
#[command(subcommand)]
command: Commands,
}
#[tokio::main]
pub async fn main() {
let args = Args::parse();
let authdb = AuthDB::new(PathBuf::from(&std::env::var("AUTHDB").unwrap()))
.await
.expect("to be able to open the database");
match args.command {
Commands::AddUser { username } => {
match authdb.add_user(Username::from(username.clone())).await {
Ok(token) => {
println!("User {} created. Auth token: {}", username, *token);
}
Err(err) => {
println!("Could not create user {}", username);
println!("\tError: {:?}", err);
}
}
}
Commands::DeleteUser { .. } => {}
Commands::ListUsers => {}
}
}

View File

@ -1,302 +0,0 @@
use base64ct::{Base64, Encoding};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use sqlx::{
sqlite::{SqlitePool, SqliteRow},
Row,
};
use std::ops::Deref;
use std::path::PathBuf;
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum AuthError {
#[error("authentication token is duplicated")]
DuplicateAuthToken,
#[error("session token is duplicated")]
DuplicateSessionToken,
#[error("database failed")]
SqlError(sqlx::Error),
}
impl From<sqlx::Error> for AuthError {
fn from(err: sqlx::Error) -> AuthError {
AuthError::SqlError(err)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub struct Username(String);
impl From<String> for Username {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Username {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<Username> for String {
fn from(s: Username) -> Self {
Self::from(&s)
}
}
impl From<&Username> for String {
fn from(s: &Username) -> Self {
let Username(s) = s;
Self::from(s)
}
}
impl Deref for Username {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl sqlx::FromRow<'_, SqliteRow> for Username {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
let name: String = row.try_get("username")?;
Ok(Username::from(name))
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub struct AuthToken(String);
impl From<String> for AuthToken {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for AuthToken {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<AuthToken> for PathBuf {
fn from(s: AuthToken) -> Self {
Self::from(&s)
}
}
impl From<&AuthToken> for PathBuf {
fn from(s: &AuthToken) -> Self {
let AuthToken(s) = s;
Self::from(s)
}
}
impl Deref for AuthToken {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub struct SessionToken(String);
impl From<String> for SessionToken {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for SessionToken {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<SessionToken> for PathBuf {
fn from(s: SessionToken) -> Self {
Self::from(&s)
}
}
impl From<&SessionToken> for PathBuf {
fn from(s: &SessionToken) -> Self {
let SessionToken(s) = s;
Self::from(s)
}
}
impl Deref for SessionToken {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone)]
pub struct AuthDB {
pool: SqlitePool,
}
impl AuthDB {
pub async fn new(path: PathBuf) -> Result<Self, sqlx::Error> {
let migrator = sqlx::migrate!("./migrations");
let pool = SqlitePool::connect(&format!("sqlite://{}", path.to_str().unwrap())).await?;
migrator.run(&pool).await?;
Ok(Self { pool })
}
pub async fn add_user(&self, username: Username) -> Result<AuthToken, AuthError> {
let mut hasher = Sha256::new();
hasher.update(Uuid::new_v4().hyphenated().to_string());
hasher.update(username.to_string());
let auth_token = Base64::encode_string(&hasher.finalize());
let _ = sqlx::query("INSERT INTO users (username, token) VALUES ($1, $2)")
.bind(username.to_string())
.bind(auth_token.clone())
.execute(&self.pool)
.await?;
Ok(AuthToken::from(auth_token))
}
pub async fn list_users(&self) -> Result<Vec<Username>, AuthError> {
let usernames = sqlx::query_as::<_, Username>("SELECT (username) FROM users")
.fetch_all(&self.pool)
.await?;
Ok(usernames)
}
pub async fn authenticate(&self, token: AuthToken) -> Result<Option<SessionToken>, AuthError> {
let results = sqlx::query("SELECT * FROM users WHERE token = $1")
.bind(token.to_string())
.fetch_all(&self.pool)
.await?;
if results.len() > 1 {
return Err(AuthError::DuplicateAuthToken);
}
if results.is_empty() {
return Ok(None);
}
let user_id: i64 = results[0].try_get("id")?;
let mut hasher = Sha256::new();
hasher.update(Uuid::new_v4().hyphenated().to_string());
hasher.update(token.to_string());
let session_token = Base64::encode_string(&hasher.finalize());
let _ = sqlx::query("INSERT INTO sessions (token, user_id) VALUES ($1, $2)")
.bind(session_token.clone())
.bind(user_id)
.execute(&self.pool)
.await?;
Ok(Some(SessionToken::from(session_token)))
}
pub async fn validate_session(
&self,
token: SessionToken,
) -> Result<Option<Username>, AuthError> {
let rows = sqlx::query(
"SELECT users.username FROM sessions INNER JOIN users ON sessions.user_id = users.id WHERE sessions.token = $1",
)
.bind(token.to_string())
.fetch_all(&self.pool)
.await?;
if rows.len() > 1 {
return Err(AuthError::DuplicateSessionToken);
}
if rows.is_empty() {
return Ok(None);
}
let username: String = rows[0].try_get("username")?;
Ok(Some(Username::from(username)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use cool_asserts::assert_matches;
use std::collections::HashSet;
#[tokio::test]
async fn can_create_and_list_users() {
let db = AuthDB::new(PathBuf::from(":memory:"))
.await
.expect("a memory-only database will be created");
let _ = db
.add_user(Username::from("savanni"))
.await
.expect("user to be created");
assert_matches!(db.list_users().await, Ok(names) => {
let names = names.into_iter().collect::<HashSet<Username>>();
assert!(names.contains(&Username::from("savanni")));
})
}
#[tokio::test]
async fn unknown_auth_token_returns_nothing() {
let db = AuthDB::new(PathBuf::from(":memory:"))
.await
.expect("a memory-only database will be created");
let _ = db
.add_user(Username::from("savanni"))
.await
.expect("user to be created");
let token = AuthToken::from("0000000000");
assert_matches!(db.authenticate(token).await, Ok(None));
}
#[tokio::test]
async fn auth_token_becomes_session_token() {
let db = AuthDB::new(PathBuf::from(":memory:"))
.await
.expect("a memory-only database will be created");
let token = db
.add_user(Username::from("savanni"))
.await
.expect("user to be created");
assert_matches!(db.authenticate(token).await, Ok(_));
}
#[tokio::test]
async fn can_validate_session_token() {
let db = AuthDB::new(PathBuf::from(":memory:"))
.await
.expect("a memory-only database will be created");
let token = db
.add_user(Username::from("savanni"))
.await
.expect("user to be created");
let session = db
.authenticate(token)
.await
.expect("token authentication should succeed")
.expect("session token should be found");
assert_matches!(
db.validate_session(session).await,
Ok(Some(username)) => {
assert_eq!(username, Username::from("savanni"));
});
}
}

View File

@ -1,12 +0,0 @@
[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"

View File

@ -1,18 +0,0 @@
[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" }

View File

@ -1,244 +0,0 @@
#![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());
}
}

View File

@ -1,158 +0,0 @@
$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();

View File

@ -1,174 +0,0 @@
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();

View File

@ -1,21 +0,0 @@
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();

View File

@ -1,92 +0,0 @@
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]);
}
}

View File

@ -1,210 +0,0 @@
$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);
}
}

View File

@ -1,11 +0,0 @@
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();

View File

@ -1,6 +0,0 @@
include <./control_panel.scad>
lid();
// lamp();

View File

@ -1,4 +0,0 @@
include <./control_panel.scad>
box_side();

View File

@ -1,10 +0,0 @@
[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" }

View File

@ -1,481 +0,0 @@
#![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);
}
}

View File

@ -1,400 +0,0 @@
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,
];

View File

@ -1,17 +0,0 @@
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];

View File

@ -1,16 +0,0 @@
[package]
name = "simulator"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
cairo-rs = { version = "0.18" }
fixed = { version = "1" }
gio = { version = "0.18" }
glib = { version = "0.18" }
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
lights-core = { path = "../core" }
pango = { version = "*" }

View File

@ -1,288 +0,0 @@
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);
}

68
build.sh Executable file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env bash
set -euo pipefail
RUST_ALL_TARGETS=(
"changeset"
"config"
"config-derive"
"coordinates"
"cyberpunk-splash"
"dashboard"
"emseries"
"flow"
"fluent-ergonomics"
"geo-types"
"gm-control-panel"
"hex-grid"
"ifc"
"kifu-core"
"kifu-gtk"
"memorycache"
"screenplay"
"sgf"
)
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
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

36
builders/rust.sh Executable file
View File

@ -0,0 +1,36 @@
#!/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
;;
test)
$CARGO test $MODULE $PARAMS
;;
run)
$CARGO run $MODULE $PARAMS
;;
release)
$CARGO build --release $MODULE $PARAMS
;;
clean)
$CARGO clean $MODULE
;;
"")
echo "No command specified. Use build | test | run | release | clean"
;;
*)
echo "$CMD is unknown. Use build | test | run | release | clean"
;;
esac

View File

@ -3,6 +3,7 @@ name = "changeset"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-only"
license-file = "../COPYING"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -26,7 +26,7 @@ pub enum Change<Key: Eq + Hash, Value> {
NewRecord(Value),
}
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug)]
pub struct Changeset<Key: Clone + Eq + Hash, Value> {
delete: HashSet<Key>,
update: HashMap<Key, Value>,
@ -34,6 +34,14 @@ pub struct Changeset<Key: Clone + Eq + Hash, Value> {
}
impl<Key: Clone + Constructable + Eq + Hash, Value> Changeset<Key, Value> {
pub fn new() -> Self {
Self {
delete: HashSet::new(),
update: HashMap::new(),
new: HashMap::new(),
}
}
pub fn add(&mut self, r: Value) -> Key {
let k = Key::new();
self.new.insert(k.clone(), r);
@ -82,7 +90,7 @@ impl<Key: Clone + Eq + Hash, Value> From<Changeset<Key, Value>> for Vec<Change<K
.into_iter()
.map(|(k, v)| Change::UpdateRecord((k, v))),
)
.chain(new.into_values().map(|v| Change::NewRecord(v)))
.chain(new.into_iter().map(|(_, v)| Change::NewRecord(v)))
.collect()
}
}
@ -92,7 +100,7 @@ mod tests {
use super::*;
use uuid::Uuid;
#[derive(Clone, PartialEq, Eq, Hash, Default)]
#[derive(Clone, PartialEq, Eq, Hash)]
struct Id(Uuid);
impl Constructable for Id {
fn new() -> Self {
@ -102,7 +110,7 @@ mod tests {
#[test]
fn it_generates_a_new_record() {
let mut set: Changeset<Id, String> = Changeset::default();
let mut set: Changeset<Id, String> = Changeset::new();
set.add("efgh".to_string());
let changes = Vec::from(set.clone());
assert_eq!(changes.len(), 1);
@ -117,7 +125,7 @@ mod tests {
#[test]
fn it_generates_a_delete_record() {
let mut set: Changeset<Id, String> = Changeset::default();
let mut set: Changeset<Id, String> = Changeset::new();
let id1 = Id::new();
set.delete(id1.clone());
let changes = Vec::from(set.clone());
@ -134,7 +142,7 @@ mod tests {
#[test]
fn update_unrelated_records() {
let mut set: Changeset<Id, String> = Changeset::default();
let mut set: Changeset<Id, String> = Changeset::new();
let id1 = Id::new();
let id2 = Id::new();
set.update(id1.clone(), "abcd".to_owned());
@ -147,7 +155,7 @@ mod tests {
#[test]
fn delete_cancels_new() {
let mut set: Changeset<Id, String> = Changeset::default();
let mut set: Changeset<Id, String> = Changeset::new();
let key = set.add("efgh".to_string());
set.delete(key);
let changes = Vec::from(set);
@ -156,7 +164,7 @@ mod tests {
#[test]
fn delete_cancels_update() {
let mut set: Changeset<Id, String> = Changeset::default();
let mut set: Changeset<Id, String> = Changeset::new();
let id = Id::new();
set.update(id.clone(), "efgh".to_owned());
set.delete(id.clone());
@ -167,7 +175,7 @@ mod tests {
#[test]
fn update_atop_new_is_new() {
let mut set: Changeset<Id, String> = Changeset::default();
let mut set: Changeset<Id, String> = Changeset::new();
let key = set.add("efgh".to_owned());
set.update(key, "wxyz".to_owned());
let changes = Vec::from(set);
@ -177,7 +185,7 @@ mod tests {
#[test]
fn updates_get_squashed() {
let mut set: Changeset<Id, String> = Changeset::default();
let mut set: Changeset<Id, String> = Changeset::new();
let id1 = Id::new();
let id2 = Id::new();
set.update(id1.clone(), "efgh".to_owned());

View File

@ -35,7 +35,7 @@ macro_rules! define_config {
$($name($struct)),+
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug)]
pub struct Config {
values: std::collections::HashMap<ConfigName, ConfigOption>,
}

View File

@ -33,12 +33,12 @@ fn main() {
let filename = args
.next()
.map(PathBuf::from)
.map(|p| PathBuf::from(p))
.expect("A filename is required");
let size = args
.next()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(3);
let map: hex_map::Map<MapVal> = hex_map::Map::new_hexagonal(size);
hex_map::write_file(filename, map).expect("to write file");
hex_map::write_file(filename, map);
}

View File

@ -10,9 +10,10 @@ Luminescent Dreams Tools is distributed in the hope that it will be useful, but
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/
/// This module contains the elements of cube coordinates.
/// Ĉi-tiu modulo enhavas la elementojn por kub-koordinato.
///
/// This code is based on https://www.redblobgames.com/grids/hexagons/
use crate::Error;
use std::collections::HashSet;
/// An address within the hex coordinate system
@ -61,7 +62,7 @@ impl AxialAddr {
pub fn is_adjacent(&self, dest: &AxialAddr) -> bool {
dest.adjacencies()
.collect::<Vec<AxialAddr>>()
.contains(self)
.contains(&self)
}
/// Measure the distance to a destination
@ -78,7 +79,7 @@ impl AxialAddr {
positions.push(item);
while !positions.is_empty() {
while positions.len() > 0 {
let elem = positions.remove(0);
for adj in elem.adjacencies() {
if self.distance(&adj) <= distance && !results.contains(&adj) {

View File

@ -14,6 +14,7 @@ use crate::{hex::AxialAddr, Error};
use nom::{
bytes::complete::tag,
character::complete::alphanumeric1,
error::ParseError,
multi::many1,
sequence::{delimited, separated_pair},
Finish, IResult, Parser,
@ -80,7 +81,7 @@ pub fn parse_data<'a, A: Default + From<String>>(
}
let cells = data
.map(|line| parse_line::<A>(line).unwrap())
.map(|line| parse_line::<A>(&line).unwrap())
.collect::<Vec<(AxialAddr, A)>>();
let cells = cells.into_iter().collect::<HashMap<AxialAddr, A>>();
Map { cells }

View File

@ -9,9 +9,9 @@ Lumeto is distributed in the hope that it will be useful, but WITHOUT ANY WARRAN
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/
use thiserror::Error;
use thiserror;
#[derive(Debug, Error)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IO error on reading or writing: {0}")]
IO(std::io::Error),

View File

@ -1,491 +0,0 @@
{
"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"
}

View File

@ -1,16 +0,0 @@
[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"

View File

@ -1,416 +0,0 @@
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();
}

View File

@ -2,13 +2,11 @@
name = "cyberpunk-splash"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cairo-rs = { version = "0.18" }
cyberpunk = { path = "../cyberpunk" }
gio = { version = "0.18" }
glib = { version = "0.18" }
gtk = { version = "0.7", package = "gtk4" }
cairo-rs = { version = "0.17" }
gio = { version = "0.17" }
glib = { version = "0.17" }
gtk = { version = "0.6", package = "gtk4" }

View File

@ -2,9 +2,8 @@ use cairo::{
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern,
TextExtents,
};
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, SlashMeter};
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*, EventControllerKey};
use glib::{GString, Object};
use gtk::{gdk::Key, prelude::*, subclass::prelude::*, EventControllerKey};
use std::{
cell::RefCell,
rc::Rc,
@ -15,6 +14,12 @@ use std::{
const WIDTH: i32 = 1600;
const HEIGHT: i32 = 600;
#[derive(Clone, Copy, Debug)]
enum Event {
Frames(u8),
Time(Duration),
}
#[derive(Clone, Copy, Debug)]
pub enum State {
Running {
@ -45,7 +50,7 @@ impl State {
*self = Self::Running {
last_update: Instant::now(),
deadline: Instant::now() + *time_remaining,
timeout: *timeout,
timeout: timeout.clone(),
};
}
}
@ -57,7 +62,7 @@ impl State {
{
*self = Self::Paused {
time_remaining: *deadline - Instant::now(),
timeout: *timeout,
timeout: timeout.clone(),
}
}
}
@ -103,13 +108,13 @@ impl TimeoutAnimation {
fn tick(&mut self, frames_elapsed: u8) {
let step_size = 1. / (self.duration * 60.);
if self.ascending {
self.intensity += step_size * frames_elapsed as f64;
self.intensity = self.intensity + step_size * frames_elapsed as f64;
if self.intensity > 1. {
self.intensity = 1.0;
self.ascending = false;
}
} else {
self.intensity -= step_size * frames_elapsed as f64;
self.intensity = self.intensity - step_size * frames_elapsed as f64;
if self.intensity < 0. {
self.intensity = 0.0;
self.ascending = true;
@ -143,6 +148,7 @@ impl SplashPrivate {
*self.height.borrow(),
2.,
8.,
8.,
(0.7, 0., 1.),
);
@ -172,7 +178,7 @@ impl SplashPrivate {
start_y: extents.height() + 10.,
start_length: 0.,
height: extents.height() / 2.,
end_length: 0.,
total_length: extents.width() + extents.height() / 2.,
invert: false,
}
.draw(&pen);
@ -184,7 +190,7 @@ impl SplashPrivate {
start_y: extents.height() + 60.,
start_length: extents.width(),
height: extents.height() / 2.,
end_length: 0.,
total_length: extents.width() + extents.height() / 2.,
invert: false,
}
.draw(&pen);
@ -209,7 +215,7 @@ impl SplashPrivate {
start_x: 20.,
start_y: center_y - 20. - title_height / 2.,
start_length,
end_length: *self.width.borrow() as f64 - 120. - start_length,
total_length: *self.width.borrow() as f64 - 120.,
cutout_length: title_width,
height: title_height,
invert: false,
@ -244,7 +250,7 @@ impl SplashPrivate {
start_y: *self.height.borrow() as f64 / 2. + 100.,
start_length: 400.,
height: 50.,
end_length: 0.,
total_length: 650.,
invert: true,
}
.draw(&pen);
@ -259,7 +265,7 @@ impl SplashPrivate {
start_y: *self.height.borrow() as f64 / 2. + 200.,
start_length: 600.,
height: 50.,
end_length: 0.,
total_length: 650.,
invert: false,
}
.draw(&pen);
@ -327,7 +333,7 @@ impl Splash {
let _ = context.set_source(&*background);
let _ = context.paint();
let state = *s.imp().state.borrow();
let state = s.imp().state.borrow().clone();
let time = match state {
State::Running { deadline, .. } => deadline - Instant::now(),
@ -353,7 +359,7 @@ impl Splash {
let mut saved_extents = s.imp().time_extents.borrow_mut();
if saved_extents.is_none() {
*saved_extents = Some(time_extents);
*saved_extents = Some(time_extents.clone());
}
let time_baseline_x = center_x - time_extents.width() / 2.;
@ -366,8 +372,8 @@ impl Splash {
time_baseline_y,
);
let (running, timeout_animation) = match state {
State::Running { timeout, .. } => (true, timeout),
State::Paused { timeout, .. } => (false, timeout),
State::Running { timeout, .. } => (true, timeout.clone()),
State::Paused { timeout, .. } => (false, timeout.clone()),
};
match timeout_animation {
Some(ref animation) => {
@ -389,7 +395,8 @@ impl Splash {
let _ = context.show_text(&time);
};
if let Some(extents) = *s.imp().time_extents.borrow() {
match *s.imp().time_extents.borrow() {
Some(extents) => {
context.set_source_rgb(0.7, 0.0, 1.0);
let time_meter = SlashMeter {
orientation: gtk::Orientation::Horizontal,
@ -400,7 +407,9 @@ impl Splash {
height: 60.,
length: 100.,
};
time_meter.draw(context);
time_meter.draw(&context);
}
None => {}
}
}
});
@ -420,6 +429,218 @@ 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 as f64 / (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,
line_width: f64,
blur_line_width: f64,
blur_size: f64,
}
impl GlowPen {
fn new(
width: i32,
height: i32,
line_width: f64,
blur_line_width: f64,
blur_size: 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,
line_width,
blur_line_width,
blur_size,
}
}
}
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();
self.draw_context.stroke();
}
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() {
let app = gtk::Application::builder()
@ -460,7 +681,7 @@ fn main() {
let countdown = match options.lookup::<String>("countdown") {
Ok(Some(countdown_str)) => {
let parts = countdown_str.split(':').collect::<Vec<&str>>();
match parts.len() {
let duration = match parts.len() {
2 => {
let minutes = parts[0].parse::<u64>().unwrap();
let seconds = parts[1].parse::<u64>().unwrap();
@ -471,7 +692,8 @@ fn main() {
Duration::from_secs(seconds)
}
_ => Duration::from_secs(300),
}
};
duration
}
_ => Duration::from_secs(300),
};
@ -498,12 +720,12 @@ fn main() {
app.connect_activate(move |app| {
let (gtk_tx, gtk_rx) =
gtk::glib::MainContext::channel::<State>(gtk::glib::Priority::DEFAULT);
gtk::glib::MainContext::channel::<State>(gtk::glib::PRIORITY_DEFAULT);
let window = gtk::ApplicationWindow::new(app);
window.present();
let splash = Splash::new(title.read().unwrap().clone(), *state.read().unwrap());
let splash = Splash::new(title.read().unwrap().clone(), state.read().unwrap().clone());
window.set_child(Some(&splash));
@ -531,7 +753,7 @@ fn main() {
gtk_rx.attach(None, move |state| {
splash.set_state(state);
glib::ControlFlow::Continue
Continue(true)
});
std::thread::spawn({
@ -541,7 +763,7 @@ fn main() {
loop {
std::thread::sleep(Duration::from_millis(1000 / 60));
state.write().unwrap().run(Instant::now());
let _ = gtk_tx.send(*state.read().unwrap());
let _ = gtk_tx.send(state.read().unwrap().clone());
}
}
});

View File

@ -1,12 +0,0 @@
[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" }

View File

@ -1,301 +0,0 @@
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
}

View File

@ -1,31 +1,32 @@
[package]
name = "dashboard"
version = "0.1.3"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
async-std = { version = "1.13" }
cairo-rs = { version = "0.18" }
adw = { version = "0.4", package = "libadwaita", features = [ "v1_2" ] }
cairo-rs = { version = "0.17" }
chrono = { version = "0.4", features = ["serde"] }
fluent-ergonomics = { path = "../fluent-ergonomics/" }
fluent = { version = "0.16" }
futures = { version = "0.3" }
geo-types = { path = "../geo-types/" }
gio = { version = "0.18" }
glib = { version = "0.18" }
gdk = { version = "0.7", package = "gdk4" }
gtk = { version = "0.7", package = "gtk4" }
gio = { version = "0.17" }
glib = { version = "0.17" }
gdk = { version = "0.6", package = "gdk4" }
gtk = { version = "0.6", package = "gtk4" }
ifc = { path = "../ifc/" }
lazy_static = { version = "1.4" }
memorycache = { path = "../memorycache/" }
reqwest = { version = "0.11", features = ["json"] }
serde_derive = { version = "1" }
serde_json = { version = "1" }
serde = { version = "1", features = [ "derive" ] }
serde = { version = "1" }
tokio = { version = "1", features = ["full"] }
unic-langid = { version = "0.9" }
[build-dependencies]
glib-build-tools = "0.18"
glib-build-tools = "0.16"

View File

@ -1,7 +1,7 @@
fn main() {
glib_build_tools::compile_resources(
&["resources"],
"gresources.xml",
"resources",
"resources/gresources.xml",
"com.luminescent-dreams.dashboard.gresource",
);
}

View File

@ -1,12 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION=`cat Cargo.toml | grep "^version =" | sed -r 's/^version = "(.+)"$/\1/'`
set -x
mkdir -p dist
cp dashboard.desktop dist
cp ../target/release/dashboard dist
strip dist/dashboard
tar -czf dashboard-${VERSION}.tgz dist/
tar -cf dashboard.tgz dist/

View File

@ -40,19 +40,16 @@ impl ApplicationWindow {
.vexpand(true)
.build();
let date_label = Date::default();
let header = adw::HeaderBar::builder()
.title_widget(&date_label)
.build();
layout.append(&header);
let date_label = Date::new();
layout.append(&date_label);
let events = Events::default();
let events = Events::new();
layout.append(&events);
let transit_card = TransitCard::default();
let transit_card = TransitCard::new();
layout.append(&transit_card);
let transit_clock = TransitClock::default();
let transit_clock = TransitClock::new();
layout.append(&transit_clock);
window.set_content(Some(&layout));

View File

@ -1,19 +1,20 @@
use std::{cell::RefCell, rc::Rc};
use chrono::NaiveDate;
use chrono::Datelike;
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use ifc::IFC;
use std::{cell::RefCell, rc::Rc};
pub struct DatePrivate {
date: Rc<RefCell<NaiveDate>>,
date: Rc<RefCell<IFC>>,
label: Rc<RefCell<gtk::Label>>,
}
impl Default for DatePrivate {
fn default() -> Self {
let date = chrono::Local::now().date_naive();
Self {
date: Rc::new(RefCell::new(date)),
date: Rc::new(RefCell::new(IFC::from(
chrono::Local::now().date_naive().with_year(12023).unwrap(),
))),
label: Rc::new(RefCell::new(gtk::Label::new(None))),
}
}
@ -34,8 +35,8 @@ glib::wrapper! {
pub struct Date(ObjectSubclass<DatePrivate>) @extends gtk::Box, gtk::Widget;
}
impl Default for Date {
fn default() -> Self {
impl Date {
pub fn new() -> Self {
let s: Self = Object::builder().build();
s.set_margin_bottom(8);
s.set_margin_top(8);
@ -47,19 +48,20 @@ impl Default for Date {
s.redraw();
s
}
}
impl Date {
pub fn update_date(&self, date: NaiveDate) {
pub fn update_date(&self, date: IFC) {
*self.imp().date.borrow_mut() = date;
self.redraw();
}
fn redraw(&self) {
let date = self.imp().date.borrow();
self.imp()
.label
.borrow_mut()
.set_text(&date.format("%Y %B %d").to_string());
let date = self.imp().date.borrow().clone();
self.imp().label.borrow_mut().set_text(&format!(
"{:?}, {:?} {}, {}",
date.weekday(),
date.month(),
date.day(),
date.year()
));
}
}

View File

@ -1,11 +1,13 @@
use crate::{
components::Date,
solstices::{self, YearlyEvents},
soluna_client::SunMoon,
};
use chrono::TimeZone;
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use gtk::{prelude::*, subclass::prelude::*, IconLookupFlags};
use ifc::IFC;
/*
#[derive(PartialEq)]
pub enum UpcomingEvent {
SpringEquinox,
@ -13,15 +15,25 @@ pub enum UpcomingEvent {
AutumnEquinox,
WinterSolstice,
}
*/
#[derive(Default)]
pub struct EventsPrivate {
spring_equinox: Date,
summer_solstice: Date,
autumn_equinox: Date,
winter_solstice: Date,
// next: UpcomingEvent,
next: UpcomingEvent,
}
impl Default for EventsPrivate {
fn default() -> Self {
Self {
spring_equinox: Date::new(),
summer_solstice: Date::new(),
autumn_equinox: Date::new(),
winter_solstice: Date::new(),
next: UpcomingEvent::SpringEquinox,
}
}
}
#[glib::object_subclass]
@ -39,8 +51,8 @@ glib::wrapper! {
pub struct Events(ObjectSubclass<EventsPrivate>) @extends gtk::Widget, gtk::Box, @implements gtk::Orientable;
}
impl Default for Events {
fn default() -> Self {
impl Events {
pub fn new() -> Self {
let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Horizontal);
s.set_spacing(8);
@ -52,25 +64,23 @@ impl Default for Events {
s
}
}
impl Events {
pub fn set_events(&self, events: YearlyEvents, next_event: solstices::Event) {
self.imp()
.spring_equinox
.update_date(events.spring_equinox.date_naive());
.update_date(IFC::from(events.spring_equinox.date_naive()));
self.imp()
.summer_solstice
.update_date(events.summer_solstice.date_naive());
.update_date(IFC::from(events.summer_solstice.date_naive()));
self.imp()
.autumn_equinox
.update_date(events.autumn_equinox.date_naive());
.update_date(IFC::from(events.autumn_equinox.date_naive()));
self.imp()
.winter_solstice
.update_date(events.winter_solstice.date_naive());
.update_date(IFC::from(events.winter_solstice.date_naive()));
self.imp().spring_equinox.remove_css_class("highlight");
self.imp().summer_solstice.remove_css_class("highlight");

View File

@ -1,5 +1,6 @@
use crate::soluna_client::SunMoon;
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use gtk::{prelude::*, subclass::prelude::*, IconLookupFlags};
#[derive(Default)]
pub struct LabelPrivate {

View File

@ -1,6 +1,6 @@
use crate::{components::Label, soluna_client::SunMoon};
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use gtk::{prelude::*, subclass::prelude::*, IconLookupFlags};
pub struct TransitCardPrivate {
sunrise: Label,
@ -35,8 +35,8 @@ glib::wrapper! {
pub struct TransitCard(ObjectSubclass<TransitCardPrivate>) @extends gtk::Grid, gtk::Widget;
}
impl Default for TransitCard {
fn default() -> Self {
impl TransitCard {
pub fn new() -> Self {
let s: Self = Object::builder().build();
s.add_css_class("card");
s.set_column_homogeneous(true);
@ -48,9 +48,7 @@ impl Default for TransitCard {
s
}
}
impl TransitCard {
pub fn update_transit(&self, transit_info: &SunMoon) {
self.imp()
.sunrise

View File

@ -7,11 +7,18 @@ use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use std::{cell::RefCell, f64::consts::PI, rc::Rc};
#[derive(Default)]
pub struct TransitClockPrivate {
info: Rc<RefCell<Option<SunMoon>>>,
}
impl Default for TransitClockPrivate {
fn default() -> Self {
Self {
info: Rc::new(RefCell::new(None)),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for TransitClockPrivate {
const NAME: &'static str = "TransitClock";
@ -27,8 +34,8 @@ glib::wrapper! {
pub struct TransitClock(ObjectSubclass<TransitClockPrivate>) @extends gtk::DrawingArea, gtk::Widget;
}
impl Default for TransitClock {
fn default() -> Self {
impl TransitClock {
pub fn new() -> Self {
let s: Self = Object::builder().build();
s.set_width_request(500);
s.set_height_request(500);
@ -93,9 +100,7 @@ impl Default for TransitClock {
s
}
}
impl TransitClock {
pub fn update_transit(&self, transit_info: SunMoon) {
*self.imp().info.borrow_mut() = Some(transit_info);
self.queue_draw();

View File

@ -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::{
env,
sync::{Arc, RwLock},
};
use async_std::channel::Sender;
use chrono::{Datelike, Local, Utc};
use geo_types::{Latitude, Longitude};
use gtk::prelude::*;
mod app_window;
use app_window::ApplicationWindow;
@ -90,7 +90,7 @@ pub fn main() {
tx: Arc::new(RwLock::new(None)),
};
runtime.spawn({
let _ = runtime.spawn({
let core = core.clone();
async move {
let soluna_client = SolunaClient::new();
@ -102,39 +102,40 @@ pub fn main() {
let now = Local::now();
let state = State {
date: now.date_naive(),
date: IFC::from(now.date_naive().with_year(12023).unwrap()),
next_event: EVENTS.next_event(now.with_timezone(&Utc)).unwrap(),
events: EVENTS.yearly_events(now.year()).unwrap(),
transit: Some(transit),
};
let gtk_tx = core.tx.read().unwrap().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));
} else {
std::thread::sleep(std::time::Duration::from_secs(1));
if let Some(ref gtk_tx) = *core.tx.read().unwrap() {
let _ = gtk_tx.send(Message::Refresh(state.clone()));
}
std::thread::sleep(std::time::Duration::from_secs(60));
}
}
});
app.connect_activate(move |app| {
let (gtk_tx, gtk_rx) = async_std::channel::unbounded();
let (gtk_tx, gtk_rx) =
gtk::glib::MainContext::channel::<Message>(gtk::glib::PRIORITY_DEFAULT);
*core.tx.write().unwrap() = Some(gtk_tx);
let window = ApplicationWindow::new(app);
window.window.present();
glib::spawn_future_local(async move {
loop {
let Message::Refresh(state) = gtk_rx.recv().await.unwrap();
window.update_state(state);
gtk_rx.attach(None, {
let window = window.clone();
move |msg| {
let Message::Refresh(state) = msg;
ApplicationWindow::update_state(&window, state);
Continue(true)
}
});
std::thread::spawn(move || {});
});
let args: Vec<String> = env::args().collect();

View File

@ -1,11 +1,11 @@
use std::collections::HashMap;
use chrono;
use chrono::prelude::*;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
// http://astropixels.com/ephemeris/soleq2001.html
const SOLSTICE_TEXT: &str = "
const SOLSTICE_TEXT: &'static str = "
2001 Mar 20 13:31 Jun 21 07:38 Sep 22 23:05 Dec 21 19:22
2002 Mar 20 19:16 Jun 21 13:25 Sep 23 04:56 Dec 22 01:15
2003 Mar 21 01:00 Jun 21 19:11 Sep 23 10:47 Dec 22 07:04
@ -91,14 +91,12 @@ impl Event {
}
fn parse_time<'a>(
year: &str,
jaro: &str,
iter: impl Iterator<Item = &'a str>,
) -> chrono::DateTime<chrono::Utc> {
let parts = iter.collect::<Vec<&str>>();
let p = format!("{} {} {} {}", year, parts[0], parts[1], parts[2]);
NaiveDateTime::parse_from_str(&p, "%Y %b %d %H:%M")
.unwrap()
.and_utc()
let partoj = iter.collect::<Vec<&str>>();
let p = format!("{} {} {} {}", jaro, partoj[0], partoj[1], partoj[2]);
chrono::Utc.datetime_from_str(&p, "%Y %b %d %H:%M").unwrap()
}
fn parse_line(year: &str, rest: &[&str]) -> YearlyEvents {
@ -120,7 +118,7 @@ fn parse_events() -> Vec<Option<YearlyEvents>> {
.lines()
.map(|line| {
match line
.split(' ')
.split(" ")
.filter(|elem| !elem.is_empty())
.collect::<Vec<&str>>()
.as_slice()
@ -136,7 +134,7 @@ pub struct Solstices(HashMap<i32, YearlyEvents>);
impl Solstices {
pub fn yearly_events(&self, year: i32) -> Option<YearlyEvents> {
self.0.get(&year).copied()
self.0.get(&year).map(|c| c.clone())
}
pub fn next_event(&self, date: chrono::DateTime<chrono::Utc>) -> Option<Event> {
@ -144,17 +142,17 @@ impl Solstices {
match year_events {
Some(year_events) => {
if date <= year_events.spring_equinox {
Some(Event::SpringEquinox(year_events.spring_equinox))
Some(Event::SpringEquinox(year_events.spring_equinox.clone()))
} else if date <= year_events.summer_solstice {
Some(Event::SummerSolstice(year_events.summer_solstice))
Some(Event::SummerSolstice(year_events.summer_solstice.clone()))
} else if date <= year_events.autumn_equinox {
Some(Event::AutumnEquinox(year_events.autumn_equinox))
Some(Event::AutumnEquinox(year_events.autumn_equinox.clone()))
} else if date <= year_events.winter_solstice {
Some(Event::WinterSolstice(year_events.winter_solstice))
Some(Event::WinterSolstice(year_events.winter_solstice.clone()))
} else {
self.0
.get(&(date.year() + 1))
.map(|_| Event::SpringEquinox(year_events.spring_equinox))
.map(|_| Event::SpringEquinox(year_events.spring_equinox.clone()))
}
}
None => None,
@ -167,7 +165,7 @@ impl From<Vec<Option<YearlyEvents>>> for Solstices {
Solstices(event_list.iter().fold(HashMap::new(), |mut m, record| {
match record {
Some(record) => {
m.insert(record.year, *record);
m.insert(record.year, record.clone());
}
None => (),
}
@ -179,24 +177,3 @@ impl From<Vec<Option<YearlyEvents>>> for Solstices {
lazy_static! {
pub static ref EVENTS: Solstices = Solstices::from(parse_events());
}
#[cfg(test)]
mod test {
use chrono::{NaiveDate, NaiveDateTime};
#[test]
fn it_can_parse_a_solstice_time() {
let p = "2001 Mar 20 13:31".to_owned();
let parsed_date = NaiveDateTime::parse_from_str(&p, "%Y %b %d %H:%M")
.unwrap()
.and_utc();
assert_eq!(
parsed_date,
NaiveDate::from_ymd_opt(2001, 03, 20)
.unwrap()
.and_hms_opt(13, 31, 0)
.unwrap()
.and_utc()
);
}
}

View File

@ -4,6 +4,7 @@
use chrono::{DateTime, Duration, Local, NaiveTime, Offset, TimeZone, Timelike, Utc};
use geo_types::{Latitude, Longitude};
use memorycache::MemoryCache;
use reqwest;
use serde::Deserialize;
const ENDPOINT: &str = "https://api.solunar.org/solunar";
@ -25,8 +26,8 @@ impl SunMoon {
let sunrise = parse_time(val.sunrise).unwrap();
let sunset = parse_time(val.sunset).unwrap();
let moonrise = val.moonrise.and_then(parse_time);
let moonset = val.moonset.and_then(parse_time);
let moonrise = val.moonrise.and_then(|v| parse_time(v));
let moonset = val.moonset.and_then(|v| parse_time(v));
Self {
sunrise,
@ -81,7 +82,7 @@ impl SolunaClient {
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
memory_cache: MemoryCache::default(),
memory_cache: MemoryCache::new(),
}
}
@ -109,7 +110,7 @@ impl SolunaClient {
.get(reqwest::header::EXPIRES)
.and_then(|header| header.to_str().ok())
.and_then(|expiration| DateTime::parse_from_rfc2822(expiration).ok())
.map(DateTime::<Utc>::from)
.map(|dt_local| DateTime::<Utc>::from(dt_local))
.unwrap_or(
Local::now()
.with_hour(0)

View File

@ -2,11 +2,11 @@ use crate::{
solstices::{Event, YearlyEvents},
soluna_client::SunMoon,
};
use chrono::NaiveDate;
use ifc::IFC;
#[derive(Clone, Debug)]
pub struct State {
pub date: NaiveDate,
pub date: IFC,
pub next_event: Event,
pub events: YearlyEvents,
pub transit: Option<SunMoon>,

View File

@ -10,6 +10,7 @@ Luminescent Dreams Tools is distributed in the hope that it will be useful, but
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/
use date_time_tz::DateTimeTz;
use types::{Recordable, Timestamp};
/// This trait is used for constructing queries for searching the database.

View File

@ -0,0 +1,177 @@
/*
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 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))
}
pub fn to_string(&self) -> String {
if self.0.timezone() == UTC {
self.0.to_rfc3339_opts(SecondsFormat::Secs, true)
} else {
format!(
"{} {}",
self.0
.with_timezone(&chrono_tz::Etc::UTC)
.to_rfc3339_opts(SecondsFormat::Secs, true,),
self.0.timezone().name()
)
}
}
}
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(format!(
"string is not a parsable datetime representation"
))))
}
}
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.ymd(2019, 5, 15).and_hms(12, 0, 0));
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.ymd(2019, 5, 15).and_hms(12, 0, 0)));
}
#[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.ymd(2019, 5, 15).and_hms(18, 0, 0)));
}
#[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.ymd(2019, 6, 15).and_hms(19, 0, 0)));
assert_eq!(t, DateTimeTz(Arizona.ymd(2019, 6, 15).and_hms(12, 0, 0)));
assert_eq!(t, DateTimeTz(Central.ymd(2019, 6, 15).and_hms(14, 0, 0)));
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.ymd(2019, 6, 15).and_hms(12, 0, 0)));
}
}

View File

@ -71,9 +71,11 @@ extern crate thiserror;
extern crate uuid;
mod criteria;
mod date_time_tz;
mod series;
mod types;
pub use criteria::*;
pub use date_time_tz::DateTimeTz;
pub use series::Series;
pub use types::{EmseriesReadError, EmseriesWriteError, Record, RecordId, Recordable, Timestamp};
pub use types::{EmseriesReadError, EmseriesWriteError, Recordable, Timestamp, UniqueId};

View File

@ -18,51 +18,13 @@ use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::{BufRead, BufReader, LineWriter, Write};
use std::iter::Iterator;
use criteria::Criteria;
use types::{EmseriesReadError, EmseriesWriteError, Record, RecordId, Recordable};
// A RecordOnDisk, a private data structure, is useful for handling all of the on-disk
// representations of a record. Unlike [Record], this one can accept an empty data value to
// represent that the data may have been deleted. This is not made public because, so far as the
// user is concerned, any record in the system must have data associated with it.
#[derive(Clone, Deserialize, Serialize)]
struct RecordOnDisk<T: Clone + Recordable> {
id: RecordId,
data: Option<T>,
}
/*
impl<T> FromStr for RecordOnDisk<T>
where
T: Clone + Recordable + DeserializeOwned + Serialize,
{
type Err = EmseriesReadError;
fn from_str(line: &str) -> Result<Self, Self::Err> {
serde_json::from_str(line).map_err(EmseriesReadError::JSONParseError)
}
}
*/
impl<T: Clone + Recordable> TryFrom<RecordOnDisk<T>> for Record<T> {
type Error = EmseriesReadError;
fn try_from(disk_record: RecordOnDisk<T>) -> Result<Self, Self::Error> {
match disk_record.data {
Some(data) => Ok(Record {
id: disk_record.id,
data,
}),
None => Err(Self::Error::RecordDeleted(disk_record.id)),
}
}
}
use types::{EmseriesReadError, EmseriesWriteError, Record, Recordable, UniqueId};
/// An open time series database.
///
@ -71,7 +33,7 @@ impl<T: Clone + Recordable> TryFrom<RecordOnDisk<T>> for Record<T> {
pub struct Series<T: Clone + Recordable + DeserializeOwned + Serialize> {
//path: String,
writer: LineWriter<File>,
records: HashMap<RecordId, Record<T>>,
records: HashMap<UniqueId, T>,
}
impl<T> Series<T>
@ -80,12 +42,12 @@ where
{
/// Open a time series database at the specified path. `path` is the full path and filename for
/// the database.
pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Series<T>, EmseriesReadError> {
pub fn open(path: &str) -> Result<Series<T>, EmseriesReadError> {
let f = OpenOptions::new()
.read(true)
.append(true)
.create(true)
.open(path)
.open(&path)
.map_err(EmseriesReadError::IOError)?;
let records = Series::load_file(&f)?;
@ -100,18 +62,20 @@ where
}
/// Load a file and return all of the records in it.
fn load_file(f: &File) -> Result<HashMap<RecordId, Record<T>>, EmseriesReadError> {
let mut records: HashMap<RecordId, Record<T>> = HashMap::new();
fn load_file(f: &File) -> Result<HashMap<UniqueId, T>, EmseriesReadError> {
let mut records: HashMap<UniqueId, T> = HashMap::new();
let reader = BufReader::new(f);
for line in reader.lines() {
match line {
Ok(line_) => {
match serde_json::from_str::<RecordOnDisk<T>>(line_.as_ref())
.map_err(EmseriesReadError::JSONParseError)
.and_then(Record::try_from)
{
Ok(record) => records.insert(record.id, record.clone()),
Err(EmseriesReadError::RecordDeleted(id)) => records.remove(&id),
/* Can't create a JSONParseError because I can't actually create the underlying error.
fail_point!("parse-line", Err(Error::JSONParseError()))
*/
match line_.parse::<Record<T>>() {
Ok(record) => match record.data {
Some(val) => records.insert(record.id.clone(), val),
None => records.remove(&record.id.clone()),
},
Err(err) => return Err(err),
};
}
@ -123,20 +87,18 @@ where
/// Put a new record into the database. A unique id will be assigned to the record and
/// returned.
pub fn put(&mut self, entry: T) -> Result<RecordId, EmseriesWriteError> {
let id = RecordId::default();
let record = Record { id, data: entry };
self.update(record)?;
Ok(id)
pub fn put(&mut self, entry: T) -> Result<UniqueId, EmseriesWriteError> {
let uuid = UniqueId::new();
self.update(uuid.clone(), entry).and_then(|_| Ok(uuid))
}
/// Update an existing record. The [RecordId] of the record passed into this function must match
/// the [RecordId] of a record already in the database.
pub fn update(&mut self, record: Record<T>) -> Result<(), EmseriesWriteError> {
self.records.insert(record.id, record.clone());
let write_res = match serde_json::to_string(&RecordOnDisk {
id: record.id,
data: Some(record.data),
/// Update an existing record. The `UniqueId` of the record passed into this function must match
/// the `UniqueId` of a record already in the database.
pub fn update(&mut self, uuid: UniqueId, entry: T) -> Result<(), EmseriesWriteError> {
self.records.insert(uuid.clone(), entry.clone());
let write_res = match serde_json::to_string(&Record {
id: uuid,
data: Some(entry),
}) {
Ok(rec_str) => self
.writer
@ -156,14 +118,14 @@ where
/// Future note: while this deletes a record from the view, it only adds an entry to the
/// database that indicates `data: null`. If record histories ever become important, the record
/// and its entire history (including this delete) will still be available.
pub fn delete(&mut self, uuid: &RecordId) -> Result<(), EmseriesWriteError> {
pub fn delete(&mut self, uuid: &UniqueId) -> Result<(), EmseriesWriteError> {
if !self.records.contains_key(uuid) {
return Ok(());
};
self.records.remove(uuid);
let rec: RecordOnDisk<T> = RecordOnDisk {
id: *uuid,
let rec: Record<T> = Record {
id: uuid.clone(),
data: None,
};
match serde_json::to_string(&rec) {
@ -176,8 +138,8 @@ where
}
/// Get all of the records in the database.
pub fn records(&self) -> impl Iterator<Item = &Record<T>> {
self.records.values()
pub fn records<'s>(&'s self) -> impl Iterator<Item = (&'s UniqueId, &'s T)> + 's {
self.records.iter()
}
/* The point of having Search is so that a lot of internal optimizations can happen once the
@ -186,29 +148,29 @@ where
pub fn search<'s>(
&'s self,
criteria: impl Criteria + 's,
) -> impl Iterator<Item = &'s Record<T>> + 's {
self.records().filter(move |&tr| criteria.apply(&tr.data))
) -> impl Iterator<Item = (&'s UniqueId, &'s T)> + 's {
self.records().filter(move |&tr| criteria.apply(tr.1))
}
/// Perform a search and sort the resulting records based on the comparison.
pub fn search_sorted<'s, C, CMP>(&'s self, criteria: C, compare: CMP) -> Vec<&'s Record<T>>
pub fn search_sorted<'s, C, CMP>(&'s self, criteria: C, compare: CMP) -> Vec<(&UniqueId, &T)>
where
C: Criteria + 's,
CMP: FnMut(&&Record<T>, &&Record<T>) -> Ordering,
CMP: FnMut(&(&UniqueId, &T), &(&UniqueId, &T)) -> Ordering,
{
let search_iter = self.search(criteria);
let mut records: Vec<&Record<T>> = search_iter.collect();
let mut records: Vec<(&UniqueId, &T)> = search_iter.collect();
records.sort_by(compare);
records
}
/// Get an exact record from the database based on unique id.
pub fn get(&self, uuid: &RecordId) -> Option<Record<T>> {
self.records.get(uuid).cloned()
pub fn get(&self, uuid: &UniqueId) -> Option<T> {
self.records.get(uuid).map(|v| v.clone())
}
/*
pub fn remove(&self, uuid: RecordId) -> Result<(), EmseriesError> {
pub fn remove(&self, uuid: UniqueId) -> Result<(), EmseriesError> {
unimplemented!()
}
*/

View File

@ -10,7 +10,10 @@ Luminescent Dreams Tools is distributed in the hope that it will be useful, but
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/
use chrono::{DateTime, FixedOffset, NaiveDate};
use chrono::NaiveDate;
use date_time_tz::DateTimeTz;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use std::{cmp::Ordering, fmt, io, str};
use thiserror::Error;
use uuid::Uuid;
@ -25,9 +28,6 @@ pub enum EmseriesReadError {
#[error("Error parsing JSON: {0}")]
JSONParseError(serde_json::error::Error),
#[error("Record was deleted")]
RecordDeleted(RecordId),
/// Indicates a general IO error
#[error("IO Error: {0}")]
IOError(io::Error),
@ -44,49 +44,19 @@ pub enum EmseriesWriteError {
JSONWriteError(serde_json::error::Error),
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// A Timestamp, stored with reference to human reckoning. This could be either a Naive Date or a
/// date and a time with a timezone. The idea of the "human reckoning" is that, no matter what
/// timezone the record was created in, we want to group things based on the date that the human
/// was perceiving at the time it was recorded.
pub enum Timestamp {
DateTime(DateTime<FixedOffset>),
Date(NaiveDate),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum TimestampJS {
DateTime(String),
Date(String),
}
impl From<Timestamp> for TimestampJS {
fn from(s: Timestamp) -> TimestampJS {
match s {
Timestamp::DateTime(ts) => TimestampJS::DateTime(ts.to_rfc3339()),
Timestamp::Date(ts) => TimestampJS::Date(ts.to_string()),
}
}
}
impl From<TimestampJS> for Timestamp {
fn from(s: TimestampJS) -> Timestamp {
match s {
TimestampJS::DateTime(ts) => {
Timestamp::DateTime(DateTime::parse_from_rfc3339(&ts).unwrap())
}
TimestampJS::Date(ts) => Timestamp::Date(ts.parse::<NaiveDate>().unwrap()),
}
}
pub enum Timestamp {
DateTime(DateTimeTz),
Date(NaiveDate),
}
impl str::FromStr for Timestamp {
type Err = chrono::ParseError;
fn from_str(line: &str) -> Result<Self, Self::Err> {
DateTime::parse_from_rfc3339(line)
.map(Timestamp::DateTime)
.or(NaiveDate::from_str(line).map(Timestamp::Date))
DateTimeTz::from_str(line)
.map(|dtz| Timestamp::DateTime(dtz))
.or(NaiveDate::from_str(line).map(|d| Timestamp::Date(d)))
}
}
@ -100,13 +70,25 @@ impl Ord for Timestamp {
fn cmp(&self, other: &Timestamp) -> Ordering {
match (self, other) {
(Timestamp::DateTime(dt1), Timestamp::DateTime(dt2)) => dt1.cmp(dt2),
(Timestamp::DateTime(dt1), Timestamp::Date(dt2)) => dt1.date_naive().cmp(dt2),
(Timestamp::Date(dt1), Timestamp::DateTime(dt2)) => dt1.cmp(&dt2.date_naive()),
(Timestamp::DateTime(dt1), Timestamp::Date(dt2)) => dt1.0.date().naive_utc().cmp(&dt2),
(Timestamp::Date(dt1), Timestamp::DateTime(dt2)) => dt1.cmp(&dt2.0.date().naive_utc()),
(Timestamp::Date(dt1), Timestamp::Date(dt2)) => dt1.cmp(dt2),
}
}
}
impl From<DateTimeTz> for Timestamp {
fn from(d: DateTimeTz) -> Self {
Self::DateTime(d)
}
}
impl From<NaiveDate> for Timestamp {
fn from(d: NaiveDate) -> Self {
Self::Date(d)
}
}
/// Any element to be put into the database needs to be Recordable. This is the common API that
/// will aid in searching and later in indexing records.
pub trait Recordable {
@ -120,88 +102,77 @@ pub trait Recordable {
/// Uniquely identifies a record.
///
/// This is a wrapper around a basic uuid with some extra convenience methods.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
pub struct RecordId(Uuid);
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
pub struct UniqueId(Uuid);
impl Default for RecordId {
fn default() -> Self {
Self(Uuid::new_v4())
impl UniqueId {
/// Create a new V4 UUID (this is the most common type in use these days).
pub fn new() -> UniqueId {
let id = Uuid::new_v4();
UniqueId(id)
}
}
impl str::FromStr for RecordId {
impl str::FromStr for UniqueId {
type Err = EmseriesReadError;
/// Parse a RecordId from a string. Raise UUIDParseError if the parsing fails.
/// Parse a UniqueId from a string. Raise UUIDParseError if the parsing fails.
fn from_str(val: &str) -> Result<Self, Self::Err> {
Uuid::parse_str(val)
.map(RecordId)
.map_err(EmseriesReadError::UUIDParseError)
.map(UniqueId)
.map_err(|err| EmseriesReadError::UUIDParseError(err))
}
}
impl fmt::Display for RecordId {
impl fmt::Display for UniqueId {
/// Convert to a hyphenated string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{}", self.0.to_hyphenated())
write!(f, "{}", self.0.to_hyphenated().to_string())
}
}
/// A record represents data that actually exists in the database. Users cannot make the record
/// directly, as the database will create them.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
/// Every record contains a unique ID and then the primary data, which itself must implementd the
/// Recordable trait.
#[derive(Clone, Deserialize, Serialize)]
pub struct Record<T: Clone + Recordable> {
pub id: RecordId,
pub data: T,
pub id: UniqueId,
pub data: Option<T>,
}
impl<T: Clone + Recordable> Record<T> {
pub fn date(&self) -> NaiveDate {
match self.data.timestamp() {
Timestamp::DateTime(dt) => dt.date_naive(),
Timestamp::Date(dt) => dt,
}
}
pub fn timestamp(&self) -> Timestamp {
self.data.timestamp()
}
pub fn map<Map, U>(self, map: Map) -> Record<U>
impl<T> str::FromStr for Record<T>
where
Map: Fn(T) -> U,
U: Clone + Recordable,
T: Clone + Recordable + DeserializeOwned + Serialize,
{
Record {
id: self.id,
data: map(self.data),
}
type Err = EmseriesReadError;
fn from_str(line: &str) -> Result<Self, Self::Err> {
serde_json::from_str(&line).map_err(|err| EmseriesReadError::JSONParseError(err))
}
}
#[cfg(test)]
mod test {
extern crate dimensioned;
extern crate serde_json;
use self::dimensioned::si::{Kilogram, KG};
use super::*;
use chrono::TimeZone;
use chrono_tz::Etc::UTC;
use chrono_tz::{Etc::UTC, US::Central};
use date_time_tz::DateTimeTz;
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct Weight(Kilogram<f64>);
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct WeightRecord {
pub date: NaiveDate,
pub date: Timestamp,
pub weight: Weight,
}
impl Recordable for WeightRecord {
fn timestamp(&self) -> Timestamp {
Timestamp::Date(self.date)
self.date.clone()
}
fn tags(&self) -> Vec<String> {
@ -210,14 +181,10 @@ mod test {
}
#[test]
fn timestamp_parses_utc_time() {
fn timestamp_parses_datetimetz_without_timezone() {
assert_eq!(
"2003-11-10T06:00:00Z".parse::<Timestamp>().unwrap(),
Timestamp::DateTime(
UTC.with_ymd_and_hms(2003, 11, 10, 6, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap())
),
Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0))),
);
}
@ -229,10 +196,9 @@ mod test {
);
}
/*
#[ignore]
#[test]
fn v_alpha_serialization() {
const WEIGHT_ENTRY: &str = "{\"data\":{\"weight\":77.79109},\"date\":\"2003-11-10\",\"id\":\"3330c5b0-783f-4919-b2c4-8169c38f65ff\"}";
const WEIGHT_ENTRY: &str = "{\"data\":{\"weight\":77.79109,\"date\":\"2003-11-10T06:00:00.000000000000Z\"},\"id\":\"3330c5b0-783f-4919-b2c4-8169c38f65ff\"}";
let rec: Record<WeightRecord> = WEIGHT_ENTRY
.parse()
@ -243,65 +209,52 @@ mod test {
);
assert_eq!(
rec.data,
WeightRecord {
date: NaiveDate::from_ymd_opt(2003, 11, 10).unwrap(),
Some(WeightRecord {
date: Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0))),
weight: Weight(77.79109 * KG),
}
})
);
}
*/
#[test]
fn serialization_output() {
let rec = WeightRecord {
date: NaiveDate::from_ymd_opt(2003, 11, 10).unwrap(),
date: Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0))),
weight: Weight(77.0 * KG),
};
assert_eq!(
serde_json::to_string(&rec).unwrap(),
"{\"date\":\"2003-11-10\",\"weight\":77.0}"
"{\"date\":\"2003-11-10T06:00:00Z\",\"weight\":77.0}"
);
let rec2 = WeightRecord {
date: NaiveDate::from_ymd_opt(2003, 11, 10).unwrap(),
date: Timestamp::DateTime(Central.ymd(2003, 11, 10).and_hms(0, 0, 0).into()),
weight: Weight(77.0 * KG),
};
assert_eq!(
serde_json::to_string(&rec2).unwrap(),
"{\"date\":\"2003-11-10\",\"weight\":77.0}"
"{\"date\":\"2003-11-10T06:00:00Z US/Central\",\"weight\":77.0}"
);
}
#[test]
fn two_datetimes_can_be_compared() {
let time1 = Timestamp::DateTime(
UTC.with_ymd_and_hms(2003, 11, 10, 6, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
);
let time2 = Timestamp::DateTime(
UTC.with_ymd_and_hms(2003, 11, 11, 6, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
);
let time1 = Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0)));
let time2 = Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 11).and_hms(6, 0, 0)));
assert!(time1 < time2);
}
#[test]
fn two_dates_can_be_compared() {
let time1: Timestamp = Timestamp::Date(NaiveDate::from_ymd_opt(2003, 11, 10).unwrap());
let time2: Timestamp = Timestamp::Date(NaiveDate::from_ymd_opt(2003, 11, 11).unwrap());
let time1 = Timestamp::Date(NaiveDate::from_ymd(2003, 11, 10));
let time2 = Timestamp::Date(NaiveDate::from_ymd(2003, 11, 11));
assert!(time1 < time2);
}
#[test]
fn datetime_and_date_can_be_compared() {
let time1 = Timestamp::DateTime(
UTC.with_ymd_and_hms(2003, 11, 10, 6, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
);
let time2 = Timestamp::Date(NaiveDate::from_ymd_opt(2003, 11, 11).unwrap());
let time1 = Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0)));
let time2 = Timestamp::Date(NaiveDate::from_ymd(2003, 11, 11));
assert!(time1 < time2)
}
}

View File

@ -20,9 +20,9 @@ extern crate emseries;
#[cfg(test)]
mod test {
use chrono::{prelude::*};
use chrono::prelude::*;
use chrono_tz::Etc::UTC;
use dimensioned::si::{Kilogram, Meter, Second, M, S};
use dimensioned::si::{Kilogram, Meter, Second, KG, M, S};
use emseries::*;
@ -34,7 +34,7 @@ mod test {
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
struct BikeTrip {
datetime: DateTime<FixedOffset>,
datetime: DateTimeTz,
distance: Distance,
duration: Duration,
comments: String,
@ -42,7 +42,7 @@ mod test {
impl Recordable for BikeTrip {
fn timestamp(&self) -> Timestamp {
Timestamp::DateTime(self.datetime)
self.datetime.clone().into()
}
fn tags(&self) -> Vec<String> {
Vec::new()
@ -52,46 +52,31 @@ mod test {
fn mk_trips() -> [BikeTrip; 5] {
[
BikeTrip {
datetime: UTC
.with_ymd_and_hms(2011, 10, 29, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
datetime: DateTimeTz(UTC.ymd(2011, 10, 29).and_hms(0, 0, 0)),
distance: Distance(58741.055 * M),
duration: Duration(11040.0 * S),
comments: String::from("long time ago"),
},
BikeTrip {
datetime: UTC
.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
datetime: DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)),
distance: Distance(17702.0 * M),
duration: Duration(2880.0 * S),
comments: String::from("day 2"),
},
BikeTrip {
datetime: UTC
.with_ymd_and_hms(2011, 11, 02, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
datetime: DateTimeTz(UTC.ymd(2011, 11, 02).and_hms(0, 0, 0)),
distance: Distance(41842.945 * M),
duration: Duration(7020.0 * S),
comments: String::from("Do Some Distance!"),
},
BikeTrip {
datetime: UTC
.with_ymd_and_hms(2011, 11, 04, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
datetime: DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)),
distance: Distance(34600.895 * M),
duration: Duration(5580.0 * S),
comments: String::from("I did a lot of distance back then"),
},
BikeTrip {
datetime: UTC
.with_ymd_and_hms(2011, 11, 05, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
datetime: DateTimeTz(UTC.ymd(2011, 11, 05).and_hms(0, 0, 0)),
distance: Distance(6437.376 * M),
duration: Duration(960.0 * S),
comments: String::from("day 5"),
@ -99,7 +84,7 @@ mod test {
]
}
fn run_test<T>(test: T)
fn run_test<T>(test: T) -> ()
where
T: FnOnce(tempfile::TempPath),
{
@ -108,14 +93,14 @@ mod test {
test(tmp_path);
}
fn run<T>(test: T)
fn run<T>(test: T) -> ()
where
T: FnOnce(Series<BikeTrip>),
{
let tmp_file = tempfile::NamedTempFile::new().expect("temporary path created");
let tmp_path = tmp_file.into_temp_path();
let ts: Series<BikeTrip> =
Series::open(&tmp_path).expect("the time series should open correctly");
let ts: Series<BikeTrip> = Series::open(&tmp_path.to_string_lossy())
.expect("the time series should open correctly");
test(ts);
}
@ -137,15 +122,11 @@ mod test {
Some(tr) => {
assert_eq!(
tr.timestamp(),
Timestamp::DateTime(
UTC.with_ymd_and_hms(2011, 10, 29, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap())
)
DateTimeTz(UTC.ymd(2011, 10, 29).and_hms(0, 0, 0)).into()
);
assert_eq!(tr.data.duration, Duration(11040.0 * S));
assert_eq!(tr.data.comments, String::from("long time ago"));
assert_eq!(tr.data, trips[0]);
assert_eq!(tr.duration, Duration(11040.0 * S));
assert_eq!(tr.comments, String::from("long time ago"));
assert_eq!(tr, trips[0]);
}
}
})
@ -155,22 +136,20 @@ mod test {
pub fn can_search_for_an_entry_with_exact_time() {
run_test(|path| {
let trips = mk_trips();
let mut ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
for trip in &trips[0..=4] {
ts.put(trip.clone()).expect("expect a successful put");
}
let v: Vec<&Record<BikeTrip>> = ts
.search(exact_time(Timestamp::DateTime(
UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
)))
let v: Vec<(&UniqueId, &BikeTrip)> = ts
.search(exact_time(
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)).into(),
))
.collect();
assert_eq!(v.len(), 1);
assert_eq!(v[0].data, trips[1]);
assert_eq!(*v[0].1, trips[1]);
})
}
@ -178,34 +157,26 @@ mod test {
pub fn can_get_entries_in_time_range() {
run_test(|path| {
let trips = mk_trips();
let mut ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
for trip in &trips[0..=4] {
ts.put(trip.clone()).expect("expect a successful put");
}
let v: Vec<&Record<BikeTrip>> = ts.search_sorted(
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
time_range(
Timestamp::DateTime(
UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
),
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)).into(),
true,
Timestamp::DateTime(
UTC.with_ymd_and_hms(2011, 11, 04, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
),
DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)).into(),
true,
),
|l, r| l.timestamp().cmp(&r.timestamp()),
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
);
assert_eq!(v.len(), 3);
assert_eq!(v[0].data, trips[1]);
assert_eq!(v[1].data, trips[2]);
assert_eq!(v[2].data, trips[3]);
assert_eq!(*v[0].1, trips[1]);
assert_eq!(*v[1].1, trips[2]);
assert_eq!(*v[2].1, trips[3]);
})
}
@ -215,8 +186,8 @@ mod test {
let trips = mk_trips();
{
let mut ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
for trip in &trips[0..=4] {
ts.put(trip.clone()).expect("expect a successful put");
@ -224,29 +195,21 @@ mod test {
}
{
let ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let v: Vec<&Record<BikeTrip>> = ts.search_sorted(
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
time_range(
Timestamp::DateTime(
UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
),
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)).into(),
true,
Timestamp::DateTime(
UTC.with_ymd_and_hms(2011, 11, 04, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
),
DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)).into(),
true,
),
|l, r| l.timestamp().cmp(&r.timestamp()),
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
);
assert_eq!(v.len(), 3);
assert_eq!(v[0].data, trips[1]);
assert_eq!(v[1].data, trips[2]);
assert_eq!(v[2].data, trips[3]);
assert_eq!(*v[0].1, trips[1]);
assert_eq!(*v[1].1, trips[2]);
assert_eq!(*v[2].1, trips[3]);
}
})
}
@ -257,8 +220,8 @@ mod test {
let trips = mk_trips();
{
let mut ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
for trip in &trips[0..=2] {
ts.put(trip.clone()).expect("expect a successful put");
@ -266,57 +229,41 @@ mod test {
}
{
let mut ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let v: Vec<&Record<BikeTrip>> = ts.search_sorted(
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
time_range(
Timestamp::DateTime(
UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
),
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)).into(),
true,
Timestamp::DateTime(
UTC.with_ymd_and_hms(2011, 11, 04, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
),
DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)).into(),
true,
),
|l, r| l.timestamp().cmp(&r.timestamp()),
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
);
assert_eq!(v.len(), 2);
assert_eq!(v[0].data, trips[1]);
assert_eq!(v[1].data, trips[2]);
assert_eq!(*v[0].1, trips[1]);
assert_eq!(*v[1].1, trips[2]);
ts.put(trips[3].clone()).expect("expect a successful put");
ts.put(trips[4].clone()).expect("expect a successful put");
}
{
let ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let v: Vec<&Record<BikeTrip>> = ts.search_sorted(
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
time_range(
Timestamp::DateTime(
UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
),
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)).into(),
true,
Timestamp::DateTime(
UTC.with_ymd_and_hms(2011, 11, 05, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
),
DateTimeTz(UTC.ymd(2011, 11, 05).and_hms(0, 0, 0)).into(),
true,
),
|l, r| l.timestamp().cmp(&r.timestamp()),
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
);
assert_eq!(v.len(), 4);
assert_eq!(v[0].data, trips[1]);
assert_eq!(v[1].data, trips[2]);
assert_eq!(v[2].data, trips[3]);
assert_eq!(v[3].data, trips[4]);
assert_eq!(*v[0].1, trips[1]);
assert_eq!(*v[1].1, trips[2]);
assert_eq!(*v[2].1, trips[3]);
assert_eq!(*v[3].1, trips[4]);
}
})
}
@ -326,8 +273,8 @@ mod test {
run_test(|path| {
let trips = mk_trips();
let mut ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
ts.put(trips[0].clone()).expect("expect a successful put");
ts.put(trips[1].clone()).expect("expect a successful put");
@ -336,8 +283,9 @@ mod test {
match ts.get(&trip_id) {
None => assert!(false, "record not found"),
Some(mut trip) => {
trip.data.distance = Distance(50000.0 * M);
ts.update(trip).expect("expect record to update");
trip.distance = Distance(50000.0 * M);
ts.update(trip_id.clone(), trip)
.expect("expect record to update");
}
};
@ -345,12 +293,12 @@ mod test {
None => assert!(false, "record not found"),
Some(trip) => {
assert_eq!(
trip.data.datetime,
UTC.with_ymd_and_hms(2011, 11, 02, 0, 0, 0).unwrap()
trip.datetime,
DateTimeTz(UTC.ymd(2011, 11, 02).and_hms(0, 0, 0))
);
assert_eq!(trip.data.distance, Distance(50000.0 * M));
assert_eq!(trip.data.duration, Duration(7020.0 * S));
assert_eq!(trip.data.comments, String::from("Do Some Distance!"));
assert_eq!(trip.distance, Distance(50000.0 * M));
assert_eq!(trip.duration, Duration(7020.0 * S));
assert_eq!(trip.comments, String::from("Do Some Distance!"));
}
}
})
@ -362,8 +310,8 @@ mod test {
let trips = mk_trips();
{
let mut ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
ts.put(trips[0].clone()).expect("expect a successful put");
ts.put(trips[1].clone()).expect("expect a successful put");
@ -372,36 +320,32 @@ mod test {
match ts.get(&trip_id) {
None => assert!(false, "record not found"),
Some(mut trip) => {
trip.data.distance = Distance(50000.0 * M);
ts.update(trip).expect("expect record to update");
trip.distance = Distance(50000.0 * M);
ts.update(trip_id, trip).expect("expect record to update");
}
};
}
{
let ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
let trips: Vec<&Record<BikeTrip>> = ts.records().collect();
let trips: Vec<(&UniqueId, &BikeTrip)> = ts.records().collect();
assert_eq!(trips.len(), 3);
let trips: Vec<&Record<BikeTrip>> = ts
.search(exact_time(Timestamp::DateTime(
UTC.with_ymd_and_hms(2011, 11, 02, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
)))
let trips: Vec<(&UniqueId, &BikeTrip)> = ts
.search(exact_time(
DateTimeTz(UTC.ymd(2011, 11, 02).and_hms(0, 0, 0)).into(),
))
.collect();
assert_eq!(trips.len(), 1);
assert_eq!(
trips[0].data.datetime,
UTC.with_ymd_and_hms(2011, 11, 02, 0, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap())
trips[0].1.datetime,
DateTimeTz(UTC.ymd(2011, 11, 02).and_hms(0, 0, 0))
);
assert_eq!(trips[0].data.distance, Distance(50000.0 * M));
assert_eq!(trips[0].data.duration, Duration(7020.0 * S));
assert_eq!(trips[0].data.comments, String::from("Do Some Distance!"));
assert_eq!(trips[0].1.distance, Distance(50000.0 * M));
assert_eq!(trips[0].1.duration, Duration(7020.0 * S));
assert_eq!(trips[0].1.comments, String::from("Do Some Distance!"));
}
})
}
@ -412,21 +356,22 @@ mod test {
let trips = mk_trips();
{
let mut ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
let trip_id = ts.put(trips[0].clone()).expect("expect a successful put");
ts.put(trips[1].clone()).expect("expect a successful put");
ts.put(trips[2].clone()).expect("expect a successful put");
ts.delete(&trip_id).expect("successful delete");
let recs: Vec<&Record<BikeTrip>> = ts.records().collect();
let recs: Vec<(&UniqueId, &BikeTrip)> = ts.records().collect();
assert_eq!(recs.len(), 2);
}
{
let ts: Series<BikeTrip> =
Series::open(&path).expect("expect the time series to open correctly");
let recs: Vec<&Record<BikeTrip>> = ts.records().collect();
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
.expect("expect the time series to open correctly");
let recs: Vec<(&UniqueId, &BikeTrip)> = ts.records().collect();
assert_eq!(recs.len(), 2);
}
})
@ -443,7 +388,7 @@ mod test {
impl Recordable for WeightRecord {
fn timestamp(&self) -> Timestamp {
Timestamp::Date(self.date)
self.date.into()
}
fn tags(&self) -> Vec<String> {

View File

@ -1 +0,0 @@
fixtures

View File

@ -1,2 +0,0 @@
fixtures
var

View File

@ -1,47 +0,0 @@
[package]
name = "file-service"
version = "0.2.0"
authors = ["savanni@luminescent-dreams.com"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "file_service"
path = "src/lib.rs"
[[bin]]
name = "file-service"
path = "src/main.rs"
[target.auth-cli.dependencies]
[dependencies]
authdb = { path = "../authdb/" }
base64ct = { version = "1", features = [ "alloc" ] }
build_html = { version = "2" }
bytes = { version = "1" }
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = [ "derive" ] }
cookie = { version = "0.17" }
futures-util = { version = "0.3" }
hex-string = "0.1.0"
http = { version = "0.2" }
image = "0.23.5"
logger = "*"
log = { version = "0.4" }
mime = "0.3.16"
mime_guess = "2.0.3"
pretty_env_logger = { version = "0.5" }
serde_json = "*"
serde = { version = "1.0", features = ["derive"] }
sha2 = { version = "0.10" }
thiserror = { version = "1" }
tokio = { version = "1", features = [ "full" ] }
uuid = { version = "0.4", features = [ "serde", "v4" ] }
warp = { version = "0.3" }
[dev-dependencies]
cool_asserts = { version = "2" }
tempdir = { version = "0.3" }

View File

@ -1 +0,0 @@
[{"jti":"ac3a46c6-3fa1-4d0a-af12-e7d3fefdc878","aud":"savanni","exp":1621351436,"iss":"savanni","iat":1589729036,"sub":"https://savanni.luminescent-dreams.com/file-service/","perms":["admin"]}]

View File

@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION=`cat Cargo.toml | grep "^version =" | sed -r 's/^version = "(.+)"$/\1/'`
mkdir -p dist
cp ../target/release/file-service dist
cp ../target/release/auth-cli dist
strip dist/file-service
strip dist/auth-cli
tar -czf file-service-${VERSION}.tgz dist/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1,279 +0,0 @@
use build_html::Html;
use bytes::Buf;
use file_service::WriteFileError;
use futures_util::StreamExt;
use http::{Error, StatusCode};
use std::collections::HashMap;
use std::io::Read;
use warp::{filters::multipart::FormData, http::Response, multipart::Part};
use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken};
const CSS: &str = include_str!("../templates/style.css");
pub async fn handle_index(
app: App,
token: Option<SessionToken>,
) -> Result<Response<String>, Error> {
match token {
Some(token) => match app.validate_session(token).await {
Ok(_) => render_gallery_page(app).await,
Err(err) => render_auth_page(Some(format!("session expired: {:?}", err))),
},
None => render_auth_page(None),
}
}
pub async fn handle_css() -> Result<Response<String>, Error> {
Response::builder()
.header("content-type", "text/css")
.status(StatusCode::OK)
.body(CSS.to_owned())
}
pub fn render_auth_page(message: Option<String>) -> Result<Response<String>, Error> {
Response::builder()
.status(StatusCode::OK)
.body(pages::auth(message).to_html_string())
}
pub async fn render_gallery_page(app: App) -> Result<Response<String>, Error> {
match app.list_files().await {
Ok(ids) => {
let mut files = vec![];
for id in ids.into_iter() {
let file = app.get_file(&id).await;
files.push(file);
}
Response::builder()
.header("content-type", "text/html")
.status(StatusCode::OK)
.body(pages::gallery(files).to_html_string())
}
Err(_) => Response::builder()
.header("content-type", "text/html")
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body("".to_owned()),
}
}
pub async fn thumbnail(
app: App,
id: String,
old_etags: Option<String>,
) -> Result<Response<Vec<u8>>, Error> {
match app.get_file(&FileId::from(id)).await {
Ok(file) => serve_file(file.info.clone(), || file.thumbnail(), old_etags),
Err(_err) => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(vec![]),
}
}
pub async fn file(
app: App,
id: String,
old_etags: Option<String>,
) -> Result<Response<Vec<u8>>, Error> {
match app.get_file(&FileId::from(id)).await {
Ok(file) => serve_file(file.info.clone(), || file.content(), old_etags),
Err(_err) => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(vec![]),
}
}
pub async fn handle_auth(
app: App,
form: HashMap<String, String>,
) -> Result<http::Response<String>, Error> {
match form.get("password") {
Some(token) => match app.authenticate(AuthToken::from(token.clone())).await {
Ok(Some(session_token)) => Response::builder()
.header("location", "/")
.header(
"set-cookie",
format!(
"session={}; Secure; HttpOnly; SameSite=Strict",
*session_token
),
)
.status(StatusCode::SEE_OTHER)
.body("".to_owned()),
Ok(None) => render_auth_page(Some("no user found".to_owned())),
Err(_) => render_auth_page(Some("invalid auth token".to_owned())),
},
None => render_auth_page(Some("no token available".to_owned())),
}
}
pub async fn handle_upload(
app: App,
token: SessionToken,
form: FormData,
) -> Result<http::Response<String>, Error> {
match app.validate_session(token).await {
Ok(Some(_)) => match process_file_upload(app, form).await {
Ok(_) => Response::builder()
.header("location", "/")
.status(StatusCode::SEE_OTHER)
.body("".to_owned()),
Err(UploadError::FilenameMissing) => Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("filename is required for all files".to_owned()),
Err(UploadError::WriteFileError(err)) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(format!("could not write to the file system: {:?}", err)),
Err(UploadError::WarpError(err)) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(format!("error with the app framework: {:?}", err)),
},
_ => Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body("".to_owned()),
}
}
pub async fn handle_delete(
app: App,
token: SessionToken,
id: FileId,
) -> Result<http::Response<String>, Error> {
match app.validate_session(token).await {
Ok(Some(_)) => match app.delete_file(id).await {
Ok(_) => Response::builder()
.header("location", "/")
.status(StatusCode::SEE_OTHER)
.body("".to_owned()),
Err(_) => unimplemented!(),
},
_ => Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body("".to_owned()),
}
}
fn serve_file<F>(
info: FileInfo,
file: F,
old_etags: Option<String>,
) -> http::Result<http::Response<Vec<u8>>>
where
F: FnOnce() -> Result<Vec<u8>, ReadFileError>,
{
match old_etags {
Some(old_etags) if old_etags != info.hash => Response::builder()
.header("content-type", info.file_type)
.status(StatusCode::NOT_MODIFIED)
.body(vec![]),
_ => match file() {
Ok(content) => Response::builder()
.header("content-type", info.file_type)
.header("etag", info.hash)
.status(StatusCode::OK)
.body(content),
Err(_) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(vec![]),
},
}
}
async fn collect_multipart(
mut stream: warp::filters::multipart::FormData,
) -> Result<Vec<(Option<String>, Option<String>, Vec<u8>)>, warp::Error> {
let mut content: Vec<(Option<String>, Option<String>, Vec<u8>)> = Vec::new();
while let Some(part) = stream.next().await {
match part {
Ok(part) => content.push(collect_content(part).await.unwrap()),
Err(err) => return Err(err),
}
}
Ok(content)
}
async fn collect_content(
mut part: Part,
) -> Result<(Option<String>, Option<String>, Vec<u8>), String> {
let mut content: Vec<u8> = Vec::new();
while let Some(Ok(data)) = part.data().await {
let mut reader = data.reader();
reader.read_to_end(&mut content).unwrap();
}
Ok((
part.content_type().map(|s| s.to_owned()),
part.filename().map(|s| s.to_owned()),
content,
))
}
/*
async fn handle_upload(
form: warp::filters::multipart::FormData,
app: App,
) -> warp::http::Result<warp::http::Response<String>> {
let files = collect_multipart(form).await;
match files {
Ok(files) => {
for (_, filename, content) in files {
match filename {
Some(filename) => {
app.add_file(filename, content).unwrap();
}
None => {
return warp::http::Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("".to_owned())
}
}
}
}
Err(_err) => {
return warp::http::Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("".to_owned())
}
}
// println!("file length: {:?}", files.map(|f| f.len()));
warp::http::Response::builder()
.header("location", "/")
.status(StatusCode::SEE_OTHER)
.body("".to_owned())
}
*/
enum UploadError {
FilenameMissing,
WriteFileError(WriteFileError),
WarpError(warp::Error),
}
impl From<WriteFileError> for UploadError {
fn from(err: WriteFileError) -> Self {
Self::WriteFileError(err)
}
}
impl From<warp::Error> for UploadError {
fn from(err: warp::Error) -> Self {
Self::WarpError(err)
}
}
async fn process_file_upload(app: App, form: FormData) -> Result<(), UploadError> {
let files = collect_multipart(form).await?;
for (_, filename, content) in files {
match filename {
Some(filename) => {
app.add_file(filename, content).await?;
}
None => return Err(UploadError::FilenameMissing),
}
}
Ok(())
}

View File

@ -1,208 +0,0 @@
use build_html::{self, Html, HtmlContainer};
#[derive(Clone, Debug, Default)]
pub struct Attributes(Vec<(String, String)>);
/*
impl FromIterator<(String, String)> for Attributes {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = (String, String)>,
{
Attributes(iter.collect::<Vec<(String, String)>>())
}
}
impl FromIterator<(&str, &str)> for Attributes {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = (&str, &str)>,
{
unimplemented!()
}
}
*/
impl ToString for Attributes {
fn to_string(&self) -> String {
self.0
.iter()
.map(|(key, value)| format!("{}=\"{}\"", key, value))
.collect::<Vec<String>>()
.join(" ")
}
}
#[derive(Clone, Debug)]
pub struct Form {
path: String,
method: String,
encoding: Option<String>,
elements: String,
}
impl Form {
pub fn new() -> Self {
Self {
path: "/".to_owned(),
method: "get".to_owned(),
encoding: None,
elements: "".to_owned(),
}
}
pub fn with_path(mut self, path: &str) -> Self {
self.path = path.to_owned();
self
}
pub fn with_method(mut self, method: &str) -> Self {
self.method = method.to_owned();
self
}
pub fn with_encoding(mut self, encoding: &str) -> Self {
self.encoding = Some(encoding.to_owned());
self
}
}
impl Html for Form {
fn to_html_string(&self) -> String {
let encoding = match self.encoding {
Some(ref encoding) => format!("enctype=\"{encoding}\"", encoding = encoding),
None => "".to_owned(),
};
format!(
"<form action=\"{path}\" method=\"{method}\" {encoding}>\n{elements}\n</form>\n",
path = self.path,
method = self.method,
encoding = encoding,
elements = self.elements.to_html_string(),
)
}
}
impl HtmlContainer for Form {
fn add_html<H: Html>(&mut self, html: H) {
self.elements.push_str(&html.to_html_string());
}
}
#[derive(Clone, Debug)]
pub struct Input {
ty: String,
name: String,
id: Option<String>,
value: Option<String>,
attributes: Attributes,
}
impl Html for Input {
fn to_html_string(&self) -> String {
let id = match self.id {
Some(ref id) => format!("id=\"{}\"", id),
None => "".to_owned(),
};
let value = match self.value {
Some(ref value) => format!("value=\"{}\"", value),
None => "".to_owned(),
};
let attrs = self.attributes.to_string();
format!(
"<input type=\"{ty}\" name=\"{name}\" {id} {value} {attrs} />\n",
ty = self.ty,
name = self.name,
id = id,
value = value,
attrs = attrs,
)
}
}
impl Input {
pub fn new(ty: &str, name: &str) -> Self {
Self {
ty: ty.to_owned(),
name: name.to_owned(),
id: None,
value: None,
attributes: Attributes::default(),
}
}
pub fn with_id(mut self, val: &str) -> Self {
self.id = Some(val.to_owned());
self
}
pub fn with_attributes<'a>(
mut self,
values: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> Self {
self.attributes = Attributes(
values
.into_iter()
.map(|(a, b)| (a.to_owned(), b.to_owned()))
.collect::<Vec<(String, String)>>(),
);
self
}
}
#[derive(Clone, Debug)]
pub struct Button {
ty: Option<String>,
name: Option<String>,
label: String,
attributes: Attributes,
}
impl Button {
pub fn new(label: &str) -> Self {
Self {
ty: None,
name: None,
label: label.to_owned(),
attributes: Attributes::default(),
}
}
pub fn with_type(mut self, ty: &str) -> Self {
self.ty = Some(ty.to_owned());
self
}
pub fn with_attributes<'a>(
mut self,
values: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> Self {
self.attributes = Attributes(
values
.into_iter()
.map(|(a, b)| (a.to_owned(), b.to_owned()))
.collect::<Vec<(String, String)>>(),
);
self
}
}
impl Html for Button {
fn to_html_string(&self) -> String {
let ty = match self.ty {
Some(ref ty) => format!("type={}", ty),
None => "".to_owned(),
};
let name = match self.name {
Some(ref name) => format!("name={}", name),
None => "".to_owned(),
};
format!(
"<button {ty} {name} {attrs}>{label}</button>",
name = name,
label = self.label,
attrs = self.attributes.to_string()
)
}
}

View File

@ -1,5 +0,0 @@
mod store;
pub use store::{
DeleteFileError, FileHandle, FileId, FileInfo, ReadFileError, Store, WriteFileError,
};

View File

@ -1,174 +0,0 @@
extern crate log;
use cookie::Cookie;
use handlers::{file, handle_auth, handle_css, handle_delete, handle_upload, thumbnail};
use std::{
collections::{HashMap, HashSet},
convert::Infallible,
net::{IpAddr, Ipv4Addr, SocketAddr},
path::PathBuf,
sync::Arc,
};
use tokio::sync::RwLock;
use warp::{Filter, Rejection};
mod handlers;
mod html;
mod pages;
const MAX_UPLOAD: u64 = 15 * 1024 * 1024;
use authdb::{AuthDB, AuthError, AuthToken, SessionToken, Username};
use file_service::{
DeleteFileError, FileHandle, FileId, FileInfo, ReadFileError, Store, WriteFileError,
};
pub use handlers::handle_index;
#[derive(Clone)]
pub struct App {
authdb: Arc<RwLock<AuthDB>>,
store: Arc<RwLock<Store>>,
}
impl App {
pub fn new(authdb: AuthDB, store: Store) -> Self {
Self {
authdb: Arc::new(RwLock::new(authdb)),
store: Arc::new(RwLock::new(store)),
}
}
pub async fn authenticate(&self, token: AuthToken) -> Result<Option<SessionToken>, AuthError> {
self.authdb.read().await.authenticate(token).await
}
pub async fn validate_session(
&self,
token: SessionToken,
) -> Result<Option<Username>, AuthError> {
self.authdb.read().await.validate_session(token).await
}
pub async fn list_files(&self) -> Result<HashSet<FileId>, ReadFileError> {
self.store.read().await.list_files()
}
pub async fn get_file(&self, id: &FileId) -> Result<FileHandle, ReadFileError> {
self.store.read().await.get_file(id)
}
pub async fn add_file(
&self,
filename: String,
content: Vec<u8>,
) -> Result<FileHandle, WriteFileError> {
self.store.write().await.add_file(filename, content)
}
pub async fn delete_file(&self, id: FileId) -> Result<(), DeleteFileError> {
self.store.write().await.delete_file(&id)?;
Ok(())
}
}
fn with_app(app: App) -> impl Filter<Extract = (App,), Error = Infallible> + Clone {
warp::any().map(move || app.clone())
}
fn parse_cookies(cookie_str: &str) -> Result<HashMap<String, String>, cookie::ParseError> {
Cookie::split_parse(cookie_str)
.map(|c| c.map(|c| (c.name().to_owned(), c.value().to_owned())))
.collect::<Result<HashMap<String, String>, cookie::ParseError>>()
}
fn get_session_token(cookies: HashMap<String, String>) -> Option<SessionToken> {
cookies.get("session").cloned().map(SessionToken::from)
}
fn maybe_with_session() -> impl Filter<Extract = (Option<SessionToken>,), Error = Rejection> + Copy
{
warp::any()
.and(warp::header::optional::<String>("cookie"))
.map(|cookie_str: Option<String>| match cookie_str {
Some(cookie_str) => parse_cookies(&cookie_str).ok().and_then(get_session_token),
None => None,
})
}
fn with_session() -> impl Filter<Extract = (SessionToken,), Error = Rejection> + Copy {
warp::any()
.and(warp::header::<String>("cookie"))
.and_then(|cookie_str: String| async move {
match parse_cookies(&cookie_str).ok().and_then(get_session_token) {
Some(session_token) => Ok(session_token),
None => Err(warp::reject()),
}
})
}
#[tokio::main]
pub async fn main() {
pretty_env_logger::init();
let authdb = AuthDB::new(PathBuf::from(&std::env::var("AUTHDB").unwrap()))
.await
.unwrap();
let store = Store::new(PathBuf::from(&std::env::var("FILE_SHARE_DIR").unwrap()));
let app = App::new(authdb, store);
let log = warp::log("file_service");
let root = warp::path!()
.and(warp::get())
.and(with_app(app.clone()))
.and(maybe_with_session())
.then(handle_index);
let styles = warp::path!("css").and(warp::get()).then(handle_css);
let auth = warp::path!("auth")
.and(warp::post())
.and(with_app(app.clone()))
.and(warp::filters::body::form())
.then(handle_auth);
let upload_via_form = warp::path!("upload")
.and(warp::post())
.and(with_app(app.clone()))
.and(with_session())
.and(warp::multipart::form().max_length(MAX_UPLOAD))
.then(handle_upload);
let delete_via_form = warp::path!("delete" / String)
.and(warp::post())
.and(with_app(app.clone()))
.and(with_session())
.then(|id, app, token| handle_delete(app, token, FileId::from(id)));
let thumbnail = warp::path!(String / "tn")
.and(warp::get())
.and(warp::header::optional::<String>("if-none-match"))
.and(with_app(app.clone()))
.then(move |id, old_etags, app: App| thumbnail(app, id, old_etags));
let file = warp::path!(String)
.and(warp::get())
.and(warp::header::optional::<String>("if-none-match"))
.and(with_app(app.clone()))
.then(move |id, old_etags, app: App| file(app, id, old_etags));
let server = warp::serve(
root.or(styles)
.or(auth)
.or(upload_via_form)
.or(delete_via_form)
.or(thumbnail)
.or(file)
.with(log),
);
server
.run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002))
.await;
}

View File

@ -1,114 +0,0 @@
use crate::html::*;
use build_html::{self, Container, ContainerType, Html, HtmlContainer};
use file_service::{FileHandle, FileInfo, ReadFileError};
pub fn auth(_message: Option<String>) -> build_html::HtmlPage {
build_html::HtmlPage::new()
.with_title("Sign In")
.with_stylesheet("/css")
.with_container(
Container::new(ContainerType::Div)
.with_attributes([("class", "authentication-page")])
.with_container(auth_form()),
)
}
fn auth_form() -> Container {
Container::default()
.with_attributes([("class", "card authentication-form")])
.with_html(
Form::new()
.with_path("/auth")
.with_method("post")
.with_container(
Container::new(ContainerType::Div)
.with_html(
Input::new("password", "password")
.with_id("for-token-input")
.with_attributes([
("size", "50"),
("class", "authentication-form__input"),
]),
)
.with_html(
Button::new("Sign In")
.with_attributes([("class", "authentication-form__button")]),
),
),
)
}
pub fn gallery(handles: Vec<Result<FileHandle, ReadFileError>>) -> build_html::HtmlPage {
let mut page = build_html::HtmlPage::new()
.with_title("Gallery")
.with_stylesheet("/css")
.with_container(
Container::new(ContainerType::Div)
.with_attributes([("class", "gallery-page")])
.with_header(1, "Gallery")
.with_html(upload_form()),
);
let mut gallery = Container::new(ContainerType::Div).with_attributes([("class", "gallery")]);
for handle in handles {
let container = match handle {
Ok(ref handle) => thumbnail(&handle.info),
Err(err) => Container::new(ContainerType::Div)
.with_attributes(vec![("class", "file")])
.with_paragraph(format!("{:?}", err)),
};
gallery.add_container(container);
}
page.add_container(gallery);
page
}
pub fn upload_form() -> Form {
Form::new()
.with_path("/upload")
.with_method("post")
.with_encoding("multipart/form-data")
.with_container(
Container::new(ContainerType::Div)
.with_attributes([("class", "card upload-form")])
.with_html(Input::new("file", "file").with_attributes([
("id", "for-selector-input"),
("placeholder", "select file"),
("class", "upload-form__selector"),
]))
.with_html(
Button::new("Upload file")
.with_attributes([("class", "upload-form__button")])
.with_type("submit"),
),
)
}
pub fn thumbnail(info: &FileInfo) -> Container {
Container::new(ContainerType::Div)
.with_attributes(vec![("class", "card thumbnail")])
.with_html(
Container::new(ContainerType::Div).with_link(
format!("/{}", *info.id),
Container::default()
.with_attributes([("class", "thumbnail")])
.with_image(format!("{}/tn", *info.id), "test data")
.to_html_string(),
),
)
.with_html(
Container::new(ContainerType::Div)
.with_html(
Container::new(ContainerType::UnorderedList)
.with_attributes(vec![("class", "thumbnail__metadata")])
.with_html(info.name.clone())
.with_html(format!("{}", info.created.format("%Y-%m-%d"))),
)
.with_html(
Form::new()
.with_path(&format!("/delete/{}", *info.id))
.with_method("post")
.with_html(Button::new("Delete")),
),
)
}

View File

@ -1,299 +0,0 @@
use super::{fileinfo::FileInfo, FileId, ReadFileError, WriteFileError};
use chrono::prelude::*;
use hex_string::HexString;
use image::imageops::FilterType;
use sha2::{Digest, Sha256};
use std::{
convert::TryFrom,
io::{Read, Write},
path::{Path, PathBuf},
};
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum PathError {
#[error("path cannot be derived from input")]
InvalidPath,
}
#[derive(Clone, Debug)]
pub struct PathResolver {
base: PathBuf,
id: FileId,
extension: String,
}
impl PathResolver {
pub fn new(base: &Path, id: FileId, extension: String) -> Self {
Self {
base: base.to_owned(),
id,
extension,
}
}
pub fn metadata_path_by_id(base: &Path, id: FileId) -> PathBuf {
let mut path = base.to_path_buf();
path.push(PathBuf::from(id.clone()));
path.set_extension("json");
path
}
pub fn id(&self) -> FileId {
self.id.clone()
}
pub fn file_path(&self) -> PathBuf {
let mut path = self.base.clone();
path.push(PathBuf::from(self.id.clone()));
path.set_extension(self.extension.clone());
path
}
pub fn metadata_path(&self) -> PathBuf {
let mut path = self.base.clone();
path.push(PathBuf::from(self.id.clone()));
path.set_extension("json");
path
}
pub fn thumbnail_path(&self) -> PathBuf {
let mut path = self.base.clone();
path.push(PathBuf::from(self.id.clone()));
path.set_extension(format!("tn.{}", self.extension));
path
}
}
impl TryFrom<String> for PathResolver {
type Error = PathError;
fn try_from(s: String) -> Result<Self, Self::Error> {
PathResolver::try_from(s.as_str())
}
}
impl TryFrom<&str> for PathResolver {
type Error = PathError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
PathResolver::try_from(Path::new(s))
}
}
impl TryFrom<PathBuf> for PathResolver {
type Error = PathError;
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
PathResolver::try_from(path.as_path())
}
}
impl TryFrom<&Path> for PathResolver {
type Error = PathError;
fn try_from(path: &Path) -> Result<Self, Self::Error> {
Ok(Self {
base: path
.parent()
.map(|s| s.to_owned())
.ok_or(PathError::InvalidPath)?,
id: path
.file_stem()
.and_then(|s| s.to_str().map(FileId::from))
.ok_or(PathError::InvalidPath)?,
extension: path
.extension()
.and_then(|s| s.to_str().map(|s| s.to_owned()))
.ok_or(PathError::InvalidPath)?,
})
}
}
/// One file in the database, complete with the path of the file and information about the
/// thumbnail of the file.
#[derive(Debug)]
pub struct FileHandle {
pub id: FileId,
pub path: PathResolver,
pub info: FileInfo,
}
impl FileHandle {
/// Create a new entry in the database
pub fn new(filename: String, root: PathBuf) -> Result<Self, WriteFileError> {
let id = FileId::from(Uuid::new_v4().hyphenated().to_string());
let path = PathBuf::from(filename);
let name = path
.file_stem()
.and_then(|s| s.to_str().map(|s| s.to_owned()))
.ok_or(WriteFileError::InvalidPath)?;
let extension = path
.extension()
.and_then(|s| s.to_str().map(|s| s.to_owned()))
.ok_or(WriteFileError::InvalidPath)?;
let path = PathResolver {
base: root.clone(),
id: id.clone(),
extension: extension.clone(),
};
let file_type = mime_guess::from_ext(&extension)
.first_or_text_plain()
.essence_str()
.to_owned();
let info = FileInfo {
id: id.clone(),
name,
size: 0,
created: Utc::now(),
file_type,
hash: "".to_owned(),
extension,
};
let mut md_file = std::fs::File::create(path.metadata_path())?;
let _ = md_file.write(&serde_json::to_vec(&info)?)?;
Ok(Self { id, path, info })
}
pub fn load(id: &FileId, root: &Path) -> Result<Self, ReadFileError> {
let info = FileInfo::load(PathResolver::metadata_path_by_id(root, id.clone()))?;
let resolver = PathResolver::new(root, id.clone(), info.extension.clone());
Ok(Self {
id: info.id.clone(),
path: resolver,
info,
})
}
pub fn set_content(&mut self, content: Vec<u8>) -> Result<(), WriteFileError> {
let mut content_file = std::fs::File::create(self.path.file_path())?;
let byte_count = content_file.write(&content)?;
self.info.size = byte_count;
self.info.hash = self.hash_content(&content).as_string();
let mut md_file = std::fs::File::create(self.path.metadata_path())?;
let _ = md_file.write(&serde_json::to_vec(&self.info)?)?;
self.write_thumbnail()?;
Ok(())
}
pub fn content(&self) -> Result<Vec<u8>, ReadFileError> {
load_content(&self.path.file_path())
}
pub fn thumbnail(&self) -> Result<Vec<u8>, ReadFileError> {
load_content(&self.path.thumbnail_path())
}
fn hash_content(&self, data: &Vec<u8>) -> HexString {
HexString::from_bytes(&Sha256::digest(data).to_vec())
}
fn write_thumbnail(&self) -> Result<(), WriteFileError> {
let img = image::open(self.path.file_path())?;
let tn = img.resize(640, 640, FilterType::Nearest);
tn.save(self.path.thumbnail_path())?;
Ok(())
}
pub fn delete(self) {
let _ = std::fs::remove_file(self.path.thumbnail_path());
let _ = std::fs::remove_file(self.path.file_path());
let _ = std::fs::remove_file(self.path.metadata_path());
}
}
fn load_content(path: &Path) -> Result<Vec<u8>, ReadFileError> {
let mut buf = Vec::new();
let mut file = std::fs::File::open(path)?;
file.read_to_end(&mut buf)?;
Ok(buf)
}
#[cfg(test)]
mod test {
use super::*;
use std::{convert::TryFrom, path::PathBuf};
use tempdir::TempDir;
#[test]
fn paths() {
let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png")
.expect("to have a valid path");
assert_eq!(
resolver.file_path(),
PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.png")
);
assert_eq!(
resolver.metadata_path(),
PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.json")
);
assert_eq!(
resolver.thumbnail_path(),
PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png")
);
}
#[test]
fn it_creates_file_info() {
let tmp = TempDir::new("var").unwrap();
let handle =
FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
assert_eq!(handle.info.name, "rawr");
assert_eq!(handle.info.size, 0);
assert_eq!(handle.info.file_type, "image/png");
assert_eq!(handle.info.extension, "png");
}
#[test]
fn it_opens_a_file() {
let tmp = TempDir::new("var").unwrap();
FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
}
#[test]
fn it_deletes_a_file() {
let tmp = TempDir::new("var").unwrap();
let f =
FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
f.delete();
}
#[test]
fn it_can_return_a_thumbnail() {
let tmp = TempDir::new("var").unwrap();
let _ =
FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
/*
assert_eq!(
f.thumbnail(),
Thumbnail {
id: String::from("rawr.png"),
root: PathBuf::from("var/"),
},
);
*/
}
#[test]
fn it_can_return_a_file_stream() {
let tmp = TempDir::new("var").unwrap();
let _ =
FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
// f.stream().expect("to succeed");
}
#[test]
fn it_raises_an_error_when_file_not_found() {
let tmp = TempDir::new("var").unwrap();
match FileHandle::load(&FileId::from("rawr"), tmp.path()) {
Err(ReadFileError::FileNotFound(_)) => assert!(true),
_ => assert!(false),
}
}
}

View File

@ -1,76 +0,0 @@
use crate::FileId;
use super::{ReadFileError, WriteFileError};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use std::{
io::{Read, Write},
path::PathBuf,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FileInfo {
pub id: FileId,
// Early versions of the application didn't support a name field, so it is possible that
// metadata won't contain the name. We can just default to an empty string when loading the
// metadata, as all future versions will require a filename when the file gets uploaded.
#[serde(default)]
pub name: String,
pub size: usize,
pub created: DateTime<Utc>,
pub file_type: String,
pub hash: String,
pub extension: String,
}
impl FileInfo {
pub fn load(path: PathBuf) -> Result<Self, ReadFileError> {
let mut content: Vec<u8> = Vec::new();
let mut file =
std::fs::File::open(path.clone()).map_err(|_| ReadFileError::FileNotFound(path))?;
file.read_to_end(&mut content)?;
let js = serde_json::from_slice(&content)?;
Ok(js)
}
pub fn save(&self, path: PathBuf) -> Result<(), WriteFileError> {
let ser = serde_json::to_string(self).unwrap();
let mut file = std::fs::File::create(path)?;
let _ = file.write(ser.as_bytes())?;
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::store::FileId;
use tempdir::TempDir;
#[test]
fn it_saves_and_loads_metadata() {
let tmp = TempDir::new("var").unwrap();
let created = Utc::now();
let info = FileInfo {
id: FileId("temp-id".to_owned()),
name: "test-image".to_owned(),
size: 23777,
created,
file_type: "image/png".to_owned(),
hash: "abcdefg".to_owned(),
extension: "png".to_owned(),
};
let mut path = tmp.path().to_owned();
path.push(&PathBuf::from(info.id.clone()));
info.save(path.clone()).unwrap();
let info_ = FileInfo::load(path).unwrap();
assert_eq!(info_.size, 23777);
assert_eq!(info_.created, info.created);
assert_eq!(info_.file_type, "image/png");
assert_eq!(info_.hash, info.hash);
}
}

View File

@ -1,269 +0,0 @@
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, ops::Deref, path::PathBuf};
use thiserror::Error;
mod filehandle;
mod fileinfo;
pub use filehandle::FileHandle;
pub use fileinfo::FileInfo;
#[derive(Debug, Error)]
pub enum WriteFileError {
#[error("root file path does not exist")]
RootNotFound,
#[error("permission denied")]
PermissionDenied,
#[error("invalid path")]
InvalidPath,
#[error("no metadata available")]
NoMetadata,
#[error("file could not be loaded")]
LoadError(#[from] ReadFileError),
#[error("image conversion failed")]
ImageError(#[from] image::ImageError),
#[error("JSON error")]
JSONError(#[from] serde_json::error::Error),
#[error("IO error")]
IOError(#[from] std::io::Error),
}
#[derive(Debug, Error)]
pub enum ReadFileError {
#[error("file not found")]
FileNotFound(PathBuf),
#[error("path is not a file")]
NotAFile,
#[error("permission denied")]
PermissionDenied,
#[error("JSON error")]
JSONError(#[from] serde_json::error::Error),
#[error("IO error")]
IOError(#[from] std::io::Error),
}
#[derive(Debug, Error)]
pub enum DeleteFileError {
#[error("file not found")]
FileNotFound(PathBuf),
#[error("metadata path is not a file")]
NotAFile,
#[error("cannot read metadata")]
PermissionDenied,
#[error("invalid metadata path")]
MetadataParseError(serde_json::error::Error),
#[error("IO error")]
IOError(#[from] std::io::Error),
}
impl From<ReadFileError> for DeleteFileError {
fn from(err: ReadFileError) -> Self {
match err {
ReadFileError::FileNotFound(path) => DeleteFileError::FileNotFound(path),
ReadFileError::NotAFile => DeleteFileError::NotAFile,
ReadFileError::PermissionDenied => DeleteFileError::PermissionDenied,
ReadFileError::JSONError(err) => DeleteFileError::MetadataParseError(err),
ReadFileError::IOError(err) => DeleteFileError::IOError(err),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub struct FileId(String);
impl From<String> for FileId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for FileId {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<FileId> for PathBuf {
fn from(s: FileId) -> Self {
Self::from(&s)
}
}
impl From<&FileId> for PathBuf {
fn from(s: &FileId) -> Self {
let FileId(s) = s;
Self::from(s)
}
}
impl Deref for FileId {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub trait FileRoot {
fn root(&self) -> PathBuf;
}
pub struct Context(PathBuf);
impl FileRoot for Context {
fn root(&self) -> PathBuf {
self.0.clone()
}
}
pub struct Store {
files_root: PathBuf,
}
impl Store {
pub fn new(files_root: PathBuf) -> Self {
Self { files_root }
}
pub fn list_files(&self) -> Result<HashSet<FileId>, ReadFileError> {
let paths = std::fs::read_dir(&self.files_root)?;
let info_files = paths
.into_iter()
.filter_map(|path| {
let path_ = path.unwrap().path();
if path_.extension().and_then(|s| s.to_str()) == Some("json") {
let stem = path_.file_stem().and_then(|s| s.to_str()).unwrap();
Some(FileId::from(stem))
} else {
None
}
})
.collect::<HashSet<FileId>>();
Ok(info_files)
}
pub fn add_file(
&mut self,
filename: String,
content: Vec<u8>,
) -> Result<FileHandle, WriteFileError> {
let mut file = FileHandle::new(filename, self.files_root.clone())?;
file.set_content(content)?;
Ok(file)
}
pub fn get_file(&self, id: &FileId) -> Result<FileHandle, ReadFileError> {
FileHandle::load(id, &self.files_root)
}
pub fn delete_file(&mut self, id: &FileId) -> Result<(), DeleteFileError> {
let handle = FileHandle::load(id, &self.files_root)?;
handle.delete();
Ok(())
}
pub fn get_metadata(&self, id: &FileId) -> Result<FileInfo, ReadFileError> {
let mut path = self.files_root.clone();
path.push(PathBuf::from(id));
path.set_extension("json");
FileInfo::load(path)
}
}
#[cfg(test)]
mod test {
use super::*;
use cool_asserts::assert_matches;
use std::{collections::HashSet, io::Read};
use tempdir::TempDir;
fn with_file<F>(test_fn: F)
where
F: FnOnce(Store, FileId, TempDir),
{
let tmp = TempDir::new("var").unwrap();
let mut buf = Vec::new();
let mut file = std::fs::File::open("fixtures/rawr.png").unwrap();
file.read_to_end(&mut buf).unwrap();
let mut store = Store::new(PathBuf::from(tmp.path()));
let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap();
test_fn(store, file_record.id, tmp);
}
#[test]
fn adds_files() {
with_file(|store, id, tmp| {
let file = store.get_file(&id).expect("to retrieve the file");
assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777);
assert!(tmp.path().join(&(*id)).with_extension("png").exists());
assert!(tmp.path().join(&(*id)).with_extension("json").exists());
assert!(tmp.path().join(&(*id)).with_extension("tn.png").exists());
});
}
#[test]
fn sets_up_metadata_for_file() {
with_file(|store, id, tmp| {
assert!(tmp.path().join(&(*id)).with_extension("png").exists());
let info = store.get_metadata(&id).expect("to retrieve the metadata");
assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => {
assert_eq!(size, 23777);
assert_eq!(file_type, "image/png");
assert_eq!(hash, "b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee".to_owned());
assert_eq!(extension, "png".to_owned());
});
});
}
/*
#[test]
fn sets_up_thumbnail_for_file() {
with_file(|store, id| {
let (_, thumbnail) = store.get_thumbnail(&id).expect("to retrieve the thumbnail");
assert_eq!(thumbnail.content().map(|file| file.len()).unwrap(), 48869);
});
}
*/
#[test]
fn deletes_associated_files() {
with_file(|mut store, id, tmp| {
store.delete_file(&id).expect("file to be deleted");
assert!(!tmp.path().join(&(*id)).with_extension("png").exists());
assert!(!tmp.path().join(&(*id)).with_extension("json").exists());
assert!(!tmp.path().join(&(*id)).with_extension("tn.png").exists());
});
}
#[test]
fn lists_files_in_the_db() {
with_file(|store, id, _| {
let resolvers = store.list_files().expect("file listing to succeed");
let ids = resolvers.into_iter().collect::<HashSet<FileId>>();
assert_eq!(ids.len(), 1);
assert!(ids.contains(&id));
});
}
}

View File

@ -1,91 +0,0 @@
use super::{ReadFileError, WriteFileError};
use image::imageops::FilterType;
use std::{
fs::remove_file,
io::Read,
path::{Path, PathBuf},
};
#[derive(Clone, Debug, PartialEq)]
pub struct Thumbnail {
pub path: PathBuf,
}
impl Thumbnail {
pub fn open(
origin_path: PathBuf,
thumbnail_path: PathBuf,
) -> Result<Thumbnail, WriteFileError> {
let s = Thumbnail {
path: PathBuf::from(thumbnail_path),
};
if !s.path.exists() {
let img = image::open(&origin_path)?;
let tn = img.resize(640, 640, FilterType::Nearest);
tn.save(&s.path)?;
}
Ok(s)
}
pub fn load(path: PathBuf) -> Result<Thumbnail, ReadFileError> {
let s = Thumbnail { path: path.clone() };
if !s.path.exists() {
return Err(ReadFileError::FileNotFound(path));
}
Ok(s)
}
/*
pub fn from_path(path: &Path) -> Result<Thumbnail, ReadFileError> {
let id = path
.file_name()
.map(|s| String::from(s.to_string_lossy()))
.ok_or(ReadFileError::NotAnImage(PathBuf::from(path)))?;
let path = path
.parent()
.ok_or(ReadFileError::FileNotFound(PathBuf::from(path)))?;
Thumbnail::open(&id, root)
}
*/
/*
pub fn stream(&self) -> Result<std::fs::File, ReadFileError> {
std::fs::File::open(self.path.clone()).map_err(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
ReadFileError::FileNotFound
} else {
ReadFileError::from(err)
}
})
}
*/
/*
pub fn delete(self) -> Result<(), WriteFileError> {
remove_file(self.path).map_err(WriteFileError::from)
}
*/
}
#[cfg(test)]
mod test {
use super::*;
use crate::store::utils::FileCleanup;
#[test]
fn it_creates_a_thumbnail_if_one_does_not_exist() {
let _ = FileCleanup(PathBuf::from("var/rawr.tn.png"));
let _ = Thumbnail::open(
PathBuf::from("fixtures/rawr.png"),
PathBuf::from("var/rawr.tn.png"),
)
.expect("thumbnail open must work");
assert!(Path::new("var/rawr.tn.png").is_file());
}
}

View File

@ -1,15 +0,0 @@
<html>
<head>
<title> {{title}} </title>
<link href="/css" rel="stylesheet" type="text/css" media="screen" />
<script src="/script"></script>
</head>
<body>
<a href="/file/{{id}}"><img src="/tn/{{id}}" /></a>
</body>
</html>

View File

@ -1,54 +0,0 @@
<html>
<head>
<title> Admin list of files </title>
<link href="/css" rel="stylesheet" type="text/css" media="screen" />
<script src="/script"></script>
</head>
<body>
<h1> Admin list of files </h1>
<div class="uploadform">
<form action="/" method="post" enctype="multipart/form-data">
<div id="file-selector">
<input type="file" name="file" id="file-selector-input" />
<label for="file-selector-input" onclick="selectFile('file-selector')">Select a file</label>
</div>
<input type="submit" value="Upload file" />
</form>
</div>
<div class="files">
{{#files}}
<div class="file">
{{#error}}
<div>
<p> {{error}} </p>
</div>
{{/error}}
{{#file}}
<div class="thumbnail">
<a href="/file/{{id}}"><img src="/tn/{{id}}" /></a>
</div>
<div>
<ul>
<li> {{date}} </li>
<li> {{type_}} </li>
<li> {{size}} </li>
</ul>
<div>
<form action="/{{id}}" method="post">
<input type="hidden" name="_method" value="delete" />
<input type="submit" value="Delete" />
</form>
</div>
</div>
{{/file}}
</div>
{{/files}}
</div>
</body>
</html>

View File

@ -1,10 +0,0 @@
const selectFile = (selectorId) => {
console.log("wide arrow functions work: " + selectorId);
const input = document.querySelector("#" + selectorId + " input[type='file']")
const label = document.querySelector("#" + selectorId + " label")
input.addEventListener("change", (e) => {
if (input.files.length > 0) {
label.innerHTML = input.files[0].name
}
})
}

View File

@ -1,186 +0,0 @@
:root {
--main-bg-color: #e5f0fc;
--fg-color: #449dfc;
--space-small: 4px;
--space-medium: 8px;
--space-large: 12px;
--hover-low: 4px 4px 4px gray;
}
body {
font-family: 'Ariel', sans-serif;
background-color: var(--main-bg-color);
}
.card {
border: 1px solid black;
border-radius: 5px;
box-shadow: var(--hover-low);
margin: var(--space-large);
padding: var(--space-medium);
}
.authentication-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 200px;
margin: 8px;
}
.authentication-form {
}
.authentication-form__label {
margin: var(--space-small);
}
.authentication-form__input {
margin: var(--space-small);
}
.gallery-page {
display: flex;
flex-direction: column;
}
.upload-form {
display: flex;
flex-direction: column;
}
.upload-form__selector {
margin: var(--space-small);
}
.upload-form__button {
margin: var(--space-small);
}
.gallery {
display: flex;
flex-wrap: wrap;
}
.thumbnail {
width: 300px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.thumbnail__image {
max-width: 100%;
border: none;
}
.thumbnail__metadata {
list-style: none;
}
/*
[type="submit"] {
border-radius: 1em;
margin: 1em;
padding: 1em;
}
.uploadform {
}
*/
/*
[type="file"] {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
overflow: hidden;
padding: 0;
position: absolute !important;
white-space: nowrap;
width: 1px;
}
[type="file"] + label {
background-color: rgb(0, 86, 112);
border-radius: 1em;
color: #fff;
cursor: pointer;
display: inline-block;
padding: 1em;
margin: 1em;
transition: background-color 0.3s;
}
[type="file"]:focus + label,
[type="file"] + label:hover {
background-color: #67b0ff;
}
[type="file"]:focus + label {
outline: 1px dotted #000;
outline: -webkit-focus-ring-color auto 5px;
}
*/
@media screen and (max-width: 1080px) { /* This is the screen width of a OnePlus 8 */
body {
font-size: xx-large;
}
.authentication-form {
width: 100%;
display: flex;
flex-direction: column;
}
.authentication-form__input {
font-size: x-large;
}
.authentication-form__button {
font-size: x-large;
}
.upload-form__selector {
font-size: larger;
}
.upload-form__button {
font-size: larger;
}
.thumbnail {
width: 100%;
}
/*
[type="submit"] {
font-size: xx-large;
width: 100%;
}
.uploadform {
display: flex;
}
[type="file"] + label {
width: 100%;
}
.thumbnail {
max-width: 100%;
margin: 1em;
}
.file {
display: flex;
flex-direction: column;
width: 100%;
}
*/
}

View File

@ -1,26 +0,0 @@
[package]
name = "fitnesstrax"
version = "0.6.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
adw = { version = "0.5", package = "libadwaita", features = [ "v1_4" ] }
async-channel = { version = "2.1" }
async-trait = { version = "0.1" }
chrono = { version = "0.4" }
chrono-tz = { version = "0.8" }
dimensioned = { version = "0.8", features = [ "serde" ] }
emseries = { path = "../../emseries" }
ft-core = { path = "../core" }
gio = { version = "0.18" }
glib = { version = "0.18" }
gdk = { version = "0.7", package = "gdk4" }
gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] }
thiserror = { version = "1.0" }
tokio = { version = "1.34", features = [ "full" ] }
[build-dependencies]
glib-build-tools = "0.18"

View File

@ -1,7 +0,0 @@
fn main() {
glib_build_tools::compile_resources(
&["resources"],
"gresources.xml",
"com.luminescent-dreams.fitnesstrax.gresource",
);
}

View File

@ -1,12 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION=`cat Cargo.toml | grep "^version =" | sed -r 's/^version = "(.+)"$/\1/'`
mkdir -p dist
cp ../../target/release/fitnesstrax dist
cp resources/com.luminescent-dreams.fitnesstrax.gschema.xml resources/fitnesstrax.desktop dist
strip dist/fitnesstrax
tar -czf fitnesstrax-${VERSION}.tgz dist/

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/luminescent-dreams/fitnesstrax/">
<file>style.css</file>
</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>

View File

@ -1,14 +0,0 @@
{ 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}
)
'';
}

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<schema id="com.luminescent-dreams.fitnesstrax.dev" path="/com/luminescent-dreams/fitnesstrax/dev/">
<key name="series-path" type="s">
<default>""</default>
<summary>Path to the series</summary>
</key>
</schema>
</schemalist>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<schema id="com.luminescent-dreams.fitnesstrax" path="/com/luminescent-dreams/fitnesstrax/">
<key name="series-path" type="s">
<default>""</default>
<summary>Path to the series</summary>
</key>
</schema>
</schemalist>

View File

@ -1,2 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,5 +0,0 @@
[Desktop Entry]
Version=0.2
Type=Application
Name=FitnessTrax
Exec=fitnesstrax

View File

@ -1,2 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,85 +0,0 @@
.welcome {
margin: 64px;
}
.welcome__title {
font-size: x-large;
padding: 8px;
}
.welcome__content {
padding: 8px;
}
.historical {
margin: 32px;
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 {
padding: 8px;
}
.day-summary > *:not(:last-child) {
margin-bottom: 8px;
}
.day-summary__date {
font-size: x-large;
}
.day-summary__weight {
margin: 4px;
}
.weight-view {
padding: 8px;
margin: 8px;
}
.step-view {
padding: 8px;
margin: 8px;
}
.about__content {
padding: 32px;
}
.about label {
margin-bottom: 16px;
}

View File

@ -1,2 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,80 +0,0 @@
/*
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(&copyright);
content.append(&gtk_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
}
}

View File

@ -1,167 +0,0 @@
/*
Copyright 2023, 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 async_trait::async_trait;
use chrono::NaiveDate;
use emseries::{time_range, Record, RecordId, Series, Timestamp};
use ft_core::TraxRecord;
use std::{
path::PathBuf,
sync::{Arc, RwLock},
};
use thiserror::Error;
use tokio::runtime::Runtime;
#[derive(Debug, Error)]
pub enum AppError {
#[error("no database loaded")]
NoDatabase,
#[error("failed to open the database")]
FailedToOpenDatabase,
#[error("unhandled error")]
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.
#[derive(Clone)]
pub struct App {
runtime: Arc<Runtime>,
database: Arc<RwLock<Option<Series<TraxRecord>>>>,
}
impl App {
pub fn new(db_path: Option<PathBuf>) -> Self {
let database = db_path.map(|path| Series::open(path).unwrap());
let runtime = Arc::new(
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap(),
);
Self {
runtime,
database: Arc::new(RwLock::new(database)),
}
}
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,
start: NaiveDate,
end: NaiveDate,
) -> Result<Vec<Record<TraxRecord>>, ReadError> {
let db = self.database.clone();
self.runtime
.spawn_blocking(move || {
if let Some(ref db) = *db.read().unwrap() {
let records = db
.search(time_range(
Timestamp::Date(start),
true,
Timestamp::Date(end),
true,
))
.cloned()
.collect::<Vec<Record<TraxRecord>>>();
Ok(records)
} else {
Err(ReadError::NoDatabase)
}
})
.await
.unwrap()
}
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
let db = self.database.clone();
self.runtime
.spawn_blocking(move || {
if let Some(ref mut db) = *db.write().unwrap() {
let id = db.put(record).unwrap();
Ok(id)
} else {
Err(AppError::NoDatabase)
}
})
.await
.unwrap()
.map_err(|_| WriteError::Unhandled)
}
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> {
let db = self.database.clone();
self.runtime
.spawn_blocking(move || {
if let Some(ref mut db) = *db.write().unwrap() {
db.update(record).map_err(|_| AppError::Unhandled)
} else {
Err(AppError::NoDatabase)
}
})
.await
.unwrap()
.map_err(|_| WriteError::Unhandled)
}
async fn delete_record(&self, _id: RecordId) -> Result<(), WriteError> {
unimplemented!()
}
}

View File

@ -1,215 +0,0 @@
/*
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 crate::{
app::App,
types::DayInterval,
view_models::DayDetailViewModel,
views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView},
};
use adw::prelude::*;
use chrono::{Duration, Local};
use gio::resources_lookup_data;
use gtk::STYLE_PROVIDER_PRIORITY_USER;
use std::{cell::RefCell, path::PathBuf, rc::Rc};
/// The application window, or the main window, is the main user interface for the app. Almost
/// everything occurs here.
#[derive(Clone)]
pub struct AppWindow {
app: App,
layout: gtk::Box,
current_view: Rc<RefCell<View>>,
settings: gio::Settings,
navigation: adw::NavigationView,
}
impl AppWindow {
/// Construct a new App Window.
///
/// adw_app is an Adwaita application. Application windows need to have access to this, but
/// otherwise we don't use this.
///
/// app is a core [crate::app::App] object which encapsulates all of the basic logic.
pub fn new(
app_id: &str,
resource_path: &str,
adw_app: &adw::Application,
ft_app: App,
) -> AppWindow {
let window = adw::ApplicationWindow::builder()
.application(adw_app)
.width_request(800)
.height_request(746)
.build();
window.connect_destroy(|s| {
let _ = gtk::prelude::WidgetExt::activate_action(s, "app.quit", None);
});
let stylesheet = String::from_utf8(
resources_lookup_data(
&format!("{}style.css", resource_path),
gio::ResourceLookupFlags::NONE,
)
.expect("stylesheet must be available in the resources")
.to_vec(),
)
.expect("to parse stylesheet");
let provider = gtk::CssProvider::new();
provider.load_from_data(&stylesheet);
#[allow(deprecated)]
let context = window.style_context();
#[allow(deprecated)]
context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER);
let navigation = adw::NavigationView::new();
let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
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());
let nav_layout = gtk::Box::new(gtk::Orientation::Vertical, 0);
nav_layout.append(&header_bar);
nav_layout.append(&layout);
navigation.push(
&adw::NavigationPage::builder()
.can_pop(false)
.title("FitnessTrax")
.child(&nav_layout)
.build(),
);
window.set_content(Some(&navigation));
window.present();
let s = Self {
app: ft_app,
layout,
current_view: Rc::new(RefCell::new(initial_view)),
settings: gio::Settings::new(app_id),
navigation,
};
s.load_records();
s.navigation.connect_popped({
let s = s.clone();
move |_, _| {
if let View::Historical(_) = *s.current_view.borrow() {
s.load_records();
}
}
});
s
}
fn show_welcome_view(&self) {
let view = View::Welcome(WelcomeView::new({
let s = self.clone();
move |path| s.on_apply_config(path)
}));
self.swap_main(view);
}
fn show_historical_view(&self, interval: DayInterval) {
let on_select_day = {
let s = self.clone();
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);
layout.append(&adw::HeaderBar::new());
// layout.append(&DayDetailView::new(date, records, s.app.clone()));
layout.append(&DayDetailView::new(view_model));
let page = &adw::NavigationPage::builder()
.title(date.format("%Y-%m-%d").to_string())
.child(&layout)
.build();
s.navigation.push(page);
});
}
};
let view = View::Historical(HistoricalView::new(
self.app.clone(),
interval,
Rc::new(on_select_day),
));
self.swap_main(view);
}
fn load_records(&self) {
glib::spawn_future_local({
let s = self.clone();
async move {
if s.app.database_is_open() {
let end = Local::now().date_naive();
let start = end - Duration::days(7);
s.show_historical_view(DayInterval { start, end });
} else {
s.show_welcome_view();
}
}
});
}
// Switch views.
//
// This function only replaces the old view with the one which matches the current view state.
// It is responsible for ensuring that the new view goes into the layout in the correct
// position.
fn swap_main(&self, view: View) {
let mut current_widget = self.current_view.borrow_mut();
self.layout.remove(&current_widget.widget());
*current_widget = view;
self.layout.append(&current_widget.widget());
}
#[allow(unused)]
fn on_apply_config(&self, path: PathBuf) {
glib::spawn_future_local({
let s = self.clone();
async move {
if s.app.open_db(path.clone()).await.is_ok() {
let _ = s.settings.set("series-path", path.to_str().unwrap());
s.load_records();
}
}
});
}
}

View File

@ -1,137 +0,0 @@
/*
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/>.
*/
//! ActionGroup and related structures
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
#[derive(Default)]
pub struct ActionGroupPrivate;
#[glib::object_subclass]
impl ObjectSubclass for ActionGroupPrivate {
const NAME: &'static str = "ActionGroup";
type Type = ActionGroup;
type ParentType = gtk::Box;
}
impl ObjectImpl for ActionGroupPrivate {}
impl WidgetImpl for ActionGroupPrivate {}
impl BoxImpl for ActionGroupPrivate {}
glib::wrapper! {
pub struct ActionGroup(ObjectSubclass<ActionGroupPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
}
impl ActionGroup {
fn new(builder: ActionGroupBuilder) -> Self {
let s: Self = Object::builder().build();
s.set_orientation(builder.orientation);
let primary_button = builder.primary_action.button();
let secondary_button = builder.secondary_action.map(|action| action.button());
let tertiary_button = builder.tertiary_action.map(|action| action.button());
if let Some(button) = tertiary_button {
s.append(&button);
}
s.set_halign(gtk::Align::End);
if let Some(button) = secondary_button {
s.append(&button);
}
s.append(&primary_button);
s
}
pub fn builder() -> ActionGroupBuilder {
ActionGroupBuilder {
orientation: gtk::Orientation::Horizontal,
primary_action: Action {
label: "Ok".to_owned(),
action: Box::new(|| {}),
},
secondary_action: None,
tertiary_action: None,
}
}
}
struct Action {
label: String,
action: Box<dyn Fn()>,
}
impl Action {
fn button(self) -> gtk::Button {
let button = gtk::Button::builder().label(self.label).build();
button.connect_clicked(move |_| (self.action)());
button
}
}
pub struct ActionGroupBuilder {
orientation: gtk::Orientation,
primary_action: Action,
secondary_action: Option<Action>,
tertiary_action: Option<Action>,
}
impl ActionGroupBuilder {
pub fn orientation(mut self, orientation: gtk::Orientation) -> Self {
self.orientation = orientation;
self
}
pub fn primary_action<A>(mut self, label: &str, action: A) -> Self
where
A: Fn() + 'static,
{
self.primary_action = Action {
label: label.to_owned(),
action: Box::new(action),
};
self
}
pub fn secondary_action<A>(mut self, label: &str, action: A) -> Self
where
A: Fn() + 'static,
{
self.secondary_action = Some(Action {
label: label.to_owned(),
action: Box::new(action),
});
self
}
pub fn tertiary_action<A>(mut self, label: &str, action: A) -> Self
where
A: Fn() + 'static,
{
self.tertiary_action = Some(Action {
label: label.to_owned(),
action: Box::new(action),
});
self
}
pub fn build(self) -> ActionGroup {
ActionGroup::new(self)
}
}

View File

@ -1,175 +0,0 @@
/*
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(&gtk::Label::new(Some("-")));
s.append(&s.imp().month.widget());
s.append(&gtk::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!()
}
}

Some files were not shown because too many files have changed in this diff Show More