Compare commits

..

2 commits

Author SHA1 Message Date
dae2882e0d playbinsong -> model::song 2024-11-05 21:44:20 +01:00
b5fadce142 fix drag and drop 2024-11-05 16:29:43 +01:00
8 changed files with 99 additions and 50 deletions

View file

@ -3,10 +3,10 @@ using Gtk 4.0;
template $AudreyUiPlayQueueSong: Box { template $AudreyUiPlayQueueSong: Box {
height-request: 48; height-request: 48;
spacing: 12; spacing: 12;
margin-start: 6;
margin-end: 6;
Box { Box {
margin-start: 6;
width-request: 36; width-request: 36;
focusable: false; focusable: false;
homogeneous: true; homogeneous: true;
@ -39,7 +39,7 @@ template $AudreyUiPlayQueueSong: Box {
margin-top: 1; margin-top: 1;
margin-bottom: 1; margin-bottom: 1;
pixel-size: 50; pixel-size: 50;
// paintable: bind template.song as <$AudreyPlaybinSong>.thumbnail; // paintable: bind template.song as <$AudreyModelSong>.thumbnail;
} }
Box title_box { Box title_box {
@ -70,7 +70,7 @@ template $AudreyUiPlayQueueSong: Box {
ellipsize: end; ellipsize: end;
max-width-chars: 90; max-width-chars: 90;
justify: fill; justify: fill;
label: bind template.song as <$AudreyPlaybinSong>.title; label: bind template.song as <$AudreyModelSong>.title;
} }
Label { Label {
@ -85,7 +85,7 @@ template $AudreyUiPlayQueueSong: Box {
ellipsize: end; ellipsize: end;
max-width-chars: 90; max-width-chars: 90;
justify: fill; justify: fill;
label: bind template.song as <$AudreyPlaybinSong>.artist; label: bind template.song as <$AudreyModelSong>.artist;
} }
} }
} }
@ -101,12 +101,12 @@ template $AudreyUiPlayQueueSong: Box {
"dim-label" "dim-label"
] ]
label: bind $format_duration(template.song as <$AudreyPlaybinSong>.duration) as <string>; label: bind $format_duration(template.song as <$AudreyModelSong>.duration) as <string>;
} }
Button { Button {
focusable: true; focusable: true;
// TODO icon-name: bind $star_button_icon_name (template.song as <$AudreyPlaybinSong>.starred) as <string>; // TODO icon-name: bind $star_button_icon_name (template.song as <$AudreyModelSong>.starred) as <string>;
icon-name: bind $star_button_icon_name() as <string>; icon-name: bind $star_button_icon_name() as <string>;
styles [ styles [
@ -116,20 +116,24 @@ template $AudreyUiPlayQueueSong: Box {
valign: center; valign: center;
} }
MenuButton { Box {
// visible: false; MenuButton {
focusable: true; // visible: false;
icon-name: "view-more"; focusable: true;
icon-name: "view-more";
styles [ styles [
"flat" "flat"
] ]
valign: center; valign: center;
popover: PopoverMenu { popover: PopoverMenu {
menu-model: song-menu; menu-model: song-menu;
}; };
}
margin-end: 6;
} }
DragSource { DragSource {

View file

@ -19,6 +19,11 @@
min-height: 15px; min-height: 15px;
} }
/* make drag and drop indicator take up entire perimeter */
#play-queue listview row {
padding: 0;
}
#play-queue .playing label.title { #play-queue .playing label.title {
font-weight: bold; font-weight: bold;
} }

View file

@ -11,8 +11,7 @@ pub mod ui;
pub mod mpris; pub mod mpris;
pub use mpris::Mpris; pub use mpris::Mpris;
pub mod playbin_song; pub mod model;
pub use playbin_song::Song as PlaybinSong;
pub mod subsonic; pub mod subsonic;

2
src/model.rs Normal file
View file

@ -0,0 +1,2 @@
mod song;
pub use song::Song;

View file

