Compare commits
2 Commits
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 0e9a35ec4e | |
Savanni D'Gerinel | f463d67b9d |
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
|
@ -1,20 +1,18 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
# "authdb",
|
||||
# "bike-lights/bike",
|
||||
"authdb",
|
||||
"bike-lights/bike",
|
||||
"bike-lights/core",
|
||||
"bike-lights/simulator",
|
||||
"changeset",
|
||||
"config",
|
||||
"config-derive",
|
||||
"coordinates",
|
||||
"cyberpunk",
|
||||
"cyber-slides",
|
||||
"cyberpunk-splash",
|
||||
"dashboard",
|
||||
"emseries",
|
||||
# "file-service",
|
||||
"file-service",
|
||||
"fitnesstrax/core",
|
||||
"fitnesstrax/app",
|
||||
"fluent-ergonomics",
|
||||
|
@ -32,6 +30,5 @@ members = [
|
|||
"sgf",
|
||||
"timezone-testing",
|
||||
"tree",
|
||||
"visions/server",
|
||||
"gm-dash/server"
|
||||
"visions/server", "gm-dash/server",
|
||||
]
|
||||
|
|
|
@ -18,7 +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.8", features = [ "runtime-tokio", "sqlite" ] }
|
||||
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite" ] }
|
||||
thiserror = { version = "1" }
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
||||
|
|
|
@ -119,7 +119,6 @@ impl<
|
|||
> UI for BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
|
||||
{
|
||||
fn check_event(&mut self, current_time: Instant) -> Option<Event> {
|
||||
/*
|
||||
if self.brake_sensor.is_high().unwrap_or(true) && !self.brake_enabled {
|
||||
self.brake_enabled = true;
|
||||
Some(Event::Brake)
|
||||
|
@ -127,8 +126,6 @@ impl<
|
|||
self.brake_enabled = false;
|
||||
Some(Event::BrakeRelease)
|
||||
} else if self.left_blinker_button.is_low(current_time) {
|
||||
*/
|
||||
if self.left_blinker_button.is_low(current_time) {
|
||||
self.left_blinker_button.set_debounce(current_time);
|
||||
Some(Event::LeftBlinker)
|
||||
} else if self.right_blinker_button.is_low(current_time) {
|
||||
|
|
|
@ -1,174 +0,0 @@
|
|||
width = 65;
|
||||
length = 75;
|
||||
height = 16;
|
||||
wall_thickness = 2;
|
||||
guide_thickness = 1;
|
||||
power_width = 21;
|
||||
output_width = 37.5;
|
||||
half_wall_thickness = wall_thickness / 2;
|
||||
standoff_thickness = 10;
|
||||
hole_diameter = 3;
|
||||
// The radius of a nut in mm. However, based on my measurements, I'm not actually sure I have this right. The short height of a nut is 7.86mm. Derive from there.
|
||||
nut_radius = 8.5 * cos(30) / 2;
|
||||
nut_height = 2.69; // mm
|
||||
screw_radius = 2;
|
||||
handlebar_radius = 15;
|
||||
clasp_thickness = 4;
|
||||
clasp_width = 35;
|
||||
circular_face_count = 48;
|
||||
|
||||
module hexagon(r, h) {
|
||||
pi = 3.1415926;
|
||||
polyhedron(
|
||||
points=[
|
||||
[r, 0, 0],
|
||||
[r * cos(60), r * sin(60), 0],
|
||||
[r * cos(120), r * sin(120), 0],
|
||||
[r * cos(180), r * sin(180), 0],
|
||||
[r * cos(240), r * sin(240), 0],
|
||||
[r * cos(300), r * sin(300), 0],
|
||||
|
||||
[r, 0, h],
|
||||
[r * cos(60), r * sin(60), h],
|
||||
[r * cos(120), r * sin(120), h],
|
||||
[r * cos(180), r * sin(180), h],
|
||||
[r * cos(240), r * sin(240), h],
|
||||
[r * cos(300), r * sin(300), h],
|
||||
],
|
||||
faces=[
|
||||
[0, 1, 2, 3, 4, 5],
|
||||
[11, 10, 9, 8, 7, 6],
|
||||
[6, 7, 1, 0],
|
||||
[7, 8, 2, 1],
|
||||
[8, 9, 3, 2],
|
||||
[9, 10, 4, 3],
|
||||
[10, 11, 5, 4],
|
||||
[11, 6, 0, 5],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Nut holders are blocks that have a hole drilled through them and a hexagonal-shaped cavity. The idea is to
|
||||
module nut_holder() {
|
||||
difference() {
|
||||
translate([-4.5, -4.5, -2]) cube([9, 9, 4]);
|
||||
union() {
|
||||
translate([0, 0, -1]) hexagon(nut_radius, 2);
|
||||
cylinder(h = 6, r = screw_radius, center = true, $fn = 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module screw_hole() {
|
||||
union() {
|
||||
translate([0, 0, 4]) cylinder(h = 2.1, r = screw_radius * 2, center = true, $fn = 24);
|
||||
cylinder(h = 6, r = screw_radius, center = true, $fn = 24);
|
||||
}
|
||||
}
|
||||
|
||||
module base() {
|
||||
cube([width, length, wall_thickness]);
|
||||
}
|
||||
|
||||
module face() {
|
||||
union() {
|
||||
cube([width, length, wall_thickness / 2]);
|
||||
translate([wall_thickness, wall_thickness, wall_thickness / 2]) cube([width-wall_thickness*2, length-wall_thickness*2, wall_thickness / 2]);
|
||||
translate([4.5 + wall_thickness, 4.5 + wall_thickness, 4]) nut_holder();
|
||||
translate([width - 4.5 - wall_thickness, 4.5 + wall_thickness, 4]) nut_holder();
|
||||
translate([width - 4.5 - wall_thickness, length - 4.5 - wall_thickness, 4]) nut_holder();
|
||||
translate([4.5 + wall_thickness, length - 4.5 - wall_thickness, 4]) nut_holder();
|
||||
}
|
||||
}
|
||||
|
||||
module wall(length) {
|
||||
cube([length, height, wall_thickness]);
|
||||
}
|
||||
|
||||
module power_wall() {
|
||||
difference() {
|
||||
wall(65);
|
||||
translate([9, 2, -.5]) cube([power_width, height, wall_thickness + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
module output_wall() {
|
||||
difference() {
|
||||
wall(65);
|
||||
translate([9, 2, -.5]) cube([output_width, height, wall_thickness + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Use hexagons as cutouts into which I can install a hex nut. This isn't quite right yet, but close.
|
||||
// hexagon(nut_radius, 1);
|
||||
|
||||
// cube([standoff_thickness, standoff_thickness, 2]);
|
||||
|
||||
/*
|
||||
difference() {
|
||||
union() {
|
||||
base();
|
||||
rotate([90, 0, 90]) wall(75);
|
||||
// translate([width - wall_thickness, 0, 0]) rotate([90, 0, 90]) wall(length);
|
||||
// rotate([90, 0, 0]) power_wall();
|
||||
// translate([0, length, 0]) rotate([90, 0, 0]) output_wall();
|
||||
// translate([wall_thickness,
|
||||
// wall_thickness,
|
||||
// wall_thickness]) standoff();
|
||||
// translate([width - wall_thickness - standoff_thickness,
|
||||
// wall_thickness,
|
||||
// wall_thickness]) standoff();
|
||||
// translate([wall_thickness,
|
||||
// length - wall_thickness - standoff_thickness,
|
||||
// wall_thickness]) standoff();
|
||||
// translate([width - wall_thickness - standoff_thickness,
|
||||
// length - wall_thickness - standoff_thickness,
|
||||
// wall_thickness]) standoff();
|
||||
}
|
||||
// translate([-half_wall_thickness, -wall_thickness - half_wall_thickness, height - half_wall_thickness]) cube([wall_thickness, length + wall_thickness * 2, wall_thickness]);
|
||||
// translate([width - half_wall_thickness, -wall_thickness - half_wall_thickness, height - half_wall_thickness]) cube([wall_thickness, length + wall_thickness * 2, wall_thickness]);
|
||||
// translate([-half_wall_thickness, -half_wall_thickness, height - half_wall_thickness]) rotate([0, 0, 270]) cube([wall_thickness, width + wall_thickness * 2, wall_thickness]);
|
||||
// translate([-half_wall_thickness, length + half_wall_thickness, height - half_wall_thickness]) rotate([0, 0, 270]) cube([wall_thickness, width + wall_thickness * 2, wall_thickness]);
|
||||
}
|
||||
*/
|
||||
|
||||
module box() {
|
||||
difference() {
|
||||
union() {
|
||||
cube([width, length, wall_thickness * 2]);
|
||||
translate([0, 0, wall_thickness]) rotate([90, 0, 90]) wall(length);
|
||||
translate([width - wall_thickness, 0, wall_thickness]) rotate([90, 0, 90]) wall(length);
|
||||
translate([0, wall_thickness, wall_thickness]) rotate([90, 0, 0]) wall(width);
|
||||
translate([0, length, wall_thickness]) rotate([90, 0, 0]) wall(width);
|
||||
}
|
||||
translate([4.5 + wall_thickness, 4.5 + wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||
translate([width - 4.5 - wall_thickness, 4.5 + wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||
translate([width - 4.5 - wall_thickness, length - 4.5 - wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||
translate([4.5 + wall_thickness, length - 4.5 - wall_thickness, 4]) rotate([180, 0, 0]) screw_hole();
|
||||
}
|
||||
}
|
||||
|
||||
module top_clasp() {
|
||||
difference() {
|
||||
union() {
|
||||
cylinder(h = clasp_width, r = handlebar_radius + clasp_thickness, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, -clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, -clasp_width / 2 + 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, clasp_width / 2 - 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([-handlebar_radius-5, -10, -clasp_width / 2 + 6]) cube([6, 20, clasp_width - 12]);
|
||||
}
|
||||
translate([-0.5, 0, 0]) cylinder(h = clasp_width+2, r = handlebar_radius + 1, center = true, $fn = circular_face_count);
|
||||
translate([-0.5, -handlebar_radius - 10, -clasp_width / 2 - 1]) cube([handlebar_radius + 10, handlebar_radius * 2 + 20, clasp_width + 2]);
|
||||
}
|
||||
}
|
||||
|
||||
module body() {
|
||||
union() {
|
||||
box();
|
||||
translate([width / 2, length / 2, -5 - handlebar_radius]) rotate([0, 90, 90]) top_clasp();
|
||||
}
|
||||
}
|
||||
|
||||
body();
|
||||
translate([width + 10, 0, 0]) face();
|
|
@ -1,21 +0,0 @@
|
|||
handlebar_radius = 15;
|
||||
clasp_thickness = 4;
|
||||
circular_face_count = 48;
|
||||
clasp_width = 35;
|
||||
|
||||
module top_clasp() {
|
||||
difference() {
|
||||
union() {
|
||||
cylinder(h = clasp_width, r = handlebar_radius + clasp_thickness, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, -clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, -clasp_width / 2 + 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, clasp_width / 2 - 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([-handlebar_radius-5, -10, -clasp_width / 2 + 6]) cube([6, 20, clasp_width - 12]);
|
||||
}
|
||||
translate([-0.5, 0, 0]) cylinder(h = clasp_width+2, r = handlebar_radius + 1, center = true, $fn = circular_face_count);
|
||||
translate([-0.5, -handlebar_radius - 10, -clasp_width / 2 - 1]) cube([handlebar_radius + 10, handlebar_radius * 2 + 20, clasp_width + 2]);
|
||||
}
|
||||
}
|
||||
|
||||
top_clasp();
|
|
@ -1,8 +1,3 @@
|
|||
|
||||
module hexagon(r, h) {
|
||||
cylinder(r = r, h = h, center = 2, $fn = 6);
|
||||
}
|
||||
|
||||
module pill(length, bevel) {
|
||||
hull() {
|
||||
translate([0, 0, (-length / 2) + bevel]) sphere(r = bevel);
|
||||
|
@ -10,27 +5,6 @@ module pill(length, bevel) {
|
|||
}
|
||||
}
|
||||
|
||||
module rounded_cube(dimensions, bevel = 0) {
|
||||
x = dimensions[0];
|
||||
y = dimensions[1];
|
||||
z = dimensions[2];
|
||||
|
||||
if (bevel > 0) {
|
||||
hull() {
|
||||
translate([-x / 2 + bevel, -y / 2 + bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||
translate([ x / 2 - bevel, -y / 2 + bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||
translate([ x / 2 - bevel, y / 2 - bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||
translate([-x / 2 + bevel, y / 2 - bevel, -z / 2 + bevel]) sphere(r = bevel);
|
||||
translate([-x / 2 + bevel, -y / 2 + bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||
translate([ x / 2 - bevel, -y / 2 + bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||
translate([ x / 2 - bevel, y / 2 - bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||
translate([-x / 2 + bevel, y / 2 - bevel, z / 2 - bevel]) sphere(r = bevel);
|
||||
}
|
||||
} else {
|
||||
cube(dimensions, center = true);
|
||||
}
|
||||
}
|
||||
|
||||
module box_face(dimensions, bevel = 0) {
|
||||
x = dimensions[0];
|
||||
y = dimensions[1];
|
||||
|
@ -74,7 +48,6 @@ module box(length, width, height, bevel = 0) {
|
|||
rotate([90, 0, 0])
|
||||
rotate([0, 90, 0])
|
||||
box_face([width, height, wall_thickness], bevel);
|
||||
|
||||
translate([length - wall_thickness + bevel, 0, bevel])
|
||||
rotate([90, 0, 0])
|
||||
rotate([0, 90, 0])
|
||||
|
@ -82,11 +55,3 @@ module box(length, width, height, bevel = 0) {
|
|||
}
|
||||
}
|
||||
|
||||
module box_side_slider(length, width, height) {
|
||||
difference() {
|
||||
box_face([width - wall_thickness * 2 + 4, height, wall_thickness], bevel);
|
||||
translate([-1, -1, 1]) cube([4-threshold, height+2, 4-threshold]);
|
||||
color("red") translate([width - wall_thickness * 2 + 1, -1, 1]) cube([4-threshold, height+2, 4-threshold]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ threshold = 0.1;
|
|||
board_length = 92;
|
||||
board_width = 72;
|
||||
board_height = 21.5;
|
||||
wall_thickness = 4;
|
||||
wall_thickness = 2;
|
||||
bevel = 0.5;
|
||||
|
||||
hinge_radius = 2.5;
|
||||
|
@ -13,63 +13,16 @@ case_width = board_width + wall_thickness * 2;
|
|||
case_length = board_length + wall_thickness * 2;
|
||||
case_height = board_height + wall_thickness;
|
||||
|
||||
handlebar_radius = 15;
|
||||
clasp_thickness = 4;
|
||||
circular_face_count = 48;
|
||||
clasp_width = 35;
|
||||
|
||||
include <./common.scad>;
|
||||
|
||||
module top_clasp() {
|
||||
difference() {
|
||||
union() {
|
||||
cylinder(h = clasp_width, r = handlebar_radius + clasp_thickness, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, -clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, -clasp_width / 2 + 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, clasp_width / 2]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([0, 0, clasp_width / 2 - 4]) cylinder(h = 1, r = handlebar_radius + clasp_thickness + 1, center = true, $fn = circular_face_count);
|
||||
translate([-handlebar_radius-5, -10, -clasp_width / 2 + 6]) cube([6, 20, clasp_width - 12]);
|
||||
}
|
||||
translate([-0.5, 0, 0]) cylinder(h = clasp_width+2, r = handlebar_radius + 1, center = true, $fn = circular_face_count);
|
||||
translate([-0.5, -handlebar_radius - 10, -clasp_width / 2 - 1]) cube([handlebar_radius + 10, handlebar_radius * 2 + 20, clasp_width + 2]);
|
||||
}
|
||||
}
|
||||
|
||||
module hinge(length) {
|
||||
difference() {
|
||||
union() {
|
||||
cube([hinge_radius * 2, length, hinge_radius], center = true);
|
||||
translate([0, 0, -1.5]) rotate([90, 0, 0]) cylinder(h = length, r = hinge_radius, center = true);
|
||||
cylinder(h = length, r = hinge_radius);
|
||||
translate([0, -hinge_radius, 0])
|
||||
cube([hinge_radius, hinge_radius * 2, length]);
|
||||
}
|
||||
translate([0, threshold / 2, -1.5]) rotate([90, 0, 0]) cylinder(h = length + threshold * 2, r = 1, center = true);
|
||||
}
|
||||
}
|
||||
|
||||
module base_case(length, width, height, bevel = 0) {
|
||||
difference() {
|
||||
union() {
|
||||
channel(length + wall_thickness / 2, width, height, bevel);
|
||||
|
||||
translate([-bevel, 0, bevel])
|
||||
rotate([90, 0, 0])
|
||||
rotate([0, 90, 0])
|
||||
box_face([width, height, wall_thickness], bevel);
|
||||
|
||||
// These are the sleds at the bottom of the case that should hold the lower of the two boards down
|
||||
color("blue") translate([0, wall_thickness - 2, wall_thickness + 4]) cube([length - 8, 4, wall_thickness / 2]);
|
||||
color("blue") translate([wall_thickness - 2, wall_thickness - 4, wall_thickness + 4]) cube([4, width, wall_thickness / 2]);
|
||||
color("blue") translate([length - 25, width - wall_thickness * 3 / 2, wall_thickness + 6]) cube([16, wall_thickness, wall_thickness / 2]);
|
||||
}
|
||||
|
||||
// This makes an indent at the bottom to accomodate solder joins
|
||||
translate([wall_thickness + 2, wall_thickness + 2, wall_thickness / 2]) cube([length, width - wall_thickness * 2 - 4, wall_thickness / 2 + threshold]);
|
||||
|
||||
// This creates a cutout that lets the power plug slide in better.
|
||||
translate([wall_thickness, width - wall_thickness, wall_thickness]) cube([length, 2, 6]);
|
||||
|
||||
// These two put in the slots that should allow the fourth wall to be slotted into place.
|
||||
color("red") translate([length - 1, wall_thickness - 2, 4]) cube([2, 2, height]);
|
||||
color("red") translate([length - 1, width - wall_thickness, 4]) cube([2, 2, height]);
|
||||
translate([0, 0, -threshold / 2]) cylinder(h = length + threshold, r = 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,16 +33,11 @@ module main_case() {
|
|||
|
||||
difference() {
|
||||
union() {
|
||||
base_case(case_length,
|
||||
box(case_length,
|
||||
case_width,
|
||||
case_height,
|
||||
bevel);
|
||||
|
||||
translate([-bevel, 0, bevel])
|
||||
rotate([90, 0, 0])
|
||||
rotate([0, 90, 0])
|
||||
box_face([case_width, case_height, wall_thickness], bevel);
|
||||
|
||||
translate([0, -hinge_radius - bevel + threshold, hinge_z_offset + bevel])
|
||||
rotate([90, 0, 0])
|
||||
rotate([0, 90, 0])
|
||||
|
@ -117,94 +65,31 @@ module main_case() {
|
|||
rotate([0, 180, 0])
|
||||
linear_extrude(1)
|
||||
text("right", size = 3);
|
||||
// translate([case_length / 2, case_width / 2, -20]) rotate([0, 90, 0]) top_clasp();
|
||||
}
|
||||
|
||||
translate([case_length / 2, case_width / 2, -threshold]) hexagon(4.5, 6);
|
||||
|
||||
# translate([8.5 + wall_thickness, case_width - wall_thickness - threshold, wall_thickness])
|
||||
# cube([60, wall_thickness * 2, 7]);
|
||||
}
|
||||
}
|
||||
|
||||
module lamp() {
|
||||
union() {
|
||||
translate([0, 0, -0.5]) cube([12.9 + threshold, 8, 4], center = true);
|
||||
translate([0, 0, .88]) cube([5 + threshold, 5 + threshold, 1.56], center = true);
|
||||
/*
|
||||
translate([0, 0, -1.56]) cube([12.9, 7.6, wall_thickness], center = true);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
module button() {
|
||||
union() {
|
||||
cube([3.5 + threshold, 6.1 + threshold, 4 + threshold], center = true);
|
||||
translate([0, 0, -0.5]) cube([1.2, 7, 3 + threshold], center = true);
|
||||
color("green", 1) translate([8.5 + wall_thickness, case_width - wall_thickness - threshold, wall_thickness])
|
||||
cube([60, wall_thickness * 2, 7]);
|
||||
}
|
||||
}
|
||||
|
||||
module lid() {
|
||||
lid_width = case_width + hinge_radius * 2 + wall_thickness;
|
||||
hinge_length = case_length / 4;
|
||||
union() {
|
||||
difference() {
|
||||
rounded_cube([case_length,
|
||||
box_face([case_length,
|
||||
lid_width,
|
||||
wall_thickness],
|
||||
bevel);
|
||||
translate([0, lid_width / 5, 0.4]) lamp();
|
||||
translate([-15, lid_width / 5, 0.4]) lamp();
|
||||
translate([15, lid_width / 5, 0.4]) lamp();
|
||||
translate([-30, lid_width / 5, 0]) button();
|
||||
translate([30, lid_width / 5, 0]) button();
|
||||
|
||||
translate([0, lid_width / 5, -2]) cube([20, 7, 3], center = true);
|
||||
|
||||
color("black") translate([-2, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||
color("black") translate([-17, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||
color("black") translate([13, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||
color("black") translate([-30, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||
color("black") translate([30, lid_width / 5 - 5, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h=5, r = 1, center = true, $fn = circular_face_count);
|
||||
color("black") translate([0, 10, -2]) rotate([0, 90, 0]) cylinder(h = 62, r = 1, center = true, $fn = circular_face_count);
|
||||
|
||||
color("red") translate([-33, 21, -2]) rotate([0, 90, 0]) cylinder(h = 5, r = 1, center = true, $fn = circular_face_count);
|
||||
color("red") translate([-35, 13, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h = 18, r = 1, center = true, $fn = circular_face_count);
|
||||
color("red") translate([33, 21, -2]) rotate([0, 90, 0]) cylinder(h = 5, r = 1, center = true, $fn = circular_face_count);
|
||||
color("red") translate([35, 13, -2]) rotate([0, 0, 90]) rotate([0, 90, 0]) cylinder(h = 18, r = 1, center = true, $fn = circular_face_count);
|
||||
color("red") translate([0, 5, -2]) rotate([0, 90, 0]) cylinder(h = 70, r = 1, center = true, $fn = circular_face_count);
|
||||
translate([(case_length - 60) / 2, 14 + hinge_radius * 2, -threshold / 2])
|
||||
cube([60, 16, wall_thickness + threshold]);
|
||||
}
|
||||
|
||||
translate([case_length / 2 - hinge_length / 2, lid_width / 2 - wall_thickness / 2 - 0.5, -wall_thickness / 2]) rotate([0, 0, 90]) hinge(hinge_length);
|
||||
translate([-case_length / 2 + hinge_length / 2, lid_width / 2 - wall_thickness / 2 - 0.5, -wall_thickness / 2]) rotate([0, 0, 90]) hinge(hinge_length);
|
||||
|
||||
translate([0, -lid_width / 2 + bevel, -3]) rounded_cube([20, wall_thickness / 2, 10], bevel);
|
||||
color("blue") translate([-9, -lid_width / 2 + 1.5, -6]) rotate([90, 0, 0]) rotate([0, 90, 0]) linear_extrude(18) circle(1, $fn = 3);
|
||||
color("blue") translate([-9, -lid_width / 2 + 1.5, -7]) rotate([90, 0, 0]) rotate([0, 90, 0]) linear_extrude(18) circle(1, $fn = 3);
|
||||
}
|
||||
}
|
||||
|
||||
module box_side() {
|
||||
box_side_slider(case_length, case_width, case_height);
|
||||
}
|
||||
|
||||
module case_base() {
|
||||
difference() {
|
||||
rounded_cube([case_length, case_width, wall_thickness + 2], bevel = 0.5);
|
||||
translate([wall_thickness, 0, 2]) rounded_cube([case_length + threshold, board_width + threshold, 2 + threshold]);
|
||||
|
||||
// These give a screw-hole in the center which will allow the clamp to be attached
|
||||
translate([0, 0, -1]) hexagon(4.5, 2);
|
||||
translate([0, 0, -wall_thickness / 2]) cylinder(r = 2, h = wall_thickness + threshold, center = true);
|
||||
|
||||
// and now a bit of an indentation to help the clip remain in place
|
||||
translate([0, 0, -4.5]) cube([clasp_width + threshold, clasp_width + threshold, wall_thickness], center = true);
|
||||
|
||||
// here are some grooves along the edges that can be used to piece parts together
|
||||
translate([wall_thickness / 2, case_width / 2 - wall_thickness / 2, wall_thickness / 2])
|
||||
cube([board_length + wall_thickness, wall_thickness / 2, wall_thickness / 2 + threshold], center = true);
|
||||
translate([wall_thickness / 2, -case_width / 2 + wall_thickness / 2, wall_thickness / 2])
|
||||
cube([board_length + wall_thickness, wall_thickness / 2, wall_thickness / 2 + threshold], center = true);
|
||||
translate([case_length / 4 + 1, hinge_radius - 0.4, -hinge_radius])
|
||||
rotate([180, 0, 0])
|
||||
rotate([0, 90, 0]) hinge(case_length / 2 - 2);
|
||||
}
|
||||
}
|
||||
|
||||
// main_case();
|
||||
// color("red", 1) translate([0, 0, case_height + wall_thickness / 2]) lid();
|
||||
// color("red", 1) translate([0, 0, 40]) lid();
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
|
||||
include <./control_panel.scad>
|
||||
|
||||
/*
|
||||
difference() {
|
||||
color("blue") rounded_cube([5, 5, 5], bevel = 0.5);
|
||||
translate([0, 0, 1]) rounded_cube([4, 4, 4]);
|
||||
};
|
||||
*/
|
||||
|
||||
case_base();
|
||||
main_case();
|
||||
|
|
|
@ -2,5 +2,4 @@
|
|||
include <./control_panel.scad>
|
||||
|
||||
lid();
|
||||
// lamp();
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
|
||||
include <./control_panel.scad>
|
||||
|
||||
box_side();
|
|
@ -336,7 +336,7 @@ impl Pattern {
|
|||
|
||||
fn body(&self) -> BodyPattern {
|
||||
match self {
|
||||
Pattern::Water => WATER_BODY,
|
||||
Pattern::Water => OFF_BODY,
|
||||
Pattern::GayPride => PRIDE_BODY,
|
||||
Pattern::TransPride => TRANS_PRIDE_BODY,
|
||||
}
|
||||
|
|
|
@ -98,74 +98,7 @@ pub const DEFAULT_FRAMES: U16F0 = U16F0::lit("30");
|
|||
|
||||
pub const WATER_DASHBOARD: DashboardPattern = [WATER_1, WATER_2, WATER_3];
|
||||
|
||||
pub const WATER_BODY: BodyPattern = [
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
|
||||
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
WATER_3,
|
||||
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
WATER_2,
|
||||
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
WATER_1,
|
||||
];
|
||||
pub const WATER_BODY: BodyPattern = [RGB_OFF; 60];
|
||||
|
||||
pub const PRIDE_DASHBOARD: DashboardPattern = [PRIDE_RED, PRIDE_GREEN, PRIDE_INDIGO];
|
||||
|
||||
|
|
|
@ -1,149 +1,168 @@
|
|||
{
|
||||
"registry+https://github.com/rust-lang/crates.io-index#addr2line@0.24.2": "1hd1i57zxgz08j6h5qrhsnm2fi0bcqvsh389fw400xm3arz2ggnz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#adler2@2.0.0": "09r6drylvgy8vv8k20lnbvwq8gp09h7smfn6h1rxsy15pgh629si",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#addr2line@0.21.0": "1jx0k3iwyqr8klqbzk6kjvr496yd94aspis10vwsj5wy7gib4c4a",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#adler32@1.2.0": "0d7jq7jsjyhsgbhnfq5fvrlh9j0i9g1fqrl2735ibv5f75yjgqda",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#adler@1.0.2": "1zim79cvzd5yrkzl3nyfx0avijwgk9fqv3yrscdy1cc79ih02qpj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.11": "04chdfkls5xmhp1d48gnjsmglbqibizs3bpbj6rsj604m10si7g8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.3": "05mrpkvdgp5d20y2p989f187ry9diliijgwrs254fs9s1m1x6q4f",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#allocator-api2@0.2.18": "0kr6lfnxvnj164j1x38g97qjlhb7akppqzvgfs0697140ixbav2w",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.6": "0yn9i8nc6mmv28ig9w3dga571q09vg9f1f650mi5z8phx42r6hli",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.2": "1w510wnixvlgimkx1zjbvlxh6xps2vjgfqgwf5a6adlbjp5rv5mj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#allocator-api2@0.2.16": "1iayppgq4wqbfbfcqmsbwgamj0s65012sskfvyx07pxavk3gyhh9",
|
||||
"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.5": "1dm1mdbs1x6y3m3pz0qlamgiskb50i4q859676kx0pz8r8pajr6n",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.3": "134jhzrz89labrdwxxnjxqjdg06qvaflj1wkfnmyapwyldfwcnn7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.0.2": "0j3na4b1nma39g4x7cwvj009awxckjf3z2vkwhldgka44hqj72g2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#anstyle-wincon@3.0.2": "19v0fv400bmp4niqpzxnhg83vz12mmqv7l2l8vi80qcdxj0lpm8w",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.4": "11yxw02b6parn29s757z96rgiqbn8qy0fk9a3p3bhczm85dhfybh",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.75": "1rmcjkim91c5mw7h9wn8nv0k6x118yz0xg0z1q18svgn42mqqrm4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.4": "04b7n722jij0v3fnm3qk072d5ysc2q30rl9fz33zpfhzah30mlwn",
|
||||
"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-channel@2.1.1": "1337ywc1paw03rdlwh100kh8pa0zyp0nrlya8bpsn6zdqi5kz8qw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#async-executor@1.8.0": "0z7rpayidhdqs4sdzjhh26z5155c1n94fycqni9793n4zjz5xbhp",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#async-global-executor@2.4.1": "1762s45cc134d38rrv0hyp41hv4iv6nmx59vswid2p0il8rvdc85",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#async-io@2.3.4": "1s679l7x6ijh8zcxqn5pqgdiyshpy4xwklv86ldm1rhfjll04js4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#async-lock@3.4.0": "060vh45i809wcqyxzs5g69nqiqah7ydz0hpkcjys9258vqn4fvpz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#async-io@2.3.1": "0rggn074kbqxxajci1aq14b17gp75rw9l6rpbazcv9q0bc6ap5wg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#async-lock@3.3.0": "0yxflkfw46rad4lv86f59b5z555dlfmg1riz1n8830rgi0qb8d6h",
|
||||
"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-task@4.7.0": "16975vx6aqy5yf16fs9xz5vx1zq8mwkzfmykvcilc1j7b6c6xczv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.77": "1adf1jh2yg39rkpmqjqyr9xyd6849p0d95425i6imgbhx0syx069",
|
||||
"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#atomic-write-file@0.1.2": "0dl4x0srdwjxm3zz3fj1c7m44i3b7mjiad550fqklj1n4bfbxkgd",
|
||||
"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#autocfg@1.1.0": "1ylp3cb47ylzabimazvbz9ms6ap784zhb6syaz6c1jqpmcmq0s6l",
|
||||
"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#backtrace@0.3.69": "0dsq23dhw4pfndkx2nsa1ml2g31idm7ss7ljxp8d57avygivg290",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#bare-metal@0.2.5": "1cy5pbb92fznnri72y6drfpjxj4qdmd62f0rrlgy70dxlppn9ssx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#base64@0.21.5": "1y8x2xs9nszj5ix7gg4ycn5a6wy7ca74zxwqri3bdqzdjha6lqrm",
|
||||
"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#bindgen@0.69.4": "18194611hn3k1dkxlha7a52sr8vmfhl9blc54xhj08cahd8wh3d0",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#bit-set@0.5.3": "1wcm9vxi00ma4rcxkl3pzzjli6ihrpn9cfdi0c5b4cvga2mxs007",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#bit-vec@0.6.3": "1ywqjnv60cdh1slhz67psnp422md6jdliji6alq0gmly2xm9p7rl",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#bit_field@0.10.2": "0qav5rpm4hqc33vmf4vc4r0mh51yjx5vmd9zhih26n9yjs3730nw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#bitfield@0.13.2": "06g7jb5r2b856vnhx76081fg90jvmy61kjqcfjysgmd5hclvvbs6",
|
||||
"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.4.1": "01ryy3kd671b0ll4bhdvhsz67vwz1lz53fz504injrd7wpv64xrj",
|
||||
"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#blocking@1.5.1": "064i3d6b8ln34fgdw49nmx9m36bwi3r3nv8c9xhcrpf4ilz92dva",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#build_html@2.4.0": "188nibbsv33vgjjiq9cn2irsgdb75gxfipavcavnyydcwxpzw21i",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#bumpalo@3.14.0": "1v4arnv9kwk54v5d0qqpv4vyw2sgr660nk0w3apzixi1cm3yfc3z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.14.0": "1ik1ma5n3bg700skkzhx50zjk7kj7mbsphi773if17l04pn2hk9p",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0": "0jzncxyf404mwqdbspihyzpkndfgda450l0893pz5xj685cg5l0z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#bytes@1.7.2": "1wzs7l57iwqmrszdpr2mmqf1b1hgvpxafc30imxhnry0zfl9m3a2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#cairo-rs@0.18.5": "1qjfkcq3mrh3p01nnn71dy3kn99g21xx3j8xcdvzn8ll2pq6x8lc",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#bytes@1.5.0": "08w2i8ac912l8vlvkv3q51cd4gr09pwlg3sjsjffcizlrb0i5gd2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#cairo-rs@0.18.3": "18d80lk853bjhx36rjaj78clzfjrmlgi01863drnmshdgxi16dpk",
|
||||
"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.0.83": "1l643zidlb5iy1dskc5ggqs4wqa29a02f44piczqc8zcnsq4y5zi",
|
||||
"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-expr@0.15.5": "1cqicd9qi8mzzgh63dw03zhbdihqfl3lbiklrkynyzkq67s5m483",
|
||||
"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-tz@0.8.4": "0xhd3dsfs72im0sbc7w889lfy7bxgjlbvqhj5a1yvxhxwb08acg2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.31": "0f6vg67pipm8cziad2yms6a639pssnvysk1m05dd9crymmdnhb3z",
|
||||
"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.4.11": "1wj5gb2fnqls00zfahg3490bdfc36d9cwpl80qjacb5jyrqzdbxz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.4.11": "1fxdsmw1ilgswz3lg2hjlvsdyyz04k78scjirlbd7c9bc83ba5m2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.4.7": "0hk4hcxl56qwqsf4hmf7c0gr19r9fbxk0ah2bgkr36pmmaph966g",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#clap_lex@0.6.0": "1l8bragdvim7mva9flvd159dskn2bdkpl0jqrr41wnjfn8pcfbvh",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#cloudabi@0.0.3": "0kxcg83jlihy0phnd2g8c2c303px3l2p3pkjz357ll6llnd5pz6x",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#color_quant@1.1.0": "12q1n427h2bbmmm1mnglr57jaz2dj9apk0plcxw7nwqiai7qjyrx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.2": "1h18ph538y8yjmbpaf8li98l0ifms2xmh3rax9666c5qfjfi3zfk",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#concurrent-queue@2.5.0": "0wrr3mzq2ijdkxwndhf79k952cp4zkz35ray8hvsxl96xrx1k82c",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.0": "1ix7w85kwvyybwi2jdkl3yva2r2bvdcc3ka2grjfzfgrapqimgxc",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#concurrent-queue@2.4.0": "0qvk23ynj311adb4z7v89wk3bs65blps4n24q8rgl23vjk6lhq6i",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#const-oid@0.9.6": "1y0jnqaq7p2wvspnx7qj76m7hjcqpz73qzvr9l2p9n2s51vr6if2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#convert_case@0.6.0": "1jn1pq6fp3rri88zyw6jlhwwgf6qiyc08d6gjv0qypgkl862n67c",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#cookie-factory@0.3.3": "18mka6fk3843qq3jw1fdfvzyv05kx7kcmirfbs2vg2kbw9qzm1cq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#cookie@0.17.0": "096c52jg9iq4lfcps2psncswv33fc30mmnaa2sbzzcfcw71kgyvy",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#cool_asserts@2.0.3": "1v18dg7ifx41k2f82j3gsnpm1fg9wk5s4zv7sf42c7pnad72b7zf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#core-foundation-sys@0.8.7": "12w8j73lazxmr1z0h98hf3z623kl8ms7g07jch7n4p8f9nwlhdkp",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#core-foundation-sys@0.8.6": "13w6sdf06r0hn7bx2b45zxsg1mm2phz34jikm6xc5qrbr6djpsh6",
|
||||
"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#cortex-m-rt-macros@0.7.0": "1iyki0wq8pj0qbjhw1mbq5njraihhyr7ydcbqzdzwg10dziz7xph",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#cortex-m-rt@0.7.3": "1cfxg502gvcmaczmaij5maxbvaxnda5w6gp14cbin44ksl9yi17f",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#cortex-m@0.7.7": "1fbca698v4gv57mv5fc48jrz8wcy6sv675n6fsrsah4qykc11ilf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.11": "1l0gzsyy576n017g9bf0vkv5hhg9cpz1h1libxyfdlzcgbh0yhnf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crc-any@2.5.0": "0wzs26q5cf29fhfnrkrjsr8dpai0rlm4im8b53by8rbrbzzwjbm6",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crc-catalog@2.4.0": "1xg7sz82w3nxp1jfn425fvn1clvbzb3zgblmxsyqpys0dckp9lqr",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.4.2": "1czp7vif73b8xslr3c9yxysmh9ws2r8824qda7j47ffs9pcnjxx9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crc@3.2.1": "0dnn23x68qakzc429s1y9k9y3g8fn5v9jwi63jcz151sngby9rk9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-deque@0.8.5": "03bp38ljx4wj6vvy4fbhx41q8f585zyqix6pncz1mkz93z08qgv1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-epoch@0.9.18": "03j2np8llwf376m3fxqx859mgp9f83hj1w34153c7a9c7i5ar0jv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-queue@0.3.11": "0d8y8y3z48r9javzj67v3p2yfswd278myz1j9vzc4sp7snslc0yz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.20": "100fksq5mm1n7zj242cclkw6yf7a4a8ix3lvpfkhxvdhbda9kv12",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.3.2": "03c8f29yx293yf43xar946xbls1g60c207m9drf8ilqhr25vsh5m",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crc@3.0.1": "1zkx87a5x06xfd6xm5956w4vmdfs0wcxpsn7iwj5jbp2rcapmv46",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#critical-section@1.1.2": "05pj0pvkdyc9r30xxabam4n8zxdbzxcddr0gdypajcbqjgwgynbh",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-deque@0.8.4": "0la7fx9n1vbx3h23va0xmcy36hziql1pkik08s3j3asv4479ma7w",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-epoch@0.9.16": "1anr32r8px0vb65cgwbwp3zhqz69scz5dgq9bmx54w5qa59yjbrd",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-queue@0.3.9": "0lz17pgydh29w8brld8dysi1m4n5bxfpnj8w9bxk0q6xpyyzbg5r",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.17": "13y7wh993i7q71kg6wcfj65w3rlmizzrz7cqgz1l9whlgw9rcvf0",
|
||||
"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.5.0": "1rcbnwfmfxhlshzbn3r7srm3azqha3mn33yxyqxkzz2wpqcjm5ky",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#debug-helper@0.3.13": "0bhnpzpgmg8dkdr27g2b49slf6ca79m4idcb01z2krs0qkifhy7m",
|
||||
"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#der@0.7.8": "070bwiyr80800h31c5zd96ckkgagfjgnrrdmz3dzg2lccsd3dypz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#deranged@0.3.10": "1p4i64nkadamksa943d6gk39sl1kximz0xr69n408fvsl1q0vcwf",
|
||||
"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#displaydoc@0.2.4": "0p8pyg10csc782qlwx3znr6qx46ni96m1qh597kmyrf6s3s8axa8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#dotenvy@0.15.7": "16s3n973n5aqym02692i1npb079n5mb0fwql42ikmwn8wnrrbbqs",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#either@1.13.0": "1w2c1mybrd7vljyxk77y9f4w9dyjrmp3yp82mk7bcm8848fazcb0",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#encoding_rs@0.8.34": "0nagpi1rjqdpvakymwmnlxzq908ncg868lml5b70n08bm82fjpdl",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#env_logger@0.10.2": "1005v71kay9kbz1d5907l0y7vh9qn2fqsp2yfgb8bjvin6m0bm2c",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#either@1.9.0": "01qy3anr7jal5lpc20791vxrw0nl6vksb5j7x56q2fycgcyy8sm2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#embedded-alloc@0.5.1": "05gqqv9nyr33vbd0i8ab2bmfcc5kwgk0msk4pk7w5fncba8igbnx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#embedded-dma@0.2.0": "0ijld5jblcka4b95s1lwxd9k109nyaap34h44g122ddjbidpwkwr",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#embedded-hal@0.2.7": "1zv6pkgg2yl0mzvh3jp326rhryqfnv4l27h78v7p7maag629i51m",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#encoding_rs@0.8.33": "1qa5k4a0ipdrxq4xg9amms9r9pnnfn7nfh2i9m3mw0ka563b6s3j",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#env_logger@0.10.1": "1kmy9xmfjaqfvd4wkxr1f7d16ld3h9b487vqs2q9r0s8f3kg7cwm",
|
||||
"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.8": "0ia28ylfsp36i27g1qih875cyyy4by2grf80ki8vhgh6vinf8n52",
|
||||
"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.4.0": "1lwprdjqp2ibbxhgm9khw7s7y7k4xiqj5i5yprqiks6mnrq4v3lm",
|
||||
"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@4.0.1": "04k7qbi5kgs36s905gxijj41kcr78xs2s6cp6vbg50254z7wvwl4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#exr@1.71.0": "1a58k179b0h8zpf1cfgc2vl60j2syg7cdgdzp9j6cgmb6lgpcal3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.0.1": "19flpv5zbzpf0rk4x77z4zf25in0brg8l7m304d3yrf47qvwxjr5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fdeflate@0.3.1": "0s5885wdsih2hqx3hsl7l8cl3666fgsgiwvglifzy229hpydmmk4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#field-offset@0.3.6": "0zq5sssaa2ckmcmxxbly8qgz3sxpb8g1lwv90sdh1z74qif2gqiq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fixed@1.28.0": "0nn85j5x8yzx10q49jdzia4yp6pnasnxpnwh0p9aqr7qkfwf1il5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#flate2@1.0.34": "1w1nf2ap4q1sq1v6v951011wcvljk449ap7q7jnnjf8hvjs8kdd1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fluent-bundle@0.15.3": "14zl0cjn361is69pb1zry4k2zzh5nzsfv0iz05wccl00x0ga5q3z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#finl_unicode@1.2.0": "1ipdx778849czik798sjbgk5yhwxqybydac18d2g9jb20dxdrkwg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fixed@1.24.0": "0zbfwzk4mrfbawpx2ahz533bkb97jzihv7fxiyhpmwf0wzkrrih2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#flate2@1.0.28": "03llhsh4gqdirnfxxb9g2w9n0721dyn4yjir3pz7z4vjaxb3yc26",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fluent-bundle@0.15.2": "1zbzm13rfz7fay7bps7jd4j1pdnlxmdzzfymyq2iawf9vq0wchp2",
|
||||
"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#fluent-syntax@0.11.0": "0y6ac7z7sbv51nsa6km5z8rkjj4nvqk91vlghq1ck5c3cjbyvay0",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fluent@0.16.0": "19s7z0gw95qdsp9hhc00xcy11nwhnx93kknjmdvdnna435w97xk1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#flume@0.11.0": "10girdbqn77wi802pdh55lwbmymy437k7kklnvj12aaiwaflbb2m",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7": "1hc2mcqha06aibcaza94vbi81j6pr9a1bbxrxjfhc91zin8yr7iz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#foreign-types-shared@0.1.1": "0jxgzd04ra4imjv8jgkmdq59kj8fsz6w4zxsbmlai34h26225c00",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#foreign-types@0.3.2": "1cgk0vyd7r45cj769jym4a6s7vwshvd0z4bqrb92q1fwibmkkwzn",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.1": "0milh8x7nl4f450s3ddhg57a3flcv6yq8hlkyk6fyr3mcb128dp1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#frunk@0.4.2": "11v242h7zjka0lckxcffn5pjgr3jzxyljy7ffr0ppy8jkssm38qi",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#frunk_core@0.4.2": "1mjqnn7dclwn8d5g0mrfkg360cgn70a7mm8arx6fc1xxn3x6j95g",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#frunk_derives@0.4.2": "0blsy6aq6rbvxcc0337g15083w24s8539fmv8rwp1qan2qprkymh",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#frunk_proc_macro_helpers@0.1.2": "0b1xl4cfrfai7qi5cb4h9x0967miv3dvwvnsmr1vg4ljhgflmd9m",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fuchsia-cprng@0.1.1": "1fnkqrbz7ixxzsb04bsz9p0zzazanma8znfdqjvh39n14vapfvx0",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31": "040vpqpqlbk099razq8lyn74m0f161zd0rp36hciqrwcg2zibzrd",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31": "0gk6yrxgi5ihfanm2y431jadrll00n5ifhnpx090c2f2q1cr1wh5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-executor@0.3.31": "17vcci6mdfzx4gbk0wx64chr2f13wwwpvyf3xd5fb1gmjzcx2a0y",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#fugit@0.3.7": "1rzp49521akq49vs9m8llgmdkk08zb77rry10a7srm9797b6l60p",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.29": "1jxsifvrbqzdadk0svbax71cba5d3qg3wgjq8i160mxmd1kdckgz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.29": "1308bpj0g36nhx2y6bl4mm6f1gnh9xyvvw2q2wpdgnb6dv3247gb",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-executor@0.3.29": "1g4pjni0sw28djx6mlcfz584abm2lpifz86cmng0kkxh7mlvhkqg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-intrusive@0.5.0": "0vwm08d1pli6bdaj0i7xhk3476qlx4pll6i0w03gzdnh7lh0r4qx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.31": "1ikmw1yfbgvsychmsihdkwa8a1knank2d9a8dk01mbjar9w1np4y",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-lite@2.3.0": "19gk4my8zhfym6gwnpdjiyv2hw8cc098skkbkhryjdaf0yspwljj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.31": "0l1n7kqzwwmgiznn0ywdc5i24z72zvh9q1dwps54mimppi7f6bhn",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31": "1xyly6naq6aqm52d5rh236snm08kw8zadydwqz8bip70s6vzlxg5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.31": "124rv4n90f5xwfsm9qw6y99755y021cmi5dhzh253s920z77s3zr",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31": "10aa1ar8bgkgbr4wzxlidkqkcxf77gffyj8j7768h831pcaq784z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31": "0xh8ddbkm9jy8kc5gbvjp9a4b6rqqxvc8471yb2qaz5wm2qhgg35",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.29": "1ajsljgny3zfxwahba9byjzclrgvm1ypakca8z854k2w7cb4mwwb",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-lite@2.2.0": "1flj85i6xm0rjicxixmajrp6rhq8i4bnbzffmrd6h23ln8jshns4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.29": "1nwd18i8kvpkdfwm045hddjli0n96zi7pn6f99zi9c74j7ym7cak",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.29": "05q8jykqddxzp8nwf00wjk5m5mqi546d7i8hsxma7hiqxrw36vg3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.29": "1qmsss8rb5ppql4qvd4r70h9gpfcpd0bg2b3qilxrnhdkc397lgg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.29": "0141rkqh0psj4h8x8lgsl1p29dhqr7z2wcixkcbs60z74kb2d5d1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#futures@0.3.29": "0dak2ilpcmyjrb1j54fzy9hlw6vd10vqljq9gd59pbrq9dqr00ns",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gcd@2.3.0": "06l4fib4dh4m6gazdrzzzinhvcpcfh05r4i4gzscl03vnjhqnx8x",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf-sys@0.18.0": "1xya543c4ffd2n7aiwwrdxsyc9casdbasafi6ixcknafckm3k61z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf@0.18.5": "1v7svvl0g7zybndmis5inaqqgi1mvcc6s1n8rkb31f5zn3qzbqah",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf@0.18.3": "0b68ssdyapvq3bgsna9frabbzhjkvvzz8jld4mxkphr29nvk4vs4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gdk4-sys@0.7.2": "1w7yvir565sjrrw828lss07749hfpfsr19jdjzwivkx36brl7ayv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gdk4@0.7.3": "1xiacc63p73apr033gjrb9dsk0y4yxnsljwfxbwfry41snd03nvy",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.11.2": "0a7w8w0rg47nmcinnfzv443lcyb8mplwc251p1jyr5xj2yh6wzv6",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7": "16lyyrzrljfq424c3n8kfwkqihlimmsg5nhshbbp48np3yjrqr45",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.15": "1mzlnrb3dgyd1fb84gvw10pyr8wdqdl4ry4sr64i1s8an66pqmn4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.11": "03q7120cc2kn7ry013i67zmjl2g9q73h1ks5z08hq5v9syz0d47y",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gif@0.11.4": "01hbw3isapzpzff8l6aw55jnaqx2bcscrbwyf3rglkbbfp397p9y",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gif@0.13.1": "1whrkvdg26gp1r7f95c6800y6ijqw5y0z8rgj6xihpi136dxdciz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gimli@0.31.1": "0gvqc0ramx8szv76jhfd4dms0zyamvlg4whhiz11j34hh3dqxqh7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gif@0.12.0": "0ibhjyrslfv9qm400gp4hd50v9ibva01j4ab9bwiq1aycy9jayc0",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gimli@0.28.1": "0lv23wc8rxvmjia3mcxc6hj9vkqnv1bqq0h8nzjcgf71mrxx6wa2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gio-sys@0.18.1": "1lip8z35iy9d184x2qwjxlbxi64q9cpayy7v1p5y9xdsa3w6smip",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gio@0.18.4": "0wsc6mnx057s4ailacg99dwgna38dbqli5x7a6y9rdw75x9qzz6l",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.16.3": "1z73bl10zmxwrv16v4f5wcky1f3z5a2v0hknca54al4k2p5ka695",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.17.10": "05p7ab2vn8962cbchi7a6hndhvw64nqk4w5kpg5z53iizsgdfrbs",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.18.0": "0p5c2ayiam5bkp9wvq9f9ihwp06nqs5j801npjlwnhrl8rpwac9l",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#glib-macros@0.18.5": "1p5cla53fcp195zp0hkqpmnn7iwmkdswhy7xh34002bw8y7j5c0b",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#glib-macros@0.18.3": "19crnw5a57w02njpbsmdqwbkncl6hw6g3mv554y8dqzcrri3jybj",
|
||||
"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#glib@0.18.4": "0kjws6ns6dym48nzxz9skhipk55flc2hy5q5kzg4w12wvizvs6wm",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#glob@0.3.1": "16zca52nglanv23q5qrwd5jinw3d3as5ylya6y1pbx47vkxvrynj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gloo-timers@0.3.0": "1519157n7xppkk6pdw5w52vy1llzn5iljkqd7q1h5609jv7l7cdv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gobject-sys@0.18.0": "0i6fhp3m6vs3wkzyc22rk2cqj68qvgddxmpaai34l72da5xi4l08",
|
||||
|
@ -155,110 +174,111 @@
|
|||
"registry+https://github.com/rust-lang/crates.io-index#gtk4-macros@0.7.2": "0bw3cchiycf7dw1bw4p8946gv38azxy05a5w0ndgcmxnz6fc8znm",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gtk4-sys@0.7.3": "1f2ylskyqkjdik9fij2m46pra4jagnif5xyalbxfk3334fmc9n2l",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#gtk4@0.7.3": "0hh8nzglmz94v1m1h6vy8z12m6fr7ia467ry0md5fa4p7sm53sss",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#h2@0.3.26": "1s7msnfv7xprzs6xzfj5sg6p8bjcdpcqcmjjbkd345cyi1x55zl1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#half@2.4.1": "123q4zzw1x4309961i69igzd1wb7pj04aaii3kwasrz3599qrl3d",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.14.5": "1wa1vy1xs3mp11bn3z9dv0jricgr6a2j0zkf1g19yz3vw4il89z5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.15.0": "1yx4xq091s7i6mw6bn77k8cp4jrpcac149xr32rg8szqsj27y20y",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#h2@0.3.22": "0y41jlflvw8niifdirgng67zdmic62cjf5m2z69hzrpn5qr50qjd",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#half@2.2.1": "1l1gdlzxgm7wc8xl5fxas20kfi1j35iyb7vfjkghbdzijcvazd02",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.14.3": "012nywlg0lj9kwanh69my5x67vjlfmzfi9a0rq4qvis2j8fil3r9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hashlink@0.8.4": "1xy8agkyp0llbqk9fcffc1xblayrrywlyrm2a7v93x8zygm4y2g8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#headers-core@0.2.0": "0ab469xfpd411mc3dhmjhmzrhqikzyj8a17jn5bkj9zfpy0n9xp7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#headers@0.3.9": "0w62gnwh2p1lml0zqdkrx9dp438881nhz32zrzdy61qa0a9kns06",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#heck@0.4.1": "1a7mqsnycv5z4z5vnv1k34548jzmc0ajic7c1j8jsaspnhw5ql4m",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0": "1sjmpsdl8czyh9ywl3qcsfsq9a307dg4ni2vnlwgnzzqhc4y0113",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hermit-abi@0.3.9": "092hxjbjnq5fmz66grd9plxd0sh6ssg5fhgwwwqbrzgzkjwdycfj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hermit-abi@0.4.0": "1k1zwllx6nfq417hy38x4akw1ivlv68ymvnzyxs76ffgsqcskxpv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hex-string@0.1.0": "02sgrgrbp693jv0v5iga7z47y6aj93cq0ia39finby9x17fw53l4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hex@0.4.3": "0w1a4davm1lgzpamwnba907aysmlrnygbqmfis2mqjx5m552a93z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hkdf@0.12.4": "1xxxzcarz151p1b858yn5skmhyrvn8fs4ivx5km3i1kjmnr8wpvv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hmac@0.12.1": "0pmbr069sfg76z7wsssfk5ddcqd9ncp79fyz6zcm6yn115yc6jbc",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#home@0.5.9": "19grxyg35rqfd802pcc9ys1q3lafzlcjcv2pl2s5q8xpyr5kblg3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#http-body@0.4.6": "1lmyjfk6bqk6k9gkn1dxq770sb78pqbqshga241hr5p995bb5skw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#http@0.2.12": "1w81s4bcbmcj9bjp7mllm8jlz6b31wzvirz8bgpzbqkpwmbvn730",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#http@1.1.0": "0n426lmcxas6h75c2cp25m933pswlrfjz10v91vc62vib2sdvf91",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#httparse@1.9.5": "0ip9v8m9lvgvq1lznl31wvn0ch1v254na7lhid9p29yx9rbx6wbx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#http@0.2.11": "1fwz3mhh86h5kfnr5767jlx9agpdggclq7xsqx930fflzakb2iw9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#http@1.0.0": "1sllw565jn8r5w7h928nsfqq33x586pyasdfr7vid01scwwgsamk",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#httparse@1.8.0": "010rrfahm1jss3p022fqf3j3jmm72vhn4iqhykahb9ynpaag75yq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3": "1aa9rd2sac0zhjqh24c9xvir96g188zldkx0hr6dnnlx5904cfyz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#humantime@2.1.0": "1r55pfkkf5v0ji1x6izrjwdq9v6sc7bv99xj6srywcar37xmnfls",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hyper-tls@0.5.0": "01crgy13102iagakf6q4mb75dprzr7ps1gj0l5hxm1cvm7gks66n",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hyper@0.10.16": "0wwjh9p3mzvg3fss2lqz5r7ddcgl1fh9w6my2j69d6k0lbcm41ha",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hyper@0.14.30": "1jayxag79yln1nzyzx652kcy1bikgwssn6c4zrrp5v7s3pbdslm1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#hyper@0.14.28": "107gkvqx4h9bl17d602zkm2dgpfq86l2dr36yzfsi8l3xcsy35mz",
|
||||
"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#iana-time-zone@0.1.58": "081vcr8z8ddhl5r1ywif6grnswk01b2ac4nks2bhn8zzdimvh9l3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#idna@0.1.5": "0kl4gs5kaydn4v07c6ka33spm9qdh2np0x7iw7g5zd8z1c7rxw1q",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#idna@0.5.0": "1xhjrcjqq0l5bpzvdgylvpkgk94panxgsirzhjnnqfdgc4a9nkb3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#image@0.23.14": "18gn2f7xp30pf9aqka877knlq308khxqiwjvsccvzaa4f9zcpzr4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#image@0.24.9": "17gnr6ifnpzvhjf6dwbl9hki8x6bji5mwcqp0048x1jm5yfi742n",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#image@0.24.7": "04d7f25b8nlszfv9a474n4a0al4m2sv9gqj3yiphhqr0syyzsgbg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#indent_write@2.2.0": "1hqjp80argdskrhd66g9sh542yxy8qi77j6rc69qd0l7l52rdzhc",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#indexmap@2.6.0": "1nmrwn8lbs19gkvhxaawffzbvrpyrb5y3drcrr645x957kz0fybh",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#intl-memoizer@0.5.2": "1nkvql7c7b76axv4g68di1p2m9bnxq1cbn6mlqcawf72zhhf08py",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#indexmap@2.1.0": "07rxrqmryr1xfnmhrjlz8ic6jw28v6h5cig3ws2c9d0wifhy2c6m",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#intl-memoizer@0.5.1": "0vx6cji8ifw77zrgipwmvy1i3v43dcm58hwjxpb1h29i98z46463",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#intl_pluralrules@7.0.2": "0wprd3h6h8nfj62d8xk71h178q7zfn3srxm787w4sawsqavsg3h7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#ipnet@2.10.1": "025p9wm94q1w2l13hbbr4cbmfygly3a2ag8g5s618l2jhq4l3hnx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#ipnet@2.9.0": "1hzrcysgwf0knf83ahb3535hrkw63mil88iqc6kjaryfblrqylcg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#iron@0.6.1": "1s4mf8395f693nhwsr0znw3j5frzn56gzllypyl50il85p50ily6",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#is-terminal@0.4.13": "0jwgjjz33kkmnwai3nsdk1pz9vb6gkqvw1d1vq7bs3q48kinh7r6",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#is_terminal_polyfill@1.70.1": "1kwfgglh91z33kl0w5i338mfpa3zs0hidq5j4ny4rmjwrikchhvr",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#itertools@0.12.1": "0s95jbb3ndj1lvfxyq5wanc0fm0r6hg6q4ngb92qlfdxvci10ads",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.11": "0nv9cqjwzr3q58qz84dcz63ggc54yhf1yqar1m858m1kfd4g3wa9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#is-terminal@0.4.9": "12xgvc7nsrp3pn8hcxajfhbli2l5wnh3679y2fmky88nhj4qj26b",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#itertools@0.10.5": "0ww45h7nxx5kj6z2y6chlskxd1igvs4j507anr6dzg99x1h25zdh",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#itertools@0.12.0": "1c07gzdlc6a1c8p8jrvvw3gs52bss3y58cs2s21d9i978l36pnr5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.10": "0k7xjfki7mnv6yzjrbnbnjllg86acmbnk4izz2jmm1hx2wd6v95i",
|
||||
"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#jpeg-decoder@0.3.0": "0gkv0zx95i4fr40fj1a10d70lqi6lfyia8r5q8qjxj8j4pj0005w",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#js-sys@0.3.66": "1ji9la5ydg0vy17q54i7dnwc0wwb9zkx662w1583pblylm6wdsff",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#kv-log-macro@1.0.7": "0zwp4bxkkp87rl7xy2dain77z977rvcry1gmr5bssdbn541v7s0d",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#language-tags@0.2.2": "16hrjdpa827carq5x4b8zhas24d8kg4s16m6nmmn1kb7cr5qh7d9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0": "1zk6dqqni0193xg6iijh7i3i44sryglwgvx20spdvwk3r6sbrlmv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.4.0": "0in6ikhw8mgl33wjv6q6xfrb5b9jr16q8ygjy803fay4zcisvaz2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#lazycell@1.3.0": "0m8gw7dn30i0zjjpjdyf6pc16c34nl71lpv461mix50x3p70h3c3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#lebe@0.5.2": "1j2l6chx19qpa5gqcw434j83gyskq3g2cnffrbl3842ymlmpq203",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libadwaita-sys@0.5.3": "16n6xsy6jhbj0jbpz8yvql6c9b89a99v9vhdz5s37mg1inisl42y",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libadwaita@0.5.3": "174pzn9dwsk8ikvrhx13vkh0zrpvb3rhg9yd2q5d2zjh0q6fgrrg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.159": "1i9xpia0hn1y8dws7all8rqng6h3lc8ymlgslnljcvm376jrf7an",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.151": "1x28f0zgp4zcwr891p8n9ag9w371sbib30vp4y6hi2052frplb9h",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libloading@0.8.5": "194dvczq4sifwkzllfmw0qkgvilpha7m5xy90gd6i446vcpz4ya9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libm@0.2.8": "0n4hk1rs8pzw8hdfmwn96c4568s93kfxqgcqswr7sajd2diaihjf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libspa-sys@0.8.0": "07yh4i5grzbxkchg6dnxlwbdw2wm5jnd7ffbhl77jr0388b9f3dz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libspa@0.8.0": "044qs48yl0llp2dmrgwxj9y1pgfy09i6fhq661zqqb9a3fwa9wv5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libsqlite3-sys@0.27.0": "05pp60ncrmyjlxxjj187808jkvpxm06w5lvvdwwvxd2qrmnj4kng",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#libyml@0.0.5": "106963pwg1gc3165bdlk8bbspmk919gk10vshhqglks3z8m700ik",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.4.14": "12gsjgbhhjwywpqcrizv80vrp7p7grsz5laqq773i33wphjsxcvq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.12": "05qvxa6g27yyva25a5ghsg85apdxkvr77yhkyhapj6r8vnf8pbq7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#linked_list_allocator@0.10.5": "11k2dv6v5kq45kbvahll434f9iwfw0vsyaycp76q3vh5ahzldyls",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.4.12": "0mhlla3gk1jgn6mrq9s255rvvq8a1w3yk2vpjiwsd6hmmy1imkf4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.11": "0iggx0h4jx63xm35861106af3jkxq06fpqhpkhgw0axi2n38y5iw",
|
||||
"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.20": "13rf7wphnwd61vazpxr7fiycin6cb1g8fmvgqg18i464p0y1drmm",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#logger@0.4.0": "14xlxvkspcfnspjil0xi63qj5cybxn1hjmr5gq8m4v1g9k5p54bc",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#matches@0.1.10": "1994402fq4viys7pjhzisj4wcw894l53g798kkm2y74laxk0jci5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#md-5@0.10.6": "1kvq5rnpm4fzwmyv5nmnxygdhhb2369888a06gdc9pxyrzh7x7nq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.4": "18z32bhxrax0fnjikv475z7ii718hq457qwmaryixfxsl2qrmjkq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.1": "12i17wh9a9plx869g7j4whf62xw68k5zd4k0k5nh6ys5mszid028",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#memchr@2.6.4": "0rq1ka8790ns41j147npvxcqcl2anxyngsdimy85ag2api0fwrgn",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.0": "0v20ihhdzkfw1jx00a7zjpk2dcp5qjq6lz302nyqamd9c4f4nqss",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#mime@0.2.6": "1q1s1ax1gaz8ld3513nvhidfwnik5asbs1ma3hp6inp5dn56nqms",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17": "16hkibgvb9klh0w0jk5crr5xv90l3wlf77ggymzjmvl1818vnxv8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#mime_guess@1.8.8": "18qcd5aa3363mb742y7lf39j7ha88pkzbv9ff2qidlsdxsjjjs91",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#mime_guess@2.0.5": "03jmg3yx6j39mg0kayf7w4a886dl3j15y8zs119zw01ccy74zi7p",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#mime_guess@2.0.4": "1vs28rxnbfwil6f48hh58lfcx90klcvg68gxdc60spwa4cy2d4j1",
|
||||
"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#miniz_oxide@0.7.1": "1ivl3rbbdm53bzscrd01g60l46lz5krl270487d8lhjvwl5hx0g7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#mio@1.0.2": "1v1cnnn44awxbcfm4zlavwgkvbyg7gp5zzjm8mqf1apkrwflvq40",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#modifier@0.1.0": "0n3fmgli1nsskl0whrfzm1gk0rmwwl6pw1q4nb9sqqmn5h8wkxa1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#multer@2.1.0": "1hjiphaypj3phqaj5igrzcia9xfmf4rr4ddigbh8zzb96k1bvb01",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#nary_tree@0.4.3": "1iqray1a716995l9mmvz5sfqrwg9a235bvrkpcn8bcqwjnwfv1pv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#native-tls@0.2.12": "0rkl65z70n7sy4d5w0qa99klg1hr43wx6kcprk4d2n9xr2r4wqd8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#native-tls@0.2.11": "0bmrlg0fmzxaycjpkgkchi93av07v2yf9k33gc12ca9gqdrn28h7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#nb@0.1.3": "0vyh31pbwrg21f8hz1ipb9i20qwnfwx47gz92i9frdhk0pd327c0",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#nb@1.1.0": "179kbn9l6vhshncycagis7f8mfjppz4fhvgnmcikqz30mp23jm4d",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#nix@0.27.1": "0ly0kkmij5f0sqz35lx9czlbk6zpihb7yh1bsy4irzwfd2f4xc1f",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#no-std-compat@0.4.1": "132vrf710zsdp40yp1z3kgc2ss8pi0z4gmihsz3y7hl4dpd56f5r",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#nom@7.1.3": "0jha9901wxam390jcf5pfa0qqfrgh8li787jx2ip0yk5b8y9hwyj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#num-bigint-dig@0.8.4": "0lb12df24wgxxbspz4gw1sf1kdqwvpdcpwq4fdlwg4gj41c1k16w",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#num-conv@0.1.0": "1ndiyg82q73783jq18isi71a7mjh56wxrk52rlvyx0mi5z9ibmai",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.46": "13w5g54a9184cqlbsq80rnxw4jj4s0d8wv75jsq5r2lms8gncsbr",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#num-iter@0.1.45": "1gzm7vc5g9qsjjl3bqk9rz1h6raxhygbrcpbfl04swlh0i506a8l",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.45": "1ncwavvwdmsqzxnn65phv6c6nn72pnv9xhpmjd6a429mzf4k6p92",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#num-iter@0.1.43": "0lp22isvzmmnidbq9n5kbdh8gj0zm3yhxv1ddsn5rp65530fc0vx",
|
||||
"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-rational@0.4.1": "1c0rb8x4avxy3jvvzv764yk7afipzxncfnqlb10r3h53s34s2f06",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.17": "0z16bi5zwgfysz6765v3rd6whfbjpihx3mhsn4dg8dzj2c221qrr",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#num_cpus@1.16.0": "0hra6ihpnh06dvfvz9ipscys0xfqa9ca9hzp384d5m02ssvgqqa1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#object@0.36.5": "0gk8lhbs229c68lapq6w6qmnm4jkj48hrcw5ilfyswy514nhmpxf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.20.2": "0xb7rw1aqr7pa4z3b00y7786gyf8awx2gca3md73afy76dzgwq8j",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#num_enum@0.5.11": "1japmqhcxwn1d3k7q8jw58y7xfby51s16nzd6dkj483cj2pnqr0z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#num_enum_derive@0.5.11": "16f7r4jila0ckcgdnfgqyhhb90w9m2pdbwayyqmwcci0j6ygkgyw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#object@0.32.1": "1c02x4kvqpnl3wn7gz9idm4jrbirbycyqjgiw6lm1g9k77fzkxcw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.19.0": "14kvw7px5z96dk4dwdm1r9cqhhy2cyj1l5n5b29mynbb8yr15nrz",
|
||||
"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.97": "02s670ir38fsavphdna07144y41dkvrcfkwnjzg82zfrrlsavsn3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.61": "0idv3n9n9f2sxq8cqzxvq44633vg5sx4n9q1p3g6dn66ikf1k13b",
|
||||
"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#panic-halt@0.2.0": "04nqaa97ph20ppyy58grwr23hrbw83pn0gf7apf73rdx1q7595ny",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#parking@2.2.0": "1blwbkq6im1hfxp5wlbr475mw98rsyc0bbr2d5n16m38z253p0dv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#parking_lot@0.12.1": "13r2xk7mnxfc5g0g6dkdxqdqad99j7s7z8zhzz4npw5r0g0v4hip",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.9": "13h0imw1aq86wj28gxkblhkzx6z1gk8q18n0v76qmmj6cliajhjc",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#parse-zoneinfo@0.3.0": "0h8g6jy4kckn2gk8sd5adaws180n1ip65xhzw5jxlq4w8ibg41f7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#paste@1.0.14": "0k7d54zz8zrz0623l3xhvws61z5q2wd3hkwim6gylk8212placfy",
|
||||
"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",
|
||||
|
@ -270,32 +290,33 @@
|
|||
"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.7.24": "0qi62gxk3x3whrmw5c4i71406icqk11qmpgln438p6qm7k4lqdh9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.2": "0azphb0a330ypqx3qvyffal5saqnks0xvl8rj73jlk3qxxgbkz4h",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.7.24": "18371fla0vsj7d6d5rlfb747xbr2in11ar9vgv5qna72bnhp2kr3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pin-project-internal@1.1.7": "133mxf5vmvnvw4idw2y2lb5bxsza2xlyfl6psjy7mz3l12nmy3rw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.14": "00nx3f04agwjlsmd3mc5rx5haibj2v8q9b52b0kwn63wcv4nz9mx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.7": "15cvflrzsgp1zbl5gv37al2r62nl8lc37xkfwf70ql3fji7gcmxy",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pin-project-internal@1.1.3": "01a4l3vb84brv9v7wl71chzxra2kynm6yvcjca66xv3ij6fgsna3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.13": "0n0bwr5qxlf0mhn2xkl36sy55118s9qmvx2yl5f3ixkb007lbywa",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.3": "08k4cpy8q3j93qqgnrbzkcgpn7g0a88l4a9nm33kyghpdhffv97x",
|
||||
"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#pio@0.2.1": "1qvq03nbx6vjix7spr5fcxcbxw39flm1y72kxl1g728gnna9dq3n",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#piper@0.2.1": "1m45fkdq7q5l9mv3b0ra10qwm0kb67rjp2q8y91958gbqjqk33b6",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pipewire-sys@0.8.0": "04hiy3rl8v3j2dfzp04gr7r8l5azzqqsvqdzwa7sipdij27ii7l4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pipewire@0.8.0": "1nldg1hz4v0qr26lzdxqpvrac4zbc3pb6436sl392425bjx4brh8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pkcs1@0.7.5": "0zz4mil3nchnxljdfs2k5ab1cjqn7kq5lqp62n9qfix01zqvkzy8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pkcs8@0.10.2": "1dx7w21gvn07azszgqd3ryjhyphsrjrmq5mmz1fbxkj5g0vv4l7r",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pkg-config@0.3.31": "1wk6yp2phl91795ia0lwkr3wl4a9xkrympvhqq8cxk4d75hwhglm",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#pkg-config@0.3.27": "0r39ryh1magcq4cz5g9x88jllsnxnhcqr753islvyk4jp9h2h1r6",
|
||||
"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.10": "0r5a8a25ad0jq2pkp2zbab3wwhpgp6jmdg6d0ybjnw6kilnvyxfx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#polling@3.4.0": "052am20b5r03nwhpnjw86rv3dwsdabvb07anv3fqxfbs65r4w19h",
|
||||
"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#ppv-lite86@0.2.17": "1pp6g52aw970adv3x2310n7glqnji96z0a9wiamzw89ibf0ayh2v",
|
||||
"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-crate@2.0.1": "06jbv5w6s04dbjbwq0iv7zil12ildf3w8dvvb4pqvhig4gm5zp4p",
|
||||
"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.78": "1bjak27pqdn4f4ih1c9nr3manzyavsgqmf76ygw9k76q8pb2lhp2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#proptest@1.4.0": "1gzmw40pgmwzb7x6jsyr88z5w151snv5rp1g0dlcp1iw3h9pdd1i",
|
||||
"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.35": "1vv8r2ncaz4pqdr78x7f138ka595sp2ncr1sa2plm4zxbsmwj7i9",
|
||||
"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",
|
||||
|
@ -312,180 +333,187 @@
|
|||
"registry+https://github.com/rust-lang/crates.io-index#rand_pcg@0.1.2": "0i0bdla18a8x4jn1w0fxsbs3jg7ajllz6azmch1zw33r06dv1ydb",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rand_xorshift@0.1.1": "0p2x8nr00hricpi2m6ca5vysiha7ybnghz79yqhhx6sl4gkfkxyb",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rand_xorshift@0.3.0": "13vcag7gmqspzyabfl1gr9ykvxd2142q2agrj8dkyjmfqmgg4nyj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rayon-core@1.12.1": "1qpwim68ai5h0j7axa8ai8z0payaawv3id0lrgkqmapx7lx8fr8l",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rayon@1.10.0": "1ylgnzwgllajalr4v00y4kj22klq2jbwllm70aha232iah0sc65l",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rayon-core@1.12.0": "1vaq0q71yfvcwlmia0iqf6ixj2fibjcf2xjy92n1m1izv1mgpqsw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rayon@1.8.0": "1cfdnvchf7j4cpha5jkcrrsr61li9i9lp5ak7xdq6d3pvc1xn9ww",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rdrand@0.4.0": "1cjq0kwx1bk7jx3kzyciiish5gqsj7620dm43dc52sr8fzmm9037",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#redox_syscall@0.5.7": "07vpgfr6a04k0x19zqr1xdlqm6fncik3zydbdi3f5g3l5k7zwvcv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.8": "18wd530ndrmygi6xnz3sp345qi0hy2kdbsa89182nwbl6br5i1rn",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.5": "0p41p3hj9ww7blnbwbj9h7rwxzxg0c1hvrdycgys8rxyhqqw859b",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#regex@1.11.0": "1n5imk7yxam409ik5nagsjpwqvbg3f0g0mznd5drf549x1g0w81q",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#redox_syscall@0.4.1": "1aiifyz5dnybfvkk4cdab9p2kmphag1yad6iknc7aszlxxldf8j7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.3": "0gs8q9yhd3kcg4pr00ag4viqxnh5l7jpyb9fsfr8hzh451w4r02z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.2": "17rd2s8xbiyf6lb4aj2nfi44zqlj98g2ays8zzj2vfs743k79360",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#regex@1.10.2": "0hxkd814n4irind8im5c9am221ri6bprx49nc7yxv02ykhd9a2rq",
|
||||
"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#reqwest@0.11.23": "0hgvzb7r46656r9vqhl5qk1kbr2xzjb91yr2cb321160ka6sxc9p",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rp-pico@0.8.0": "0mmx8dyl0q1a9fgz12hrvwd7civqbd1j7g1w5c5i6pcfdwg7fhb3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rp2040-boot2@0.3.0": "08dv9ndvdzyjz4wdlxcikf1m1s6wwi80027ldkihx59zyr2g74kw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rp2040-hal-macros@0.1.0": "0piaczzlbrfdhidnqkg04xs1rzal3w3zjplrh6pf3vwpwiir0iw6",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rp2040-hal@0.9.2": "1jk725cf6nx6rhn06swbx47yaq3j134m0hpnv47p5mkdgspbkwhz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rp2040-pac@0.5.0": "0k3fm4fww6gcy7w7zwbmmqn9wzz4sih13l1m93sl7x8mb0vxin8j",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rsa@0.9.6": "1z0d1aavfm0v4pv8jqmqhhvvhvblla1ydzlvwykpc3mkzhj523jx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rustc-demangle@0.1.24": "07zysaafgrkzy2rjgwqdj2a8qdpsm6zv6f5pgpk9x0lm40z9b6vi",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rustc-demangle@0.1.23": "0xnbk2bmyzshacjm2g1kd4zzv2y2az14bw3sjccq5qkpmsfvn9nn",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rustc-hash@1.1.0": "1qkc5khrmv5pqi5l5ca9p5nl5hs742cagrndhbrlk3dhlrx3zm08",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rustc_version@0.4.1": "14lvdsmr5si5qbqzrajgb6vfn69k0sfygrvfvr2mps26xwi3mjyg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rustix@0.38.37": "04b8f99c2g36gyggf4aphw8742k2b1vls3364n2z493whj5pijwa",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rustls-pemfile@1.0.4": "1324n5bcns0rnw6vywr5agff3rwfvzphi7rmbyzwnv6glkhclx0w",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rustc_version@0.2.3": "02h3x57lcr8l2pm0a645s9whdh33pn5cnrwvn5cb57vcrc53x3hk",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rustc_version@0.4.0": "0rpk9rcdk405xhbmgclsh4pai0svn49x35aggl4nhbkd4a2zb85z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#rustix@0.38.28": "05m3vacvbqbg6r6ksmx9k5afpi0lppjdv712crrpsrfax2jp5rbj",
|
||||
"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#ryu@1.0.16": "0k7b90xr48ag5bzmfjp82rljasw2fx28xr3bg1lrpx7b5sljm3gr",
|
||||
"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.22": "126zy5jb95fc5hvzyjwiq6lc81r08rdcn6affn00ispp9jzk6dqc",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#scoped-tls@1.0.1": "15524h04mafihcvfpgxd8f4bgc3k95aclz8grjkg9a0rxcvn9kz1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#scoped_threadpool@0.1.9": "1a26d3lk40s9mrf4imhbik7caahmw2jryhhb6vqv6fplbbgzal8x",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0": "0jcz9sd47zlsgcnm1hdw0664krxwb5gczlif4qngj2aif8vky54l",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#security-framework-sys@2.12.0": "1dml0lp9lrvvi01s011lyss5kzzsmakaamdwsxr0431jd4l2jjpa",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#security-framework@2.11.1": "00ldclwx78dm61v7wkach9lcx76awlrv0fdgjdwch4dmy12j4yw9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#security-framework-sys@2.9.1": "0yhciwlsy9dh0ps1gw3197kvyqx1bvc4knrhiznhid6kax196cp9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#security-framework@2.9.2": "1pplxk15s5yxvi2m1sz5xfmjibp96cscdcl432w9jzbk0frlzdh5",
|
||||
"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.0.2": "1rmdglwnd77wcw2gv76finpgzjhkynx422d0jpahrf2fsqn37273",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#semver-parser@0.7.0": "18vhypw6zgccnrlm5ps1pwa0khz7ry927iznpr88b87cagr1v2iq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#semver@0.9.0": "00q4lkcj0rrgbhviv9sd4p6qmdsipkwkbra7rh11jrhq5kpvjzhx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#semver@1.0.20": "140hmbfa743hbmah1zjf07s8apavhvn04204qjigjiz5w6iscvw3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde@0.9.15": "1bsla8l5xr9pp5sirkal6mngxcq6q961km88jvf339j5ff8j7dil",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.210": "0flc0z8wgax1k4j5bf2zyq48bgzyv425jkd5w0i6wbh7f8j5kqy8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.210": "07yzy4wafk79ps0hmbqmsqh5xjna4pm4q57wc847bb8gl3nh4f94",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.128": "1n43nia50ybpcfmh3gcw4lcc627qsg9nyakzwgkk9pm10xklbxbg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.8": "1q89g70azwi4ybilz5jb8prfpa575165lmrffd49vmcf76qpqq47",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.209": "029yqqbb3c8v3gc720fhxn49dhgvb88zbyprdg5621riwzzy1z4r",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.209": "0w114ksg1ymnmqdisd0g1j3g8jgz6pam45xg6yb47dfpkybip0x5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.127": "1b99lgg1d986gwz5fbmmzmvjmqg5bx0lzmhy6rqp5gc2kxnw0hw0",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.5": "1hgh6s3jjwyzhfk3xwb6pnnr1misq9nflwq0f026jafi37s24dpb",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde_urlencoded@0.7.1": "1zgklbdaysj3230xivihs30qi5vkhigg323a9m62k8jwf4a1qjfk",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#serde_yml@0.0.12": "1p8xwz4znd6fj962y22fdvvv16gb8c0hx4iv5hjplngiidcdvqjr",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sha1@0.10.6": "1fnnxlfg08xhkmwf2ahv634as30l1i3xhlhkvxflmasi5nd85gz3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sha2@0.10.8": "1j1x78zk9il95w9iv46dh9wm73r6xrgj32y6lzzw7bxws9dbfgbr",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0": "0r1y6bv26c1scpxvhg2cabimrmwgbp4p3wy6syj9n0c4s3q2znhg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.2": "1cb5akgq8ajnd5spyn587srvs4n26ryq0p78nswffwhv46sf1sd9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.1": "18crkkw5k82bvcx088xlf5g4n3772m24qhzgfan80nda7d3rn8nq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#signature@2.2.0": "1pi9hd5vqfr3q3k49k37z06p7gs5si0in32qia4mmr1dancr6m3p",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#simd-adler32@0.3.7": "1zkq40c3iajcnr5936gjp9jjh1lpzhy44p3dq3fiw75iwr1w2vfn",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#siphasher@0.2.3": "1b53m53l24lyhr505lwqzrpjyq5qfnic71mynrcfvm43rybf938b",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#siphasher@0.3.11": "03axamhmwsrmh0psdw3gf7c0zc4fyl5yjxfifz9qfka6yhkqid9q",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#slab@0.4.9": "0rxvsgir0qw5lkycrqgb1cxsvxzjv9bmx73bk5y42svnzfba94lg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#smallvec@1.13.2": "0rsw5samawl3wsw6glrsb127rx6sh89a8wyikicw6dkdcjd1lpiw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#smallvec@1.11.2": "0w79x38f7c0np7hqfmzrif9zmn0avjvvm31b166zdk9d1aad1k2d",
|
||||
"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.5": "1sgq315f1njky114ip7wcy83qlphv9qclprfjwvxcpfblmcsqpvv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#spin@0.5.2": "0b84m6dbzrwf2kxylnw82d3dr8w06av7rfkr8s85fb5f43rwyqvf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#spin@0.9.8": "0rvam5r0p3a6qhc18scqpvpgb3ckzyqxpgdfyjnghh8ja7byi039",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#spki@0.7.3": "17fj8k5fmx4w9mp27l970clrh5qa7r5sjdvbsln987xhb34dc7nr",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlformat@0.2.6": "14470h40gn0f6jw9xxzbpwh5qy1fgvkhkfz8xjyzgi0cvf9kmfkv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-core@0.7.4": "1xiyr35dq10sf7lq00291svcj9wbaaz1ihandjmrng9a6jlmkfi4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros-core@0.7.4": "1j7k0fw7n6pgabqnj6cbp8s3rmd3yvqr4chjj878cvd1m99yycsq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros@0.7.4": "09rih250868nfkax022y5dyk24a7qfw6scjy3sgalbzb8lihx92f",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-mysql@0.7.4": "066lxhb80xgb8r5m2yy3a7ydjvp0b6wsk9s7whwfa83d46817lqy",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-postgres@0.7.4": "0zjp30wj4n2f25dnb32vsg6jfpa3gw6dmfd0i5pr4kw91fw4x0kw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-sqlite@0.7.4": "1ap0bb2hazbrdgd7mhnckdg9xcchx0k094di9gnhpnhlhh5fyi5j",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx@0.7.4": "1ahadprvyhjraq0c5712x3kdkp1gkwfm9nikrmcml2h03bzwr8n9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#stringprep@0.1.5": "1cb3jis4h2b767csk272zw92lc6jzfzvh8d6m1cd86yqjb9z6kbv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1": "0kzvqlw8hxqb7y598w1s0hxlnmi84sg5vsipp3yg5na5d1rvba3x",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#subtle@2.6.1": "14ijxaymghbl1p0wql9cib5zlwiina7kall6w7g89csprkgbvhhk",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlformat@0.2.3": "0v0p70wjdshj18zgjjac9xlx8hmpx33xhq7g8x9rg4s4gjyvg0ff",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-core@0.7.3": "1gdz44yb9qwxv4xl4hv6w4vbqx0zzdlzsf9j9gcj1qir6wy0ljyq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros-core@0.7.3": "0h88wahkxa6nam536lhwr1y0yxlr6la8b1x0hs0n88v790clbgfh",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros@0.7.3": "19gjwisiym07q7ibkp9nkvvbywjh0r5rc572msvzyzadvh01r5l9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-mysql@0.7.3": "190ygz5a3pqcd9vvqjv2i4r1xh8vi53j4272yrld07zpblwrawg3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-postgres@0.7.3": "090wm9s6mm53ggn1xwr183cnn8yxly8rgcksdk4hrlfcnz1hmb6n",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx-sqlite@0.7.3": "143laha7wf8dmi0xwycwqmvxdcnb25dq7jnqrsgvmis8v6vpc291",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sqlx@0.7.3": "1kv3hyx7izmmsjqh3l47zrfhjlcblpg20cvnk7pr8dm7klkkr86v",
|
||||
"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.4": "1rkfsf7riynsmqj3hbldfrvmna0i9chx2sz39qdpl40s4d7dfhdv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#strsim@0.10.0": "08s69r4rcrahwnickvi0kq49z524ci50capybln83mg6b473qivk",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#subtle@2.5.0": "1g2yjs7gffgmdvkkq0wrrh0pxds3q0dv6dhkw9cdpbib656xdkc1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#syn@1.0.109": "0ds2if4600bd59wsv7jjgfkayfzy3hnazs394kz6zdkmna8l3dkj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#syn@2.0.79": "147mk4sgigmvsb9l8qzj199ygf0fgb0bphwdsghn8205pz82q4w9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@0.1.2": "0q01lyj0gr9a93n10nxsn8lwbzq97jqd6b768x17c8f7v7gccir0",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#syn@2.0.48": "0gqgfygmrxmp8q32lia9p294kdd501ybn6kn2h4gqza0irik2d8g",
|
||||
"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#system-deps@6.2.0": "0c836abhh3k8yn5ymg8wx383ay7n731gkrbbp3gma352yq7mhb9a",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.12.12": "02lk65ik5ffb8vl9qzq02v0df8kxrp16zih78a33mji49789zhql",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tempdir@0.3.7": "1n5n86zxpgd85y0mswrp5cfdisizq2rv3la906g6ipyc03xvbwhm",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tempfile@3.13.0": "0nyagmbd4v5g6nzfydiihcn6l9j1w9bxgzyca5lyzgnhcbyckwph",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#termcolor@1.4.1": "0mappjh3fj3p2nmrg4y7qv94rchwi9mzmgmfflr8p2awdj7lyy86",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@1.0.64": "1hvzmjx9iamln854l74qyhs0jl2pg3hhqzpqm9p8gszmf9v4x408",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.64": "114s8lmssxl0c2480s671am88vzlasbaikxbvfv8pyqrq6mzh2nm",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tempfile@3.8.1": "1r88v07zdafzf46y63vs39rmzwl4vqd4g2c5qarz9mqa8nnavwby",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#termcolor@1.4.0": "0jfllflbxxffghlq6gx4csv0bv0qv77943dcx01h9zssy39w66zz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@1.0.51": "1ps9ylhlk2vn19fv3cxp40j3wcg1xmb117g2z2fbf4vmg2bj4x01",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.51": "1drvyim21w5sga3izvnvivrdp06l2c24xwbhp0vg1mhn2iz2277i",
|
||||
"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#tiff@0.9.0": "04b2fd3clxm0pmdlfip8xj594zyrsfwmh641i6x1gfiz9l7jn5vd",
|
||||
"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.16": "0gx4ngf5g7ydqa8lf7kh9sy72rd4dhvpi31y1jvswi0288rpw696",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#time@0.1.45": "0nl0pzv9yf56djy8y5dx25nka5pr2q1ivlandb3d24pksgx7ly8v",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#time@0.3.36": "11g8hdpahgrf1wwl2rpsg5nxq3aj7ri6xr672v4qcij6cgjqizax",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.6": "0bxqaw7z8r2kzngxlzlgvld1r6jbnwyylyvyjbv1q71rvgaga5wi",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tinyvec@1.8.0": "0f5rf6a2wzyv6w4jmfga9iw7rp9fp5gf4d604xgjsf3d9wgqhpj4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#time@0.3.31": "0gjqcdsdbh0r5vi4c2vrj5a6prdviapx731wwn07cvpqqd1blmzn",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.5": "1khf3j95bwwksj2hw76nlvwlwpwi4d1j421lj6x35arqqprjph43",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tinyvec@1.6.0": "0l6bl2h62a5m44jdnpn7lmj14rd44via8180i7121fvm73mmrk47",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tinyvec_macros@0.1.1": "081gag86208sc3y6sdkshgw3vysm5d34p431dzw0bshz66ncng0z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tokio-macros@2.4.0": "0lnpg14h1v3fh2jvnc8cz7cjf0m7z1xgkwfpcyy632g829imjgb9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tokio-native-tls@0.3.1": "1wkfg6zn85zckmv4im7mv20ca6b1vmlib5xwz9p7g19wjfmpdbmv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tokio-stream@0.1.16": "1wc65gprcsyzqlr0k091glswy96kph90i32gffi4ksyh03hnqkjg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tokio-stream@0.1.14": "0hi8hcwavh5sdi1ivc9qc4yvyr32f153c212dpd7sb366y6rhz1r",
|
||||
"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.10": "058y6x4mf0fsqji9rfyb77qbfyc50y4pk2spqgj6xsyr693z66al",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tokio@1.39.3": "1xgzhj7bxqqpjaabjkgsx8hi0f600bzj4iyp9f0a9gr3k6dwkawv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#toml@0.8.2": "0g9ysjaqvm2mv8q85xpqfn7hi710hj24sd56k49wyddvvyq8lp8q",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.6.3": "0jsy7v8bdvmzsci6imj8fzgd255fmy5fzp6zsri14yrry7i77nkw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.19.15": "08bl7rp5g6jwmfpad9s8jpw8wjrciadpnbaswgywpr9hv9qbfnqv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.20.2": "0f7k5svmxw98fhi28jpcyv7ldr2s3c867pjbji65bdxjpd44svir",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3": "1hzfkvkci33ra94xjx64vv3pp0sq346w06fpkcdwjcid7zhvdycd",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.2": "0lmfzmmvid2yp2l36mbavhmqgsvzqf7r2wiwz73ml4xmwaf1rg5n",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.27": "1rvb5dn9z6d0xdj14r403z0af0bbaqhg02hq4jc97g5wds6lqw1l",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.32": "0m5aglin3cdwxpvbg6kz0r9r0k31j48n0kcfwsp6l49z26k3svf0",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.40": "1vv48dac9zgj9650pg2b4d0j3w6f3x9gbggf43scq5hrlysklln3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#traitobject@0.1.0": "0yb0n8822mr59j200fyr2fxgzzgqljyxflx9y8bdy3rlaqngilgg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#try-lock@0.2.5": "0jqijrrvm1pyq34zn1jmy2vihd4jcrjlvsh4alkjahhssjnsn8g4",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#tungstenite@0.21.0": "1qaphb5kgwgid19p64grhv2b9kxy7f1059yy92l9kwrlx90sdwcy",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#type-map@0.5.0": "17qaga12nkankr7hi2mv43f4lnc78hg480kz6j9zmy4g0h28ddny",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#type-map@0.4.0": "0ilsqq7pcl3k9ggxv2x5fbxxfd6x7ljsndrhc38jmjwnbr63dlxn",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#typeable@0.1.2": "11w8dywgnm32hb291izjvh4zjd037ccnkk77ahk63l913zwzc40l",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#typemap@0.3.3": "1xm1gbvz9qisj1l6d36hrl9pw8imr8ngs6qyanjnsad3h0yfcfv5",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#typenum@1.17.0": "09dqxv69m9lj9zvv6xw5vxaqx15ps0vxyy5myg33i0kbqvq0pzs2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#typeshare-annotation@1.0.4": "0kx38ah6638pkqq5cac7nmvbg6x43v7fj5jgibla4lj8fv1dc5d6",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#typeshare@1.0.3": "11riglm8incm0vq7ciyd907w1sc6frfn7h7ab0yp8bkcnycp7w84",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#typeshare-annotation@1.0.2": "1adpfhyz3lqjjbq2ym69mv62ymqyd5651gxlqdy8aa446l70srzw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#typeshare@1.0.1": "1mi7snkx2b4g84x8vx38v1myg5r6g48c865j0nz5zcsc8lpilkgl",
|
||||
"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#unic-langid-impl@0.9.4": "1ijvqmsrg6qw3b1h9bh537pvwk2jn2kl6ck3z3qlxspxcch5mmab",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unic-langid@0.9.4": "05pm5p3j29c9jw9a4dr3v64g3x6g3zh37splj47i7vclszk251r3",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicase@1.4.2": "0cwazh4qsmm9msckjk86zc1z35xg7hjxjykrgjalzdv367w6aivz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicase@2.7.0": "12gd74j79f94k4clxpf06l99wiv4p30wjr0qm04ihqk9zgdd9lpp",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-bidi@0.3.17": "14vqdsnrm3y5anj6h5zz5s32w88crraycblb88d9k23k9ns7vcas",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.13": "1zm1xylzsdfvm2a5ib9li3g5pp7qnkv4amhspydvgbmd9k6mc6z9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-normalization@0.1.24": "0mnrk809z3ix1wspcqy97ld5wxdb31f3xz6nsvg5qcv289ycjcsh",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-properties@0.1.3": "1l3mbgzwz8g14xcs09p4ww3hjkjcf0i1ih13nsg72bhj8n5jl3z7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-segmentation@1.12.0": "14qla2jfx74yyb9ds3d2mpwpa4l4lzb9z57c6d2ba511458z5k7n",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-width@0.1.14": "1bzn2zv0gp8xxbxbhifw778a7fc93pa6a1kj24jgg9msj07f7mkx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-bidi@0.3.14": "05i4ps31vskq1wdp8yf315fxivyh1frijly9d4gb5clygbr2h9bg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.12": "0jzf1znfpb2gx8nr8mvmyqs1crnv79l57nxnbiszc7xf7ynbjm1k",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-normalization@0.1.22": "08d95g7b1irc578b2iyhzv4xhsa4pfvwsqxcl9lbcpabzkq16msw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-segmentation@1.10.1": "0dky2hm5k51xy11hc3nk85p533rvghd462b6i0c532b7hl4j9mhx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#unicode-width@0.1.13": "0p92vl8n7qc8mxz45xn6qbgi0259z96n32a158l6vj5bywwdadh3",
|
||||
"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.0": "0cs65961miawncdg2z20171w0vqrmraswv2ihdpd8lxp7cp31rii",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#urlencoding@2.1.3": "1nj99jp37k47n0hvaz5fvz7z6jd0sb4ppvfy3nphr1zbnyixpy6s",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#usb-device@0.2.9": "0205a850jhw9gb96scwfx1k4iwpjvighvz3m80mjkda9r2nw6v0z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#utf-8@0.7.6": "1a9ns3fvgird0snjkd3wbdhwd3zdpc2h5gpyybrfr6ra5pkqxk09",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2": "088807qwjq46azicqwbhlmzwrbkz7l4hpw43sdkdyyk524vdxaq6",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.1": "02ip1a0az0qmc2786vxk2nqwsgcwf17d3a38fkf0q7hrmwh9c6vi",
|
||||
"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.6.1": "0q45jxahvysldn3iy04m8xmr8hgig80855y9gq9di8x72v7myfay",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#value-bag@1.7.0": "02r8wccrzi3bzlkrslkcfw9pwp8kwif9szif2i9arn9dzqx44vhj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#vcell@0.1.3": "00n0ss2z3rh0ihig6d4w7xp72g58f7g1m6s5v4h3nc6jacdrqhvp",
|
||||
"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-compare@0.1.1": "0acg4pmjdbmclg0m7yhijn979mdy66z3k8qrcnvn634f1gy456jp",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#version_check@0.1.5": "1pf91pvj8n6akh7w6j5ypka6aqz08b3qpzgs0ak2kjf4frkiljwi",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5": "0nhhi4i5x89gm911azqbn7avs9mdacw2i3vcz3cnmz3mv4rqz4hb",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.4": "0gs8grwdlgh0xq660d7wr80x14vxbizmd8dbp29p2pdncx8lp1s9",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#void@1.0.2": "0zc8f0ksxvmhvgx4fdg0zyn6vdnbxd2xv9hfx4nhzg6kbs4f80ka",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#volatile-register@0.2.2": "1k0rkm81qyhn4r8f03z0sch2kyikkgjjfalpaami9c08c8m7whyy",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wait-timeout@0.2.0": "1xpkk0j5l9pfmjfh1pi0i89invlavfrd9av5xp0zhxgb29dhy84z",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#want@0.3.1": "03hbfrnvqqdchb5kgxyavb9jabwza0dmh2vw5kg0dq8rxl57d9xz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#warp@0.3.7": "07137zd13lchy5hxpspd0hs6sl19b0fv2zc1chf02nwnzw1d4y23",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasi@0.10.0+wasi-snapshot-preview1": "07y3l8mzfzzz4cj09c8y90yak4hpsi9g7pllyzpr6xvwrabka50s",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasi@0.11.0+wasi-snapshot-preview1": "08z4hxwkpdpalxjps1ai9y7ihin26y9f476i53dv98v45gkqg3cw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasite@0.1.0": "0nw5h9nmcl4fyf4j5d4mfdjfgvwi1cakpi349wc4zrr59wxxinmq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-backend@0.2.93": "0yypblaf94rdgqs5xw97499xfwgs1096yx026d6h88v563d9dqwx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-futures@0.4.43": "1vf8kmaj95xn5893y1bdlav47y5niq85q5bms9pfj8d6cc7k1sb1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro-support@0.2.93": "0dp8w6jmw44srym6l752nkr3hkplyw38a2fxz5f3j1ch9p3l1hxg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro@0.2.93": "1kycd1xfx4d9xzqknvzbiqhwb5fzvjqrrn88x692q1vblj8lqp2q",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-shared@0.2.93": "1104bny0hv40jfap3hp8jhs0q4ya244qcrvql39i38xlghq0lan6",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen@0.2.93": "1dfr7pka5kwvky2fx82m9d060p842hc5fyyw8igryikcdb0xybm8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#web-sys@0.3.70": "1h1jspkqnrx1iybwhwhc3qq8c8fn4hy5jcf0wxjry4mxv6pymz96",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#weezl@0.1.8": "10lhndjgs6y5djpg3b420xngcr6jkmv70q8rb1qcicbily35pa2k",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#whoami@1.5.2": "0vdvm6sga4v9515l6glqqfnmzp246nq66dd09cw5ri4fyn3mnb9p",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-backend@0.2.89": "09l8lyylsdssz993h4fzja69zpvpykaw84fivs210fjgwqjzcmhv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-futures@0.4.39": "04lsxpw4jqfwh7c0crzx0smj52nvwp1w3bh4098sq90149da2dmc",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro-support@0.2.89": "10sj1gr2naxv5q116yjb929hhpvz45dxbkvyk8hyc2lknzy85szh",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro@0.2.89": "1cl2w7k5jn2jbd5kx613c8k8vjvda22hfgcgx7y2mk93fbrxnqh1",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-shared@0.2.89": "17s5rppad113c6ggkaq8c3cg7a3zz15i78wxcg6mcl1n15iv7fbs",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen@0.2.89": "0kh6akdldy13z9xqj0skz6b4npq1d98bjkgzb8ccq59hibvd9l0f",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#web-sys@0.3.66": "03q1z22djv5ncqkyydcvnchmdsl5gvnyzcyixkxnifw6xi24mhjh",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#weezl@0.1.7": "1frdbq6y5jn2j93i20hc80swpkj30p1wffwxj1nr4fp09m6id4wi",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#whoami@1.4.1": "0l6ca9pl92wmngsn1dh9ih716v216nmn2zvcn94k04x9p1b3gz12",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#winapi-i686-pc-windows-gnu@0.4.0": "1dmpa6mvcvzz16zg6d5vrfy4bxgg541wxrcip7cnshi06v38ffxc",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#winapi-util@0.1.9": "1fqhkcl9scd230cnfj8apfficpf5c9vhwnk4yy9xfc1sw69iq8ng",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#winapi-util@0.1.6": "15i5lm39wd44004i9d5qspry2cynkrpvwzghr6s2c3dsk28nz7pj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#winapi-x86_64-pc-windows-gnu@0.4.0": "0gqq64czqb64kskjryj8isp62m2sgvx25yyj3kpc2myh85w24bki",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#winapi@0.3.9": "06gl025x418lchw1wxj64ycr7gha83m44cjr5sarhynd9xkrm0sw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows-core@0.52.0": "1nc3qv7sy24x0nlnb32f7alzpd6f72l4p24vl65vydbyil669ark",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows-core@0.51.1": "0r1f57hsshsghjyc7ypp2s0i78f7b1vr93w68sdb8baxyf2czy7i",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.48.0": "1aan23v5gs7gya1lc46hqn9mdh8yph3fhxmhxlw36pn6pqc28zb7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.52.0": "0gd3v4ji88490zgb6b5mq5zgbvwv7zx1ibn8v3x83rwcdbryaar8",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.59.0": "0fw5672ziw8b3zpmnbp9pdv1famk74f1l9fcbc3zsrzdg56vqf0y",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.48.5": "034ljxqshifs1lan89xwpcy1hp0lhdh4b5n0d2z4fwjx2piacbws",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.52.6": "0wwrx625nwlfp7k93r2rra568gad1mwd888h1jwnl0vfg5r4ywlv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.52.0": "1kg7a27ynzw8zz3krdgy6w5gbqcji27j1sz4p7xk2j5j8082064a",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.48.5": "1n05v7qblg1ci3i567inc7xrkmywczxrs1z3lj3rkkxw18py6f1b",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.52.6": "1lrcq38cr2arvmz19v32qaggvj8bh1640mdm9c2fr877h0hn591j",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.52.0": "1shmn1kbdc0bpphcxz0vlph96bxz0h1jlmh93s9agf2dbpin8xyb",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.48.5": "1g5l4ry968p73g6bg6jgyvy9lb8fyhcs54067yzxpcpkf44k2dfw",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.52.6": "0sfl0nysnz32yyfh773hpi49b1q700ah6y7sacmjbqjjn5xjmv09",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.52.0": "1vvmy1ypvzdvxn9yf0b8ygfl85gl2gpcyvsvqppsmlpisil07amv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.48.5": "0gklnglwd9ilqx7ac3cn8hbhkraqisd0n83jxzf9837nvvkiand7",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.52.6": "02zspglbykh1jh9pi7gn8g1f97jh1rrccni9ivmrfbl0mgamm6wf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnullvm@0.52.6": "0rpdx1537mw6slcpqa0rm3qixmsb79nbhqy5fsm3q2q9ik9m5vhf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.52.0": "04zkglz4p3pjsns5gbz85v4s5aw102raz4spj4b0lmm33z5kg1m2",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.48.5": "01m4rik437dl9rdf0ndnm2syh10hizvq0dajdkv2fjqcywrw4mcg",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.52.6": "0rkcqmp4zzmfvrrrx01260q3xkpzi6fzi2x2pgdcdry50ny4h294",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.52.0": "16kvmbvx0vr0zbgnaz6nsks9ycvfh5xp05bjrhq65kj623iyirgz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.48.5": "13kiqqcvz2vnyxzydjh73hwgigsdr2z1xpzx313kxll34nyhmm2k",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.52.6": "0y0sifqcb56a56mvn7xjgs8g43p33mfqkd8wj1yhrgxzma05qyhl",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.52.0": "1zdy4qn178sil5sdm63lm7f0kkcjg6gvdwmcprd2yjmwn8ns6vrx",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.48.5": "1k24810wfbgz8k48c2yknqjmiigmql6kk3knmddkv8k8g1v54yqb",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.52.6": "03gda7zjx1qh8k9nnlgb7m3w3s1xkysg55hkd1wjch8pqhyv5m94",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.52.0": "17lllq4l2k1lqgcnw1cccphxp9vs7inq99kjlm2lfl9zklg7wr8s",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.48.5": "0f4mdp895kkjh9zv8dxvn4pc10xr7839lf5pa9l0193i2pkgr57d",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.52.6": "1v7rb5cibyzx8vak29pdrk8nx9hycsjs4w0jgms08qk49jl6v7sq",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#winnow@0.5.40": "0xk8maai7gyxda673mmw3pj1hdizy5fpi7287vaywykkk19sk4zm",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.52.0": "012wfq37f18c09ij5m6rniw7xxn5fcvrxbqd0wd8vgnl3hfn9yfz",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#winnow@0.5.30": "1ifj9vnqna5qp0d7nb9mrinzf8j7zi1m0gv75870vm91jyw3sp4v",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#winreg@0.50.0": "1cddmp929k882mdh6i9f2as848f13qqna6czwsqzkh1pqnr5fkjj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#yansi-term@0.1.2": "1w8vjlvxba6yvidqdvxddx3crl6z66h39qxj8xi6aqayw2nk0p7y",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.7.35": "0gnf2ap2y92nwdalzz3x7142f2b83sni66l39vxp2ijd6j080kzs",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.7.35": "1w36q7b9il2flg0qskapgi9ymgg7p985vniqd09vi0mwib8lz6qv",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.1": "1pjdrmjwmszpxfd7r860jx54cyk94qk59x13sc307cvr5256glyf",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.7.31": "06k0zk4x4n9s1blgxmxqb1g81y8q334aayx61gyy6v9y1dajkhdk",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.7.31": "0gcfyrmlrhmsz16qxjp2qzr6vixyaw1p04zl28f08lxkvfz62h0w",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zeroize@1.7.0": "0bfvby7k9pdp6623p98yz2irqnamcyzpn7zh20nqmdn68b0lwnsj",
|
||||
"registry+https://github.com/rust-lang/crates.io-index#zune-inflate@0.2.54": "00kg24jh3zqa3i6rg6yksnb71bch9yi1casqydl00s7nw8pk7avk"
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
[package]
|
||||
name = "cyber-slides"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-std = "1.13.0"
|
||||
cairo-rs = "0.18"
|
||||
cyberpunk = { path = "../cyberpunk" }
|
||||
gio = "0.18"
|
||||
glib = "0.18"
|
||||
gtk = { version = "0.7", package = "gtk4" }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_yml = "0.0.12"
|
|
@ -1,416 +0,0 @@
|
|||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::Read,
|
||||
ops::Index,
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::{Arc, RwLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use cairo::{Context, Rectangle};
|
||||
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, Text};
|
||||
use glib::{GString, Object};
|
||||
use gtk::{
|
||||
glib::{self, Propagation},
|
||||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
EventControllerKey,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const FPS: u64 = 60;
|
||||
const PURPLE: (f64, f64, f64) = (0.7, 0., 1.);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum Position {
|
||||
Top,
|
||||
Middle,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Step {
|
||||
text: String,
|
||||
position: Position,
|
||||
transition: Duration,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Script(Vec<Step>);
|
||||
|
||||
impl Script {
|
||||
fn from_file(path: &Path) -> Result<Script, serde_yml::Error> {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
let mut f = File::open(path).unwrap();
|
||||
f.read_to_end(&mut buf).unwrap();
|
||||
let script = serde_yml::from_slice(&buf)?;
|
||||
Ok(Self(script))
|
||||
}
|
||||
|
||||
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a Step> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Script {
|
||||
fn default() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<usize> for Script {
|
||||
type Output = Step;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
struct Fade {
|
||||
text: String,
|
||||
position: Position,
|
||||
duration: Duration,
|
||||
|
||||
start_time: Instant,
|
||||
}
|
||||
|
||||
trait Animation {
|
||||
fn position(&self) -> Position;
|
||||
|
||||
fn tick(&self, now: Instant, context: &Context, width: f64);
|
||||
}
|
||||
|
||||
impl Animation for Fade {
|
||||
fn position(&self) -> Position {
|
||||
self.position.clone()
|
||||
}
|
||||
|
||||
fn tick(&self, now: Instant, context: &Context, width: f64) {
|
||||
let total_frames = self.duration.as_secs() * FPS;
|
||||
let alpha_rate: f64 = 1. / total_frames as f64;
|
||||
|
||||
let frames = (now - self.start_time).as_secs_f64() * FPS as f64;
|
||||
let alpha = alpha_rate * frames as f64;
|
||||
|
||||
let text_display = Text::new(self.text.clone(), context, 64., width);
|
||||
let _ = context.move_to(0., text_display.extents().height());
|
||||
let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha);
|
||||
text_display.draw();
|
||||
}
|
||||
}
|
||||
|
||||
struct CrossFade {
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
position: Position,
|
||||
duration: Duration,
|
||||
|
||||
start_time: Instant,
|
||||
}
|
||||
|
||||
impl Animation for CrossFade {
|
||||
fn position(&self) -> Position {
|
||||
self.position.clone()
|
||||
}
|
||||
|
||||
fn tick(&self, now: Instant, context: &Context, width: f64) {
|
||||
let total_frames = self.duration.as_secs() * FPS;
|
||||
let alpha_rate: f64 = 1. / total_frames as f64;
|
||||
|
||||
let frames = (now - self.start_time).as_secs_f64() * FPS as f64;
|
||||
let alpha = alpha_rate * frames as f64;
|
||||
|
||||
let text_display = Text::new(self.old_text.clone(), context, 64., width);
|
||||
let _ = context.move_to(0., text_display.extents().height());
|
||||
let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, 1. - alpha);
|
||||
text_display.draw();
|
||||
|
||||
let text_display = Text::new(self.new_text.clone(), context, 64., width);
|
||||
let _ = context.move_to(0., text_display.extents().height());
|
||||
let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha);
|
||||
text_display.draw();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CyberScreenState {
|
||||
script: Script,
|
||||
idx: Option<usize>,
|
||||
top: Option<Step>,
|
||||
middle: Option<Step>,
|
||||
bottom: Option<Step>,
|
||||
}
|
||||
|
||||
impl Default for CyberScreenState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
script: Script(vec![]),
|
||||
idx: None,
|
||||
top: None,
|
||||
middle: None,
|
||||
bottom: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CyberScreenState {
|
||||
fn new(script: Script) -> CyberScreenState {
|
||||
let mut s = CyberScreenState::default();
|
||||
s.script = script;
|
||||
s
|
||||
}
|
||||
|
||||
fn next_page(&mut self) -> Box<dyn Animation> {
|
||||
let idx = match self.idx {
|
||||
None => 0,
|
||||
Some(idx) => {
|
||||
if idx < self.script.len() {
|
||||
idx + 1
|
||||
} else {
|
||||
idx
|
||||
}
|
||||
}
|
||||
};
|
||||
self.idx = Some(idx);
|
||||
let step = self.script[idx].clone();
|
||||
|
||||
let (old, new) = match step.position {
|
||||
Position::Top => {
|
||||
let old = self.top.replace(step.clone());
|
||||
(old, step)
|
||||
}
|
||||
Position::Middle => {
|
||||
let old = self.middle.replace(step.clone());
|
||||
(old, step)
|
||||
}
|
||||
Position::Bottom => {
|
||||
let old = self.bottom.replace(step.clone());
|
||||
(old, step)
|
||||
}
|
||||
};
|
||||
|
||||
match old {
|
||||
Some(old) => Box::new(CrossFade {
|
||||
old_text: old.text.clone(),
|
||||
new_text: new.text.clone(),
|
||||
position: new.position,
|
||||
duration: new.transition,
|
||||
start_time: Instant::now(),
|
||||
}),
|
||||
None => Box::new(Fade {
|
||||
text: new.text.clone(),
|
||||
position: new.position,
|
||||
duration: new.transition,
|
||||
start_time: Instant::now(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CyberScreenPrivate {
|
||||
state: Rc<RefCell<CyberScreenState>>,
|
||||
// For crossfading to work, I have to detect that there is an old animation in a position, and
|
||||
// replace it with the new one.
|
||||
animations: Rc<RefCell<HashMap<Position, Box<dyn Animation>>>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for CyberScreenPrivate {
|
||||
const NAME: &'static str = "CyberScreen";
|
||||
type Type = CyberScreen;
|
||||
type ParentType = gtk::DrawingArea;
|
||||
}
|
||||
|
||||
impl ObjectImpl for CyberScreenPrivate {}
|
||||
impl WidgetImpl for CyberScreenPrivate {}
|
||||
impl DrawingAreaImpl for CyberScreenPrivate {}
|
||||
|
||||
impl CyberScreenPrivate {
|
||||
fn set_script(&self, script: Script) {
|
||||
*self.state.borrow_mut() = CyberScreenState::new(script);
|
||||
}
|
||||
|
||||
fn next_page(&self) {
|
||||
let transition = self.state.borrow_mut().next_page();
|
||||
self.animations
|
||||
.borrow_mut()
|
||||
.insert(transition.position(), transition);
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct CyberScreen(ObjectSubclass<CyberScreenPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||
}
|
||||
|
||||
impl CyberScreen {
|
||||
fn new(script: Script) -> Self {
|
||||
let s: Self = Object::builder().build();
|
||||
s.imp().set_script(script);
|
||||
|
||||
s.set_draw_func({
|
||||
let s = s.clone();
|
||||
move |_, context, width, height| {
|
||||
let now = Instant::now();
|
||||
let _ = context.set_source_rgb(0., 0., 0.);
|
||||
let _ = context.paint();
|
||||
|
||||
let pen = GlowPen::new(width, height, 2., 8., (0.7, 0., 1.));
|
||||
AsymLineCutout {
|
||||
orientation: gtk::Orientation::Horizontal,
|
||||
start_x: 25.,
|
||||
start_y: height as f64 / 7.,
|
||||
start_length: width as f64 / 3.,
|
||||
cutout_length: width as f64 / 3. - 100.,
|
||||
height: 50.,
|
||||
end_length: width as f64 / 3. - 50.,
|
||||
invert: false,
|
||||
}
|
||||
.draw(&pen);
|
||||
pen.stroke();
|
||||
|
||||
AsymLine {
|
||||
orientation: gtk::Orientation::Horizontal,
|
||||
start_x: width as f64 / 4.,
|
||||
start_y: height as f64 * 6. / 7.,
|
||||
start_length: width as f64 * 2. / 3. - 25.,
|
||||
height: 50.,
|
||||
end_length: 0.,
|
||||
invert: false,
|
||||
}
|
||||
.draw(&pen);
|
||||
pen.stroke();
|
||||
|
||||
let tracery = pen.finish();
|
||||
let _ = context.set_source(tracery);
|
||||
let _ = context.paint();
|
||||
|
||||
let mut animations = s.imp().animations.borrow_mut();
|
||||
|
||||
let lr_margin = 50.;
|
||||
let max_width = width as f64 - lr_margin * 2.;
|
||||
let region_height = height as f64 / 5.;
|
||||
|
||||
if let Some(animation) = animations.get(&Position::Top) {
|
||||
let y = height as f64 * 1. / 5.;
|
||||
let surface = context
|
||||
.target()
|
||||
.create_for_rectangle(Rectangle::new(20., y, max_width, region_height))
|
||||
.unwrap();
|
||||
let ctx = Context::new(&surface).unwrap();
|
||||
animation.tick(now, &ctx, max_width);
|
||||
}
|
||||
if let Some(animation) = animations.get(&Position::Middle) {
|
||||
let y = height as f64 * 2. / 5.;
|
||||
let surface = context
|
||||
.target()
|
||||
.create_for_rectangle(Rectangle::new(20., y, max_width, region_height))
|
||||
.unwrap();
|
||||
let ctx = Context::new(&surface).unwrap();
|
||||
animation.tick(now, &ctx, max_width);
|
||||
}
|
||||
if let Some(animation) = animations.get(&Position::Bottom) {
|
||||
let y = height as f64 * 3. / 5.;
|
||||
let surface = context
|
||||
.target()
|
||||
.create_for_rectangle(Rectangle::new(20., y, max_width, region_height))
|
||||
.unwrap();
|
||||
let ctx = Context::new(&surface).unwrap();
|
||||
animation.tick(now, &ctx, max_width);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
fn next_page(&self) {
|
||||
self.imp().next_page();
|
||||
self.queue_draw();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let script = Arc::new(RwLock::new(Script::default()));
|
||||
let app = gtk::Application::builder()
|
||||
.application_id("com.luminescent-dreams.cyberpunk-slideshow")
|
||||
.build();
|
||||
|
||||
app.add_main_option(
|
||||
"script",
|
||||
glib::char::Char::from(b's'),
|
||||
glib::OptionFlags::IN_MAIN,
|
||||
glib::OptionArg::String,
|
||||
"",
|
||||
None,
|
||||
);
|
||||
|
||||
app.connect_handle_local_options({
|
||||
let script = script.clone();
|
||||
move |_, options| {
|
||||
if let Some(script_path) = options.lookup::<String>("script").unwrap() {
|
||||
let mut script = script.write().unwrap();
|
||||
*script = Script::from_file(Path::new(&script_path)).unwrap();
|
||||
-1
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.connect_activate(move |app| {
|
||||
let window = gtk::ApplicationWindow::new(app);
|
||||
let screen = CyberScreen::new(script.read().unwrap().clone());
|
||||
|
||||
let events = EventControllerKey::new();
|
||||
|
||||
events.connect_key_released({
|
||||
let app = app.clone();
|
||||
let window = window.clone();
|
||||
let screen = screen.clone();
|
||||
move |_, key, _, _| {
|
||||
let name = key
|
||||
.name()
|
||||
.map(|s| s.as_str().to_owned())
|
||||
.unwrap_or("".to_owned());
|
||||
match name.as_ref() {
|
||||
"Right" => screen.next_page(),
|
||||
"q" => app.quit(),
|
||||
"Escape" => window.unfullscreen(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.add_controller(events);
|
||||
|
||||
window.set_child(Some(&screen));
|
||||
window.set_width_request(800);
|
||||
window.set_height_request(600);
|
||||
window.present();
|
||||
|
||||
window.connect_maximized_notify(|window| {
|
||||
window.fullscreen();
|
||||
});
|
||||
|
||||
let _ = glib::spawn_future_local({
|
||||
let screen = screen.clone();
|
||||
async move {
|
||||
loop {
|
||||
screen.queue_draw();
|
||||
async_std::task::sleep(Duration::from_millis(1000 / FPS)).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.run();
|
||||
}
|
|
@ -8,7 +8,6 @@ license = "GPL-3.0-only"
|
|||
|
||||
[dependencies]
|
||||
cairo-rs = { version = "0.18" }
|
||||
cyberpunk = { path = "../cyberpunk" }
|
||||
gio = { version = "0.18" }
|
||||
glib = { version = "0.18" }
|
||||
gtk = { version = "0.7", package = "gtk4" }
|
||||
|
|
|
@ -2,7 +2,6 @@ use cairo::{
|
|||
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern,
|
||||
TextExtents,
|
||||
};
|
||||
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, SlashMeter};
|
||||
use glib::Object;
|
||||
use gtk::{prelude::*, subclass::prelude::*, EventControllerKey};
|
||||
use std::{
|
||||
|
@ -172,7 +171,7 @@ impl SplashPrivate {
|
|||
start_y: extents.height() + 10.,
|
||||
start_length: 0.,
|
||||
height: extents.height() / 2.,
|
||||
end_length: 0.,
|
||||
total_length: extents.width() + extents.height() / 2.,
|
||||
invert: false,
|
||||
}
|
||||
.draw(&pen);
|
||||
|
@ -184,7 +183,7 @@ impl SplashPrivate {
|
|||
start_y: extents.height() + 60.,
|
||||
start_length: extents.width(),
|
||||
height: extents.height() / 2.,
|
||||
end_length: 0.,
|
||||
total_length: extents.width() + extents.height() / 2.,
|
||||
invert: false,
|
||||
}
|
||||
.draw(&pen);
|
||||
|
@ -209,7 +208,7 @@ impl SplashPrivate {
|
|||
start_x: 20.,
|
||||
start_y: center_y - 20. - title_height / 2.,
|
||||
start_length,
|
||||
end_length: *self.width.borrow() as f64 - 120. - start_length,
|
||||
total_length: *self.width.borrow() as f64 - 120.,
|
||||
cutout_length: title_width,
|
||||
height: title_height,
|
||||
invert: false,
|
||||
|
@ -244,7 +243,7 @@ impl SplashPrivate {
|
|||
start_y: *self.height.borrow() as f64 / 2. + 100.,
|
||||
start_length: 400.,
|
||||
height: 50.,
|
||||
end_length: 0.,
|
||||
total_length: 650.,
|
||||
invert: true,
|
||||
}
|
||||
.draw(&pen);
|
||||
|
@ -259,7 +258,7 @@ impl SplashPrivate {
|
|||
start_y: *self.height.borrow() as f64 / 2. + 200.,
|
||||
start_length: 600.,
|
||||
height: 50.,
|
||||
end_length: 0.,
|
||||
total_length: 650.,
|
||||
invert: false,
|
||||
}
|
||||
.draw(&pen);
|
||||
|
@ -420,6 +419,212 @@ impl Splash {
|
|||
}
|
||||
}
|
||||
|
||||
struct AsymLineCutout {
|
||||
orientation: gtk::Orientation,
|
||||
start_x: f64,
|
||||
start_y: f64,
|
||||
start_length: f64,
|
||||
total_length: f64,
|
||||
cutout_length: f64,
|
||||
height: f64,
|
||||
invert: bool,
|
||||
}
|
||||
|
||||
impl AsymLineCutout {
|
||||
fn draw(&self, pen: &impl Pen) {
|
||||
let dodge = if self.invert {
|
||||
self.height
|
||||
} else {
|
||||
-self.height
|
||||
};
|
||||
match self.orientation {
|
||||
gtk::Orientation::Horizontal => {
|
||||
pen.move_to(self.start_x, self.start_y);
|
||||
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||
pen.line_to(
|
||||
self.start_x + self.start_length + self.height,
|
||||
self.start_y + dodge,
|
||||
);
|
||||
pen.line_to(
|
||||
self.start_x + self.start_length + self.height + self.cutout_length,
|
||||
self.start_y + dodge,
|
||||
);
|
||||
pen.line_to(
|
||||
self.start_x
|
||||
+ self.start_length
|
||||
+ self.height
|
||||
+ self.cutout_length
|
||||
+ (self.height / 2.),
|
||||
self.start_y + dodge / 2.,
|
||||
);
|
||||
pen.line_to(self.total_length, self.start_y + dodge / 2.);
|
||||
}
|
||||
gtk::Orientation::Vertical => {
|
||||
pen.move_to(self.start_x, self.start_y);
|
||||
pen.line_to(self.start_x, self.start_y + self.start_length);
|
||||
pen.line_to(
|
||||
self.start_x + dodge,
|
||||
self.start_y + self.start_length + self.height,
|
||||
);
|
||||
pen.line_to(
|
||||
self.start_x + dodge,
|
||||
self.start_y + self.start_length + self.height + self.cutout_length,
|
||||
);
|
||||
pen.line_to(
|
||||
self.start_x + dodge / 2.,
|
||||
self.start_y
|
||||
+ self.start_length
|
||||
+ self.height
|
||||
+ self.cutout_length
|
||||
+ (self.height / 2.),
|
||||
);
|
||||
pen.line_to(self.start_x + dodge / 2., self.total_length);
|
||||
}
|
||||
_ => panic!("unknown orientation"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AsymLine {
|
||||
orientation: gtk::Orientation,
|
||||
start_x: f64,
|
||||
start_y: f64,
|
||||
start_length: f64,
|
||||
height: f64,
|
||||
total_length: f64,
|
||||
invert: bool,
|
||||
}
|
||||
|
||||
impl AsymLine {
|
||||
fn draw(&self, pen: &impl Pen) {
|
||||
let dodge = if self.invert {
|
||||
self.height
|
||||
} else {
|
||||
-self.height
|
||||
};
|
||||
match self.orientation {
|
||||
gtk::Orientation::Horizontal => {
|
||||
pen.move_to(self.start_x, self.start_y);
|
||||
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||
pen.line_to(
|
||||
self.start_x + self.start_length + self.height,
|
||||
self.start_y + dodge,
|
||||
);
|
||||
pen.line_to(self.start_x + self.total_length, self.start_y + dodge);
|
||||
}
|
||||
gtk::Orientation::Vertical => {}
|
||||
_ => panic!("unknown orientation"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SlashMeter {
|
||||
orientation: gtk::Orientation,
|
||||
start_x: f64,
|
||||
start_y: f64,
|
||||
count: u8,
|
||||
fill_count: u8,
|
||||
height: f64,
|
||||
length: f64,
|
||||
}
|
||||
|
||||
impl SlashMeter {
|
||||
fn draw(&self, context: &Context) {
|
||||
match self.orientation {
|
||||
gtk::Orientation::Horizontal => {
|
||||
let angle: f64 = 0.8;
|
||||
let run = self.height / angle.tan();
|
||||
let width = self.length / (self.count as f64 * 2.);
|
||||
|
||||
for c in 0..self.count {
|
||||
context.set_line_width(1.);
|
||||
|
||||
let start_x = self.start_x + c as f64 * width * 2.;
|
||||
context.move_to(start_x, self.start_y);
|
||||
context.line_to(start_x + run, self.start_y - self.height);
|
||||
context.line_to(start_x + run + width, self.start_y - self.height);
|
||||
context.line_to(start_x + width, self.start_y);
|
||||
context.line_to(start_x, self.start_y);
|
||||
if c < self.fill_count {
|
||||
let _ = context.fill();
|
||||
} else {
|
||||
let _ = context.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
gtk::Orientation::Vertical => {}
|
||||
_ => panic!("unknown orientation"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait Pen {
|
||||
fn move_to(&self, x: f64, y: f64);
|
||||
fn line_to(&self, x: f64, y: f64);
|
||||
fn stroke(&self);
|
||||
|
||||
fn finish(self) -> Pattern;
|
||||
}
|
||||
|
||||
struct GlowPen {
|
||||
blur_context: Context,
|
||||
draw_context: Context,
|
||||
}
|
||||
|
||||
impl GlowPen {
|
||||
fn new(
|
||||
width: i32,
|
||||
height: i32,
|
||||
line_width: f64,
|
||||
blur_line_width: f64,
|
||||
color: (f64, f64, f64),
|
||||
) -> Self {
|
||||
let blur_context =
|
||||
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
||||
blur_context.set_line_width(blur_line_width);
|
||||
blur_context.set_source_rgba(color.0, color.1, color.2, 0.5);
|
||||
blur_context.push_group();
|
||||
blur_context.set_line_cap(LineCap::Round);
|
||||
|
||||
let draw_context =
|
||||
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
||||
draw_context.set_line_width(line_width);
|
||||
draw_context.set_source_rgb(color.0, color.1, color.2);
|
||||
draw_context.push_group();
|
||||
draw_context.set_line_cap(LineCap::Round);
|
||||
|
||||
Self {
|
||||
blur_context,
|
||||
draw_context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pen for GlowPen {
|
||||
fn move_to(&self, x: f64, y: f64) {
|
||||
self.blur_context.move_to(x, y);
|
||||
self.draw_context.move_to(x, y);
|
||||
}
|
||||
|
||||
fn line_to(&self, x: f64, y: f64) {
|
||||
self.blur_context.line_to(x, y);
|
||||
self.draw_context.line_to(x, y);
|
||||
}
|
||||
|
||||
fn stroke(&self) {
|
||||
self.blur_context.stroke().expect("to draw the blur line");
|
||||
self.draw_context
|
||||
.stroke()
|
||||
.expect("to draw the regular line");
|
||||
}
|
||||
|
||||
fn finish(self) -> Pattern {
|
||||
let foreground = self.draw_context.pop_group().unwrap();
|
||||
self.blur_context.set_source(foreground).unwrap();
|
||||
self.blur_context.paint().unwrap();
|
||||
self.blur_context.pop_group().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let app = gtk::Application::builder()
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
[package]
|
||||
name = "cyberpunk"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cairo-rs = { version = "0.18" }
|
||||
gio = { version = "0.18" }
|
||||
glib = { version = "0.18" }
|
||||
gtk = { version = "0.7", package = "gtk4" }
|
|
@ -1,301 +0,0 @@
|
|||
use cairo::{
|
||||
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, Pattern,
|
||||
TextExtents,
|
||||
};
|
||||
|
||||
pub struct AsymLineCutout {
|
||||
pub orientation: gtk::Orientation,
|
||||
pub start_x: f64,
|
||||
pub start_y: f64,
|
||||
pub start_length: f64,
|
||||
pub cutout_length: f64,
|
||||
pub end_length: f64,
|
||||
pub height: f64,
|
||||
pub invert: bool,
|
||||
}
|
||||
|
||||
impl AsymLineCutout {
|
||||
pub fn draw(&self, pen: &impl Pen) {
|
||||
let dodge = if self.invert {
|
||||
self.height
|
||||
} else {
|
||||
-self.height
|
||||
};
|
||||
match self.orientation {
|
||||
gtk::Orientation::Horizontal => {
|
||||
pen.move_to(self.start_x, self.start_y);
|
||||
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||
pen.line_to(
|
||||
self.start_x + self.start_length + self.height,
|
||||
self.start_y + dodge,
|
||||
);
|
||||
pen.line_to(
|
||||
self.start_x + self.start_length + self.height + self.cutout_length,
|
||||
self.start_y + dodge,
|
||||
);
|
||||
pen.line_to(
|
||||
self.start_x
|
||||
+ self.start_length
|
||||
+ self.height
|
||||
+ self.cutout_length
|
||||
+ (self.height / 2.),
|
||||
self.start_y + dodge / 2.,
|
||||
);
|
||||
pen.line_to(
|
||||
self.start_x
|
||||
+ self.start_length
|
||||
+ self.height
|
||||
+ self.cutout_length
|
||||
+ (self.height / 2.)
|
||||
+ self.end_length,
|
||||
self.start_y + dodge / 2.,
|
||||
);
|
||||
}
|
||||
gtk::Orientation::Vertical => {
|
||||
pen.move_to(self.start_x, self.start_y);
|
||||
pen.line_to(self.start_x, self.start_y + self.start_length);
|
||||
pen.line_to(
|
||||
self.start_x + dodge,
|
||||
self.start_y + self.start_length + self.height,
|
||||
);
|
||||
pen.line_to(
|
||||
self.start_x + dodge,
|
||||
self.start_y + self.start_length + self.height + self.cutout_length,
|
||||
);
|
||||
pen.line_to(
|
||||
self.start_x + dodge / 2.,
|
||||
self.start_y
|
||||
+ self.start_length
|
||||
+ self.height
|
||||
+ self.cutout_length
|
||||
+ (self.height / 2.),
|
||||
);
|
||||
pen.line_to(
|
||||
self.start_x + dodge / 2.,
|
||||
self.start_y
|
||||
+ self.start_length
|
||||
+ self.height
|
||||
+ self.cutout_length
|
||||
+ (self.height / 2.)
|
||||
+ self.end_length,
|
||||
);
|
||||
}
|
||||
_ => panic!("unknown orientation"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Represents an asymetrical line that starts at one location, then a 45-degree angle and then
|
||||
// another line afterwards.
|
||||
pub struct AsymLine {
|
||||
// Will this be drawn left-to-right or up-to-down?
|
||||
pub orientation: gtk::Orientation,
|
||||
|
||||
// Starting address
|
||||
pub start_x: f64,
|
||||
pub start_y: f64,
|
||||
|
||||
// Length of the first segment
|
||||
pub start_length: f64,
|
||||
|
||||
// Height to dodge over to the next section
|
||||
pub height: f64,
|
||||
|
||||
// Total length of the entire line.
|
||||
pub end_length: f64,
|
||||
|
||||
// When normal, the angle dodge is upwards. When inverted, the angle dodge is downwards.
|
||||
pub invert: bool,
|
||||
}
|
||||
|
||||
impl AsymLine {
|
||||
pub fn draw(&self, pen: &impl Pen) {
|
||||
let dodge = if self.invert {
|
||||
self.height
|
||||
} else {
|
||||
-self.height
|
||||
};
|
||||
match self.orientation {
|
||||
gtk::Orientation::Horizontal => {
|
||||
pen.move_to(self.start_x, self.start_y);
|
||||
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||
pen.line_to(
|
||||
self.start_x + self.start_length + self.height,
|
||||
self.start_y + dodge,
|
||||
);
|
||||
pen.line_to(
|
||||
self.start_x + self.start_length + self.height + self.end_length,
|
||||
self.start_y + dodge,
|
||||
);
|
||||
}
|
||||
gtk::Orientation::Vertical => {}
|
||||
_ => panic!("unknown orientation"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SlashMeter {
|
||||
pub orientation: gtk::Orientation,
|
||||
pub start_x: f64,
|
||||
pub start_y: f64,
|
||||
pub count: u8,
|
||||
pub fill_count: u8,
|
||||
pub height: f64,
|
||||
pub length: f64,
|
||||
}
|
||||
|
||||
impl SlashMeter {
|
||||
pub fn draw(&self, context: &Context) {
|
||||
match self.orientation {
|
||||
gtk::Orientation::Horizontal => {
|
||||
let angle: f64 = 0.8;
|
||||
let run = self.height / angle.tan();
|
||||
let width = self.length / (self.count as f64 * 2.);
|
||||
|
||||
for c in 0..self.count {
|
||||
context.set_line_width(1.);
|
||||
|
||||
let start_x = self.start_x + c as f64 * width * 2.;
|
||||
context.move_to(start_x, self.start_y);
|
||||
context.line_to(start_x + run, self.start_y - self.height);
|
||||
context.line_to(start_x + run + width, self.start_y - self.height);
|
||||
context.line_to(start_x + width, self.start_y);
|
||||
context.line_to(start_x, self.start_y);
|
||||
if c < self.fill_count {
|
||||
let _ = context.fill();
|
||||
} else {
|
||||
let _ = context.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
gtk::Orientation::Vertical => {}
|
||||
_ => panic!("unknown orientation"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a pen for drawing a pattern. This is good for complex patterns that may require
|
||||
/// multiple identical steps.
|
||||
pub trait Pen {
|
||||
/// Move the pen to a location.
|
||||
fn move_to(&self, x: f64, y: f64);
|
||||
|
||||
/// Draw a line from the current location to the specified destination.
|
||||
fn line_to(&self, x: f64, y: f64);
|
||||
|
||||
/// Instantiate the line.
|
||||
fn stroke(&self);
|
||||
|
||||
/// Convert all of the drawing into a pattern that can be painted to a drawing context.
|
||||
fn finish(self) -> Pattern;
|
||||
}
|
||||
|
||||
pub struct GlowPen {
|
||||
blur_context: Context,
|
||||
draw_context: Context,
|
||||
}
|
||||
|
||||
impl GlowPen {
|
||||
pub fn new(
|
||||
width: i32,
|
||||
height: i32,
|
||||
line_width: f64,
|
||||
blur_line_width: f64,
|
||||
color: (f64, f64, f64),
|
||||
) -> Self {
|
||||
let blur_context =
|
||||
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
||||
blur_context.set_line_width(blur_line_width);
|
||||
blur_context.set_source_rgba(color.0, color.1, color.2, 0.5);
|
||||
blur_context.push_group();
|
||||
blur_context.set_line_cap(LineCap::Round);
|
||||
|
||||
let draw_context =
|
||||
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
||||
draw_context.set_line_width(line_width);
|
||||
draw_context.set_source_rgb(color.0, color.1, color.2);
|
||||
draw_context.push_group();
|
||||
draw_context.set_line_cap(LineCap::Round);
|
||||
|
||||
Self {
|
||||
blur_context,
|
||||
draw_context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pen for GlowPen {
|
||||
fn move_to(&self, x: f64, y: f64) {
|
||||
self.blur_context.move_to(x, y);
|
||||
self.draw_context.move_to(x, y);
|
||||
}
|
||||
|
||||
fn line_to(&self, x: f64, y: f64) {
|
||||
self.blur_context.line_to(x, y);
|
||||
self.draw_context.line_to(x, y);
|
||||
}
|
||||
|
||||
fn stroke(&self) {
|
||||
self.blur_context.stroke().expect("to draw the blur line");
|
||||
self.draw_context
|
||||
.stroke()
|
||||
.expect("to draw the regular line");
|
||||
}
|
||||
|
||||
fn finish(self) -> Pattern {
|
||||
let foreground = self.draw_context.pop_group().unwrap();
|
||||
self.blur_context.set_source(foreground).unwrap();
|
||||
self.blur_context.paint().unwrap();
|
||||
self.blur_context.pop_group().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Text<'a> {
|
||||
content: Vec<String>,
|
||||
context: &'a Context,
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
pub fn new(content: String, context: &'a Context, size: f64, width: f64) -> Self {
|
||||
context.select_font_face("Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold);
|
||||
context.set_font_size(size);
|
||||
|
||||
let lines = word_wrap(content, context, width);
|
||||
|
||||
Self { content: lines, context }
|
||||
}
|
||||
|
||||
pub fn extents(&self) -> TextExtents {
|
||||
self.context.text_extents(&self.content[0]).unwrap()
|
||||
}
|
||||
|
||||
pub fn draw(&self) {
|
||||
let mut baseline = 0.;
|
||||
for line in self.content.iter() {
|
||||
baseline += self.context.text_extents(line).unwrap().height() + 10.;
|
||||
self.context.move_to(0., baseline);
|
||||
let _ = self.context.show_text(&line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn word_wrap(content: String, context: &Context, max_width: f64) -> Vec<String> {
|
||||
let mut lines = vec![];
|
||||
let words: Vec<&str> = content.split_whitespace().collect();
|
||||
let mut start: usize = 0;
|
||||
let mut line = String::new();
|
||||
|
||||
for idx in 0..words.len() + 1 {
|
||||
line = words[start..idx].join(" ");
|
||||
let extents = context.text_extents(&line).unwrap();
|
||||
if extents.width() > max_width {
|
||||
let line = words[start..idx-1].join(" ");
|
||||
start = idx-1;
|
||||
lines.push(line.clone());
|
||||
}
|
||||
}
|
||||
if line.len() > 0 {
|
||||
lines.push(line);
|
||||
}
|
||||
lines
|
||||
}
|
18
flake.lock
18
flake.lock
|
@ -5,11 +5,11 @@
|
|||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -35,11 +35,11 @@
|
|||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1714906307,
|
||||
"narHash": "sha256-UlRZtrCnhPFSJlDQE7M0eyhgvuuHBTe1eJ9N9AQlJQ0=",
|
||||
"lastModified": 1681303793,
|
||||
"narHash": "sha256-JEdQHsYuCfRL2PICHlOiH/2ue3DwoxUX7DJ6zZxZXFk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "25865a40d14b3f9cf19f19b924e2ab4069b09588",
|
||||
"rev": "fe2ecaf706a5907b5e54d979fbde4924d84b65fc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -76,11 +76,11 @@
|
|||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731966246,
|
||||
"narHash": "sha256-e/V7Ffm5wPd9DVzCThnPZ7lFxd43bb64tSk8/oGP4Ag=",
|
||||
"lastModified": 1698205128,
|
||||
"narHash": "sha256-jP+81TkldLtda8bzmsBWahETGsyFkoDOCT244YkA+S4=",
|
||||
"owner": "1Password",
|
||||
"repo": "typeshare",
|
||||
"rev": "e0e5f27ee34d7d4da76a9dc96a11552e98be56da",
|
||||
"rev": "c3ee2ad8f27774c45db7af4f2ba746c4ae11de21",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -34,17 +34,17 @@
|
|||
pkgs.libadwaita
|
||||
pkgs.librsvg
|
||||
pkgs.nodejs
|
||||
pkgs.nodePackages.eslint
|
||||
pkgs.nodePackages.typescript
|
||||
pkgs.nodePackages.typescript-language-server
|
||||
pkgs.openssl
|
||||
pkgs.pipewire
|
||||
pkgs.pkg-config
|
||||
pkgs.rustup
|
||||
pkgs.sqlite
|
||||
pkgs.cargo-nextest
|
||||
pkgs.wasm-pack
|
||||
pkgs.sqlx-cli
|
||||
pkgs.udev
|
||||
pkgs.wasm-pack
|
||||
pkgs.go-task
|
||||
typeshare.packages."x86_64-linux".default
|
||||
pkgs.nodePackages_latest.typescript-language-server
|
||||
];
|
||||
|
@ -82,7 +82,6 @@
|
|||
};
|
||||
|
||||
in rec {
|
||||
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;
|
||||
|
@ -92,7 +91,6 @@
|
|||
all = pkgs.symlinkJoin {
|
||||
name = "all";
|
||||
paths = [
|
||||
cyber-slides
|
||||
cyberpunk-splash
|
||||
dashboard
|
||||
file-service
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'eslint:recommended'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
root: true,
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "otg-pwa",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "eslint ./src/**/.ts",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"author": "Savanni D'Gerinel <savanni@luminescent-dreams.com>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^7.10.0",
|
||||
"@typescript-eslint/parser": "^7.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.12",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { c_to_k, f_to_k, k_to_c, k_to_f } from './conversions';
|
||||
|
||||
describe('temperature conversions', () => {
|
||||
it('should convert celsius to kelvin', () => {
|
||||
expect(c_to_k(0)).toBe(273.15);
|
||||
expect(c_to_k(100)).toBe(373.15);
|
||||
});
|
||||
|
||||
it('should convert fahrenheit to kelvin', () => {
|
||||
expect(f_to_k(32)).toBe(273.15);
|
||||
expect(f_to_k(212)).toBe(373.15);
|
||||
});
|
||||
|
||||
it('should convert kelvin to celsius', () => {
|
||||
expect(k_to_c(273.15)).toBe(0);
|
||||
expect(k_to_c(373.15)).toBe(100);
|
||||
});
|
||||
|
||||
it('should convert kelvin to fahrenheit', () => {
|
||||
expect(k_to_f(273.15)).toBe(32);
|
||||
expect(k_to_f(373.15)).toBe(212);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
import { getMessage } from './index'
|
||||
|
||||
describe('getMessage()', () => {
|
||||
it('should return the correct message when called', () => {
|
||||
expect(getMessage()).toBe('Hello, world')
|
||||
})
|
||||
|
||||
it('should be super smart', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
*/
|
|
@ -0,0 +1,6 @@
|
|||
export const c_to_k = (value: number) => value + 273.15;
|
||||
export const f_to_k = (value: number) => ((value - 32) * 5 / 9) + 273.15;
|
||||
export const k_to_c = (value: number) => value - 273.15;
|
||||
export const k_to_f = (value: number) => (value - 273.15) * 9 / 5 + 32;
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>On The Grid</title>
|
||||
<style>
|
||||
:body {
|
||||
font-family: "sans-serif";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,83 @@
|
|||
import { c_to_k, f_to_k, k_to_c, k_to_f } from './conversions';
|
||||
|
||||
interface TemperatureChanged {
|
||||
source: string,
|
||||
value: number,
|
||||
}
|
||||
|
||||
class TemperatureField extends HTMLElement {
|
||||
static observedAttributes = ['value'];
|
||||
|
||||
value = 0;
|
||||
inputElement: HTMLInputElement;
|
||||
shadow: ShadowRoot;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inputElement = document.createElement('input');
|
||||
this.inputElement.type = "text";
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.inputElement.onchange = ev => {
|
||||
if (ev.target instanceof HTMLInputElement) {
|
||||
let value = parseFloat(ev.target.value);
|
||||
|
||||
if (this.id == "f") {
|
||||
const event = new CustomEvent('temperature-changed', { detail: { source: this.id, value: f_to_k(value) } });
|
||||
this.dispatchEvent(event);
|
||||
} else if (this.id == "c") {
|
||||
const event = new CustomEvent('temperature-changed', { detail: { source: this.id, value: c_to_k(value) } });
|
||||
this.dispatchEvent(event);
|
||||
} else if (this.id == "k") {
|
||||
const event = new CustomEvent('temperature-changed', { detail: { source: this.id, value } });
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.shadow.appendChild(this.inputElement);
|
||||
}
|
||||
|
||||
setTemperature(v: number) {
|
||||
console.log("set the temperature on ", this.id, "to ", v);
|
||||
}
|
||||
}
|
||||
|
||||
class App {
|
||||
temperatureK = 0;
|
||||
|
||||
updateTemperature(value: number) {
|
||||
const f = document.getElementById("f");
|
||||
if (f instanceof TemperatureField) {
|
||||
f.setTemperature(value);
|
||||
}
|
||||
const c = document.getElementById("c");
|
||||
if (c instanceof TemperatureField) {
|
||||
c.setTemperature(value);
|
||||
}
|
||||
const k = document.getElementById("k");
|
||||
if (k instanceof TemperatureField) {
|
||||
k.setTemperature(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('temperature-field', TemperatureField);
|
||||
|
||||
const app = new App();
|
||||
|
||||
document.getElementById("f")?.addEventListener('temperature-changed', ev => {
|
||||
if (ev instanceof CustomEvent) { app.updateTemperature(ev.detail.value) }
|
||||
});
|
||||
document.getElementById("c")?.addEventListener('temperature-changed', ev => {
|
||||
if (ev instanceof CustomEvent) { app.updateTemperature(ev.detail.value) }
|
||||
});
|
||||
document.getElementById("k")?.addEventListener('temperature-changed', ev => {
|
||||
if (ev instanceof CustomEvent) { app.updateTemperature(ev.detail.value) }
|
||||
});
|
||||
|
|
@ -1,15 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./visions.ts"]
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
[build]
|
||||
target = "thumbv6m-none-eabi"
|
||||
|
||||
[target.thumbv6m-none-eabi]
|
||||
rustflags = [
|
||||
"-C", "link-arg=--nmagic",
|
||||
"-C", "link-arg=-Tlink.x",
|
||||
"-C", "llvm-args=--inline-threshold=5",
|
||||
"-C", "no-vectorize-loops",
|
||||
]
|
||||
|
||||
runner = "elf2uf2-rs -d"
|
|
@ -1,11 +0,0 @@
|
|||
[package]
|
||||
name = "halloween-leds"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cortex-m-rt = "0.7.3"
|
||||
embedded-hal = "1.0.0"
|
||||
embedded-io = "0.6.1"
|
||||
panic-halt = "1.0.0"
|
||||
rp-pico = "0.9.0"
|
|
@ -1,125 +0,0 @@
|
|||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
/// This application demonstrates using a Raspberry Pi Pico to control an individual SK9822 module.
|
||||
/// Keep in mind that the Pico, though it accepts 5V for power, it runs on 3.3V logic. The GPIO
|
||||
/// pins will emit only 3.3 volts, and the SK9822 needs 5V logic. So, make sure that the GPIO pins
|
||||
/// run through a transistor or a logic level lhifter to go from 3.3V logic to 5V logic.
|
||||
use embedded_hal::{delay::DelayNs, spi::SpiBus};
|
||||
use panic_halt as _;
|
||||
use rp_pico::{
|
||||
entry,
|
||||
hal::{
|
||||
clocks::init_clocks_and_plls,
|
||||
fugit::RateExtU32,
|
||||
gpio::{
|
||||
bank0::{Gpio10, Gpio11},
|
||||
FunctionSpi, Pin, PullDown,
|
||||
},
|
||||
spi::Spi,
|
||||
Clock, Sio, Timer, Watchdog,
|
||||
},
|
||||
pac, Pins,
|
||||
};
|
||||
|
||||
const FPS: u32 = 60;
|
||||
const MS_PER_FRAME: u32 = 1000 / FPS;
|
||||
const XOSC_CRYSTAL_FREQ: u32 = 12_000_000; // MHz, https://forums.raspberrypi.com/viewtopic.php?t=356764
|
||||
|
||||
#[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);
|
||||
|
||||
// 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: Pin<Gpio10, FunctionSpi, PullDown> = pins.gpio10.into_function();
|
||||
let spi_sdo: Pin<Gpio11, FunctionSpi, PullDown> = pins.gpio11.into_function();
|
||||
|
||||
// Now, create the SPI function abstraction for SPI1 with spi_clk and spi_sdo.
|
||||
let mut spi = Spi::<_, _, _, 8>::new(peripherals.SPI1, (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_1,
|
||||
);
|
||||
|
||||
// byte count: 4 for the start frame
|
||||
// 1 * 4 for three lights with 4 bytes per light
|
||||
// 4 for the end frame
|
||||
// = 20 bytes
|
||||
let mut lights: [u8; 12] = [0; 12];
|
||||
// We just skip the first four bytes, because the start frame is four bytes of 0.
|
||||
|
||||
// Set the first byte of the one and only lamp. The first byte follows the pattern of three 1
|
||||
// bits followed by five additional bits that indicate an overall brightness level of the
|
||||
// pixel. The datasheet for the SK9822 doesn't specify the exact effect, but it does mean that
|
||||
// the higher this number is, the brighter 255 means for an given LED in the array. 1 is the
|
||||
// lowest brightness that emits light, and 31 is the highest supported brightness.
|
||||
lights[4] = 0xe0 + 1;
|
||||
// Set the Blue light of the dotstar to 255, assuming the dotstar frame format is RBG. Note
|
||||
// that the standard SK9822 datasheed indicates that the format is BGR. Your mileage may vary.
|
||||
lights[6] = 255;
|
||||
|
||||
// The end frame is four bytes of 255.
|
||||
lights[8] = 0xff;
|
||||
lights[9] = 0xff;
|
||||
lights[10] = 0xff;
|
||||
lights[11] = 0xff;
|
||||
|
||||
|
||||
// The rest of this is just a stock pulsating animation which is slightly brightening and
|
||||
// dimming the *blue* LED (on my set of dotstars).
|
||||
let mut brightness = 1;
|
||||
let mut step = 1;
|
||||
loop {
|
||||
if brightness == 64 && step == 1 {
|
||||
step = -1;
|
||||
} else if brightness == 1 && step == -1 {
|
||||
step = 1;
|
||||
};
|
||||
lights[5] = brightness as u8;
|
||||
brightness = brightness + step;
|
||||
|
||||
let _ = spi.write(lights.as_slice());
|
||||
timer.delay_ms(MS_PER_FRAME);
|
||||
}
|
||||
}
|
|
@ -33,9 +33,9 @@ use std::{error::Error, fmt};
|
|||
/// statement.
|
||||
pub trait FatalError: Error {}
|
||||
|
||||
/// ResultExt<A, FE, E> represents a return value that might be a success, might be a fatal error, or
|
||||
/// Result<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 ResultExt<A, E, FE> {
|
||||
pub enum Result<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 ResultExt<A, E, FE> {
|
|||
Fatal(FE),
|
||||
}
|
||||
|
||||
impl<A, E, FE> ResultExt<A, E, FE> {
|
||||
impl<A, E, FE> Result<A, E, FE> {
|
||||
/// Apply an infallible function to a successful value.
|
||||
pub fn map<B, O>(self, mapper: O) -> ResultExt<B, E, FE>
|
||||
pub fn map<B, O>(self, mapper: O) -> Result<B, E, FE>
|
||||
where
|
||||
O: FnOnce(A) -> B,
|
||||
{
|
||||
match self {
|
||||
ResultExt::Ok(val) => ResultExt::Ok(mapper(val)),
|
||||
ResultExt::Err(err) => ResultExt::Err(err),
|
||||
ResultExt::Fatal(err) => ResultExt::Fatal(err),
|
||||
Result::Ok(val) => Result::Ok(mapper(val)),
|
||||
Result::Err(err) => Result::Err(err),
|
||||
Result::Fatal(err) => Result::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) -> ResultExt<B, E, FE>
|
||||
pub fn and_then<B, O>(self, handler: O) -> Result<B, E, FE>
|
||||
where
|
||||
O: FnOnce(A) -> ResultExt<B, E, FE>,
|
||||
O: FnOnce(A) -> Result<B, E, FE>,
|
||||
{
|
||||
match self {
|
||||
ResultExt::Ok(val) => handler(val),
|
||||
ResultExt::Err(err) => ResultExt::Err(err),
|
||||
ResultExt::Fatal(err) => ResultExt::Fatal(err),
|
||||
Result::Ok(val) => handler(val),
|
||||
Result::Err(err) => Result::Err(err),
|
||||
Result::Fatal(err) => Result::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) -> ResultExt<A, F, FE>
|
||||
pub fn map_err<F, O>(self, mapper: O) -> Result<A, F, FE>
|
||||
where
|
||||
O: FnOnce(E) -> F,
|
||||
{
|
||||
match self {
|
||||
ResultExt::Ok(val) => ResultExt::Ok(val),
|
||||
ResultExt::Err(err) => ResultExt::Err(mapper(err)),
|
||||
ResultExt::Fatal(err) => ResultExt::Fatal(err),
|
||||
Result::Ok(val) => Result::Ok(val),
|
||||
Result::Err(err) => Result::Err(mapper(err)),
|
||||
Result::Fatal(err) => Result::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) -> ResultExt<A, F, FE>
|
||||
pub fn or_else<O, F>(self, handler: O) -> Result<A, F, FE>
|
||||
where
|
||||
O: FnOnce(E) -> ResultExt<A, F, FE>,
|
||||
O: FnOnce(E) -> Result<A, F, FE>,
|
||||
{
|
||||
match self {
|
||||
ResultExt::Ok(val) => ResultExt::Ok(val),
|
||||
ResultExt::Err(err) => handler(err),
|
||||
ResultExt::Fatal(err) => ResultExt::Fatal(err),
|
||||
Result::Ok(val) => Result::Ok(val),
|
||||
Result::Err(err) => handler(err),
|
||||
Result::Fatal(err) => Result::Fatal(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from a normal `Result` type to a `ResultExt` type. The error condition for a `Result` will
|
||||
/// Convert from a normal `Result` type to a `Result` 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 ResultExt<A, E, FE> {
|
||||
impl<A, E, FE> From<std::result::Result<A, E>> for Result<A, E, FE> {
|
||||
fn from(r: std::result::Result<A, E>) -> Self {
|
||||
match r {
|
||||
Ok(val) => ResultExt::Ok(val),
|
||||
Err(err) => ResultExt::Err(err),
|
||||
Ok(val) => Result::Ok(val),
|
||||
Err(err) => Result::Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, E, FE> fmt::Debug for ResultExt<A, E, FE>
|
||||
impl<A, E, FE> fmt::Debug for Result<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 {
|
||||
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)),
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, E, FE> PartialEq for ResultExt<A, E, FE>
|
||||
impl<A, E, FE> PartialEq for Result<A, E, FE>
|
||||
where
|
||||
A: PartialEq,
|
||||
FE: PartialEq,
|
||||
|
@ -133,27 +133,27 @@ where
|
|||
{
|
||||
fn eq(&self, rhs: &Self) -> bool {
|
||||
match (self, rhs) {
|
||||
(ResultExt::Ok(val), ResultExt::Ok(rhs)) => val == rhs,
|
||||
(ResultExt::Err(_), ResultExt::Err(_)) => true,
|
||||
(ResultExt::Fatal(_), ResultExt::Fatal(_)) => true,
|
||||
(Result::Ok(val), Result::Ok(rhs)) => val == rhs,
|
||||
(Result::Err(_), Result::Err(_)) => true,
|
||||
(Result::Fatal(_), Result::Fatal(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to create an ok value.
|
||||
pub fn ok<A, E: Error, FE: FatalError>(val: A) -> ResultExt<A, E, FE> {
|
||||
ResultExt::Ok(val)
|
||||
pub fn ok<A, E: Error, FE: FatalError>(val: A) -> Result<A, E, FE> {
|
||||
Result::Ok(val)
|
||||
}
|
||||
|
||||
/// Convenience function to create an error value.
|
||||
pub fn error<A, E: Error, FE: FatalError>(err: E) -> ResultExt<A, E, FE> {
|
||||
ResultExt::Err(err)
|
||||
pub fn error<A, E: Error, FE: FatalError>(err: E) -> Result<A, E, FE> {
|
||||
Result::Err(err)
|
||||
}
|
||||
|
||||
/// Convenience function to create a fatal value.
|
||||
pub fn fatal<A, E: Error, FE: FatalError>(err: FE) -> ResultExt<A, E, FE> {
|
||||
ResultExt::Fatal(err)
|
||||
pub fn fatal<A, E: Error, FE: FatalError>(err: FE) -> Result<A, E, FE> {
|
||||
Result::Fatal(err)
|
||||
}
|
||||
|
||||
/// Return early from the current function if the value is a fatal error.
|
||||
|
@ -161,9 +161,9 @@ pub fn fatal<A, E: Error, FE: FatalError>(err: FE) -> ResultExt<A, E, FE> {
|
|||
macro_rules! return_fatal {
|
||||
($x:expr) => {
|
||||
match $x {
|
||||
ResultExt::Fatal(err) => return ResultExt::Fatal(err),
|
||||
ResultExt::Err(err) => Err(err),
|
||||
ResultExt::Ok(val) => Ok(val),
|
||||
Result::Fatal(err) => return Result::Fatal(err),
|
||||
Result::Err(err) => Err(err),
|
||||
Result::Ok(val) => Ok(val),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -173,9 +173,9 @@ macro_rules! return_fatal {
|
|||
macro_rules! return_error {
|
||||
($x:expr) => {
|
||||
match $x {
|
||||
ResultExt::Ok(val) => val,
|
||||
ResultExt::Err(err) => return ResultExt::Err(err),
|
||||
ResultExt::Fatal(err) => return ResultExt::Fatal(err),
|
||||
Result::Ok(val) => val,
|
||||
Result::Err(err) => return Result::Err(err),
|
||||
Result::Fatal(err) => return Result::Fatal(err),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -210,19 +210,19 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn it_can_map_things() {
|
||||
let success: ResultExt<i32, Error, FatalError> = ok(15);
|
||||
let success: Result<i32, Error, FatalError> = ok(15);
|
||||
assert_eq!(ok(16), success.map(|v| v + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_chain_success() {
|
||||
let success: ResultExt<i32, Error, FatalError> = ok(15);
|
||||
let success: Result<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: ResultExt<i32, Error, FatalError> = error(Error::Error);
|
||||
let failure: Result<i32, Error, FatalError> = error(Error::Error);
|
||||
assert_eq!(
|
||||
ok::<i32, Error, FatalError>(16),
|
||||
failure.or_else(|_| ok(16))
|
||||
|
@ -231,7 +231,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn early_exit_on_fatal() {
|
||||
fn ok_func() -> ResultExt<i32, Error, FatalError> {
|
||||
fn ok_func() -> Result<i32, Error, FatalError> {
|
||||
let value = return_fatal!(ok::<i32, Error, FatalError>(15));
|
||||
match value {
|
||||
Ok(_) => ok(14),
|
||||
|
@ -239,7 +239,7 @@ mod test {
|
|||
}
|
||||
}
|
||||
|
||||
fn err_func() -> ResultExt<i32, Error, FatalError> {
|
||||
fn err_func() -> Result<i32, Error, FatalError> {
|
||||
let value = return_fatal!(error::<i32, Error, FatalError>(Error::Error));
|
||||
match value {
|
||||
Ok(_) => panic!("shouldn't have gotten here"),
|
||||
|
@ -247,7 +247,7 @@ mod test {
|
|||
}
|
||||
}
|
||||
|
||||
fn fatal_func() -> ResultExt<i32, Error, FatalError> {
|
||||
fn fatal_func() -> Result<i32, Error, FatalError> {
|
||||
let _ = return_fatal!(fatal::<i32, Error, FatalError>(FatalError::FatalError));
|
||||
panic!("failed to bail");
|
||||
}
|
||||
|
@ -259,18 +259,18 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn it_can_early_exit_on_all_errors() {
|
||||
fn ok_func() -> ResultExt<i32, Error, FatalError> {
|
||||
fn ok_func() -> Result<i32, Error, FatalError> {
|
||||
let value = return_error!(ok::<i32, Error, FatalError>(15));
|
||||
assert_eq!(value, 15);
|
||||
ok(14)
|
||||
}
|
||||
|
||||
fn err_func() -> ResultExt<i32, Error, FatalError> {
|
||||
fn err_func() -> Result<i32, Error, FatalError> {
|
||||
return_error!(error::<i32, Error, FatalError>(Error::Error));
|
||||
panic!("failed to bail");
|
||||
}
|
||||
|
||||
fn fatal_func() -> ResultExt<i32, Error, FatalError> {
|
||||
fn fatal_func() -> Result<i32, Error, FatalError> {
|
||||
return_error!(fatal::<i32, Error, FatalError>(FatalError::FatalError));
|
||||
panic!("failed to bail");
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
[toolchain]
|
||||
channel = "1.81.0"
|
||||
channel = "1.77.0"
|
||||
targets = [ "wasm32-unknown-unknown", "thumbv6m-none-eabi" ]
|
||||
|
|
18
script.yml
18
script.yml
|
@ -1,18 +0,0 @@
|
|||
- text: The distinguishing thing about magic is that it includes some kind of personal element. The person who is performing the magic is relevant to the magic. -- Ted Chang, Marie Brennan
|
||||
position: top
|
||||
transition:
|
||||
secs: 1
|
||||
nanos: 0
|
||||
|
||||
- text: Any sufficiently advanced technology is indistinguishable from magic. -- Arthur C. Clark.
|
||||
position: middle
|
||||
transition:
|
||||
secs: 1
|
||||
nanos: 0
|
||||
|
||||
- text: Science is our Magic.
|
||||
position: bottom
|
||||
transition:
|
||||
secs: 1
|
||||
nanos: 0
|
||||
|
|
@ -6,27 +6,9 @@ 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/" }
|
||||
futures = { version = "0.3.31" }
|
||||
http = { version = "1" }
|
||||
include_dir = { version = "0.7.4" }
|
||||
lazy_static = { version = "1.5.0" }
|
||||
mime = { version = "0.3.17" }
|
||||
mime_guess = { version = "2.0.5" }
|
||||
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" }
|
||||
typeshare = { version = "1.0.4" }
|
||||
urlencoding = { version = "2.1.3" }
|
||||
uuid = { version = "1.11.0", features = ["v4"] }
|
||||
warp = { version = "0.3" }
|
||||
|
||||
[dev-dependencies]
|
||||
cool_asserts = "2.0.3"
|
||||
authdb = { path = "../../authdb/" }
|
||||
http = { version = "1" }
|
||||
serde_json = { version = "*" }
|
||||
serde = { version = "1" }
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
warp = { version = "0.3" }
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- cargo build
|
||||
|
||||
test:
|
||||
cmds:
|
||||
# - cargo watch -x 'test -- --nocapture'
|
||||
- cargo watch -x 'nextest run'
|
||||
|
||||
dev:
|
||||
cmds:
|
||||
- cargo watch -x run
|
|
@ -1,32 +0,0 @@
|
|||
CREATE TABLE users(
|
||||
uuid TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
password TEXT,
|
||||
admin BOOLEAN,
|
||||
enabled BOOLEAN
|
||||
);
|
||||
|
||||
CREATE TABLE games(
|
||||
uuid TEXT PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
INSERT INTO users VALUES ("admin", "admin", "", true, true);
|
||||
|
|
@ -1 +0,0 @@
|
|||
{ "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." ] }
|
|
@ -1,156 +0,0 @@
|
|||
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}")]
|
||||
UnexpectedError(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::UnexpectedError(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct AssetId(String);
|
||||
|
||||
impl AssetId {
|
||||
pub fn as_str<'a>(&'a self) -> &'a 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<'a>(&'a self) -> AssetIter<'a>;
|
||||
|
||||
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<'a>(&'a self) -> AssetIter<'a> {
|
||||
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<'a>(&'a self) -> AssetIter<'a> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,330 +0,0 @@
|
|||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use async_std::sync::RwLock;
|
||||
use mime::Mime;
|
||||
use result_extended::{error, fatal, ok, return_error, ResultExt};
|
||||
use serde::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, UserId},
|
||||
types::{AppError, FatalError, Game, Message, Tabletop, User, RGB},
|
||||
};
|
||||
|
||||
const DEFAULT_BACKGROUND_COLOR: RGB = RGB {
|
||||
red: 0xca,
|
||||
green: 0xb9,
|
||||
blue: 0xbb,
|
||||
};
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct Status {
|
||||
pub admin_enabled: bool,
|
||||
}
|
||||
|
||||
#[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 mut 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 {
|
||||
admin_enabled: !admin_user.password.is_empty(),
|
||||
})
|
||||
}
|
||||
|
||||
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 list_users(&self) -> ResultExt<Vec<User>, AppError, FatalError> {
|
||||
let users = self.0.write().await.db.users().await;
|
||||
match users {
|
||||
Ok(users) => ok(users.into_iter().map(|u| User::from(u)).collect()),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_games(&self) -> ResultExt<Vec<Game>, AppError, FatalError> {
|
||||
let games = self.0.write().await.db.games().await;
|
||||
match games {
|
||||
// Ok(games) => ok(games.into_iter().map(|g| Game::from(g)).collect()),
|
||||
Ok(games) => unimplemented!(),
|
||||
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::UnexpectedError(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 mut 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 set_password(
|
||||
&self,
|
||||
uuid: UserId,
|
||||
password: String,
|
||||
) -> ResultExt<(), AppError, FatalError> {
|
||||
let mut state = self.0.write().await;
|
||||
let user = match state.db.user(uuid.clone()).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(Some(uuid), &user.name, &password, user.admin, user.enabled)
|
||||
.await
|
||||
{
|
||||
Ok(_) => ok(()),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
|
||||
use cool_asserts::assert_matches;
|
||||
|
||||
use crate::{
|
||||
asset_db::mocks::MemoryAssets,
|
||||
database::{DbConn, DiskDb},
|
||||
};
|
||||
|
||||
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);
|
||||
Core::new(assets, conn)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_lists_available_images() {
|
||||
let core = test_core();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,651 +0,0 @@
|
|||
use std::path::Path;
|
||||
|
||||
use async_std::channel::{bounded, Receiver, Sender};
|
||||
use async_trait::async_trait;
|
||||
use include_dir::{include_dir, Dir};
|
||||
use lazy_static::lazy_static;
|
||||
use rusqlite::{
|
||||
types::{FromSql, FromSqlResult, ValueRef},
|
||||
Connection,
|
||||
};
|
||||
use rusqlite_migration::Migrations;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::types::FatalError;
|
||||
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||
|
||||
lazy_static! {
|
||||
static ref MIGRATIONS: Migrations<'static> =
|
||||
Migrations::from_directory(&MIGRATIONS_DIR).unwrap();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Request {
|
||||
Charsheet(CharacterId),
|
||||
Games,
|
||||
User(UserId),
|
||||
Users,
|
||||
SaveUser(Option<UserId>, String, String, bool, bool),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DatabaseRequest {
|
||||
tx: Sender<DatabaseResponse>,
|
||||
req: Request,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DatabaseResponse {
|
||||
Charsheet(Option<CharsheetRow>),
|
||||
Games(Vec<GameRow>),
|
||||
User(Option<UserRow>),
|
||||
Users(Vec<UserRow>),
|
||||
SaveUser(UserId),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct UserId(String);
|
||||
|
||||
impl UserId {
|
||||
pub fn new() -> Self {
|
||||
Self(format!("{}", Uuid::new_v4().hyphenated()))
|
||||
}
|
||||
|
||||
pub fn as_str<'a>(&'a self) -> &'a str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct GameId(String);
|
||||
|
||||
impl GameId {
|
||||
pub fn new() -> Self {
|
||||
Self(format!("{}", Uuid::new_v4().hyphenated()))
|
||||
}
|
||||
|
||||
pub fn as_str<'a>(&'a self) -> &'a 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)]
|
||||
pub struct CharacterId(String);
|
||||
|
||||
impl CharacterId {
|
||||
pub fn new() -> Self {
|
||||
Self(format!("{}", Uuid::new_v4().hyphenated()))
|
||||
}
|
||||
|
||||
pub fn as_str<'a>(&'a self) -> &'a 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 UserRow {
|
||||
pub id: UserId,
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
pub admin: bool,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Role {
|
||||
userid: UserId,
|
||||
gameid: GameId,
|
||||
role: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GameRow {
|
||||
pub id: UserId,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CharsheetRow {
|
||||
id: String,
|
||||
game: GameId,
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Database: Send + Sync {
|
||||
async fn user(&mut self, _: UserId) -> Result<Option<UserRow>, FatalError>;
|
||||
|
||||
async fn save_user(
|
||||
&mut self,
|
||||
user_id: Option<UserId>,
|
||||
name: &str,
|
||||
password: &str,
|
||||
admin: bool,
|
||||
enabled: bool,
|
||||
) -> Result<UserId, FatalError>;
|
||||
|
||||
async fn users(&mut self) -> Result<Vec<UserRow>, FatalError>;
|
||||
|
||||
async fn games(&mut self) -> Result<Vec<GameRow>, FatalError>;
|
||||
|
||||
async fn character(&mut self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError>;
|
||||
}
|
||||
|
||||
pub struct DiskDb {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
/*
|
||||
fn setup_test_database(conn: &Connection) -> Result<(), FatalError> {
|
||||
let mut gamecount_stmt = conn.prepare("SELECT count(*) FROM games").unwrap();
|
||||
let mut count = gamecount_stmt.query([]).unwrap();
|
||||
if count.next().unwrap().unwrap().get::<usize, usize>(0) == Ok(0) {
|
||||
let admin_id = format!("{}", Uuid::new_v4());
|
||||
let user_id = format!("{}", Uuid::new_v4());
|
||||
let game_id = format!("{}", Uuid::new_v4());
|
||||
let char_id = CharacterId::new();
|
||||
|
||||
let mut user_stmt = conn
|
||||
.prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
user_stmt
|
||||
.execute((admin_id.clone(), "admin", "abcdefg", true, true))
|
||||
.unwrap();
|
||||
user_stmt
|
||||
.execute((user_id.clone(), "savanni", "abcdefg", false, true))
|
||||
.unwrap();
|
||||
|
||||
let mut game_stmt = conn
|
||||
.prepare("INSERT INTO games VALUES (?, ?)")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
game_stmt
|
||||
.execute((game_id.clone(), "Circle of Bluest Sky"))
|
||||
.unwrap();
|
||||
|
||||
let mut role_stmt = conn
|
||||
.prepare("INSERT INTO roles VALUES (?, ?, ?)")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
role_stmt
|
||||
.execute((user_id.clone(), game_id.clone(), "gm"))
|
||||
.unwrap();
|
||||
|
||||
let mut sheet_stmt = conn
|
||||
.prepare("INSERT INTO characters VALUES (?, ?, ?)")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
|
||||
sheet_stmt.execute((char_id.as_str(), game_id, 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." ] }"#))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
*/
|
||||
|
||||
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)))?;
|
||||
|
||||
// setup_test_database(&conn)?;
|
||||
|
||||
Ok(DiskDb { conn })
|
||||
}
|
||||
|
||||
fn user(&self, id: UserId) -> Result<Option<UserRow>, FatalError> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE uuid=?")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
let items: Vec<UserRow> = stmt
|
||||
.query_map([id.as_str()], |row| {
|
||||
Ok(UserRow {
|
||||
id: row.get(0).unwrap(),
|
||||
name: row.get(1).unwrap(),
|
||||
password: row.get(2).unwrap(),
|
||||
admin: row.get(3).unwrap(),
|
||||
enabled: row.get(4).unwrap(),
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<UserRow>, rusqlite::Error>>()
|
||||
.unwrap();
|
||||
match &items[..] {
|
||||
[] => Ok(None),
|
||||
[item] => Ok(Some(item.clone())),
|
||||
_ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
fn users(&self) -> Result<Vec<UserRow>, FatalError> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT * FROM users")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
let items = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(UserRow {
|
||||
id: row.get(0).unwrap(),
|
||||
name: row.get(1).unwrap(),
|
||||
password: row.get(2).unwrap(),
|
||||
admin: row.get(3).unwrap(),
|
||||
enabled: row.get(4).unwrap(),
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<UserRow>, rusqlite::Error>>()
|
||||
.unwrap();
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn save_user(
|
||||
&self,
|
||||
user_id: Option<UserId>,
|
||||
name: &str,
|
||||
password: &str,
|
||||
admin: bool,
|
||||
enabled: bool,
|
||||
) -> Result<UserId, FatalError> {
|
||||
match user_id {
|
||||
None => {
|
||||
let user_id = UserId::new();
|
||||
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, enabled))
|
||||
.unwrap();
|
||||
Ok(user_id)
|
||||
}
|
||||
Some(user_id) => {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare(
|
||||
"UPDATE users SET name=?, password=?, admin=?, enabled=? WHERE uuid=?",
|
||||
)
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
stmt.execute((name, password, admin, enabled, user_id.as_str()))
|
||||
.unwrap();
|
||||
Ok(user_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_game(&self, game_id: Option<GameId>, name: &str) -> Result<GameId, FatalError> {
|
||||
match game_id {
|
||||
None => {
|
||||
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(), name)).unwrap();
|
||||
Ok(game_id)
|
||||
}
|
||||
Some(game_id) => {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("UPDATE games SET name=? WHERE uuid=?")
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
stmt.execute((name, game_id.as_str())).unwrap();
|
||||
Ok(game_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
||||
while let Ok(DatabaseRequest { tx, req }) = requestor.recv().await {
|
||||
println!("Request received: {:?}", req);
|
||||
match req {
|
||||
Request::Charsheet(id) => {
|
||||
let sheet = db.character(id);
|
||||
println!("sheet retrieved: {:?}", sheet);
|
||||
match sheet {
|
||||
Ok(sheet) => {
|
||||
tx.send(DatabaseResponse::Charsheet(sheet)).await.unwrap();
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
Request::Games => {
|
||||
unimplemented!();
|
||||
}
|
||||
Request::User(uid) => {
|
||||
let user = db.user(uid);
|
||||
match user {
|
||||
Ok(user) => {
|
||||
tx.send(DatabaseResponse::User(user)).await.unwrap();
|
||||
}
|
||||
err => panic!("{:?}", err),
|
||||
}
|
||||
}
|
||||
Request::SaveUser(user_id, username, password, admin, enabled) => {
|
||||
let user_id = db.save_user(user_id, username.as_ref(), password.as_ref(), admin, enabled);
|
||||
match user_id {
|
||||
Ok(user_id) => {
|
||||
tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap();
|
||||
}
|
||||
err => panic!("{:?}", err),
|
||||
}
|
||||
}
|
||||
Request::Users => {
|
||||
let users = db.users();
|
||||
match users {
|
||||
Ok(users) => {
|
||||
tx.send(DatabaseResponse::Users(users)).await.unwrap();
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("ending db_handler");
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Database for DbConn {
|
||||
async fn user(&mut self, uid: UserId) -> Result<Option<UserRow>, FatalError> {
|
||||
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||
|
||||
let request = DatabaseRequest {
|
||||
tx,
|
||||
req: Request::User(uid),
|
||||
};
|
||||
|
||||
match self.conn.send(request).await {
|
||||
Ok(()) => (),
|
||||
Err(_) => return Err(FatalError::DatabaseConnectionLost),
|
||||
};
|
||||
|
||||
match rx.recv().await {
|
||||
Ok(DatabaseResponse::User(user)) => Ok(user),
|
||||
Ok(_) => Err(FatalError::MessageMismatch),
|
||||
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_user(
|
||||
&mut self,
|
||||
user_id: Option<UserId>,
|
||||
name: &str,
|
||||
password: &str,
|
||||
admin: bool,
|
||||
enabled: bool,
|
||||
) -> Result<UserId, FatalError> {
|
||||
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||
|
||||
let request = DatabaseRequest {
|
||||
tx,
|
||||
req: Request::SaveUser(
|
||||
user_id,
|
||||
name.to_owned(),
|
||||
password.to_owned(),
|
||||
admin,
|
||||
enabled,
|
||||
),
|
||||
};
|
||||
|
||||
match self.conn.send(request).await {
|
||||
Ok(()) => (),
|
||||
Err(_) => return Err(FatalError::DatabaseConnectionLost),
|
||||
};
|
||||
|
||||
match rx.recv().await {
|
||||
Ok(DatabaseResponse::SaveUser(user_id)) => Ok(user_id),
|
||||
Ok(_) => Err(FatalError::MessageMismatch),
|
||||
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
||||
}
|
||||
}
|
||||
|
||||
async fn users(&mut self) -> Result<Vec<UserRow>, FatalError> {
|
||||
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||
|
||||
let request = DatabaseRequest {
|
||||
tx,
|
||||
req: Request::Users,
|
||||
};
|
||||
|
||||
match self.conn.send(request).await {
|
||||
Ok(()) => (),
|
||||
Err(_) => return Err(FatalError::DatabaseConnectionLost),
|
||||
};
|
||||
|
||||
match rx.recv().await {
|
||||
Ok(DatabaseResponse::Users(lst)) => Ok(lst),
|
||||
Ok(_) => Err(FatalError::MessageMismatch),
|
||||
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
||||
}
|
||||
}
|
||||
|
||||
async fn games(&mut self) -> Result<Vec<GameRow>, FatalError> {
|
||||
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||
|
||||
let request = DatabaseRequest {
|
||||
tx,
|
||||
req: Request::Games,
|
||||
};
|
||||
|
||||
match self.conn.send(request).await {
|
||||
Ok(()) => (),
|
||||
Err(_) => return Err(FatalError::DatabaseConnectionLost),
|
||||
};
|
||||
|
||||
match rx.recv().await {
|
||||
Ok(DatabaseResponse::Games(lst)) => Ok(lst),
|
||||
Ok(_) => Err(FatalError::MessageMismatch),
|
||||
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
||||
}
|
||||
}
|
||||
|
||||
async fn character(&mut self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
|
||||
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||
|
||||
let request = DatabaseRequest {
|
||||
tx,
|
||||
req: Request::Charsheet(id),
|
||||
};
|
||||
|
||||
match self.conn.send(request).await {
|
||||
Ok(()) => (),
|
||||
Err(_) => return Err(FatalError::DatabaseConnectionLost),
|
||||
};
|
||||
|
||||
match rx.recv().await {
|
||||
Ok(DatabaseResponse::Charsheet(row)) => Ok(row),
|
||||
Ok(_) => Err(FatalError::MessageMismatch),
|
||||
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cool_asserts::assert_matches;
|
||||
|
||||
use super::*;
|
||||
|
||||
const soren: &'static 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.save_user(None, "admin", "abcdefg", true, true);
|
||||
let game_id = db.save_game(None, "Candela").unwrap();
|
||||
(db, game_id)
|
||||
}
|
||||
|
||||
#[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 mut conn = DbConn::new(memory_db);
|
||||
|
||||
assert_matches!(
|
||||
conn.character(CharacterId::from("1")).await,
|
||||
ResultExt::Ok(None)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,18 +1,6 @@
|
|||
use std::future::Future;
|
||||
use authdb::{AuthDB, AuthToken};
|
||||
use http::{response::Response, status::StatusCode, Error};
|
||||
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use result_extended::{error, ok, return_error, ResultExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use warp::{http::Response, http::StatusCode, reply::Reply, ws::Message};
|
||||
|
||||
use crate::{
|
||||
asset_db::AssetId,
|
||||
core::Core,
|
||||
database::{CharacterId, UserId},
|
||||
types::{AppError, FatalError},
|
||||
};
|
||||
|
||||
/*
|
||||
pub async fn handle_auth(
|
||||
auth_ctx: &AuthDB,
|
||||
auth_token: AuthToken,
|
||||
|
@ -34,224 +22,3 @@ pub async fn handle_auth(
|
|||
.body("".to_owned()),
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
pub async fn handler<F>(f: F) -> impl Reply
|
||||
where
|
||||
F: Future<Output = ResultExt<Response<Vec<u8>>, AppError, FatalError>>,
|
||||
{
|
||||
match f.await {
|
||||
ResultExt::Ok(response) => response,
|
||||
ResultExt::Err(AppError::NotFound(_)) => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(vec![])
|
||||
.unwrap(),
|
||||
ResultExt::Err(_) => Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(vec![])
|
||||
.unwrap(),
|
||||
ResultExt::Fatal(err) => {
|
||||
panic!("Shutting down with fatal error: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_server_status(core: Core) -> impl Reply {
|
||||
handler(async move {
|
||||
let status = return_error!(core.status().await);
|
||||
ok(Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(serde_json::to_vec(&status).unwrap())
|
||||
.unwrap())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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) -> impl Reply {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,27 +1,19 @@
|
|||
use authdb::{AuthDB, AuthError, AuthToken, SessionToken, Username};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use asset_db::{AssetId, FsAssets};
|
||||
use authdb::AuthError;
|
||||
use database::DbConn;
|
||||
use handlers::{
|
||||
handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_get_users, handle_register_client, handle_server_status, handle_set_admin_password, handle_set_background_image, handle_unregister_client, RegisterRequest
|
||||
sync::Arc,
|
||||
};
|
||||
use warp::{
|
||||
// header,
|
||||
http::{Response, StatusCode},
|
||||
reply::Reply,
|
||||
header,
|
||||
http::StatusCode,
|
||||
reply::{Json, Reply},
|
||||
Filter,
|
||||
};
|
||||
|
||||
mod asset_db;
|
||||
mod core;
|
||||
mod database;
|
||||
mod handlers;
|
||||
mod types;
|
||||
use handlers::handle_auth;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Unauthorized;
|
||||
|
@ -31,7 +23,6 @@ impl warp::reject::Reject for Unauthorized {}
|
|||
struct AuthDBError(AuthError);
|
||||
impl warp::reject::Reject for AuthDBError {}
|
||||
|
||||
/*
|
||||
fn with_session(
|
||||
auth_ctx: Arc<AuthDB>,
|
||||
) -> impl Filter<Extract = (Username,), Error = warp::Rejection> + Clone {
|
||||
|
@ -80,10 +71,8 @@ fn route_echo_authenticated(
|
|||
warp::reply::json(&vec!["authenticated", username.as_str(), param.as_str()])
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible> {
|
||||
println!("handle_rejection: {:?}", err);
|
||||
if let Some(Unauthorized) = err.find() {
|
||||
Ok(warp::reply::with_status(
|
||||
"".to_owned(),
|
||||
|
@ -99,121 +88,14 @@ async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible
|
|||
|
||||
#[tokio::main]
|
||||
pub async fn main() {
|
||||
let conn = DbConn::new(Some("/home/savanni/game.db"));
|
||||
let auth_db = AuthDB::new(PathBuf::from("./auth_db.sqlite"))
|
||||
.await
|
||||
.expect("AuthDB should initialize");
|
||||
let auth_ctx: Arc<AuthDB> = Arc::new(auth_db);
|
||||
|
||||
let core = core::Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
||||
let log = warp::log("visions::api");
|
||||
|
||||
let route_server_status = warp::path!("api" / "v1" / "status")
|
||||
.and(warp::get())
|
||||
.then({
|
||||
let core = core.clone();
|
||||
move || handle_server_status(core.clone())
|
||||
});
|
||||
|
||||
let route_image = warp::path!("api" / "v1" / "image" / String)
|
||||
.and(warp::get())
|
||||
.then({
|
||||
let core = core.clone();
|
||||
move |file_name| handle_file(core.clone(), AssetId::from(file_name))
|
||||
});
|
||||
|
||||
let route_available_images = warp::path!("api" / "v1" / "image").and(warp::get()).then({
|
||||
let core = core.clone();
|
||||
move || handle_available_images(core.clone())
|
||||
});
|
||||
|
||||
let route_register_client = warp::path!("api" / "v1" / "client")
|
||||
.and(warp::post())
|
||||
.then({
|
||||
let core = core.clone();
|
||||
move || handle_register_client(core.clone(), RegisterRequest {})
|
||||
});
|
||||
|
||||
let route_unregister_client = warp::path!("api" / "v1" / "client" / String)
|
||||
.and(warp::delete())
|
||||
.then({
|
||||
let core = core.clone();
|
||||
move |client_id| handle_unregister_client(core.clone(), client_id)
|
||||
});
|
||||
|
||||
let route_websocket = warp::path("ws")
|
||||
.and(warp::ws())
|
||||
.and(warp::path::param())
|
||||
.then({
|
||||
let core = core.clone();
|
||||
move |ws, client_id| handle_connect_websocket(core.clone(), ws, client_id)
|
||||
});
|
||||
|
||||
let route_set_bg_image_options = warp::path!("api" / "v1" / "tabletop" / "bg_image")
|
||||
.and(warp::options())
|
||||
.map({
|
||||
move || {
|
||||
Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Access-Control-Allow-Methods", "PUT")
|
||||
.header("Access-Control-Allow-Headers", "content-type")
|
||||
.header("Content-Type", "application/json")
|
||||
.body("")
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
let route_set_bg_image = warp::path!("api" / "v1" / "tabletop" / "bg_image")
|
||||
.and(warp::put())
|
||||
.and(warp::body::json())
|
||||
.then({
|
||||
let core = core.clone();
|
||||
move |body| handle_set_background_image(core.clone(), body)
|
||||
})
|
||||
.with(log);
|
||||
|
||||
let route_get_users = warp::path!("api" / "v1" / "users")
|
||||
.and(warp::get())
|
||||
.then({
|
||||
let core = core.clone();
|
||||
move || handle_get_users(core.clone())
|
||||
});
|
||||
|
||||
let route_get_charsheet = warp::path!("api" / "v1" / "charsheet" / String)
|
||||
.and(warp::get())
|
||||
.then({
|
||||
let core = core.clone();
|
||||
move |charid| handle_get_charsheet(core.clone(), charid)
|
||||
});
|
||||
|
||||
let route_set_admin_password_options = warp::path!("api" / "v1" / "admin_password")
|
||||
.and(warp::options())
|
||||
.map({
|
||||
move || {
|
||||
Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Access-Control-Allow-Methods", "PUT")
|
||||
.header("Access-Control-Allow-Headers", "content-type")
|
||||
.header("Content-Type", "application/json")
|
||||
.body("")
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
let route_set_admin_password = warp::path!("api" / "v1" / "admin_password")
|
||||
.and(warp::put())
|
||||
.and(warp::body::json())
|
||||
.then({
|
||||
let core = core.clone();
|
||||
move |body| handle_set_admin_password(core.clone(), body)
|
||||
});
|
||||
|
||||
let filter = route_server_status
|
||||
.or(route_register_client)
|
||||
.or(route_unregister_client)
|
||||
.or(route_websocket)
|
||||
.or(route_image)
|
||||
.or(route_available_images)
|
||||
.or(route_set_bg_image_options)
|
||||
.or(route_set_bg_image)
|
||||
.or(route_get_users)
|
||||
.or(route_get_charsheet)
|
||||
.or(route_set_admin_password_options)
|
||||
.or(route_set_admin_password)
|
||||
let filter = route_echo_authenticated(auth_ctx.clone())
|
||||
.or(route_authenticate(auth_ctx.clone()))
|
||||
.or(route_echo_unauthenticated())
|
||||
.recover(handle_rejection);
|
||||
|
||||
let server = warp::serve(filter);
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use thiserror::Error;
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::{asset_db::AssetId, database::UserRow};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FatalError {
|
||||
#[error("Non-unique database key {0}")]
|
||||
NonUniqueDatabaseKey(String),
|
||||
|
||||
#[error("Database migrations failed {0}")]
|
||||
DatabaseMigrationFailure(String),
|
||||
|
||||
#[error("Failed to construct a query")]
|
||||
ConstructQueryFailure(String),
|
||||
|
||||
#[error("Database connection lost")]
|
||||
DatabaseConnectionLost,
|
||||
|
||||
#[error("Unexpected response for message")]
|
||||
MessageMismatch,
|
||||
}
|
||||
|
||||
impl result_extended::FatalError for FatalError {}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AppError {
|
||||
#[error("something wasn't found {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("object inaccessible {0}")]
|
||||
Inaccessible(String),
|
||||
|
||||
#[error("the requested operation is not allowed")]
|
||||
PermissionDenied,
|
||||
|
||||
#[error("invalid json {0}")]
|
||||
JsonError(serde_json::Error),
|
||||
|
||||
#[error("wat {0}")]
|
||||
UnexpectedError(String),
|
||||
}
|
||||
|
||||
#[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, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[typeshare]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
pub admin: bool,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl From<UserRow> for User {
|
||||
fn from(row: UserRow) -> Self {
|
||||
Self {
|
||||
id: row.id.as_str().to_owned(),
|
||||
name: row.name.to_owned(),
|
||||
password: row.password.to_owned(),
|
||||
admin: row.admin,
|
||||
enabled: row.enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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: String,
|
||||
pub name: String,
|
||||
pub players: Vec<Player>,
|
||||
}
|
||||
|
||||
#[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),
|
||||
}
|
||||
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
release:
|
||||
NODE_ENV=production npm run build
|
||||
|
||||
dev:
|
||||
npm run build
|
||||
|
||||
server:
|
||||
npx http-server ./dist
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# 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/).
|
|
@ -1,9 +0,0 @@
|
|||
version: '3'
|
||||
|
||||
tasks:
|
||||
dev:
|
||||
cmds:
|
||||
- cd ../visions-types && task build
|
||||
- npm install
|
||||
- npm run start
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -1,50 +1,24 @@
|
|||
{
|
||||
"name": "ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.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"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "webpack.config.js",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
"author": "",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.8",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"webpack": "^5.85.0",
|
||||
"webpack-cli": "^5.1.3"
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB |
|
@ -1,43 +0,0 @@
|
|||
<!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>
|
Binary file not shown.
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
|
@ -1,37 +0,0 @@
|
|||
.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);
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
// render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
|
@ -1,83 +0,0 @@
|
|||
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 { GmView } from './views/GmView/GmView';
|
||||
import { WebsocketProvider } from './components/WebsocketProvider';
|
||||
import { PlayerView } from './views/PlayerView/PlayerView';
|
||||
import { Admin } from './views/Admin/Admin';
|
||||
import Candela from './plugins/Candela';
|
||||
import { Authentication } from './views/Authentication/Authentication';
|
||||
import { StateContext, StateProvider } from './providers/StateProvider/StateProvider';
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
interface AuthedViewProps {
|
||||
client: Client;
|
||||
}
|
||||
|
||||
const AuthedView = ({ client, children }: PropsWithChildren<AuthedViewProps>) => {
|
||||
const [state, manager] = useContext(StateContext);
|
||||
return (
|
||||
<Authentication onAdminPassword={(password) => {
|
||||
manager.setAdminPassword(password);
|
||||
}} onAuth={(username, password) => console.log(username, password)}>
|
||||
{children}
|
||||
</Authentication>
|
||||
);
|
||||
}
|
||||
|
||||
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}> <PlayerView client={client} /> </AuthedView> </StateProvider>
|
||||
},
|
||||
{
|
||||
path: "/gm",
|
||||
element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <GmView client={client} /> </WebsocketProvider> : <div> </div>
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
element: <Admin client={client} />
|
||||
},
|
||||
{
|
||||
path: "/candela",
|
||||
element: <CandelaCharsheet client={client} />
|
||||
},
|
||||
{
|
||||
path: "/design",
|
||||
element: <DesignPage />
|
||||
}
|
||||
]);
|
||||
return (
|
||||
<div className="App">
|
||||
<RouterProvider router={router} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -1,73 +0,0 @@
|
|||
export type PlayingField = {
|
||||
backgroundImage: string;
|
||||
}
|
||||
|
||||
export class Client {
|
||||
private base: URL;
|
||||
|
||||
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() {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = '/api/v1/users/';
|
||||
return fetch(url).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 setAdminPassword(password: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/admin_password`;
|
||||
console.log("setting the admin password to: ", password);
|
||||
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(password) });
|
||||
}
|
||||
|
||||
async status() {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/status`;
|
||||
return fetch(url).then((response) => response.json());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
interface GuageProps {
|
||||
current: number,
|
||||
max: number,
|
||||
}
|
||||
|
||||
export const SimpleGuage = ({ current, max }: GuageProps) => <> {current} / {max}</>
|
|
@ -1,3 +0,0 @@
|
|||
.tabletop > img {
|
||||
max-width: 100%;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import './Tabletop.css';
|
||||
import { RGB } from 'visions-types';
|
||||
|
||||
interface TabletopElementProps {
|
||||
backgroundColor: RGB;
|
||||
backgroundUrl: URL | undefined;
|
||||
}
|
||||
|
||||
export const TabletopElement = ({ backgroundColor, backgroundUrl }: TabletopElementProps) => {
|
||||
const tabletopColorStyle = `rgb(${backgroundColor.red}, ${backgroundColor.green}, ${backgroundColor.blue})`;
|
||||
|
||||
return <div className="tabletop"> {backgroundUrl && <img src={backgroundUrl.toString()} alt="playing field" />} </div>
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
.thumbnail > img {
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Client } from '../../client';
|
||||
import './Thumbnail.css';
|
||||
|
||||
interface ThumbnailProps {
|
||||
id: string;
|
||||
url: URL;
|
||||
onclick?: () => void;
|
||||
}
|
||||
|
||||
export const ThumbnailElement = ({ id, url, onclick }: ThumbnailProps) => {
|
||||
const clickHandler = () => {
|
||||
if (onclick) { onclick(); }
|
||||
}
|
||||
return (<div id={id} className="thumbnail" onClick={clickHandler}> <img src={url.toString()} /> </div>)
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import React, { createContext, PropsWithChildren, useEffect, useReducer } from "react";
|
||||
import useWebSocket from "react-use-websocket";
|
||||
import { Message, Tabletop } from "visions-types";
|
||||
|
||||
type WebsocketState = { }
|
||||
|
||||
export const WebsocketContext = createContext<WebsocketState>({});
|
||||
|
||||
interface WebsocketProviderProps {
|
||||
websocketUrl: string;
|
||||
}
|
||||
|
||||
export const WebsocketProvider = ({ websocketUrl, children }: PropsWithChildren<WebsocketProviderProps>) => {
|
||||
return <div> {children} </div>;
|
||||
/*
|
||||
const { lastMessage } = useWebSocket(websocketUrl);
|
||||
|
||||
const [state, dispatch] = useReducer(handleMessage, initialState());
|
||||
|
||||
useEffect(() => {
|
||||
if (lastMessage !== null) {
|
||||
const message: Message = JSON.parse(lastMessage.data);
|
||||
dispatch(message);
|
||||
}
|
||||
}, [lastMessage]);
|
||||
|
||||
return (<WebsocketContext.Provider value={state}>
|
||||
{children}
|
||||
</WebsocketContext.Provider>);
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
const handleMessage = (state: TabletopState, message: Message): TabletopState => {
|
||||
console.log(message);
|
||||
switch (message.type) {
|
||||
case "UpdateTabletop": {
|
||||
return {
|
||||
...state,
|
||||
tabletop: message.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -1,5 +0,0 @@
|
|||
import { ThumbnailElement } from './Thumbnail/Thumbnail'
|
||||
import { TabletopElement } from './Tabletop/Tabletop'
|
||||
import { SimpleGuage } from './Guages/SimpleGuage'
|
||||
|
||||
export default { ThumbnailElement, TabletopElement, SimpleGuage }
|
|
@ -1,15 +0,0 @@
|
|||
:root {
|
||||
--border-standard: 2px solid black;
|
||||
--border-radius-standard: 4px;
|
||||
--border-shadow-shallow: 1px 1px 2px black;
|
||||
--padding-m: 8px;
|
||||
--margin-s: 4px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: var(--border-standard);
|
||||
border-radius: var(--border-radius-standard);
|
||||
box-shadow: var(--border-shadow-shallow);
|
||||
padding: var(--padding-m);
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,22 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { Client } from './client';
|
||||
|
||||
const client = new Client();
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App client={client} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
Before Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,11 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
const App = () => <div>App</div>;
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
|
@ -1,110 +0,0 @@
|
|||
.charsheet__header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.charsheet__header > div {
|
||||
margin: 8px;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.charsheet__body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.charsheet__body > div {
|
||||
margin: 8px;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
position: relative;
|
||||
border: 2px solid black;
|
||||
border-radius: 4px;
|
||||
|
||||
padding-left: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.action-group:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
.action-group__header {
|
||||
display: flex;
|
||||
font-size: xx-large;
|
||||
justify-content: space-between;
|
||||
margin: 4px;
|
||||
margin-left: -4px;
|
||||
padding-left: 4px;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-group__action {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.action-group__dots {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.action-group__guage-column {
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.action-group__guage-top {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.action-group__guage-top:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
left: 2px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.action-group__guage-top_filled {
|
||||
background-color: black;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.action-group__guage-bottom {
|
||||
background-color: white;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.action-group__guage-bottom_filled {
|
||||
background-color: black;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.action-group__guage-bottom:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
}
|
||||
|
||||
.action-group__guage {
|
||||
display: flex;
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
import React from 'react';
|
||||
import { assertNever } from '.';
|
||||
import './Charsheet.css';
|
||||
import { DriveGuage } from './DriveGuage/DriveGuage';
|
||||
import { Charsheet, Nerve, Cunning, Intuition } from './types';
|
||||
|
||||
interface CharsheetProps {
|
||||
sheet: Charsheet,
|
||||
}
|
||||
|
||||
interface ActionElementProps {
|
||||
name: string,
|
||||
gilded: boolean,
|
||||
value: number,
|
||||
}
|
||||
|
||||
const ActionElement = ({ name, gilded, value }: ActionElementProps) => {
|
||||
let dots = [];
|
||||
for (let i = 0; i < value; i++) {
|
||||
dots.push("\u25ef");
|
||||
}
|
||||
let diamond = gilded ? "\u25c6" : "\u25c7";
|
||||
return (<div>
|
||||
<h2 className="action-group__action"> {diamond} {name} </h2>
|
||||
<div className="action-group__dots"> {dots} </div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
interface ActionGroupElementProps {
|
||||
group: Nerve | Cunning | Intuition;
|
||||
}
|
||||
|
||||
const ActionGroupElement = ({ group }: ActionGroupElementProps) => {
|
||||
var title;
|
||||
var elements = [];
|
||||
|
||||
switch (group.type_) {
|
||||
case "nerve": {
|
||||
title = <div className="action-group__header"> Nerve <DriveGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Move" gilded={group.move.gilded} value={group.move.score} />);
|
||||
elements.push(<ActionElement name="Strike" gilded={group.strike.gilded} value={group.strike.score} />);
|
||||
elements.push(<ActionElement name="Control" gilded={group.control.gilded} value={group.control.score} />);
|
||||
break
|
||||
}
|
||||
case "cunning": {
|
||||
title = <div className="action-group__header"> Cunning <DriveGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Sway" gilded={group.sway.gilded} value={group.sway.score} />);
|
||||
elements.push(<ActionElement name="Read" gilded={group.read.gilded} value={group.read.score} />);
|
||||
elements.push(<ActionElement name="Hide" gilded={group.hide.gilded} value={group.hide.score} />);
|
||||
break
|
||||
}
|
||||
case "intuition": {
|
||||
title = <div className="action-group__header"> Intuition <DriveGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Survey" gilded={group.survey.gilded} value={group.survey.score} />);
|
||||
elements.push(<ActionElement name="Focus" gilded={group.focus.gilded} value={group.focus.score} />);
|
||||
elements.push(<ActionElement name="Sense" gilded={group.sense.gilded} value={group.sense.score} />);
|
||||
break
|
||||
}
|
||||
default: {
|
||||
assertNever(group);
|
||||
}
|
||||
}
|
||||
|
||||
return (<div className="action-group">
|
||||
{title}
|
||||
{elements}
|
||||
</div>)
|
||||
}
|
||||
|
||||
interface AbilitiesElementProps {
|
||||
role: string
|
||||
role_abilities: string[]
|
||||
specialty: string
|
||||
specialty_abilities: string[]
|
||||
}
|
||||
|
||||
const AbilitiesElement = ({ role, role_abilities, specialty, specialty_abilities }: AbilitiesElementProps) => {
|
||||
return (<div>
|
||||
<h1> ROLE: {role} </h1>
|
||||
<ul>
|
||||
{role_abilities.map((ability) => <li>{ability}</li>)}
|
||||
</ul>
|
||||
<h1> SPECIALTY: {role} </h1>
|
||||
<ul>
|
||||
{specialty_abilities.map((ability) => <li>{ability}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const CharsheetElement = ({ sheet }: CharsheetProps) => {
|
||||
return (<div>
|
||||
<div className="charsheet__header">
|
||||
<div> Candela Obscura </div>
|
||||
<div>
|
||||
<p> {sheet.name} </p>
|
||||
<p> {sheet.pronouns} </p>
|
||||
<p> {sheet.circle} </p>
|
||||
</div>
|
||||
<div>
|
||||
<p> {sheet.style} </p>
|
||||
<p> {sheet.catalyst} </p>
|
||||
<p> {sheet.question} </p>
|
||||
</div>
|
||||
</div >
|
||||
<div className="charsheet__body">
|
||||
<div>
|
||||
<ActionGroupElement group={sheet.nerve} />
|
||||
<ActionGroupElement group={sheet.cunning} />
|
||||
<ActionGroupElement group={sheet.intuition} />
|
||||
</div>
|
||||
<div> <AbilitiesElement {...sheet} /> </div>
|
||||
<div> Marks, Scars, Relationships </div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
/*
|
||||
export const CharsheetElement = () => {
|
||||
const sheet = {
|
||||
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 },
|
||||
} as Nerve,
|
||||
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 },
|
||||
} as Cunning,
|
||||
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 },
|
||||
} as Intuition,
|
||||
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've deduced.",
|
||||
],
|
||||
};
|
||||
|
||||
return <CharsheetElement_ sheet={sheet} />
|
||||
}
|
||||
*/
|
|
@ -1,24 +0,0 @@
|
|||
.candela-panel-actions {
|
||||
border: 1px solid black;
|
||||
padding: 4px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.candela-panel-actions__header {
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
justify-content: space-between;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.candela-panel-actions__action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.candela-panel-actions__action_gilded {
|
||||
font-weight: bold;
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
import React from 'react';
|
||||
import { assertNever } from '.';
|
||||
import { SimpleGuage } from '../../components/Guages/SimpleGuage';
|
||||
import { Charsheet, Nerve, Cunning, Intuition } from './types';
|
||||
import './CharsheetPanel.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface CharsheetPanelProps {
|
||||
sheet: Charsheet;
|
||||
}
|
||||
|
||||
interface ActionElementProps {
|
||||
name: string,
|
||||
gilded: boolean,
|
||||
value: number,
|
||||
}
|
||||
|
||||
const ActionElement = ({ name, gilded, value }: ActionElementProps) => {
|
||||
const className = gilded ? "candela-panel-actions__action_gilded" : "candela-panel-actions__action";
|
||||
return (<div className={classNames({
|
||||
"candela-panel-actions__action": true,
|
||||
"candela-panel-actions__action_gilded": gilded,
|
||||
})}> <div> {name} </div> <div> {value} </div> </div>);
|
||||
}
|
||||
|
||||
|
||||
interface ActionGroupElementProps {
|
||||
group: Nerve | Cunning | Intuition;
|
||||
}
|
||||
|
||||
const ActionGroupElement = ({ group }: ActionGroupElementProps) => {
|
||||
var title;
|
||||
var elements = [];
|
||||
|
||||
switch (group.type_) {
|
||||
case "nerve": {
|
||||
title = <div className="candela-panel-actions__header"> <div> Nerve </div> <SimpleGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Move" gilded={group.move.gilded} value={group.move.score} />);
|
||||
elements.push(<ActionElement name="Strike" gilded={group.strike.gilded} value={group.strike.score} />);
|
||||
elements.push(<ActionElement name="Control" gilded={group.control.gilded} value={group.control.score} />);
|
||||
break
|
||||
}
|
||||
case "cunning": {
|
||||
title = <div className="candela-panel-actions__header"> <div> Cunning </div> <SimpleGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Sway" gilded={group.sway.gilded} value={group.sway.score} />);
|
||||
elements.push(<ActionElement name="Read" gilded={group.read.gilded} value={group.read.score} />);
|
||||
elements.push(<ActionElement name="Hide" gilded={group.hide.gilded} value={group.hide.score} />);
|
||||
break
|
||||
}
|
||||
case "intuition": {
|
||||
title = <div className="candela-panel-actions__header"> <div> Intuition </div> <SimpleGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Survey" gilded={group.survey.gilded} value={group.survey.score} />);
|
||||
elements.push(<ActionElement name="Focus" gilded={group.focus.gilded} value={group.focus.score} />);
|
||||
elements.push(<ActionElement name="Sense" gilded={group.sense.gilded} value={group.sense.score} />);
|
||||
break
|
||||
}
|
||||
default: {
|
||||
assertNever(group);
|
||||
}
|
||||
}
|
||||
|
||||
return (<div className="candela-panel-actions">
|
||||
{title}
|
||||
{elements}
|
||||
</div>)
|
||||
}
|
||||
|
||||
|
||||
export const CharsheetPanelElement = ({ sheet }: CharsheetPanelProps) => {
|
||||
return (<div className="candela-panel">
|
||||
<div className="candela-panel__header">
|
||||
<p> {sheet.name} ({sheet.pronouns}) </p>
|
||||
<p> {sheet.specialty} </p>
|
||||
</div>
|
||||
|
||||
<div className="candela-panel__action-groups">
|
||||
<ActionGroupElement group={sheet.nerve} />
|
||||
<ActionGroupElement group={sheet.cunning} />
|
||||
<ActionGroupElement group={sheet.intuition} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul>
|
||||
{sheet.role_abilities.map((ability) => <li> {ability} </li>)}
|
||||
{sheet.specialty_abilities.map((ability) => <li> {ability} </li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
/*
|
||||
export const CharsheetPanelElement = () => {
|
||||
const sheet = {
|
||||
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 },
|
||||
} as Nerve,
|
||||
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 },
|
||||
} as Cunning,
|
||||
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 },
|
||||
} as Intuition,
|
||||
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've deduced.",
|
||||
],
|
||||
};
|
||||
|
||||
return <CharsheetPanelElement_ sheet={sheet} />
|
||||
}
|
||||
*/
|
|
@ -1,44 +0,0 @@
|
|||
.drive-guages_on-light {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.drive-guage {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.drive-guage__element {
|
||||
border: 1px solid black;
|
||||
margin: 1px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.drive-guage__spacer {
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.drive-guage__top {
|
||||
margin: 1px;
|
||||
width: 8px;
|
||||
border: 1px solid black;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.drive-guage__top_filled {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.drive-guage__bottom {
|
||||
margin: 1px;
|
||||
border: 1px solid black;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.drive-guage__bottom_filled {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.drive-guages_on-dark {
|
||||
background-color: black;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import React from 'react';
|
||||
import './DriveGuage.css';
|
||||
|
||||
interface Guage {
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export const DriveGuage = ({ current, max }: Guage) => {
|
||||
let components = [];
|
||||
for (let i = 0; i < 9; i++) {
|
||||
components.push(<div className="drive-guage__element">
|
||||
{i < current ? <div className="drive-guage__top drive-guage__top_filled"> </div> :
|
||||
<div className="drive-guage__top"> </div>}
|
||||
{i < max ? <div className="drive-guage__bottom drive-guage__bottom_filled"> </div> :
|
||||
<div className="drive-guage__bottom"> </div>}
|
||||
</div>)
|
||||
}
|
||||
components.splice(3, 0, <div className="drive-guage__spacer"> </div>);
|
||||
components.splice(7, 0, <div className="drive-guage__spacer"> </div>);
|
||||
return (<div className="drive-guage">
|
||||
{components}
|
||||
</div>)
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { CharsheetElement } from './Charsheet';
|
||||
import { CharsheetPanelElement } from './CharsheetPanel';
|
||||
|
||||
export function assertNever(value: never) {
|
||||
throw new Error("Unexpected value: " + value);
|
||||
}
|
||||
|
||||
export default { CharsheetElement, CharsheetPanelElement };
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
|
||||
export type Guage = {
|
||||
current: number,
|
||||
max: number,
|
||||
}
|
||||
|
||||
export type Action = {
|
||||
gilded: boolean,
|
||||
score: number,
|
||||
}
|
||||
|
||||
export type Actions = { [key: string]: Action }
|
||||
|
||||
export type ActionGroup = {
|
||||
drives: Guage,
|
||||
resistances: Guage,
|
||||
actions: Actions,
|
||||
}
|
||||
|
||||
export type Nerve = {
|
||||
type_: "nerve",
|
||||
drives: Guage,
|
||||
resistances: Guage,
|
||||
move: Action,
|
||||
strike: Action,
|
||||
control: Action,
|
||||
}
|
||||
|
||||
export type Cunning = {
|
||||
type_: "cunning",
|
||||
drives: Guage,
|
||||
resistances: Guage,
|
||||
sway: Action,
|
||||
read: Action,
|
||||
hide: Action,
|
||||
}
|
||||
|
||||
export type Intuition = {
|
||||
type_: "intuition",
|
||||
drives: Guage,
|
||||
resistances: Guage,
|
||||
survey: Action,
|
||||
focus: Action,
|
||||
sense: Action,
|
||||
}
|
||||
|
||||
export type Charsheet = {
|
||||
type_: string,
|
||||
name: string,
|
||||
pronouns: string
|
||||
circle: string
|
||||
style: string,
|
||||
catalyst: string,
|
||||
question: string,
|
||||
|
||||
nerve: Nerve,
|
||||
cunning: Cunning,
|
||||
intuition: Intuition,
|
||||
|
||||
role: string,
|
||||
role_abilities: string[],
|
||||
specialty: string,
|
||||
specialty_abilities: string[],
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer, useRef } from "react";
|
||||
import { Status, Tabletop } from "visions-types";
|
||||
import { Client } from "../../client";
|
||||
import { assertNever } from "../../plugins/Candela";
|
||||
|
||||
type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", userid: string };
|
||||
|
||||
type AppState = {
|
||||
auth: AuthState;
|
||||
tabletop: Tabletop;
|
||||
}
|
||||
|
||||
type Action = { type: "SetAuthState", content: AuthState };
|
||||
|
||||
const initialState = (): AppState => (
|
||||
{
|
||||
auth: { type: "NoAdmin" },
|
||||
tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined }
|
||||
}
|
||||
);
|
||||
|
||||
const stateReducer = (state: AppState, action: Action): AppState => {
|
||||
return { ...state, auth: action.content }
|
||||
}
|
||||
|
||||
class StateManager {
|
||||
client: Client | undefined;
|
||||
dispatch: React.Dispatch<Action> | undefined;
|
||||
|
||||
constructor(client: Client | undefined, dispatch: React.Dispatch<any> | undefined) {
|
||||
this.client = client;
|
||||
this.dispatch = dispatch;
|
||||
}
|
||||
|
||||
async status() {
|
||||
if (!this.client || !this.dispatch) return;
|
||||
|
||||
const { admin_enabled } = await this.client.status();
|
||||
if (!admin_enabled) {
|
||||
this.dispatch({ type: "SetAuthState", content: { type: "NoAdmin" } });
|
||||
} else {
|
||||
this.dispatch({ type: "SetAuthState", content: { type: "Unauthed" } });
|
||||
}
|
||||
}
|
||||
|
||||
async setAdminPassword(password: string) {
|
||||
if (!this.client || !this.dispatch) return;
|
||||
|
||||
await this.client.setAdminPassword(password);
|
||||
await this.status();
|
||||
}
|
||||
}
|
||||
|
||||
export const StateContext = createContext<[AppState, StateManager]>([initialState(), new StateManager(undefined, undefined)]);
|
||||
|
||||
interface StateProviderProps { client: Client; }
|
||||
|
||||
export const StateProvider = ({ client, children }: PropsWithChildren<StateProviderProps>) => {
|
||||
const [state, dispatch] = useReducer(stateReducer, initialState());
|
||||
|
||||
const stateManager = useRef(new StateManager(client, dispatch));
|
||||
|
||||
useEffect(() => {
|
||||
stateManager.current.status();
|
||||
}, [stateManager]);
|
||||
|
||||
return <StateContext.Provider value={[state, stateManager.current]}>
|
||||
{children}
|
||||
</StateContext.Provider>;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="react-scripts" />
|
|
@ -1,15 +0,0 @@
|
|||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
|
@ -1,5 +0,0 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
|
@ -1,47 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Game, User } from 'visions-types';
|
||||
import { Client } from '../../client';
|
||||
|
||||
interface UserRowProps {
|
||||
user: User,
|
||||
}
|
||||
|
||||
const UserRow = ({ user }: UserRowProps) => {
|
||||
return (<tr>
|
||||
<td> {user.name} </td>
|
||||
<td> {user.admin && "admin"} </td>
|
||||
<td> {user.enabled && "enabled"} </td>
|
||||
</tr>);
|
||||
}
|
||||
|
||||
interface GameRowProps {
|
||||
game: Game,
|
||||
}
|
||||
|
||||
const GameRow = ({ game }: GameRowProps) => {
|
||||
return (<tr>
|
||||
<td> {game.name} </td>
|
||||
</tr>);
|
||||
}
|
||||
|
||||
interface AdminProps {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
export const Admin = ({ client }: AdminProps) => {
|
||||
const [users, setUsers] = useState<Array<User>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
client.users().then((u) => {
|
||||
console.log(u);
|
||||
setUsers(u);
|
||||
});
|
||||
}, [client]);
|
||||
|
||||
console.log(users);
|
||||
return (<table>
|
||||
<tbody>
|
||||
{users.map((user) => <UserRow user={user} />)}
|
||||
</tbody>
|
||||
</table>);
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
@import '../../design.css';
|
||||
|
||||
.auth {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.auth__input-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.auth__input-line > * {
|
||||
margin: var(--margin-s);
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import { PropsWithChildren, useContext, useState } from 'react';
|
||||
import { StateContext } from '../../providers/StateProvider/StateProvider';
|
||||
import { assertNever } from '../../plugins/Candela';
|
||||
import './Authentication.css';
|
||||
|
||||
interface AuthenticationProps {
|
||||
onAdminPassword: (password: string) => void;
|
||||
onAuth: (username: string, password: string) => void;
|
||||
}
|
||||
|
||||
export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithChildren<AuthenticationProps>) => {
|
||||
// No admin password set: prompt for the admin password
|
||||
// Password set, nobody logged in: prompt for login
|
||||
// User logged in: show the children
|
||||
|
||||
let [userField, setUserField] = useState<string>("");
|
||||
let [pwField, setPwField] = useState<string>("");
|
||||
let [state, _] = useContext(StateContext);
|
||||
|
||||
switch (state.auth.type) {
|
||||
case "NoAdmin": {
|
||||
return <div className="auth">
|
||||
<div className="card">
|
||||
<h1> Welcome to your new Visions VTT Instance </h1>
|
||||
<p> Set your admin password: </p>
|
||||
<input type="password" placeholder="Password" onChange={(evt) => setPwField(evt.target.value)} />
|
||||
<input type="submit" value="Submit" onClick={() => onAdminPassword(pwField)} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
case "Unauthed": {
|
||||
return <div className="auth card">
|
||||
<div className="card">
|
||||
<h1> Welcome to Visions VTT </h1>
|
||||
<div className="auth__input-line">
|
||||
<input type="text" placeholder="Username" onChange={(evt) => setUserField(evt.target.value)} />
|
||||
<input type="password" placeholder="Password" onChange={(evt) => setPwField(evt.target.value)} />
|
||||
<input type="submit" value="Sign in" onClick={() => onAuth(userField, pwField)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
case "Authed": {
|
||||
return <div> {children} </div>;
|
||||
}
|
||||
default: {
|
||||
assertNever(state.auth);
|
||||
return <div></div>;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
.section {
|
||||
border: 2px solid black;
|
||||
border-radius: 4px;
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import React from 'react';
|
||||
import { DriveGuage } from '../../plugins/Candela/DriveGuage/DriveGuage';
|
||||
|
||||
const DriveGuages = () => {
|
||||
return (<div className="section">
|
||||
<div className="drive-guages_on-light">
|
||||
<DriveGuage current={2} max={4} />
|
||||
</div>
|
||||
<div className="drive-guages_on-dark">
|
||||
<DriveGuage current={2} max={4} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
export const DesignPage = () => {
|
||||
return (<div>
|
||||
<DriveGuages />
|
||||
</div>);
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
.gm-view {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { Client } from '../../client';
|
||||
import { StateContext } from '../../providers/StateProvider/StateProvider';
|
||||
import { TabletopElement } from '../../components/Tabletop/Tabletop';
|
||||
import { ThumbnailElement } from '../../components/Thumbnail/Thumbnail';
|
||||
import { WebsocketContext } from '../../components/WebsocketProvider';
|
||||
import './GmView.css';
|
||||
|
||||
interface GmViewProps {
|
||||
client: Client
|
||||
}
|
||||
|
||||
export const GmView = ({ client }: GmViewProps) => {
|
||||
const [state, dispatch] = useContext(StateContext);
|
||||
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
client.availableImages().then((images) => setImages(images));
|
||||
}, [client]);
|
||||
|
||||
const backgroundUrl = state.tabletop.backgroundImage ? client.imageUrl(state.tabletop.backgroundImage) : undefined;
|
||||
return (<div className="gm-view">
|
||||
<div>
|
||||
{images.map((imageName) => <ThumbnailElement id={imageName} url={client.imageUrl(imageName)} onclick={() => { client.setBackgroundImage(imageName); }} />)}
|
||||
</div>
|
||||
<TabletopElement backgroundColor={state.tabletop.backgroundColor} backgroundUrl={backgroundUrl} />
|
||||
</div>)
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
.player-view {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.player-view__left-panel {
|
||||
min-width: 100px;
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
.player-view__right-panel {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import './PlayerView.css';
|
||||
import { WebsocketContext } from '../../components/WebsocketProvider';
|
||||
import { Client } from '../../client';
|
||||
import { TabletopElement } from '../../components/Tabletop/Tabletop';
|
||||
import Candela from '../../plugins/Candela';
|
||||
import { StateContext } from '../../providers/StateProvider/StateProvider';
|
||||
|
||||
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803";
|
||||
|
||||
interface PlayerViewProps {
|
||||
client: Client;
|
||||
}
|
||||
|
||||
export const PlayerView = ({ client }: PlayerViewProps) => {
|
||||
const [state, dispatch] = useContext(StateContext);
|
||||
|
||||
const [charsheet, setCharsheet] = useState(undefined);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
client.charsheet(TEST_CHARSHEET_UUID).then((c) => {
|
||||
setCharsheet(c)
|
||||
});
|
||||
},
|
||||
[client, setCharsheet]
|
||||
);
|
||||
|
||||
const backgroundColor = state.tabletop.backgroundColor;
|
||||
const tabletopColorStyle = `rgb(${backgroundColor.red}, ${backgroundColor.green}, ${backgroundColor.blue})`;
|
||||
const backgroundUrl = state.tabletop.backgroundImage ? client.imageUrl(state.tabletop.backgroundImage) : undefined;
|
||||
|
||||
return (<div className="player-view" style={{ backgroundColor: tabletopColorStyle }}>
|
||||
<div className="player-view__middle-panel"> <TabletopElement backgroundColor={backgroundColor} backgroundUrl={backgroundUrl} /> </div>
|
||||
<div className="player-view__right-panel">
|
||||
{charsheet ? <Candela.CharsheetPanelElement sheet={charsheet} /> : <div> </div>}</div>
|
||||
</div>)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue