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));