@ -35,7 +35,7 @@ mod imp {
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for Song { impl ObjectSubclass for Song {
const NAME: &'static str = "AudreyPlaybinSong"; const NAME: &'static str = "AudreyModelSong";
type Type = super::Song; type Type = super::Song;
} }

View file

@ -1,5 +1,5 @@
mod imp { mod imp {
use crate::PlaybinSong; use crate::model::Song as ModelSong;
use glib::subclass::InitializingObject; use glib::subclass::InitializingObject;
use gtk::{gdk, gio, glib, prelude::*, subclass::prelude::*}; use gtk::{gdk, gio, glib, prelude::*, subclass::prelude::*};
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
@ -28,7 +28,7 @@ mod imp {
#[property(set, get)] #[property(set, get)]
displayed_position: Cell<u32>, displayed_position: Cell<u32>,
#[property(get, set)] #[property(get, set)]
song: RefCell<Option<PlaybinSong>>, song: RefCell<Option<ModelSong>>,
drag_pos: Cell<(i32, i32)>, drag_pos: Cell<(i32, i32)>,
drag_widget: Cell<Option<gtk::ListBox>>, drag_widget: Cell<Option<gtk::ListBox>>,
@ -112,8 +112,7 @@ mod imp {
fn on_drag_prepare(&self, x: f64, y: f64) -> Option<gdk::ContentProvider> { fn on_drag_prepare(&self, x: f64, y: f64) -> Option<gdk::ContentProvider> {
if self.draggable.get() { if self.draggable.get() {
self.drag_pos.replace((x as i32, y as i32)); self.drag_pos.replace((x as i32, y as i32));
let value = self.obj().to_value(); Some(gdk::ContentProvider::for_value(&self.obj().to_value()))
Some(gdk::ContentProvider::for_value(&value))
} else { } else {
None None
} }
@ -122,6 +121,7 @@ mod imp {
#[template_callback] #[template_callback]
fn on_drag_begin(&self, drag: &gdk::Drag) { fn on_drag_begin(&self, drag: &gdk::Drag) {
let drag_widget = gtk::ListBox::new(); let drag_widget = gtk::ListBox::new();
drag_widget.add_css_class("boxed-list");
let drag_row: super::Song = glib::Object::new(); let drag_row: super::Song = glib::Object::new();
drag_row.set_draggable(false); drag_row.set_draggable(false);
@ -148,19 +148,23 @@ mod imp {
} }
#[template_callback] #[template_callback]
fn on_drop(&self, _value: glib::Value, _x: f64, _y: f64) -> bool { // why BoxedValue? see https://discourse.gnome.org/t/gtk-rs-passing-widget-as-gvalue-through-drag-and-drop-with-composite-template/16449/2
/* FIXME: WrongValueType(ValueTypeMismatchError { actual: GValue, requested: AudreyUiPlayQueueSong }) fn on_drop(&self, value: glib::BoxedValue, _x: f64, _y: f64) -> bool {
let source: super::Song = value.get().unwrap(); let source: super::Song = value.get().unwrap();
source.imp().drag_widget.set(None); source.imp().drag_widget.set(None);
self.obj().playbin().move_track( let from = source.position();
source.displayed_position() - 1, let to = self.obj().position();
self.obj().displayed_position() - 1,
);
true */ // see playlist_move for justification
false if from < to {
self.obj().window().playlist_move(from, to + 1);
} else {
self.obj().window().playlist_move(from, to);
}
true
} }
fn set_playlist_pos(&self, playlist_pos: i32) { fn set_playlist_pos(&self, playlist_pos: i32) {
@ -170,7 +174,7 @@ mod imp {
} }
} }
use crate::PlaybinSong; use crate::model::Song as ModelSong;
use adw::prelude::*; use adw::prelude::*;
use gtk::glib; use gtk::glib;
@ -186,7 +190,7 @@ impl Song {
} }
pub fn bind(&self, position: u32, window: &crate::ui::Window) { pub fn bind(&self, position: u32, window: &crate::ui::Window) {
let song: PlaybinSong = window let song: ModelSong = window
.playlist_model() .playlist_model()
.item(position) .item(position)
.unwrap() .unwrap()

View file

@ -1,5 +1,5 @@
mod imp { mod imp {
use crate::PlaybinSong; use crate::model::Song;
use adw::prelude::*; use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::subclass::InitializingObject; use glib::subclass::InitializingObject;
@ -16,7 +16,7 @@ mod imp {
pulse_bar: TemplateChild<gtk::ProgressBar>, pulse_bar: TemplateChild<gtk::ProgressBar>,
#[property(get, set, nullable)] #[property(get, set, nullable)]
song: RefCell<Option<PlaybinSong>>, song: RefCell<Option<Song>>,
#[property(get, set)] #[property(get, set)]
playing_cover_art: RefCell<Option<gdk::Paintable>>, playing_cover_art: RefCell<Option<gdk::Paintable>>,
#[property(get, set, default = true)] #[property(get, set, default = true)]
@ -168,18 +168,18 @@ mod imp {
// these are nedeed because with regular bindings, if song becomes None, the labes are not // these are nedeed because with regular bindings, if song becomes None, the labes are not
// updated // updated
#[template_callback] #[template_callback]
fn song_title(&self, song: Option<&PlaybinSong>) -> String { fn song_title(&self, song: Option<&Song>) -> String {
song.map(PlaybinSong::title).unwrap_or("".to_string()) song.map(Song::title).unwrap_or("".to_string())
} }
#[template_callback] #[template_callback]
fn song_artist(&self, song: Option<&PlaybinSong>) -> String { fn song_artist(&self, song: Option<&Song>) -> String {
song.map(PlaybinSong::artist).unwrap_or("".to_string()) song.map(Song::artist).unwrap_or("".to_string())
} }
#[template_callback] #[template_callback]
fn song_album(&self, song: Option<&PlaybinSong>) -> String { fn song_album(&self, song: Option<&Song>) -> String {
song.map(PlaybinSong::album).unwrap_or("".to_string()) song.map(Song::album).unwrap_or("".to_string())
} }
} }

View file

@ -1,6 +1,6 @@
mod imp { mod imp {
use crate::model::Song;
use crate::mpv; use crate::mpv;
use crate::PlaybinSong;
use adw::prelude::*; use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::subclass::InitializingObject; use glib::subclass::InitializingObject;
@ -26,7 +26,7 @@ mod imp {
#[property(get, set, nullable)] #[property(get, set, nullable)]
playing_cover_art: RefCell<Option<gdk::Paintable>>, playing_cover_art: RefCell<Option<gdk::Paintable>>,
#[property(type = Option<PlaybinSong>, get = Self::song, nullable)] #[property(type = Option<Song>, get = Self::song, nullable)]
_song: (), _song: (),
pub(super) setup: crate::ui::Setup, pub(super) setup: crate::ui::Setup,
@ -95,7 +95,7 @@ mod imp {
setup: Default::default(), setup: Default::default(),
api: Default::default(), api: Default::default(),
mpv, mpv,
playlist_model: gio::ListStore::new::<PlaybinSong>(), playlist_model: gio::ListStore::new::<Song>(),
_volume: (), _volume: (),
_mute: (), _mute: (),
@ -398,7 +398,7 @@ mod imp {
Rc::clone(api.as_ref().unwrap()) Rc::clone(api.as_ref().unwrap())
}; };
for song in api.get_random_songs(10).await.unwrap().into_iter() { for song in api.get_random_songs(10).await.unwrap().into_iter() {
let song = PlaybinSong::from_child(&api, &song); let song = Song::from_child(&api, &song);
self.mpv self.mpv
.command(["loadfile", &song.stream_url(), "append-play"]) .command(["loadfile", &song.stream_url(), "append-play"])
.unwrap(); .unwrap();
@ -474,7 +474,7 @@ mod imp {
{ {
let left = duration.map(|f| f as i64); let left = duration.map(|f| f as i64);
let right = self.song().as_ref().map(crate::PlaybinSong::duration); let right = self.song().as_ref().map(crate::model::Song::duration);
if left != right { if left != right {
event!( event!(
Level::WARN, Level::WARN,
@ -498,11 +498,11 @@ mod imp {
.unwrap() .unwrap()
} }
fn song(&self) -> Option<PlaybinSong> { fn song(&self) -> Option<Song> {
if self.obj().playlist_pos() < 0 { if self.obj().playlist_pos() < 0 {
None None
} else { } else {
let song: PlaybinSong = self let song: Song = self
.obj() .obj()
.playlist_model() .playlist_model()
.item(self.obj().playlist_pos() as u32) .item(self.obj().playlist_pos() as u32)
@ -627,4 +627,39 @@ impl Window {
} }
} }
} }
pub fn playlist_move(&self, from: u32, to: u32) {
// NOTE: for mpv, to refers to the "gap" right before i
// so playlist-move i 0 makes a track the first
// playlist-move i 1 makes a track the second
// and playlist-move i playlist-count makes a track the last
// (so if to is the position of another track, from is left behind to)
self.imp()
.mpv
.command(["playlist-move", &from.to_string(), &to.to_string()])
.unwrap();
if from < to {
// F1234T -> 1234FT
let mut spliced = Vec::with_capacity((to - from) as usize);
for i in from + 1..to {
spliced.push(self.playlist_model().item(i).unwrap());
}
spliced.push(self.playlist_model().item(from).unwrap());
self.playlist_model()
.splice(from, spliced.len() as u32, &spliced);
} else if to < from {
// T1234F -> FT1234
let mut spliced = Vec::with_capacity((from - to + 1) as usize);
spliced.push(self.playlist_model().item(from).unwrap());
spliced.push(self.playlist_model().item(to).unwrap());
for i in to + 1..from {
spliced.push(self.playlist_model().item(i).unwrap());
}
self.playlist_model()
.splice(to, spliced.len() as u32, &spliced);
}
}
} }