QNAL26AOMT5LCCJW7JIV52HLKE4UD4PTNZAMMFYVVOFGQXL4BPBAC
GIFNVLRQXQV3H7FHJLLBTNC3YSDCIZWEGFNDASF4E34O3ID4AA7QC
E4EK4FRNN26ZCGODRDFKDJI7BLBADJO5FWDRYJWU22V4X4QDQM7QC
6WU5J774AV5UOKZVLB2EL66SCC5DVFKK2V2UT5SM2NFFRYE6MAZQC
5SK7OGIQDEO4H3AFMGGBJY5LWBISEKKVDFUEPQ3YUPCEQHEYE3YAC
CJ3W6DCERC3FVMB7FJOA2V5MTAHHORYOEJB3J5HFJ5PFBVK2UXWQC
2OEKDJGNRB26WDYNIPXLD3X5KFEXUQN2EACBXO6T4F5PPB3YL2TQC
O7LARFJLYA7QBV73LE6N3DWVVKHCE43LI6EMCD34AXYHP6M5PWDQC
N6XOUOHS2X4M2BREDJXHRGICO4RFPNRS7OOYYQKNGUNQ5MK6LK7AC
XYB2PJR4B2375XMG6ZKMIUWHL6UFJEX4YYHCOBZVZHOB3VERU4SAC
pub fn new(config: StreamConfig, receiver: crossbeam_channel::Receiver<Event>) -> Self {
Self {
receiver,
config,
pub fn new() -> anyhow::Result<PlayerController> {
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("no output device available");
let config = device.default_output_config().expect("error while configs");
debug!("playing... {:?}", config);
let (to_player, from_controller) = crossbeam_channel::unbounded();
let nb_queued = Arc::new(AtomicUsize::new(0));
let mut player = Player {
receiver: from_controller,
config: config.config(),
}
};
let player_state = player.state.clone();
let stream = device.build_output_stream(
&config.into(),
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
player.process(data);
},
move |err| {
println!("{err:?}");
},
)?;
PlayerController::new(player_state, to_player, stream, nb_queued)
}
Event::SetSource(mut source) => {
let source_sample_rate = source.sample_rate;
let mut resampler =
Resampler::new((self.config.sample_rate.0 as f64) / source_sample_rate)
.unwrap();
resampler.process(&mut source).ok();
self.resampler = Some(resampler);
self.state.write().set_song(source.info);
self.source = Some(source);
}
}
pub fn next_source(&mut self) {
if *self.state.read() != State::Idle {
return;
}
if let Some(mut source) = self.source_queue.pop_front() {
let source_sample_rate = source.sample_rate;
let mut resampler =
Resampler::new((self.config.sample_rate.0 as f64) / source_sample_rate).unwrap();
resampler.process(&mut source).ok();
self.resampler = Some(resampler);
self.state.write().set_song(source.info.clone());
self.source = Some(source);
self.nb_queued
.store(self.source_queue.len(), atomic::Ordering::Relaxed);
pub fn new() -> Result<Self> {
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("no output device available");
let config = device.default_output_config().expect("error while configs");
debug!("playing... {:?}", config);
let (to_player, from_controller) = crossbeam_channel::unbounded();
let mut player = Player::new(config.config(), from_controller);
let player_state = player.state.clone();
let stream = device.build_output_stream(
&config.into(),
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
player.process(data);
},
move |err| {
println!("{err:?}");
},
)?;
pub fn new(
state: Arc<RwLock<player::State>>,
sender: crossbeam_channel::Sender<Event>,
stream: cpal::Stream,
nb_queued: Arc<AtomicUsize>,
) -> Result<Self> {
use druid::{
};
/// Converts a `Widget<String>` to a `Widget<T>`, not modifying the data if the input is invalid
pub struct ParseLazy<W> {
widget: W,
state: String,
}
impl<W> ParseLazy<W> {
pub fn new(widget: W) -> Self {
Self {
widget,
state: String::new(),
}
}
}
impl<T: FromStr + Display + Data, W: Widget<String>> Widget<T> for ParseLazy<W> {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
self.widget.event(ctx, event, &mut self.state, env);
if let Ok(res) = self.state.parse() {
*data = res;
}
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
if let LifeCycle::WidgetAdded = event {
self.state = data.to_string();
}
self.widget.lifecycle(ctx, event, &self.state, env)
}
fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
let old = mem::replace(&mut self.state, data.to_string());
self.widget.update(ctx, &old, &self.state, env)
}
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &T, env: &Env) -> Size {
self.widget.layout(ctx, bc, &self.state, env)
}
fn paint(&mut self, paint: &mut PaintCtx, _data: &T, env: &Env) {
self.widget.paint(paint, &self.state, env)
}
fn id(&self) -> Option<WidgetId> {
self.widget.id()
}
}
kurbo::Size, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx,
PaintCtx, UpdateCtx, Widget, WidgetId,
use std::{fmt::Display, mem, str::FromStr};
use std::f64::consts::TAU;
pub struct Knob {
initial_data: f32,
value_preview: f32,
click: Point,
}
impl Knob {
pub fn new() -> Self {
Self {
initial_data: 0.0,
value_preview: 0.0,
click: Point::ORIGIN,
}
}
}
const SPEED: f32 = 10e-3;
impl Widget<f32> for Knob {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut f32, _env: &Env) {
match event {
Event::MouseDown(evt) => {
if !ctx.is_disabled() {
ctx.set_active(true);
self.click = evt.pos;
self.value_preview = *data;
self.initial_data = *data;
ctx.request_paint();
}
}
Event::MouseMove(evt) => {
if ctx.is_active() {
let off = evt.pos - self.click;
self.value_preview = (self.initial_data + (off.x - off.y) as f32 * SPEED)
.min(1.0)
.max(0.0);
ctx.request_paint();
}
}
Event::MouseUp(evt) => {
if ctx.is_active() && !ctx.is_disabled() {
let off = evt.pos - self.click;
*data = (self.initial_data + (off.x - off.y) as f32 * SPEED)
.min(1.0)
.max(0.0);
ctx.request_paint();
}
ctx.set_active(false);
}
_ => (),
}
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &f32, _env: &Env) {
if let LifeCycle::HotChanged(_) | LifeCycle::DisabledChanged(_) = event {
ctx.request_paint();
}
}
ctx.request_paint();
}
fn layout(
&mut self,
_ctx: &mut LayoutCtx,
bc: &BoxConstraints,
_data: &f32,
_env: &Env,
) -> Size {
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &f32, env: &Env) {
let size = ctx.size();
let center = (size / 2.0).to_vec2().to_point();
let radius = size.width / 2.0 - 2.0;
center,
radii: Vec2::new(radius, radius),
start_angle: 0.0,
sweep_angle: if ctx.is_active() {
self.value_preview as f64 * TAU
} else {
*data as f64 * TAU
},
x_rotation: -TAU / 4.0,
}
.into_path(0.1);
}
}
ctx.fill(wheel, &env.get(crate::theme::ACCENT));
ctx.stroke(arc_path, &env.get(crate::theme::ACCENT_DIM), 2.0);
let mut wheel = arc_path.clone();
wheel.line_to(center);
wheel.line_to(center - Vec2::new(0.0, radius));
let arc_path: BezPath = kurbo::Arc {
fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &f32, _data: &f32, _env: &Env) {
use druid::{
kurbo::{self, BezPath, Shape, Size},
piet::RenderContext,
widget::prelude::*,
Point, Vec2,
};
use crate::state::State;
use druid::{
BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point,
Rect, Selector, Size, UpdateCtx, Widget, WidgetPod,
};
type ChildBuilder = Box<dyn Fn(&Env) -> Box<dyn Widget<State>>>;
pub const SHOW_AT: Selector<(Point, BoxConstraints, ChildBuilder)> =
Selector::new("dropdown.show-at");
pub const SHOW_MIDDLE: Selector<(BoxConstraints, ChildBuilder)> =
Selector::new("dropdown.show-middle");
pub const HIDE: Selector = Selector::new("dropdown.hide");
pub struct Child {
origin: Option<Point>,
bc: BoxConstraints,
widget: WidgetPod<State, Box<dyn Widget<State>>>,
}
pub struct Overlay {
child: Option<Child>,
}
impl Overlay {
pub fn new() -> Overlay {
Overlay { child: None }
}
}
impl Widget<State> for Overlay {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut State, env: &Env) {
let mut remove_child = false;
if let Some(child) = &mut self.child {
child.widget.event(ctx, event, data, env);
match event {
Event::MouseDown(mouse) => {
if !child.widget.layout_rect().contains(mouse.pos) {
ctx.set_active(true);
}
ctx.set_handled();
}
Event::MouseUp(mouse) => {
if ctx.is_active() && !child.widget.layout_rect().contains(mouse.pos) {
remove_child = true;
ctx.set_active(false);
}
ctx.set_handled();
}
Event::MouseMove(_) => {
ctx.set_handled();
}
_ => {}
}
}
match event {
Event::Command(cmd) if cmd.is(SHOW_AT) => {
let (pos, bc, child_builder) = cmd.get_unchecked(SHOW_AT);
self.child = Some(Child {
origin: Some(*pos),
bc: *bc,
widget: WidgetPod::new(child_builder(env)),
});
ctx.children_changed();
}
Event::Command(cmd) if cmd.is(SHOW_MIDDLE) => {
let (bc, child_builder) = cmd.get_unchecked(SHOW_MIDDLE);
self.child = Some(Child {
origin: None,
bc: *bc,
widget: WidgetPod::new(child_builder(env)),
});
ctx.children_changed();
}
Event::Command(cmd) if cmd.is(HIDE) => {
remove_child = true;
}
_ => {}
}
if remove_child {
self.child = None;
ctx.request_layout();
ctx.request_paint();
}
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &State, env: &Env) {
if let Some(child) = &mut self.child {
child.widget.lifecycle(ctx, event, data, env);
}
}
fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &State, data: &State, env: &Env) {
if let Some(child) = &mut self.child {
child.widget.update(ctx, data, env);
}
}
fn layout(
&mut self,
ctx: &mut LayoutCtx,
bc: &BoxConstraints,
data: &State,
env: &Env,
) -> Size {
if let Some(child) = &mut self.child {
let size = child.widget.layout(ctx, &child.bc, data, env);
let origin = child
.origin
.unwrap_or_else(|| (bc.max().to_vec2() / 2.0 - size.to_vec2() / 2.0).to_point());
child
.widget
.set_layout_rect(ctx, data, env, Rect::from_origin_size(origin, size));
}
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &State, env: &Env) {
if let Some(child) = &mut self.child {
child.widget.paint(ctx, data, env);
}
}
}
pub mod overlay;
use druid::kurbo::{Point, Rect, Size};
use druid::{
BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
UpdateCtx, Widget, WidgetPod,
};
pub struct Stack<T: Data> {
children: Vec<WidgetPod<T, Box<dyn Widget<T>>>>,
}
impl<T: Data> Stack<T> {
pub fn new() -> Self {
Stack {
children: Vec::new(),
}
}
pub fn with_child(mut self, child: impl Widget<T> + 'static) -> Self {
self.add_child(child);
self
}
pub fn add_child(&mut self, child: impl Widget<T> + 'static) {
self.children.push(WidgetPod::new(child).boxed());
}
}
impl<T: Data> Widget<T> for Stack<T> {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
self.children.reverse();
for child in &mut self.children {
child.event(ctx, event, data, env);
if ctx.is_handled() {
break;
}
}
self.children.reverse()
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
for child in &mut self.children {
child.lifecycle(ctx, event, data, env);
}
}
fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
for child in &mut self.children {
child.update(ctx, data, env);
}
}
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
for child in &mut self.children {
child.layout(ctx, bc, data, env);
child.set_layout_rect(
ctx,
data,
env,
Rect::from_origin_size(Point::ORIGIN, bc.max()),
)
}
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
for child in &mut self.children {
child.paint(ctx, data, env);
}
}
}
use std::{fmt::Display, mem, str::FromStr};
use druid::{
kurbo::Size, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx,
PaintCtx, UpdateCtx, Widget, WidgetId,
};
/// Converts a `Widget<String>` to a `Widget<T>`, not modifying the data if the input is invalid
pub struct ParseLazy<W> {
widget: W,
state: String,
}
impl<W> ParseLazy<W> {
pub fn new(widget: W) -> Self {
Self {
widget,
state: String::new(),
}
}
}
impl<T: FromStr + Display + Data, W: Widget<String>> Widget<T> for ParseLazy<W> {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
self.widget.event(ctx, event, &mut self.state, env);
if let Ok(res) = self.state.parse() {
*data = res;
}
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
if let LifeCycle::WidgetAdded = event {
self.state = data.to_string();
}
self.widget.lifecycle(ctx, event, &self.state, env)
}
fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
let old = mem::replace(&mut self.state, data.to_string());
self.widget.update(ctx, &old, &self.state, env)
}
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &T, env: &Env) -> Size {
self.widget.layout(ctx, bc, &self.state, env)
}
fn paint(&mut self, paint: &mut PaintCtx, _data: &T, env: &Env) {
self.widget.paint(paint, &self.state, env)
}
fn id(&self) -> Option<WidgetId> {
self.widget.id()
}
}
pub mod knob;
pub mod parse_lazy;
pub mod stack;
use std::f64::consts::TAU;
use druid::{
kurbo::{self, BezPath, Shape, Size},
piet::RenderContext,
widget::prelude::*,
Point, Vec2,
};
pub struct Knob {
initial_data: f32,
value_preview: f32,
click: Point,
}
impl Knob {
pub fn new() -> Self {
Self {
initial_data: 0.0,
value_preview: 0.0,
click: Point::ORIGIN,
}
}
}
const SPEED: f32 = 10e-3;
impl Widget<f32> for Knob {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut f32, _env: &Env) {
match event {
Event::MouseDown(evt) => {
if !ctx.is_disabled() {
ctx.set_active(true);
self.click = evt.pos;
self.value_preview = *data;
self.initial_data = *data;
ctx.request_paint();
}
}
Event::MouseMove(evt) => {
if ctx.is_active() {
let off = evt.pos - self.click;
self.value_preview = (self.initial_data + (off.x - off.y) as f32 * SPEED)
.min(1.0)
.max(0.0);
ctx.request_paint();
}
}
Event::MouseUp(evt) => {
if ctx.is_active() && !ctx.is_disabled() {
let off = evt.pos - self.click;
*data = (self.initial_data + (off.x - off.y) as f32 * SPEED)
.min(1.0)
.max(0.0);
ctx.request_paint();
}
ctx.set_active(false);
}
_ => (),
}
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &f32, _env: &Env) {
if let LifeCycle::HotChanged(_) | LifeCycle::DisabledChanged(_) = event {
ctx.request_paint();
}
}
fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &f32, _data: &f32, _env: &Env) {
ctx.request_paint();
}
fn layout(
&mut self,
_ctx: &mut LayoutCtx,
bc: &BoxConstraints,
_data: &f32,
_env: &Env,
) -> Size {
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &f32, env: &Env) {
let size = ctx.size();
let center = (size / 2.0).to_vec2().to_point();
let radius = size.width / 2.0 * 0.7;
let arc_path: BezPath = kurbo::Arc {
center,
radii: Vec2::new(radius, radius),
start_angle: 0.0,
sweep_angle: if ctx.is_active() {
self.value_preview as f64 * TAU
} else {
*data as f64 * TAU
},
x_rotation: -TAU / 4.0,
}
.into_path(0.1);
let mut wheel = arc_path.clone();
wheel.line_to(center);
wheel.line_to(center - Vec2::new(0.0, radius));
ctx.fill(wheel, &env.get(crate::theme::ACCENT));
ctx.stroke(arc_path, &env.get(crate::theme::ACCENT_DIM), 2.0);
}
}
Maybe::new(|| media_bar::ui(), || SizedBox::empty())
.lens(Map::new(
|s: &Rc<player::State>| s.get_playing().map(|s| Rc::new(s.clone())),
|s: &mut Rc<player::State>, inner: Option<Rc<player::state::Playing>>| {
*s = Rc::new(
inner
.map(|s| player::State::Playing((*s).clone()))
.unwrap_or(player::State::Idle),
);
},
))
.lens(State::player_state),
Maybe::new(|| media_bar::ui(), || SizedBox::empty()).lens(Map::new(
|s: &State| {
s.player_state.get_playing().map(|p| MediaBarState {
playing: Rc::new(p.clone()),
current_song: s.current_song.clone(),
})
},
|s: &mut State, inner: Option<MediaBarState>| {
s.player_state = Rc::new(
inner
.map(|s| player::State::Playing((*s.playing).clone()))
.unwrap_or(player::State::Idle),
);
},
)),
fn query_box() -> impl Widget<String> {
ControllerHost::new(
TextBox::new()
.with_placeholder("*")
.with_text_alignment(TextAlignment::Center),
Enter::new(|ctx, _, _| ctx.submit_command(command::QUERY_RUN)),
)
.expand_width()
fn query_box() -> impl Widget<State> {
Flex::row()
.with_child(play_query_button())
.with_default_spacer()
.with_flex_child(
ControllerHost::new(
TextBox::new()
.with_placeholder("*")
.with_text_alignment(TextAlignment::Center),
Enter::new(|ctx, _, _| ctx.submit_command(command::QUERY_RUN)),
)
.expand_width()
.lens(State::query),
1.0,
)
fn play_button() -> impl Widget<SongListItem> {
fn play_query_button() -> impl Widget<State> {
Painter::new(|ctx, _: &State, env| draw_icon_button(ctx, env, ICON_PLAY))
.fix_size(36.0, 36.0)
.on_click(|ctx: &mut EventCtx, _, _| {
ctx.submit_command(command::QUERY_PLAY);
})
}
fn play_song_button() -> impl Widget<SongListItem> {
let right_buttons = Flex::row()
.with_flex_spacer(1.0)
.with_child(Button::new("☰").on_click(
move |ctx: &mut EventCtx, _: &mut MediaBarState, _| {
ctx.submit_command(overlay::SHOW_MIDDLE.with((
BoxConstraints::tight(Size::new(300.0, 300.0)),
Box::new(move |env| {
Box::new(
Container::new(
Flex::column()
.with_child(
Maybe::new(
|| {
Label::new(|data: &Rc<Song>, _: &_| {
data.title.clone()
})
},
|| SizedBox::empty(),
)
.lens(State::current_song),
)
.with_flex_child(
List::new(|| {
Label::new(|data: &Rc<Song>, _: &_| data.title.clone())
})
.lens(State::queue),
1.0,
),
)
.border(env.get(theme::FOREGROUND), 1.0),
)
}),
)))
},
));
let song_info = Maybe::new(
|| Label::new(|data: &Rc<Song>, _: &_| data.title.clone()),
|| SizedBox::empty(),
)
.expand_width();
// pub fn playing_lens() -> druid::lens::Map<
// impl Fn(&Rc<player::State>) -> Option<Rc<player::state::Playing>>,
// impl Fn(&mut Rc<player::State>, Option<Rc<player::state::Playing>>),
// > {
// druid::lens::Map::new(
// |s: &Rc<player::State>| s.get_playing().map(|s| Rc::new(s.clone())),
// |s: &mut Rc<player::State>, inner: Option<Rc<player::state::Playing>>| {
// *s = Rc::new(
// inner
// .map(|s| player::State::Playing((*s).clone()))
// .unwrap_or(player::State::Idle),
// );
// },
// )
// }
}
_ if cmd.is(command::QUERY_PLAY) => {
let db = data.db.write();
let mut queue: im::Vector<Rc<Song>> = data
.songs
.iter()
.filter_map(|item| db.get_song(item.song.id).ok())
.map(|s| Rc::new(s))
.collect();
if let Some(song) = queue.pop_front() {
self.player
.queue_song(Url::parse(&song.source).unwrap())
.unwrap();
data.current_song = Some(song);
}
data.queue = queue;
druid::Handled::No
// queue next song
let until_empty = ps
.get_playing()
.map(|p| p.song.duration - p.offset)
.unwrap_or_default() + Duration::from_secs(
self.player.nb_queued() as u64 * 10000000,
);
if until_empty < Duration::from_secs(1) {
if let Some(song) = data.queue.pop_front() {
self.player
.queue_song(Url::parse(&song.source).unwrap())
.unwrap();
data.current_song = Some(song);
} else {
data.current_song = None;
}
}
if let Ok(song) = data.db.read().get_song(*id) {
match Url::parse(&song.source) {
Ok(url) => {
self.player.set_song(&url).unwrap();
self.player.play().unwrap();
}
Err(e) => {
panic!("{} {}", e, song.source);
}
}
}
let db = data.db.read();
let song = db.get_song(*id).unwrap();
let url = Url::parse(&song.source).unwrap();
self.player.queue_song(url).unwrap();
data.current_song = Some(Rc::new(song.clone()));
self.player.play().unwrap();
pub const SONG_ADD: Selector<NewSong> = Selector::new("song-add");
pub const SONG_DELETE: Selector<Uuid> = Selector::new("song-delete");
pub const SONG_ADD: Selector<NewSong> = Selector::new("song.add");
pub const SONG_DELETE: Selector<Uuid> = Selector::new("song.delete");
pub const TAG_ADD: Selector<Uuid> = Selector::new("tag-add");
pub const TAG_RENAME: Selector<(String, String)> = Selector::new("tag-rename");
pub const TAG_TWEAK: Selector<(String, f32)> = Selector::new("tag-tweak");
pub const TAG_DELETE: Selector<String> = Selector::new("tag-delete");
pub const TAG_ADD: Selector<Uuid> = Selector::new("tag.add");
pub const TAG_RENAME: Selector<(String, String)> = Selector::new("tag.rename");
pub const TAG_TWEAK: Selector<(String, f32)> = Selector::new("tag.tweak");
pub const TAG_DELETE: Selector<String> = Selector::new("tag.delete");