N6XOUOHS2X4M2BREDJXHRGICO4RFPNRS7OOYYQKNGUNQ5MK6LK7AC #[derive(Debug, Clone, PartialEq)]pub enum PlayerState {Idle,Playing {song: SongInfo,offset: Duration,paused: bool,},}use PlayerState::*;impl Default for PlayerState {fn default() -> Self {Idle}}impl PlayerState {pub fn current_song(&self) -> Option<&SongInfo> {match self {Playing { song, .. } => Some(song),_ => None,}}pub fn current_time(&self) -> Option<&Duration> {match self {Playing { offset, .. } => Some(offset),_ => None,}}fn set_song(&mut self, song: SongInfo) {*self = Playing {song,offset: Duration::ZERO,paused: true,};}
fn play(&mut self) -> Result<()> {match self {Playing { paused, .. } => {*paused = false;Ok(())}_ => Err(anyhow!("wrong state")),}}fn pause(&mut self) -> Result<()> {match self {Playing { paused, .. } => {*paused = true;Ok(())}_ => Err(anyhow!("wrong state")),}}fn seek(&mut self, position: Duration) -> Result<()> {match self {Playing { offset, .. } => {*offset = position;Ok(())}_ => Err(anyhow!("wrong state")),}}}
if let Some(song) = data.current_song() {let position = song.duration.mul_f64(evt.pos.x / ctx.size().width);ctx.submit_command(command::PLAYER_SEEK.with(position));}
let position = data.song.duration.mul_f64(evt.pos.x / ctx.size().width);ctx.submit_command(command::PLAYER_SEEK.with(position));
if let Some((position, song)) = match data.as_ref() {PlayerState::Playing { offset, song, .. } => Some((offset, song)),_ => None,} {let progress = if ctx.is_active() {self.position_preview} else {position.as_secs_f64() / song.duration.as_secs_f64()};let progress_right = Point::new(size.width * progress, size.height / 2.0);
let progress = if ctx.is_active() {self.position_preview} else {data.offset.as_secs_f64() / data.song.duration.as_secs_f64()};let progress_right = Point::new(size.width * progress, size.height / 2.0);
ctx.stroke_styled(Line::new(left, progress_right),&env.get(druid::theme::PRIMARY_DARK),6.0,&StrokeStyle {line_join: LineJoin::Round,line_cap: LineCap::Round,..Default::default()},);}
ctx.stroke_styled(Line::new(left, progress_right),&env.get(theme::ACCENT),6.0,&StrokeStyle {line_join: LineJoin::Round,line_cap: LineCap::Round,..Default::default()},);
root.add_child(player_controller_ui());
root.add_child(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),);
Flex::column().cross_axis_alignment(CrossAxisAlignment::Fill).with_child(Flex::row().with_child(Label::new("title")).with_child(TextBox::new().lens(SongEdit::title)),).with_default_spacer().with_child(Flex::row().with_child(Label::new("source")).with_child(TextBox::new().lens(SongEdit::source)),).with_default_spacer().with_child(List::new(|| TagEdit::new()).lens(SongEdit::tags)).with_child(Button::new("+").on_click(|ctx, data: &mut SongEdit, _| {ctx.submit_command(command::TAG_ADD.with(*data.id))})).with_flex_spacer(1.0).with_child(Button::new("CLOSE").on_click(|ctx, _: &mut SongEdit, _| {
let col =Flex::column().cross_axis_alignment(CrossAxisAlignment::Fill).with_child(Flex::row().with_child(Label::new("title")).with_child(TextBox::new().lens(SongEdit::title)),).with_default_spacer().with_child(Flex::row().with_child(Label::new("source")).with_child(TextBox::new().lens(SongEdit::source)),).with_default_spacer().with_child(List::new(|| TagEdit::new()).lens(SongEdit::tags)).with_child(Button::new("+").on_click(|ctx, data: &mut SongEdit, _| {ctx.submit_command(command::TAG_ADD.with(*data.id))})).with_flex_spacer(1.0).with_child(Button::new("CLOSE").on_click(|ctx, _: &mut SongEdit, _| {
}),).env_scope(|env, _| env.set(theme::BORDER_DARK, Color::TRANSPARENT)).fix_width(400.0)
})).env_scope(|env, _| env.set(theme::BORDER_DARK, Color::TRANSPARENT)).fix_width(400.0).padding(8.0);Container::new(col).background(crate::theme::BACKGROUND_HIGHLIGHT0)
fn player_controller_ui() -> impl Widget<State> {let buttons = Flex::row().with_child(Button::new("🔀")).with_default_spacer().with_child(Button::new("⏮")).with_default_spacer().with_child(Button::new("⏯").on_click(|ctx, _, _| ctx.submit_command(command::PLAYER_PLAY_PAUSE)),).with_default_spacer().with_child(Button::new("⏭")).with_default_spacer().with_child(Button::new("🔁"));
fn play_button() -> impl Widget<SongListItem> {Painter::new(|ctx, _: &SongListItem, env| draw_icon_button(ctx, env, ICON_PLAY)).fix_size(36.0, 36.0).on_click(|ctx: &mut EventCtx, item: &mut SongListItem, _| {ctx.submit_command(command::SONG_PLAY.with(item.song.id));})}
Flex::column().with_child(buttons).with_child(Flex::row().with_child(Label::new(|data: &Rc<PlayerState>, _: &_| {format_duration(data.current_time().unwrap_or(&Duration::ZERO))})).with_default_spacer().with_flex_child(PlayerBar::default(), 1.0).with_default_spacer().with_child(Label::new(|data: &Rc<PlayerState>, _: &_| {format_duration(data.current_song().map(|s| &s.duration).unwrap_or(&Duration::ZERO),)})),).expand_width().lens(State::player_state)}
pub const ICON_PLAY: &str = "M0.750,0.567 L0.750,1.433 L1.500,1.000 L0.750,0.567";pub const ICON_PAUSE: &str = "M0.598,0.521 L0.598,1.479 L0.866,1.479 L0.866,0.521 L0.598,0.521 M1.402,0.521 L1.134,0.521 L1.134,1.479 L1.402,1.479 L1.402,0.521";pub const ICON_PREV: &str = "M1.250,0.567 L0.700,0.885 L0.700,0.567 L0.500,0.567 L0.500,1.000 L0.500,1.433 L0.700,1.433 L0.700,1.115 L1.250,1.433 L1.250,0.567";pub const ICON_NEXT: &str = "M0.750,1.433 L1.300,1.115 L1.300,1.433 L1.500,1.433 L1.500,1.000 L1.500,0.567 L1.300,0.567 L1.300,0.885 L0.750,0.567 L0.750,1.433";pub const ICON_EDIT: &str = "M1.000 0.513 L0.700,0.513 L0.700,1.000 L0.700,1.487 L1.000,1.487 L1.300,1.487 L1.300,1.017 L1.024,1.017 L1.024,0.699 L1.210,0.513 L1.000,0.513 M1.654 0.564 L1.477,0.387 L1.124,0.741 L1.124,0.917 L1.300,0.917 L1.654,0.564";pub const ICON_DELETE: &str = "M0.814 1.394 L1.000,1.394 L1.186,1.394 A0.044,0.044 0 0,0 1.229,1.357 L1.348,0.695 A0.044,0.044 0 0,0 1.393,0.651 A0.044,0.044 0 0,0 1.348,0.606 L0.652,0.606 A0.044,0.044 0 0,0 0.607,0.651 A0.044,0.044 0 0,0 0.652,0.695 L0.771,1.357 A0.044,0.044 0 0,0 0.814,1.394";
fn format_duration(d: &Duration) -> String {format!("{:02}:{:02}", d.as_secs() / 60, d.as_secs() % 60)
pub fn draw_icon_button(ctx: &mut PaintCtx, env: &Env, icon_svg: &str) {let size = ctx.size();let rad = size.min_side() / 2.0;if ctx.is_hot() {ctx.fill(Circle::new((size.to_vec2() / 2.0).to_point(), rad),&env.get(crate::theme::BACKGROUND_HIGHLIGHT1),);}ctx.fill(Affine::scale(rad) * BezPath::from_svg(icon_svg).unwrap(),&env.get(theme::FOREGROUND_LIGHT),);
use std::{rc::Rc, time::Duration};use druid::{widget::{Flex, Label, Painter},EventCtx, Widget, WidgetExt,};use tf_player::player::{self, state::Playing};use super::{draw_icon_button, ICON_NEXT, ICON_PAUSE, ICON_PLAY, ICON_PREV};use crate::{command, widget::player_bar::PlayerBar};pub fn ui() -> impl Widget<Rc<player::state::Playing>> {let buttons = Flex::row().with_child(prev_button()).with_default_spacer().with_child(play_pause_button()).with_default_spacer().with_child(next_button());Flex::column().with_child(buttons).with_child(Flex::row().with_child(Label::new(|data: &Rc<Playing>, _: &_| {format_duration(&data.offset)})).with_default_spacer().with_flex_child(PlayerBar::default(), 1.0).with_default_spacer().with_child(Label::new(|data: &Rc<Playing>, _: &_| {format_duration(&data.song.duration)})),).expand_width()}fn play_pause_button() -> impl Widget<Rc<Playing>> {Painter::new(|ctx, data: &Rc<Playing>, env| {draw_icon_button(ctx, env, if data.paused { ICON_PLAY } else { ICON_PAUSE })}).fix_size(36.0, 36.0).on_click(|ctx: &mut EventCtx, _, _| {ctx.submit_command(command::PLAYER_PLAY_PAUSE);})}fn prev_button() -> impl Widget<Rc<Playing>> {Painter::new(|ctx, _, env| draw_icon_button(ctx, env, ICON_PREV)).fix_size(36.0, 36.0).on_click(|ctx: &mut EventCtx, _, _| {ctx.submit_command(command::PLAYER_PREV);})}fn next_button() -> impl Widget<Rc<Playing>> {Painter::new(|ctx, _, env| draw_icon_button(ctx, env, ICON_NEXT)).fix_size(36.0, 36.0).on_click(|ctx: &mut EventCtx, _, _| {ctx.submit_command(command::PLAYER_NEXT);})}fn format_duration(d: &Duration) -> String {format!("{:02}:{:02}", d.as_secs() / 60, d.as_secs() % 60)}
pub const THEME_ACCENT: Key<Color> = Key::new("theme.accent");
pub const ACCENT: Key<Color> = Key::new("theme.accent");pub const ACCENT_DIM: Key<Color> = Key::new("theme.accent-dim");pub const BACKGROUND: Key<Color> = Key::new("theme.background");pub const BACKGROUND_HIGHLIGHT0: Key<Color> = Key::new("theme.background-highlight-0");pub const BACKGROUND_HIGHLIGHT1: Key<Color> = Key::new("theme.background-highlight-1");
const ACCENT: Color = color(0x26c4e7);
mod colors {use druid::Color;use super::color;pub const BACKGROUND: Color = color(0x282C34);pub const BACKGROUND_HIGHLIGHT0: Color = color(0x2c313a);pub const BACKGROUND_HIGHLIGHT1: Color = color(0x3a404c);pub const ACCENT: Color = color(0x98c379);pub const ACCENT_DIM: Color = color(0x8bb16e);}
env.set(THEME_ACCENT, ACCENT);
env.set(ACCENT, colors::ACCENT);env.set(ACCENT_DIM, colors::ACCENT_DIM);env.set(BACKGROUND, colors::BACKGROUND);env.set(BACKGROUND_HIGHLIGHT0, colors::BACKGROUND_HIGHLIGHT0);env.set(BACKGROUND_HIGHLIGHT1, colors::BACKGROUND_HIGHLIGHT1);
env.set(WINDOW_BACKGROUND_COLOR, color(0xffffff));env.set(BACKGROUND_DARK, color(0xeeeeee));env.set(BACKGROUND_LIGHT, color(0xffffff));
env.set(WINDOW_BACKGROUND_COLOR, color(0x282C34));env.set(BACKGROUND_DARK, color(0x282C34));env.set(BACKGROUND_LIGHT, color(0x363b43));