Compare commits
27 Commits
music-play
...
main
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 81d452694d | |
Savanni D'Gerinel | 88cf32047b | |
Savanni D'Gerinel | 6cae7dbb0e | |
Savanni D'Gerinel | 80776c65d8 | |
Savanni D'Gerinel | 1c54e0832b | |
Savanni D'Gerinel | aee4528fb3 | |
Savanni D'Gerinel | 0535b6da5a | |
Savanni D'Gerinel | b55324feab | |
Savanni D'Gerinel | 50d8a9670e | |
Savanni D'Gerinel | 9cda35e766 | |
Savanni D'Gerinel | d0f461a5eb | |
Savanni D'Gerinel | 70c013218a | |
Savanni D'Gerinel | 37c7e04820 | |
Savanni D'Gerinel | 291663d4a3 | |
Savanni D'Gerinel | 2b0fc7639e | |
Savanni D'Gerinel | 80d8dedbaf | |
Savanni D'Gerinel | d7a70119c8 | |
Savanni D'Gerinel | 54c4b99ab6 | |
Savanni D'Gerinel | ef5415303b | |
Savanni D'Gerinel | 8d183d6d8c | |
Savanni D'Gerinel | 0b949111d2 | |
Savanni D'Gerinel | 6164cb3b39 | |
Savanni D'Gerinel | 22f0f9061c | |
Savanni D'Gerinel | 0bb5e62f96 | |
Savanni D'Gerinel | 06aedc34bb | |
Savanni D'Gerinel | 84b077e20c | |
Savanni D'Gerinel | fc2e88add2 |
|
@ -2,6 +2,9 @@
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"authdb",
|
"authdb",
|
||||||
|
"bike-lights/bike",
|
||||||
|
"bike-lights/core",
|
||||||
|
"bike-lights/simulator",
|
||||||
"changeset",
|
"changeset",
|
||||||
"config",
|
"config",
|
||||||
"config-derive",
|
"config-derive",
|
||||||
|
@ -19,7 +22,6 @@ members = [
|
||||||
"icon-test",
|
"icon-test",
|
||||||
"ifc",
|
"ifc",
|
||||||
"memorycache",
|
"memorycache",
|
||||||
"music-player/server",
|
|
||||||
"nom-training",
|
"nom-training",
|
||||||
"otg/core",
|
"otg/core",
|
||||||
"otg/gtk",
|
"otg/gtk",
|
||||||
|
@ -28,5 +30,5 @@ members = [
|
||||||
"sgf",
|
"sgf",
|
||||||
"timezone-testing",
|
"timezone-testing",
|
||||||
"tree",
|
"tree",
|
||||||
"visions/server",
|
"visions/server", "gm-dash/server",
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
[build]
|
||||||
|
target = "thumbv6m-none-eabi"
|
||||||
|
|
||||||
|
[target.thumbv6m-none-eabi]
|
||||||
|
rustflags = [
|
||||||
|
"-C", "link-arg=--nmagic",
|
||||||
|
"-C", "link-arg=-Tlink.x",
|
||||||
|
"-C", "inline-threshold=5",
|
||||||
|
"-C", "no-vectorize-loops",
|
||||||
|
]
|
||||||
|
|
||||||
|
runner = "elf2uf2-rs -d"
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "bike"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
az = { version = "1" }
|
||||||
|
cortex-m-rt = { version = "0.7.3" }
|
||||||
|
cortex-m = { version = "0.7.7" }
|
||||||
|
embedded-alloc = { version = "0.5.1" }
|
||||||
|
embedded-hal = { version = "0.2.7" }
|
||||||
|
fixed = { version = "1" }
|
||||||
|
fugit = { version = "0.3.7" }
|
||||||
|
lights-core = { path = "../core" }
|
||||||
|
panic-halt = { version = "0.2.0" }
|
||||||
|
rp-pico = { version = "0.8.0" }
|
|
@ -0,0 +1,241 @@
|
||||||
|
#![no_main]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
use az::*;
|
||||||
|
use core::cell::RefCell;
|
||||||
|
use cortex_m::delay::Delay;
|
||||||
|
use embedded_alloc::Heap;
|
||||||
|
use embedded_hal::{blocking::spi::Write, digital::v2::InputPin, digital::v2::OutputPin};
|
||||||
|
use fixed::types::I16F16;
|
||||||
|
use fugit::RateExtU32;
|
||||||
|
use lights_core::{App, BodyPattern, DashboardPattern, Event, Instant, FPS, UI};
|
||||||
|
use panic_halt as _;
|
||||||
|
use rp_pico::{
|
||||||
|
entry,
|
||||||
|
hal::{
|
||||||
|
clocks::init_clocks_and_plls,
|
||||||
|
gpio::{FunctionSio, Pin, PinId, PullDown, PullUp, SioInput, SioOutput},
|
||||||
|
pac::{CorePeripherals, Peripherals},
|
||||||
|
spi::{Enabled, Spi, SpiDevice, ValidSpiPinout},
|
||||||
|
watchdog::Watchdog,
|
||||||
|
Clock, Sio,
|
||||||
|
},
|
||||||
|
Pins,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static HEAP: Heap = Heap::empty();
|
||||||
|
|
||||||
|
const LIGHT_SCALE: I16F16 = I16F16::lit("256.0");
|
||||||
|
const DASHBOARD_BRIGHTESS: u8 = 1;
|
||||||
|
const BODY_BRIGHTNESS: u8 = 8;
|
||||||
|
|
||||||
|
struct DebouncedButton<P: PinId> {
|
||||||
|
debounce: Instant,
|
||||||
|
pin: Pin<P, FunctionSio<SioInput>, PullUp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: PinId> DebouncedButton<P> {
|
||||||
|
fn new(pin: Pin<P, FunctionSio<SioInput>, PullUp>) -> Self {
|
||||||
|
Self {
|
||||||
|
debounce: Instant((0 as u32).into()),
|
||||||
|
pin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_low(&self, time: Instant) -> bool {
|
||||||
|
if time <= self.debounce {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.pin.is_low().unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_debounce(&mut self, time: Instant) {
|
||||||
|
self.debounce = time + Instant((250 as u32).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BikeUI<
|
||||||
|
D: SpiDevice,
|
||||||
|
P: ValidSpiPinout<D>,
|
||||||
|
LeftId: PinId,
|
||||||
|
RightId: PinId,
|
||||||
|
PreviousId: PinId,
|
||||||
|
NextId: PinId,
|
||||||
|
BrakeId: PinId,
|
||||||
|
> {
|
||||||
|
spi: RefCell<Spi<Enabled, D, P, 8>>,
|
||||||
|
left_blinker_button: DebouncedButton<LeftId>,
|
||||||
|
right_blinker_button: DebouncedButton<RightId>,
|
||||||
|
previous_animation_button: DebouncedButton<PreviousId>,
|
||||||
|
next_animation_button: DebouncedButton<NextId>,
|
||||||
|
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
|
||||||
|
brake_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
D: SpiDevice,
|
||||||
|
P: ValidSpiPinout<D>,
|
||||||
|
LeftId: PinId,
|
||||||
|
RightId: PinId,
|
||||||
|
PreviousId: PinId,
|
||||||
|
NextId: PinId,
|
||||||
|
BrakeId: PinId,
|
||||||
|
> BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
|
||||||
|
{
|
||||||
|
fn new(
|
||||||
|
spi: Spi<Enabled, D, P, 8>,
|
||||||
|
left_blinker_button: Pin<LeftId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
right_blinker_button: Pin<RightId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
previous_animation_button: Pin<PreviousId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
next_animation_button: Pin<NextId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
spi: RefCell::new(spi),
|
||||||
|
left_blinker_button: DebouncedButton::new(left_blinker_button),
|
||||||
|
right_blinker_button: DebouncedButton::new(right_blinker_button),
|
||||||
|
previous_animation_button: DebouncedButton::new(previous_animation_button),
|
||||||
|
next_animation_button: DebouncedButton::new(next_animation_button),
|
||||||
|
brake_sensor,
|
||||||
|
|
||||||
|
brake_enabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
D: SpiDevice,
|
||||||
|
P: ValidSpiPinout<D>,
|
||||||
|
LeftId: PinId,
|
||||||
|
RightId: PinId,
|
||||||
|
PreviousId: PinId,
|
||||||
|
NextId: PinId,
|
||||||
|
BrakeId: PinId,
|
||||||
|
> UI for BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
|
||||||
|
{
|
||||||
|
fn check_event(&mut self, current_time: Instant) -> Option<Event> {
|
||||||
|
if self.brake_sensor.is_high().unwrap_or(true) && !self.brake_enabled {
|
||||||
|
self.brake_enabled = true;
|
||||||
|
Some(Event::Brake)
|
||||||
|
} else if self.brake_sensor.is_low().unwrap_or(false) && self.brake_enabled {
|
||||||
|
self.brake_enabled = false;
|
||||||
|
Some(Event::BrakeRelease)
|
||||||
|
} else if self.left_blinker_button.is_low(current_time) {
|
||||||
|
self.left_blinker_button.set_debounce(current_time);
|
||||||
|
Some(Event::LeftBlinker)
|
||||||
|
} else if self.right_blinker_button.is_low(current_time) {
|
||||||
|
self.right_blinker_button.set_debounce(current_time);
|
||||||
|
Some(Event::RightBlinker)
|
||||||
|
} else if self.previous_animation_button.is_low(current_time) {
|
||||||
|
self.previous_animation_button.set_debounce(current_time);
|
||||||
|
Some(Event::PreviousPattern)
|
||||||
|
} else if self.next_animation_button.is_low(current_time) {
|
||||||
|
self.next_animation_button.set_debounce(current_time);
|
||||||
|
Some(Event::NextPattern)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern) {
|
||||||
|
let mut lights: [u8; 260] = [0; 260];
|
||||||
|
lights[256] = 0xff;
|
||||||
|
lights[257] = 0xff;
|
||||||
|
lights[258] = 0xff;
|
||||||
|
lights[259] = 0xff;
|
||||||
|
for (idx, rgb) in dashboard_lights.iter().enumerate() {
|
||||||
|
lights[(idx + 1) * 4 + 0] = 0xe0 + DASHBOARD_BRIGHTESS;
|
||||||
|
lights[(idx + 1) * 4 + 1] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 1) * 4 + 2] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 1) * 4 + 3] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
|
||||||
|
}
|
||||||
|
for (idx, rgb) in body_lights.iter().enumerate() {
|
||||||
|
lights[(idx + 4) * 4 + 0] = 0xe0 + BODY_BRIGHTNESS;
|
||||||
|
lights[(idx + 4) * 4 + 1] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 4) * 4 + 2] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 4) * 4 + 3] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
|
||||||
|
}
|
||||||
|
let mut spi = self.spi.borrow_mut();
|
||||||
|
spi.write(lights.as_slice());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[entry]
|
||||||
|
fn main() -> ! {
|
||||||
|
{
|
||||||
|
use core::mem::MaybeUninit;
|
||||||
|
const HEAP_SIZE: usize = 8096;
|
||||||
|
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||||
|
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pac = Peripherals::take().unwrap();
|
||||||
|
let core = CorePeripherals::take().unwrap();
|
||||||
|
let sio = Sio::new(pac.SIO);
|
||||||
|
let mut watchdog = Watchdog::new(pac.WATCHDOG);
|
||||||
|
|
||||||
|
let pins = Pins::new(
|
||||||
|
pac.IO_BANK0,
|
||||||
|
pac.PADS_BANK0,
|
||||||
|
sio.gpio_bank0,
|
||||||
|
&mut pac.RESETS,
|
||||||
|
);
|
||||||
|
|
||||||
|
let clocks = init_clocks_and_plls(
|
||||||
|
12_000_000u32,
|
||||||
|
pac.XOSC,
|
||||||
|
pac.CLOCKS,
|
||||||
|
pac.PLL_SYS,
|
||||||
|
pac.PLL_USB,
|
||||||
|
&mut pac.RESETS,
|
||||||
|
&mut watchdog,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
|
||||||
|
let mut spi_clk = pins.gpio10.into_function();
|
||||||
|
let mut spi_sdo = pins.gpio11.into_function();
|
||||||
|
let spi = Spi::<_, _, _, 8>::new(pac.SPI1, (spi_sdo, spi_clk));
|
||||||
|
let mut spi = spi.init(
|
||||||
|
&mut pac.RESETS,
|
||||||
|
clocks.peripheral_clock.freq(),
|
||||||
|
1_u32.MHz(),
|
||||||
|
embedded_hal::spi::MODE_1,
|
||||||
|
);
|
||||||
|
|
||||||
|
let left_blinker_button = pins.gpio16.into_pull_up_input();
|
||||||
|
let right_blinker_button = pins.gpio17.into_pull_up_input();
|
||||||
|
let previous_animation_button = pins.gpio27.into_pull_up_input();
|
||||||
|
let next_animation_button = pins.gpio26.into_pull_up_input();
|
||||||
|
let brake_sensor = pins.gpio18.into_pull_up_input();
|
||||||
|
|
||||||
|
let mut led_pin = pins.led.into_push_pull_output();
|
||||||
|
|
||||||
|
let ui = BikeUI::new(
|
||||||
|
spi,
|
||||||
|
left_blinker_button,
|
||||||
|
right_blinker_button,
|
||||||
|
previous_animation_button,
|
||||||
|
next_animation_button,
|
||||||
|
brake_sensor,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut app = App::new(Box::new(ui));
|
||||||
|
|
||||||
|
led_pin.set_high();
|
||||||
|
|
||||||
|
let mut time = Instant::default();
|
||||||
|
let delay_ms = 1000 / (FPS as u32);
|
||||||
|
loop {
|
||||||
|
app.tick(time);
|
||||||
|
|
||||||
|
delay.delay_ms(delay_ms);
|
||||||
|
time = time + Instant(delay_ms.into());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "lights-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
az = { version = "1" }
|
||||||
|
fixed = { version = "1" }
|
|
@ -0,0 +1,481 @@
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
use az::*;
|
||||||
|
use core::{
|
||||||
|
clone::Clone,
|
||||||
|
cmp::PartialEq,
|
||||||
|
default::Default,
|
||||||
|
ops::{Add, Sub},
|
||||||
|
option::Option,
|
||||||
|
};
|
||||||
|
use fixed::types::{I48F16, I8F8, U128F0, U16F0};
|
||||||
|
|
||||||
|
mod patterns;
|
||||||
|
pub use patterns::*;
|
||||||
|
|
||||||
|
mod types;
|
||||||
|
pub use types::{BodyPattern, DashboardPattern, RGB};
|
||||||
|
|
||||||
|
fn calculate_frames(starting_time: U128F0, now: U128F0) -> U16F0 {
|
||||||
|
let frames_128 = (now - starting_time) / U128F0::from(FPS);
|
||||||
|
(frames_128 % U128F0::from(U16F0::MAX)).cast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_slope(start: I8F8, end: I8F8, frames: U16F0) -> I8F8 {
|
||||||
|
let slope_i16f16 = (I48F16::from(end) - I48F16::from(start)) / I48F16::from(frames);
|
||||||
|
slope_i16f16.saturating_as()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linear_ease(value: I8F8, frames: U16F0, slope: I8F8) -> I8F8 {
|
||||||
|
let value_i16f16 = I48F16::from(value) + I48F16::from(frames) * I48F16::from(slope);
|
||||||
|
value_i16f16.saturating_as()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
|
pub struct Instant(pub U128F0);
|
||||||
|
|
||||||
|
impl Default for Instant {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(U128F0::from(0 as u8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add for Instant {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, r: Self) -> Self::Output {
|
||||||
|
Self(self.0 + r.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub for Instant {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, r: Self) -> Self::Output {
|
||||||
|
Self(self.0 - r.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FPS: u8 = 30;
|
||||||
|
|
||||||
|
pub trait UI {
|
||||||
|
fn check_event(&mut self, current_time: Instant) -> Option<Event>;
|
||||||
|
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Animation {
|
||||||
|
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub struct DefaultAnimation {}
|
||||||
|
|
||||||
|
impl Animation for DefaultAnimation {
|
||||||
|
fn tick(&mut self, _: Instant) -> (DashboardPattern, BodyPattern) {
|
||||||
|
(WATER_DASHBOARD, WATER_BODY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub struct Fade {
|
||||||
|
starting_dashboard: DashboardPattern,
|
||||||
|
starting_lights: BodyPattern,
|
||||||
|
|
||||||
|
start_time: Instant,
|
||||||
|
dashboard_slope: [RGB<I8F8>; 3],
|
||||||
|
body_slope: [RGB<I8F8>; 60],
|
||||||
|
frames: U16F0,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fade {
|
||||||
|
fn new(
|
||||||
|
dashboard: DashboardPattern,
|
||||||
|
lights: BodyPattern,
|
||||||
|
ending_dashboard: DashboardPattern,
|
||||||
|
ending_lights: BodyPattern,
|
||||||
|
frames: U16F0,
|
||||||
|
time: Instant,
|
||||||
|
) -> Self {
|
||||||
|
let mut dashboard_slope = [Default::default(); 3];
|
||||||
|
let mut body_slope = [Default::default(); 60];
|
||||||
|
for i in 0..3 {
|
||||||
|
let slope = RGB {
|
||||||
|
r: calculate_slope(dashboard[i].r, ending_dashboard[i].r, frames),
|
||||||
|
g: calculate_slope(dashboard[i].g, ending_dashboard[i].g, frames),
|
||||||
|
b: calculate_slope(dashboard[i].b, ending_dashboard[i].b, frames),
|
||||||
|
};
|
||||||
|
dashboard_slope[i] = slope;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..60 {
|
||||||
|
let slope = RGB {
|
||||||
|
r: calculate_slope(lights[i].r, ending_lights[i].r, frames),
|
||||||
|
g: calculate_slope(lights[i].g, ending_lights[i].g, frames),
|
||||||
|
b: calculate_slope(lights[i].b, ending_lights[i].b, frames),
|
||||||
|
};
|
||||||
|
body_slope[i] = slope;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
starting_dashboard: dashboard,
|
||||||
|
starting_lights: lights,
|
||||||
|
start_time: time,
|
||||||
|
dashboard_slope,
|
||||||
|
body_slope,
|
||||||
|
frames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation for Fade {
|
||||||
|
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern) {
|
||||||
|
let mut frames = calculate_frames(self.start_time.0, time.0);
|
||||||
|
if frames > self.frames {
|
||||||
|
frames = self.frames
|
||||||
|
}
|
||||||
|
let mut dashboard_pattern: DashboardPattern = OFF_DASHBOARD;
|
||||||
|
let mut body_pattern: BodyPattern = OFF_BODY;
|
||||||
|
|
||||||
|
for i in 0..3 {
|
||||||
|
dashboard_pattern[i].r = linear_ease(
|
||||||
|
self.starting_dashboard[i].r,
|
||||||
|
frames,
|
||||||
|
self.dashboard_slope[i].r,
|
||||||
|
);
|
||||||
|
dashboard_pattern[i].g = linear_ease(
|
||||||
|
self.starting_dashboard[i].g,
|
||||||
|
frames,
|
||||||
|
self.dashboard_slope[i].g,
|
||||||
|
);
|
||||||
|
dashboard_pattern[i].b = linear_ease(
|
||||||
|
self.starting_dashboard[i].b,
|
||||||
|
frames,
|
||||||
|
self.dashboard_slope[i].b,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..60 {
|
||||||
|
body_pattern[i].r =
|
||||||
|
linear_ease(self.starting_lights[i].r, frames, self.body_slope[i].r);
|
||||||
|
body_pattern[i].g =
|
||||||
|
linear_ease(self.starting_lights[i].g, frames, self.body_slope[i].g);
|
||||||
|
body_pattern[i].b =
|
||||||
|
linear_ease(self.starting_lights[i].b, frames, self.body_slope[i].b);
|
||||||
|
}
|
||||||
|
|
||||||
|
(dashboard_pattern, body_pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FadeDirection {
|
||||||
|
Transition,
|
||||||
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BlinkerDirection {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Blinker {
|
||||||
|
transition: Fade,
|
||||||
|
fade_in: Fade,
|
||||||
|
fade_out: Fade,
|
||||||
|
direction: FadeDirection,
|
||||||
|
|
||||||
|
start_time: Instant,
|
||||||
|
frames: U16F0,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Blinker {
|
||||||
|
fn new(
|
||||||
|
starting_dashboard: DashboardPattern,
|
||||||
|
starting_body: BodyPattern,
|
||||||
|
direction: BlinkerDirection,
|
||||||
|
time: Instant,
|
||||||
|
) -> Self {
|
||||||
|
let mut ending_dashboard = OFF_DASHBOARD.clone();
|
||||||
|
|
||||||
|
match direction {
|
||||||
|
BlinkerDirection::Left => {
|
||||||
|
ending_dashboard[0].r = LEFT_BLINKER_DASHBOARD[0].r;
|
||||||
|
ending_dashboard[0].g = LEFT_BLINKER_DASHBOARD[0].g;
|
||||||
|
ending_dashboard[0].b = LEFT_BLINKER_DASHBOARD[0].b;
|
||||||
|
}
|
||||||
|
BlinkerDirection::Right => {
|
||||||
|
ending_dashboard[2].r = RIGHT_BLINKER_DASHBOARD[2].r;
|
||||||
|
ending_dashboard[2].g = RIGHT_BLINKER_DASHBOARD[2].g;
|
||||||
|
ending_dashboard[2].b = RIGHT_BLINKER_DASHBOARD[2].b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ending_body = OFF_BODY.clone();
|
||||||
|
match direction {
|
||||||
|
BlinkerDirection::Left => {
|
||||||
|
for i in 0..30 {
|
||||||
|
ending_body[i].r = LEFT_BLINKER_BODY[i].r;
|
||||||
|
ending_body[i].g = LEFT_BLINKER_BODY[i].g;
|
||||||
|
ending_body[i].b = LEFT_BLINKER_BODY[i].b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BlinkerDirection::Right => {
|
||||||
|
for i in 30..60 {
|
||||||
|
ending_body[i].r = RIGHT_BLINKER_BODY[i].r;
|
||||||
|
ending_body[i].g = RIGHT_BLINKER_BODY[i].g;
|
||||||
|
ending_body[i].b = RIGHT_BLINKER_BODY[i].b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Blinker {
|
||||||
|
transition: Fade::new(
|
||||||
|
starting_dashboard.clone(),
|
||||||
|
starting_body.clone(),
|
||||||
|
ending_dashboard.clone(),
|
||||||
|
ending_body.clone(),
|
||||||
|
BLINKER_FRAMES,
|
||||||
|
time,
|
||||||
|
),
|
||||||
|
fade_in: Fade::new(
|
||||||
|
OFF_DASHBOARD.clone(),
|
||||||
|
OFF_BODY.clone(),
|
||||||
|
ending_dashboard.clone(),
|
||||||
|
ending_body.clone(),
|
||||||
|
BLINKER_FRAMES,
|
||||||
|
time,
|
||||||
|
),
|
||||||
|
fade_out: Fade::new(
|
||||||
|
ending_dashboard.clone(),
|
||||||
|
ending_body.clone(),
|
||||||
|
OFF_DASHBOARD.clone(),
|
||||||
|
OFF_BODY.clone(),
|
||||||
|
BLINKER_FRAMES,
|
||||||
|
time,
|
||||||
|
),
|
||||||
|
direction: FadeDirection::Transition,
|
||||||
|
start_time: time,
|
||||||
|
frames: BLINKER_FRAMES,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation for Blinker {
|
||||||
|
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern) {
|
||||||
|
let frames = calculate_frames(self.start_time.0, time.0);
|
||||||
|
if frames > self.frames {
|
||||||
|
match self.direction {
|
||||||
|
FadeDirection::Transition => {
|
||||||
|
self.direction = FadeDirection::FadeOut;
|
||||||
|
self.fade_out.start_time = time;
|
||||||
|
}
|
||||||
|
FadeDirection::FadeIn => {
|
||||||
|
self.direction = FadeDirection::FadeOut;
|
||||||
|
self.fade_out.start_time = time;
|
||||||
|
}
|
||||||
|
FadeDirection::FadeOut => {
|
||||||
|
self.direction = FadeDirection::FadeIn;
|
||||||
|
self.fade_in.start_time = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.start_time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.direction {
|
||||||
|
FadeDirection::Transition => self.transition.tick(time),
|
||||||
|
FadeDirection::FadeIn => self.fade_in.tick(time),
|
||||||
|
FadeDirection::FadeOut => self.fade_out.tick(time),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
Brake,
|
||||||
|
BrakeRelease,
|
||||||
|
LeftBlinker,
|
||||||
|
NextPattern,
|
||||||
|
PreviousPattern,
|
||||||
|
RightBlinker,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Pattern {
|
||||||
|
Water,
|
||||||
|
GayPride,
|
||||||
|
TransPride,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pattern {
|
||||||
|
fn previous(&self) -> Pattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => Pattern::TransPride,
|
||||||
|
Pattern::GayPride => Pattern::Water,
|
||||||
|
Pattern::TransPride => Pattern::GayPride,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&self) -> Pattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => Pattern::GayPride,
|
||||||
|
Pattern::GayPride => Pattern::TransPride,
|
||||||
|
Pattern::TransPride => Pattern::Water,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dashboard(&self) -> DashboardPattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => WATER_DASHBOARD,
|
||||||
|
Pattern::GayPride => PRIDE_DASHBOARD,
|
||||||
|
Pattern::TransPride => TRANS_PRIDE_DASHBOARD,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body(&self) -> BodyPattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => OFF_BODY,
|
||||||
|
Pattern::GayPride => PRIDE_BODY,
|
||||||
|
Pattern::TransPride => TRANS_PRIDE_BODY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum State {
|
||||||
|
Pattern(Pattern),
|
||||||
|
Brake,
|
||||||
|
LeftBlinker,
|
||||||
|
RightBlinker,
|
||||||
|
BrakeLeftBlinker,
|
||||||
|
BrakeRightBlinker,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
ui: Box<dyn UI>,
|
||||||
|
state: State,
|
||||||
|
home_pattern: Pattern,
|
||||||
|
current_animation: Box<dyn Animation>,
|
||||||
|
dashboard_lights: DashboardPattern,
|
||||||
|
lights: BodyPattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(ui: Box<dyn UI>) -> Self {
|
||||||
|
let pattern = Pattern::Water;
|
||||||
|
Self {
|
||||||
|
ui,
|
||||||
|
state: State::Pattern(pattern),
|
||||||
|
home_pattern: pattern,
|
||||||
|
current_animation: Box::new(Fade::new(
|
||||||
|
OFF_DASHBOARD,
|
||||||
|
OFF_BODY,
|
||||||
|
pattern.dashboard(),
|
||||||
|
pattern.body(),
|
||||||
|
DEFAULT_FRAMES,
|
||||||
|
Instant((0 as u32).into()),
|
||||||
|
)),
|
||||||
|
dashboard_lights: OFF_DASHBOARD,
|
||||||
|
lights: OFF_BODY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_animation(&mut self, time: Instant) {
|
||||||
|
match self.state {
|
||||||
|
State::Pattern(ref pattern) => {
|
||||||
|
self.current_animation = Box::new(Fade::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
pattern.dashboard(),
|
||||||
|
pattern.body(),
|
||||||
|
DEFAULT_FRAMES,
|
||||||
|
time,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
State::Brake => {
|
||||||
|
self.current_animation = Box::new(Fade::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
BRAKES_DASHBOARD,
|
||||||
|
BRAKES_BODY,
|
||||||
|
BRAKES_FRAMES,
|
||||||
|
time,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
State::LeftBlinker => {
|
||||||
|
self.current_animation = Box::new(Blinker::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
BlinkerDirection::Left,
|
||||||
|
time,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
State::RightBlinker => {
|
||||||
|
self.current_animation = Box::new(Blinker::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
BlinkerDirection::Right,
|
||||||
|
time,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
State::BrakeLeftBlinker => (),
|
||||||
|
State::BrakeRightBlinker => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_state(&mut self, event: Event) {
|
||||||
|
match event {
|
||||||
|
Event::Brake => {
|
||||||
|
if self.state == State::Brake {
|
||||||
|
self.state = State::Pattern(self.home_pattern);
|
||||||
|
} else {
|
||||||
|
self.state = State::Brake;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::BrakeRelease => self.state = State::Pattern(self.home_pattern),
|
||||||
|
Event::LeftBlinker => match self.state {
|
||||||
|
State::Brake => self.state = State::BrakeLeftBlinker,
|
||||||
|
State::BrakeLeftBlinker => self.state = State::Brake,
|
||||||
|
State::LeftBlinker => self.state = State::Pattern(self.home_pattern),
|
||||||
|
_ => self.state = State::LeftBlinker,
|
||||||
|
},
|
||||||
|
Event::NextPattern => match self.state {
|
||||||
|
State::Pattern(ref pattern) => {
|
||||||
|
self.home_pattern = pattern.next();
|
||||||
|
self.state = State::Pattern(self.home_pattern);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Event::PreviousPattern => match self.state {
|
||||||
|
State::Pattern(ref pattern) => {
|
||||||
|
self.home_pattern = pattern.previous();
|
||||||
|
self.state = State::Pattern(self.home_pattern);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Event::RightBlinker => match self.state {
|
||||||
|
State::Brake => self.state = State::BrakeRightBlinker,
|
||||||
|
State::BrakeRightBlinker => self.state = State::Brake,
|
||||||
|
State::RightBlinker => self.state = State::Pattern(self.home_pattern),
|
||||||
|
_ => self.state = State::RightBlinker,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self, time: Instant) {
|
||||||
|
match self.ui.check_event(time) {
|
||||||
|
Some(event) => {
|
||||||
|
self.update_state(event);
|
||||||
|
self.update_animation(time);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (dashboard, lights) = self.current_animation.tick(time);
|
||||||
|
self.dashboard_lights = dashboard.clone();
|
||||||
|
self.lights = lights.clone();
|
||||||
|
self.ui.update_lights(dashboard, lights);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,333 @@
|
||||||
|
use crate::{BodyPattern, DashboardPattern, RGB};
|
||||||
|
use fixed::types::{I8F8, U16F0};
|
||||||
|
|
||||||
|
pub const RGB_OFF: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0"),
|
||||||
|
g: I8F8::lit("0"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RGB_WHITE: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1"),
|
||||||
|
g: I8F8::lit("1"),
|
||||||
|
b: I8F8::lit("1"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BRAKES_RED: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1"),
|
||||||
|
g: I8F8::lit("0"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BLINKER_AMBER: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1"),
|
||||||
|
g: I8F8::lit("0.15"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_RED: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.95"),
|
||||||
|
g: I8F8::lit("0.00"),
|
||||||
|
b: I8F8::lit("0.00"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_ORANGE: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1.0"),
|
||||||
|
g: I8F8::lit("0.25"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_YELLOW: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1.0"),
|
||||||
|
g: I8F8::lit("0.85"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_GREEN: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0"),
|
||||||
|
g: I8F8::lit("0.95"),
|
||||||
|
b: I8F8::lit("0.05"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_INDIGO: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.04"),
|
||||||
|
g: I8F8::lit("0.15"),
|
||||||
|
b: I8F8::lit("0.55"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_VIOLET: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.75"),
|
||||||
|
g: I8F8::lit("0.0"),
|
||||||
|
b: I8F8::lit("0.80"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TRANS_BLUE: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.06"),
|
||||||
|
g: I8F8::lit("0.41"),
|
||||||
|
b: I8F8::lit("0.98"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TRANS_PINK: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.96"),
|
||||||
|
g: I8F8::lit("0.16"),
|
||||||
|
b: I8F8::lit("0.32"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WATER_1: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.0"),
|
||||||
|
g: I8F8::lit("0.0"),
|
||||||
|
b: I8F8::lit("0.75"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WATER_2: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.8"),
|
||||||
|
g: I8F8::lit("0.8"),
|
||||||
|
b: I8F8::lit("0.8"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WATER_3: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.00"),
|
||||||
|
g: I8F8::lit("0.75"),
|
||||||
|
b: I8F8::lit("0.75"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const OFF_DASHBOARD: DashboardPattern = [RGB_OFF; 3];
|
||||||
|
pub const OFF_BODY: BodyPattern = [RGB_OFF; 60];
|
||||||
|
|
||||||
|
pub const DEFAULT_FRAMES: U16F0 = U16F0::lit("30");
|
||||||
|
|
||||||
|
pub const WATER_DASHBOARD: DashboardPattern = [WATER_1, WATER_2, WATER_3];
|
||||||
|
|
||||||
|
pub const WATER_BODY: BodyPattern = [RGB_OFF; 60];
|
||||||
|
|
||||||
|
pub const PRIDE_DASHBOARD: DashboardPattern = [PRIDE_RED, PRIDE_GREEN, PRIDE_INDIGO];
|
||||||
|
|
||||||
|
pub const PRIDE_BODY: BodyPattern = [
|
||||||
|
// Left Side
|
||||||
|
// Red
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
// Orange
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
// Yellow
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
// Green
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
// Indigo
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
// Violet
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
// Right Side
|
||||||
|
// Violet
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
// Indigo
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
// Green
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
// Yellow
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
// Orange
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
// Red
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const TRANS_PRIDE_DASHBOARD: DashboardPattern = [TRANS_BLUE, RGB_WHITE, TRANS_PINK];
|
||||||
|
|
||||||
|
pub const TRANS_PRIDE_BODY: BodyPattern = [
|
||||||
|
// Left Side
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, RGB_WHITE, RGB_WHITE, RGB_WHITE, RGB_WHITE,
|
||||||
|
RGB_WHITE, RGB_WHITE, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE,
|
||||||
|
// Right side
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, RGB_WHITE, RGB_WHITE, RGB_WHITE, RGB_WHITE,
|
||||||
|
RGB_WHITE, RGB_WHITE, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const BRAKES_FRAMES: U16F0 = U16F0::lit("15");
|
||||||
|
|
||||||
|
pub const BRAKES_DASHBOARD: DashboardPattern = [BRAKES_RED; 3];
|
||||||
|
|
||||||
|
pub const BRAKES_BODY: BodyPattern = [BRAKES_RED; 60];
|
||||||
|
|
||||||
|
pub const BLINKER_FRAMES: U16F0 = U16F0::lit("10");
|
||||||
|
|
||||||
|
pub const LEFT_BLINKER_DASHBOARD: DashboardPattern = [BLINKER_AMBER, RGB_OFF, RGB_OFF];
|
||||||
|
|
||||||
|
pub const LEFT_BLINKER_BODY: BodyPattern = [
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const RIGHT_BLINKER_DASHBOARD: DashboardPattern = [RGB_OFF, RGB_OFF, BLINKER_AMBER];
|
||||||
|
|
||||||
|
pub const RIGHT_BLINKER_BODY: BodyPattern = [
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
];
|
|
@ -0,0 +1,17 @@
|
||||||
|
use core::default::Default;
|
||||||
|
use fixed::types::I8F8;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, Debug)]
|
||||||
|
pub struct RGB<T> {
|
||||||
|
pub r: T,
|
||||||
|
pub g: T,
|
||||||
|
pub b: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DASHBOARD_LIGHT_COUNT: usize = 3;
|
||||||
|
|
||||||
|
pub type DashboardPattern = [RGB<I8F8>; DASHBOARD_LIGHT_COUNT];
|
||||||
|
|
||||||
|
const BODY_LIGHT_COUNT: usize = 60;
|
||||||
|
|
||||||
|
pub type BodyPattern = [RGB<I8F8>; BODY_LIGHT_COUNT];
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "simulator"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
||||||
|
cairo-rs = { version = "0.18" }
|
||||||
|
fixed = { version = "1" }
|
||||||
|
gio = { version = "0.18" }
|
||||||
|
glib = { version = "0.18" }
|
||||||
|
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
|
||||||
|
lights-core = { path = "../core" }
|
||||||
|
pango = { version = "*" }
|
|
@ -0,0 +1,288 @@
|
||||||
|
use adw::prelude::*;
|
||||||
|
use fixed::types::{I8F8, U128F0};
|
||||||
|
use glib::{Object, Sender};
|
||||||
|
use gtk::subclass::prelude::*;
|
||||||
|
use lights_core::{
|
||||||
|
App, BodyPattern, DashboardPattern, Event, Instant, FPS, OFF_BODY, OFF_DASHBOARD, RGB, UI,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
env,
|
||||||
|
rc::Rc,
|
||||||
|
sync::mpsc::{Receiver, TryRecvError},
|
||||||
|
};
|
||||||
|
|
||||||
|
const WIDTH: i32 = 640;
|
||||||
|
const HEIGHT: i32 = 480;
|
||||||
|
|
||||||
|
pub struct Update {
|
||||||
|
dashboard: DashboardPattern,
|
||||||
|
lights: BodyPattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DashboardLightsPrivate {
|
||||||
|
lights: Rc<RefCell<DashboardPattern>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for DashboardLightsPrivate {
|
||||||
|
const NAME: &'static str = "DashboardLights";
|
||||||
|
type Type = DashboardLights;
|
||||||
|
type ParentType = gtk::DrawingArea;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
lights: Rc::new(RefCell::new(OFF_DASHBOARD)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for DashboardLightsPrivate {}
|
||||||
|
impl WidgetImpl for DashboardLightsPrivate {}
|
||||||
|
impl DrawingAreaImpl for DashboardLightsPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct DashboardLights(ObjectSubclass<DashboardLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DashboardLights {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
s.set_width_request(WIDTH);
|
||||||
|
s.set_height_request(100);
|
||||||
|
|
||||||
|
s.set_draw_func({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, context, width, _| {
|
||||||
|
let start = width as f64 / 2. - 150.;
|
||||||
|
let lights = s.imp().lights.borrow();
|
||||||
|
for i in 0..3 {
|
||||||
|
context.set_source_rgb(
|
||||||
|
lights[i].r.into(),
|
||||||
|
lights[i].g.into(),
|
||||||
|
lights[i].b.into(),
|
||||||
|
);
|
||||||
|
context.rectangle(start + 100. * i as f64, 10., 80., 80.);
|
||||||
|
let _ = context.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_lights(&self, lights: DashboardPattern) {
|
||||||
|
*self.imp().lights.borrow_mut() = lights;
|
||||||
|
self.queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BikeLightsPrivate {
|
||||||
|
lights: Rc<RefCell<BodyPattern>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for BikeLightsPrivate {
|
||||||
|
const NAME: &'static str = "BikeLights";
|
||||||
|
type Type = BikeLights;
|
||||||
|
type ParentType = gtk::DrawingArea;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
lights: Rc::new(RefCell::new(OFF_BODY)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for BikeLightsPrivate {}
|
||||||
|
impl WidgetImpl for BikeLightsPrivate {}
|
||||||
|
impl DrawingAreaImpl for BikeLightsPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct BikeLights(ObjectSubclass<BikeLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BikeLights {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
s.set_width_request(WIDTH);
|
||||||
|
s.set_height_request(640);
|
||||||
|
|
||||||
|
let center = WIDTH as f64 / 2.;
|
||||||
|
|
||||||
|
s.set_draw_func({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, context, _, _| {
|
||||||
|
let lights = s.imp().lights.borrow();
|
||||||
|
for i in 0..30 {
|
||||||
|
context.set_source_rgb(
|
||||||
|
lights[i].r.into(),
|
||||||
|
lights[i].g.into(),
|
||||||
|
lights[i].b.into(),
|
||||||
|
);
|
||||||
|
context.rectangle(center - 45., 5. + 20. * i as f64, 15., 15.);
|
||||||
|
let _ = context.fill();
|
||||||
|
}
|
||||||
|
for i in 0..30 {
|
||||||
|
context.set_source_rgb(
|
||||||
|
lights[i + 30].r.into(),
|
||||||
|
lights[i + 30].g.into(),
|
||||||
|
lights[i + 30].b.into(),
|
||||||
|
);
|
||||||
|
context.rectangle(center + 15., 5. + 20. * (30. - (i + 1) as f64), 15., 15.);
|
||||||
|
let _ = context.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_lights(&self, lights: [RGB<I8F8>; 60]) {
|
||||||
|
*self.imp().lights.borrow_mut() = lights;
|
||||||
|
self.queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GTKUI {
|
||||||
|
tx: Sender<Update>,
|
||||||
|
rx: Receiver<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UI for GTKUI {
|
||||||
|
fn check_event(&mut self, _: Instant) -> Option<Event> {
|
||||||
|
match self.rx.try_recv() {
|
||||||
|
Ok(event) => Some(event),
|
||||||
|
Err(TryRecvError::Empty) => None,
|
||||||
|
Err(TryRecvError::Disconnected) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_lights(&self, dashboard_lights: DashboardPattern, lights: BodyPattern) {
|
||||||
|
self.tx
|
||||||
|
.send(Update {
|
||||||
|
dashboard: dashboard_lights,
|
||||||
|
lights,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let adw_app = adw::Application::builder()
|
||||||
|
.application_id("com.luminescent-dreams.bike-light-simulator")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
adw_app.connect_activate(move |adw_app| {
|
||||||
|
let (update_tx, update_rx) =
|
||||||
|
gtk::glib::MainContext::channel::<Update>(gtk::glib::Priority::DEFAULT);
|
||||||
|
let (event_tx, event_rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut bike_app = App::new(Box::new(GTKUI {
|
||||||
|
tx: update_tx,
|
||||||
|
rx: event_rx,
|
||||||
|
}));
|
||||||
|
loop {
|
||||||
|
bike_app.tick(Instant(U128F0::from(
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis(),
|
||||||
|
)));
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1000 / (FPS as u64)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let window = adw::ApplicationWindow::builder()
|
||||||
|
.application(adw_app)
|
||||||
|
.default_width(WIDTH)
|
||||||
|
.default_height(HEIGHT)
|
||||||
|
.build();
|
||||||
|
let layout = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.build();
|
||||||
|
let controls = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let dashboard_lights = DashboardLights::new();
|
||||||
|
let bike_lights = BikeLights::new();
|
||||||
|
|
||||||
|
let left_button = gtk::Button::builder().label("L").build();
|
||||||
|
let brake_button = gtk::Button::builder().label("Brakes").build();
|
||||||
|
let right_button = gtk::Button::builder().label("R").build();
|
||||||
|
|
||||||
|
left_button.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::LeftBlinker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
brake_button.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::Brake);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
right_button.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::RightBlinker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
controls.append(&left_button);
|
||||||
|
controls.append(&brake_button);
|
||||||
|
controls.append(&right_button);
|
||||||
|
layout.append(&controls);
|
||||||
|
|
||||||
|
let pattern_controls = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let previous_pattern = gtk::Button::builder().label("Previous").build();
|
||||||
|
let next_pattern = gtk::Button::builder().label("Next").build();
|
||||||
|
|
||||||
|
previous_pattern.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::PreviousPattern);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
next_pattern.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::NextPattern);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pattern_controls.append(&previous_pattern);
|
||||||
|
pattern_controls.append(&next_pattern);
|
||||||
|
layout.append(&pattern_controls);
|
||||||
|
|
||||||
|
layout.append(&dashboard_lights);
|
||||||
|
layout.append(&bike_lights);
|
||||||
|
|
||||||
|
update_rx.attach(None, {
|
||||||
|
let dashboard_lights = dashboard_lights.clone();
|
||||||
|
let bike_lights = bike_lights.clone();
|
||||||
|
move |Update { dashboard, lights }| {
|
||||||
|
dashboard_lights.set_lights(dashboard);
|
||||||
|
bike_lights.set_lights(lights);
|
||||||
|
glib::ControlFlow::Continue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.set_content(Some(&layout));
|
||||||
|
window.present();
|
||||||
|
});
|
||||||
|
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
ApplicationExtManual::run_with_args(&adw_app, &args);
|
||||||
|
}
|
|
@ -45,6 +45,7 @@
|
||||||
pkgs.udev
|
pkgs.udev
|
||||||
pkgs.wasm-pack
|
pkgs.wasm-pack
|
||||||
typeshare.packages."x86_64-linux".default
|
typeshare.packages."x86_64-linux".default
|
||||||
|
pkgs.nodePackages_latest.typescript-language-server
|
||||||
];
|
];
|
||||||
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
||||||
ENV = "dev";
|
ENV = "dev";
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "gm-dash"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pipewire = "0.8.0"
|
||||||
|
serde = { version = "1.0.209", features = ["alloc", "derive"] }
|
||||||
|
serde_json = "1.0.127"
|
||||||
|
tokio = { version = "1.39.3", features = ["full"] }
|
||||||
|
warp = "0.3.7"
|
|
@ -0,0 +1,25 @@
|
||||||
|
use pipewire::{context::Context, main_loop::MainLoop};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mainloop = MainLoop::new(None)?;
|
||||||
|
let context = Context::new(&mainloop)?;
|
||||||
|
let core = context.connect(None)?;
|
||||||
|
let registry = core.get_registry()?;
|
||||||
|
|
||||||
|
let _listener = registry
|
||||||
|
.add_listener_local()
|
||||||
|
.global(|global| {
|
||||||
|
if global.props.and_then(|p| p.get("media.class")) == Some("Audio/Sink"){
|
||||||
|
println!(
|
||||||
|
"\t{:?} {:?}",
|
||||||
|
global.props.and_then(|p| p.get("node.description")),
|
||||||
|
global.props.and_then(|p| p.get("media.class"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.register();
|
||||||
|
|
||||||
|
mainloop.run();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
use pipewire::{context::Context, main_loop::MainLoop};
|
||||||
|
use std::{
|
||||||
|
net::{Ipv6Addr, SocketAddrV6},
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
use tokio::task::spawn_blocking;
|
||||||
|
use warp::{serve, Filter};
|
||||||
|
|
||||||
|
struct State_ {
|
||||||
|
device_list: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct State {
|
||||||
|
internal: Arc<RwLock<State_>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
fn new() -> State {
|
||||||
|
let internal = State_ {
|
||||||
|
device_list: vec![],
|
||||||
|
};
|
||||||
|
State {
|
||||||
|
internal: Arc::new(RwLock::new(internal)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_audio(&self, device: String) {
|
||||||
|
let mut st = self.internal.write().unwrap();
|
||||||
|
(*st).device_list.push(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn audio_devices(&self) -> Vec<String> {
|
||||||
|
let st = self.internal.read().unwrap();
|
||||||
|
(*st).device_list.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> State {
|
||||||
|
State::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn server_main(state: State) {
|
||||||
|
let localhost: Ipv6Addr = "::1".parse().unwrap();
|
||||||
|
let server_addr = SocketAddrV6::new(localhost, 3001, 0, 0);
|
||||||
|
|
||||||
|
let root = warp::path!().map(|| "ok".to_string());
|
||||||
|
let list_output_devices = warp::path!("output_devices").map({
|
||||||
|
let state = state.clone();
|
||||||
|
move || {
|
||||||
|
let devices = state.audio_devices();
|
||||||
|
serde_json::to_string(&devices).unwrap()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let routes = root.or(list_output_devices);
|
||||||
|
|
||||||
|
serve(routes).run(server_addr).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_add_audio_device(state: State, props: &pipewire::spa::utils::dict::DictRef)
|
||||||
|
{
|
||||||
|
if props.get("media.class") == Some("Audio/Sink") {
|
||||||
|
if let Some(device_name) = props.get("node.description") {
|
||||||
|
state.add_audio(device_name.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pipewire_loop(state: State) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mainloop = MainLoop::new(None)?;
|
||||||
|
let context = Context::new(&mainloop)?;
|
||||||
|
let core = context.connect(None)?;
|
||||||
|
let registry = core.get_registry()?;
|
||||||
|
|
||||||
|
let _listener = registry
|
||||||
|
.add_listener_local()
|
||||||
|
.global({
|
||||||
|
let state = state.clone();
|
||||||
|
move |global_data| {
|
||||||
|
if let Some(props) = global_data.props {
|
||||||
|
handle_add_audio_device(state.clone(), props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.register();
|
||||||
|
|
||||||
|
mainloop.run();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pipewire_main(state: State) {
|
||||||
|
pipewire_loop(state).expect("pipewire should not error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let state = State::default();
|
||||||
|
|
||||||
|
spawn_blocking({
|
||||||
|
let state = state.clone();
|
||||||
|
move || pipewire_main(state)
|
||||||
|
});
|
||||||
|
|
||||||
|
server_main(state.clone()).await;
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"@types/jest": "^27.5.2",
|
||||||
|
"@types/node": "^16.18.105",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.26.1",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import App from './App';
|
import Dashboard from './Dashboard';
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
test('renders learn react link', () => {
|
||||||
render(<App />);
|
render(<Dashboard />);
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
expect(linkElement).toBeInTheDocument();
|
expect(linkElement).toBeInTheDocument();
|
||||||
});
|
});
|
|
@ -0,0 +1,77 @@
|
||||||
|
import './Dashboard.css';
|
||||||
|
import Card from './components/Card/Card';
|
||||||
|
import Launcher from './components/Launcher/Launcher';
|
||||||
|
import Launchpad from './components/Launchpad/Launchpad';
|
||||||
|
|
||||||
|
const LightThemes = () => <Card name="Light Themes">
|
||||||
|
<Launchpad
|
||||||
|
exclusive={true}
|
||||||
|
options={[
|
||||||
|
{ title: "Dark reds" },
|
||||||
|
{ title: "Watery" },
|
||||||
|
{ title: "Sunset" },
|
||||||
|
{ title: "Darkness" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
const LightSetup = () => <div> </div>
|
||||||
|
|
||||||
|
interface LightProps {
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Light = ({ name }: LightProps) => <div> <p> {name} </p> </div>
|
||||||
|
|
||||||
|
const Tracks = () => <Card name="Tracks">
|
||||||
|
<Launchpad
|
||||||
|
exclusive={false}
|
||||||
|
options={[
|
||||||
|
{ title: "City BGM" },
|
||||||
|
{ title: "Chat on the streets" },
|
||||||
|
{ title: "Abandoned structure" },
|
||||||
|
{ title: "Water dripping" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
interface TrackProps {
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
const Track = ({ name }: TrackProps) => <Launcher title={name} />
|
||||||
|
|
||||||
|
const Presets = () => <Card name="Presets">
|
||||||
|
<Launchpad
|
||||||
|
exclusive={true}
|
||||||
|
options={[
|
||||||
|
{ title: "Gilcrest Falls day" },
|
||||||
|
{ title: "Gilcrest Falls night" },
|
||||||
|
{ title: "Empty colony" },
|
||||||
|
{ title: "Surk colony" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
interface PresetProps {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// const Scene = ({ name }: PresetProps) => <Launcher title={name} activated={false} />
|
||||||
|
|
||||||
|
const SceneEditor = () => <div> </div>
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<div className="layout">
|
||||||
|
<Presets />
|
||||||
|
<div>
|
||||||
|
<LightThemes />
|
||||||
|
<Tracks />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard;
|
|
@ -0,0 +1,78 @@
|
||||||
|
.palette {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette div {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 1em;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-purple div.item-1 {
|
||||||
|
background-color: var(--purple-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-purple div.item-2 {
|
||||||
|
background-color: var(--purple-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-purple div.item-3 {
|
||||||
|
background-color: var(--purple-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-purple div.item-4 {
|
||||||
|
background-color: var(--purple-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-purple div.item-5 {
|
||||||
|
background-color: var(--purple-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-blue div.item-1 {
|
||||||
|
background-color: var(--blue-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-blue div.item-2 {
|
||||||
|
background-color: var(--blue-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-blue div.item-3 {
|
||||||
|
background-color: var(--blue-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-blue div.item-4 {
|
||||||
|
background-color: var(--blue-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-blue div.item-5 {
|
||||||
|
background-color: var(--blue-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-grey div.item-1 {
|
||||||
|
background-color: var(--grey-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-grey div.item-2 {
|
||||||
|
background-color: var(--grey-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-grey div.item-3 {
|
||||||
|
background-color: var(--grey-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-grey div.item-4 {
|
||||||
|
background-color: var(--grey-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-grey div.item-5 {
|
||||||
|
background-color: var(--grey-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.design_horizontal {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import './Design.css'
|
||||||
|
import Launchpad from './components/Launchpad/Launchpad'
|
||||||
|
|
||||||
|
const PaletteGrey = () => <div className="palette palette-grey">
|
||||||
|
<div className="item-1" />
|
||||||
|
<div className="item-2" />
|
||||||
|
<div className="item-3" />
|
||||||
|
<div className="item-4" />
|
||||||
|
<div className="item-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
const PalettePurple = () => <div className="palette palette-purple">
|
||||||
|
<div className="item-1" />
|
||||||
|
<div className="item-2" />
|
||||||
|
<div className="item-3" />
|
||||||
|
<div className="item-4" />
|
||||||
|
<div className="item-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
const PaletteBlue = () => <div className="palette palette-blue">
|
||||||
|
<div className="item-1" />
|
||||||
|
<div className="item-2" />
|
||||||
|
<div className="item-3" />
|
||||||
|
<div className="item-4" />
|
||||||
|
<div className="item-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
const Launchpads = () => <div className="design_horizontal">
|
||||||
|
<Launchpad
|
||||||
|
exclusive={true}
|
||||||
|
options={[
|
||||||
|
{ title: "Grey" },
|
||||||
|
{ title: "Purple" },
|
||||||
|
{ title: "Blue" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Launchpad
|
||||||
|
exclusive={false}
|
||||||
|
flow={"vertical"}
|
||||||
|
options={[
|
||||||
|
{ title: "Grey" },
|
||||||
|
{ title: "Purple" },
|
||||||
|
{ title: "Blue" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
const Design = () => <div>
|
||||||
|
<PaletteGrey />
|
||||||
|
<PalettePurple />
|
||||||
|
<PaletteBlue />
|
||||||
|
<Launchpads />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
export default Design;
|
|
@ -0,0 +1,15 @@
|
||||||
|
.card {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: var(--spacer-l);
|
||||||
|
box-shadow: 4px 4px 4px 0px var(--shadow-1),
|
||||||
|
8px 8px 8px 0px var(--shadow-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__title {
|
||||||
|
color: var(--title-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__body {
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import './Card.css';
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Card = ({ name, children }: PropsWithChildren<CardProps>) => (
|
||||||
|
<div className="card">
|
||||||
|
<h1 className="card__title"> {name} </h1>
|
||||||
|
<div className="card__body">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Card;
|
|
@ -0,0 +1,11 @@
|
||||||
|
.activator {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: var(--spacer-m);
|
||||||
|
padding: var(--spacer-s);
|
||||||
|
box-shadow: var(--shadow-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activator_enabled {
|
||||||
|
box-shadow: var(--activator-ring);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import './Launcher.css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export interface LauncherProps {
|
||||||
|
title: string,
|
||||||
|
icon?: string,
|
||||||
|
activated?: boolean,
|
||||||
|
onSelected?: (key: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Launcher = ({ title, activated = false, onSelected = (key) => {} }: LauncherProps) => {
|
||||||
|
const classnames = activated ? "activator activator_enabled" : "activator";
|
||||||
|
console.log("classnames ", activated, classnames);
|
||||||
|
return (
|
||||||
|
<div className={classnames} onClick={() => onSelected(title)}>
|
||||||
|
<p> {title} </p>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Launcher;
|
|
@ -0,0 +1,18 @@
|
||||||
|
.launchpad {
|
||||||
|
display: flex;
|
||||||
|
border: var(--border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--shadow-depression);
|
||||||
|
margin: var(--spacer-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.launchpad_horizontal-flow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.launchpad_vertical-flow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import './Launchpad.css';
|
||||||
|
import Launcher, { LauncherProps } from '../Launcher/Launcher';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
export interface Selectable {
|
||||||
|
onSelected?: (key: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Flow = "horizontal" | "vertical";
|
||||||
|
|
||||||
|
interface LaunchpadProps {
|
||||||
|
exclusive: boolean;
|
||||||
|
flow?: Flow;
|
||||||
|
options: Array<LauncherProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exclusiveSelect = (state: { [key: string]: boolean }, targetId: string) => {
|
||||||
|
console.log("running exclusiveSelect on ", targetId);
|
||||||
|
return { [targetId]: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiSelect = (state: { [key: string]: boolean }, targetId: string) => {
|
||||||
|
if (state[targetId]) {
|
||||||
|
return { ...state, [targetId]: false };
|
||||||
|
} else {
|
||||||
|
return { ...state, [targetId]: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Launchpad = ({ flow = "horizontal", options, exclusive }: LaunchpadProps) => {
|
||||||
|
const [selected, dispatch] = React.useReducer(exclusive ? exclusiveSelect : multiSelect, {});
|
||||||
|
|
||||||
|
let classOptions = [ "launchpad" ];
|
||||||
|
|
||||||
|
if (flow === "horizontal") {
|
||||||
|
classOptions.push("launchpad_horizontal-flow");
|
||||||
|
} else {
|
||||||
|
classOptions.push("launchpad_vertical-flow");
|
||||||
|
}
|
||||||
|
|
||||||
|
let tiedOptions = options.map(option =>
|
||||||
|
<Launcher key={option.title} title={option.title} onSelected={(key: string) => dispatch(key)} activated={selected[option.title]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (<div className={classnames(classOptions)}> {tiedOptions} </div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Launchpad;
|
|
@ -0,0 +1,50 @@
|
||||||
|
:root {
|
||||||
|
--purple-1: hsl(265, 50%, 25%);
|
||||||
|
--purple-2: hsl(265, 60%, 35%);
|
||||||
|
--purple-3: hsl(265, 70%, 45%);
|
||||||
|
--purple-4: hsl(265, 80%, 55%);
|
||||||
|
--purple-5: hsl(265, 90%, 60%);
|
||||||
|
|
||||||
|
--blue-1: hsl(210, 50%, 25%);
|
||||||
|
--blue-2: hsl(210, 60%, 35%);
|
||||||
|
--blue-3: hsl(210, 70%, 45%);
|
||||||
|
--blue-4: hsl(210, 80%, 55%);
|
||||||
|
--blue-5: hsl(210, 90%, 65%);
|
||||||
|
|
||||||
|
--grey-1: hsl(210, 0%, 25%);
|
||||||
|
--grey-2: hsl(210, 0%, 40%);
|
||||||
|
--grey-3: hsl(210, 0%, 55%);
|
||||||
|
--grey-4: hsl(210, 0%, 70%);
|
||||||
|
--grey-5: hsl(210, 0%, 85%);
|
||||||
|
|
||||||
|
--title-color: var(--grey-1);
|
||||||
|
|
||||||
|
--border: 1px solid var(--purple-1);
|
||||||
|
--activator-ring: 0px 0px 8px 4px var(--blue-4);
|
||||||
|
--shadow-depression: inset 1px 1px 2px 0px var(--purple-1);
|
||||||
|
--shadow-shallow: 1px 1px 2px 0px var(--purple-1);
|
||||||
|
--shadow-deep: 2px 2px 4px 0px var(--purple-1),
|
||||||
|
4px 4px 8px 0px var(--purple-2);
|
||||||
|
|
||||||
|
--border-radius: 8px;
|
||||||
|
|
||||||
|
--spacer-xs: 2px;
|
||||||
|
--spacer-s: 4px;
|
||||||
|
--spacer-m: 8px;
|
||||||
|
--spacer-l: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: var(--grey-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
|
@ -1,15 +1,27 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import Dashboard from './Dashboard';
|
||||||
|
import Design from './Design';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
);
|
);
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <Dashboard />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/design",
|
||||||
|
element: <Design />,
|
||||||
|
},
|
||||||
|
]);
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<RouterProvider router={router} />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"name": "music-player-client",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "main.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"watch": "webpack --watch"
|
|
||||||
},
|
|
||||||
"author": "Savanni D'Gerinel <savanni@luminescent-dreams.com>",
|
|
||||||
"license": "GPL-3.0-or-later",
|
|
||||||
"dependencies": {
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"systemjs": "^6.13.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/lodash": "^4.14.191",
|
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
|
||||||
"css-loader": "^6.7.3",
|
|
||||||
"style-loader": "^3.3.1",
|
|
||||||
"ts-loader": "^9.4.2",
|
|
||||||
"typescript": "^4.9.5",
|
|
||||||
"webpack": "^5.91.0",
|
|
||||||
"webpack-cli": "^5.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
const colorGrey100 = "hsl(240 10% 95%)";
|
|
||||||
|
|
||||||
export class StyleProvider {
|
|
||||||
styles: Record<string, string> = {};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.styles["backgroundColorLight"] = colorGrey100;
|
|
||||||
}
|
|
||||||
|
|
||||||
get(key: string) {
|
|
||||||
return this.styles[key]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2015",
|
|
||||||
"module": "commonjs",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"rootDir": "src",
|
|
||||||
"outDir": "./dist",
|
|
||||||
"lib": ["es2016", "DOM"],
|
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true,
|
|
||||||
"noImplicitAny": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
|
@ -1,43 +1,27 @@
|
||||||
{
|
{
|
||||||
"name": "client",
|
"name": "music-player-client",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"description": "",
|
||||||
"dependencies": {
|
"main": "main.js",
|
||||||
"@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.97",
|
|
||||||
"@types/react": "^18.3.3",
|
|
||||||
"@types/react-dom": "^18.3.0",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"typescript": "^4.9.5",
|
|
||||||
"web-vitals": "^2.1.4"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"build": "webpack",
|
||||||
"build": "react-scripts build",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"test": "react-scripts test",
|
"watch": "webpack --watch"
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"author": "Savanni D'Gerinel <savanni@luminescent-dreams.com>",
|
||||||
"extends": [
|
"license": "GPL-3.0-or-later",
|
||||||
"react-app",
|
"dependencies": {
|
||||||
"react-app/jest"
|
"lodash": "^4.17.21",
|
||||||
]
|
"systemjs": "^6.13.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"devDependencies": {
|
||||||
"production": [
|
"@types/lodash": "^4.14.191",
|
||||||
">0.2%",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"not dead",
|
"css-loader": "^6.7.3",
|
||||||
"not op_mini all"
|
"style-loader": "^3.3.1",
|
||||||
],
|
"ts-loader": "^9.4.2",
|
||||||
"development": [
|
"typescript": "^4.9.5",
|
||||||
"last 1 chrome version",
|
"webpack": "^5.75.0",
|
||||||
"last 1 firefox version",
|
"webpack-cli": "^5.0.1"
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
.App {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import logo from './logo.svg';
|
|
||||||
import './App.css';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<header className="App-header">
|
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to reload.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
className="App-link"
|
|
||||||
href="https://reactjs.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Learn React
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TextField } from "../TextField/TextField";
|
import { TextField } from "./TextField";
|
||||||
|
|
||||||
export class NowPlaying extends HTMLElement {
|
export class NowPlaying extends HTMLElement {
|
||||||
onStop: () => void;
|
onStop: () => void;
|
|
@ -1,15 +1,4 @@
|
||||||
import { TextField } from "../TextField/TextField";
|
import { TextField } from "./TextField";
|
||||||
import { StyleProvider } from "../../StyleProvider";
|
|
||||||
|
|
||||||
const style = `
|
|
||||||
.track-card {
|
|
||||||
display: grid;
|
|
||||||
gap: 4px 4px;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
background-color: $background-color-light;
|
|
||||||
box-shadow: $elevation-low;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export class TrackCard extends HTMLElement {
|
export class TrackCard extends HTMLElement {
|
||||||
trackNumberContainer: TextField;
|
trackNumberContainer: TextField;
|
||||||
|
@ -17,17 +6,14 @@ export class TrackCard extends HTMLElement {
|
||||||
albumContainer: TextField;
|
albumContainer: TextField;
|
||||||
artistContainer: TextField;
|
artistContainer: TextField;
|
||||||
durationContainer: TextField;
|
durationContainer: TextField;
|
||||||
styleProvider: StyleProvider;
|
|
||||||
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return ["trackId", "trackNumber", "name", "album", "artist", "duration"];
|
return ["trackId", "trackNumber", "name", "album", "artist", "duration"];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(styleProvider: StyleProvider) {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.styleProvider = styleProvider
|
|
||||||
|
|
||||||
this.trackNumberContainer = document.createElement("text-field");
|
this.trackNumberContainer = document.createElement("text-field");
|
||||||
|
|
||||||
this.nameContainer = document.createElement("text-field");
|
this.nameContainer = document.createElement("text-field");
|
||||||
|
@ -114,11 +100,6 @@ export class TrackCard extends HTMLElement {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
const container = document.createElement("data-card");
|
const container = document.createElement("data-card");
|
||||||
container.classList.add("track-card");
|
container.classList.add("track-card");
|
||||||
container.style.setProperty("display", "grid");
|
|
||||||
container.style.setProperty("gap", "4px 4px");
|
|
||||||
container.style.setProperty("border", "1px solid --border-color");
|
|
||||||
container.style.setProperty("background-color", this.styleProvider.get("backgroundColorLight"));
|
|
||||||
container.style.setProperty("box-shadow", "$elevation-low");
|
|
||||||
|
|
||||||
// this.appendChild(this.trackNumberContainer);
|
// this.appendChild(this.trackNumberContainer);
|
||||||
container.appendChild(this.nameContainer);
|
container.appendChild(this.nameContainer);
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,11 +1,10 @@
|
||||||
import * as _ from "lodash";
|
import * as _ from "lodash";
|
||||||
import { TrackInfo, getTracks, playTrack, stopPlayback } from "./client";
|
import { TrackInfo, getTracks, playTrack, stopPlayback } from "./client";
|
||||||
import { DataCard } from "./components/DataCard/DataCard";
|
import { DataCard } from "./components/DataCard";
|
||||||
import { NowPlaying } from "./components/NowPlaying/NowPlaying";
|
import { NowPlaying } from "./components/NowPlaying";
|
||||||
import { TextField } from "./components/TextField/TextField";
|
import { TextField } from "./components/TextField";
|
||||||
import { TrackCard } from "./components/TrackCard/TrackCard";
|
import { TrackCard } from "./components/TrackCard";
|
||||||
import { PlaylistRow } from "./components/PlaylistRow/PlaylistRow";
|
import { PlaylistRow } from "./components/PlaylistRow";
|
||||||
import { StyleProvider } from "./StyleProvider";
|
|
||||||
|
|
||||||
window.customElements.define("data-card", DataCard);
|
window.customElements.define("data-card", DataCard);
|
||||||
window.customElements.define("now-playing", NowPlaying);
|
window.customElements.define("now-playing", NowPlaying);
|
||||||
|
@ -25,11 +24,9 @@ declare global {
|
||||||
|
|
||||||
const updateTrackList = (tracks: TrackInfo[]) => {
|
const updateTrackList = (tracks: TrackInfo[]) => {
|
||||||
const playlist = document.querySelector(".track-list__tracks");
|
const playlist = document.querySelector(".track-list__tracks");
|
||||||
const styleProvider = new StyleProvider();
|
|
||||||
if (playlist) {
|
if (playlist) {
|
||||||
_.map(tracks, (info) => {
|
_.map(tracks, (info) => {
|
||||||
let card: TrackCard = document.createElement("track-card");
|
let card: TrackCard = document.createElement("track-card");
|
||||||
card.styleProvider = styleProvider;
|
|
||||||
let listItem: PlaylistRow = document.createElement("playlist-row");
|
let listItem: PlaylistRow = document.createElement("playlist-row");
|
||||||
|
|
||||||
card.trackId = info.id;
|
card.trackId = info.id;
|
|
@ -1,26 +1,14 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es2015",
|
||||||
"lib": [
|
"module": "commonjs",
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"rootDir": "src",
|
||||||
"isolatedModules": true,
|
"outDir": "./dist",
|
||||||
"noEmit": true,
|
"lib": ["es2016", "DOM"],
|
||||||
"jsx": "react-jsx"
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"]
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ const CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
entry: ['./src/main.ts'],
|
entry: ['./src/main.ts', './styles.scss'],
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
@ -15,6 +15,10 @@ module.exports = {
|
||||||
test: /\.html$/i,
|
test: /\.html$/i,
|
||||||
type: 'asset/resource',
|
type: 'asset/resource',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.s[ac]ss$/i,
|
||||||
|
use: ['style-loader', 'css-loader', 'sass-loader'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
|
@ -6,12 +6,12 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
result-extended = { path = "../../result-extended" }
|
flow = { path = "../../flow" }
|
||||||
gstreamer = { version = "0.19" }
|
gstreamer = { version = "0.19" }
|
||||||
id3 = { version = "1.6" }
|
id3 = { version = "1.6" }
|
||||||
mime_guess = { version = "2.0" }
|
mime_guess = { version = "2.0" }
|
||||||
mime = { version = "0.3" }
|
mime = { version = "0.3" }
|
||||||
rusqlite = { version = "0.29" }
|
rusqlite = { version = "0.28" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = { version = "1.0" }
|
serde_json = { version = "1.0" }
|
||||||
thiserror = { version = "1.0" }
|
thiserror = { version = "1.0" }
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use result_extended::Result;
|
use flow::Flow;
|
||||||
use music_player::{
|
use music_player::{
|
||||||
core::{ControlMsg, Core},
|
core::{ControlMsg, Core},
|
||||||
database::{MemoryIndex, MusicIndex},
|
database::{MemoryIndex, MusicIndex},
|
||||||
|
@ -29,8 +29,8 @@ enum Response<A: Serialize> {
|
||||||
Fatal(String),
|
Fatal(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: Serialize> From<std::result::Result<A, Error>> for Response<A> {
|
impl<A: Serialize> From<Result<A, Error>> for Response<A> {
|
||||||
fn from(res: std::result::Result<A, Error>) -> Self {
|
fn from(res: Result<A, Error>) -> Self {
|
||||||
match res {
|
match res {
|
||||||
Ok(val) => Self::Success(val),
|
Ok(val) => Self::Success(val),
|
||||||
Err(err) => Self::Failure(format!("{}", err)),
|
Err(err) => Self::Failure(format!("{}", err)),
|
||||||
|
@ -38,12 +38,12 @@ impl<A: Serialize> From<std::result::Result<A, Error>> for Response<A> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: Serialize> From<Result<A, Error, FatalError>> for Response<A> {
|
impl<A: Serialize> From<Flow<A, FatalError, Error>> for Response<A> {
|
||||||
fn from(res: Result<A, Error, FatalError>) -> Self {
|
fn from(res: Flow<A, FatalError, Error>) -> Self {
|
||||||
match res {
|
match res {
|
||||||
Result::Ok(val) => Self::Success(val),
|
Flow::Ok(val) => Self::Success(val),
|
||||||
Result::Fatal(fatal) => Self::Fatal(format!("{}", fatal)),
|
Flow::Fatal(fatal) => Self::Fatal(format!("{}", fatal)),
|
||||||
Result::Err(err) => Self::Failure(format!("{}", err)),
|
Flow::Err(err) => Self::Failure(format!("{}", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
scanner::MusicScanner,
|
scanner::MusicScanner,
|
||||||
Error, FatalError,
|
Error, FatalError,
|
||||||
};
|
};
|
||||||
use result_extended::{ok, return_error, Result};
|
use flow::{ok, return_error, Flow};
|
||||||
use gstreamer::{format::ClockTime, prelude::*, MessageView};
|
use gstreamer::{format::ClockTime, prelude::*, MessageView};
|
||||||
use std::{
|
use std::{
|
||||||
sync::{Arc, Mutex, RwLock},
|
sync::{Arc, Mutex, RwLock},
|
||||||
|
@ -52,7 +52,7 @@ impl Core {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
db: Arc<dyn MusicIndex>,
|
db: Arc<dyn MusicIndex>,
|
||||||
scanner: Arc<dyn MusicScanner>,
|
scanner: Arc<dyn MusicScanner>,
|
||||||
) -> std::result::Result<Core, FatalError> {
|
) -> Result<Core, FatalError> {
|
||||||
let db = db;
|
let db = db;
|
||||||
|
|
||||||
let next_scan = Arc::new(RwLock::new(Instant::now()));
|
let next_scan = Arc::new(RwLock::new(Instant::now()));
|
||||||
|
@ -70,17 +70,17 @@ impl Core {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_tracks<'a>(&'a self) -> Result<Vec<TrackInfo>, Error, FatalError> {
|
pub fn list_tracks<'a>(&'a self) -> Flow<Vec<TrackInfo>, FatalError, Error> {
|
||||||
self.db.list_tracks().map_err(Error::DatabaseError)
|
self.db.list_tracks().map_err(Error::DatabaseError)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play_track<'a>(&'a mut self, id: TrackId) -> std::result::Result<(), Error> {
|
pub fn play_track<'a>(&'a mut self, id: TrackId) -> Result<(), Error> {
|
||||||
self.stop_playback()?;
|
self.stop_playback()?;
|
||||||
self.playback_controller = Some(Playback::new(id)?);
|
self.playback_controller = Some(Playback::new(id)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop_playback<'a>(&'a mut self) -> std::result::Result<(), Error> {
|
pub fn stop_playback<'a>(&'a mut self) -> Result<(), Error> {
|
||||||
match self.playback_controller {
|
match self.playback_controller {
|
||||||
Some(ref controller) => controller.stop()?,
|
Some(ref controller) => controller.stop()?,
|
||||||
None => (),
|
None => (),
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
||||||
media::{TrackId, TrackInfo},
|
media::{TrackId, TrackInfo},
|
||||||
FatalError,
|
FatalError,
|
||||||
};
|
};
|
||||||
use result_extended::{error, ok, Result};
|
use flow::{error, ok, Flow};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -21,10 +21,10 @@ pub enum DatabaseError {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait MusicIndex: Sync + Send {
|
pub trait MusicIndex: Sync + Send {
|
||||||
fn add_track(&self, track: TrackInfo) -> Result<(), DatabaseError, FatalError>;
|
fn add_track(&self, track: TrackInfo) -> Flow<(), FatalError, DatabaseError>;
|
||||||
fn remove_track(&self, id: &TrackId) -> Result<(), DatabaseError, FatalError>;
|
fn remove_track(&self, id: &TrackId) -> Flow<(), FatalError, DatabaseError>;
|
||||||
fn get_track_info(&self, id: &TrackId) -> Result<Option<TrackInfo>, DatabaseError, FatalError>;
|
fn get_track_info(&self, id: &TrackId) -> Flow<Option<TrackInfo>, FatalError, DatabaseError>;
|
||||||
fn list_tracks<'a>(&'a self) -> Result<Vec<TrackInfo>, DatabaseError, FatalError>;
|
fn list_tracks<'a>(&'a self) -> Flow<Vec<TrackInfo>, FatalError, DatabaseError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MemoryIndex {
|
pub struct MemoryIndex {
|
||||||
|
@ -40,13 +40,13 @@ impl MemoryIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MusicIndex for MemoryIndex {
|
impl MusicIndex for MemoryIndex {
|
||||||
fn add_track(&self, info: TrackInfo) -> Result<(), DatabaseError, FatalError> {
|
fn add_track(&self, info: TrackInfo) -> Flow<(), FatalError, DatabaseError> {
|
||||||
let mut tracks = self.tracks.write().unwrap();
|
let mut tracks = self.tracks.write().unwrap();
|
||||||
tracks.insert(info.id.clone(), info);
|
tracks.insert(info.id.clone(), info);
|
||||||
ok(())
|
ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_track(&self, id: &TrackId) -> Result<(), DatabaseError, FatalError> {
|
fn remove_track(&self, id: &TrackId) -> Flow<(), FatalError, DatabaseError> {
|
||||||
let mut tracks = self.tracks.write().unwrap();
|
let mut tracks = self.tracks.write().unwrap();
|
||||||
tracks.remove(&id);
|
tracks.remove(&id);
|
||||||
ok(())
|
ok(())
|
||||||
|
@ -55,7 +55,7 @@ impl MusicIndex for MemoryIndex {
|
||||||
fn get_track_info<'a>(
|
fn get_track_info<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
id: &TrackId,
|
id: &TrackId,
|
||||||
) -> Result<Option<TrackInfo>, DatabaseError, FatalError> {
|
) -> Flow<Option<TrackInfo>, FatalError, DatabaseError> {
|
||||||
let track = {
|
let track = {
|
||||||
let tracks = self.tracks.read().unwrap();
|
let tracks = self.tracks.read().unwrap();
|
||||||
tracks.get(&id).cloned()
|
tracks.get(&id).cloned()
|
||||||
|
@ -63,7 +63,7 @@ impl MusicIndex for MemoryIndex {
|
||||||
ok(track)
|
ok(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_tracks<'a>(&'a self) -> Result<Vec<TrackInfo>, DatabaseError, FatalError> {
|
fn list_tracks<'a>(&'a self) -> Flow<Vec<TrackInfo>, FatalError, DatabaseError> {
|
||||||
ok(self
|
ok(self
|
||||||
.tracks
|
.tracks
|
||||||
.read()
|
.read()
|
||||||
|
@ -92,7 +92,7 @@ pub struct Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub fn new(path: PathBuf) -> Result<Database, DatabaseError, FatalError> {
|
pub fn new(path: PathBuf) -> Flow<Database, FatalError, DatabaseError> {
|
||||||
let connection = match Connection::open(path.clone()) {
|
let connection = match Connection::open(path.clone()) {
|
||||||
Ok(connection) => connection,
|
Ok(connection) => connection,
|
||||||
Err(err) => return error(DatabaseError::UnhandledError(err.to_string())),
|
Err(err) => return error(DatabaseError::UnhandledError(err.to_string())),
|
||||||
|
@ -136,7 +136,7 @@ mod test {
|
||||||
index.add_track(info.clone());
|
index.add_track(info.clone());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Result::Ok(Some(info)),
|
Flow::Ok(Some(info)),
|
||||||
index.get_track_info(&TrackId::from("track_1".to_owned()))
|
index.get_track_info(&TrackId::from("track_1".to_owned()))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,4 +33,4 @@ pub enum FatalError {
|
||||||
UnexpectedError,
|
UnexpectedError,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl result_extended::FatalError for FatalError {}
|
impl flow::FatalError for FatalError {}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{media::TrackId, Error, FatalError};
|
use crate::{media::TrackId, Error, FatalError};
|
||||||
|
use flow::{ok, return_error, Flow};
|
||||||
use gstreamer::{format::ClockTime, prelude::*, MessageView, StateChangeError};
|
use gstreamer::{format::ClockTime, prelude::*, MessageView, StateChangeError};
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.77.0"
|
channel = "1.77.0"
|
||||||
targets = [ "wasm32-unknown-unknown" ]
|
targets = [ "wasm32-unknown-unknown", "thumbv6m-none-eabi" ]
|
||||||
|
|