Merge pull request 'SAO music player controller' (#42) from sao-music-player into main
Reviewed-on: #42
This commit is contained in:
commit
a84f11ec4a
|
@ -10,6 +10,7 @@ crate-type = ["cdylib"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
glam = "^0.21"
|
glam = "^0.21"
|
||||||
keyframe = "1"
|
keyframe = "1"
|
||||||
|
canary-music-player = { path = "../../apps/music-player" }
|
||||||
canary-script = { path = "../../crates/script" }
|
canary-script = { path = "../../crates/script" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
|
@ -6,17 +6,25 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
|
||||||
pub mod anim;
|
pub mod anim;
|
||||||
pub mod main_menu;
|
pub mod main_menu;
|
||||||
|
pub mod music_player;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
use api::*;
|
use api::*;
|
||||||
use canary_script::*;
|
use canary_script::*;
|
||||||
use main_menu::MainMenuPanel;
|
use main_menu::MainMenuPanel;
|
||||||
|
use music_player::MusicPlayerPanel;
|
||||||
use widgets::Widget;
|
use widgets::Widget;
|
||||||
|
|
||||||
export_abi!(bind_panel_impl);
|
export_abi!(bind_panel_impl);
|
||||||
|
|
||||||
fn bind_panel_impl(panel: Panel, _protocol: Message, msg: Message) -> Box<dyn PanelImpl> {
|
fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box<dyn PanelImpl> {
|
||||||
MainMenuPanel::bind(panel, msg)
|
let protocol = protocol.to_vec();
|
||||||
|
let protocol = String::from_utf8(protocol).unwrap();
|
||||||
|
|
||||||
|
match protocol.as_str() {
|
||||||
|
"tebibyte-media.desktop.music-player-controller" => MusicPlayerPanel::bind(panel, msg),
|
||||||
|
_ => MainMenuPanel::bind(panel, msg),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ICON_FONT: &str = "Iosevka Nerd Font";
|
pub const ICON_FONT: &str = "Iosevka Nerd Font";
|
||||||
|
|
|
@ -0,0 +1,362 @@
|
||||||
|
use api::*;
|
||||||
|
use canary_script::*;
|
||||||
|
|
||||||
|
use canary_music_player::{AlbumInfo, PlaybackStatus, ProgressChanged, TrackInfo};
|
||||||
|
|
||||||
|
use crate::widgets::prelude::*;
|
||||||
|
use button::{RoundButton, RoundButtonStyle};
|
||||||
|
use dialog::{DialogBodyStyle, DialogFooterStyle};
|
||||||
|
use shell::Offset;
|
||||||
|
use text::{HorizontalAlignment, Label, LabelText};
|
||||||
|
|
||||||
|
pub struct MusicPlayerPanel {
|
||||||
|
panel: Panel,
|
||||||
|
widget: Option<MusicPlayerWidget>,
|
||||||
|
disconnected: Offset<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PanelImpl for MusicPlayerPanel {
|
||||||
|
fn update(&mut self, dt: f32) {
|
||||||
|
if let Some(widget) = self.widget.as_mut() {
|
||||||
|
Widget::update(widget, dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self) {
|
||||||
|
let ctx = DrawContext::new(self.panel);
|
||||||
|
|
||||||
|
if let Some(widget) = self.widget.as_mut() {
|
||||||
|
Widget::draw(widget, &ctx);
|
||||||
|
} else {
|
||||||
|
self.disconnected.draw(&ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_resize(&mut self, new_size: Vec2) {
|
||||||
|
self.disconnected.set_offset(new_size / 2.0);
|
||||||
|
|
||||||
|
if let Some(widget) = self.widget.as_mut() {
|
||||||
|
widget.resize(new_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
||||||
|
if let Some(widget) = self.widget.as_mut() {
|
||||||
|
Widget::on_cursor_event(widget, kind, at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_message(&mut self, msg: Message) {
|
||||||
|
use canary_music_player::{serde_json::from_slice, InMsg};
|
||||||
|
let msg = msg.to_vec();
|
||||||
|
let msg: InMsg = match from_slice(&msg) {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
use InMsg::*;
|
||||||
|
match (self.widget.as_mut(), msg) {
|
||||||
|
(Some(_), Disconnected) => self.widget = None,
|
||||||
|
(None, Connected) => self.widget = Some(MusicPlayerWidget::new()),
|
||||||
|
(Some(widget), AlbumChanged(info)) => widget.update_album(info),
|
||||||
|
(Some(widget), TrackChanged(info)) => widget.update_track(info),
|
||||||
|
(Some(widget), PlaybackStatusChanged(status)) => widget.update_playback_status(status),
|
||||||
|
(Some(widget), ProgressChanged(progress)) => widget.update_progress(progress),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MusicPlayerPanel {
|
||||||
|
pub fn bind(panel: Panel, _msg: Message) -> Box<dyn PanelImpl> {
|
||||||
|
let dc_text = LabelText {
|
||||||
|
font: Font::new(crate::DISPLAY_FONT),
|
||||||
|
text: "Disconnected".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let disconnected = Label::new_centered(dc_text, 10.0, Color::WHITE);
|
||||||
|
let disconnected = Offset::new(disconnected, Vec2::ZERO);
|
||||||
|
|
||||||
|
Box::new(Self {
|
||||||
|
panel,
|
||||||
|
widget: None,
|
||||||
|
disconnected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MusicPlayerStyle {
|
||||||
|
pub body: DialogBodyStyle,
|
||||||
|
pub footer: DialogFooterStyle,
|
||||||
|
pub rounding: f32,
|
||||||
|
pub art_margin: f32,
|
||||||
|
pub button_spacing: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MusicPlayerStyle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
body: Default::default(),
|
||||||
|
footer: Default::default(),
|
||||||
|
rounding: 5.0,
|
||||||
|
art_margin: 5.0,
|
||||||
|
button_spacing: 15.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MusicPlayerWidget {
|
||||||
|
artist: Offset<Label>,
|
||||||
|
album: Offset<Label>,
|
||||||
|
track: Offset<Label>,
|
||||||
|
previous: Offset<RoundButton>,
|
||||||
|
play: Offset<RoundButton>,
|
||||||
|
next: Offset<RoundButton>,
|
||||||
|
position: Offset<Label>,
|
||||||
|
duration: Offset<Label>,
|
||||||
|
style: MusicPlayerStyle,
|
||||||
|
art_rect: Rect,
|
||||||
|
body_rect: Rect,
|
||||||
|
footer_rect: Rect,
|
||||||
|
position_secs: f32,
|
||||||
|
position_dirty: bool,
|
||||||
|
status: PlaybackStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Container for MusicPlayerWidget {
|
||||||
|
fn with_children(&mut self, mut f: impl FnMut(&mut dyn Widget)) {
|
||||||
|
f(&mut self.artist);
|
||||||
|
f(&mut self.album);
|
||||||
|
f(&mut self.track);
|
||||||
|
f(&mut self.previous);
|
||||||
|
f(&mut self.play);
|
||||||
|
f(&mut self.next);
|
||||||
|
f(&mut self.position);
|
||||||
|
f(&mut self.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, dt: f32) {
|
||||||
|
if let PlaybackStatus::Playing = self.status {
|
||||||
|
self.position_secs += dt;
|
||||||
|
self.position_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.position_dirty {
|
||||||
|
self.position_dirty = false;
|
||||||
|
self.position
|
||||||
|
.set_text(&Self::format_time(self.position_secs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, ctx: &DrawContext) {
|
||||||
|
ctx.draw_partially_rounded_rect(
|
||||||
|
CornerFlags::TOP,
|
||||||
|
self.body_rect,
|
||||||
|
self.style.rounding,
|
||||||
|
self.style.body.color,
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.draw_rounded_rect(self.art_rect, self.style.rounding, Color::MAGENTA);
|
||||||
|
|
||||||
|
ctx.draw_partially_rounded_rect(
|
||||||
|
CornerFlags::BOTTOM,
|
||||||
|
self.footer_rect,
|
||||||
|
self.style.rounding,
|
||||||
|
self.style.footer.color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MusicPlayerWidget {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let style = MusicPlayerStyle::default();
|
||||||
|
let display_font = Font::new(crate::DISPLAY_FONT);
|
||||||
|
let content_font = Font::new(crate::CONTENT_FONT);
|
||||||
|
|
||||||
|
let make_body_label = |content: &str| {
|
||||||
|
let text = LabelText {
|
||||||
|
font: display_font,
|
||||||
|
text: content.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let label = Label::new_centered(text, 10.0, Color::BLACK);
|
||||||
|
Offset::new(label, Vec2::ZERO)
|
||||||
|
};
|
||||||
|
|
||||||
|
let make_footer_label = |content: &str| {
|
||||||
|
let text = LabelText {
|
||||||
|
font: content_font,
|
||||||
|
text: content.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = style.footer.height;
|
||||||
|
let scale = size * 0.4;
|
||||||
|
let baseline = size * 0.125;
|
||||||
|
let label = Label::new(
|
||||||
|
text,
|
||||||
|
HorizontalAlignment::Center,
|
||||||
|
scale,
|
||||||
|
Color::BLACK,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
baseline,
|
||||||
|
);
|
||||||
|
Offset::new(label, Vec2::ZERO)
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon_font = Font::new(crate::ICON_FONT);
|
||||||
|
|
||||||
|
let prev_text = LabelText {
|
||||||
|
font: icon_font,
|
||||||
|
text: "玲".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let play_text = LabelText {
|
||||||
|
font: icon_font,
|
||||||
|
text: "契".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let next_text = LabelText {
|
||||||
|
font: icon_font,
|
||||||
|
text: "怜".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let primary_button = RoundButtonStyle {
|
||||||
|
radius: style.footer.height * 0.3,
|
||||||
|
spacing: style.footer.height * 0.1,
|
||||||
|
thickness: style.footer.height * 0.025,
|
||||||
|
body_color: Color(0xf1b841ff),
|
||||||
|
ring_color: Color(0xf1b841ff),
|
||||||
|
icon_color: Color::BLACK,
|
||||||
|
};
|
||||||
|
|
||||||
|
let secondary_button = RoundButtonStyle {
|
||||||
|
radius: style.footer.height * 0.25,
|
||||||
|
spacing: style.footer.height * 0.05,
|
||||||
|
thickness: style.footer.height * 0.025,
|
||||||
|
body_color: Color::WHITE,
|
||||||
|
ring_color: Color::BLACK,
|
||||||
|
icon_color: Color::BLACK,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prev = RoundButton::new(secondary_button.clone(), Some(prev_text));
|
||||||
|
let play = RoundButton::new(primary_button, Some(play_text));
|
||||||
|
let next = RoundButton::new(secondary_button, Some(next_text));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
artist: make_body_label("Artist"),
|
||||||
|
album: make_body_label("Album"),
|
||||||
|
track: make_body_label("Track"),
|
||||||
|
previous: Offset::new(prev, Vec2::ZERO),
|
||||||
|
play: Offset::new(play, Vec2::ZERO),
|
||||||
|
next: Offset::new(next, Vec2::ZERO),
|
||||||
|
position: make_footer_label("--:--"),
|
||||||
|
duration: make_footer_label("--:--"),
|
||||||
|
style,
|
||||||
|
art_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||||
|
body_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||||
|
footer_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||||
|
position_secs: 0.0,
|
||||||
|
position_dirty: false,
|
||||||
|
status: PlaybackStatus::Paused,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_time(secs: f32) -> String {
|
||||||
|
let duration = secs.floor() as usize;
|
||||||
|
let seconds = duration % 60;
|
||||||
|
let minutes = (duration / 60) % 60;
|
||||||
|
let hours = (duration / 60) / 60;
|
||||||
|
|
||||||
|
if hours > 0 {
|
||||||
|
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
|
||||||
|
} else {
|
||||||
|
format!("{:02}:{:02}", minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&mut self, new_size: Vec2) {
|
||||||
|
let style = &self.style;
|
||||||
|
let width = new_size.x;
|
||||||
|
let body_height = new_size.y - style.footer.height;
|
||||||
|
let body_height = body_height.max(0.0);
|
||||||
|
let body_size = Vec2::new(width, body_height);
|
||||||
|
let footer_size = Vec2::new(width, style.footer.height);
|
||||||
|
|
||||||
|
let art_size = body_height - style.art_margin * 2.0;
|
||||||
|
self.art_rect = Rect::from_xy_size(Vec2::splat(style.art_margin), Vec2::splat(art_size));
|
||||||
|
|
||||||
|
let label_x = (width + self.art_rect.br.x) / 2.0;
|
||||||
|
let artist_baseline = body_height * 0.25;
|
||||||
|
let album_baseline = body_height * 0.55;
|
||||||
|
let track_baseline = body_height * 0.85;
|
||||||
|
|
||||||
|
let button_y = body_height + style.footer.height / 2.0;
|
||||||
|
let previous_x = style.button_spacing * 0.5;
|
||||||
|
let play_x = style.button_spacing * 1.5;
|
||||||
|
let next_x = style.button_spacing * 2.5;
|
||||||
|
let position_x = style.button_spacing * 3.5;
|
||||||
|
let duration_x = width - style.button_spacing * 0.75;
|
||||||
|
|
||||||
|
self.artist.set_offset(Vec2::new(label_x, artist_baseline));
|
||||||
|
self.album.set_offset(Vec2::new(label_x, album_baseline));
|
||||||
|
self.track.set_offset(Vec2::new(label_x, track_baseline));
|
||||||
|
self.position.set_offset(Vec2::new(position_x, button_y));
|
||||||
|
self.duration.set_offset(Vec2::new(duration_x, button_y));
|
||||||
|
|
||||||
|
self.body_rect = Rect::from_xy_size(Vec2::ZERO, body_size);
|
||||||
|
self.footer_rect = Rect::from_xy_size(self.body_rect.bl(), footer_size);
|
||||||
|
|
||||||
|
self.previous.set_offset(Vec2::new(previous_x, button_y));
|
||||||
|
self.play.set_offset(Vec2::new(play_x, button_y));
|
||||||
|
self.next.set_offset(Vec2::new(next_x, button_y));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_album(&mut self, info: AlbumInfo) {
|
||||||
|
self.album.set_text(
|
||||||
|
info.title
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("<album here>"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_track(&mut self, info: TrackInfo) {
|
||||||
|
self.artist.set_text(
|
||||||
|
info.artists
|
||||||
|
.first()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("<artist here>"),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.track.set_text(
|
||||||
|
info.title
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("<album here>"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_progress(&mut self, progress: ProgressChanged) {
|
||||||
|
self.position_secs = progress.position;
|
||||||
|
self.position_dirty = true;
|
||||||
|
|
||||||
|
if let Some(length) = progress.length {
|
||||||
|
self.duration.set_text(&Self::format_time(length));
|
||||||
|
} else {
|
||||||
|
self.duration.set_text("--:--");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_playback_status(&mut self, status: PlaybackStatus) {
|
||||||
|
self.status = status;
|
||||||
|
|
||||||
|
let icon = match status {
|
||||||
|
PlaybackStatus::Playing => "契",
|
||||||
|
PlaybackStatus::Paused => "",
|
||||||
|
PlaybackStatus::Stopped => "栗",
|
||||||
|
};
|
||||||
|
|
||||||
|
self.play.set_text(icon);
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,12 @@ impl RoundButton {
|
||||||
icon,
|
icon,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&mut self, text: &str) {
|
||||||
|
if let Some(icon) = self.icon.as_mut() {
|
||||||
|
icon.set_text(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Button for RoundButton {
|
impl Button for RoundButton {
|
||||||
|
|
|
@ -104,7 +104,7 @@ impl Default for DialogFooterStyle {
|
||||||
icon_font: Font::new(crate::ICON_FONT),
|
icon_font: Font::new(crate::ICON_FONT),
|
||||||
button_radius: 7.5,
|
button_radius: 7.5,
|
||||||
color: Color::WHITE,
|
color: Color::WHITE,
|
||||||
height: 20.0,
|
height: 15.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,26 @@ impl Label {
|
||||||
offset: Vec2::ZERO,
|
offset: Vec2::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_centered(text: LabelText, scale: f32, color: Color) -> Self {
|
||||||
|
Self::new(
|
||||||
|
text,
|
||||||
|
HorizontalAlignment::Center,
|
||||||
|
scale,
|
||||||
|
color,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&mut self, text: &str) {
|
||||||
|
if self.text.text != text {
|
||||||
|
self.text.text = text.to_string();
|
||||||
|
self.layout = None;
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Label {
|
impl Widget for Label {
|
||||||
|
@ -108,6 +128,14 @@ impl Icon {
|
||||||
offset: Vec2::ZERO,
|
offset: Vec2::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&mut self, text: &str) {
|
||||||
|
if self.text.text != text {
|
||||||
|
self.text.text = text.to_string();
|
||||||
|
self.layout = None;
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Icon {
|
impl Widget for Icon {
|
||||||
|
|
Loading…
Reference in New Issue