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 invalidpub 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 invalidpub 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 songlet 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");