Compare commits
101 Commits
9a9a541e18
...
bbdb80c69d
Author | SHA1 | Date | |
---|---|---|---|
bbdb80c69d | |||
940692375a | |||
a7352743ff | |||
85c8a8ba11 | |||
e856fa2515 | |||
7a07f07104 | |||
816f1f0b46 | |||
151876bcd4 | |||
9802124822 | |||
87b187c8f1 | |||
4a0dc5b87a | |||
94a821d657 | |||
dcd5514433 | |||
90224a6841 | |||
84ee790f0b | |||
ac3a21f3f0 | |||
ef0e9f16b8 | |||
06bb0811e0 | |||
b138e6da0a | |||
e19d97663d | |||
d0ba8d921d | |||
f9e903da54 | |||
a2cdaef689 | |||
49c7a37d28 | |||
aa155dc090 | |||
da5144caea | |||
f9e4dcd68a | |||
35abcfcf28 | |||
cc4f8c1515 | |||
d5b4d051a5 | |||
81143f0b9c | |||
f59c3544b4 | |||
208083d39e | |||
08462388ea | |||
dc8cb834e0 | |||
4dc7a50000 | |||
5bb9f00a0d | |||
f6eb942371 | |||
792437af44 | |||
d9f1efb8d3 | |||
b2a7577c9d | |||
82e41d711b | |||
822dfe2a13 | |||
a0f1a0b81c | |||
a33b94e5b3 | |||
a18cdb0710 | |||
c31870367f | |||
e4c5ce0236 | |||
085a82776e | |||
2b1a0b99f8 | |||
fb34d0d965 | |||
d5f4b7cfa5 | |||
1d400ce38b | |||
e62ff9aa7a | |||
2a616ef6c9 | |||
f6a45a9223 | |||
7d7e6ef300 | |||
af0ab5d020 | |||
7ca1581b55 | |||
5e89b8257d | |||
7466ef2a6f | |||
e505c21bc8 | |||
e8bc0590c6 | |||
5c23f326b6 | |||
d7e4293da0 | |||
afb510d92e | |||
82c8f3f96e | |||
d8ea2aac40 | |||
995390ae4b | |||
970e957143 | |||
b506d479d3 | |||
d78a471437 | |||
341e184947 | |||
38d76e0048 | |||
253940c2ae | |||
d3db9d60c2 | |||
b382c68382 | |||
0202b7bd59 | |||
311cd9c9a5 | |||
db8e67420f | |||
c79610bd79 | |||
cadb3ab435 | |||
71b114c9b2 | |||
0f42ebcc30 | |||
5535632466 | |||
5d66558180 | |||
154efcb6df | |||
2ab6e88634 | |||
e20ec206a8 | |||
c1ee4074b0 | |||
f0ce3a9fab | |||
e5deaa51d9 | |||
45275be11b | |||
54162d0072 | |||
a8170fd5c6 | |||
0237393c0b | |||
962ea66506 | |||
69ef3c3892 | |||
6416931c67 | |||
c35cbd75d7 | |||
addfd2072c |
.envrcCargo.lockCargo.nixCargo.tomlTaskfile.ymlflake.lockflake.nix
authdb
bike-lights
config/src
coordinates/src
crate-hashes.jsoncyber-slides/src
cyberpunk-splash
cyberpunk/src
dashboard/src
emseries
file-service
fitnesstrax
app/src
core/src/bin
fluent-ergonomics/src
gm-control-panel/src
gm-dash/server/src
hex-grid/src
ifc
memorycache/src
nom-training/src
otg
pico-st7789
result-extended/src
rust-toolchainscreenplay
sgf/src
timezone-testing/src
tree/src
visions-prototype
1589
Cargo.lock
generated
1589
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,6 @@ members = [
|
||||
"gm-control-panel",
|
||||
"hex-grid",
|
||||
"icon-test",
|
||||
"ifc",
|
||||
"memorycache",
|
||||
"nom-training",
|
||||
"otg/core",
|
||||
@ -32,5 +31,7 @@ members = [
|
||||
"sgf",
|
||||
"timezone-testing",
|
||||
"tree",
|
||||
"visions/server", "gm-dash/server", "halloween-leds"
|
||||
"visions/server",
|
||||
"gm-dash/server",
|
||||
"pico-st7789",
|
||||
]
|
||||
|
16
Taskfile.yml
Normal file
16
Taskfile.yml
Normal file
@ -0,0 +1,16 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- cargo build --release
|
||||
|
||||
update:
|
||||
cmds:
|
||||
- task build
|
||||
- crate2nix generate
|
||||
- nix build
|
||||
|
||||
lint:
|
||||
cmds:
|
||||
- cargo watch -x clippy
|
@ -18,9 +18,7 @@ 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" }
|
||||
sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite" ] }
|
||||
thiserror = { version = "1" }
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
||||
|
@ -38,7 +38,7 @@ pub struct Instant(pub U128F0);
|
||||
|
||||
impl Default for Instant {
|
||||
fn default() -> Self {
|
||||
Self(U128F0::from(0 as u8))
|
||||
Self(U128F0::from(0_u8))
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,7 +198,7 @@ impl Blinker {
|
||||
direction: BlinkerDirection,
|
||||
time: Instant,
|
||||
) -> Self {
|
||||
let mut ending_dashboard = OFF_DASHBOARD.clone();
|
||||
let mut ending_dashboard = OFF_DASHBOARD;
|
||||
|
||||
match direction {
|
||||
BlinkerDirection::Left => {
|
||||
@ -213,7 +213,7 @@ impl Blinker {
|
||||
}
|
||||
}
|
||||
|
||||
let mut ending_body = OFF_BODY.clone();
|
||||
let mut ending_body = OFF_BODY;
|
||||
match direction {
|
||||
BlinkerDirection::Left => {
|
||||
for i in 0..30 {
|
||||
@ -233,26 +233,26 @@ impl Blinker {
|
||||
|
||||
Blinker {
|
||||
transition: Fade::new(
|
||||
starting_dashboard.clone(),
|
||||
starting_body.clone(),
|
||||
ending_dashboard.clone(),
|
||||
ending_body.clone(),
|
||||
starting_dashboard,
|
||||
starting_body,
|
||||
ending_dashboard,
|
||||
ending_body,
|
||||
BLINKER_FRAMES,
|
||||
time,
|
||||
),
|
||||
fade_in: Fade::new(
|
||||
OFF_DASHBOARD.clone(),
|
||||
OFF_BODY.clone(),
|
||||
ending_dashboard.clone(),
|
||||
ending_body.clone(),
|
||||
OFF_DASHBOARD,
|
||||
OFF_BODY,
|
||||
ending_dashboard,
|
||||
ending_body,
|
||||
BLINKER_FRAMES,
|
||||
time,
|
||||
),
|
||||
fade_out: Fade::new(
|
||||
ending_dashboard.clone(),
|
||||
ending_body.clone(),
|
||||
OFF_DASHBOARD.clone(),
|
||||
OFF_BODY.clone(),
|
||||
ending_dashboard,
|
||||
ending_body,
|
||||
OFF_DASHBOARD,
|
||||
OFF_BODY,
|
||||
BLINKER_FRAMES,
|
||||
time,
|
||||
),
|
||||
@ -375,7 +375,7 @@ impl App {
|
||||
pattern.dashboard(),
|
||||
pattern.body(),
|
||||
DEFAULT_FRAMES,
|
||||
Instant((0 as u32).into()),
|
||||
Instant(0_u32.into()),
|
||||
)),
|
||||
dashboard_lights: OFF_DASHBOARD,
|
||||
lights: OFF_BODY,
|
||||
@ -386,8 +386,8 @@ impl App {
|
||||
match self.state {
|
||||
State::Pattern(ref pattern) => {
|
||||
self.current_animation = Box::new(Fade::new(
|
||||
self.dashboard_lights.clone(),
|
||||
self.lights.clone(),
|
||||
self.dashboard_lights,
|
||||
self.lights,
|
||||
pattern.dashboard(),
|
||||
pattern.body(),
|
||||
DEFAULT_FRAMES,
|
||||
@ -396,8 +396,8 @@ impl App {
|
||||
}
|
||||
State::Brake => {
|
||||
self.current_animation = Box::new(Fade::new(
|
||||
self.dashboard_lights.clone(),
|
||||
self.lights.clone(),
|
||||
self.dashboard_lights,
|
||||
self.lights,
|
||||
BRAKES_DASHBOARD,
|
||||
BRAKES_BODY,
|
||||
BRAKES_FRAMES,
|
||||
@ -406,16 +406,16 @@ impl App {
|
||||
}
|
||||
State::LeftBlinker => {
|
||||
self.current_animation = Box::new(Blinker::new(
|
||||
self.dashboard_lights.clone(),
|
||||
self.lights.clone(),
|
||||
self.dashboard_lights,
|
||||
self.lights,
|
||||
BlinkerDirection::Left,
|
||||
time,
|
||||
));
|
||||
}
|
||||
State::RightBlinker => {
|
||||
self.current_animation = Box::new(Blinker::new(
|
||||
self.dashboard_lights.clone(),
|
||||
self.lights.clone(),
|
||||
self.dashboard_lights,
|
||||
self.lights,
|
||||
BlinkerDirection::Right,
|
||||
time,
|
||||
));
|
||||
@ -441,19 +441,13 @@ impl App {
|
||||
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::NextPattern => if let State::Pattern(ref pattern) = self.state {
|
||||
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::PreviousPattern => if let State::Pattern(ref pattern) = self.state {
|
||||
self.home_pattern = pattern.previous();
|
||||
self.state = State::Pattern(self.home_pattern);
|
||||
},
|
||||
Event::RightBlinker => match self.state {
|
||||
State::Brake => self.state = State::BrakeRightBlinker,
|
||||
@ -465,17 +459,14 @@ impl App {
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, time: Instant) {
|
||||
match self.ui.check_event(time) {
|
||||
Some(event) => {
|
||||
self.update_state(event);
|
||||
self.update_animation(time);
|
||||
}
|
||||
None => {}
|
||||
if let Some(event) = self.ui.check_event(time) {
|
||||
self.update_state(event);
|
||||
self.update_animation(time);
|
||||
};
|
||||
|
||||
let (dashboard, lights) = self.current_animation.tick(time);
|
||||
self.dashboard_lights = dashboard.clone();
|
||||
self.lights = lights.clone();
|
||||
self.dashboard_lights = dashboard;
|
||||
self.lights = lights;
|
||||
self.ui.update_lights(dashboard, lights);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
||||
async-std = "1.13.0"
|
||||
cairo-rs = { version = "0.18" }
|
||||
fixed = { version = "1" }
|
||||
gio = { version = "0.18" }
|
||||
|
@ -1,6 +1,7 @@
|
||||
use adw::prelude::*;
|
||||
use async_std::channel::Sender;
|
||||
use fixed::types::{I8F8, U128F0};
|
||||
use glib::{Object, Sender};
|
||||
use glib::Object;
|
||||
use gtk::subclass::prelude::*;
|
||||
use lights_core::{
|
||||
App, BodyPattern, DashboardPattern, Event, Instant, FPS, OFF_BODY, OFF_DASHBOARD, RGB, UI,
|
||||
@ -45,6 +46,12 @@ glib::wrapper! {
|
||||
pub struct DashboardLights(ObjectSubclass<DashboardLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||
}
|
||||
|
||||
impl Default for DashboardLights {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DashboardLights {
|
||||
pub fn new() -> Self {
|
||||
let s: Self = Object::builder().build();
|
||||
@ -103,6 +110,12 @@ glib::wrapper! {
|
||||
pub struct BikeLights(ObjectSubclass<BikeLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||
}
|
||||
|
||||
impl Default for BikeLights {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BikeLights {
|
||||
pub fn new() -> Self {
|
||||
let s: Self = Object::builder().build();
|
||||
@ -161,12 +174,15 @@ impl UI for GTKUI {
|
||||
}
|
||||
|
||||
fn update_lights(&self, dashboard_lights: DashboardPattern, lights: BodyPattern) {
|
||||
self.tx
|
||||
.send(Update {
|
||||
dashboard: dashboard_lights,
|
||||
lights,
|
||||
})
|
||||
.unwrap();
|
||||
let tx = self.tx.clone();
|
||||
glib::spawn_future(async move {
|
||||
let _ = tx
|
||||
.send(Update {
|
||||
dashboard: dashboard_lights,
|
||||
lights,
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,8 +192,7 @@ fn main() {
|
||||
.build();
|
||||
|
||||
adw_app.connect_activate(move |adw_app| {
|
||||
let (update_tx, update_rx) =
|
||||
gtk::glib::MainContext::channel::<Update>(gtk::glib::Priority::DEFAULT);
|
||||
let (update_tx, update_rx) = async_std::channel::unbounded();
|
||||
let (event_tx, event_rx) = std::sync::mpsc::channel();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
@ -269,6 +284,17 @@ fn main() {
|
||||
layout.append(&dashboard_lights);
|
||||
layout.append(&bike_lights);
|
||||
|
||||
glib::spawn_future_local({
|
||||
let dashboard_lights = dashboard_lights.clone();
|
||||
let bike_lights = bike_lights.clone();
|
||||
async move {
|
||||
while let Ok(Update { dashboard, lights }) = update_rx.recv().await {
|
||||
dashboard_lights.set_lights(dashboard);
|
||||
bike_lights.set_lights(lights);
|
||||
}
|
||||
}
|
||||
});
|
||||
/*
|
||||
update_rx.attach(None, {
|
||||
let dashboard_lights = dashboard_lights.clone();
|
||||
let bike_lights = bike_lights.clone();
|
||||
@ -278,6 +304,7 @@ fn main() {
|
||||
glib::ControlFlow::Continue
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
window.set_content(Some(&layout));
|
||||
window.present();
|
||||
|
@ -40,6 +40,12 @@ macro_rules! define_config {
|
||||
values: std::collections::HashMap<ConfigName, ConfigOption>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -145,7 +145,7 @@ mod tests {
|
||||
|
||||
let coord2 = &lst1[idx];
|
||||
assert!(coord2.is_adjacent(&coord1));
|
||||
assert!(coord1.is_adjacent(&coord2));
|
||||
assert!(coord1.is_adjacent(coord2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -166,10 +166,10 @@ mod tests {
|
||||
let hexaddr = AxialAddr::new(q, r);
|
||||
let en_distancaj_hexaddr: Vec<AxialAddr> = hexaddr.addresses(distance).collect();
|
||||
|
||||
let expected_cnt = ((0..distance+1).map(|v| v * 6).fold(1, |acc, val| acc + val)) as usize;
|
||||
let expected_cnt = (0..distance+1).map(|v| v * 6).fold(1, |acc, val| acc + val);
|
||||
assert_eq!(en_distancaj_hexaddr.len(), expected_cnt);
|
||||
for c in en_distancaj_hexaddr {
|
||||
assert!(c.distance(&hexaddr) <= distance as usize);
|
||||
assert!(c.distance(&hexaddr) <= distance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,112 +5,125 @@
|
||||
"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#allocator-api2@0.2.21": "08zrzs022xwndihvzdn78yqarv2b9696y67i6h78nla3ww87jgb8",
|
||||
"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#anstream@0.6.18": "16sjk4x3ns2c3ya1x28a44kh6p47c7vhk27251i015hik1lm7k4a",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.6": "1acqayy22fwzsrvr6n0lz6a4zvjjcvgr5sm941m7m0b2fr81cb9v",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.1.2": "036nm3lkyk43xbps1yql3583fp4hg3b1600is7mcyxs1gzrpm53r",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#anstyle-wincon@3.0.7": "0kmf0fq4c8yribdpdpylzz1zccpy84hizmcsac3wrac1f7kk8dfa",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.10": "1yai2vppmd7zlvlrp9grwll60knrmscalf8l2qpfz8b7y5lkpk2m",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.95": "010vd1ki8w84dzgx6c81sc8qm9n02fxic1gkpv52zp4nwrn0kb1l",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#assert-json-diff@2.0.2": "04mg3w0rh3schpla51l18362hsirl23q93aisws2irrj32wg5r27",
|
||||
"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-io@2.4.0": "0n8h0vy53n4vdkq529scqnkzm9vcl3r73za9nj81s2nfrhiv78j3",
|
||||
"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#async-trait@0.1.85": "0mm0gwad44zs7mna4a0m1z4dhzpmydfj73w4wm23c8xpnhrli4rz",
|
||||
"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#auto-future@1.0.0": "0wykbakzh227vz6frx9p48zsq0wpswgmb7v3917m53m7gr2pw7iw",
|
||||
"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#axum-core@0.4.5": "16b1496c4gm387q20hkv5ic3k5bd6xmnvk50kwsy6ymr8rhvvwh9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#axum-macros@0.4.2": "1klv77c889jm05bzayaaiinalarhvh2crc2w4nvp3l581xaj7lap",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#axum-test@16.4.1": "1p5qxacvxsagnqq30nr2wznjyhgb8svsfb925ah3d2b0s91s9qv3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#axum@0.7.9": "07z7wqczi9i8xb4460rvn39p4wjqwr32hx907crd1vwb2fy8ijpd",
|
||||
"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.22.1": "1imqzgh7bxcikp5vx3shqvw9j09g9ly0xr0jma0q66i52r7jbcvj",
|
||||
"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-set@0.8.0": "18riaa10s6n59n39vix0cr7l2dgwdhcpbcm97x1xbyfp1q47x008",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#bit-vec@0.8.0": "1xxa1s2cj291r7k1whbxq840jxvmdsq9xgh7bvrxl46m80fllxjy",
|
||||
"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#bitflags@2.8.0": "0dixc6168i98652jxf0z9nbyn0zcis3g6hi6qdr7z5dbhcygas4g",
|
||||
"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#bytemuck@1.21.0": "18wj81x9xhqcd6985r8qxmbik6szjfjfj62q3xklw8h2p3x7srgg",
|
||||
"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#bytes@1.9.0": "16ykzx24v1x4f42v2lxyvlczqhdfji3v7r4ghwckpwijzvb1hn9j",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#bytesize@1.3.0": "1k3aak70iwz4s2gsjbxf0ws4xnixqbdz6p2ha96s06748fpniqx3",
|
||||
"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#cc@1.2.10": "0aaj2ivamhfzhgb9maasnfkh03s2mzhzpzwrkghgzbkfnv5qy80k",
|
||||
"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#chrono@0.4.39": "09g8nf409lb184kl9j4s85k0kn8wzgjkp5ls9zid50b886fwqdky",
|
||||
"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#clap@4.5.26": "10v7qvn90calfbhap1c4r249i5c7fbxj09fn3szfz9pkis85xsx8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.5.26": "08f1mzcvi7zjhm7hvz6al4jnv70ccqhwiaq74hihlspwnl0iic4n",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.5.24": "131ih3dm76srkbpfx7zfspp9b556zgzj31wqhl0ji2b39lcmbdsl",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#clap_lex@0.7.4": "19nwfls5db269js5n822vkc8dw0wjq2h1wf0hgr06ld2g52d2spl",
|
||||
"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#colorchoice@1.0.3": "1439m3r3jy3xqck8aa13q658visn71ki76qa93cy55wkmalwlqsv",
|
||||
"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#cookie@0.18.1": "0iy749flficrlvgr3hjmf3igr738lk81n5akzf4ym4cs6cxg7pjd",
|
||||
"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#cpufeatures@0.2.16": "1hy466fkhxjbb16i7na95wz8yr14d0kd578pwzj5lbkz14jh5f0n",
|
||||
"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-deque@0.8.6": "0l9f1saqp1gn5qy0rxvkmz4m6n7fc0b3dbm6q1r5pmgpnyvi3lcx",
|
||||
"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#crossbeam-queue@0.3.12": "059igaxckccj6ndmg45d5yf7cm4ps46c18m21afq3pwiiz1bnn0g",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21": "0a3aa2bmc8q35fb67432w16wvi54sfmb69rk9h5bhd18vw0c99fh",
|
||||
"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#data-encoding@2.7.0": "0vxdv88fnvnxw29gk84gj5662xlv0p5hmlxpwp7d60cckp8fwq0f",
|
||||
"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#diff@0.1.13": "1j0nzjxci2zqx63hdcihkp0a4dkdmzxd7my4m7zk6cjyfy34j9an",
|
||||
"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#encoding_rs@0.8.35": "1wv64xdrr9v37rqqdjsyb8l8wzlcbab80ryxhrszvnj59wy0y0vm",
|
||||
"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#errno@0.3.10": "0pgblicz1kjz9wa9m0sghkhh2zw1fhq1mxzj7ndjm746kg5m5n1k",
|
||||
"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-strategy@0.5.3": "1ch5gf6knllyq12jkb5zdfag573dh44307q4pwwi2g37sc6lwgiw",
|
||||
"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#event-listener@5.4.0": "1bii2gn3vaa33s0gr2zph7cagiq0ppcfxcxabs24ri9z9kgar4il",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#exr@1.73.0": "1q47yq78q9k210r6jy1wwrilxwwxqavik9l3l426rd17k7srfcgq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fallible-iterator@0.3.0": "0ja6l56yka5vn4y4pk6hn88z0bpny7a8k1919aqjzp0j1yhy9k1a",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fallible-streaming-iterator@0.1.9": "0nj6j26p71bjy8h42x6jahx1hn0ng6mc2miwpgwnp8vnwqf4jq3k",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0": "1ghiahsw1jd68df895cy5h3gzwk30hndidn3b682zmshpgmrx41p",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fdeflate@0.3.7": "130ga18vyxbb5idbgi07njymdaavvk6j08yh1dfarm294ssm6s0y",
|
||||
"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#flate2@1.0.35": "0z6h0wa095wncpfngx75wyhyjnqwld7wax401gsvnzjhzgdbydn9",
|
||||
"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#flume@0.11.1": "15ch0slxa8sqsi6c73a0ky6vdnh48q8cxjf7rksa3243m394s3ns",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7": "1hc2mcqha06aibcaza94vbi81j6pr9a1bbxrxjfhc91zin8yr7iz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#foldhash@0.1.4": "0vsxw2iwpgs7yy6l7pndm7b8nllaq5vdxwnmjn1qpm5kyzhzvlm0",
|
||||
"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",
|
||||
@ -120,7 +133,7 @@
|
||||
"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-lite@2.6.0": "0cmmgszlmkwsac9pyw5rfjakmshgx4wmzmlyn6mmjs0jav4axvgm",
|
||||
"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",
|
||||
@ -144,7 +157,7 @@
|
||||
"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#glob@0.3.2": "1cm2w34b5w45fxr522h5b0fv1bxchfswcj560m3pnjbia7asvld8",
|
||||
"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",
|
||||
@ -158,8 +171,9 @@
|
||||
"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#hashbrown@0.15.2": "12dj0yfn59p3kh3679ac0w1fagvzf4z2zp87a13gbbqbzw0185dz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hashlink@0.10.0": "1h8lzvnl9qxi3zyagivzz2p1hp6shgddfmccyf6jv7s1cdicz0kk",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hashlink@0.9.1": "1byq4nyrflm5s6wdx5qwp96l1qbp2d0nljvrr5yqrsfy51qzz93b",
|
||||
"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",
|
||||
@ -170,24 +184,41 @@
|
||||
"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#home@0.5.11": "1kxb4k87a9sayr8jipr7nq9wpgmjk4hk4047hmf9kc24692k75aq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#http-body-util@0.1.2": "0kslwazg4400qnc2azkrgqqci0fppv12waicnsy5d8hncvbjjd3r",
|
||||
"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-body@1.0.1": "111ir5k2b9ihz5nr9cz7cwm7fnydca7dx4hc7vr16scfzghxrzhy",
|
||||
"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#http@1.2.0": "1skglzdf98j5nzxlii540n11is0w4l80mi5sm3xrj716asps4v7i",
|
||||
"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-util@0.1.10": "1d1iwrkysjhq63pg54zk3vfby1j7zmxzm9zzyfr4lwvp0szcybfz",
|
||||
"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#hyper@0.14.32": "1rvcb0smz8q1i0y6p7rwxr02x5sclfg2hhxf3g0774zczn0cgps1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hyper@1.5.2": "1q7akfb443yrjzkmnnbp2vs8zi15hgbk466rr4y144v4ppabhvr5",
|
||||
"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#icu_collections@1.5.0": "09j5kskirl59mvqc8kabhy7005yyy7dp88jw9f6f3gkf419a8byv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#icu_locid@1.5.0": "0dznvd1c5b02iilqm044q4hvar0sqibq1z46prqwjzwif61vpb0k",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#icu_locid_transform@1.5.0": "0kmmi1kmj9yph6mdgkc7v3wz6995v7ly3n80vbg0zr78bp1iml81",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#icu_locid_transform_data@1.5.0": "0vkgjixm0wzp2n3v5mw4j89ly05bg3lx96jpdggbwlpqi0rzzj7x",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer@1.5.0": "0kx8qryp8ma8fw1vijbgbnf7zz9f2j4d14rw36fmjs7cl86kxkhr",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@1.5.0": "05lmk0zf0q7nzjnj5kbmsigj3qgr0rwicnn5pqi9n7krmbvzpjpq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#icu_properties@1.5.1": "1xgf584rx10xc1p7zjr78k0n4zn3g23rrg6v2ln31ingcq3h5mlk",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@1.5.0": "0scms7pd5a7yxx9hfl167f5qdf44as6r3bd8myhlngnxqgxyza37",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#icu_provider@1.5.0": "1nb8vvgw8dv2inqklvk05fs0qxzkw8xrg2n9vgid6y7gm3423m3f",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#icu_provider_macros@1.5.0": "1mjs0w7fcm2lcqmbakhninzrjwqs485lkps4hz0cv3k36y9rxj0y",
|
||||
"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#idna@1.0.3": "0zlajvm2k3wy0ay8plr07w22hxkkmrxkffa6ah57ac6nci984vv8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#idna_adapter@1.2.0": "0wggnkiivaj5lw0g0384ql2d7zk4ppkn3b1ry4n0ncjpr7qivjns",
|
||||
"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#include_dir@0.7.4": "1pfh3g45z88kwq93skng0n6g3r7zkhq9ldqs9y8rvr7i11s12gcj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#include_dir_macros@0.7.4": "0x8smnf6knd86g69p19z5lpfsaqp8w0nx14kdpkz1m8bxnkqbavw",
|
||||
"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#indexmap@2.7.0": "07s7jmdymvd0rm4yswp0j3napx57hkjm9gs9n55lvs2g78vj5y32",
|
||||
"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",
|
||||
@ -195,10 +226,10 @@
|
||||
"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#itoa@1.0.14": "0x26kr9m062mafaxgcf2p6h2x7cmixm0zw95aipzn2hr3d5jlnnp",
|
||||
"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#js-sys@0.3.77": "13x2qcky5l22z4xgivi59xhjjx4kxir1zg7gcj0f1ijzd4yg7yhw",
|
||||
"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",
|
||||
@ -206,19 +237,21 @@
|
||||
"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#libc@0.2.169": "02m253hs8gw0m1n8iyrsc4n15yzbqwhddi7w1l0ds7i92kdsiaxm",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libloading@0.8.6": "0d2ccr88f8kv3x7va2ccjxalcjnhrci4j2kwxp7lfmbkpjs4wbzw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libm@0.2.11": "1yjgk18rk71rjbqcw9l1zaqna89p9s603k7n327nqs8dn88vwmc3",
|
||||
"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#libsqlite3-sys@0.30.1": "0jcikvgbj84xc7ikdmpc8m4y5lyqgrb9aqblphwk67kv95xgp69f",
|
||||
"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#linux-raw-sys@0.4.15": "1aq7r2g7786hyxhv40spzf2nhag5xbw2axxc1k8z5k1dsgdm4v6j",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#litemap@0.7.4": "012ili3vppd4952sh6y3qwcd0jkd0bq2qpr9h7cppc8sj11k7saf",
|
||||
"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#log@0.4.25": "17ydv5zhfv1zzygy458bmg3f3jx1vfziv9d74817w76yhfqgbjq4",
|
||||
"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#matchit@0.7.3": "156bgdmmlv4crib31qhgg49nsjk88dxkdqp80ha2pk2rk6n6ax0f",
|
||||
"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",
|
||||
@ -229,9 +262,8 @@
|
||||
"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#miniz_oxide@0.8.3": "093r1kd1r9dyf05cbvsibgmh96pxp3qhzfvpd6f15bpggamjqh5q",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#mio@1.0.3": "1gah0h4ia3avxbwym0b6bi6lr6rpysmj9zvw6zis5yq0z0xq91i8",
|
||||
"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",
|
||||
@ -246,33 +278,32 @@
|
||||
"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#object@0.36.7": "11vv97djn9nc5n6w1gc6bd96d2qk2c8cg1kw5km9bsi3v4a8x532",
|
||||
"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#openssl-sys@0.9.104": "0hf712xcxmycnlc09r8d446b3mwqchsbfrjv374fp7grrc3g7as5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.68": "1xbiz2bmba2fibg70s462yk2fndp3f9vz11c7iw0ilh2y54bqx31",
|
||||
"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.11.3": "0y6hxp1d48rx2434wgi5g8j1pr8s5jja29ha2b65435fh057imhz",
|
||||
"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.11.3": "0si1n6zr93kzjs3wah04ikw8z6npsr39jw4dam8yi9czg2609y5f",
|
||||
"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.11.3": "0gc4np7s91ynrgw73s2i7iakhb4lzdv1gcyx7yhlc0n214a2701w",
|
||||
"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.11.3": "1rallyvh28jqd9i916gk5gk2igdmzlgvv5q0l3xbf3m6y8pbrsk7",
|
||||
"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-project-internal@1.1.8": "1yzfhf6l27nhzv7r5hfrwj2g0x7xmfhgil19fj9am4srqp06csnm",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16": "16wzc7z7dfkf9bmjin22f5282783f6mdksnr0nv0j5ym5f9gyg1v",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.8": "05jr3xfy1spgmz3q19l4mmvv46vgvkvsgphamifx7x45swxcabhy",
|
||||
"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",
|
||||
@ -282,20 +313,21 @@
|
||||
"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#png@0.17.16": "09kmkms9fmkbkarw0lnf0scqvjwwg3r7riddag0i3q39r0pil5c2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#polling@3.7.4": "0bs4nhwfwsvlzlhah2gbhj3aa9ynvchv2g350wapswh26a65c156",
|
||||
"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_assertions@1.4.1": "0v8iq35ca4rw3rza5is3wjxwsf88303ivys07anc5yviybi31q9s",
|
||||
"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#proc-macro2@1.0.93": "169dw9wch753if1mgyi2nfl1il77gslvh6y2q46qplprwml6m530",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#proptest@1.6.0": "0l4y4bb8hffv7cys7d59qwqdmvmqjfzz0x9vblc08209clqfkjhl",
|
||||
"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#quote@1.0.38": "1k0s75w61k6ch0rs263r4j69b7vj1wadqgb9dia4ylc9mymcqk8f",
|
||||
"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",
|
||||
@ -315,34 +347,40 @@
|
||||
"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#redox_syscall@0.5.8": "0d48ylyd6gsamynyp257p6n2zl4dw2fhnn5z9y3nhgpri6rn5a03",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.9": "02092l8zfh3vkmk47yjc8d631zhhcd49ck2zr133prvd3z38v7l0",
|
||||
"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#regex@1.11.1": "148i41mzbx8bmq32hsj1q4karkzzx5m60qza6gdw4pdc9qdyyi5m",
|
||||
"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#reserve-port@2.0.1": "10x21rdb1hjzp6n5flbbw3hfd7brmirckz1q0zsf3a7s5d516f4q",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rsa@0.9.7": "06amqm85raq26v6zg00fbf93lbj3kx559n2lpxc3wrvbbiy5vis7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rusqlite@0.32.1": "0vlx040bppl414pbjgbp7qr4jdxwszi9krx0m63zzf2f2whvflvp",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rusqlite_migration@1.3.1": "076dm65g0sngzrb93r07va4l5zl3gjx9gq5mlsh21p7p0bl44fwj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rust-multipart-rfc7578_2@0.6.1": "0mwd3i2mk91n6diaxnkw28vyjbifhrm5ls73pcpfzz8a1i0lidq3",
|
||||
"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#rustix@0.38.43": "1xjfhdnmqsbwnfmm77vyh7ldhqx0g9waqm4982404d7jdgp93257",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rustls-pemfile@1.0.4": "1324n5bcns0rnw6vywr5agff3rwfvzphi7rmbyzwnv6glkhclx0w",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.19": "1m39qd65jcd1xgqzdm3017ppimiggh2446xngwp1ngr8hjbmpi7p",
|
||||
"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#schannel@0.1.27": "0gbbhy28v72kd5iina0z2vcdl3vz63mk5idvkzn5r52z6jmfna8z",
|
||||
"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-sys@2.14.0": "0chwn01qrnvs59i5220bymd38iddy4krbnmfnhf4k451aqfj7ns9",
|
||||
"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#self_cell@1.1.0": "1gmxk5bvnnimcif7v1jk8ai2azfvh9djki545nd86vsnphjgrzf2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#semver@1.0.24": "1fmvjjkd3f64y5fqr1nakkq371mnwzv09fbz5mbmdxril63ypdiw",
|
||||
"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@1.0.217": "0w2ck1p1ajmrv1cf51qf7igjn2nc51r0izzc00fzmmhkvxjl5z02",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.217": "180r3rj5gi5s1m23q66cr5wlfgc5jrs6n1mdmql2njnhk37zg6ss",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.136": "1lipcjhh1zazh283i4wsl4l14knh81q2rlkwmag8v8s2rwihqsik",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde_path_to_error@0.1.16": "19hlz2359l37ifirskpcds7sxg0gzpqvfilibs7whdys0128i6dg",
|
||||
"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",
|
||||
@ -353,59 +391,66 @@
|
||||
"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#siphasher@1.0.1": "17f35782ma3fn6sh21c027kjmd227xyrx06ffi8gw4xzv9yry6an",
|
||||
"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#socket2@0.5.8": "1s7vjmb5gzp3iaqi94rh9r63k9cj00kjgbfn7gn60kmnk6fjcw69",
|
||||
"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#sqlx-core@0.8.3": "1q31dawr61wc6q2f12my4fw082mbv8sxwz1082msjsk76rlpn03a",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros-core@0.8.3": "1bg7sn6l8dc4pzrqx2dwc3sp7dbn97msfqahpycnl55bqnn917sf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros@0.8.3": "047k67sylscv0gdhwwqrn0s33jy1mvq8rmqq6s8fygv4g2ny44ii",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-mysql@0.8.3": "0czjzzjm2y6lkhxvvzrzwgp0pmlhymcnym20hn9n9kh01s7jfq25",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-postgres@0.8.3": "04wnjl51kfx0qbfsfmhqdshpmw32vzz2p8dksmj6gvb3ydbqmff5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-sqlite@0.8.3": "0h05ca26g428h4337k4nm0ww75bcdkiqzp883m7fc92v78fsfp7q",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx@0.8.3": "0pvlpq0plgyxf5kikcv786pf0pjv8dx5shlvz72l510d7hxyf424",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.0": "1lxjr8q2n534b2lhkxd6l6wcddzjvnksi58zv11f9y0jjmr15wd8",
|
||||
"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#syn@2.0.96": "102wk3cgawimi3i0q3r3xw3i858zkyingg6y7gsxfy733amsvl6m",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@0.1.2": "0q01lyj0gr9a93n10nxsn8lwbzq97jqd6b768x17c8f7v7gccir0",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@1.0.2": "0qvjyasd6w18mjg5xlaq5jgy84jsjfsvmnn12c13gypxbv75dwhb",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#synstructure@0.13.1": "0wc9f002ia2zqcbj0q2id5x6n7g1zjqba7qkg2mr0qvvmdk7dby8",
|
||||
"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#tempfile@3.15.0": "016pmkbwn3shas44gcwq1kc9lajalb90qafhiip5fvv8h6f5b2ls",
|
||||
"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#thiserror-impl@1.0.69": "1h84fmn2nai41cxbhk6pqf46bxqq1b344v8yz089w1chzi76rvjg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@2.0.11": "1hkkn7p2y4cxbffcrprybkj0qy1rl1r6waxmxqvr764axaxc3br6",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69": "0lizjay08agcr5hs9yfzzj6axs53a2rgx070a1dsi3jpkcrzbamn",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.11": "1z0649rpa8c2smzx129bz4qvxmdihj30r2km6vfpcv9yny2g4lnl",
|
||||
"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-macros@0.2.19": "1pl558z26pp342l5y91n6dxb60xwhar975wk6jc4npiygq0ycd18",
|
||||
"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#time@0.3.37": "08bvydyc14plkwhchzia5bcdbmm0mk5fzilsdpjx06w6hf48drrm",
|
||||
"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@1.8.1": "1s41rv7n39sjsxz3kd3d4adw45ndkxz1d18rfbz2wd7s9n8bhb82",
|
||||
"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-macros@2.5.0": "1f6az2xbvqp7am417b78d1za8axbvjvxnmkakz9vr8s52czx81kf",
|
||||
"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-stream@0.1.17": "0ix0770hfp4x5rh5bl7vsnr3d4iz4ms43i522xw70xaap9xqv9gc",
|
||||
"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#tokio-util@0.7.13": "0y0h10a52c7hrldmr3410bp7j3fadq0jn9nf7awddgd2an6smz6p",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tokio@1.43.0": "17pdm49ihlhfw3rpxix3kdh2ppl1yv7nwp1kxazi5r1xz97zlq9x",
|
||||
"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-http@0.6.2": "15wnvhl6cpir9125s73bqjzjsvfb0fmndmsimnl2ddnlhfvs6gs0",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tower-layer@0.3.3": "03kq92fdzxin51w8iqix06dcfgydyvx7yr6izjq0p626v9n2l70j",
|
||||
"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#tower@0.5.2": "1ybmd59nm4abl9bsvy6rx31m4zvzp5rja2slzpn712y9b68ssffh",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.28": "0v92l9cxs42rdm4m5hsa8z7ln1xsiw1zc2iil8c6k7lzq0jf2nir",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.33": "170gc7cxyjx824r9kr17zc9gvzx89ypqfdzq259pr56gg5bwjwp6",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.41": "1l5xrzyjfyayrwhvhldfnwdyligi1mpqm8mzbi2m1d6y6p2hlkkq",
|
||||
"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",
|
||||
@ -414,29 +459,30 @@
|
||||
"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#typeshare@1.0.4": "1svc92lg35r12mqdpbs4wbkw7g72v2302niyw5v1w290250hzghr",
|
||||
"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#unicase@2.8.1": "0fd5ddbhpva7wrln2iah054ar2pc1drqjcll0f493vj3fv8l9f3m",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-bidi@0.3.18": "1xcxwbsqa24b8vfchhzyyzgj0l6bn51ib5v8j6krha0m77dva72w",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.14": "10ywa1pg0glgkr4l3dppjxizr9r2b7im0ycbfa0137l69z5fdfdd",
|
||||
"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#url@2.5.4": "0q6sgznyy2n4l5lm16zahkisvc9nip9aa5q1pps7656xra3bdy1j",
|
||||
"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#utf16_iter@1.0.5": "0ik2krdr73hfgsdzw0218fn35fa09dg2hvbi1xp3bmdfrp9js8y8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4": "1gmna9flnj8dbyd8ba17zigrp9c4c3zclngf5lnb5yvz1ri41hdn",
|
||||
"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#uuid@1.12.0": "1i2i7ar5651d58ip1l8cghg3y60pn0rqmssvw6lm8d4s3xc1hh3l",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#value-bag@1.10.0": "1lnsixdpi1ldms1adxyafyx7lyrqxhhskgwrjckmml6majmc9x1y",
|
||||
"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",
|
||||
@ -447,13 +493,13 @@
|
||||
"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#wasm-bindgen-backend@0.2.100": "1ihbf1hq3y81c4md9lyh6lcwbx6a5j0fw4fygd423g62lm8hc2ig",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-futures@0.4.50": "0q8ymi6i9r3vxly551dhxcyai7nc491mspj0j1wbafxwq074fpam",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro-support@0.2.100": "1plm8dh20jg2id0320pbmrlsv6cazfv6b6907z19ys4z1jj7xs4a",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro@0.2.100": "01xls2dvzh38yj17jgrbiib1d3nyad7k2yw9s0mpklwys333zrkz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-shared@0.2.100": "0gffxvqgbh9r9xl36gprkfnh3w9gl8wgia6xrin7v11sjcxxf18s",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen@0.2.100": "1x8ymcm6yi3i1rwj78myl1agqv2m86i648myy3lc97s9swlqkp0y",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#web-sys@0.3.77": "1lnmc1ffbq34qw91nndklqqm75rasaffj2g4f8h1yvqqz4pdvdik",
|
||||
"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",
|
||||
@ -483,9 +529,18 @@
|
||||
"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#write16@1.0.0": "0dnryvrrbrnl7vvf5vb1zkmwldhjkf2n5znliviam7bm4900z2fi",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#writeable@0.5.5": "0lawr6y0bwqfyayf3z8zmqlhpnzhdx0ahs54isacbhyjwa7g778y",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#yansi-term@0.1.2": "1w8vjlvxba6yvidqdvxddx3crl6z66h39qxj8xi6aqayw2nk0p7y",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#yansi@1.0.1": "0jdh55jyv0dpd38ij4qh60zglbw9aa8wafqai6m0wa7xaxk3mrfg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#yoke-derive@0.7.5": "0m4i4a7gy826bfvnqa9wy6sp90qf0as3wps3wb0smjaamn68g013",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#yoke@0.7.5": "0h3znzrdmll0a7sglzf9ji0p5iqml11wrj1dypaf6ad6kbpnl3hj",
|
||||
"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#zerofrom-derive@0.1.5": "022q55phhb44qbrcfbc48k0b741fl8gnazw3hpmmndbx5ycfspjr",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.5": "0bnd8vjcllzrvr3wvn8x14k2hkrpyy1fm3crkn2y3plmr44fxwyg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.1": "1pjdrmjwmszpxfd7r860jx54cyk94qk59x13sc307cvr5256glyf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zerovec-derive@0.10.3": "1ik322dys6wnap5d3gcsn09azmssq466xryn5czfm13mn7gsdbvf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zerovec@0.10.4": "0yghix7n3fjfdppwghknzvx9v8cf826h2qal5nqvy8yzg4yqjaxa",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zune-inflate@0.2.54": "00kg24jh3zqa3i6rg6yksnb71bch9yi1casqydl00s7nw8pk7avk"
|
||||
}
|
@ -12,9 +12,9 @@ use std::{
|
||||
|
||||
use cairo::{Context, Rectangle};
|
||||
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, Text};
|
||||
use glib::{GString, Object};
|
||||
use glib::Object;
|
||||
use gtk::{
|
||||
glib::{self, Propagation},
|
||||
glib::{self},
|
||||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
EventControllerKey,
|
||||
@ -40,6 +40,7 @@ struct Step {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Default)]
|
||||
struct Script(Vec<Step>);
|
||||
|
||||
impl Script {
|
||||
@ -51,7 +52,7 @@ impl Script {
|
||||
Ok(Self(script))
|
||||
}
|
||||
|
||||
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a Step> {
|
||||
fn iter(&self) -> impl Iterator<Item = &'_ Step> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
@ -60,11 +61,6 @@ impl Script {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Script {
|
||||
fn default() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<usize> for Script {
|
||||
type Output = Step;
|
||||
@ -98,11 +94,11 @@ impl Animation for Fade {
|
||||
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 alpha = alpha_rate * frames;
|
||||
|
||||
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);
|
||||
context.move_to(0., text_display.extents().height());
|
||||
context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha);
|
||||
text_display.draw();
|
||||
}
|
||||
}
|
||||
@ -126,16 +122,16 @@ impl Animation for CrossFade {
|
||||
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 alpha = alpha_rate * frames;
|
||||
|
||||
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);
|
||||
context.move_to(0., text_display.extents().height());
|
||||
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);
|
||||
context.move_to(0., text_display.extents().height());
|
||||
context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha);
|
||||
text_display.draw();
|
||||
}
|
||||
}
|
||||
@ -163,9 +159,7 @@ impl Default for CyberScreenState {
|
||||
|
||||
impl CyberScreenState {
|
||||
fn new(script: Script) -> CyberScreenState {
|
||||
let mut s = CyberScreenState::default();
|
||||
s.script = script;
|
||||
s
|
||||
CyberScreenState { script, ..Default::default() }
|
||||
}
|
||||
|
||||
fn next_page(&mut self) -> Box<dyn Animation> {
|
||||
@ -260,7 +254,7 @@ impl CyberScreen {
|
||||
let s = s.clone();
|
||||
move |_, context, width, height| {
|
||||
let now = Instant::now();
|
||||
let _ = context.set_source_rgb(0., 0., 0.);
|
||||
context.set_source_rgb(0., 0., 0.);
|
||||
let _ = context.paint();
|
||||
|
||||
let pen = GlowPen::new(width, height, 2., 8., (0.7, 0., 1.));
|
||||
@ -293,7 +287,7 @@ impl CyberScreen {
|
||||
let _ = context.set_source(tracery);
|
||||
let _ = context.paint();
|
||||
|
||||
let mut animations = s.imp().animations.borrow_mut();
|
||||
let animations = s.imp().animations.borrow_mut();
|
||||
|
||||
let lr_margin = 50.;
|
||||
let max_width = width as f64 - lr_margin * 2.;
|
||||
|
@ -7,6 +7,7 @@ license = "GPL-3.0-only"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-std = "1.13.0"
|
||||
cairo-rs = { version = "0.18" }
|
||||
cyberpunk = { path = "../cyberpunk" }
|
||||
gio = { version = "0.18" }
|
||||
|
@ -1,5 +1,5 @@
|
||||
use cairo::{
|
||||
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern,
|
||||
Context, FontSlant, FontWeight, Format, ImageSurface, LinearGradient, Pattern,
|
||||
TextExtents,
|
||||
};
|
||||
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, SlashMeter};
|
||||
@ -497,8 +497,7 @@ fn main() {
|
||||
});
|
||||
|
||||
app.connect_activate(move |app| {
|
||||
let (gtk_tx, gtk_rx) =
|
||||
gtk::glib::MainContext::channel::<State>(gtk::glib::Priority::DEFAULT);
|
||||
let (gtk_tx, gtk_rx) = async_std::channel::unbounded();
|
||||
|
||||
let window = gtk::ApplicationWindow::new(app);
|
||||
window.present();
|
||||
@ -529,19 +528,25 @@ fn main() {
|
||||
});
|
||||
window.add_controller(keyboard_events);
|
||||
|
||||
gtk_rx.attach(None, move |state| {
|
||||
splash.set_state(state);
|
||||
glib::ControlFlow::Continue
|
||||
glib::spawn_future_local({
|
||||
let splash = splash.clone();
|
||||
async move {
|
||||
while let Ok(state) = gtk_rx.recv().await {
|
||||
println!("received state");
|
||||
splash.set_state(state);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::spawn({
|
||||
glib::spawn_future_local({
|
||||
let state = state.clone();
|
||||
move || {
|
||||
async move {
|
||||
state.write().unwrap().start();
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_millis(1000 / 60));
|
||||
async_std::task::sleep(Duration::from_millis(1000 / 60)).await;
|
||||
state.write().unwrap().run(Instant::now());
|
||||
let _ = gtk_tx.send(*state.read().unwrap());
|
||||
println!("state: {:?}", state.read().unwrap());
|
||||
let _ = gtk_tx.send(*state.read().unwrap()).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -274,7 +274,7 @@ impl<'a> Text<'a> {
|
||||
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);
|
||||
let _ = self.context.show_text(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -294,7 +294,7 @@ fn word_wrap(content: String, context: &Context, max_width: f64) -> Vec<String>
|
||||
lines.push(line.clone());
|
||||
}
|
||||
}
|
||||
if line.len() > 0 {
|
||||
if !line.is_empty() {
|
||||
lines.push(line);
|
||||
}
|
||||
lines
|
||||
|
@ -31,7 +31,9 @@ impl ApplicationWindow {
|
||||
|
||||
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 layout = gtk::Box::builder()
|
||||
|
@ -36,6 +36,7 @@ impl Default for TransitClock {
|
||||
s.set_draw_func({
|
||||
let s = s.clone();
|
||||
move |_, context, width, height| {
|
||||
#[allow(deprecated)]
|
||||
let style_context = WidgetExt::style_context(&s);
|
||||
let center_x = width as f64 / 2.;
|
||||
let center_y = height as f64 / 2.;
|
||||
@ -45,7 +46,9 @@ impl Default for TransitClock {
|
||||
let sunrise = info.sunrise - NaiveTime::from_hms_opt(0, 0, 0).unwrap();
|
||||
let sunset = info.sunset - NaiveTime::from_hms_opt(0, 0, 0).unwrap();
|
||||
|
||||
#[allow(deprecated)]
|
||||
let night_color = style_context.lookup_color("dark_5").unwrap();
|
||||
#[allow(deprecated)]
|
||||
let day_color = style_context.lookup_color("blue_1").unwrap();
|
||||
|
||||
PieChart::new(&style_context)
|
||||
|
@ -1,5 +1,7 @@
|
||||
use cairo::Context;
|
||||
use gtk::{gdk::RGBA, prelude::*, StyleContext};
|
||||
use gtk::{gdk::RGBA, prelude::*};
|
||||
#[allow(deprecated)]
|
||||
use gtk::StyleContext;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -27,7 +29,9 @@ pub struct PieChart {
|
||||
}
|
||||
|
||||
impl PieChart {
|
||||
#[allow(deprecated)]
|
||||
pub fn new(style_context: &StyleContext) -> Self {
|
||||
#[allow(deprecated)]
|
||||
Self {
|
||||
rotation: 0.,
|
||||
wedges: vec![],
|
||||
|
@ -132,7 +132,7 @@ impl SolunaClient {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use serde_json;
|
||||
|
||||
|
||||
const EXAMPLE: &str = "{\"sunRise\":\"7:15\",\"sunTransit\":\"12:30\",\"sunSet\":\"17:45\",\"moonRise\":null,\"moonTransit\":\"7:30\",\"moonUnder\":\"19:54\",\"moonSet\":\"15:02\",\"moonPhase\":\"Waning Crescent\",\"moonIllumination\":0.35889454647387764,\"sunRiseDec\":7.25,\"sunTransitDec\":12.5,\"sunSetDec\":17.75,\"moonRiseDec\":null,\"moonSetDec\":15.033333333333333,\"moonTransitDec\":7.5,\"moonUnderDec\":19.9,\"minor1Start\":null,\"minor1Stop\":null,\"minor2StartDec\":14.533333333333333,\"minor2Start\":\"14:32\",\"minor2StopDec\":15.533333333333333,\"minor2Stop\":\"15:32\",\"major1StartDec\":6.5,\"major1Start\":\"06:30\",\"major1StopDec\":8.5,\"major1Stop\":\"08:30\",\"major2StartDec\":18.9,\"major2Start\":\"18:54\",\"major2StopDec\":20.9,\"major2Stop\":\"20:54\",\"dayRating\":1,\"hourlyRating\":{\"0\":20,\"1\":20,\"2\":0,\"3\":0,\"4\":0,\"5\":0,\"6\":20,\"7\":40,\"8\":40,\"9\":20,\"10\":0,\"11\":0,\"12\":0,\"13\":0,\"14\":0,\"15\":20,\"16\":20,\"17\":20,\"18\":40,\"19\":20,\"20\":20,\"21\":20,\"22\":0,\"23\":0}}";
|
||||
|
||||
|
@ -9,6 +9,7 @@ documentation = "https://docs.rs/emseries"
|
||||
homepage = "https://github.com/luminescent-dreams/emseries"
|
||||
repository = "https://github.com/luminescent-dreams/emseries"
|
||||
categories = ["database-implementations"]
|
||||
edition = "2021"
|
||||
|
||||
include = [
|
||||
"**/*.rs",
|
||||
|
@ -10,7 +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 types::{Recordable, Timestamp};
|
||||
use crate::types::{Recordable, Timestamp};
|
||||
|
||||
/// This trait is used for constructing queries for searching the database.
|
||||
pub trait Criteria {
|
||||
|
@ -10,10 +10,6 @@ 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/>.
|
||||
*/
|
||||
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate uuid;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::ser::Serialize;
|
||||
use std::cmp::Ordering;
|
||||
@ -24,8 +20,8 @@ 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};
|
||||
use crate::criteria::Criteria;
|
||||
use crate::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
|
||||
|
11
file-service/Taskfile.yml
Normal file
11
file-service/Taskfile.yml
Normal file
@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- cargo build
|
||||
|
||||
lint:
|
||||
cmds:
|
||||
- cargo watch -x clippy
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use build_html::{self, Html, HtmlContainer};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@ -23,13 +25,14 @@ impl FromIterator<(&str, &str)> for Attributes {
|
||||
}
|
||||
*/
|
||||
|
||||
impl ToString for Attributes {
|
||||
fn to_string(&self) -> String {
|
||||
self.0
|
||||
impl Display for Attributes {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let result = self.0
|
||||
.iter()
|
||||
.map(|(key, value)| format!("{}=\"{}\"", key, value))
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
.join(" ");
|
||||
write!(f, "{}", result)
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,7 +205,7 @@ impl Html for Button {
|
||||
"<button {ty} {name} {attrs}>{label}</button>",
|
||||
name = name,
|
||||
label = self.label,
|
||||
attrs = self.attributes.to_string()
|
||||
attrs = self.attributes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -118,17 +118,21 @@ impl Deref for FileId {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
pub trait FileRoot {
|
||||
fn root(&self) -> PathBuf;
|
||||
}
|
||||
*/
|
||||
|
||||
pub struct Context(PathBuf);
|
||||
// pub struct Context(PathBuf);
|
||||
|
||||
/*
|
||||
impl FileRoot for Context {
|
||||
fn root(&self) -> PathBuf {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
pub struct Store {
|
||||
files_root: PathBuf,
|
||||
|
@ -153,7 +153,7 @@ mod test {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn it_allows_valid_dates() {
|
||||
let reference = chrono::NaiveDate::from_ymd_opt(2006, 01, 02).unwrap();
|
||||
let reference = chrono::NaiveDate::from_ymd_opt(2006, 1, 2).unwrap();
|
||||
let field = DateField::new(reference);
|
||||
field.imp().year.set_value(Some(2023));
|
||||
field.imp().month.set_value(Some(10));
|
||||
|
@ -80,10 +80,10 @@ impl TimeFormatter {
|
||||
0 => Err(ParseError),
|
||||
1 => Err(ParseError),
|
||||
2 => chrono::NaiveTime::from_hms_opt(parts[0], parts[1], 0)
|
||||
.map(|v| TimeFormatter(v))
|
||||
.map(TimeFormatter)
|
||||
.ok_or(ParseError),
|
||||
3 => chrono::NaiveTime::from_hms_opt(parts[0], parts[1], parts[2])
|
||||
.map(|v| TimeFormatter(v))
|
||||
.map(TimeFormatter)
|
||||
.ok_or(ParseError),
|
||||
_ => Err(ParseError),
|
||||
}
|
||||
|
@ -443,7 +443,7 @@ mod test {
|
||||
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
|
||||
let id = RecordId::default();
|
||||
let record = Record {
|
||||
id: id,
|
||||
id,
|
||||
data: record,
|
||||
};
|
||||
self.put_records.write().unwrap().push(record.clone());
|
||||
@ -509,7 +509,7 @@ mod test {
|
||||
Record {
|
||||
id: RecordId::default(),
|
||||
data: TraxRecord::TimeDistance(ft_core::TimeDistance {
|
||||
datetime: oct_13_am.clone(),
|
||||
datetime: oct_13_am,
|
||||
activity: TimeDistanceActivity::Biking,
|
||||
distance: Some(15000. * si::M),
|
||||
duration: Some(3600. * si::S),
|
||||
|
@ -144,7 +144,7 @@ impl HistoricalView {
|
||||
let mut model = gio::ListStore::new::<Date>();
|
||||
let mut days = interval.days().map(Date::new).collect::<Vec<Date>>();
|
||||
days.reverse();
|
||||
model.extend(days.into_iter());
|
||||
model.extend(days);
|
||||
self.imp()
|
||||
.list_view
|
||||
.set_model(Some(>k::NoSelection::new(Some(model))));
|
||||
|
@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with Fit
|
||||
use chrono::SecondsFormat;
|
||||
use chrono_tz::Etc::UTC;
|
||||
use dimensioned::si;
|
||||
use emseries::{Record, RecordId, Series, Timestamp};
|
||||
use emseries::{Record, RecordId};
|
||||
use ft_core::{self, DurationWorkout, DurationWorkoutActivity, SetRepActivity, TraxRecord};
|
||||
use serde::{
|
||||
de::{self, Visitor},
|
||||
@ -26,7 +26,7 @@ use serde::{
|
||||
use std::{
|
||||
fmt,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read},
|
||||
io::{BufRead, BufReader},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
|
18
flake.lock
generated
18
flake.lock
generated
@ -5,11 +5,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -35,11 +35,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681303793,
|
||||
"narHash": "sha256-JEdQHsYuCfRL2PICHlOiH/2ue3DwoxUX7DJ6zZxZXFk=",
|
||||
"lastModified": 1714906307,
|
||||
"narHash": "sha256-UlRZtrCnhPFSJlDQE7M0eyhgvuuHBTe1eJ9N9AQlJQ0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fe2ecaf706a5907b5e54d979fbde4924d84b65fc",
|
||||
"rev": "25865a40d14b3f9cf19f19b924e2ab4069b09588",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -76,11 +76,11 @@
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1698205128,
|
||||
"narHash": "sha256-jP+81TkldLtda8bzmsBWahETGsyFkoDOCT244YkA+S4=",
|
||||
"lastModified": 1731966246,
|
||||
"narHash": "sha256-e/V7Ffm5wPd9DVzCThnPZ7lFxd43bb64tSk8/oGP4Ag=",
|
||||
"owner": "1Password",
|
||||
"repo": "typeshare",
|
||||
"rev": "c3ee2ad8f27774c45db7af4f2ba746c4ae11de21",
|
||||
"rev": "e0e5f27ee34d7d4da76a9dc96a11552e98be56da",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -22,6 +22,7 @@
|
||||
name = "ld-tools-devshell";
|
||||
buildInputs = [
|
||||
pkgs.cargo-nextest
|
||||
pkgs.cargo-watch
|
||||
pkgs.clang
|
||||
pkgs.crate2nix
|
||||
pkgs.glib
|
||||
@ -44,6 +45,7 @@
|
||||
pkgs.sqlx-cli
|
||||
pkgs.udev
|
||||
pkgs.wasm-pack
|
||||
pkgs.go-task
|
||||
typeshare.packages."x86_64-linux".default
|
||||
pkgs.nodePackages_latest.typescript-language-server
|
||||
];
|
||||
@ -84,7 +86,7 @@
|
||||
cyber-slides = cargo_nix.workspaceMembers.cyber-slides.build;
|
||||
cyberpunk-splash = cargo_nix.workspaceMembers.cyberpunk-splash.build;
|
||||
dashboard = cargo_nix.workspaceMembers.dashboard.build;
|
||||
file-service = cargo_nix.workspaceMembers.file-service.build;
|
||||
# file-service = cargo_nix.workspaceMembers.file-service.build;
|
||||
fitnesstrax = cargo_nix.workspaceMembers.fitnesstrax.build;
|
||||
otg-gtk = cargo_nix.workspaceMembers.otg-gtk.build;
|
||||
|
||||
@ -94,7 +96,7 @@
|
||||
cyber-slides
|
||||
cyberpunk-splash
|
||||
dashboard
|
||||
file-service
|
||||
# file-service
|
||||
fitnesstrax
|
||||
otg-gtk
|
||||
];
|
||||
|
@ -294,21 +294,21 @@ mod tests {
|
||||
use fluent_bundle::{FluentArgs, FluentValue};
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
||||
const EN_TRANSLATIONS: &'static str = "
|
||||
const EN_TRANSLATIONS: &str = "
|
||||
preferences = Preferences
|
||||
history = History
|
||||
time_display = {$time} during the day
|
||||
nested_display = nesting a time display: {time_display}
|
||||
";
|
||||
|
||||
const EO_TRANSLATIONS: &'static str = "
|
||||
const EO_TRANSLATIONS: &str = "
|
||||
history = Historio
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn translations() {
|
||||
let en_id = "en-US".parse::<LanguageIdentifier>().unwrap();
|
||||
let mut fluent = FluentErgo::new(&vec![en_id.clone()]);
|
||||
let mut fluent = FluentErgo::new(&[en_id.clone()]);
|
||||
fluent
|
||||
.add_from_text(en_id, String::from(EN_TRANSLATIONS))
|
||||
.expect("text should load");
|
||||
@ -322,7 +322,7 @@ history = Historio
|
||||
fn translation_fallback() {
|
||||
let eo_id = "eo".parse::<LanguageIdentifier>().unwrap();
|
||||
let en_id = "en".parse::<LanguageIdentifier>().unwrap();
|
||||
let mut fluent = FluentErgo::new(&vec![eo_id.clone(), en_id.clone()]);
|
||||
let mut fluent = FluentErgo::new(&[eo_id.clone(), en_id.clone()]);
|
||||
fluent
|
||||
.add_from_text(en_id, String::from(EN_TRANSLATIONS))
|
||||
.expect("text should load");
|
||||
@ -342,7 +342,7 @@ history = Historio
|
||||
#[test]
|
||||
fn placeholder_insertion_should_strip_placeholder_markers() {
|
||||
let en_id = "en".parse::<LanguageIdentifier>().unwrap();
|
||||
let mut fluent = FluentErgo::new(&vec![en_id.clone()]);
|
||||
let mut fluent = FluentErgo::new(&[en_id.clone()]);
|
||||
fluent
|
||||
.add_from_text(en_id, String::from(EN_TRANSLATIONS))
|
||||
.expect("text should load");
|
||||
@ -357,7 +357,7 @@ history = Historio
|
||||
#[test]
|
||||
fn placeholder_insertion_should_strip_nested_placeholder_markers() {
|
||||
let en_id = "en".parse::<LanguageIdentifier>().unwrap();
|
||||
let mut fluent = FluentErgo::new(&vec![en_id.clone()]);
|
||||
let mut fluent = FluentErgo::new(&[en_id.clone()]);
|
||||
fluent
|
||||
.add_from_text(en_id, String::from(EN_TRANSLATIONS))
|
||||
.expect("text should load");
|
||||
|
@ -7,8 +7,8 @@ use std::iter::Iterator;
|
||||
#[derive(Clone)]
|
||||
pub struct ApplicationWindow {
|
||||
pub window: adw::ApplicationWindow,
|
||||
pub layout: gtk::FlowBox,
|
||||
pub playlists: Vec<PlaylistCard>,
|
||||
// pub layout: gtk::FlowBox,
|
||||
// pub playlists: Vec<PlaylistCard>,
|
||||
}
|
||||
|
||||
impl ApplicationWindow {
|
||||
@ -31,7 +31,9 @@ impl ApplicationWindow {
|
||||
|
||||
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 layout = gtk::FlowBox::new();
|
||||
@ -57,8 +59,10 @@ impl ApplicationWindow {
|
||||
|
||||
Self {
|
||||
window,
|
||||
/*
|
||||
layout,
|
||||
playlists,
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,12 +27,12 @@ impl State {
|
||||
|
||||
fn add_audio(&self, device: String) {
|
||||
let mut st = self.internal.write().unwrap();
|
||||
(*st).device_list.push(device);
|
||||
st.device_list.push(device);
|
||||
}
|
||||
|
||||
fn audio_devices(&self) -> Vec<String> {
|
||||
let st = self.internal.read().unwrap();
|
||||
(*st).device_list.clone()
|
||||
st.device_list.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,8 +107,6 @@ impl ObjectSubclass for HexGridWindowPrivate {
|
||||
layout.append(&drawing_area);
|
||||
layout.append(&sidebar);
|
||||
|
||||
layout.show();
|
||||
|
||||
Self {
|
||||
drawing_area,
|
||||
hex_address,
|
||||
|
400
ifc/Cargo.lock
generated
400
ifc/Cargo.lock
generated
@ -1,400 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxxbridge-flags",
|
||||
"cxxbridge-macro",
|
||||
"link-cplusplus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"scratch",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"cxx-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "international-fixed-calendar"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scratch"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.152"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.152"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "ifc"
|
||||
description = "chrono-compatible-ish date objects for the International Fixed Calendar"
|
||||
version = "0.1.0"
|
||||
authors = ["Savanni D'Gerinel <savanni@luminescent-dreams.com>"]
|
||||
edition = "2018"
|
||||
keywords = ["date", "time", "calendar"]
|
||||
categories = ["date-and-time"]
|
||||
license = "GPL-3.0-only"
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = { version = "1" }
|
@ -1,8 +0,0 @@
|
||||
# International Fixed Calendar
|
||||
|
||||
This is a fun project implementing a library for the [International Fixed Calendar](https://en.wikipedia.org/wiki/International_Fixed_Calendar).
|
||||
|
||||
This is at least somewhat compatible with [Chrono](https://github.com/chronotope/chrono), in that I have implemented these traits:
|
||||
|
||||
* `From<NaiveDate>`
|
||||
* `Datelike`
|
1042
ifc/src/lib.rs
1042
ifc/src/lib.rs
File diff suppressed because it is too large
Load Diff
@ -1,34 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title> {{month}} {{year}} </title>
|
||||
<link href="/css" rel="stylesheet" type="text/css" media="screen" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1> IFC Fixed Calendar: {{month}}, {{year}} years after the invention of agriculture </h1>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> Sunday </th>
|
||||
<th> Monday </th>
|
||||
<th> Tuesday </th>
|
||||
<th> Wednesday </th>
|
||||
<th> Thursday </th>
|
||||
<th> Friday </th>
|
||||
<th> Saturday </th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#weeks}}
|
||||
<tr>
|
||||
{{#days}}
|
||||
<td class="{{highlight}}"> {{day}} </td>
|
||||
{{/days}}
|
||||
</tr>
|
||||
{{/weeks}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,35 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>{{month}} {{year}}</title>
|
||||
<link href="/css" rel="stylesheet" type="text/css" media="screen" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
IFC Fixed Calendar: {{month}}, {{year}} years after the invention of
|
||||
agriculture
|
||||
</h1>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sunday</th>
|
||||
<th>Monday</th>
|
||||
<th>Tuesday</th>
|
||||
<th>Wednesday</th>
|
||||
<th>Thursday</th>
|
||||
<th>Friday</th>
|
||||
<th>Saturday</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#weeks}}
|
||||
<tr>
|
||||
{{#days}}
|
||||
<td class="{{highlight}}">{{day}}</td>
|
||||
{{/days}}
|
||||
</tr>
|
||||
{{/weeks}}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@ -1,12 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>{{day_out_of_time}} {{year}}</title>
|
||||
<link href="/css" rel="stylesheet" type="text/css" media="screen" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
IFC Fixed Calendar: {{day_out_of_time}}, {{year}} years after the
|
||||
invention of agriculture
|
||||
</h1>
|
||||
</body>
|
||||
</html>
|
@ -1,18 +0,0 @@
|
||||
|
||||
table {
|
||||
width: 98%;
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
width: 14%;
|
||||
font-family: sans-serif;
|
||||
font-size: larger;
|
||||
border: 1px solid black;
|
||||
padding: 1em 0em 5em 1em;
|
||||
}
|
||||
|
||||
.today {
|
||||
background-color: rgb(200, 200, 255);
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
/*
|
||||
Copyright 2020-2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||
|
||||
This file is part of the Luminescent Dreams Tools.
|
||||
|
||||
Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
extern crate chrono;
|
||||
extern crate chrono_tz;
|
||||
extern crate ifc as IFC;
|
||||
|
||||
use chrono::{Datelike, Utc};
|
||||
|
||||
fn main() {
|
||||
let d = IFC::IFC::from(Utc::today());
|
||||
println!("{} {}, {}", d.month(), d.day(), d.year());
|
||||
}
|
119
ifc/src/web.rs
119
ifc/src/web.rs
@ -1,119 +0,0 @@
|
||||
/*
|
||||
Copyright 2020-2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||
|
||||
This file is part of the Luminescent Dreams Tools.
|
||||
|
||||
Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use chrono::{Datelike, Utc};
|
||||
use ifc as IFC;
|
||||
use iron::headers;
|
||||
use iron::middleware::Handler;
|
||||
use iron::modifiers::Header;
|
||||
use iron::prelude::*;
|
||||
use iron::status;
|
||||
use mustache::{compile_str, Template};
|
||||
use router::Router;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const STYLES: &'static str = include_str!("static/styles.css");
|
||||
pub const INDEX: &'static str = include_str!("static/index.html");
|
||||
|
||||
pub struct IndexHandler {
|
||||
pub template: Template,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DayEntry {
|
||||
day: u8,
|
||||
highlight: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Week {
|
||||
days: Vec<DayEntry>,
|
||||
}
|
||||
|
||||
impl Week {
|
||||
fn new(start_day: u8, today: u8) -> Week {
|
||||
Week {
|
||||
days: (1..8)
|
||||
.map(|d| DayEntry {
|
||||
day: d + start_day,
|
||||
highlight: if today == (d + start_day) {
|
||||
String::from("today")
|
||||
} else {
|
||||
String::from("")
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct IndexTemplateParams {
|
||||
month: String,
|
||||
year: i32,
|
||||
weeks: Vec<Week>,
|
||||
}
|
||||
|
||||
impl IndexTemplateParams {
|
||||
fn new(date: IFC::IFC) -> IndexTemplateParams {
|
||||
let day = date.day() as u8;
|
||||
IndexTemplateParams {
|
||||
month: String::from(IFC::Month::from(date.month())),
|
||||
year: date.year(),
|
||||
weeks: (0..4).map(|wn| Week::new(wn * 7, day)).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler for IndexHandler {
|
||||
fn handle(&self, _: &mut Request) -> IronResult<Response> {
|
||||
let d = IFC::IFC::from(Utc::today());
|
||||
Ok(Response::with((
|
||||
status::Ok,
|
||||
Header(headers::ContentType(iron::mime::Mime(
|
||||
iron::mime::TopLevel::Text,
|
||||
iron::mime::SubLevel::Html,
|
||||
vec![],
|
||||
))),
|
||||
self.template
|
||||
.render_to_string(&IndexTemplateParams::new(d))
|
||||
.expect("the template to render"),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn css(_: &mut Request) -> IronResult<Response> {
|
||||
Ok(Response::with((
|
||||
status::Ok,
|
||||
Header(headers::ContentType(iron::mime::Mime(
|
||||
iron::mime::TopLevel::Text,
|
||||
iron::mime::SubLevel::Css,
|
||||
vec![],
|
||||
))),
|
||||
STYLES,
|
||||
)))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut router = Router::new();
|
||||
router.get(
|
||||
"/",
|
||||
IndexHandler {
|
||||
template: compile_str(INDEX).expect("the template to compile"),
|
||||
},
|
||||
"index",
|
||||
);
|
||||
|
||||
router.get("/css", css, "styles");
|
||||
|
||||
Iron::new(router).http("127.0.0.1:3000").unwrap();
|
||||
}
|
@ -78,7 +78,7 @@ mod tests {
|
||||
})
|
||||
.await;
|
||||
assert_eq!(value, Value(16));
|
||||
assert_eq!(*run.read().unwrap(), true);
|
||||
assert!(*run.read().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@ -97,6 +97,6 @@ mod tests {
|
||||
})
|
||||
.await;
|
||||
assert_eq!(value, Value(15));
|
||||
assert_eq!(*run.read().unwrap(), false);
|
||||
assert!(!(*run.read().unwrap()));
|
||||
}
|
||||
}
|
||||
|
@ -80,12 +80,12 @@ mod tests {
|
||||
use super::*;
|
||||
use cool_asserts::assert_matches;
|
||||
|
||||
const DATA: &'static str = "15";
|
||||
const DATA: &str = "15";
|
||||
|
||||
#[test]
|
||||
fn function() {
|
||||
let resp = parse_number_a::<nom::error::VerboseError<&str>>()
|
||||
.map(|val| Container(val))
|
||||
.map(Container)
|
||||
.parse(DATA);
|
||||
assert_matches!(resp, Ok((_, content)) =>
|
||||
assert_eq!(content, Container(15))
|
||||
@ -95,7 +95,7 @@ mod tests {
|
||||
#[test]
|
||||
fn parser() {
|
||||
let resp = parse_number_b::<nom::error::VerboseError<&str>>()
|
||||
.map(|val| Container(val))
|
||||
.map(Container)
|
||||
.parse(DATA);
|
||||
assert_matches!(resp, Ok((_, content)) =>
|
||||
assert_eq!(content, Container(15))
|
||||
|
@ -396,8 +396,7 @@ mod test {
|
||||
(Coordinate { column: 17, row: 0 }, Color::White),
|
||||
(Coordinate { column: 17, row: 1 }, Color::White),
|
||||
(Coordinate { column: 18, row: 1 }, Color::White),
|
||||
]
|
||||
.into_iter(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
test(board);
|
||||
@ -436,33 +435,32 @@ mod test {
|
||||
},
|
||||
Color::Black,
|
||||
),
|
||||
]
|
||||
.into_iter(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(board.group(&Coordinate { column: 18, row: 3 }).is_none());
|
||||
assert_eq!(
|
||||
board
|
||||
.group(&Coordinate { column: 3, row: 3 })
|
||||
.map(|g| board.liberties(&g)),
|
||||
.map(|g| board.liberties(g)),
|
||||
Some(4)
|
||||
);
|
||||
assert_eq!(
|
||||
board
|
||||
.group(&Coordinate { column: 0, row: 3 })
|
||||
.map(|g| board.liberties(&g)),
|
||||
.map(|g| board.liberties(g)),
|
||||
Some(3)
|
||||
);
|
||||
assert_eq!(
|
||||
board
|
||||
.group(&Coordinate { column: 0, row: 0 })
|
||||
.map(|g| board.liberties(&g)),
|
||||
.map(|g| board.liberties(g)),
|
||||
Some(2)
|
||||
);
|
||||
assert_eq!(
|
||||
board
|
||||
.group(&Coordinate { column: 18, row: 9 })
|
||||
.map(|g| board.liberties(&g)),
|
||||
.map(|g| board.liberties(g)),
|
||||
Some(3)
|
||||
);
|
||||
assert_eq!(
|
||||
@ -471,7 +469,7 @@ mod test {
|
||||
column: 18,
|
||||
row: 18
|
||||
})
|
||||
.map(|g| board.liberties(&g)),
|
||||
.map(|g| board.liberties(g)),
|
||||
Some(2)
|
||||
);
|
||||
}
|
||||
@ -614,7 +612,7 @@ mod test {
|
||||
for (board, coordinate, group, liberties) in test_cases {
|
||||
assert_eq!(board.group(&coordinate), group.as_ref());
|
||||
assert_eq!(
|
||||
board.group(&coordinate).map(|g| board.liberties(&g)),
|
||||
board.group(&coordinate).map(|g| board.liberties(g)),
|
||||
liberties,
|
||||
"{:?}",
|
||||
coordinate
|
||||
@ -688,11 +686,11 @@ mod test {
|
||||
fn validate_group_comparisons() {
|
||||
{
|
||||
let b1 = Goban::from_coordinates(
|
||||
vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(),
|
||||
vec![(Coordinate { column: 7, row: 9 }, Color::White)],
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Goban::from_coordinates(
|
||||
vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(),
|
||||
vec![(Coordinate { column: 7, row: 9 }, Color::White)],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -704,16 +702,14 @@ mod test {
|
||||
vec![
|
||||
(Coordinate { column: 7, row: 9 }, Color::White),
|
||||
(Coordinate { column: 8, row: 10 }, Color::White),
|
||||
]
|
||||
.into_iter(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Goban::from_coordinates(
|
||||
vec![
|
||||
(Coordinate { column: 8, row: 10 }, Color::White),
|
||||
(Coordinate { column: 7, row: 9 }, Color::White),
|
||||
]
|
||||
.into_iter(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -732,8 +728,7 @@ mod test {
|
||||
(Coordinate { column: 10, row: 9 }, Color::Black),
|
||||
(Coordinate { column: 9, row: 8 }, Color::Black),
|
||||
(Coordinate { column: 9, row: 10 }, Color::Black),
|
||||
]
|
||||
.into_iter(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -587,8 +587,7 @@ mod test {
|
||||
(Coordinate { column: 17, row: 0 }, Color::White),
|
||||
(Coordinate { column: 17, row: 1 }, Color::White),
|
||||
(Coordinate { column: 18, row: 1 }, Color::White),
|
||||
]
|
||||
.into_iter(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
state.current_player = Color::Black;
|
||||
@ -612,8 +611,7 @@ mod test {
|
||||
(Coordinate { column: 10, row: 9 }, Color::Black),
|
||||
(Coordinate { column: 9, row: 8 }, Color::Black),
|
||||
(Coordinate { column: 9, row: 10 }, Color::Black),
|
||||
]
|
||||
.into_iter(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -132,10 +132,10 @@ impl GameReviewViewModel {
|
||||
// the board state by applying the child.
|
||||
pub fn next_move(&self) {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let current_position = inner.current_position.clone();
|
||||
let current_position = inner.current_position;
|
||||
match current_position {
|
||||
Some(current_position) => {
|
||||
let current_id = current_position.clone();
|
||||
let current_id = current_position;
|
||||
let node = inner.game.trees[0].get(current_id).unwrap();
|
||||
if let Some(next_id) = node.first_child().map(|child| child.node_id()) {
|
||||
inner.current_position = Some(next_id);
|
||||
@ -180,7 +180,7 @@ mod test {
|
||||
where
|
||||
F: FnOnce(GameReviewViewModel),
|
||||
{
|
||||
let records = sgf::parse_sgf_file(&Path::new("../../sgf/test_data/branch_test.sgf"))
|
||||
let records = sgf::parse_sgf_file(Path::new("../../sgf/test_data/branch_test.sgf"))
|
||||
.expect("to successfully load the test file");
|
||||
let record = records[0]
|
||||
.as_ref()
|
||||
|
@ -18,7 +18,7 @@ use crate::{CoreApi, ResourceManager};
|
||||
use adw::prelude::*;
|
||||
|
||||
use glib::Propagation;
|
||||
use gtk::{gdk::Key, EventControllerKey};
|
||||
use gtk::EventControllerKey;
|
||||
use otg_core::{
|
||||
settings::{SettingsRequest, SettingsResponse},
|
||||
CoreRequest, CoreResponse, GameReviewViewModel,
|
||||
|
@ -102,14 +102,8 @@ impl GameReview {
|
||||
}
|
||||
}
|
||||
|
||||
match *s.goban.borrow_mut() {
|
||||
Some(ref mut goban) => goban.set_board_state(view.game_view()),
|
||||
None => {}
|
||||
};
|
||||
match *s.review_tree.borrow() {
|
||||
Some(ref tree) => tree.queue_draw(),
|
||||
None => {}
|
||||
}
|
||||
if let Some(ref mut goban) = *s.goban.borrow_mut() { goban.set_board_state(view.game_view()) };
|
||||
if let Some(ref tree) = *s.review_tree.borrow() { tree.queue_draw() }
|
||||
Propagation::Stop
|
||||
}
|
||||
});
|
||||
@ -169,9 +163,6 @@ impl GameReview {
|
||||
*self.review_tree.borrow_mut() = Some(review_tree);
|
||||
}
|
||||
|
||||
fn redraw(&self) {
|
||||
}
|
||||
|
||||
pub fn widget(&self) -> gtk::Widget {
|
||||
self.widget.clone().upcast::<gtk::Widget>()
|
||||
}
|
||||
|
11
pico-st7789/.cargo/config.toml
Normal file
11
pico-st7789/.cargo/config.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[build]
|
||||
target = "thumbv6m-none-eabi"
|
||||
|
||||
[target.thumbv6m-none-eabi]
|
||||
rustflags = [
|
||||
"-C", "link-arg=--nmagic",
|
||||
"-C", "link-arg=-Tlink.x",
|
||||
"-C", "no-vectorize-loops",
|
||||
]
|
||||
|
||||
runner = "elf2uf2-rs -d"
|
11
pico-st7789/Cargo.toml
Normal file
11
pico-st7789/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "pico-st7789"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cortex-m-rt = "0.7.3"
|
||||
embedded-hal = "1.0.0"
|
||||
fugit = "0.3.7"
|
||||
panic-halt = "1.0.0"
|
||||
rp-pico = "0.9.0"
|
140
pico-st7789/src/main.rs
Normal file
140
pico-st7789/src/main.rs
Normal file
@ -0,0 +1,140 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
use embedded_hal::{delay::DelayNs, digital::OutputPin, spi::SpiBus};
|
||||
use fugit::RateExtU32;
|
||||
use panic_halt as _;
|
||||
use rp_pico::{
|
||||
entry,
|
||||
hal::{
|
||||
clocks::init_clocks_and_plls,
|
||||
gpio::{FunctionSio, Pin, PinId, PullDown, SioOutput},
|
||||
spi::{Enabled, Spi, SpiDevice, ValidSpiPinout},
|
||||
Clock, Sio, Timer, Watchdog,
|
||||
},
|
||||
pac, Pins,
|
||||
};
|
||||
|
||||
mod st7789;
|
||||
use st7789::{ST7789Display, SETUP_PROGRAM};
|
||||
pub use st7789::Step;
|
||||
|
||||
const XOSC_CRYSTAL_FREQ: u32 = 12_000_000; // MHz, https://forums.raspberrypi.com/viewtopic.php?t=356764
|
||||
|
||||
const ROWS: usize = 320;
|
||||
const COLUMNS: usize = 170;
|
||||
const FRAMEBUF: usize = ROWS * COLUMNS * 3;
|
||||
|
||||
/*
|
||||
struct Frame {
|
||||
columns: usize,
|
||||
buf: [u8; FRAMEBUF],
|
||||
}
|
||||
*/
|
||||
|
||||
#[entry]
|
||||
unsafe fn main() -> ! {
|
||||
// rp_pico::pac::Peripherals is a reference to physical hardware defined on the Pico.
|
||||
let mut peripherals = pac::Peripherals::take().unwrap();
|
||||
|
||||
// SIO inidcates "Single Cycle IO". I don't know what this means, but it could mean that this
|
||||
// is a class of IO operations that can be run in a single clock cycle, such as switching a
|
||||
// GPIO pin on or off.
|
||||
let sio = Sio::new(peripherals.SIO);
|
||||
|
||||
// Many of the following systems require a watchdog. I do not know what this does, either, but
|
||||
// it may be some failsafe software that will reset operations if the watchdog detects a lack
|
||||
// of activity.
|
||||
let mut watchdog = Watchdog::new(peripherals.WATCHDOG);
|
||||
|
||||
// Here we grab the GPIO pins in bank 0.
|
||||
let pins = Pins::new(
|
||||
peripherals.IO_BANK0,
|
||||
peripherals.PADS_BANK0,
|
||||
sio.gpio_bank0,
|
||||
&mut peripherals.RESETS,
|
||||
);
|
||||
|
||||
// Initialize an abstraction of the clock system with a batch of standard hardware clocks.
|
||||
let clocks = init_clocks_and_plls(
|
||||
XOSC_CRYSTAL_FREQ,
|
||||
peripherals.XOSC,
|
||||
peripherals.CLOCKS,
|
||||
peripherals.PLL_SYS,
|
||||
peripherals.PLL_USB,
|
||||
&mut peripherals.RESETS,
|
||||
&mut watchdog,
|
||||
)
|
||||
.ok()
|
||||
.unwrap();
|
||||
|
||||
// An abstraction for a timer which we can use to delay the code.
|
||||
let mut timer = Timer::new(peripherals.TIMER, &mut peripherals.RESETS, &clocks);
|
||||
|
||||
let mut led = pins.led.into_function();
|
||||
|
||||
// Grab the clock and data pins for SPI1. For Clock pins and for Data pins, there are only two
|
||||
// pins each on the Pico which can function for SPI1.
|
||||
let spi_clk = pins.gpio2.into_function();
|
||||
let spi_sdo = pins.gpio3.into_function();
|
||||
// let spi_sdi = pins.gpio4.into_function();
|
||||
// Chip select 1 means the chip is not enabled
|
||||
let mut board_select = pins.gpio13.into_function();
|
||||
let mut data_command = pins.gpio15.into_function();
|
||||
let mut reset = pins.gpio14.into_function();
|
||||
|
||||
let _ = reset.set_low();
|
||||
let _ = board_select.set_high();
|
||||
let _ = data_command.set_high();
|
||||
|
||||
// Now, create the SPI function abstraction for SPI1 with spi_clk and spi_sdo.
|
||||
let mut spi = Spi::<_, _, _, 8>::new(peripherals.SPI0, (spi_sdo, spi_clk)).init(
|
||||
&mut peripherals.RESETS,
|
||||
// The SPI system uses the peripheral clock
|
||||
clocks.peripheral_clock.freq(),
|
||||
// Transmit data at a rate of 1Mbit.
|
||||
1_u32.MHz(),
|
||||
// Run with SPI Mode 1. This means that the clock line should start high and that data will
|
||||
// be sampled starting at the first falling edge.
|
||||
embedded_hal::spi::MODE_3,
|
||||
);
|
||||
|
||||
let mut display = ST7789Display::new(board_select, data_command, spi);
|
||||
|
||||
let _ = reset.set_high();
|
||||
timer.delay_ms(10);
|
||||
for step in SETUP_PROGRAM {
|
||||
let mut display = display.acquire();
|
||||
display.send_command(&step, &mut timer);
|
||||
}
|
||||
|
||||
timer.delay_ms(1000);
|
||||
|
||||
let mut frame: [u8; FRAMEBUF] = [0; FRAMEBUF];
|
||||
|
||||
let mut strength = 0;
|
||||
loop {
|
||||
led.set_high();
|
||||
{
|
||||
let display = display.acquire();
|
||||
display.send_buf(&frame);
|
||||
}
|
||||
|
||||
for x in 80..90 {
|
||||
for y in 155..165 {
|
||||
write_pixel(&mut frame, x, y, (0, 0, 63));
|
||||
}
|
||||
}
|
||||
|
||||
timer.delay_ms(10);
|
||||
led.set_low();
|
||||
|
||||
timer.delay_ms(1000);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_pixel(framebuf: &mut [u8], x: usize, y: usize, color: (u8, u8, u8)) {
|
||||
framebuf[(y * COLUMNS + x) * 3 + 0] = color.0 << 2;
|
||||
framebuf[(y * COLUMNS + x) * 3 + 1] = color.1 << 2;
|
||||
framebuf[(y * COLUMNS + x) * 3 + 2] = color.2 << 2;
|
||||
}
|
204
pico-st7789/src/st7789.rs
Normal file
204
pico-st7789/src/st7789.rs
Normal file
@ -0,0 +1,204 @@
|
||||
use embedded_hal::{delay::DelayNs, digital::OutputPin, spi::SpiBus};
|
||||
use rp_pico::hal::{
|
||||
gpio::{FunctionSio, Pin, PinId, PullDown, SioOutput},
|
||||
spi::{Enabled, SpiDevice, ValidSpiPinout},
|
||||
Spi, Timer,
|
||||
};
|
||||
|
||||
pub struct Step {
|
||||
param_cnt: usize,
|
||||
command: u8,
|
||||
params: [u8; 4],
|
||||
delay: Option<u32>,
|
||||
}
|
||||
|
||||
impl Step {
|
||||
pub fn send_command<D, Pinout, P>(
|
||||
&self,
|
||||
spi: &mut Spi<Enabled, D, Pinout, 8>,
|
||||
data_command: &mut Pin<P, FunctionSio<SioOutput>, PullDown>,
|
||||
) where
|
||||
D: SpiDevice,
|
||||
Pinout: ValidSpiPinout<D>,
|
||||
P: PinId,
|
||||
{
|
||||
let _ = data_command.set_low();
|
||||
let _ = spi.write(&[self.command]);
|
||||
if self.param_cnt > 0 {
|
||||
let _ = data_command.set_high();
|
||||
let _ = spi.write(&self.params[0..self.param_cnt]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NOP: u8 = 0x00;
|
||||
|
||||
const SWRESET: Step = Step {
|
||||
param_cnt: 0,
|
||||
command: 0x01,
|
||||
params: [0, 0, 0, 0],
|
||||
delay: Some(150),
|
||||
};
|
||||
const SLPOUT: Step = Step {
|
||||
param_cnt: 0,
|
||||
command: 0x11,
|
||||
params: [0, 0, 0, 0],
|
||||
delay: Some(10),
|
||||
};
|
||||
const COLMOD: u8 = 0x3a;
|
||||
const MADCTL: Step = Step {
|
||||
param_cnt: 1,
|
||||
command: 0x36,
|
||||
params: [0x00, 0, 0, 0],
|
||||
delay: None,
|
||||
};
|
||||
const CASET: u8 = 0x2a;
|
||||
const RASET: u8 = 0x2b;
|
||||
const INVON: Step = Step {
|
||||
param_cnt: 0,
|
||||
command: 0x21,
|
||||
params: [0, 0, 0, 0],
|
||||
delay: Some(10),
|
||||
};
|
||||
const NORON: Step = Step {
|
||||
param_cnt: 0,
|
||||
command: 0x13,
|
||||
params: [0, 0, 0, 0],
|
||||
delay: Some(10),
|
||||
};
|
||||
const DISPOFF: Step = Step {
|
||||
param_cnt: 0,
|
||||
command: 0x28,
|
||||
params: [0, 0, 0, 0],
|
||||
delay: Some(10),
|
||||
};
|
||||
const DISPON: Step = Step {
|
||||
param_cnt: 0,
|
||||
command: 0x29,
|
||||
params: [0, 0, 0, 0],
|
||||
delay: Some(10),
|
||||
};
|
||||
const RAMWR: u8 = 0x2c;
|
||||
|
||||
// Adafruit setup instructions
|
||||
// SWRESET (0x01), 150ms delay
|
||||
// SLPOUT (0x11), 10ms delay
|
||||
// COLMOD (0x3a) 0x55 (65K RGB, 16bit/pixel), 10ms delay
|
||||
// MADCTL (0x36) 0x00,
|
||||
// memory data access control, RGB
|
||||
// CASET 0x00, 0, 0, 170,
|
||||
// column address set, 4 parameters
|
||||
// 0x00, 0x00 indicates xstart is 0
|
||||
// 0x00, 170 indicates xend is 170
|
||||
// RASET 0x00, 0, 320 >> 8, 320 & 0xFF,
|
||||
// row address set, 4 parameters
|
||||
// 0x00, 0x00 indicates ystart is 0
|
||||
// 3230 >> 8, 320 & 0xff indicates that 320 is the last y address
|
||||
// INVON, 10ms delay
|
||||
// invert the display
|
||||
// NORON, 10ms delay
|
||||
// normal display mode
|
||||
// DISPON, 10ms delay
|
||||
// turn the display on
|
||||
|
||||
pub const SETUP_PROGRAM: [Step; 8] = [
|
||||
SWRESET,
|
||||
SLPOUT,
|
||||
Step {
|
||||
param_cnt: 1,
|
||||
command: COLMOD,
|
||||
params: [0x66, 0, 0, 0],
|
||||
delay: Some(10),
|
||||
},
|
||||
MADCTL,
|
||||
Step {
|
||||
param_cnt: 4,
|
||||
command: CASET,
|
||||
params: [0, 35, 0, 204],
|
||||
delay: None,
|
||||
},
|
||||
/*
|
||||
Step {
|
||||
param_cnt: 4,
|
||||
command: RASET,
|
||||
params: [0, 0, (320 >> 8) as u8, (320 & 0xff) as u8],
|
||||
delay: None,
|
||||
},
|
||||
*/
|
||||
INVON,
|
||||
NORON,
|
||||
DISPON,
|
||||
];
|
||||
|
||||
pub struct ST7789Display<
|
||||
BoardSelectId: PinId,
|
||||
DataCommandId: PinId,
|
||||
D: SpiDevice,
|
||||
Pinout: ValidSpiPinout<D>,
|
||||
> {
|
||||
inner: ST7789DisplayEnabled<BoardSelectId, DataCommandId, D, Pinout>,
|
||||
}
|
||||
|
||||
impl<BoardSelectId: PinId, DataCommandId: PinId, D: SpiDevice, Pinout: ValidSpiPinout<D>>
|
||||
ST7789Display<BoardSelectId, DataCommandId, D, Pinout>
|
||||
{
|
||||
pub fn new(
|
||||
board_select: Pin<BoardSelectId, FunctionSio<SioOutput>, PullDown>,
|
||||
data_command: Pin<DataCommandId, FunctionSio<SioOutput>, PullDown>,
|
||||
spi: Spi<Enabled, D, Pinout, 8>,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: ST7789DisplayEnabled {
|
||||
board_select,
|
||||
data_command,
|
||||
spi,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acquire(
|
||||
&mut self,
|
||||
) -> &mut ST7789DisplayEnabled<BoardSelectId, DataCommandId, D, Pinout> {
|
||||
self.inner.board_select.set_low();
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ST7789DisplayEnabled<
|
||||
BoardSelectId: PinId,
|
||||
DataCommandId: PinId,
|
||||
D: SpiDevice,
|
||||
Pinout: ValidSpiPinout<D>,
|
||||
> {
|
||||
board_select: Pin<BoardSelectId, FunctionSio<SioOutput>, PullDown>,
|
||||
data_command: Pin<DataCommandId, FunctionSio<SioOutput>, PullDown>,
|
||||
spi: Spi<Enabled, D, Pinout, 8>,
|
||||
}
|
||||
|
||||
impl<BoardSelectId: PinId, DataCommandId: PinId, D: SpiDevice, Pinout: ValidSpiPinout<D>>
|
||||
ST7789DisplayEnabled<BoardSelectId, DataCommandId, D, Pinout>
|
||||
{
|
||||
pub fn send_command(&mut self, step: &Step, timer: &mut Timer) {
|
||||
step.send_command(&mut self.spi, &mut self.data_command);
|
||||
if let Some(delay) = step.delay {
|
||||
timer.delay_ms(delay);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_buf(&mut self, frame: &[u8]) {
|
||||
// let _ = DISPOFF.send_command(&mut self.spi, &mut self.data_command);
|
||||
let _ = self.data_command.set_low();
|
||||
let _ = self.spi.write(&[RAMWR]);
|
||||
let _ = self.data_command.set_high();
|
||||
let _ = self.spi.write(&frame);
|
||||
// let _ = DISPON.send_command(&mut self.spi, &mut self.data_command);
|
||||
}
|
||||
}
|
||||
|
||||
impl<BoardSelectId: PinId, DataCommandId: PinId, D: SpiDevice, Pinout: ValidSpiPinout<D>> Drop
|
||||
for ST7789DisplayEnabled<BoardSelectId, DataCommandId, D, Pinout>
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
self.board_select.set_high();
|
||||
}
|
||||
}
|
@ -33,9 +33,9 @@ use std::{error::Error, fmt};
|
||||
/// statement.
|
||||
pub trait FatalError: Error {}
|
||||
|
||||
/// Result<A, FE, E> represents a return value that might be a success, might be a fatal error, or
|
||||
/// ResultExt<A, FE, E> represents a return value that might be a success, might be a fatal error, or
|
||||
/// might be a normal handleable error.
|
||||
pub enum Result<A, E, FE> {
|
||||
pub enum ResultExt<A, E, FE> {
|
||||
/// The operation was successful
|
||||
Ok(A),
|
||||
/// Ordinary errors. These should be handled and the application should recover gracefully.
|
||||
@ -45,72 +45,72 @@ pub enum Result<A, E, FE> {
|
||||
Fatal(FE),
|
||||
}
|
||||
|
||||
impl<A, E, FE> Result<A, E, FE> {
|
||||
impl<A, E, FE> ResultExt<A, E, FE> {
|
||||
/// Apply an infallible function to a successful value.
|
||||
pub fn map<B, O>(self, mapper: O) -> Result<B, E, FE>
|
||||
pub fn map<B, O>(self, mapper: O) -> ResultExt<B, E, FE>
|
||||
where
|
||||
O: FnOnce(A) -> B,
|
||||
{
|
||||
match self {
|
||||
Result::Ok(val) => Result::Ok(mapper(val)),
|
||||
Result::Err(err) => Result::Err(err),
|
||||
Result::Fatal(err) => Result::Fatal(err),
|
||||
ResultExt::Ok(val) => ResultExt::Ok(mapper(val)),
|
||||
ResultExt::Err(err) => ResultExt::Err(err),
|
||||
ResultExt::Fatal(err) => ResultExt::Fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a potentially fallible function to a successful value.
|
||||
///
|
||||
/// Like `Result.and_then`, the mapping function can itself fail.
|
||||
pub fn and_then<B, O>(self, handler: O) -> Result<B, E, FE>
|
||||
pub fn and_then<B, O>(self, handler: O) -> ResultExt<B, E, FE>
|
||||
where
|
||||
O: FnOnce(A) -> Result<B, E, FE>,
|
||||
O: FnOnce(A) -> ResultExt<B, E, FE>,
|
||||
{
|
||||
match self {
|
||||
Result::Ok(val) => handler(val),
|
||||
Result::Err(err) => Result::Err(err),
|
||||
Result::Fatal(err) => Result::Fatal(err),
|
||||
ResultExt::Ok(val) => handler(val),
|
||||
ResultExt::Err(err) => ResultExt::Err(err),
|
||||
ResultExt::Fatal(err) => ResultExt::Fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a normal error from one type to another. This is useful for converting an error from
|
||||
/// one type to another, especially in re-throwing an underlying error. `?` syntax does not
|
||||
/// work with `Result`, so you will likely need to use this a lot.
|
||||
pub fn map_err<F, O>(self, mapper: O) -> Result<A, F, FE>
|
||||
pub fn map_err<F, O>(self, mapper: O) -> ResultExt<A, F, FE>
|
||||
where
|
||||
O: FnOnce(E) -> F,
|
||||
{
|
||||
match self {
|
||||
Result::Ok(val) => Result::Ok(val),
|
||||
Result::Err(err) => Result::Err(mapper(err)),
|
||||
Result::Fatal(err) => Result::Fatal(err),
|
||||
ResultExt::Ok(val) => ResultExt::Ok(val),
|
||||
ResultExt::Err(err) => ResultExt::Err(mapper(err)),
|
||||
ResultExt::Fatal(err) => ResultExt::Fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide a function to use to recover from (or simply re-throw) an error.
|
||||
pub fn or_else<O, F>(self, handler: O) -> Result<A, F, FE>
|
||||
pub fn or_else<O, F>(self, handler: O) -> ResultExt<A, F, FE>
|
||||
where
|
||||
O: FnOnce(E) -> Result<A, F, FE>,
|
||||
O: FnOnce(E) -> ResultExt<A, F, FE>,
|
||||
{
|
||||
match self {
|
||||
Result::Ok(val) => Result::Ok(val),
|
||||
Result::Err(err) => handler(err),
|
||||
Result::Fatal(err) => Result::Fatal(err),
|
||||
ResultExt::Ok(val) => ResultExt::Ok(val),
|
||||
ResultExt::Err(err) => handler(err),
|
||||
ResultExt::Fatal(err) => ResultExt::Fatal(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from a normal `Result` type to a `Result` type. The error condition for a `Result` will
|
||||
/// Convert from a normal `Result` type to a `ResultExt` type. The error condition for a `Result` will
|
||||
/// be treated as `Result::Err`, never `Result::Fatal`.
|
||||
impl<A, E, FE> From<std::result::Result<A, E>> for Result<A, E, FE> {
|
||||
impl<A, E, FE> From<std::result::Result<A, E>> for ResultExt<A, E, FE> {
|
||||
fn from(r: std::result::Result<A, E>) -> Self {
|
||||
match r {
|
||||
Ok(val) => Result::Ok(val),
|
||||
Err(err) => Result::Err(err),
|
||||
Ok(val) => ResultExt::Ok(val),
|
||||
Err(err) => ResultExt::Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, E, FE> fmt::Debug for Result<A, E, FE>
|
||||
impl<A, E, FE> fmt::Debug for ResultExt<A, E, FE>
|
||||
where
|
||||
A: fmt::Debug,
|
||||
FE: fmt::Debug,
|
||||
@ -118,14 +118,14 @@ where
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Result::Ok(val) => f.write_fmt(format_args!("Result::Ok {:?}", val)),
|
||||
Result::Err(err) => f.write_fmt(format_args!("Result::Err {:?}", err)),
|
||||
Result::Fatal(err) => f.write_fmt(format_args!("Result::Fatal {:?}", err)),
|
||||
ResultExt::Ok(val) => f.write_fmt(format_args!("Result::Ok {:?}", val)),
|
||||
ResultExt::Err(err) => f.write_fmt(format_args!("Result::Err {:?}", err)),
|
||||
ResultExt::Fatal(err) => f.write_fmt(format_args!("Result::Fatal {:?}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, E, FE> PartialEq for Result<A, E, FE>
|
||||
impl<A, E, FE> PartialEq for ResultExt<A, E, FE>
|
||||
where
|
||||
A: PartialEq,
|
||||
FE: PartialEq,
|
||||
@ -133,27 +133,34 @@ where
|
||||
{
|
||||
fn eq(&self, rhs: &Self) -> bool {
|
||||
match (self, rhs) {
|
||||
(Result::Ok(val), Result::Ok(rhs)) => val == rhs,
|
||||
(Result::Err(_), Result::Err(_)) => true,
|
||||
(Result::Fatal(_), Result::Fatal(_)) => true,
|
||||
(ResultExt::Ok(val), ResultExt::Ok(rhs)) => val == rhs,
|
||||
(ResultExt::Err(_), ResultExt::Err(_)) => true,
|
||||
(ResultExt::Fatal(_), ResultExt::Fatal(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to create an ok value.
|
||||
pub fn ok<A, E: Error, FE: FatalError>(val: A) -> Result<A, E, FE> {
|
||||
Result::Ok(val)
|
||||
pub fn ok<A, E: Error, FE: FatalError>(val: A) -> ResultExt<A, E, FE> {
|
||||
ResultExt::Ok(val)
|
||||
}
|
||||
|
||||
/// Convenience function to create an error value.
|
||||
pub fn error<A, E: Error, FE: FatalError>(err: E) -> Result<A, E, FE> {
|
||||
Result::Err(err)
|
||||
pub fn error<A, E: Error, FE: FatalError>(err: E) -> ResultExt<A, E, FE> {
|
||||
ResultExt::Err(err)
|
||||
}
|
||||
|
||||
/// Convenience function to create a fatal value.
|
||||
pub fn fatal<A, E: Error, FE: FatalError>(err: FE) -> Result<A, E, FE> {
|
||||
Result::Fatal(err)
|
||||
pub fn fatal<A, E: Error, FE: FatalError>(err: FE) -> ResultExt<A, E, FE> {
|
||||
ResultExt::Fatal(err)
|
||||
}
|
||||
|
||||
pub fn result_as_fatal<A, E: Error, FE: FatalError>(result: Result<A, FE>) -> ResultExt<A, E, FE> {
|
||||
match result {
|
||||
Ok(a) => ResultExt::Ok(a),
|
||||
Err(err) => ResultExt::Fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return early from the current function if the value is a fatal error.
|
||||
@ -161,9 +168,9 @@ pub fn fatal<A, E: Error, FE: FatalError>(err: FE) -> Result<A, E, FE> {
|
||||
macro_rules! return_fatal {
|
||||
($x:expr) => {
|
||||
match $x {
|
||||
Result::Fatal(err) => return Result::Fatal(err),
|
||||
Result::Err(err) => Err(err),
|
||||
Result::Ok(val) => Ok(val),
|
||||
ResultExt::Fatal(err) => return ResultExt::Fatal(err),
|
||||
ResultExt::Err(err) => Err(err),
|
||||
ResultExt::Ok(val) => Ok(val),
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -173,9 +180,9 @@ macro_rules! return_fatal {
|
||||
macro_rules! return_error {
|
||||
($x:expr) => {
|
||||
match $x {
|
||||
Result::Ok(val) => val,
|
||||
Result::Err(err) => return Result::Err(err),
|
||||
Result::Fatal(err) => return Result::Fatal(err),
|
||||
ResultExt::Ok(val) => val,
|
||||
ResultExt::Err(err) => return ResultExt::Err(err),
|
||||
ResultExt::Fatal(err) => return ResultExt::Fatal(err),
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -210,19 +217,19 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn it_can_map_things() {
|
||||
let success: Result<i32, Error, FatalError> = ok(15);
|
||||
let success: ResultExt<i32, Error, FatalError> = ok(15);
|
||||
assert_eq!(ok(16), success.map(|v| v + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_chain_success() {
|
||||
let success: Result<i32, Error, FatalError> = ok(15);
|
||||
let success: ResultExt<i32, Error, FatalError> = ok(15);
|
||||
assert_eq!(ok(16), success.and_then(|v| ok(v + 1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_handle_an_error() {
|
||||
let failure: Result<i32, Error, FatalError> = error(Error::Error);
|
||||
let failure: ResultExt<i32, Error, FatalError> = error(Error::Error);
|
||||
assert_eq!(
|
||||
ok::<i32, Error, FatalError>(16),
|
||||
failure.or_else(|_| ok(16))
|
||||
@ -231,7 +238,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn early_exit_on_fatal() {
|
||||
fn ok_func() -> Result<i32, Error, FatalError> {
|
||||
fn ok_func() -> ResultExt<i32, Error, FatalError> {
|
||||
let value = return_fatal!(ok::<i32, Error, FatalError>(15));
|
||||
match value {
|
||||
Ok(_) => ok(14),
|
||||
@ -239,7 +246,7 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
fn err_func() -> Result<i32, Error, FatalError> {
|
||||
fn err_func() -> ResultExt<i32, Error, FatalError> {
|
||||
let value = return_fatal!(error::<i32, Error, FatalError>(Error::Error));
|
||||
match value {
|
||||
Ok(_) => panic!("shouldn't have gotten here"),
|
||||
@ -247,7 +254,7 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
fn fatal_func() -> Result<i32, Error, FatalError> {
|
||||
fn fatal_func() -> ResultExt<i32, Error, FatalError> {
|
||||
let _ = return_fatal!(fatal::<i32, Error, FatalError>(FatalError::FatalError));
|
||||
panic!("failed to bail");
|
||||
}
|
||||
@ -259,18 +266,18 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn it_can_early_exit_on_all_errors() {
|
||||
fn ok_func() -> Result<i32, Error, FatalError> {
|
||||
fn ok_func() -> ResultExt<i32, Error, FatalError> {
|
||||
let value = return_error!(ok::<i32, Error, FatalError>(15));
|
||||
assert_eq!(value, 15);
|
||||
ok(14)
|
||||
}
|
||||
|
||||
fn err_func() -> Result<i32, Error, FatalError> {
|
||||
fn err_func() -> ResultExt<i32, Error, FatalError> {
|
||||
return_error!(error::<i32, Error, FatalError>(Error::Error));
|
||||
panic!("failed to bail");
|
||||
}
|
||||
|
||||
fn fatal_func() -> Result<i32, Error, FatalError> {
|
||||
fn fatal_func() -> ResultExt<i32, Error, FatalError> {
|
||||
return_error!(fatal::<i32, Error, FatalError>(FatalError::FatalError));
|
||||
panic!("failed to bail");
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.81.0"
|
||||
channel = "1.85.0"
|
||||
targets = [ "wasm32-unknown-unknown", "thumbv6m-none-eabi" ]
|
||||
components = [ "rustfmt", "rust-analyzer", "clippy" ]
|
||||
|
@ -7,5 +7,6 @@ license = "GPL-3.0-only"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-std = "1.13.0"
|
||||
glib = { version = "0.18" }
|
||||
gtk = { version = "0.7", package = "gtk4" }
|
||||
|
@ -66,9 +66,8 @@ impl Screenplay {
|
||||
/// This function currently returns no errors, instead panicing if anything goes wrong.
|
||||
pub fn new(gtk_app: >k::Application, screens: Vec<Screen>) -> Result<Self, Error> {
|
||||
let window = gtk::ApplicationWindow::new(gtk_app);
|
||||
window.show();
|
||||
|
||||
let (sender, receiver) = gtk::glib::MainContext::channel(gtk::glib::Priority::DEFAULT);
|
||||
let (sender, receiver) = async_std::channel::unbounded();
|
||||
|
||||
let layout = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
@ -90,11 +89,15 @@ impl Screenplay {
|
||||
layout.append(&frame);
|
||||
|
||||
listbox.connect_row_activated(move |_, row| {
|
||||
match row.index() {
|
||||
-1 => sender.send(Action::Deselect),
|
||||
idx => sender.send(Action::SelectPage(idx as usize)),
|
||||
}
|
||||
.unwrap()
|
||||
let sender = sender.clone();
|
||||
let row = row.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
match row.index() {
|
||||
-1 => sender.send(Action::Deselect).await,
|
||||
idx => sender.send(Action::SelectPage(idx as usize)).await,
|
||||
}
|
||||
.unwrap()
|
||||
});
|
||||
});
|
||||
|
||||
screens.iter().for_each(|Screen { title, .. }| {
|
||||
@ -108,8 +111,14 @@ impl Screenplay {
|
||||
let storybook = Self { frame, screens };
|
||||
|
||||
{
|
||||
let mut storybook = storybook.clone();
|
||||
receiver.attach(None, move |message| storybook.process_action(message));
|
||||
glib::spawn_future_local({
|
||||
let mut storybook = storybook.clone();
|
||||
async move {
|
||||
while let Ok(msg) = receiver.recv().await {
|
||||
storybook.process_action(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(storybook)
|
||||
|
@ -10,7 +10,7 @@ use nom::{
|
||||
IResult, Parser,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write, num::ParseIntError, time::Duration};
|
||||
use std::{fmt::Display, num::ParseIntError, time::Duration};
|
||||
|
||||
impl From<ParseSizeError> for Error {
|
||||
fn from(_: ParseSizeError) -> Self {
|
||||
@ -142,9 +142,9 @@ impl From<&GameType> for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for GameType {
|
||||
fn to_string(&self) -> String {
|
||||
String::from(self)
|
||||
impl Display for GameType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", String::from(self))
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,12 +189,15 @@ pub struct Tree {
|
||||
pub root: Node,
|
||||
}
|
||||
|
||||
impl ToString for Tree {
|
||||
fn to_string(&self) -> String {
|
||||
format!("({})", self.root.to_string())
|
||||
impl Display for Tree {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "({})", self.root)
|
||||
}
|
||||
}
|
||||
|
||||
struct Properties<'a>(&'a Vec<Property>);
|
||||
struct Nodes<'a>(&'a Vec<Node>);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Node {
|
||||
pub properties: Vec<Property>,
|
||||
@ -264,8 +267,12 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Node {
|
||||
fn to_string(&self) -> String {
|
||||
impl Display for Node {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, ";{}", Properties(&self.properties))?;
|
||||
write!(f, "{}", Nodes(&self.next))?;
|
||||
Ok(())
|
||||
/*
|
||||
let props = self
|
||||
.properties
|
||||
.iter()
|
||||
@ -281,11 +288,36 @@ impl ToString for Node {
|
||||
} else {
|
||||
self.next
|
||||
.iter()
|
||||
.map(|node| format!("({})", node.to_string()))
|
||||
.map(|node| write!(f, "({})", node.to_string()))
|
||||
.collect::<Vec<String>>()
|
||||
.join("")
|
||||
};
|
||||
format!(";{}{}", props, next)
|
||||
write!(f, ";{}{}", props, next)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Properties<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for property in self.0.iter() {
|
||||
write!(f, "{}", property)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Nodes<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.0.len() == 1 {
|
||||
for node in self.0.iter() {
|
||||
write!(f, "{}", node)?;
|
||||
}
|
||||
} else {
|
||||
for node in self.0.iter() {
|
||||
write!(f, "({})", node)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -460,95 +492,96 @@ pub struct UnknownProperty {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl ToString for Property {
|
||||
fn to_string(&self) -> String {
|
||||
impl Display for Property {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Property::Move((color, Move::Move(mv))) => {
|
||||
format!("{}[{}]", color.abbreviation(), mv)
|
||||
write!(f, "{}[{}]", color.abbreviation(), mv)
|
||||
}
|
||||
Property::Move((color, Move::Pass)) => {
|
||||
format!("{}[]", color.abbreviation())
|
||||
write!(f, "{}[]", color.abbreviation())
|
||||
}
|
||||
Property::TimeLeft((color, time)) => {
|
||||
format!("{}[{}]", color.abbreviation(), time.as_secs())
|
||||
write!(f, "{}[{}]", color.abbreviation(), time.as_secs())
|
||||
}
|
||||
Property::Comment(value) => format!("C[{}]", value),
|
||||
Property::Annotation(Annotation::BadMove) => "BM[]".to_owned(),
|
||||
Property::Annotation(Annotation::DoubtfulMove) => "DO[]".to_owned(),
|
||||
Property::Annotation(Annotation::InterestingMove) => "IT[]".to_owned(),
|
||||
Property::Annotation(Annotation::Tesuji) => "TE[]".to_owned(),
|
||||
Property::Application(app) => format!("AP[{}]", app),
|
||||
Property::Charset(set) => format!("CA[{}]", set),
|
||||
Property::FileFormat(ff) => format!("FF[{}]", ff),
|
||||
Property::GameType(gt) => format!("GM[{}]", gt.to_string()),
|
||||
Property::Comment(value) => write!(f, "C[{}]", value),
|
||||
Property::Annotation(Annotation::BadMove) => write!(f, "BM[]"),
|
||||
Property::Annotation(Annotation::DoubtfulMove) => write!(f, "DO[]"),
|
||||
Property::Annotation(Annotation::InterestingMove) => write!(f, "IT[]"),
|
||||
Property::Annotation(Annotation::Tesuji) => write!(f, "TE[]"),
|
||||
Property::Application(app) => write!(f, "AP[{}]", app),
|
||||
Property::Charset(set) => write!(f, "CA[{}]", set),
|
||||
Property::FileFormat(ff) => write!(f, "FF[{}]", ff),
|
||||
Property::GameType(gt) => write!(f, "GM[{}]", gt),
|
||||
Property::VariationDisplay => unimplemented!(),
|
||||
Property::BoardSize(Size { width, height }) => {
|
||||
if width == height {
|
||||
format!("SZ[{}]", width)
|
||||
write!(f, "SZ[{}]", width)
|
||||
} else {
|
||||
format!("SZ[{}:{}]", width, height)
|
||||
write!(f, "SZ[{}:{}]", width, height)
|
||||
}
|
||||
}
|
||||
Property::SetupBlackStones(positions) => {
|
||||
format!("AB[{}]", positions.compressed_list(),)
|
||||
write!(f, "AB[{}]", positions.compressed_list(),)
|
||||
}
|
||||
Property::ClearStones(positions) => {
|
||||
format!("AE[{}]", positions.compressed_list(),)
|
||||
write!(f, "AE[{}]", positions.compressed_list(),)
|
||||
}
|
||||
Property::SetupWhiteStones(positions) => {
|
||||
format!("AW[{}]", positions.compressed_list(),)
|
||||
write!(f, "AW[{}]", positions.compressed_list(),)
|
||||
}
|
||||
Property::NextPlayer(color) => format!("PL[{}]", color.abbreviation()),
|
||||
Property::Evaluation(Evaluation::Even) => "DM[]".to_owned(),
|
||||
Property::Evaluation(Evaluation::GoodForBlack) => "GB[]".to_owned(),
|
||||
Property::Evaluation(Evaluation::GoodForWhite) => "GW[]".to_owned(),
|
||||
Property::Evaluation(Evaluation::Unclear) => "UC[]".to_owned(),
|
||||
Property::Hotspot => "HO[]".to_owned(),
|
||||
Property::Value(value) => format!("V[{}]", value),
|
||||
Property::Annotator(value) => format!("AN[{}]", value),
|
||||
Property::BlackRank(value) => format!("BR[{}]", value),
|
||||
Property::BlackTeam(value) => format!("BT[{}]", value),
|
||||
Property::Copyright(value) => format!("CP[{}]", value),
|
||||
Property::NextPlayer(color) => write!(f, "PL[{}]", color.abbreviation()),
|
||||
Property::Evaluation(Evaluation::Even) => write!(f, "DM[]"),
|
||||
Property::Evaluation(Evaluation::GoodForBlack) => write!(f, "GB[]"),
|
||||
Property::Evaluation(Evaluation::GoodForWhite) => write!(f, "GW[]"),
|
||||
Property::Evaluation(Evaluation::Unclear) => write!(f, "UC[]"),
|
||||
Property::Hotspot => write!(f, "HO[]"),
|
||||
Property::Value(value) => write!(f, "V[{}]", value),
|
||||
Property::Annotator(value) => write!(f, "AN[{}]", value),
|
||||
Property::BlackRank(value) => write!(f, "BR[{}]", value),
|
||||
Property::BlackTeam(value) => write!(f, "BT[{}]", value),
|
||||
Property::Copyright(value) => write!(f, "CP[{}]", value),
|
||||
Property::EventDates(_) => unimplemented!(),
|
||||
Property::EventName(value) => format!("EV[{}]", value),
|
||||
Property::GameName(value) => format!("GN[{}]", value),
|
||||
Property::ExtraGameInformation(value) => format!("GC[{}]", value),
|
||||
Property::GameOpening(value) => format!("ON[{}]", value),
|
||||
Property::Overtime(value) => format!("OT[{}]", value),
|
||||
Property::BlackPlayer(value) => format!("PB[{}]", value),
|
||||
Property::GameLocation(value) => format!("PC[{}]", value),
|
||||
Property::WhitePlayer(value) => format!("PW[{}]", value),
|
||||
Property::EventName(value) => write!(f, "EV[{}]", value),
|
||||
Property::GameName(value) => write!(f, "GN[{}]", value),
|
||||
Property::ExtraGameInformation(value) => write!(f, "GC[{}]", value),
|
||||
Property::GameOpening(value) => write!(f, "ON[{}]", value),
|
||||
Property::Overtime(value) => write!(f, "OT[{}]", value),
|
||||
Property::BlackPlayer(value) => write!(f, "PB[{}]", value),
|
||||
Property::GameLocation(value) => write!(f, "PC[{}]", value),
|
||||
Property::WhitePlayer(value) => write!(f, "PW[{}]", value),
|
||||
Property::Result(_) => unimplemented!(),
|
||||
Property::Round(value) => format!("RO[{}]", value),
|
||||
Property::Ruleset(value) => format!("RU[{}]", value),
|
||||
Property::Source(value) => format!("SO[{}]", value),
|
||||
Property::TimeLimit(value) => format!("TM[{}]", value.as_secs()),
|
||||
Property::User(value) => format!("US[{}]", value),
|
||||
Property::WhiteRank(value) => format!("WR[{}]", value),
|
||||
Property::WhiteTeam(value) => format!("WT[{}]", value),
|
||||
Property::Round(value) => write!(f, "RO[{}]", value),
|
||||
Property::Ruleset(value) => write!(f, "RU[{}]", value),
|
||||
Property::Source(value) => write!(f, "SO[{}]", value),
|
||||
Property::TimeLimit(value) => write!(f, "TM[{}]", value.as_secs()),
|
||||
Property::User(value) => write!(f, "US[{}]", value),
|
||||
Property::WhiteRank(value) => write!(f, "WR[{}]", value),
|
||||
Property::WhiteTeam(value) => write!(f, "WT[{}]", value),
|
||||
Property::Territory(Color::White, positions) => {
|
||||
positions
|
||||
.iter()
|
||||
.fold("TW".to_owned(), |mut output, Position(p)| {
|
||||
let _ = write!(output, "{}", p);
|
||||
output
|
||||
})
|
||||
write!(f, "TW[{}]", Positions(positions))
|
||||
}
|
||||
Property::Territory(Color::Black, positions) => {
|
||||
positions
|
||||
.iter()
|
||||
.fold("TB".to_owned(), |mut output, Position(p)| {
|
||||
let _ = write!(output, "{}", p);
|
||||
output
|
||||
})
|
||||
write!(f, "TB[{}]", Positions(positions))
|
||||
}
|
||||
Property::Unknown(UnknownProperty { ident, value }) => {
|
||||
format!("{}[{}]", ident, value)
|
||||
write!(f, "{}[{}]", ident, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Positions<'a>(&'a Vec<Position>);
|
||||
|
||||
impl Display for Positions<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for Position(p) in self.0.iter() {
|
||||
write!(f, "{}", p)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>(
|
||||
input: &'a str,
|
||||
) -> IResult<&'a str, Vec<Tree>, E> {
|
||||
|
@ -53,6 +53,7 @@ pub enum Error {
|
||||
// InvalidField,
|
||||
// InvalidBoardSize,
|
||||
Incomplete,
|
||||
#[allow(dead_code)]
|
||||
InvalidSgf(VerboseNomError),
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,7 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{DateTime, FixedOffset, NaiveDate, TimeZone};
|
||||
use chrono_tz::{
|
||||
America::{New_York, Phoenix},
|
||||
Tz,
|
||||
US::Mountain,
|
||||
};
|
||||
use chrono_tz::America::{New_York, Phoenix};
|
||||
|
||||
#[test]
|
||||
fn it_saves_with_offset() {
|
||||
@ -37,7 +33,7 @@ mod tests {
|
||||
date.with_timezone(&New_York),
|
||||
FixedOffset::west_opt(4 * 60 * 60)
|
||||
.unwrap()
|
||||
.with_ymd_and_hms(2023, 10, 14, 03, 0, 0)
|
||||
.with_ymd_and_hms(2023, 10, 14, 3, 0, 0)
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -62,7 +62,7 @@ impl<T> Tree<T> {
|
||||
// Do a depth-first-search in order to get the path to a node. Start with a naive recursive
|
||||
// implementation, then switch to a stack-based implementation in order to avoid exceeding the
|
||||
// stack.
|
||||
pub fn path_to<F>(&self, f: F) -> Vec<Node<T>>
|
||||
pub fn path_to<F>(&self, _f: F) -> Vec<Node<T>>
|
||||
where
|
||||
F: FnOnce(&T) -> bool + Copy,
|
||||
{
|
||||
@ -206,17 +206,20 @@ mod tests {
|
||||
assert!(tree2.find_bfs(|val| *val == "17").is_some());
|
||||
}
|
||||
|
||||
/*
|
||||
#[test]
|
||||
fn path_to_on_empty_tree_returns_empty() {
|
||||
let tree: Tree<&str> = Tree::default();
|
||||
|
||||
assert_eq!(tree.path_to(|val| *val == "i"), vec![]);
|
||||
}
|
||||
*/
|
||||
|
||||
// A
|
||||
// B G H
|
||||
// C I
|
||||
// D E F
|
||||
/*
|
||||
#[test]
|
||||
fn it_can_find_a_path_to_a_node() {
|
||||
let (tree, a) = Tree::new("A");
|
||||
@ -232,4 +235,5 @@ mod tests {
|
||||
assert_eq!(tree.path_to(|val| *val == "z"), vec![]);
|
||||
assert_eq!(tree.path_to(|val| *val == "i"), vec![a, h, i]);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
35
visions-prototype/server/Cargo.toml
Normal file
35
visions-prototype/server/Cargo.toml
Normal file
@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "visions"
|
||||
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 = { version = "1.13.0" }
|
||||
async-trait = { version = "0.1.83" }
|
||||
authdb = { path = "../../authdb/" }
|
||||
axum = { version = "0.7.9", features = [ "macros" ] }
|
||||
chrono = { version = "0.4.39", features = ["serde"] }
|
||||
futures = { version = "0.3.31" }
|
||||
include_dir = { version = "0.7.4" }
|
||||
lazy_static = { version = "1.5.0" }
|
||||
mime = { version = "0.3.17" }
|
||||
mime_guess = { version = "2.0.5" }
|
||||
pretty_env_logger = { version = "0.5.0" }
|
||||
result-extended = { path = "../../result-extended" }
|
||||
rusqlite = { version = "0.32.1" }
|
||||
rusqlite_migration = { version = "1.3.1", features = ["from-directory"] }
|
||||
serde = { version = "1" }
|
||||
serde_json = { version = "*" }
|
||||
thiserror = { version = "2.0.3" }
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
tokio-stream = { version = "0.1.16" }
|
||||
tower-http = { version = "0.6.2", features = ["cors"] }
|
||||
typeshare = { version = "1.0.4" }
|
||||
urlencoding = { version = "2.1.3" }
|
||||
uuid = { version = "1.11.0", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
cool_asserts = "2.0.3"
|
||||
axum-test = "16.4.1"
|
23
visions-prototype/server/Taskfile.yml
Normal file
23
visions-prototype/server/Taskfile.yml
Normal file
@ -0,0 +1,23 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- cargo watch -x build
|
||||
|
||||
test:
|
||||
cmds:
|
||||
- cargo watch -x 'nextest run'
|
||||
|
||||
dev:
|
||||
cmds:
|
||||
- cargo watch -x run
|
||||
|
||||
lint:
|
||||
cmds:
|
||||
- cargo watch -x clippy
|
||||
|
||||
release:
|
||||
cmds:
|
||||
- task lint
|
||||
- cargo build --release
|
41
visions-prototype/server/migrations/01-initial-db/up.sql
Normal file
41
visions-prototype/server/migrations/01-initial-db/up.sql
Normal file
@ -0,0 +1,41 @@
|
||||
CREATE TABLE users(
|
||||
uuid TEXT PRIMARY KEY,
|
||||
name TEXT UNIQUE,
|
||||
password TEXT,
|
||||
admin BOOLEAN,
|
||||
state TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE sessions(
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
|
||||
FOREIGN KEY(user_id) REFERENCES users(uuid)
|
||||
);
|
||||
|
||||
CREATE TABLE games(
|
||||
id TEXT PRIMARY KEY,
|
||||
type_ TEXT,
|
||||
gm TEXT,
|
||||
name TEXT,
|
||||
|
||||
FOREIGN KEY(gm) REFERENCES users(uuid)
|
||||
);
|
||||
|
||||
CREATE TABLE characters(
|
||||
uuid TEXT PRIMARY KEY,
|
||||
game TEXT,
|
||||
data TEXT,
|
||||
|
||||
FOREIGN KEY(game) REFERENCES games(uuid)
|
||||
);
|
||||
|
||||
CREATE TABLE roles(
|
||||
user_id TEXT,
|
||||
game_id TEXT,
|
||||
role TEXT,
|
||||
|
||||
FOREIGN KEY(user_id) REFERENCES users(uuid),
|
||||
FOREIGN KEY(game_id) REFERENCES games(uuid)
|
||||
);
|
||||
|
1
visions-prototype/server/soren-jensen.json
Normal file
1
visions-prototype/server/soren-jensen.json
Normal file
@ -0,0 +1 @@
|
||||
{ "type_": "Candela", "name": "Soren Jensen", "pronouns": "he/him", "circle": "Circle of the Bluest Sky", "style": "dapper gentleman", "catalyst": "a cursed book", "question": "What were the contents of that book?", "nerve": { "type_": "nerve", "drives": { "current": 1, "max": 2 }, "resistances": { "current": 0, "max": 3 }, "move": { "gilded": false, "score": 2 }, "strike": { "gilded": false, "score": 1 }, "control": { "gilded": true, "score": 0 } }, "cunning": { "type_": "cunning", "drives": { "current": 1, "max": 1 }, "resistances": { "current": 0, "max": 3 }, "sway": { "gilded": false, "score": 0 }, "read": { "gilded": false, "score": 0 }, "hide": { "gilded": false, "score": 0 } }, "intuition": { "type_": "intuition", "drives": { "current": 0, "max": 0 }, "resistances": { "current": 0, "max": 3 }, "survey": { "gilded": false, "score": 0 }, "focus": { "gilded": false, "score": 0 }, "sense": { "gilded": false, "score": 0 } }, "role": "Slink", "role_abilities": [ "Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?" ], "specialty": "Detective", "specialty_abilities": [ "Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you have deduced." ] }
|
156
visions-prototype/server/src/asset_db.rs
Normal file
156
visions-prototype/server/src/asset_db.rs
Normal file
@ -0,0 +1,156 @@
|
||||
use std::{
|
||||
collections::{hash_map::Iter, HashMap}, fmt::{self, Display}, fs, io::Read, path::PathBuf
|
||||
};
|
||||
|
||||
use mime::Mime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use typeshare::typeshare;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Asset could not be found")]
|
||||
NotFound,
|
||||
#[error("Asset could not be opened")]
|
||||
Inaccessible,
|
||||
|
||||
#[error("An unexpected IO error occured when retrieving an asset {0}")]
|
||||
Unexpected(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Error {
|
||||
use std::io::ErrorKind::*;
|
||||
|
||||
match err.kind() {
|
||||
NotFound => Error::NotFound,
|
||||
PermissionDenied | UnexpectedEof => Error::Inaccessible,
|
||||
_ => Error::Unexpected(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct AssetId(String);
|
||||
|
||||
impl AssetId {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AssetId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "AssetId({})", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for AssetId {
|
||||
fn from(s: &str) -> Self {
|
||||
AssetId(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AssetId {
|
||||
fn from(s: String) -> Self {
|
||||
AssetId(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AssetIter<'a>(Iter<'a, AssetId, String>);
|
||||
|
||||
impl<'a> Iterator for AssetIter<'a> {
|
||||
type Item = (&'a AssetId, &'a String);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Assets {
|
||||
fn assets(&self) -> AssetIter;
|
||||
|
||||
fn get(&self, asset_id: AssetId) -> Result<(Mime, Vec<u8>), Error>;
|
||||
}
|
||||
|
||||
pub struct FsAssets {
|
||||
assets: HashMap<AssetId, String>,
|
||||
}
|
||||
|
||||
impl FsAssets {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
let dir = fs::read_dir(path).unwrap();
|
||||
let mut assets = HashMap::new();
|
||||
|
||||
for dir_ent in dir {
|
||||
let path = dir_ent.unwrap().path();
|
||||
let file_name = path.file_name().unwrap().to_str().unwrap();
|
||||
assets.insert(AssetId::from(file_name), path.to_str().unwrap().to_owned());
|
||||
}
|
||||
Self {
|
||||
assets,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Assets for FsAssets {
|
||||
fn assets(&self) -> AssetIter {
|
||||
AssetIter(self.assets.iter())
|
||||
}
|
||||
|
||||
fn get(&self, asset_id: AssetId) -> Result<(Mime, Vec<u8>), Error> {
|
||||
let path = match self.assets.get(&asset_id) {
|
||||
Some(asset) => Ok(asset),
|
||||
None => Err(Error::NotFound),
|
||||
}?;
|
||||
let mime = mime_guess::from_path(path).first().unwrap();
|
||||
let mut content: Vec<u8> = Vec::new();
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
file.read_to_end(&mut content)?;
|
||||
Ok((mime, content))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod mocks {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct MemoryAssets {
|
||||
asset_paths: HashMap<AssetId, String>,
|
||||
assets: HashMap<AssetId, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl MemoryAssets {
|
||||
pub fn new(data: Vec<(AssetId, String, Vec<u8>)>) -> Self {
|
||||
let mut asset_paths = HashMap::new();
|
||||
let mut assets = HashMap::new();
|
||||
data.into_iter().for_each(|(asset, path, data)| {
|
||||
asset_paths.insert(asset.clone(), path);
|
||||
assets.insert(asset, data);
|
||||
});
|
||||
Self {
|
||||
asset_paths,
|
||||
assets,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Assets for MemoryAssets {
|
||||
fn assets(&self) -> AssetIter<'_> {
|
||||
AssetIter(self.asset_paths.iter())
|
||||
}
|
||||
|
||||
fn get(&self, asset_id: AssetId) -> Result<(Mime, Vec<u8>), Error> {
|
||||
match (self.asset_paths.get(&asset_id), self.assets.get(&asset_id)) {
|
||||
(Some(path), Some(data)) => {
|
||||
let mime = mime_guess::from_path(path).first().unwrap();
|
||||
Ok((mime, data.to_vec()))
|
||||
}
|
||||
_ => Err(Error::NotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
525
visions-prototype/server/src/core.rs
Normal file
525
visions-prototype/server/src/core.rs
Normal file
@ -0,0 +1,525 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use async_std::sync::RwLock;
|
||||
use chrono::{DateTime, Duration, TimeDelta, Utc};
|
||||
use mime::Mime;
|
||||
use result_extended::{error, fatal, ok, result_as_fatal, return_error, ResultExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
asset_db::{self, AssetId, Assets},
|
||||
database::{CharacterId, Database, GameId, SessionId, UserId},
|
||||
types::AccountState,
|
||||
types::{AppError, FatalError, GameOverview, Message, Rgb, Tabletop, User, UserOverview},
|
||||
};
|
||||
|
||||
const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb {
|
||||
red: 0xca,
|
||||
green: 0xb9,
|
||||
blue: 0xbb,
|
||||
};
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct Status {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
#[typeshare]
|
||||
pub enum AuthResponse {
|
||||
Success(SessionId),
|
||||
PasswordReset(SessionId),
|
||||
Locked,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WebsocketClient {
|
||||
sender: Option<UnboundedSender<Message>>,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub asset_store: Box<dyn Assets + Sync + Send + 'static>,
|
||||
pub db: Box<dyn Database + Sync + Send + 'static>,
|
||||
pub clients: HashMap<String, WebsocketClient>,
|
||||
|
||||
pub tabletop: Tabletop,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Core(Arc<RwLock<AppState>>);
|
||||
|
||||
impl Core {
|
||||
pub fn new<A, DB>(assetdb: A, db: DB) -> Self
|
||||
where
|
||||
A: Assets + Sync + Send + 'static,
|
||||
DB: Database + Sync + Send + 'static,
|
||||
{
|
||||
Self(Arc::new(RwLock::new(AppState {
|
||||
asset_store: Box::new(assetdb),
|
||||
db: Box::new(db),
|
||||
clients: HashMap::new(),
|
||||
tabletop: Tabletop {
|
||||
background_color: DEFAULT_BACKGROUND_COLOR,
|
||||
background_image: None,
|
||||
},
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> {
|
||||
/*
|
||||
let state = self.0.write().await;
|
||||
let admin_user = return_error!(match state.db.user(&UserId::from("admin")).await {
|
||||
Ok(Some(admin_user)) => ok(admin_user),
|
||||
Ok(None) => {
|
||||
return ok(Status {
|
||||
admin_enabled: false,
|
||||
});
|
||||
}
|
||||
Err(err) => fatal(err),
|
||||
});
|
||||
|
||||
ok(Status {
|
||||
ok: !admin_user.password.is_empty(),
|
||||
})
|
||||
*/
|
||||
ok(Status { ok: true })
|
||||
}
|
||||
|
||||
pub async fn register_client(&self) -> String {
|
||||
let mut state = self.0.write().await;
|
||||
let uuid = Uuid::new_v4().simple().to_string();
|
||||
|
||||
let client = WebsocketClient { sender: None };
|
||||
|
||||
state.clients.insert(uuid.clone(), client);
|
||||
uuid
|
||||
}
|
||||
|
||||
pub async fn unregister_client(&self, client_id: String) {
|
||||
let mut state = self.0.write().await;
|
||||
let _ = state.clients.remove(&client_id);
|
||||
}
|
||||
|
||||
pub async fn connect_client(&self, client_id: String) -> UnboundedReceiver<Message> {
|
||||
let mut state = self.0.write().await;
|
||||
|
||||
match state.clients.get_mut(&client_id) {
|
||||
Some(client) => {
|
||||
let (tx, rx) = unbounded_channel();
|
||||
client.sender = Some(tx);
|
||||
rx
|
||||
}
|
||||
None => {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn user_by_username(
|
||||
&self,
|
||||
username: &str,
|
||||
) -> ResultExt<Option<User>, AppError, FatalError> {
|
||||
let state = self.0.read().await;
|
||||
match state.db.user_by_username(username).await {
|
||||
Ok(Some(user_row)) => ok(Some(User::from(user_row))),
|
||||
Ok(None) => ok(None),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_users(&self) -> ResultExt<Vec<UserOverview>, AppError, FatalError> {
|
||||
let users = self.0.write().await.db.users().await;
|
||||
match users {
|
||||
Ok(users) => ok(users
|
||||
.into_iter()
|
||||
.map(|user| UserOverview {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
state: user.state,
|
||||
is_admin: user.admin,
|
||||
})
|
||||
.collect()),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn user(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
) -> ResultExt<Option<UserOverview>, AppError, FatalError> {
|
||||
let users = return_error!(self.list_users().await);
|
||||
match users.into_iter().find(|user| user.id == user_id) {
|
||||
Some(user) => ok(Some(user)),
|
||||
None => return ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_user(&self, username: &str) -> ResultExt<UserId, AppError, FatalError> {
|
||||
let state = self.0.read().await;
|
||||
match return_error!(self.user_by_username(username).await) {
|
||||
Some(_) => error(AppError::UsernameUnavailable),
|
||||
None => match state
|
||||
.db
|
||||
.create_user(username, "", false, AccountState::PasswordReset(Utc::now() + Duration::minutes(60)))
|
||||
.await
|
||||
{
|
||||
Ok(user_id) => ok(user_id),
|
||||
Err(err) => fatal(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn disable_user(&self, _userid: UserId) -> ResultExt<(), AppError, FatalError> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
pub async fn list_games(&self) -> ResultExt<Vec<GameOverview>, AppError, FatalError> {
|
||||
let games = self.0.read().await.db.games().await;
|
||||
match games {
|
||||
// Ok(games) => ok(games.into_iter().map(|g| Game::from(g)).collect()),
|
||||
Ok(games) => ok(games
|
||||
.into_iter()
|
||||
.map(|game| GameOverview {
|
||||
id: game.id,
|
||||
type_: "".to_owned(),
|
||||
name: game.name,
|
||||
gm: game.gm,
|
||||
players: game.players,
|
||||
})
|
||||
.collect::<Vec<GameOverview>>()),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_game(
|
||||
&self,
|
||||
gm: &UserId,
|
||||
game_type: &str,
|
||||
game_name: &str,
|
||||
) -> ResultExt<GameId, AppError, FatalError> {
|
||||
let state = self.0.read().await;
|
||||
match state.db.create_game(gm, game_type, game_name).await {
|
||||
Ok(game_id) => ok(game_id),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn tabletop(&self) -> Tabletop {
|
||||
self.0.read().await.tabletop.clone()
|
||||
}
|
||||
|
||||
pub async fn get_asset(
|
||||
&self,
|
||||
asset_id: AssetId,
|
||||
) -> ResultExt<(Mime, Vec<u8>), AppError, FatalError> {
|
||||
ResultExt::from(
|
||||
self.0
|
||||
.read()
|
||||
.await
|
||||
.asset_store
|
||||
.get(asset_id.clone())
|
||||
.map_err(|err| match err {
|
||||
asset_db::Error::NotFound => AppError::NotFound(format!("{}", asset_id)),
|
||||
asset_db::Error::Inaccessible => {
|
||||
AppError::Inaccessible(format!("{}", asset_id))
|
||||
}
|
||||
asset_db::Error::Unexpected(err) => AppError::Inaccessible(format!("{}", err)),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn available_images(&self) -> Vec<AssetId> {
|
||||
self.0
|
||||
.read()
|
||||
.await
|
||||
.asset_store
|
||||
.assets()
|
||||
.filter_map(
|
||||
|(asset_id, value)| match mime_guess::from_path(value).first() {
|
||||
Some(mime) if mime.type_() == mime::IMAGE => Some(asset_id.clone()),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn set_background_image(
|
||||
&self,
|
||||
asset: AssetId,
|
||||
) -> ResultExt<(), AppError, FatalError> {
|
||||
let tabletop = {
|
||||
let mut state = self.0.write().await;
|
||||
state.tabletop.background_image = Some(asset.clone());
|
||||
state.tabletop.clone()
|
||||
};
|
||||
self.publish(Message::UpdateTabletop(tabletop)).await;
|
||||
ok(())
|
||||
}
|
||||
|
||||
pub async fn get_charsheet(
|
||||
&self,
|
||||
id: CharacterId,
|
||||
) -> ResultExt<Option<serde_json::Value>, AppError, FatalError> {
|
||||
let state = self.0.write().await;
|
||||
let cr = state.db.character(&id).await;
|
||||
match cr {
|
||||
Ok(Some(row)) => ok(Some(row.data)),
|
||||
Ok(None) => ok(None),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn publish(&self, message: Message) {
|
||||
let state = self.0.read().await;
|
||||
|
||||
state.clients.values().for_each(|client| {
|
||||
if let Some(ref sender) = client.sender {
|
||||
let _ = sender.send(message.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn save_user(
|
||||
&self,
|
||||
id: UserId,
|
||||
name: &str,
|
||||
password: &str,
|
||||
admin: bool,
|
||||
account_state: AccountState,
|
||||
) -> ResultExt<UserId, AppError, FatalError> {
|
||||
let state = self.0.read().await;
|
||||
match state
|
||||
.db
|
||||
.save_user(User {
|
||||
id,
|
||||
name: name.to_owned(),
|
||||
password: password.to_owned(),
|
||||
admin,
|
||||
state: account_state,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(uuid) => ok(uuid),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_password(
|
||||
&self,
|
||||
uuid: UserId,
|
||||
password: String,
|
||||
) -> ResultExt<(), AppError, FatalError> {
|
||||
let state = self.0.write().await;
|
||||
let user = match state.db.user(&uuid).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => return error(AppError::NotFound(uuid.as_str().to_owned())),
|
||||
Err(err) => return fatal(err),
|
||||
};
|
||||
match state
|
||||
.db
|
||||
.save_user(User {
|
||||
password,
|
||||
state: AccountState::Normal,
|
||||
..user
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => ok(()),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn auth(
|
||||
&self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> ResultExt<AuthResponse, AppError, FatalError> {
|
||||
let now = Utc::now();
|
||||
let state = self.0.read().await;
|
||||
let user = state.db.user_by_username(username).await.unwrap().unwrap();
|
||||
let user_info = return_error!(match state.db.user_by_username(username).await {
|
||||
Ok(Some(row)) if row.password == password => ok(row),
|
||||
Ok(_) => error(AppError::AuthFailed),
|
||||
Err(err) => fatal(err),
|
||||
});
|
||||
|
||||
match user_info.state {
|
||||
AccountState::Normal => result_as_fatal(state.db.create_session(&user_info.id).await)
|
||||
.map(|session_id| AuthResponse::Success(session_id)),
|
||||
|
||||
AccountState::PasswordReset(exp) => {
|
||||
if exp < now {
|
||||
error(AppError::AuthFailed)
|
||||
} else {
|
||||
result_as_fatal(state.db.create_session(&user_info.id).await)
|
||||
.map(|session_id| AuthResponse::PasswordReset(session_id))
|
||||
}
|
||||
}
|
||||
|
||||
AccountState::Locked => ok(AuthResponse::Locked),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn session(
|
||||
&self,
|
||||
session_id: &SessionId,
|
||||
) -> ResultExt<Option<User>, AppError, FatalError> {
|
||||
let state = self.0.read().await;
|
||||
match state.db.session(session_id).await {
|
||||
Ok(Some(user_row)) => ok(Some(User::from(user_row))),
|
||||
Ok(None) => ok(None),
|
||||
Err(fatal_error) => fatal(fatal_error),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_session(&self, session_id: &SessionId) -> ResultExt<(), AppError, FatalError> {
|
||||
let state = self.0.read().await;
|
||||
match state.db.delete_session(session_id).await {
|
||||
Ok(_) => ok(()),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_expiration_date() -> DateTime<Utc> {
|
||||
Utc::now() + TimeDelta::days(365)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
|
||||
use cool_asserts::assert_matches;
|
||||
|
||||
use crate::{asset_db::mocks::MemoryAssets, database::DbConn};
|
||||
|
||||
async fn test_core() -> Core {
|
||||
let assets = MemoryAssets::new(vec![
|
||||
(
|
||||
AssetId::from("asset_1"),
|
||||
"asset_1.png".to_owned(),
|
||||
String::from("abcdefg").into_bytes(),
|
||||
),
|
||||
(
|
||||
AssetId::from("asset_2"),
|
||||
"asset_2.jpg".to_owned(),
|
||||
String::from("abcdefg").into_bytes(),
|
||||
),
|
||||
(
|
||||
AssetId::from("asset_3"),
|
||||
"asset_3".to_owned(),
|
||||
String::from("abcdefg").into_bytes(),
|
||||
),
|
||||
(
|
||||
AssetId::from("asset_4"),
|
||||
"asset_4".to_owned(),
|
||||
String::from("abcdefg").into_bytes(),
|
||||
),
|
||||
(
|
||||
AssetId::from("asset_5"),
|
||||
"asset_5".to_owned(),
|
||||
String::from("abcdefg").into_bytes(),
|
||||
),
|
||||
]);
|
||||
let memory_db: Option<PathBuf> = None;
|
||||
let conn = DbConn::new(memory_db);
|
||||
conn.create_user("admin", "aoeu", true, AccountState::Normal)
|
||||
.await
|
||||
.unwrap();
|
||||
conn.create_user(
|
||||
"gm_1",
|
||||
"aoeu",
|
||||
false,
|
||||
AccountState::PasswordReset(Utc::now()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
Core::new(assets, conn)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_lists_available_images() {
|
||||
let core = test_core().await;
|
||||
let image_paths = core.available_images().await;
|
||||
assert_eq!(image_paths.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_retrieves_an_asset() {
|
||||
let core = test_core().await;
|
||||
assert_matches!(core.get_asset(AssetId::from("asset_1")).await, ResultExt::Ok((mime, data)) => {
|
||||
assert_eq!(mime.type_(), mime::IMAGE);
|
||||
assert_eq!(data, "abcdefg".as_bytes());
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_can_retrieve_the_default_tabletop() {
|
||||
let core = test_core().await;
|
||||
assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => {
|
||||
assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
|
||||
assert_eq!(background_image, None);
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_can_change_the_tabletop_background() {
|
||||
let core = test_core().await;
|
||||
assert_matches!(
|
||||
core.set_background_image(AssetId::from("asset_1")).await,
|
||||
ResultExt::Ok(())
|
||||
);
|
||||
assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => {
|
||||
assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
|
||||
assert_eq!(background_image, Some(AssetId::from("asset_1")));
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_sends_notices_to_clients_on_tabletop_change() {
|
||||
let core = test_core().await;
|
||||
let client_id = core.register_client().await;
|
||||
let mut receiver = core.connect_client(client_id).await;
|
||||
|
||||
assert_matches!(
|
||||
core.set_background_image(AssetId::from("asset_1")).await,
|
||||
ResultExt::Ok(())
|
||||
);
|
||||
match receiver.recv().await {
|
||||
Some(Message::UpdateTabletop(Tabletop {
|
||||
background_color,
|
||||
background_image,
|
||||
})) => {
|
||||
assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
|
||||
assert_eq!(background_image, Some(AssetId::from("asset_1")));
|
||||
}
|
||||
None => panic!("receiver did not get a message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_creates_a_sessionid_on_successful_auth() {
|
||||
let core = test_core().await;
|
||||
match core.auth("admin", "aoeu").await {
|
||||
ResultExt::Ok(AuthResponse::Success(session_id)) => {
|
||||
let st = core.0.read().await;
|
||||
match st.db.session(&session_id).await {
|
||||
Ok(Some(user_row)) => assert_eq!(user_row.name, "admin"),
|
||||
Ok(None) => panic!("no matching user row for the session id"),
|
||||
Err(err) => panic!("{}", err),
|
||||
}
|
||||
}
|
||||
ResultExt::Ok(AuthResponse::PasswordReset(_)) => panic!("user is in password reset state"),
|
||||
ResultExt::Ok(AuthResponse::Locked) => panic!("user has been locked"),
|
||||
ResultExt::Err(err) => panic!("{}", err),
|
||||
ResultExt::Fatal(err) => panic!("{}", err),
|
||||
}
|
||||
}
|
||||
}
|
398
visions-prototype/server/src/database/disk_db.rs
Normal file
398
visions-prototype/server/src/database/disk_db.rs
Normal file
@ -0,0 +1,398 @@
|
||||
use std::path::Path;
|
||||
|
||||
use async_std::channel::Receiver;
|
||||
use include_dir::{include_dir, Dir};
|
||||
use lazy_static::lazy_static;
|
||||
use rusqlite::Connection;
|
||||
use rusqlite_migration::Migrations;
|
||||
|
||||
use crate::{
|
||||
database::{DatabaseResponse, Request},
|
||||
types::{AccountState, FatalError, Game, User},
|
||||
};
|
||||
|
||||
use super::{types::GameId, CharacterId, CharsheetRow, DatabaseRequest, SessionId, UserId};
|
||||
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||
|
||||
lazy_static! {
|
||||
static ref MIGRATIONS: Migrations<'static> =
|
||||
Migrations::from_directory(&MIGRATIONS_DIR).unwrap();
|
||||
}
|
||||
|
||||
pub struct DiskDb {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl DiskDb {
|
||||
pub fn new<P>(path: Option<P>) -> Result<Self, FatalError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut conn = match path {
|
||||
None => Connection::open(":memory:").expect("to create a memory connection"),
|
||||
Some(path) => Connection::open(path).expect("to create connection"),
|
||||
};
|
||||
|
||||
MIGRATIONS
|
||||
.to_latest(&mut conn)
|
||||
.map_err(|err| FatalError::DatabaseMigrationFailure(format!("{}", err)))?;
|
||||
|
||||
Ok(DiskDb { conn })
|
||||
}
|
||||
|
||||
pub fn user(&self, id: &UserId) -> Result<Option<User>, FatalError> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT * FROM users WHERE uuid=?")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
let items: Vec<User> = stmt
|
||||
.query_map([id.as_str()], |row| {
|
||||
Ok(User {
|
||||
id: row.get(0).unwrap(),
|
||||
name: row.get(1).unwrap(),
|
||||
password: row.get(2).unwrap(),
|
||||
admin: row.get(3).unwrap(),
|
||||
state: row.get(4).unwrap(),
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<User>, rusqlite::Error>>()
|
||||
.unwrap();
|
||||
match &items[..] {
|
||||
[] => Ok(None),
|
||||
[item] => Ok(Some(item.clone())),
|
||||
_ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_by_username(&self, username: &str) -> Result<Option<User>, FatalError> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT * FROM users WHERE name=?")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
let items: Vec<User> = stmt
|
||||
.query_map([username], |row| {
|
||||
Ok(User {
|
||||
id: row.get(0).unwrap(),
|
||||
name: row.get(1).unwrap(),
|
||||
password: row.get(2).unwrap(),
|
||||
admin: row.get(3).unwrap(),
|
||||
state: row.get(4).unwrap(),
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<User>, rusqlite::Error>>()
|
||||
.unwrap();
|
||||
match &items[..] {
|
||||
[] => Ok(None),
|
||||
[item] => Ok(Some(item.clone())),
|
||||
_ => Err(FatalError::NonUniqueDatabaseKey(username.to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_user(
|
||||
&self,
|
||||
name: &str,
|
||||
password: &str,
|
||||
admin: bool,
|
||||
state: AccountState,
|
||||
) -> Result<UserId, FatalError> {
|
||||
let user_id = UserId::default();
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
stmt.execute((user_id.as_str(), name, password, admin, state))
|
||||
.unwrap();
|
||||
Ok(user_id)
|
||||
}
|
||||
|
||||
pub fn save_user(&self, user: User) -> Result<UserId, FatalError> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("UPDATE users SET name=?, password=?, admin=?, state=? WHERE uuid=?")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
stmt.execute((
|
||||
user.name,
|
||||
user.password,
|
||||
user.admin,
|
||||
user.state,
|
||||
user.id.as_str(),
|
||||
))
|
||||
.unwrap();
|
||||
Ok(user.id)
|
||||
}
|
||||
|
||||
pub fn users(&self) -> Result<Vec<User>, FatalError> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT * FROM users")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
let items = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(User {
|
||||
id: row.get(0).unwrap(),
|
||||
name: row.get(1).unwrap(),
|
||||
password: row.get(2).unwrap(),
|
||||
admin: row.get(3).unwrap(),
|
||||
state: row.get(4).unwrap(),
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<User>, rusqlite::Error>>()
|
||||
.unwrap();
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub fn create_game(
|
||||
&self,
|
||||
gm: &UserId,
|
||||
game_type: &str,
|
||||
name: &str,
|
||||
) -> Result<GameId, FatalError> {
|
||||
let game_id = GameId::new();
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("INSERT INTO games VALUES (?, ?, ?, ?)")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
stmt.execute((game_id.as_str(), game_type, gm.as_str(), name))
|
||||
.unwrap();
|
||||
Ok(game_id)
|
||||
}
|
||||
|
||||
pub fn save_game(&self, game: Game) -> Result<(), FatalError> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("UPDATE games SET gm=? type_=? name=? WHERE id=?")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
stmt.execute((game.gm.as_str(), game.type_, game.name, game.id.as_str()))
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn games(&self) -> Result<Vec<Game>, FatalError> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT * FROM games")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
let items = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(Game {
|
||||
id: row.get(0).unwrap(),
|
||||
type_: row.get(1).unwrap(),
|
||||
gm: row.get(2).unwrap(),
|
||||
name: row.get(3).unwrap(),
|
||||
players: vec![],
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<Game>, rusqlite::Error>>()
|
||||
.unwrap();
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub fn session(&self, session_id: &SessionId) -> Result<Option<User>, FatalError> {
|
||||
let mut stmt = self.conn
|
||||
.prepare("SELECT u.uuid, u.name, u.password, u.admin, u.state FROM sessions s INNER JOIN users u ON u.uuid = s.user_id WHERE s.id = ?")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
|
||||
let items: Vec<User> = stmt
|
||||
.query_map([session_id.as_str()], |row| {
|
||||
Ok(User {
|
||||
id: row.get(0).unwrap(),
|
||||
name: row.get(1).unwrap(),
|
||||
password: row.get(2).unwrap(),
|
||||
admin: row.get(3).unwrap(),
|
||||
state: row.get(4).unwrap(),
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<User>, rusqlite::Error>>()
|
||||
.unwrap();
|
||||
match &items[..] {
|
||||
[] => Ok(None),
|
||||
[item] => Ok(Some(item.clone())),
|
||||
_ => Err(FatalError::NonUniqueDatabaseKey(
|
||||
session_id.as_str().to_owned(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_session(&self, user_id: &UserId) -> Result<SessionId, FatalError> {
|
||||
match self.user(user_id) {
|
||||
Ok(Some(_)) => {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("INSERT INTO sessions VALUES (?, ?)")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
|
||||
let session_id = SessionId::new();
|
||||
stmt.execute((session_id.as_str(), user_id.as_str()))
|
||||
.unwrap();
|
||||
Ok(session_id)
|
||||
}
|
||||
Ok(None) => Err(FatalError::DatabaseKeyMissing),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_session(&self, session_id: &SessionId) -> Result<(), FatalError> {
|
||||
match self.session(session_id) {
|
||||
Ok(Some(_)) => {
|
||||
let mut stmt = self.conn.prepare("DELETE FROM sessions WHERE id = ?")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
|
||||
let session_id = SessionId::new();
|
||||
stmt.execute((session_id.as_str(),)).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
Ok(None) => Err(FatalError::DatabaseKeyMissing),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn character(&self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT uuid, game, data FROM characters WHERE uuid=?")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
let items: Vec<CharsheetRow> = stmt
|
||||
.query_map([id.as_str()], |row| {
|
||||
let data: String = row.get(2).unwrap();
|
||||
Ok(CharsheetRow {
|
||||
id: row.get(0).unwrap(),
|
||||
game: row.get(1).unwrap(),
|
||||
data: serde_json::from_str(&data).unwrap(),
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<CharsheetRow>, rusqlite::Error>>()
|
||||
.unwrap();
|
||||
match &items[..] {
|
||||
[] => Ok(None),
|
||||
[item] => Ok(Some(item.clone())),
|
||||
_ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_character(
|
||||
&self,
|
||||
char_id: Option<CharacterId>,
|
||||
game: GameId,
|
||||
character: serde_json::Value,
|
||||
) -> std::result::Result<CharacterId, FatalError> {
|
||||
match char_id {
|
||||
None => {
|
||||
let char_id = CharacterId::new();
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("INSERT INTO characters VALUES (?, ?, ?)")
|
||||
.unwrap();
|
||||
stmt.execute((char_id.as_str(), game.as_str(), character.to_string()))
|
||||
.unwrap();
|
||||
|
||||
Ok(char_id)
|
||||
}
|
||||
Some(char_id) => {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("UPDATE characters SET data=? WHERE uuid=?")
|
||||
.unwrap();
|
||||
stmt.execute((character.to_string(), char_id.as_str()))
|
||||
.unwrap();
|
||||
|
||||
Ok(char_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
||||
while let Ok(DatabaseRequest { tx, req }) = requestor.recv().await {
|
||||
match req {
|
||||
Request::Charsheet(id) => {
|
||||
let sheet = db.character(id);
|
||||
match sheet {
|
||||
Ok(sheet) => {
|
||||
tx.send(DatabaseResponse::Charsheet(sheet)).await.unwrap();
|
||||
}
|
||||
_ => unimplemented!("errors for Charsheet"),
|
||||
}
|
||||
}
|
||||
Request::CreateUser(username, password, admin, state) => {
|
||||
let user_id = db.create_user(&username, &password, admin, state).unwrap();
|
||||
tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap();
|
||||
}
|
||||
Request::CreateGame(_, _, _) => {}
|
||||
Request::CreateSession(id) => {
|
||||
let session_id = db.create_session(&id).unwrap();
|
||||
tx.send(DatabaseResponse::CreateSession(session_id))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
Request::DeleteSession(id) => {
|
||||
db.delete_session(&id).unwrap();
|
||||
tx.send(DatabaseResponse::DeleteSession).await.unwrap();
|
||||
}
|
||||
Request::Games => match db.games() {
|
||||
Ok(games) => tx.send(DatabaseResponse::Games(games)).await.unwrap(),
|
||||
_ => unimplemented!("errors for Request::Games"),
|
||||
},
|
||||
Request::Game(_game_id) => {
|
||||
unimplemented!("Request::Game handler");
|
||||
}
|
||||
Request::SaveGame(game) => {
|
||||
let id = game.id.clone();
|
||||
let save_result = db.save_game(game);
|
||||
match save_result {
|
||||
Ok(_) => {
|
||||
tx.send(DatabaseResponse::SaveGame(id)).await.unwrap();
|
||||
}
|
||||
err => panic!("{:?}", err),
|
||||
}
|
||||
}
|
||||
Request::User(uid) => {
|
||||
let user = db.user(&uid);
|
||||
match user {
|
||||
Ok(user) => {
|
||||
tx.send(DatabaseResponse::User(user)).await.unwrap();
|
||||
}
|
||||
err => panic!("{:?}", err),
|
||||
}
|
||||
}
|
||||
Request::UserByUsername(username) => {
|
||||
let user = db.user_by_username(&username);
|
||||
match user {
|
||||
Ok(user) => tx.send(DatabaseResponse::User(user)).await.unwrap(),
|
||||
err => panic!("{:?}", err),
|
||||
}
|
||||
}
|
||||
Request::SaveUser(user) => {
|
||||
let user_id = db.save_user(user);
|
||||
match user_id {
|
||||
Ok(user_id) => {
|
||||
tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap();
|
||||
}
|
||||
err => panic!("{:?}", err),
|
||||
}
|
||||
}
|
||||
Request::Session(session_id) => {
|
||||
let user = db.session(&session_id);
|
||||
match user {
|
||||
Ok(user) => tx.send(DatabaseResponse::Session(user)).await.unwrap(),
|
||||
err => panic!("{:?}", err),
|
||||
}
|
||||
}
|
||||
Request::Users => {
|
||||
let users = db.users();
|
||||
match users {
|
||||
Ok(users) => {
|
||||
tx.send(DatabaseResponse::Users(users)).await.unwrap();
|
||||
}
|
||||
_ => unimplemented!("request::Users"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
230
visions-prototype/server/src/database/mod.rs
Normal file
230
visions-prototype/server/src/database/mod.rs
Normal file
@ -0,0 +1,230 @@
|
||||
mod disk_db;
|
||||
mod types;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use async_std::channel::{bounded, Sender};
|
||||
use async_trait::async_trait;
|
||||
use disk_db::{db_handler, DiskDb};
|
||||
pub use types::{CharacterId, CharsheetRow, GameId, SessionId, UserId};
|
||||
|
||||
use crate::types::{AccountState, FatalError, Game, User};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Request {
|
||||
Charsheet(CharacterId),
|
||||
CreateGame(UserId, String, String),
|
||||
CreateSession(UserId),
|
||||
CreateUser(String, String, bool, AccountState),
|
||||
DeleteSession(SessionId),
|
||||
Game(GameId),
|
||||
Games,
|
||||
SaveGame(Game),
|
||||
SaveUser(User),
|
||||
Session(SessionId),
|
||||
User(UserId),
|
||||
UserByUsername(String),
|
||||
Users,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DatabaseRequest {
|
||||
tx: Sender<DatabaseResponse>,
|
||||
req: Request,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DatabaseResponse {
|
||||
Charsheet(Option<CharsheetRow>),
|
||||
CreateSession(SessionId),
|
||||
DeleteSession,
|
||||
Games(Vec<Game>),
|
||||
Game(Option<Game>),
|
||||
SaveGame(GameId),
|
||||
SaveUser(UserId),
|
||||
Session(Option<User>),
|
||||
User(Option<User>),
|
||||
Users(Vec<User>),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Database: Send + Sync {
|
||||
async fn create_session(&self, id: &UserId) -> Result<SessionId, FatalError>;
|
||||
async fn session(&self, id: &SessionId) -> Result<Option<User>, FatalError>;
|
||||
async fn delete_session(&self, id: &SessionId) -> Result<(), FatalError>;
|
||||
|
||||
async fn character(&self, id: &CharacterId) -> Result<Option<CharsheetRow>, FatalError>;
|
||||
|
||||
async fn create_game(
|
||||
&self,
|
||||
gm: &UserId,
|
||||
game_type: &str,
|
||||
name: &str,
|
||||
) -> Result<GameId, FatalError>;
|
||||
async fn save_game(&self, game: Game) -> Result<GameId, FatalError>;
|
||||
async fn game(&self, _: &GameId) -> Result<Option<Game>, FatalError>;
|
||||
async fn games(&self) -> Result<Vec<Game>, FatalError>;
|
||||
|
||||
async fn create_user(
|
||||
&self,
|
||||
name: &str,
|
||||
password: &str,
|
||||
admin: bool,
|
||||
state: AccountState,
|
||||
) -> Result<UserId, FatalError>;
|
||||
async fn save_user(&self, user: User) -> Result<UserId, FatalError>;
|
||||
async fn user(&self, _: &UserId) -> Result<Option<User>, FatalError>;
|
||||
async fn user_by_username(&self, _: &str) -> Result<Option<User>, FatalError>;
|
||||
async fn users(&self) -> Result<Vec<User>, FatalError>;
|
||||
}
|
||||
|
||||
pub struct DbConn {
|
||||
conn: Sender<DatabaseRequest>,
|
||||
handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl DbConn {
|
||||
pub fn new<P>(path: Option<P>) -> Self
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let (tx, rx) = bounded::<DatabaseRequest>(5);
|
||||
let db = DiskDb::new(path).unwrap();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
db_handler(db, rx).await;
|
||||
});
|
||||
|
||||
Self { conn: tx, handle }
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! send_request {
|
||||
($s:expr, $req:expr, $resp_h:pat => $block:expr) => {{
|
||||
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||
let request = DatabaseRequest { tx, req: $req };
|
||||
match $s.conn.send(request).await {
|
||||
Ok(()) => (),
|
||||
Err(_) => return Err(FatalError::DatabaseConnectionLost),
|
||||
};
|
||||
|
||||
match rx
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|_| FatalError::DatabaseConnectionLost)
|
||||
{
|
||||
Ok($resp_h) => $block,
|
||||
Ok(_) => Err(FatalError::MessageMismatch),
|
||||
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Database for DbConn {
|
||||
async fn create_session(&self, id: &UserId) -> Result<SessionId, FatalError> {
|
||||
send_request!(self, Request::CreateSession(id.to_owned()), DatabaseResponse::CreateSession(session_id) => Ok(session_id))
|
||||
}
|
||||
async fn session(&self, id: &SessionId) -> Result<Option<User>, FatalError> {
|
||||
send_request!(self, Request::Session(id.to_owned()), DatabaseResponse::Session(row) => Ok(row))
|
||||
}
|
||||
async fn delete_session(&self, id: &SessionId) -> Result<(), FatalError> {
|
||||
send_request!(self, Request::DeleteSession(id.to_owned()), DatabaseResponse::DeleteSession => Ok(()))
|
||||
}
|
||||
async fn character(&self, id: &CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
|
||||
send_request!(self, Request::Charsheet(id.to_owned()), DatabaseResponse::Charsheet(row) => Ok(row))
|
||||
}
|
||||
|
||||
async fn create_game(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
game_type: &str,
|
||||
game_name: &str,
|
||||
) -> Result<GameId, FatalError> {
|
||||
send_request!(self, Request::CreateGame(user_id.to_owned(), game_type.to_owned(), game_name.to_owned()), DatabaseResponse::SaveGame(game_id) => Ok(game_id))
|
||||
}
|
||||
async fn save_game(&self, game: Game) -> Result<GameId, FatalError> {
|
||||
send_request!(self, Request::SaveGame(game), DatabaseResponse::SaveGame(game_id) => Ok(game_id))
|
||||
}
|
||||
async fn game(&self, game_id: &GameId) -> Result<Option<Game>, FatalError> {
|
||||
send_request!(self, Request::Game(game_id.clone()), DatabaseResponse::Game(game) => Ok(game))
|
||||
}
|
||||
async fn games(&self) -> Result<Vec<Game>, FatalError> {
|
||||
send_request!(self, Request::Games, DatabaseResponse::Games(lst) => Ok(lst))
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
&self,
|
||||
name: &str,
|
||||
password: &str,
|
||||
admin: bool,
|
||||
state: AccountState,
|
||||
) -> Result<UserId, FatalError> {
|
||||
send_request!(self,
|
||||
Request::CreateUser(name.to_owned(), password.to_owned(), admin, state),
|
||||
DatabaseResponse::SaveUser(user_id) => Ok(user_id))
|
||||
}
|
||||
async fn save_user(&self, user: User) -> Result<UserId, FatalError> {
|
||||
send_request!(self,
|
||||
Request::SaveUser(user),
|
||||
DatabaseResponse::SaveUser(user_id) => Ok(user_id))
|
||||
}
|
||||
async fn user(&self, uid: &UserId) -> Result<Option<User>, FatalError> {
|
||||
send_request!(self, Request::User(uid.clone()), DatabaseResponse::User(user) => Ok(user))
|
||||
}
|
||||
async fn user_by_username(&self, username: &str) -> Result<Option<User>, FatalError> {
|
||||
send_request!(self, Request::UserByUsername(username.to_owned()), DatabaseResponse::User(user) => Ok(user))
|
||||
}
|
||||
async fn users(&self) -> Result<Vec<User>, FatalError> {
|
||||
send_request!(self, Request::Users, DatabaseResponse::Users(lst) => Ok(lst))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cool_asserts::assert_matches;
|
||||
use disk_db::DiskDb;
|
||||
use types::GameId;
|
||||
|
||||
use super::*;
|
||||
|
||||
const SOREN: &str = r#"{ "type_": "Candela", "name": "Soren Jensen", "pronouns": "he/him", "circle": "Circle of the Bluest Sky", "style": "dapper gentleman", "catalyst": "a cursed book", "question": "What were the contents of that book?", "nerve": { "type_": "nerve", "drives": { "current": 1, "max": 2 }, "resistances": { "current": 0, "max": 3 }, "move": { "gilded": false, "score": 2 }, "strike": { "gilded": false, "score": 1 }, "control": { "gilded": true, "score": 0 } }, "cunning": { "type_": "cunning", "drives": { "current": 1, "max": 1 }, "resistances": { "current": 0, "max": 3 }, "sway": { "gilded": false, "score": 0 }, "read": { "gilded": false, "score": 0 }, "hide": { "gilded": false, "score": 0 } }, "intuition": { "type_": "intuition", "drives": { "current": 0, "max": 0 }, "resistances": { "current": 0, "max": 3 }, "survey": { "gilded": false, "score": 0 }, "focus": { "gilded": false, "score": 0 }, "sense": { "gilded": false, "score": 0 } }, "role": "Slink", "role_abilities": [ "Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?" ], "specialty": "Detective", "specialty_abilities": [ "Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you have deduced." ] }"#;
|
||||
|
||||
fn setup_db() -> (DiskDb, GameId) {
|
||||
let no_path: Option<PathBuf> = None;
|
||||
let db = DiskDb::new(no_path).unwrap();
|
||||
|
||||
db.create_user("admin", "abcdefg", true, AccountState::Normal)
|
||||
.unwrap();
|
||||
let game_id = db
|
||||
.create_game(
|
||||
&UserId::from("admin"),
|
||||
"Candela",
|
||||
"Circle of the Winter Solstice",
|
||||
)
|
||||
.unwrap();
|
||||
(db, game_id)
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn it_can_retrieve_a_character() {
|
||||
let (db, game_id) = setup_db();
|
||||
|
||||
assert_matches!(db.character(CharacterId::from("1")), Ok(None));
|
||||
|
||||
let js: serde_json::Value = serde_json::from_str(SOREN).unwrap();
|
||||
let soren_id = db.save_character(None, game_id, js.clone()).unwrap();
|
||||
assert_matches!(db.character(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_can_retrieve_a_character_through_conn() {
|
||||
let memory_db: Option<PathBuf> = None;
|
||||
let conn = DbConn::new(memory_db);
|
||||
|
||||
assert_matches!(conn.character(&CharacterId::from("1")).await, Ok(None));
|
||||
}
|
||||
}
|
209
visions-prototype/server/src/database/types.rs
Normal file
209
visions-prototype/server/src/database/types.rs
Normal file
@ -0,0 +1,209 @@
|
||||
use std::fmt;
|
||||
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct UserId(String);
|
||||
|
||||
impl UserId {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UserId {
|
||||
fn default() -> Self {
|
||||
Self(format!("{}", Uuid::new_v4().hyphenated()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for UserId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for UserId {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for UserId {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
match value {
|
||||
ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for UserId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct SessionId(String);
|
||||
|
||||
impl SessionId {
|
||||
pub fn new() -> Self {
|
||||
Self(format!("{}", Uuid::new_v4().hyphenated()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for SessionId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for SessionId {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for SessionId {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
match value {
|
||||
ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SessionId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct GameId(String);
|
||||
|
||||
impl GameId {
|
||||
pub fn new() -> Self {
|
||||
Self(format!("{}", Uuid::new_v4().hyphenated()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for GameId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for GameId {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for GameId {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
match value {
|
||||
ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct CharacterId(String);
|
||||
|
||||
impl CharacterId {
|
||||
pub fn new() -> Self {
|
||||
Self(format!("{}", Uuid::new_v4().hyphenated()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for CharacterId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CharacterId {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for CharacterId {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
match value {
|
||||
ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Role {
|
||||
userid: UserId,
|
||||
gameid: GameId,
|
||||
role: String,
|
||||
}
|
||||
|
||||
/*
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GameRow {
|
||||
pub id: GameId,
|
||||
pub gm: UserId,
|
||||
pub game_type: String,
|
||||
pub name: String,
|
||||
}
|
||||
*/
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CharsheetRow {
|
||||
pub id: String,
|
||||
pub game: GameId,
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SessionRow {
|
||||
id: SessionId,
|
||||
user_id: SessionId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DateTime(pub chrono::DateTime<Utc>);
|
||||
|
||||
impl FromSql for DateTime {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
match value {
|
||||
ValueRef::Text(text) => String::from_utf8(text.to_vec())
|
||||
.map_err(|_err| FromSqlError::InvalidType)
|
||||
.and_then(|s| {
|
||||
NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S")
|
||||
.map_err(|_err| FromSqlError::InvalidType)
|
||||
})
|
||||
.and_then(|dt| Ok(DateTime(dt.and_utc()))),
|
||||
_ => Err(FromSqlError::InvalidType),
|
||||
}
|
||||
}
|
||||
}
|
29
visions-prototype/server/src/handlers/game_management.rs
Normal file
29
visions-prototype/server/src/handlers/game_management.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use axum::http::HeaderMap;
|
||||
use result_extended::ResultExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
core::Core,
|
||||
database::GameId,
|
||||
types::{AppError, FatalError},
|
||||
};
|
||||
|
||||
use super::auth_required;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||
pub struct CreateGameRequest {
|
||||
pub game_type: String,
|
||||
pub game_name: String,
|
||||
}
|
||||
|
||||
pub async fn create_game(
|
||||
core: Core,
|
||||
headers: HeaderMap,
|
||||
req: CreateGameRequest,
|
||||
) -> ResultExt<GameId, AppError, FatalError> {
|
||||
auth_required(core.clone(), headers, |user| async move {
|
||||
core.create_game(&user.id, &req.game_type, &req.game_name)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
244
visions-prototype/server/src/handlers/mod.rs
Normal file
244
visions-prototype/server/src/handlers/mod.rs
Normal file
@ -0,0 +1,244 @@
|
||||
mod game_management;
|
||||
mod user_management;
|
||||
mod types;
|
||||
|
||||
use axum::{http::StatusCode, Json};
|
||||
use futures::Future;
|
||||
pub use game_management::*;
|
||||
pub use user_management::*;
|
||||
pub use types::*;
|
||||
|
||||
use result_extended::ResultExt;
|
||||
|
||||
use crate::{
|
||||
core::Core,
|
||||
types::{AppError, FatalError},
|
||||
};
|
||||
|
||||
pub async fn wrap_handler<F, A, Fut>(f: F) -> (StatusCode, Json<Option<A>>)
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = ResultExt<A, AppError, FatalError>>,
|
||||
{
|
||||
match f().await {
|
||||
ResultExt::Ok(val) => (StatusCode::OK, Json(Some(val))),
|
||||
ResultExt::Err(AppError::BadRequest) => (StatusCode::BAD_REQUEST, Json(None)),
|
||||
ResultExt::Err(AppError::CouldNotCreateObject) => (StatusCode::BAD_REQUEST, Json(None)),
|
||||
ResultExt::Err(AppError::NotFound(_)) => (StatusCode::NOT_FOUND, Json(None)),
|
||||
ResultExt::Err(AppError::Inaccessible(_)) => (StatusCode::NOT_FOUND, Json(None)),
|
||||
ResultExt::Err(AppError::PermissionDenied) => (StatusCode::FORBIDDEN, Json(None)),
|
||||
ResultExt::Err(AppError::AuthFailed) => (StatusCode::UNAUTHORIZED, Json(None)),
|
||||
ResultExt::Err(AppError::JsonError(_)) => (StatusCode::INTERNAL_SERVER_ERROR, Json(None)),
|
||||
ResultExt::Err(AppError::UnexpectedError(_)) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(None))
|
||||
}
|
||||
ResultExt::Err(AppError::UsernameUnavailable) => (StatusCode::BAD_REQUEST, Json(None)),
|
||||
ResultExt::Fatal(err) => {
|
||||
panic!("The server encountered a fatal error: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn healthcheck(core: Core) -> Vec<u8> {
|
||||
match core.status().await {
|
||||
ResultExt::Ok(s) => serde_json::to_vec(&HealthCheck {
|
||||
ok: s.ok,
|
||||
})
|
||||
.unwrap(),
|
||||
ResultExt::Err(_) => serde_json::to_vec(&HealthCheck { ok: false }).unwrap(),
|
||||
ResultExt::Fatal(err) => panic!("{}", err),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
pub async fn handle_file(core: Core, asset_id: AssetId) -> impl Reply {
|
||||
handler(async move {
|
||||
let (mime, bytes) = return_error!(core.get_asset(asset_id).await);
|
||||
ok(Response::builder()
|
||||
.header("content-type", mime.to_string())
|
||||
.body(bytes)
|
||||
.unwrap())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_available_images(core: Core) -> impl Reply {
|
||||
handler(async move {
|
||||
let image_paths: Vec<String> = core
|
||||
.available_images()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|path| format!("{}", path.as_str()))
|
||||
.collect();
|
||||
|
||||
ok(Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(serde_json::to_vec(&image_paths).unwrap())
|
||||
.unwrap())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct RegisterRequest {}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct RegisterResponse {
|
||||
url: String,
|
||||
}
|
||||
|
||||
pub async fn handle_register_client(core: Core, _request: RegisterRequest) -> impl Reply {
|
||||
handler(async move {
|
||||
let client_id = core.register_client().await;
|
||||
|
||||
ok(Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(
|
||||
serde_json::to_vec(&RegisterResponse {
|
||||
url: format!("ws://127.0.0.1:8001/ws/{}", client_id),
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_unregister_client(core: Core, client_id: String) -> impl Reply {
|
||||
handler(async move {
|
||||
core.unregister_client(client_id).await;
|
||||
|
||||
ok(Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.body(vec![])
|
||||
.unwrap())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_connect_websocket(
|
||||
core: Core,
|
||||
ws: warp::ws::Ws,
|
||||
client_id: String,
|
||||
) -> impl Reply {
|
||||
ws.on_upgrade(move |socket| {
|
||||
println!("upgrading websocket");
|
||||
let core = core.clone();
|
||||
async move {
|
||||
let (mut ws_sender, _) = socket.split();
|
||||
let mut receiver = core.connect_client(client_id.clone()).await;
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
let tabletop = core.tabletop().await;
|
||||
let _ = ws_sender
|
||||
.send(Message::text(
|
||||
serde_json::to_string(&crate::types::Message::UpdateTabletop(tabletop))
|
||||
.unwrap(),
|
||||
))
|
||||
.await;
|
||||
while let Some(msg) = receiver.recv().await {
|
||||
println!("Relaying message: {:?}", msg);
|
||||
let _ = ws_sender
|
||||
.send(Message::text(serde_json::to_string(&msg).unwrap()))
|
||||
.await;
|
||||
}
|
||||
println!("process ended for id {}", client_id);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_set_background_image(core: Core, image_name: String) -> impl Reply {
|
||||
handler(async move {
|
||||
let _ = core.set_background_image(AssetId::from(image_name)).await;
|
||||
|
||||
ok(Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Access-Control-Allow-Methods", "*")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(vec![])
|
||||
.unwrap())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_get_users(core: Core) -> Response<Vec<u8>> {
|
||||
unimplemented!()
|
||||
/*
|
||||
handler(async move {
|
||||
let users = match core.list_users().await {
|
||||
ResultExt::Ok(users) => users,
|
||||
ResultExt::Err(err) => return ResultExt::Err(err),
|
||||
ResultExt::Fatal(err) => return ResultExt::Fatal(err),
|
||||
};
|
||||
|
||||
ok(Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(serde_json::to_vec(&users).unwrap())
|
||||
.unwrap())
|
||||
})
|
||||
.await
|
||||
*/
|
||||
}
|
||||
|
||||
pub async fn handle_get_games(core: Core) -> impl Reply {
|
||||
handler(async move {
|
||||
let games = match core.list_games().await {
|
||||
ResultExt::Ok(games) => games,
|
||||
ResultExt::Err(err) => return ResultExt::Err(err),
|
||||
ResultExt::Fatal(err) => return ResultExt::Fatal(err),
|
||||
};
|
||||
|
||||
ok(Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(serde_json::to_vec(&games).unwrap())
|
||||
.unwrap())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_get_charsheet(core: Core, charid: String) -> impl Reply {
|
||||
handler(async move {
|
||||
let sheet = match core.get_charsheet(CharacterId::from(charid)).await {
|
||||
ResultExt::Ok(sheet) => sheet,
|
||||
ResultExt::Err(err) => return ResultExt::Err(err),
|
||||
ResultExt::Fatal(err) => return ResultExt::Fatal(err),
|
||||
};
|
||||
|
||||
match sheet {
|
||||
Some(sheet) => ok(Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(serde_json::to_vec(&sheet).unwrap())
|
||||
.unwrap()),
|
||||
None => ok(Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(vec![])
|
||||
.unwrap()),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_set_admin_password(core: Core, password: String) -> impl Reply {
|
||||
handler(async move {
|
||||
let status = return_error!(core.status().await);
|
||||
if status.admin_enabled {
|
||||
return error(AppError::PermissionDenied);
|
||||
}
|
||||
|
||||
core.set_password(UserId::from("admin"), password).await;
|
||||
ok(Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Access-Control-Allow-Methods", "*")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(vec![])
|
||||
.unwrap())
|
||||
})
|
||||
.await
|
||||
}
|
||||
*/
|
83
visions-prototype/server/src/handlers/types.rs
Normal file
83
visions-prototype/server/src/handlers/types.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::database::UserId;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||
pub struct HealthCheck {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
#[typeshare]
|
||||
pub enum AccountState {
|
||||
Normal,
|
||||
PasswordReset(String),
|
||||
Locked,
|
||||
}
|
||||
|
||||
impl From<crate::types::AccountState> for AccountState {
|
||||
fn from(s: crate::types::AccountState) -> Self {
|
||||
match s {
|
||||
crate::types::AccountState::Normal => Self::Normal,
|
||||
crate::types::AccountState::PasswordReset(r) => {
|
||||
Self::PasswordReset(format!("{}", r.format("%Y-%m-%d %H:%M:%S")))
|
||||
}
|
||||
crate::types::AccountState::Locked => Self::Locked,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[typeshare]
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
pub admin: bool,
|
||||
pub state: AccountState,
|
||||
}
|
||||
|
||||
impl From<crate::types::User> for User {
|
||||
fn from(u: crate::types::User) -> Self {
|
||||
Self {
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
password: u.password,
|
||||
admin: u.admin,
|
||||
state: AccountState::from(u.state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[typeshare]
|
||||
pub struct UserOverview {
|
||||
pub id: UserId,
|
||||
pub name: String,
|
||||
pub is_admin: bool,
|
||||
pub state: AccountState,
|
||||
pub games: Vec<crate::types::GameOverview>,
|
||||
}
|
||||
|
||||
impl UserOverview {
|
||||
pub fn new(user: crate::types::UserOverview, games: Vec<crate::types::GameOverview>) -> Self {
|
||||
let s = Self::from(user);
|
||||
Self{ games, ..s }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::types::UserOverview> for UserOverview {
|
||||
fn from(input: crate::types::UserOverview) -> Self {
|
||||
Self {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
is_admin: input.is_admin,
|
||||
state: AccountState::from(input.state),
|
||||
games: vec![],
|
||||
}
|
||||
}
|
||||
}
|
183
visions-prototype/server/src/handlers/user_management.rs
Normal file
183
visions-prototype/server/src/handlers/user_management.rs
Normal file
@ -0,0 +1,183 @@
|
||||
use axum::{http::HeaderMap, Json};
|
||||
use futures::Future;
|
||||
use result_extended::{error, ok, return_error, ResultExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::{
|
||||
core::{AuthResponse, Core},
|
||||
database::{SessionId, UserId},
|
||||
types::{AppError, FatalError, User},
|
||||
};
|
||||
|
||||
use super::UserOverview;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct AuthRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct CreateUserRequest {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct SetPasswordRequest {
|
||||
pub password_1: String,
|
||||
pub password_2: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct SetAdminPasswordRequest {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
fn parse_session_header(headers: HeaderMap) -> ResultExt<Option<SessionId>, AppError, FatalError> {
|
||||
match headers.get("Authorization") {
|
||||
Some(token) => {
|
||||
println!("check_session: {:?}", token);
|
||||
match token
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.split(" ")
|
||||
.collect::<Vec<&str>>()
|
||||
.as_slice()
|
||||
{
|
||||
[_schema, token] => ok(Some(SessionId::from(*token))),
|
||||
_ => error(AppError::BadRequest),
|
||||
}
|
||||
}
|
||||
None => ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_session(
|
||||
core: &Core,
|
||||
headers: HeaderMap,
|
||||
) -> ResultExt<Option<User>, AppError, FatalError> {
|
||||
match return_error!(parse_session_header(headers)) {
|
||||
Some(session_id) => core.session(&session_id).await,
|
||||
None => ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn auth_required<F, A, Fut>(
|
||||
core: Core,
|
||||
headers: HeaderMap,
|
||||
f: F,
|
||||
) -> ResultExt<A, AppError, FatalError>
|
||||
where
|
||||
F: FnOnce(User) -> Fut,
|
||||
Fut: Future<Output = ResultExt<A, AppError, FatalError>>,
|
||||
{
|
||||
match return_error!(check_session(&core, headers).await) {
|
||||
Some(user) => f(user).await,
|
||||
None => error(AppError::AuthFailed),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn admin_required<F, A, Fut>(
|
||||
core: Core,
|
||||
headers: HeaderMap,
|
||||
f: F,
|
||||
) -> ResultExt<A, AppError, FatalError>
|
||||
where
|
||||
F: FnOnce(User) -> Fut,
|
||||
Fut: Future<Output = ResultExt<A, AppError, FatalError>>,
|
||||
{
|
||||
match return_error!(check_session(&core, headers).await) {
|
||||
Some(user) => {
|
||||
if user.admin {
|
||||
f(user).await
|
||||
} else {
|
||||
error(AppError::PermissionDenied)
|
||||
}
|
||||
}
|
||||
None => error(AppError::AuthFailed),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check_password(
|
||||
core: Core,
|
||||
req: Json<AuthRequest>,
|
||||
) -> ResultExt<AuthResponse, AppError, FatalError> {
|
||||
let Json(AuthRequest { username, password }) = req;
|
||||
println!("check_password: {} {}", username, password);
|
||||
let result = core.auth(&username, &password).await;
|
||||
println!("auth result: {:?}", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub async fn delete_session(core: Core, headers: HeaderMap,) -> ResultExt<(), AppError, FatalError> {
|
||||
/*
|
||||
auth_required(core.clone(), headers, |user| async move {
|
||||
match user_id {
|
||||
Some(user_id) => core.delete_session
|
||||
None => (),
|
||||
}
|
||||
}).await
|
||||
*/
|
||||
|
||||
match return_error!(parse_session_header(headers)) {
|
||||
Some(session_id) => core.delete_session(&session_id).await,
|
||||
None => error(AppError::AuthFailed),
|
||||
}
|
||||
// await core.delete_session(session_id);
|
||||
}
|
||||
|
||||
pub async fn get_user(
|
||||
core: Core,
|
||||
headers: HeaderMap,
|
||||
user_id: Option<UserId>,
|
||||
) -> ResultExt<Option<UserOverview>, AppError, FatalError> {
|
||||
auth_required(core.clone(), headers, |user| async move {
|
||||
match user_id {
|
||||
Some(user_id) => core.user(user_id).await,
|
||||
None => core.user(user.id).await,
|
||||
}
|
||||
.map(|maybe_user| maybe_user.map(|user| UserOverview::from(user)))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_users(
|
||||
core: Core,
|
||||
headers: HeaderMap,
|
||||
) -> ResultExt<Vec<UserOverview>, AppError, FatalError> {
|
||||
auth_required(core.clone(), headers, |_user| async move {
|
||||
core.list_users().await.map(|users| users.into_iter().map(|user| UserOverview::from(user)).collect::<Vec<UserOverview>>())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_user(
|
||||
core: Core,
|
||||
headers: HeaderMap,
|
||||
req: CreateUserRequest,
|
||||
) -> ResultExt<UserId, AppError, FatalError> {
|
||||
admin_required(core.clone(), headers, |_admin| async {
|
||||
core.create_user(&req.username).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_password(
|
||||
core: Core,
|
||||
headers: HeaderMap,
|
||||
req: SetPasswordRequest,
|
||||
) -> ResultExt<(), AppError, FatalError> {
|
||||
auth_required(core.clone(), headers, |user| async {
|
||||
if req.password_1 == req.password_2 {
|
||||
core.set_password(user.id, req.password_1).await
|
||||
} else {
|
||||
error(AppError::BadRequest)
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
33
visions-prototype/server/src/main.rs
Normal file
33
visions-prototype/server/src/main.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use core::Core;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use asset_db::FsAssets;
|
||||
use database::DbConn;
|
||||
|
||||
mod asset_db;
|
||||
mod core;
|
||||
mod database;
|
||||
mod handlers;
|
||||
mod routes;
|
||||
mod types;
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() {
|
||||
/*
|
||||
pretty_env_logger::init();
|
||||
|
||||
let unauthenticated_endpoints = route_healthcheck().or(route_authenticate(core.clone()));
|
||||
let authenticated_endpoints = route_image(core.clone());
|
||||
*/
|
||||
|
||||
let conn = DbConn::new(Some("/home/savanni/game.db"));
|
||||
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
||||
|
||||
let app = routes::routes(core);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:8001")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
468
visions-prototype/server/src/routes.rs
Normal file
468
visions-prototype/server/src/routes.rs
Normal file
@ -0,0 +1,468 @@
|
||||
use axum::{
|
||||
extract::Path,
|
||||
http::{
|
||||
header::{AUTHORIZATION, CONTENT_TYPE},
|
||||
HeaderMap, Method,
|
||||
},
|
||||
routing::{get, post, put},
|
||||
Json, Router,
|
||||
};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
use crate::{
|
||||
core::Core,
|
||||
database::UserId,
|
||||
handlers::{
|
||||
check_password, create_game, create_user, delete_session, get_user, get_users, healthcheck, set_password, wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest
|
||||
},
|
||||
};
|
||||
|
||||
pub fn routes(core: Core) -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/api/v1/health",
|
||||
get({
|
||||
let core = core.clone();
|
||||
move || healthcheck(core)
|
||||
})
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_methods([Method::GET])
|
||||
.allow_origin(Any),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/auth",
|
||||
post({
|
||||
let core = core.clone();
|
||||
move |req: Json<AuthRequest>| wrap_handler(|| check_password(core, req))
|
||||
})
|
||||
.delete({
|
||||
let core = core.clone();
|
||||
move |headers: HeaderMap| wrap_handler(|| delete_session(core, headers))
|
||||
})
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_methods([Method::DELETE, Method::POST])
|
||||
.allow_headers([AUTHORIZATION, CONTENT_TYPE])
|
||||
.allow_origin(Any),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
// By default, just get the self user.
|
||||
"/api/v1/user",
|
||||
get({
|
||||
let core = core.clone();
|
||||
move |headers: HeaderMap| wrap_handler(|| get_user(core, headers, None))
|
||||
})
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_methods([Method::GET])
|
||||
.allow_headers([AUTHORIZATION])
|
||||
.allow_origin(Any),
|
||||
)
|
||||
.put({
|
||||
let core = core.clone();
|
||||
move |headers: HeaderMap, req: Json<CreateUserRequest>| {
|
||||
let Json(req) = req;
|
||||
wrap_handler(|| create_user(core, headers, req))
|
||||
}
|
||||
})
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_methods([Method::PUT])
|
||||
.allow_headers([AUTHORIZATION, CONTENT_TYPE])
|
||||
.allow_origin(Any),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/users",
|
||||
get({
|
||||
let core = core.clone();
|
||||
move |headers: HeaderMap| wrap_handler(|| get_users(core, headers))
|
||||
})
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_methods([Method::GET])
|
||||
.allow_headers([AUTHORIZATION])
|
||||
.allow_origin(Any),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/user/password",
|
||||
put({
|
||||
let core = core.clone();
|
||||
move |headers: HeaderMap, req: Json<SetPasswordRequest>| {
|
||||
let Json(req) = req;
|
||||
wrap_handler(|| set_password(core, headers, req))
|
||||
}
|
||||
})
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_methods([Method::PUT])
|
||||
.allow_headers([AUTHORIZATION, CONTENT_TYPE])
|
||||
.allow_origin(Any),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/user/:user_id",
|
||||
get({
|
||||
let core = core.clone();
|
||||
move |user_id: Path<UserId>, headers: HeaderMap| {
|
||||
let Path(user_id) = user_id;
|
||||
wrap_handler(|| get_user(core, headers, Some(user_id)))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/games",
|
||||
put({
|
||||
let core = core.clone();
|
||||
move |headers: HeaderMap, req: Json<CreateGameRequest>| {
|
||||
let Json(req) = req;
|
||||
wrap_handler(|| create_game(core, headers, req))
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use chrono::Utc;
|
||||
use cool_asserts::assert_matches;
|
||||
use result_extended::ResultExt;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
asset_db::FsAssets,
|
||||
core::{AuthResponse, Core},
|
||||
database::{Database, DbConn, GameId, SessionId, UserId},
|
||||
handlers::CreateGameRequest,
|
||||
types::UserOverview,
|
||||
};
|
||||
|
||||
async fn initialize_test_server() -> (Core, TestServer) {
|
||||
let password_exp = Utc::now() + Duration::from_secs(5);
|
||||
let memory_db: Option<PathBuf> = None;
|
||||
let conn = DbConn::new(memory_db);
|
||||
let _admin_id = conn
|
||||
.create_user(
|
||||
"admin",
|
||||
"aoeu",
|
||||
true,
|
||||
crate::types::AccountState::PasswordReset(password_exp),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
||||
let app = routes(core.clone());
|
||||
let server = TestServer::new(app).unwrap();
|
||||
(core, server)
|
||||
}
|
||||
|
||||
async fn setup_with_admin() -> (Core, TestServer) {
|
||||
let (core, server) = initialize_test_server().await;
|
||||
core.set_password(UserId::from("admin"), "aoeu".to_owned())
|
||||
.await;
|
||||
(core, server)
|
||||
}
|
||||
|
||||
async fn setup_with_disabled_user() -> (Core, TestServer) {
|
||||
let (core, server) = setup_with_admin().await;
|
||||
let uuid = match core.create_user("shephard").await {
|
||||
ResultExt::Ok(uuid) => uuid,
|
||||
ResultExt::Err(err) => panic!("{}", err),
|
||||
ResultExt::Fatal(err) => panic!("{}", err),
|
||||
};
|
||||
core.disable_user(uuid).await;
|
||||
(core, server)
|
||||
}
|
||||
|
||||
async fn setup_with_user() -> (Core, TestServer) {
|
||||
let (core, server) = setup_with_admin().await;
|
||||
let response = server
|
||||
.post("/api/v1/auth")
|
||||
.json(&AuthRequest {
|
||||
username: "admin".to_owned(),
|
||||
password: "aoeu".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
let session_id: Option<SessionId> = response.json();
|
||||
let session_id = session_id.unwrap();
|
||||
|
||||
let response = server
|
||||
.put("/api/v1/user")
|
||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||
.json(&CreateUserRequest {
|
||||
username: "savanni".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
|
||||
let response = server
|
||||
.put("/api/v1/user")
|
||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||
.json(&CreateUserRequest {
|
||||
username: "shephard".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
|
||||
(core, server)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_returns_a_healthcheck() {
|
||||
let (_core, server) = initialize_test_server().await;
|
||||
|
||||
let response = server.get("/api/v1/health").await;
|
||||
response.assert_status_ok();
|
||||
let b: crate::handlers::HealthCheck = response.json();
|
||||
assert_eq!(b, crate::handlers::HealthCheck { ok: true });
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn a_new_user_has_an_expired_password() {
|
||||
let (_core, server) = setup_with_admin().await;
|
||||
|
||||
let response = server
|
||||
.post("/api/v1/auth")
|
||||
.add_header("Content-Type", "application/json")
|
||||
.json(&AuthRequest {
|
||||
username: "admin".to_owned(),
|
||||
password: "aoeu".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
let session_id = response.json::<Option<AuthResponse>>().unwrap();
|
||||
|
||||
let session_id = match session_id {
|
||||
AuthResponse::PasswordReset(session_id) => session_id,
|
||||
AuthResponse::Success(_) => panic!("admin user password has already been set"),
|
||||
AuthResponse::Locked => panic!("admin user is already expired"),
|
||||
};
|
||||
|
||||
let response = server
|
||||
.put("/api/v1/user")
|
||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||
.json("savanni")
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
|
||||
let response = server
|
||||
.post("/api/v1/auth")
|
||||
.add_header("Content-Type", "application/json")
|
||||
.json(&AuthRequest {
|
||||
username: "savanni".to_owned(),
|
||||
password: "".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
let session = response.json::<Option<AuthResponse>>().unwrap();
|
||||
assert_matches!(session, AuthResponse::PasswordReset(_));
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn it_refuses_to_authenticate_a_disabled_user() {
|
||||
let (_core, _server) = setup_with_disabled_user().await;
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn it_forces_changing_expired_password() {
|
||||
let (_core, _server) = setup_with_user().await;
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_authenticates_a_user() {
|
||||
let (_core, server) = setup_with_admin().await;
|
||||
|
||||
let response = server
|
||||
.post("/api/v1/auth")
|
||||
.json(&AuthRequest {
|
||||
username: "admin".to_owned(),
|
||||
password: "wrong".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status(StatusCode::UNAUTHORIZED);
|
||||
|
||||
let response = server
|
||||
.post("/api/v1/auth")
|
||||
.json(&AuthRequest {
|
||||
username: "unknown".to_owned(),
|
||||
password: "wrong".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status(StatusCode::UNAUTHORIZED);
|
||||
|
||||
let response = server
|
||||
.post("/api/v1/auth")
|
||||
.json(&AuthRequest {
|
||||
username: "admin".to_owned(),
|
||||
password: "aoeu".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
assert_matches!(response.json(), Some(AuthResponse::PasswordReset(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_returns_user_profile() {
|
||||
let (_core, server) = setup_with_admin().await;
|
||||
|
||||
let response = server.get("/api/v1/user").await;
|
||||
response.assert_status(StatusCode::UNAUTHORIZED);
|
||||
|
||||
let response = server
|
||||
.post("/api/v1/auth")
|
||||
.json(&AuthRequest {
|
||||
username: "admin".to_owned(),
|
||||
password: "aoeu".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
let session_id = assert_matches!(response.json(), Some(AuthResponse::PasswordReset(session_id)) => session_id);
|
||||
|
||||
let response = server
|
||||
.get("/api/v1/user")
|
||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
let profile: Option<UserOverview> = response.json();
|
||||
let profile = profile.unwrap();
|
||||
assert_eq!(profile.name, "admin");
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn a_user_can_get_any_user_profile() {
|
||||
let (core, server) = setup_with_user().await;
|
||||
|
||||
let savanni = match core.user_by_username("savanni").await {
|
||||
ResultExt::Ok(Some(savanni)) => savanni,
|
||||
ResultExt::Ok(None) => panic!("user was not initialized"),
|
||||
ResultExt::Err(err) => panic!("{:?}", err),
|
||||
ResultExt::Fatal(err) => panic!("{:?}", err),
|
||||
};
|
||||
|
||||
let response = server
|
||||
.post("/api/v1/auth")
|
||||
.json(&AuthRequest {
|
||||
username: "savanni".to_owned(),
|
||||
password: "".to_owned(),
|
||||
})
|
||||
.await;
|
||||
let session_id: Option<SessionId> = response.json();
|
||||
let session_id = session_id.unwrap();
|
||||
|
||||
let response = server
|
||||
.get(&format!("/api/v1/user/{}", savanni.id))
|
||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
let profile: Option<UserOverview> = response.json();
|
||||
let profile = profile.unwrap();
|
||||
assert_eq!(profile.name, "savanni");
|
||||
|
||||
let response = server
|
||||
.get("/api/v1/user/admin")
|
||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||
.await;
|
||||
response.assert_status_ok();
|
||||
let profile: Option<UserOverview> = response.json();
|
||||
let profile = profile.unwrap();
|
||||
assert_eq!(profile.name, "admin");
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn a_user_can_change_their_password() {
|
||||
let (_core, server) = setup_with_user().await;
|
||||
|
||||
let response = server
|
||||
.post("/api/v1/auth")
|
||||
.json(&AuthRequest {
|
||||
username: "savanni".to_owned(),
|
||||
password: "".to_owned(),
|
||||
})
|
||||
.await;
|
||||
let session_id: Option<SessionId> = response.json();
|
||||
let session_id = session_id.unwrap();
|
||||
|
||||
let response = server
|
||||
.get("/api/v1/user")
|
||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||
.await;
|
||||
let profile = response.json::<Option<UserOverview>>().unwrap();
|
||||
assert_eq!(profile.name, "savanni");
|
||||
|
||||
let response = server
|
||||
.put("/api/v1/user/password")
|
||||
.json(&SetPasswordRequest {
|
||||
password_1: "abcdefg".to_owned(),
|
||||
password_2: "abcd".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status(StatusCode::UNAUTHORIZED);
|
||||
|
||||
let response = server
|
||||
.put("/api/v1/user/password")
|
||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||
.json(&SetPasswordRequest {
|
||||
password_1: "abcdefg".to_owned(),
|
||||
password_2: "abcd".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status(StatusCode::BAD_REQUEST);
|
||||
|
||||
let response = server
|
||||
.put("/api/v1/user/password")
|
||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||
.json(&SetPasswordRequest {
|
||||
password_1: "abcdefg".to_owned(),
|
||||
password_2: "abcdefg".to_owned(),
|
||||
})
|
||||
.await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn a_user_can_create_a_game() {
|
||||
let (_core, server) = setup_with_user().await;
|
||||
|
||||
let response = server
|
||||
.post("/api/v1/auth")
|
||||
.json(&AuthRequest {
|
||||
username: "savanni".to_owned(),
|
||||
password: "".to_owned(),
|
||||
})
|
||||
.await;
|
||||
let session_id = response.json::<Option<SessionId>>().unwrap();
|
||||
|
||||
let response = server
|
||||
.put("/api/v1/games")
|
||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||
.json(&CreateGameRequest {
|
||||
game_type: "Candela".to_owned(),
|
||||
game_name: "Circle of the Winter Solstice".to_owned(),
|
||||
})
|
||||
.await;
|
||||
let _game_id = response.json::<Option<GameId>>().unwrap();
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn gms_can_invite_others_into_a_game() {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
201
visions-prototype/server/src/types.rs
Normal file
201
visions-prototype/server/src/types.rs
Normal file
@ -0,0 +1,201 @@
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use rusqlite::{
|
||||
types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef},
|
||||
ToSql,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use thiserror::Error;
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::{
|
||||
asset_db::AssetId,
|
||||
database::{GameId, UserId},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FatalError {
|
||||
#[error("Failed to construct a query")]
|
||||
ConstructQueryFailure(String),
|
||||
|
||||
#[error("Database connection lost")]
|
||||
DatabaseConnectionLost,
|
||||
|
||||
#[error("Expected database key is missing")]
|
||||
DatabaseKeyMissing,
|
||||
|
||||
#[error("Database migrations failed {0}")]
|
||||
DatabaseMigrationFailure(String),
|
||||
|
||||
#[error("Unexpected response for message")]
|
||||
MessageMismatch,
|
||||
|
||||
#[error("Non-unique database key {0}")]
|
||||
NonUniqueDatabaseKey(String),
|
||||
}
|
||||
|
||||
impl result_extended::FatalError for FatalError {}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AppError {
|
||||
#[error("invalid request")]
|
||||
BadRequest,
|
||||
|
||||
#[error("could not create an object")]
|
||||
CouldNotCreateObject,
|
||||
|
||||
#[error("something wasn't found {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("object inaccessible {0}")]
|
||||
Inaccessible(String),
|
||||
|
||||
#[error("the requested operation is not allowed")]
|
||||
PermissionDenied,
|
||||
|
||||
#[error("the requested username/password combination was not found")]
|
||||
AuthFailed,
|
||||
|
||||
#[error("invalid json {0}")]
|
||||
JsonError(serde_json::Error),
|
||||
|
||||
#[error("wat {0}")]
|
||||
UnexpectedError(String),
|
||||
|
||||
#[error("this username is not available")]
|
||||
UsernameUnavailable,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[typeshare]
|
||||
pub struct Rgb {
|
||||
pub red: u32,
|
||||
pub green: u32,
|
||||
pub blue: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AccountState {
|
||||
Normal,
|
||||
PasswordReset(DateTime<Utc>),
|
||||
Locked,
|
||||
}
|
||||
|
||||
impl FromSql for AccountState {
|
||||
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
|
||||
if let ValueRef::Text(text) = value {
|
||||
let text = String::from_utf8(text.to_vec()).unwrap();
|
||||
if text.starts_with("Normal") {
|
||||
Ok(AccountState::Normal)
|
||||
} else if text.starts_with("PasswordReset") {
|
||||
let exp_str = text.strip_prefix("PasswordReset ").unwrap();
|
||||
let exp = NaiveDateTime::parse_from_str(exp_str, "%Y-%m-%d %H:%M:%S")
|
||||
.unwrap()
|
||||
.and_utc();
|
||||
Ok(AccountState::PasswordReset(exp))
|
||||
} else if text.starts_with("Locked") {
|
||||
Ok(AccountState::Locked)
|
||||
} else {
|
||||
Err(FromSqlError::InvalidType)
|
||||
}
|
||||
} else {
|
||||
Err(FromSqlError::InvalidType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for AccountState {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
match self {
|
||||
AccountState::Normal => Ok(ToSqlOutput::Borrowed(ValueRef::Text("Normal".as_bytes()))),
|
||||
AccountState::PasswordReset(expiration) => Ok(ToSqlOutput::Owned(Value::Text(
|
||||
format!("PasswordReset {}", expiration.format("%Y-%m-%d %H:%M:%S")),
|
||||
))),
|
||||
AccountState::Locked => Ok(ToSqlOutput::Borrowed(ValueRef::Text("Locked".as_bytes()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
pub admin: bool,
|
||||
pub state: AccountState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[typeshare]
|
||||
pub enum PlayerRole {
|
||||
Gm,
|
||||
Player,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[typeshare]
|
||||
pub struct Player {
|
||||
user_id: String,
|
||||
role: PlayerRole,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[typeshare]
|
||||
pub struct Game {
|
||||
pub id: GameId,
|
||||
pub type_: String,
|
||||
pub name: String,
|
||||
pub gm: UserId,
|
||||
pub players: Vec<UserId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[typeshare]
|
||||
pub struct Tabletop {
|
||||
pub background_color: Rgb,
|
||||
pub background_image: Option<AssetId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
#[typeshare]
|
||||
pub enum Message {
|
||||
UpdateTabletop(Tabletop),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UserOverview {
|
||||
pub id: UserId,
|
||||
pub name: String,
|
||||
pub is_admin: bool,
|
||||
pub state: AccountState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[typeshare]
|
||||
pub struct GameOverview {
|
||||
pub id: GameId,
|
||||
pub type_: String,
|
||||
pub name: String,
|
||||
pub gm: UserId,
|
||||
pub players: Vec<UserId>,
|
||||
}
|
||||
|
||||
/*
|
||||
impl From<GameRow> for GameOverview {
|
||||
fn from(row: GameRow) -> Self {
|
||||
Self {
|
||||
id: row.id,
|
||||
gm: row.gm,
|
||||
game_type: row.game_type,
|
||||
game_name: row.name,
|
||||
players: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
23
visions-prototype/ui/.gitignore
vendored
Normal file
23
visions-prototype/ui/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
46
visions-prototype/ui/README.md
Normal file
46
visions-prototype/ui/README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
14
visions-prototype/ui/Taskfile.yml
Normal file
14
visions-prototype/ui/Taskfile.yml
Normal file
@ -0,0 +1,14 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
dev:
|
||||
cmds:
|
||||
- cd ../visions-types && task build
|
||||
- npm install
|
||||
- npm run start
|
||||
|
||||
test:
|
||||
cmds:
|
||||
- cd ../visions-types && task build
|
||||
- npm install
|
||||
- npm run test
|
14754
visions-prototype/ui/package-lock.json
generated
Normal file
14754
visions-prototype/ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
visions-prototype/ui/package.json
Normal file
51
visions-prototype/ui/package.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.119",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-router": "^5.1.20",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"classnames": "^2.5.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router": "^6.28.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-use-websocket": "^4.11.1",
|
||||
"typescript": "^4.9.5",
|
||||
"visions-types": "../visions-types",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
BIN
visions-prototype/ui/public/favicon.ico
Normal file
BIN
visions-prototype/ui/public/favicon.ico
Normal file
Binary file not shown.
After Width: 64px | Height: 64px | Size: 3.8 KiB |
43
visions-prototype/ui/public/index.html
Normal file
43
visions-prototype/ui/public/index.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
BIN
visions-prototype/ui/public/logo192.png
Normal file
BIN
visions-prototype/ui/public/logo192.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 5.2 KiB |
BIN
visions-prototype/ui/public/logo512.png
Normal file
BIN
visions-prototype/ui/public/logo512.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 9.4 KiB |
25
visions-prototype/ui/public/manifest.json
Normal file
25
visions-prototype/ui/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
visions-prototype/ui/public/robots.txt
Normal file
3
visions-prototype/ui/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
37
visions-prototype/ui/src/App.css
Normal file
37
visions-prototype/ui/src/App.css
Normal file
@ -0,0 +1,37 @@
|
||||
.App {
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
72
visions-prototype/ui/src/App.tsx
Normal file
72
visions-prototype/ui/src/App.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { PropsWithChildren, useContext, useEffect, useState } from 'react'
|
||||
import './App.css'
|
||||
import { Client } from './client'
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
||||
import { DesignPage } from './views/Design/Design'
|
||||
import Candela from './plugins/Candela'
|
||||
import { Authentication } from './views/Authentication/Authentication'
|
||||
import { StateContext, StateProvider } from './providers/StateProvider/StateProvider'
|
||||
import { MainView } from './views'
|
||||
import { AdminView } from './views/Admin/Admin'
|
||||
|
||||
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"
|
||||
|
||||
interface AppProps {
|
||||
client: Client
|
||||
}
|
||||
|
||||
/*
|
||||
const CandelaCharsheet = ({ client }: { client: Client }) => {
|
||||
let [sheet, setSheet] = useState(undefined)
|
||||
useEffect(
|
||||
() => { client.charsheet(TEST_CHARSHEET_UUID).then((c) => setSheet(c)) },
|
||||
[client, setSheet]
|
||||
)
|
||||
|
||||
return sheet ? <Candela.CharsheetElement sheet={sheet} /> : <div> </div>
|
||||
}
|
||||
*/
|
||||
|
||||
const App = ({ client }: AppProps) => {
|
||||
console.log("rendering app")
|
||||
const [websocketUrl, setWebsocketUrl] = useState<string | undefined>(undefined)
|
||||
|
||||
// useEffect(() => {
|
||||
// client.registerWebsocket().then((url) => setWebsocketUrl(url))
|
||||
// }, [client])
|
||||
|
||||
let router =
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: (
|
||||
<StateProvider client={client}>
|
||||
<AuthedView client={client}>
|
||||
<MainView client={client} />
|
||||
</AuthedView>
|
||||
</StateProvider>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
element: <AdminView client={client} />
|
||||
},
|
||||
/*
|
||||
{
|
||||
path: "/candela",
|
||||
element: <CandelaCharsheet client={client} />
|
||||
},
|
||||
*/
|
||||
{
|
||||
path: "/design",
|
||||
element: <DesignPage />
|
||||
}
|
||||
])
|
||||
return (
|
||||
<div className="App">
|
||||
<RouterProvider router={router} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
149
visions-prototype/ui/src/client.ts
Normal file
149
visions-prototype/ui/src/client.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { AuthResponse, SessionId, UserId, UserOverview } from "visions-types";
|
||||
|
||||
export type PlayingField = {
|
||||
backgroundImage: string;
|
||||
}
|
||||
|
||||
export type ServerResponse<A> = { type: "Unauthorized" } | { type: "Unexpected", status: number } | A;
|
||||
|
||||
export interface Client {
|
||||
users: (sessionId: SessionId) => Promise<Array<UserOverview>>;
|
||||
createUser: (sessionId: SessionId, username: string) => Promise<UserId>;
|
||||
auth: (username: string, password: string) => Promise<ServerResponse<AuthResponse>>;
|
||||
setPassword: (sessionId: SessionId, password_1: string, password_2: string) => Promise<void>;
|
||||
logout: (sessionId: SessionId) => Promise<void>;
|
||||
}
|
||||
|
||||
export class Connection implements Client {
|
||||
private base: URL;
|
||||
private sessionId: string | undefined;
|
||||
|
||||
constructor() {
|
||||
this.base = new URL("http://localhost:8001");
|
||||
}
|
||||
|
||||
registerWebsocket() {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `api/v1/client`;
|
||||
return fetch(url, { method: 'POST' }).then((response) => response.json()).then((ws) => ws.url);
|
||||
}
|
||||
|
||||
/*
|
||||
unregisterWebsocket() {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `api/v1/client`;
|
||||
return fetch(url, { method: 'POST' }).then((response => response.json()));
|
||||
}
|
||||
*/
|
||||
|
||||
imageUrl(imageId: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/image/${imageId}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
async playingField(): Promise<PlayingField> {
|
||||
return { backgroundImage: "trans-ferris.jpg" };
|
||||
}
|
||||
|
||||
async availableImages(): Promise<string[]> {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/image`;
|
||||
return fetch(url).then((response) => response.json());
|
||||
}
|
||||
|
||||
async setBackgroundImage(name: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/tabletop/bg_image`;
|
||||
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) });
|
||||
}
|
||||
|
||||
async users(sessionId: string): Promise<Array<UserOverview>> {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = '/api/v1/users';
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: [['Authorization', `Bearer ${sessionId}`]]
|
||||
}).then((response) => response.json());
|
||||
}
|
||||
|
||||
async charsheet(id: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/charsheet/${id}`;
|
||||
return fetch(url).then((response) => response.json());
|
||||
}
|
||||
|
||||
async createUser(sessionId: string, username: string): Promise<UserId> {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = '/api/v1/user';
|
||||
const response: Response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: [['Authorization', `Bearer ${sessionId}`],
|
||||
['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({ username }),
|
||||
});
|
||||
const userId: UserId = await response.json();
|
||||
return userId;
|
||||
}
|
||||
|
||||
async setPassword(sessionId: string, password_1: string, password_2: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/user/password`;
|
||||
|
||||
await fetch(url, {
|
||||
method: 'PUT', headers: [['Authorization', `Bearer ${sessionId}`], ['Content-Type', 'application/json']], body: JSON.stringify({
|
||||
password_1, password_2,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async auth(username: string, password: string): Promise<ServerResponse<AuthResponse>> {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/auth`
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: [['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({ 'username': username, 'password': password })
|
||||
});
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else if (response.status == 401) {
|
||||
return await response.json().then(() => ({ type: "Unauthorized" }));
|
||||
} else {
|
||||
return await response.json().then(() => ({ type: "Unexpected", status: response.status }));
|
||||
}
|
||||
}
|
||||
|
||||
async logout(sessionId: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/auth`
|
||||
await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: [['Authorization', `Bearer ${sessionId}`]],
|
||||
});
|
||||
}
|
||||
|
||||
async profile(sessionId: SessionId, userId: UserId | undefined): Promise<UserOverview | undefined> {
|
||||
const url = new URL(this.base);
|
||||
if (userId) {
|
||||
url.pathname = `/api/v1/user${userId}`
|
||||
} else {
|
||||
url.pathname = `/api/v1/user`
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: [['Authorization', `Bearer ${sessionId}`]],
|
||||
});
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
async health() {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/health`;
|
||||
return fetch(url).then((response) => response.json()).then((response) => {
|
||||
console.log("health response: ", response);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
8
visions-prototype/ui/src/components/Card/Card.css
Normal file
8
visions-prototype/ui/src/components/Card/Card.css
Normal file
@ -0,0 +1,8 @@
|
||||
.card {
|
||||
border: var(--border-standard);
|
||||
border-radius: var(--border-radius-standard);
|
||||
box-shadow: var(--border-shadow-shallow);
|
||||
padding: var(--padding-m);
|
||||
}
|
||||
|
||||
|
16
visions-prototype/ui/src/components/Card/Card.tsx
Normal file
16
visions-prototype/ui/src/components/Card/Card.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import './Card.css';
|
||||
|
||||
interface CardElementProps {
|
||||
name?: string,
|
||||
}
|
||||
|
||||
export const CardElement = ({ name, children }: PropsWithChildren<CardElementProps>) => (
|
||||
<div className="card">
|
||||
{name && <h1 className="card__title">{name}</h1> }
|
||||
<div className="card__body">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { GameOverview } from "visions-types"
|
||||
import { CardElement } from '../Card/Card';
|
||||
|
||||
|
||||
export const GameOverviewElement = ({ name, gm, players }: GameOverview) => {
|
||||
return (<CardElement name={name}>
|
||||
<p><i>GM</i> {gm}</p>
|
||||
<ul>
|
||||
{players.map((player) => player)}
|
||||
</ul>
|
||||
</CardElement>)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user