Compare commits
No commits in common. "trunk" and "int" have entirely different histories.
50 changed files with 810 additions and 3452 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
target/
|
|
1034
Cargo.lock
generated
1034
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
16
Cargo.toml
16
Cargo.toml
|
@ -1,16 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "audrey"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
adw = { version = "0.7.0", package = "libadwaita", features = ["v1_6"] }
|
|
||||||
gettext-rs = { version = "0.7.2", features = ["gettext-system"] }
|
|
||||||
gtk = { version = "0.9.2", package = "gtk4", features = ["v4_16"] }
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
bindgen = "0.70.1"
|
|
||||||
glib-build-tools = "0.20.0"
|
|
||||||
|
|
||||||
[profile.dev]
|
|
||||||
split-debuginfo = "unpacked"
|
|
21
build.rs
21
build.rs
|
@ -1,21 +0,0 @@
|
||||||
fn main() {
|
|
||||||
let meson_build_root =
|
|
||||||
std::env::var("MESON_BUILD_ROOT").expect("build this through meson please!!");
|
|
||||||
glib_build_tools::compile_resources(
|
|
||||||
&["resources", &format!("{meson_build_root}/resources")],
|
|
||||||
"resources/audrey.gresource.xml",
|
|
||||||
"audrey.gresource",
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: consider using meson to pass include paths
|
|
||||||
println!("cargo:rustc-link-lib=mpv");
|
|
||||||
let bindings = bindgen::Builder::default()
|
|
||||||
.header("src/mpv/wrapper.h")
|
|
||||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
|
||||||
.generate()
|
|
||||||
.expect("could not generate bindings for mpv");
|
|
||||||
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
|
||||||
bindings
|
|
||||||
.write_to_file(out_path.join("mpv_ffi.rs"))
|
|
||||||
.expect("could not write mpv bindings");
|
|
||||||
}
|
|
49
meson.build
49
meson.build
|
@ -8,10 +8,10 @@ project(
|
||||||
|
|
||||||
i18n = import('i18n')
|
i18n = import('i18n')
|
||||||
gnome = import('gnome')
|
gnome = import('gnome')
|
||||||
fs = import('fs')
|
|
||||||
cc = meson.get_compiler('c')
|
cc = meson.get_compiler('c')
|
||||||
valac = meson.get_compiler('vala')
|
valac = meson.get_compiler('vala')
|
||||||
|
|
||||||
|
|
||||||
srcdir = meson.project_source_root() / 'src'
|
srcdir = meson.project_source_root() / 'src'
|
||||||
|
|
||||||
config_h = configuration_data()
|
config_h = configuration_data()
|
||||||
|
@ -38,48 +38,13 @@ add_project_arguments(
|
||||||
language: 'c',
|
language: 'c',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
subdir('data')
|
subdir('data')
|
||||||
subdir('po')
|
|
||||||
subdir('resources')
|
|
||||||
subdir('src')
|
subdir('src')
|
||||||
|
subdir('po')
|
||||||
|
|
||||||
if get_option('buildtype') == 'debug'
|
gnome.post_install(
|
||||||
rust_args = []
|
glib_compile_schemas: true,
|
||||||
rust_target = 'debug'
|
gtk_update_icon_cache: true,
|
||||||
else
|
update_desktop_database: true,
|
||||||
rust_args = ['--release']
|
|
||||||
rust_target = 'release'
|
|
||||||
endif
|
|
||||||
|
|
||||||
cargo = find_program('cargo')
|
|
||||||
cargo_env = environment()
|
|
||||||
cargo_env.set('MESON_BUILD_ROOT', meson.project_build_root())
|
|
||||||
cargo_env.append(
|
|
||||||
'RUSTFLAGS',
|
|
||||||
'-L ' + fs.parent(audrey_c.full_path()),
|
|
||||||
separator: ' ',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
custom_target(
|
|
||||||
'cargo-build',
|
|
||||||
build_by_default: true,
|
|
||||||
build_always_stale: true,
|
|
||||||
depends: [audrey_c, blueprints],
|
|
||||||
input: config_rs,
|
|
||||||
console: true,
|
|
||||||
# means nothing (always stale and uncopied), we can't cp since env: drops the /bin/sh wrapper
|
|
||||||
output: meson.project_name(),
|
|
||||||
command: [
|
|
||||||
cargo,
|
|
||||||
'build',
|
|
||||||
rust_args,
|
|
||||||
'--manifest-path',
|
|
||||||
meson.project_source_root() / 'Cargo.toml',
|
|
||||||
'--target-dir',
|
|
||||||
meson.project_build_root() / 'cargo-target',
|
|
||||||
],
|
|
||||||
env: cargo_env,
|
|
||||||
)
|
|
||||||
|
|
||||||
run_target('clippy', command: [cargo, 'clippy'], env: cargo_env)
|
|
||||||
run_target('cargo-doc', command: [cargo, 'doc'], env: cargo_env)
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<gresources>
|
|
||||||
<gresource prefix="/eu/callcc/audrey">
|
|
||||||
<file>style.css</file>
|
|
||||||
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks">play_queue.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks">play_queue_song.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks">playbar.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks">setup.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks">window.ui</file>
|
|
||||||
</gresource>
|
|
||||||
</gresources>
|
|
|
@ -1,31 +0,0 @@
|
||||||
blueprints = custom_target(
|
|
||||||
'blueprints',
|
|
||||||
input: files(
|
|
||||||
'play_queue.blp',
|
|
||||||
'play_queue_song.blp',
|
|
||||||
'playbar.blp',
|
|
||||||
'setup.blp',
|
|
||||||
'window.blp',
|
|
||||||
),
|
|
||||||
output: [
|
|
||||||
'play_queue.ui',
|
|
||||||
'play_queue_song.ui',
|
|
||||||
'playbar.ui',
|
|
||||||
'setup.ui',
|
|
||||||
'window.ui',
|
|
||||||
],
|
|
||||||
command: [
|
|
||||||
find_program('blueprint-compiler'),
|
|
||||||
'batch-compile',
|
|
||||||
'@OUTDIR@',
|
|
||||||
'@CURRENT_SOURCE_DIR@',
|
|
||||||
'@INPUT@',
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
removeme = gnome.compile_resources(
|
|
||||||
'audrey-resources',
|
|
||||||
'audrey.gresource.xml',
|
|
||||||
c_name: 'audrey',
|
|
||||||
dependencies: blueprints,
|
|
||||||
)
|
|
|
@ -1,46 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
template $AudreyUiPlayQueue: Adw.Bin {
|
|
||||||
name: "play-queue";
|
|
||||||
|
|
||||||
child: Stack {
|
|
||||||
visible-child-name: bind $visible_child_name (template.playbin as <$AudreyPlaybin>.play-queue-length) as <string>;
|
|
||||||
|
|
||||||
StackPage {
|
|
||||||
name: "empty";
|
|
||||||
|
|
||||||
child: Adw.StatusPage {
|
|
||||||
icon-name: "list-remove-all";
|
|
||||||
title: "No songs queued";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
StackPage {
|
|
||||||
name: "not-empty";
|
|
||||||
|
|
||||||
child: ScrolledWindow {
|
|
||||||
hexpand: true;
|
|
||||||
vscrollbar-policy: always;
|
|
||||||
hscrollbar-policy: never;
|
|
||||||
|
|
||||||
ListView view {
|
|
||||||
show-separators: true;
|
|
||||||
single-click-activate: true;
|
|
||||||
|
|
||||||
activate => $on_row_activated ();
|
|
||||||
|
|
||||||
model: NoSelection {
|
|
||||||
model: bind template.playbin as <$AudreyPlaybin>.play_queue;
|
|
||||||
};
|
|
||||||
|
|
||||||
factory: SignalListItemFactory {
|
|
||||||
setup => $on_song_list_setup ();
|
|
||||||
bind => $on_song_list_bind ();
|
|
||||||
unbind => $on_song_list_unbind ();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
|
|
||||||
template $AudreyUiPlayQueueSong: Box {
|
|
||||||
height-request: 48;
|
|
||||||
spacing: 12;
|
|
||||||
margin-start: 6;
|
|
||||||
margin-end: 6;
|
|
||||||
|
|
||||||
Box {
|
|
||||||
width-request: 36;
|
|
||||||
focusable: false;
|
|
||||||
homogeneous: true;
|
|
||||||
|
|
||||||
Image {
|
|
||||||
visible: bind template.current;
|
|
||||||
focusable: false;
|
|
||||||
halign: end;
|
|
||||||
icon-size: normal;
|
|
||||||
icon-name: "media-playback-start";
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
visible: bind template.current inverted;
|
|
||||||
focusable: false;
|
|
||||||
halign: end;
|
|
||||||
justify: right;
|
|
||||||
styles [ "dim-label", "numeric" ]
|
|
||||||
|
|
||||||
label: bind template.displayed_position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
|
||||||
visible: bind template.show_cover;
|
|
||||||
margin-top: 1;
|
|
||||||
margin-bottom: 1;
|
|
||||||
pixel-size: 50;
|
|
||||||
paintable: bind template.song as <$AudreyPlaybinSong>.thumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
Box title_box {
|
|
||||||
styles [ "header"]
|
|
||||||
focusable: false;
|
|
||||||
hexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Box {
|
|
||||||
styles [ "title" ]
|
|
||||||
orientation: vertical;
|
|
||||||
|
|
||||||
Label {
|
|
||||||
styles [ "title" ]
|
|
||||||
focusable: false;
|
|
||||||
xalign: 0;
|
|
||||||
halign: start;
|
|
||||||
hexpand: true;
|
|
||||||
ellipsize: end;
|
|
||||||
max-width-chars: 90;
|
|
||||||
justify: fill;
|
|
||||||
|
|
||||||
label: bind template.song as <$AudreyPlaybinSong>.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
styles [ "subtitle" ]
|
|
||||||
focusable: false;
|
|
||||||
xalign: 0;
|
|
||||||
halign: start;
|
|
||||||
hexpand: true;
|
|
||||||
ellipsize: end;
|
|
||||||
max-width-chars: 90;
|
|
||||||
justify: fill;
|
|
||||||
|
|
||||||
label: bind template.song as <$AudreyPlaybinSong>.artist;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
focusable: false;
|
|
||||||
halign: end;
|
|
||||||
hexpand: true;
|
|
||||||
single-line-mode: true;
|
|
||||||
styles [ "numeric", "dim-label" ]
|
|
||||||
|
|
||||||
label: bind $format_duration (template.song as <$AudreyPlaybinSong>.duration) as <string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
focusable: true;
|
|
||||||
// TODO icon-name: bind $star_button_icon_name (template.song as <$AudreyPlaybinSong>.starred) as <string>;
|
|
||||||
icon-name: bind $star_button_icon_name () as <string>;
|
|
||||||
styles [ "flat" ]
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuButton {
|
|
||||||
//visible: false;
|
|
||||||
focusable: true;
|
|
||||||
icon-name: "view-more";
|
|
||||||
styles [ "flat" ]
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
popover: PopoverMenu {
|
|
||||||
menu-model: song-menu;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
DragSource {
|
|
||||||
actions: move;
|
|
||||||
propagation-phase: capture;
|
|
||||||
|
|
||||||
prepare => $on_drag_prepare ();
|
|
||||||
drag-begin => $on_drag_begin ();
|
|
||||||
}
|
|
||||||
|
|
||||||
DropTarget {
|
|
||||||
actions: move;
|
|
||||||
formats: "AudreyUiPlayQueueSong";
|
|
||||||
preload: true;
|
|
||||||
|
|
||||||
drop => $on_drop ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
menu song-menu {
|
|
||||||
item ("View song", "song.view")
|
|
||||||
item ("View artist", "song.view-artist")
|
|
||||||
item ("View album", "song.view-album")
|
|
||||||
item ("Share", "song.share")
|
|
||||||
item ("Remove", "song.remove")
|
|
||||||
}
|
|
|
@ -1,172 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
template $AudreyUiPlaybar: Adw.Bin {
|
|
||||||
child: CenterBox {
|
|
||||||
hexpand: true;
|
|
||||||
styles [
|
|
||||||
"toolbar",
|
|
||||||
]
|
|
||||||
|
|
||||||
[start]
|
|
||||||
Box {
|
|
||||||
AspectFrame {
|
|
||||||
visible: false; // FIXME annoying annoying annoying annoying
|
|
||||||
//visible: bind template.show_cover_art;
|
|
||||||
|
|
||||||
vexpand: true;
|
|
||||||
ratio: 1.0;
|
|
||||||
obey-child: false;
|
|
||||||
|
|
||||||
child: Picture {
|
|
||||||
content-fit: scale_down;
|
|
||||||
paintable: bind template.playing_cover_art;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Box {
|
|
||||||
margin-start: 6;
|
|
||||||
orientation: vertical;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Label {
|
|
||||||
styles [ "heading" ]
|
|
||||||
xalign: 0;
|
|
||||||
halign: start;
|
|
||||||
label: bind $song_title (template.song) as <string>;
|
|
||||||
ellipsize: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
styles [ "caption" ]
|
|
||||||
xalign: 0;
|
|
||||||
label: bind $song_artist (template.song) as <string>;
|
|
||||||
ellipsize: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
styles [ "caption" ]
|
|
||||||
xalign: 0;
|
|
||||||
label: bind $song_album (template.song) as <string>;
|
|
||||||
ellipsize: end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[center]
|
|
||||||
Box {
|
|
||||||
orientation: vertical;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
CenterBox {
|
|
||||||
[start]
|
|
||||||
Label play_position_label {
|
|
||||||
styles [
|
|
||||||
"caption",
|
|
||||||
"numeric",
|
|
||||||
]
|
|
||||||
|
|
||||||
label: bind $format_timestamp (template.playbin as <$AudreyPlaybin>.position) as <string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
[center]
|
|
||||||
Scale play_position {
|
|
||||||
name: "seek-scale";
|
|
||||||
orientation: horizontal;
|
|
||||||
width-request: 400;
|
|
||||||
sensitive: bind $playbin_active (template.playbin as <$AudreyPlaybin>.state as <$AudreyPlaybinState>) as <bool>;
|
|
||||||
|
|
||||||
adjustment: Adjustment {
|
|
||||||
lower: 0;
|
|
||||||
value: bind template.playbin as <$AudreyPlaybin>.position;
|
|
||||||
upper: bind template.playbin as <$AudreyPlaybin>.duration;
|
|
||||||
};
|
|
||||||
|
|
||||||
change-value => $on_play_position_seek (template);
|
|
||||||
}
|
|
||||||
|
|
||||||
[end]
|
|
||||||
Label play_duration {
|
|
||||||
styles [
|
|
||||||
"caption",
|
|
||||||
"numeric",
|
|
||||||
]
|
|
||||||
|
|
||||||
label: bind $format_timestamp (template.playbin as <$AudreyPlaybin>.duration) as <string>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box {
|
|
||||||
halign: center;
|
|
||||||
orientation: horizontal;
|
|
||||||
|
|
||||||
Button {
|
|
||||||
icon-name: "media-skip-backward";
|
|
||||||
valign: center;
|
|
||||||
sensitive: bind $playbin_active (template.playbin as <$AudreyPlaybin>.state as <$AudreyPlaybinState>) as <bool>;
|
|
||||||
|
|
||||||
clicked => $on_skip_backward_clicked (template);
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
icon-name: "media-seek-backward";
|
|
||||||
valign: center;
|
|
||||||
sensitive: bind $playbin_active (template.playbin as <$AudreyPlaybin>.state as <$AudreyPlaybinState>) as <bool>;
|
|
||||||
|
|
||||||
clicked => $seek_backward (template);
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
icon-name: bind $play_pause_icon_name (template.playbin as <$AudreyPlaybin>.state as <$AudreyPlaybinState>) as <string>;
|
|
||||||
valign: center;
|
|
||||||
sensitive: bind $can_press_play (template.playbin as <$AudreyPlaybin>.state as <$AudreyPlaybinState>, template.playbin as <$AudreyPlaybin>.play-queue-length) as <bool>;
|
|
||||||
|
|
||||||
clicked => $on_play_pause_clicked (template);
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
icon-name: "media-seek-forward";
|
|
||||||
valign: center;
|
|
||||||
sensitive: bind $playbin_active (template.playbin as <$AudreyPlaybin>.state as <$AudreyPlaybinState>) as <bool>;
|
|
||||||
|
|
||||||
clicked => $seek_forward (template);
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
icon-name: "media-skip-forward";
|
|
||||||
valign: center;
|
|
||||||
sensitive: bind $playbin_active (template.playbin as <$AudreyPlaybin>.state as <$AudreyPlaybinState>) as <bool>;
|
|
||||||
|
|
||||||
clicked => $on_skip_forward_clicked (template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[end]
|
|
||||||
Box {
|
|
||||||
Button {
|
|
||||||
icon-name: "non-starred";
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
icon-name: bind $mute_button_icon_name (template.playbin as <$AudreyPlaybin>.mute) as <string>;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
clicked => $on_mute_toggle (template);
|
|
||||||
}
|
|
||||||
|
|
||||||
Scale {
|
|
||||||
name: "volume-scale";
|
|
||||||
orientation: horizontal;
|
|
||||||
width-request: 130;
|
|
||||||
|
|
||||||
adjustment: Adjustment {
|
|
||||||
lower: 0;
|
|
||||||
value: bind template.volume bidirectional;
|
|
||||||
upper: 100;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
template $AudreyUiWindow: Adw.ApplicationWindow {
|
|
||||||
title: _("audrey");
|
|
||||||
default-width: 800;
|
|
||||||
default-height: 600;
|
|
||||||
|
|
||||||
Adw.ToolbarView {
|
|
||||||
top-bar-style: raised;
|
|
||||||
bottom-bar-style: raised;
|
|
||||||
|
|
||||||
[top]
|
|
||||||
Adw.HeaderBar {
|
|
||||||
[start]
|
|
||||||
Button {
|
|
||||||
icon-name: "media-playlist-shuffle";
|
|
||||||
sensitive: bind template.can_click_shuffle_all;
|
|
||||||
clicked => $shuffle_all ();
|
|
||||||
}
|
|
||||||
|
|
||||||
title-widget: Adw.ViewSwitcher {
|
|
||||||
stack: stack;
|
|
||||||
policy: wide;
|
|
||||||
};
|
|
||||||
|
|
||||||
[end]
|
|
||||||
Button {
|
|
||||||
icon-name: "applications-system";
|
|
||||||
clicked => $show_setup_dialog ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content: Adw.ViewStack stack {
|
|
||||||
vexpand: true;
|
|
||||||
Adw.ViewStackPage {
|
|
||||||
icon-name: "media-optical-cd";
|
|
||||||
title: _("Albums");
|
|
||||||
|
|
||||||
child: Adw.NavigationView {
|
|
||||||
Adw.NavigationPage {
|
|
||||||
title: _("Albums");
|
|
||||||
vexpand: true;
|
|
||||||
|
|
||||||
child: Adw.ToolbarView {
|
|
||||||
[top]
|
|
||||||
CenterBox {
|
|
||||||
styles [ "toolbar" ]
|
|
||||||
|
|
||||||
[start]
|
|
||||||
DropDown {
|
|
||||||
selected: 0;
|
|
||||||
model: StringList {
|
|
||||||
strings [
|
|
||||||
_("Random"),
|
|
||||||
_("Recently added"),
|
|
||||||
_("Most played"),
|
|
||||||
_("Recently played"),
|
|
||||||
_("Starred"),
|
|
||||||
_("Name"),
|
|
||||||
_("Artist"),
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[center]
|
|
||||||
SearchEntry {
|
|
||||||
placeholder-text: _("Search...");
|
|
||||||
}
|
|
||||||
|
|
||||||
[end]
|
|
||||||
Box {
|
|
||||||
Button {
|
|
||||||
icon-name: "view-refresh";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrolledWindow {
|
|
||||||
vexpand: true;
|
|
||||||
GridView {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ViewStackPage {
|
|
||||||
icon-name: "media-playback-start";
|
|
||||||
title: _("Play queue");
|
|
||||||
name: "play-queue";
|
|
||||||
|
|
||||||
child: Box {
|
|
||||||
name: "play-queue-page";
|
|
||||||
homogeneous: true;
|
|
||||||
|
|
||||||
Adw.Clamp {
|
|
||||||
halign: center;
|
|
||||||
margin-top: 24;
|
|
||||||
margin-bottom: 24;
|
|
||||||
margin-start: 24;
|
|
||||||
margin-end: 24;
|
|
||||||
|
|
||||||
Picture {
|
|
||||||
valign: center;
|
|
||||||
styles [ "frame" ]
|
|
||||||
halign: center;
|
|
||||||
paintable: bind template.playing_cover_art;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$AudreyUiPlayQueue play_queue {
|
|
||||||
hexpand: true;
|
|
||||||
halign: fill;
|
|
||||||
|
|
||||||
margin-top: 48;
|
|
||||||
margin-bottom: 48;
|
|
||||||
margin-start: 24;
|
|
||||||
margin-end: 24;
|
|
||||||
|
|
||||||
styles [ "frame" ]
|
|
||||||
playbin: bind template.playbin;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
[bottom]
|
|
||||||
$AudreyUiPlaybar playbar {
|
|
||||||
song: bind template.song;
|
|
||||||
playbin: bind template.playbin;
|
|
||||||
playing_cover_art: bind template.playing_cover_art;
|
|
||||||
show_cover_art: bind $show_playbar_cover_art (stack.visible-child-name) as <bool>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
mod imp {
|
|
||||||
use crate::ui;
|
|
||||||
use adw::prelude::*;
|
|
||||||
use adw::subclass::prelude::*;
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Application;
|
|
||||||
|
|
||||||
#[glib::object_subclass]
|
|
||||||
impl ObjectSubclass for Application {
|
|
||||||
const NAME: &'static str = "AudreyApplication";
|
|
||||||
type Type = super::Application;
|
|
||||||
type ParentType = adw::Application;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectImpl for Application {}
|
|
||||||
|
|
||||||
impl ApplicationImpl for Application {
|
|
||||||
fn activate(&self) {
|
|
||||||
self.parent_activate();
|
|
||||||
match self.obj().active_window() {
|
|
||||||
None => ui::Window::new(self.obj().as_ref()).present(),
|
|
||||||
Some(win) => win.present(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GtkApplicationImpl for Application {}
|
|
||||||
|
|
||||||
impl AdwApplicationImpl for Application {}
|
|
||||||
}
|
|
||||||
|
|
||||||
use gtk::{gio, glib};
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct Application(ObjectSubclass<imp::Application>)
|
|
||||||
@extends adw::Application, gtk::Application, gio::Application,
|
|
||||||
@implements gio::ActionGroup, gio::ActionMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Application {
|
|
||||||
fn default() -> Self {
|
|
||||||
glib::Object::builder::<Application>()
|
|
||||||
.property("application-id", "eu.callcc.audrey")
|
|
||||||
.property("flags", gio::ApplicationFlags::default())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Application {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -41,8 +41,4 @@ public class Audrey.Application : Adw.Application {
|
||||||
private void on_preferences_action () {
|
private void on_preferences_action () {
|
||||||
message ("app.preferences action activated");
|
message ("app.preferences action activated");
|
||||||
}
|
}
|
||||||
|
|
||||||
~Application () {
|
|
||||||
debug ("destroying application");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
11
src/audrey.gresource.xml
Normal file
11
src/audrey.gresource.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gresources>
|
||||||
|
<gresource prefix="/eu/callcc/audrey">
|
||||||
|
<file>style.css</file>
|
||||||
|
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">ui/play_queue.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">ui/play_queue_song.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">ui/setup.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">ui/window.ui</file>
|
||||||
|
</gresource>
|
||||||
|
</gresources>
|
41
src/main.rs
41
src/main.rs
|
@ -1,41 +0,0 @@
|
||||||
mod application;
|
|
||||||
pub use application::Application;
|
|
||||||
|
|
||||||
mod meson_config;
|
|
||||||
pub mod ui;
|
|
||||||
|
|
||||||
pub mod mpris;
|
|
||||||
pub use mpris::Mpris;
|
|
||||||
|
|
||||||
pub mod playbin;
|
|
||||||
pub use playbin::Playbin;
|
|
||||||
|
|
||||||
pub mod subsonic;
|
|
||||||
|
|
||||||
use gettextrs::{bind_textdomain_codeset, bindtextdomain, setlocale, textdomain, LocaleCategory};
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk::{gio, glib};
|
|
||||||
|
|
||||||
pub mod mpv;
|
|
||||||
|
|
||||||
#[link(name = "audrey")]
|
|
||||||
#[link(name = "gcrypt")]
|
|
||||||
#[link(name = "json-glib-1.0")]
|
|
||||||
#[link(name = "secret-1")]
|
|
||||||
#[link(name = "soup-3.0")]
|
|
||||||
extern "C" {}
|
|
||||||
|
|
||||||
fn main() -> glib::ExitCode {
|
|
||||||
gio::resources_register_include!("audrey.gresource").expect("could not register resources");
|
|
||||||
|
|
||||||
ui::Playbar::ensure_type();
|
|
||||||
|
|
||||||
gtk::disable_setlocale();
|
|
||||||
bindtextdomain("audrey", meson_config::LOCALEDIR).expect("failed to bind text domain");
|
|
||||||
bind_textdomain_codeset("audrey", "UTF-8").expect("failed to bind textdomaincodeset");
|
|
||||||
textdomain("audrey").expect("unable to switch to text domain");
|
|
||||||
setlocale(LocaleCategory::LcAll, "");
|
|
||||||
setlocale(LocaleCategory::LcNumeric, "C.UTF-8");
|
|
||||||
let app = Application::new();
|
|
||||||
app.run()
|
|
||||||
}
|
|
|
@ -1,16 +1,17 @@
|
||||||
audrey_sources = [
|
audrey_sources = [
|
||||||
'application.vala',
|
'application.vala',
|
||||||
'globalconf.vala',
|
'globalconf.vala',
|
||||||
|
'main.vala',
|
||||||
'mpris.vala',
|
'mpris.vala',
|
||||||
'playbin.vala',
|
'playbin.vala',
|
||||||
'subsonic.vala',
|
'subsonic.vala',
|
||||||
'ui/play_queue.vala',
|
'ui/play_queue.vala',
|
||||||
'ui/playbar.vapi',
|
|
||||||
'ui/setup.vala',
|
'ui/setup.vala',
|
||||||
'ui/window.vala',
|
'ui/window.vala',
|
||||||
]
|
]
|
||||||
|
|
||||||
audrey_deps = [
|
audrey_deps = [
|
||||||
|
config_dep,
|
||||||
dependency('gtk4', version: '>= 4.16'),
|
dependency('gtk4', version: '>= 4.16'),
|
||||||
dependency('json-glib-1.0', version: '>= 1.10'),
|
dependency('json-glib-1.0', version: '>= 1.10'),
|
||||||
dependency('libadwaita-1', version: '>= 1.6'),
|
dependency('libadwaita-1', version: '>= 1.6'),
|
||||||
|
@ -20,26 +21,42 @@ audrey_deps = [
|
||||||
dependency('mpv', version: '>= 2.3'),
|
dependency('mpv', version: '>= 2.3'),
|
||||||
]
|
]
|
||||||
|
|
||||||
audrey_sources += removeme
|
subdir('ui')
|
||||||
|
|
||||||
config_rs = configure_file(
|
audrey_sources += gnome.compile_resources(
|
||||||
output: 'meson_config.rs',
|
'audrey-resources',
|
||||||
input: 'meson_config.rs.in',
|
'audrey.gresource.xml',
|
||||||
configuration: config_h,
|
c_name: 'audrey',
|
||||||
|
dependencies: blueprints,
|
||||||
|
# this is a huge hack, as by default only src/ui to the 'blueprints' dep
|
||||||
|
# and ../src is passed, which means the ui/xyz.ui prefix will not be found
|
||||||
|
# anywhere, as src/ui already has the ui in the path.
|
||||||
|
# the reason we have to put blp in its own ui folder, is because:
|
||||||
|
# - blueprint-compiler batch-compile outputs in the same directory structure,
|
||||||
|
# i.e. if you pass ui/x.blp outdir foo, it will create foo/ui/x.ui,
|
||||||
|
# - meson doesn't support output:[] with dir/ prefix
|
||||||
|
# .. which means, all the files are always dirty; meson will try find 'src/x.ui'
|
||||||
|
# as an output for dirty tracking, but blp is outputting src/ui/x.ui
|
||||||
|
# because of the directory structure.
|
||||||
|
# using manual blueprint-compiler invocations to put the output in the
|
||||||
|
# 'correct' tracked place doesn't work either,
|
||||||
|
# since then it will be in this dir and not in ui/ and so all the ui/ prefixes
|
||||||
|
# in gresources won't find it, and we can't put it in a ui/ output from the
|
||||||
|
# current dir due to the above and meson not letting you.
|
||||||
|
#so since we use ui/ prefix, we need a meson.build in ui/ and then this hack
|
||||||
|
# to ensure compile sees the ui/ prefix starting from this src/ dir still,
|
||||||
|
# despite the dependency normally doing this for you
|
||||||
|
# note that the 'documented' way to use blp is with outdir:[.] which is
|
||||||
|
# fundamentally broken as then nothing is tracked except the dirs existence, which
|
||||||
|
# is racy as the dependency is satistfied before anything is compiled
|
||||||
|
extra_args: ['--sourcedir', 'src'],
|
||||||
)
|
)
|
||||||
|
|
||||||
audrey_c = static_library(
|
executable(
|
||||||
'audrey',
|
'audrey',
|
||||||
audrey_sources,
|
audrey_sources,
|
||||||
dependencies: audrey_deps,
|
dependencies: audrey_deps,
|
||||||
include_directories: config_inc,
|
include_directories: config_inc,
|
||||||
install: true,
|
install: true,
|
||||||
vala_args: [
|
vala_args: ['--vapidir', meson.current_source_dir() / 'vapi'],
|
||||||
'--vapidir',
|
|
||||||
meson.current_source_dir() / 'vapi',
|
|
||||||
'--gresources',
|
|
||||||
meson.project_source_root() / 'resources/audrey.gresource.xml',
|
|
||||||
],
|
|
||||||
vala_header: 'audrey.h',
|
|
||||||
vala_gir: 'audrey-0.gir',
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
include!(concat!(env!("MESON_BUILD_ROOT"), "/src/meson_config.rs"));
|
|
|
@ -1 +0,0 @@
|
||||||
pub static LOCALEDIR: &str = @LOCALEDIR@;
|
|
30
src/mpris.rs
30
src/mpris.rs
|
@ -1,30 +0,0 @@
|
||||||
mod player;
|
|
||||||
pub use player::Player;
|
|
||||||
|
|
||||||
mod ffi {
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreyMpris {
|
|
||||||
parent_instance: glib::gobject_ffi::GObject,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreyMprisClass {
|
|
||||||
parent_class: glib::gobject_ffi::GObjectClass,
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
pub fn audrey_mpris_get_type() -> glib::ffi::GType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct Mpris(Object<ffi::AudreyMpris, ffi::AudreyMprisClass>);
|
|
||||||
|
|
||||||
match fn {
|
|
||||||
type_ => || ffi::audrey_mpris_get_type(),
|
|
||||||
}
|
|
||||||
}
|
|
217
src/mpris.vala
217
src/mpris.vala
|
@ -1,5 +1,5 @@
|
||||||
[DBus (name = "org.mpris.MediaPlayer2")]
|
[DBus (name = "org.mpris.MediaPlayer2")]
|
||||||
class Audrey.Mpris : Object {
|
class Mpris : Object {
|
||||||
internal signal void on_raise ();
|
internal signal void on_raise ();
|
||||||
internal signal void on_quit ();
|
internal signal void on_quit ();
|
||||||
|
|
||||||
|
@ -20,19 +20,10 @@ class Audrey.Mpris : Object {
|
||||||
public string desktop_entry { owned get { return "eu.callcc.audrey"; } }
|
public string desktop_entry { owned get { return "eu.callcc.audrey"; } }
|
||||||
public string[] supported_uri_schemes { owned get { return {}; } }
|
public string[] supported_uri_schemes { owned get { return {}; } }
|
||||||
public string[] supported_mime_types { owned get { return {}; } }
|
public string[] supported_mime_types { owned get { return {}; } }
|
||||||
|
|
||||||
internal Mpris (Ui.Window window) {
|
|
||||||
this.on_raise.connect (() => window.present ());
|
|
||||||
this.on_quit.connect (() => window.close ());
|
|
||||||
}
|
|
||||||
|
|
||||||
~Mpris () {
|
|
||||||
debug ("destroying mpris");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DBus (name = "org.mpris.MediaPlayer2.Player")]
|
[DBus (name = "org.mpris.MediaPlayer2.Player")]
|
||||||
class Audrey.MprisPlayer : Object {
|
class MprisPlayer : Object {
|
||||||
internal signal void on_next ();
|
internal signal void on_next ();
|
||||||
internal signal void on_previous ();
|
internal signal void on_previous ();
|
||||||
internal signal void on_pause ();
|
internal signal void on_pause ();
|
||||||
|
@ -40,7 +31,7 @@ class Audrey.MprisPlayer : Object {
|
||||||
internal signal void on_stop ();
|
internal signal void on_stop ();
|
||||||
internal signal void on_play ();
|
internal signal void on_play ();
|
||||||
internal signal void on_seek (int64 offset);
|
internal signal void on_seek (int64 offset);
|
||||||
internal signal void on_set_position (ObjectPath track_id, int64 position);
|
internal signal void on_set_position (string track_id, int64 position);
|
||||||
|
|
||||||
public void next () throws Error { this.on_next (); }
|
public void next () throws Error { this.on_next (); }
|
||||||
public void previous () throws Error { this.on_previous (); }
|
public void previous () throws Error { this.on_previous (); }
|
||||||
|
@ -49,206 +40,26 @@ class Audrey.MprisPlayer : Object {
|
||||||
public void stop () throws Error { this.on_stop (); }
|
public void stop () throws Error { this.on_stop (); }
|
||||||
public void play () throws Error { this.on_play (); }
|
public void play () throws Error { this.on_play (); }
|
||||||
public void seek (int64 offset) throws Error { this.on_seek (offset); }
|
public void seek (int64 offset) throws Error { this.on_seek (offset); }
|
||||||
public void set_position (ObjectPath track_id, int64 position) throws Error { this.on_set_position (track_id, position); }
|
public void set_position (string track_id, int64 position) throws Error { this.on_set_position (track_id, position); }
|
||||||
public void open_uri (string uri) throws Error { assert (false); }
|
public void open_uri (string uri) throws Error { assert (false); }
|
||||||
|
|
||||||
public signal void seeked (int64 position);
|
public signal void seeked (int64 position);
|
||||||
|
|
||||||
public string playback_status { owned get; internal set; default = "Stopped"; }
|
public string playback_status { owned get; internal set; default = "Stopped"; }
|
||||||
public string loop_status { owned get; /*set;*/ default = "None"; }
|
public string loop_status { owned get; set; default = "None"; }
|
||||||
public double rate { get; /*set*/ default = 1.0; }
|
public double rate { get; set; default = 1.0; }
|
||||||
public bool shuffle { get; /*set*/ default = false; }
|
public bool shuffle { get; set; default = false; }
|
||||||
public HashTable<string, Variant> metadata { owned get; private set; default = new HashTable<string, Variant> (null, null); }
|
public HashTable<string, Variant> metadata_map { owned get; default = new HashTable<string,Variant>(null, null); }
|
||||||
public double volume { get; set; default = 1.0; }
|
public double volume { get; set; default = 1.0; }
|
||||||
[CCode (notify = false)]
|
[CCode (notify = false)]
|
||||||
public int64 position { get; default = 0; }
|
public int64 position { get; default = 0; }
|
||||||
public double minimum_rate { get { return 1.0; } }
|
public double minimum_rate { get { return 1.0; } }
|
||||||
public double maximum_rate { get { return 1.0; } }
|
public double maximum_rate { get { return 1.0; } }
|
||||||
public bool can_go_next { get; private set; default = false; }
|
public bool can_go_next { get; default = false; }
|
||||||
public bool can_go_previous { get; private set; default = false; }
|
public bool can_go_previous { get; default = false; }
|
||||||
public bool can_play { get; private set; default = false; }
|
public bool can_play { get; default = false; }
|
||||||
public bool can_pause { get; private set; default = false; }
|
public bool can_pause { get; default = false; }
|
||||||
public bool can_seek { get; private set; default = false; }
|
public bool can_seek { get; default = false; }
|
||||||
[CCode (notify = false)]
|
[CCode (notify = false)]
|
||||||
public bool can_control { get { return true; } }
|
public bool can_control { get { return false; } }
|
||||||
|
|
||||||
internal Subsonic.Client api { get; set; }
|
|
||||||
|
|
||||||
internal MprisPlayer (DBusConnection conn, Playbin playbin) {
|
|
||||||
playbin.bind_property (
|
|
||||||
"state",
|
|
||||||
this,
|
|
||||||
"playback_status",
|
|
||||||
BindingFlags.DEFAULT,
|
|
||||||
(binding, from, ref to) => {
|
|
||||||
switch (from.get_enum ()) {
|
|
||||||
case PlaybinState.STOPPED:
|
|
||||||
to.set_string ("Stopped");
|
|
||||||
this.can_go_next = false;
|
|
||||||
this.can_go_previous = false;
|
|
||||||
this.can_play = false;
|
|
||||||
this.can_pause = false;
|
|
||||||
this.can_seek = false;
|
|
||||||
return true;
|
|
||||||
case PlaybinState.PAUSED:
|
|
||||||
to.set_string ("Paused");
|
|
||||||
this.can_go_next = true;
|
|
||||||
this.can_go_previous = true;
|
|
||||||
this.can_play = true;
|
|
||||||
this.can_pause = true;
|
|
||||||
this.can_seek = true;
|
|
||||||
return true;
|
|
||||||
case PlaybinState.PLAYING:
|
|
||||||
to.set_string ("Playing");
|
|
||||||
this.can_go_next = true;
|
|
||||||
this.can_go_previous = true;
|
|
||||||
this.can_play = true;
|
|
||||||
this.can_pause = true;
|
|
||||||
this.can_seek = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
playbin.bind_property (
|
|
||||||
"volume",
|
|
||||||
this,
|
|
||||||
"volume",
|
|
||||||
BindingFlags.BIDIRECTIONAL,
|
|
||||||
(binding, from, ref to) => {
|
|
||||||
to.set_double (from.get_int () / 100.0);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
(binding, from, ref to) => {
|
|
||||||
to.set_int ((int) (from.get_double () * 100.0));
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
playbin.new_track.connect ((playbin) => {
|
|
||||||
PlaybinSong song = (PlaybinSong) playbin.play_queue.get_item (playbin.play_queue_position);
|
|
||||||
|
|
||||||
var metadata = new HashTable<string, Variant> (null, null);
|
|
||||||
metadata["mpris:trackid"] = new ObjectPath (@"/eu/callcc/audrey/track/$(song.id)");
|
|
||||||
metadata["mpris:length"] = (int64) song.duration * 1000000;
|
|
||||||
if (this.api != null) metadata["mpris:artUrl"] = this.api.cover_art_uri (song.id);
|
|
||||||
metadata["xesam:album"] = song.album;
|
|
||||||
metadata["xesam:artist"] = new string[] {song.artist};
|
|
||||||
if (song.genre != null) metadata["xesam:genre"] = song.genre;
|
|
||||||
metadata["xesam:title"] = song.title;
|
|
||||||
metadata["xesam:trackNumber"] = song.track;
|
|
||||||
metadata["xesam:useCount"] = song.play_count;
|
|
||||||
// TODO metadata["xesam:userRating"] = song.starred != null ? 1.0 : 0.0;
|
|
||||||
|
|
||||||
this.metadata = metadata;
|
|
||||||
});
|
|
||||||
|
|
||||||
playbin.stopped.connect (() => {
|
|
||||||
this.metadata = new HashTable<string, Variant> (null, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.on_next.connect (() => playbin.go_to_next_track ());
|
|
||||||
this.on_previous.connect (() => playbin.go_to_prev_track ());
|
|
||||||
this.on_play.connect (() => playbin.play ());
|
|
||||||
this.on_pause.connect (() => playbin.pause ());
|
|
||||||
this.on_play_pause.connect (() => {
|
|
||||||
if (playbin.state == PlaybinState.PAUSED) playbin.play ();
|
|
||||||
else if (playbin.state == PlaybinState.PLAYING) playbin.pause ();
|
|
||||||
});
|
|
||||||
this.on_stop.connect (() => {
|
|
||||||
playbin.stop ();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: seeking from mpris
|
|
||||||
// TODO: trigger the seeked signal when applicable
|
|
||||||
|
|
||||||
this.notify.connect ((p) => {
|
|
||||||
var builder = new VariantBuilder (VariantType.ARRAY);
|
|
||||||
var invalid_builder = new VariantBuilder (new VariantType ("as"));
|
|
||||||
|
|
||||||
string dbus_name;
|
|
||||||
Variant dbus_value;
|
|
||||||
|
|
||||||
switch (p.name) {
|
|
||||||
case "playback-status":
|
|
||||||
dbus_name = "PlaybackStatus";
|
|
||||||
dbus_value = this.playback_status;
|
|
||||||
break;
|
|
||||||
case "loop-status":
|
|
||||||
dbus_name = "LoopStatus";
|
|
||||||
dbus_value = this.loop_status;
|
|
||||||
break;
|
|
||||||
case "rate":
|
|
||||||
dbus_name = "Rate";
|
|
||||||
dbus_value = this.rate;
|
|
||||||
break;
|
|
||||||
case "shuffle":
|
|
||||||
dbus_name = "Shuffle";
|
|
||||||
dbus_value = this.shuffle;
|
|
||||||
break;
|
|
||||||
case "metadata":
|
|
||||||
dbus_name = "Metadata";
|
|
||||||
dbus_value = this.metadata;
|
|
||||||
break;
|
|
||||||
case "volume":
|
|
||||||
dbus_name = "Volume";
|
|
||||||
dbus_value = this.volume;
|
|
||||||
break;
|
|
||||||
case "minimum-rate":
|
|
||||||
dbus_name = "MinimumRate";
|
|
||||||
dbus_value = this.minimum_rate;
|
|
||||||
break;
|
|
||||||
case "maximum-rate":
|
|
||||||
dbus_name = "MaximumRate";
|
|
||||||
dbus_value = this.maximum_rate;
|
|
||||||
break;
|
|
||||||
case "can-go-next":
|
|
||||||
dbus_name = "CanGoNext";
|
|
||||||
dbus_value = this.can_go_next;
|
|
||||||
break;
|
|
||||||
case "can-go-previous":
|
|
||||||
dbus_name = "CanGoPrevious";
|
|
||||||
dbus_value = this.can_go_previous;
|
|
||||||
break;
|
|
||||||
case "can-play":
|
|
||||||
dbus_name = "CanPlay";
|
|
||||||
dbus_value = this.can_play;
|
|
||||||
break;
|
|
||||||
case "can-pause":
|
|
||||||
dbus_name = "CanPause";
|
|
||||||
dbus_value = this.can_pause;
|
|
||||||
break;
|
|
||||||
case "can-seek":
|
|
||||||
dbus_name = "CanSeek";
|
|
||||||
dbus_value = this.can_seek;
|
|
||||||
break;
|
|
||||||
case "api":
|
|
||||||
// internal, ignored
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
warning (@"unknown mpris player property $(p.name)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.add ("{sv}", dbus_name, dbus_value);
|
|
||||||
|
|
||||||
try {
|
|
||||||
conn.emit_signal (
|
|
||||||
null,
|
|
||||||
"/org/mpris/MediaPlayer2",
|
|
||||||
"org.freedesktop.DBus.Properties",
|
|
||||||
"PropertiesChanged",
|
|
||||||
new Variant (
|
|
||||||
"(sa{sv}as)",
|
|
||||||
"org.mpris.MediaPlayer2.Player",
|
|
||||||
builder,
|
|
||||||
invalid_builder));
|
|
||||||
} catch (Error e) {
|
|
||||||
error (@"could not notify of mpris property changes: $(e.message)");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
~MprisPlayer () {
|
|
||||||
debug ("destroying mpris player");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
mod ffi {
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreyMprisPlayer {
|
|
||||||
parent_instance: glib::gobject_ffi::GObject,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreyMprisClassPlayer {
|
|
||||||
parent_class: glib::gobject_ffi::GObjectClass,
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
pub fn audrey_mpris_player_get_type() -> glib::ffi::GType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct Player(Object<ffi::AudreyMprisPlayer, ffi::AudreyMprisClassPlayer>);
|
|
||||||
|
|
||||||
match fn {
|
|
||||||
type_ => || ffi::audrey_mpris_player_get_type(),
|
|
||||||
}
|
|
||||||
}
|
|
199
src/mpv.rs
199
src/mpv.rs
|
@ -1,199 +0,0 @@
|
||||||
mod ffi;
|
|
||||||
|
|
||||||
#[link(name = "mpv")]
|
|
||||||
extern "C" {}
|
|
||||||
|
|
||||||
pub struct Error(std::ffi::c_int);
|
|
||||||
|
|
||||||
impl Error {
|
|
||||||
fn from_return_code(rc: std::ffi::c_int) -> Result<(), Self> {
|
|
||||||
if rc == 0 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Self(rc))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for Error {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
|
||||||
f.debug_tuple("Error")
|
|
||||||
.field(unsafe {
|
|
||||||
&std::ffi::CStr::from_ptr(ffi::mpv_error_string(self.0 as std::ffi::c_int))
|
|
||||||
})
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::ptr::NonNull;
|
|
||||||
|
|
||||||
pub struct Handle(NonNull<ffi::mpv_handle>);
|
|
||||||
|
|
||||||
impl Handle {
|
|
||||||
pub fn create() -> Self {
|
|
||||||
Self(NonNull::new(unsafe { ffi::mpv_create() }).expect("could not create mpv handle"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn initialize(&self) -> Result<(), Error> {
|
|
||||||
Error::from_return_code(unsafe { ffi::mpv_initialize(self.0.as_ptr()) })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wait_event(&mut self, timeout: f64) -> Option<Event<'_>> {
|
|
||||||
let event = unsafe { &*ffi::mpv_wait_event(self.0.as_ptr(), timeout) };
|
|
||||||
match event.event_id {
|
|
||||||
0 => None,
|
|
||||||
1 => Some(Event::Shutdown),
|
|
||||||
2 => todo!("Event::LogMessage"),
|
|
||||||
3 => Some(Event::GetPropertyReply(
|
|
||||||
event.reply_userdata,
|
|
||||||
Error::from_return_code(event.error).map(|()| {
|
|
||||||
let data = unsafe { &*(event.data as *mut ffi::mpv_event_property) };
|
|
||||||
EventProperty {
|
|
||||||
name: unsafe { std::ffi::CStr::from_ptr(data.name) }
|
|
||||||
.to_str()
|
|
||||||
.unwrap(),
|
|
||||||
value: unsafe { FormatData::new(data.format, data.data) },
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
4 => todo!("Event::SetPropertyReply"),
|
|
||||||
5 => todo!("Event::CommandReply"),
|
|
||||||
6 => todo!("Event::StartFile"),
|
|
||||||
7 => todo!("Event::EndFile"),
|
|
||||||
8 => todo!("Event::FileLoaded"),
|
|
||||||
16 => todo!("Event::ClientMessage"),
|
|
||||||
17 => todo!("Event::VideoReconfig"),
|
|
||||||
18 => todo!("Event::AudioReconfig"),
|
|
||||||
20 => todo!("Event::Seek"),
|
|
||||||
21 => todo!("Event::PlaybackRestart"),
|
|
||||||
22 => todo!("Event::PropertyChange"),
|
|
||||||
24 => todo!("Event::QueueOverflow"),
|
|
||||||
25 => todo!("Event::Hook"),
|
|
||||||
_ => Some(Event::Ignore(event.event_id)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum FormatData<'a> {
|
|
||||||
None,
|
|
||||||
String(Cow<'a, str>),
|
|
||||||
OsdString(&'a str),
|
|
||||||
Flag(bool),
|
|
||||||
Int64(i64),
|
|
||||||
Double(f64),
|
|
||||||
//Node(Node<'a>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> FormatData<'a> {
|
|
||||||
unsafe fn new(format: ffi::mpv_format, data: *mut std::ffi::c_void) -> Self {
|
|
||||||
match format {
|
|
||||||
0 => Self::None,
|
|
||||||
1 => {
|
|
||||||
let data = data as *mut *mut std::ffi::c_char;
|
|
||||||
Self::String(String::from_utf8_lossy(
|
|
||||||
std::ffi::CStr::from_ptr(*data).to_bytes(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
2 => {
|
|
||||||
let data = data as *mut *mut std::ffi::c_char;
|
|
||||||
Self::OsdString(
|
|
||||||
std::ffi::CStr::from_ptr(*data)
|
|
||||||
.to_str()
|
|
||||||
.expect("OSD string wasn't UTF-8"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
3 => {
|
|
||||||
let data = data as *mut std::ffi::c_int;
|
|
||||||
Self::Flag(match *data {
|
|
||||||
0 => false,
|
|
||||||
1 => true,
|
|
||||||
_ => panic!("invalid mpv flag value"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
4 => {
|
|
||||||
let data = data as *mut i64;
|
|
||||||
Self::Int64(*data)
|
|
||||||
}
|
|
||||||
5 => {
|
|
||||||
let data = data as *mut f64;
|
|
||||||
Self::Double(*data)
|
|
||||||
}
|
|
||||||
6 => todo!(),
|
|
||||||
7 => todo!(),
|
|
||||||
8 => todo!(),
|
|
||||||
9 => todo!(),
|
|
||||||
_ => panic!("invalid mpv format"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EventProperty<'a> {
|
|
||||||
pub name: &'a str,
|
|
||||||
pub value: FormatData<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EventLogMessage<'a> {
|
|
||||||
pub prefix: &'a str,
|
|
||||||
pub level: &'a str,
|
|
||||||
pub text: &'a str,
|
|
||||||
pub log_level: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum EndFileReason {
|
|
||||||
Eof,
|
|
||||||
Stop,
|
|
||||||
Quit,
|
|
||||||
Error(Error),
|
|
||||||
Redirect,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EventStartFile {
|
|
||||||
pub playlist_entry_id: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EventEndFile {
|
|
||||||
pub reason: EndFileReason,
|
|
||||||
pub playlist_entry_id: i64,
|
|
||||||
pub playlist_insert_id: i64,
|
|
||||||
pub playlist_insert_num_entries: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EventClientMessage<'a> {
|
|
||||||
pub num_args: i32,
|
|
||||||
pub args: &'a [&'a str],
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EventHook<'a> {
|
|
||||||
pub name: &'a str,
|
|
||||||
pub id: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Event<'a> {
|
|
||||||
Shutdown,
|
|
||||||
LogMessage(EventLogMessage<'a>),
|
|
||||||
GetPropertyReply(u64, Result<EventProperty<'a>, Error>),
|
|
||||||
SetPropertyReply(u64, Result<(), Error>),
|
|
||||||
//CommandReply(u64, Result<Node<'a>, Error>),
|
|
||||||
StartFile(EventStartFile),
|
|
||||||
EndFile(EventEndFile),
|
|
||||||
FileLoaded,
|
|
||||||
ClientMessage(EventClientMessage<'a>),
|
|
||||||
VideoReconfig,
|
|
||||||
AudioReconfig,
|
|
||||||
Seek,
|
|
||||||
PlaybackRestart,
|
|
||||||
PropertyChange(u64, EventProperty<'a>),
|
|
||||||
QueueOverflow,
|
|
||||||
Hook(u64, EventHook<'a>),
|
|
||||||
// "Keep in mind that later ABI compatible releases might add new event
|
|
||||||
// types. These should be ignored by the API user."
|
|
||||||
Ignore(u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Handle {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// destroy? terminate_destroy??
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
#![allow(non_camel_case_types)]
|
|
||||||
#![allow(non_snake_case)]
|
|
||||||
#![allow(non_upper_case_globals)]
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/mpv_ffi.rs"));
|
|
|
@ -1,4 +0,0 @@
|
||||||
#define MPV_ENABLE_DEPRECATED 0
|
|
||||||
#include <mpv/client.h>
|
|
||||||
|
|
||||||
const int BUILD_VERSION = MPV_CLIENT_API_VERSION;
|
|
|
@ -1,77 +0,0 @@
|
||||||
mod song;
|
|
||||||
pub use song::Song;
|
|
||||||
|
|
||||||
mod state;
|
|
||||||
pub use state::State;
|
|
||||||
|
|
||||||
mod ffi {
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreyPlaybin {
|
|
||||||
parent_instance: glib::gobject_ffi::GObject,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreyPlaybinClass {
|
|
||||||
parent_class: glib::gobject_ffi::GObjectClass,
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
pub fn audrey_playbin_get_type() -> glib::ffi::GType;
|
|
||||||
pub fn audrey_playbin_get_state(self_: *mut AudreyPlaybin) -> super::state::ffi::State;
|
|
||||||
pub fn audrey_playbin_pause(self_: *mut AudreyPlaybin);
|
|
||||||
pub fn audrey_playbin_play(self_: *mut AudreyPlaybin);
|
|
||||||
pub fn audrey_playbin_get_volume(self_: *mut AudreyPlaybin) -> std::ffi::c_int;
|
|
||||||
pub fn audrey_playbin_set_volume(self_: *mut AudreyPlaybin, volume: std::ffi::c_int);
|
|
||||||
pub fn audrey_playbin_seek(self_: *mut AudreyPlaybin, position: f64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct Playbin(Object<ffi::AudreyPlaybin, ffi::AudreyPlaybinClass>);
|
|
||||||
|
|
||||||
match fn {
|
|
||||||
type_ => || ffi::audrey_playbin_get_type(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Playbin {
|
|
||||||
pub fn get_state(&self) -> State {
|
|
||||||
use glib::translate::ToGlibPtr;
|
|
||||||
|
|
||||||
unsafe { glib::translate::from_glib(ffi::audrey_playbin_get_state(self.to_glib_none().0)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause(&self) {
|
|
||||||
use glib::translate::ToGlibPtr;
|
|
||||||
|
|
||||||
unsafe { ffi::audrey_playbin_pause(self.to_glib_none().0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(&self) {
|
|
||||||
use glib::translate::ToGlibPtr;
|
|
||||||
|
|
||||||
unsafe { ffi::audrey_playbin_play(self.to_glib_none().0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_volume(&self) -> i32 {
|
|
||||||
use glib::translate::ToGlibPtr;
|
|
||||||
|
|
||||||
unsafe { ffi::audrey_playbin_get_volume(self.to_glib_none().0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_volume(&self, value: i32) {
|
|
||||||
use glib::translate::ToGlibPtr;
|
|
||||||
|
|
||||||
unsafe { ffi::audrey_playbin_set_volume(self.to_glib_none().0, value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn seek(&self, position: f64) {
|
|
||||||
use glib::translate::ToGlibPtr;
|
|
||||||
|
|
||||||
unsafe { ffi::audrey_playbin_seek(self.to_glib_none().0, position) }
|
|
||||||
}
|
|
||||||
}
|
|
338
src/playbin.vala
338
src/playbin.vala
|
@ -1,75 +1,15 @@
|
||||||
public enum Audrey.PlaybinState {
|
public enum PlaybinState {
|
||||||
STOPPED,
|
STOPPED,
|
||||||
PAUSED,
|
PAUSED,
|
||||||
PLAYING,
|
PLAYING,
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct Audrey.CommandCallback {
|
public class Playbin : GLib.Object {
|
||||||
unowned SourceFunc callback;
|
|
||||||
int error;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Audrey.PlaybinSong : Object {
|
|
||||||
private Subsonic.Song inner;
|
|
||||||
public string id { get { return inner.id; } }
|
|
||||||
public string title { get { return inner.title; } }
|
|
||||||
public string artist { get { return inner.artist; } }
|
|
||||||
public string album { get { return inner.album; } }
|
|
||||||
public string? genre { get { return inner.genre; } }
|
|
||||||
public int64 duration { get { return inner.duration; } }
|
|
||||||
public int64 track { get { return inner.track; } }
|
|
||||||
public int64 play_count { get { return inner.play_count; } }
|
|
||||||
|
|
||||||
public Gdk.Paintable? thumbnail { get; private set; }
|
|
||||||
|
|
||||||
private Cancellable cancel_loading_thumbnail;
|
|
||||||
|
|
||||||
public PlaybinSong (Subsonic.Client api, Subsonic.Song song) {
|
|
||||||
this.api = api;
|
|
||||||
this.inner = song;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Subsonic.Client api;
|
|
||||||
|
|
||||||
public void need_cover_art () {
|
|
||||||
/* TODO
|
|
||||||
if (this.cancel_loading_thumbnail != null) return;
|
|
||||||
if (this.thumbnail != null) return;
|
|
||||||
|
|
||||||
this.cancel_loading_thumbnail = new Cancellable ();
|
|
||||||
// TODO: dpi scaling maybe?? probably
|
|
||||||
api.cover_art.begin (this.id, 50, Priority.LOW, this.cancel_loading_thumbnail, (obj, res) => {
|
|
||||||
try {
|
|
||||||
var pixbuf = api.cover_art.end (res);
|
|
||||||
this.thumbnail = Gdk.Texture.for_pixbuf (pixbuf);
|
|
||||||
} catch (Error e) {
|
|
||||||
if (!(e is IOError.CANCELLED)) {
|
|
||||||
warning ("could not fetch cover art for song %s: %s", this.id, e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.cancel_loading_thumbnail = null;
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
~PlaybinSong () {
|
|
||||||
if (this.cancel_loading_thumbnail != null) {
|
|
||||||
this.cancel_loading_thumbnail.cancel ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Audrey.Playbin : GLib.Object {
|
|
||||||
private Mpv.Handle mpv = new Mpv.Handle ();
|
private Mpv.Handle mpv = new Mpv.Handle ();
|
||||||
private int _volume = 100;
|
|
||||||
private bool _mute = false;
|
|
||||||
private ListStore _play_queue = new ListStore (typeof (PlaybinSong));
|
|
||||||
|
|
||||||
// try to prevent wait_event to be called twice
|
|
||||||
private bool is_handling_event = false;
|
|
||||||
|
|
||||||
public PlaybinState state { get; private set; default = PlaybinState.STOPPED; }
|
public PlaybinState state { get; private set; default = PlaybinState.STOPPED; }
|
||||||
|
|
||||||
|
private int _volume = 100;
|
||||||
public int volume {
|
public int volume {
|
||||||
get { return _volume; }
|
get { return _volume; }
|
||||||
set {
|
set {
|
||||||
|
@ -82,6 +22,7 @@ public class Audrey.Playbin : GLib.Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool _mute = false;
|
||||||
public bool mute {
|
public bool mute {
|
||||||
get { return _mute; }
|
get { return _mute; }
|
||||||
set {
|
set {
|
||||||
|
@ -94,36 +35,50 @@ public class Audrey.Playbin : GLib.Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// invariant: negative iff stopped, otherwise < play queue length
|
public uint play_queue_position { get; private set; }
|
||||||
public int play_queue_position { get; private set; default = -1; }
|
|
||||||
|
|
||||||
// signalled when a new track is current
|
public signal void now_playing (Subsonic.Song now, Subsonic.Song? next);
|
||||||
public signal void new_track ();
|
|
||||||
// signalled when the last track is over
|
|
||||||
public signal void stopped ();
|
public signal void stopped ();
|
||||||
|
|
||||||
// these are mostly synced with mpv
|
|
||||||
public double position { get; private set; default = 0.0; }
|
public double position { get; private set; default = 0.0; }
|
||||||
public double duration { get; private set; default = 0.0; }
|
public double duration { get; private set; default = 0.0; }
|
||||||
|
|
||||||
public weak Subsonic.Client api { get; set; default = null; }
|
public Subsonic.Client api { get; set; default = null; }
|
||||||
|
|
||||||
public ListModel play_queue { get { return this._play_queue; } }
|
public ListStore play_queue { get; private set; }
|
||||||
public uint play_queue_length { get; private set; default = 0; }
|
|
||||||
|
|
||||||
private async Mpv.Error mpv_command_async (string[] args) {
|
private void on_play_queue_items_changed (ListModel play_queue, uint position, uint removed, uint added) {
|
||||||
CommandCallback cc = {};
|
for (uint i = 0; i < removed; i += 1) {
|
||||||
|
assert (this.mpv.command ({
|
||||||
|
"playlist-remove",
|
||||||
|
position.to_string (),
|
||||||
|
}) >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
this.mpv.command_async ((uint64) &cc, args);
|
for (uint i = 0; i < added; i += 1) {
|
||||||
|
assert (this.mpv.command ({
|
||||||
|
"loadfile",
|
||||||
|
this.api.stream_uri (((Subsonic.Song) play_queue.get_item (position+i)).id),
|
||||||
|
"insert-at-play",
|
||||||
|
(position+i).to_string (),
|
||||||
|
}) >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
cc.callback = this.mpv_command_async.callback;
|
if (this.play_queue_position == position && removed > 0) {
|
||||||
yield;
|
if (this.play_queue_position < this.play_queue.get_n_items ()) {
|
||||||
return cc.error;
|
// edge case: new track plays, playlist-pos doesn't change, so now_playing never n gets triggered
|
||||||
|
this.now_playing (
|
||||||
|
(Subsonic.Song) this.play_queue.get_item (this.play_queue_position),
|
||||||
|
(Subsonic.Song?) this.play_queue.get_item (this.play_queue_position+1));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Playbin () {
|
public Playbin () {
|
||||||
|
this.play_queue = new ListStore (typeof (Subsonic.Song));
|
||||||
|
this.play_queue.items_changed.connect (this.on_play_queue_items_changed);
|
||||||
|
|
||||||
assert (this.mpv.initialize () >= 0);
|
assert (this.mpv.initialize () >= 0);
|
||||||
assert (this.mpv.set_property_string ("audio-client-name", "audrey") >= 0);
|
|
||||||
assert (this.mpv.set_property_string ("user-agent", Audrey.Const.user_agent) >= 0);
|
assert (this.mpv.set_property_string ("user-agent", Audrey.Const.user_agent) >= 0);
|
||||||
assert (this.mpv.set_property_string ("video", "no") >= 0);
|
assert (this.mpv.set_property_string ("video", "no") >= 0);
|
||||||
assert (this.mpv.set_property_string ("prefetch-playlist", "yes") >= 0);
|
assert (this.mpv.set_property_string ("prefetch-playlist", "yes") >= 0);
|
||||||
|
@ -131,16 +86,9 @@ public class Audrey.Playbin : GLib.Object {
|
||||||
assert (this.mpv.observe_property (0, "time-pos", Mpv.Format.DOUBLE) >= 0);
|
assert (this.mpv.observe_property (0, "time-pos", Mpv.Format.DOUBLE) >= 0);
|
||||||
assert (this.mpv.observe_property (1, "duration", Mpv.Format.DOUBLE) >= 0);
|
assert (this.mpv.observe_property (1, "duration", Mpv.Format.DOUBLE) >= 0);
|
||||||
assert (this.mpv.observe_property (2, "playlist-pos", Mpv.Format.INT64) >= 0);
|
assert (this.mpv.observe_property (2, "playlist-pos", Mpv.Format.INT64) >= 0);
|
||||||
assert (this.mpv.observe_property (3, "pause", Mpv.Format.FLAG) >= 0);
|
|
||||||
|
|
||||||
this.mpv.wakeup_callback = () => {
|
this.mpv.wakeup_callback = () => {
|
||||||
Idle.add (() => {
|
Idle.add (() => {
|
||||||
if (this.is_handling_event) {
|
|
||||||
warning ("main thread mpv wakeup callback called twice");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.is_handling_event = true;
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
var event = this.mpv.wait_event (0.0);
|
var event = this.mpv.wait_event (0.0);
|
||||||
if (event.event_id == Mpv.EventId.NONE) break;
|
if (event.event_id == Mpv.EventId.NONE) break;
|
||||||
|
@ -161,42 +109,25 @@ public class Audrey.Playbin : GLib.Object {
|
||||||
case 1:
|
case 1:
|
||||||
assert (data.name == "duration");
|
assert (data.name == "duration");
|
||||||
if (data.format == Mpv.Format.NONE) {
|
if (data.format == Mpv.Format.NONE) {
|
||||||
// this.duration = 0.0; i think this prevents the fallback below from working
|
this.duration = 0.0;
|
||||||
} else {
|
} else {
|
||||||
this.duration = data.parse_double ();
|
this.duration = data.parse_double ();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
// here as a sanity check
|
|
||||||
// should always match our own play_queu_position/state
|
|
||||||
assert (data.name == "playlist-pos");
|
assert (data.name == "playlist-pos");
|
||||||
int64 playlist_pos = data.parse_int64 ();
|
if (data.parse_int64 () < 0) {
|
||||||
if (playlist_pos < 0) {
|
debug ("playlist-pos is null, sending stopped event");
|
||||||
if (this.state != PlaybinState.STOPPED) {
|
this.play_queue_position = this.play_queue.get_n_items ();
|
||||||
error ("mpv has no current playlist entry, but we think it's index %d", this.play_queue_position);
|
this.state = PlaybinState.STOPPED;
|
||||||
}
|
this.stopped ();
|
||||||
assert (this.play_queue_position < 0);
|
|
||||||
} else {
|
} else {
|
||||||
if (this.state == PlaybinState.STOPPED) {
|
this.play_queue_position = (uint) data.parse_int64 ();
|
||||||
error ("mpv is at playlist entry %d, but we're stopped", (int) playlist_pos);
|
debug (@"playlist-pos has been updated to $(this.play_queue_position)");
|
||||||
}
|
this.now_playing (
|
||||||
if (this.play_queue_position != (int) playlist_pos) {
|
(Subsonic.Song) this.play_queue.get_item (this.play_queue_position),
|
||||||
error ("mpv is at playlist entry %d, but we think it's %d", (int) playlist_pos, this.play_queue_position);
|
(Subsonic.Song?) this.play_queue.get_item (this.play_queue_position+1));
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 3:
|
|
||||||
// also here as a sanity check
|
|
||||||
// should always match our own state
|
|
||||||
assert (data.name == "pause");
|
|
||||||
bool pause = data.parse_flag ();
|
|
||||||
if (pause && this.state != PlaybinState.PAUSED) {
|
|
||||||
error (@"mpv is paused, but we are @(this.state)");
|
|
||||||
}
|
|
||||||
if (!pause && this.state == PlaybinState.PAUSED) {
|
|
||||||
error ("mpv is not paused, but we are paused");
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -208,40 +139,14 @@ public class Audrey.Playbin : GLib.Object {
|
||||||
|
|
||||||
case Mpv.EventId.START_FILE:
|
case Mpv.EventId.START_FILE:
|
||||||
debug ("START_FILE received");
|
debug ("START_FILE received");
|
||||||
|
|
||||||
// estimate duration from api data
|
|
||||||
// while mpv doesn't know it
|
|
||||||
this.duration = ((PlaybinSong) this._play_queue.get_item (this.play_queue_position)).duration;
|
|
||||||
|
|
||||||
this.new_track ();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mpv.EventId.END_FILE:
|
case Mpv.EventId.END_FILE:
|
||||||
|
debug ("END_FILE received");
|
||||||
var data = event.parse_end_file ();
|
var data = event.parse_end_file ();
|
||||||
debug (@"END_FILE received (reason: $(data.reason))");
|
|
||||||
|
|
||||||
if (data.error < 0) {
|
if (data.error < 0) {
|
||||||
warning ("playback of track aborted: %s", data.error.to_string ());
|
warning ("playback of track aborted: %s", data.error.to_string ());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.reason == Mpv.EndFileReason.EOF) {
|
|
||||||
// assume this is a proper transition
|
|
||||||
this.play_queue_position += 1;
|
|
||||||
|
|
||||||
if (this.play_queue_position == this._play_queue.get_n_items ()) {
|
|
||||||
// reached the end (?)
|
|
||||||
this.state = PlaybinState.STOPPED;
|
|
||||||
this.play_queue_position = -1;
|
|
||||||
this.stopped ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Mpv.EventId.COMMAND_REPLY:
|
|
||||||
unowned CommandCallback *cc = (CommandCallback *) event.reply_userdata;
|
|
||||||
cc.error = event.error;
|
|
||||||
cc.callback ();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -250,19 +155,14 @@ public class Audrey.Playbin : GLib.Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.is_handling_event = false;
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void seek (double position) {
|
public void seek (double position) {
|
||||||
var rc = this.mpv.command ({"seek", position.to_string (), "absolute"});
|
|
||||||
if (rc < 0) {
|
|
||||||
warning (@"could not seek to $position: $rc");
|
|
||||||
} else {
|
|
||||||
this.position = position;
|
this.position = position;
|
||||||
}
|
assert (this.mpv.command ({"seek", position.to_string (), "absolute"}) >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// manually changes which track in the play queue to play
|
// manually changes which track in the play queue to play
|
||||||
|
@ -270,9 +170,7 @@ public class Audrey.Playbin : GLib.Object {
|
||||||
requires (position < this.play_queue.get_n_items ())
|
requires (position < this.play_queue.get_n_items ())
|
||||||
{
|
{
|
||||||
assert (this.mpv.command ({"playlist-play-index", position.to_string ()}) >= 0);
|
assert (this.mpv.command ({"playlist-play-index", position.to_string ()}) >= 0);
|
||||||
this.play_queue_position = (int) position;
|
|
||||||
this.state = PlaybinState.PLAYING;
|
this.state = PlaybinState.PLAYING;
|
||||||
this.play (); // make sure mpv actually starts playing the track
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void pause () {
|
public void pause () {
|
||||||
|
@ -287,12 +185,7 @@ public class Audrey.Playbin : GLib.Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void play () {
|
public void play () {
|
||||||
if (this.state == PlaybinState.STOPPED) {
|
assert (this.state != PlaybinState.STOPPED);
|
||||||
// allow only when playlist is not empty
|
|
||||||
// and start from the top
|
|
||||||
assert (this._play_queue.get_n_items () > 0);
|
|
||||||
this.select_track (0);
|
|
||||||
} else {
|
|
||||||
this.state = PlaybinState.PLAYING;
|
this.state = PlaybinState.PLAYING;
|
||||||
debug ("setting state to playing");
|
debug ("setting state to playing");
|
||||||
var ret = this.mpv.set_property_flag("pause", false);
|
var ret = this.mpv.set_property_flag("pause", false);
|
||||||
|
@ -300,139 +193,16 @@ public class Audrey.Playbin : GLib.Object {
|
||||||
debug (@"failed to set state to playing ($(ret)): $(ret.to_string())");
|
debug (@"failed to set state to playing ($(ret)): $(ret.to_string())");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void go_to_next_track ()
|
public void next_track () {
|
||||||
requires (this.state != PlaybinState.STOPPED)
|
assert (this.state != PlaybinState.STOPPED);
|
||||||
{
|
this.state = PlaybinState.PLAYING;
|
||||||
if (this.play_queue_position+1 < this._play_queue.get_n_items ()) {
|
|
||||||
this.play_queue_position += 1;
|
|
||||||
assert (this.mpv.command ({"playlist-next-playlist"}) >= 0);
|
assert (this.mpv.command ({"playlist-next-playlist"}) >= 0);
|
||||||
} else {
|
|
||||||
warning ("tried to skip forward at end of play queue, ignoring");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void go_to_prev_track ()
|
public void prev_track () {
|
||||||
requires (this.state != PlaybinState.STOPPED)
|
assert (this.state != PlaybinState.STOPPED);
|
||||||
{
|
this.state = PlaybinState.PLAYING;
|
||||||
if (this.play_queue_position > 0) {
|
|
||||||
this.play_queue_position -= 1;
|
|
||||||
assert (this.mpv.command ({"playlist-prev-playlist"}) >= 0);
|
assert (this.mpv.command ({"playlist-prev-playlist"}) >= 0);
|
||||||
} else {
|
|
||||||
warning ("tried to skip to prev track at start of play queue, ignoring");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void remove_track (uint position)
|
|
||||||
requires (position < this._play_queue.get_n_items ())
|
|
||||||
{
|
|
||||||
assert (this.mpv.command({"playlist-remove", position.to_string ()}) >= 0);
|
|
||||||
this._play_queue.remove (position);
|
|
||||||
this.play_queue_length -= 1;
|
|
||||||
if (this.play_queue_position > position) this.play_queue_position -= 1;
|
|
||||||
if (this.play_queue_position == this._play_queue.get_n_items ()) {
|
|
||||||
// we just killed the last track
|
|
||||||
this.state = PlaybinState.STOPPED;
|
|
||||||
this.play_queue_position = -1;
|
|
||||||
this.stopped ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear () {
|
|
||||||
assert (this.mpv.command({"playlist-clear"}) >= 0);
|
|
||||||
if (this.state != PlaybinState.STOPPED) {
|
|
||||||
assert (this.mpv.command({"playlist-remove", "current"}) >= 0);
|
|
||||||
}
|
|
||||||
this.state = PlaybinState.STOPPED;
|
|
||||||
this._play_queue.remove_all ();
|
|
||||||
this.play_queue_length = 0;
|
|
||||||
this.play_queue_position = -1;
|
|
||||||
|
|
||||||
this.stopped ();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void append_track (Subsonic.Song song) {
|
|
||||||
assert (this.mpv.command ({
|
|
||||||
"loadfile",
|
|
||||||
this.api.stream_uri (song.id),
|
|
||||||
"append",
|
|
||||||
}) >= 0);
|
|
||||||
this._play_queue.append (new PlaybinSong (this.api, song));
|
|
||||||
this.play_queue_length += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void append_track_async (Subsonic.Song song) {
|
|
||||||
var err = yield this.mpv_command_async ({
|
|
||||||
"loadfile",
|
|
||||||
this.api.stream_uri (song.id),
|
|
||||||
"append",
|
|
||||||
});
|
|
||||||
assert (err >= 0);
|
|
||||||
|
|
||||||
this._play_queue.append (new PlaybinSong (this.api, song));
|
|
||||||
this.play_queue_length += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void move_track (uint from, uint to)
|
|
||||||
requires (from < this._play_queue.get_n_items ())
|
|
||||||
requires (to < this._play_queue.get_n_items ())
|
|
||||||
{
|
|
||||||
debug (@"moving track $from to $to");
|
|
||||||
|
|
||||||
if (from < to) {
|
|
||||||
// why offset to? because if the playlist is 01234,
|
|
||||||
// mpv takes "move 1 to 3" to mean 02134, not 02314
|
|
||||||
// that is, the target is a "gap", not a playlist entry
|
|
||||||
// from -> 0 1 2 3 4 5
|
|
||||||
// to -> 0 1 2 3 4 5 6
|
|
||||||
assert(this.mpv.command({
|
|
||||||
"playlist-move",
|
|
||||||
from.to_string (),
|
|
||||||
(to+1).to_string (),
|
|
||||||
}) >= 0);
|
|
||||||
|
|
||||||
// F0123T -> 0123TF
|
|
||||||
var additions = new Object[to-from+1];
|
|
||||||
for (uint i = from+1; i < to; i += 1) {
|
|
||||||
additions[i-from-1] = this._play_queue.get_item (i);
|
|
||||||
}
|
|
||||||
additions[to-from-1] = this._play_queue.get_item (to);
|
|
||||||
additions[to-from] = this._play_queue.get_item (from);
|
|
||||||
this._play_queue.splice(from, to-from+1, additions);
|
|
||||||
|
|
||||||
if (this.play_queue_position == (int) from) this.play_queue_position = (int) to;
|
|
||||||
else if (this.play_queue_position > (int) from && this.play_queue_position <= (int) to) this.play_queue_position -= 1;
|
|
||||||
} else if (from > to) {
|
|
||||||
assert(this.mpv.command({
|
|
||||||
"playlist-move",
|
|
||||||
from.to_string (),
|
|
||||||
to.to_string (),
|
|
||||||
}) >= 0);
|
|
||||||
|
|
||||||
// T0123F -> FT0123
|
|
||||||
var additions = new Object[from-to+1];
|
|
||||||
additions[0] = this._play_queue.get_item (from);
|
|
||||||
for (uint i = to; i < from; i += 1) {
|
|
||||||
additions[i-to+1] = this._play_queue.get_item (i);
|
|
||||||
}
|
|
||||||
this._play_queue.splice (to, from-to+1, additions);
|
|
||||||
|
|
||||||
if (this.play_queue_position == (int) from) this.play_queue_position = (int) to;
|
|
||||||
else if (this.play_queue_position >= (int) to && this.play_queue_position < (int) from) this.play_queue_position += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop () {
|
|
||||||
debug ("stopping playback");
|
|
||||||
// don't clear the playlist, just in case (less state changes to sync)
|
|
||||||
assert(this.mpv.command({"stop", "keep-playlist"}) >= 0);
|
|
||||||
this.state = PlaybinState.STOPPED;
|
|
||||||
this.play_queue_position = -1;
|
|
||||||
this.stopped ();
|
|
||||||
}
|
|
||||||
|
|
||||||
~Playbin () {
|
|
||||||
debug ("destroying playbin");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
mod ffi {
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreyPlaybinSong {
|
|
||||||
parent_instance: glib::gobject_ffi::GObject,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreyPlaybinSongClass {
|
|
||||||
parent_class: glib::gobject_ffi::GObjectClass,
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
pub fn audrey_playbin_song_get_type() -> glib::ffi::GType;
|
|
||||||
pub fn audrey_playbin_song_get_title(
|
|
||||||
self_: *mut AudreyPlaybinSong,
|
|
||||||
) -> *const std::ffi::c_char;
|
|
||||||
pub fn audrey_playbin_song_get_artist(
|
|
||||||
self_: *mut AudreyPlaybinSong,
|
|
||||||
) -> *const std::ffi::c_char;
|
|
||||||
pub fn audrey_playbin_song_get_album(
|
|
||||||
self_: *mut AudreyPlaybinSong,
|
|
||||||
) -> *const std::ffi::c_char;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct Song(Object<ffi::AudreyPlaybinSong, ffi::AudreyPlaybinSongClass>);
|
|
||||||
|
|
||||||
match fn {
|
|
||||||
type_ => || ffi::audrey_playbin_song_get_type(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Song {
|
|
||||||
pub fn get_title(&self) -> std::borrow::Cow<'_, str> {
|
|
||||||
use glib::translate::ToGlibPtr;
|
|
||||||
|
|
||||||
// TODO: memory management....
|
|
||||||
String::from_utf8_lossy(unsafe {
|
|
||||||
std::ffi::CStr::from_ptr(ffi::audrey_playbin_song_get_title(self.to_glib_none().0))
|
|
||||||
.to_bytes()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_artist(&self) -> std::borrow::Cow<'_, str> {
|
|
||||||
use glib::translate::ToGlibPtr;
|
|
||||||
|
|
||||||
// TODO: memory management....
|
|
||||||
String::from_utf8_lossy(unsafe {
|
|
||||||
std::ffi::CStr::from_ptr(ffi::audrey_playbin_song_get_artist(self.to_glib_none().0))
|
|
||||||
.to_bytes()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_album(&self) -> std::borrow::Cow<'_, str> {
|
|
||||||
use glib::translate::ToGlibPtr;
|
|
||||||
|
|
||||||
// TODO: memory management....
|
|
||||||
String::from_utf8_lossy(unsafe {
|
|
||||||
std::ffi::CStr::from_ptr(ffi::audrey_playbin_song_get_album(self.to_glib_none().0))
|
|
||||||
.to_bytes()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
pub mod ffi {
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
pub type State = std::ffi::c_int;
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
pub fn audrey_playbin_state_get_type() -> glib::ffi::GType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use glib::prelude::*;
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
||||||
pub enum State {
|
|
||||||
Stopped,
|
|
||||||
Paused,
|
|
||||||
Playing,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl glib::translate::FromGlib<ffi::State> for State {
|
|
||||||
unsafe fn from_glib(value: ffi::State) -> Self {
|
|
||||||
match value {
|
|
||||||
0 => Self::Stopped,
|
|
||||||
1 => Self::Paused,
|
|
||||||
2 => Self::Playing,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl<'a> glib::value::FromValue<'a> for State {
|
|
||||||
type Checker = glib::value::GenericValueTypeChecker<Self>;
|
|
||||||
|
|
||||||
unsafe fn from_value(value: &'a glib::Value) -> Self {
|
|
||||||
use glib::translate::ToGlibPtr;
|
|
||||||
|
|
||||||
glib::translate::from_glib(glib::gobject_ffi::g_value_get_enum(value.to_glib_none().0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StaticType for State {
|
|
||||||
fn static_type() -> glib::Type {
|
|
||||||
unsafe { glib::translate::from_glib(ffi::audrey_playbin_state_get_type()) }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,3 @@
|
||||||
/*
|
|
||||||
#play-queue-page {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
var(--window-bg-color),
|
|
||||||
var(--accent-bg-color));
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#seek-scale slider {
|
#seek-scale slider {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
opacity: 0%;
|
opacity: 0%;
|
||||||
|
@ -19,6 +11,6 @@
|
||||||
min-height: 15px;
|
min-height: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#play-queue .playing label.title {
|
.playing .title-label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
mod client;
|
|
||||||
pub use client::Client;
|
|
||||||
|
|
||||||
mod song;
|
|
||||||
pub use song::Song;
|
|
|
@ -1,11 +1,11 @@
|
||||||
public errordomain Audrey.Subsonic.Error {
|
public errordomain Subsonic.Error {
|
||||||
BAD_AUTHN,
|
BAD_AUTHN,
|
||||||
ERROR,
|
ERROR,
|
||||||
}
|
}
|
||||||
|
|
||||||
public delegate void Audrey.Subsonic.SongCallback (Song song);
|
public delegate void Subsonic.SongCallback (Song song);
|
||||||
|
|
||||||
public class Audrey.Subsonic.Artist : Object {
|
public class Subsonic.Artist : Object {
|
||||||
public string index;
|
public string index;
|
||||||
public string id;
|
public string id;
|
||||||
public string name { get; private set; }
|
public string name { get; private set; }
|
||||||
|
@ -38,7 +38,7 @@ public class Audrey.Subsonic.Artist : Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Audrey.Subsonic.Album : Object {
|
public class Subsonic.Album : Object {
|
||||||
public string id;
|
public string id;
|
||||||
public string name;
|
public string name;
|
||||||
|
|
||||||
|
@ -53,18 +53,15 @@ public class Audrey.Subsonic.Album : Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Audrey.Subsonic.Song {
|
public class Subsonic.Song : Object {
|
||||||
public string id;
|
public string id { get; private set; }
|
||||||
public string title;
|
public string title { get; private set; }
|
||||||
public string album;
|
public string album { get; private set; }
|
||||||
public string artist;
|
public string artist { get; private set; }
|
||||||
public int64 track;
|
public int64 track { get; private set; }
|
||||||
public int64 year;
|
public int64 year { get; private set; }
|
||||||
public DateTime? starred; // TODO
|
public DateTime? starred { get; private set; }
|
||||||
public int64 duration;
|
public int64 duration { get; private set; }
|
||||||
public int64 play_count;
|
|
||||||
public string? genre;
|
|
||||||
public string cover_art;
|
|
||||||
|
|
||||||
public Song (Json.Reader reader) {
|
public Song (Json.Reader reader) {
|
||||||
reader.read_member ("id");
|
reader.read_member ("id");
|
||||||
|
@ -94,22 +91,53 @@ public struct Audrey.Subsonic.Song {
|
||||||
reader.read_member ("duration");
|
reader.read_member ("duration");
|
||||||
this.duration = reader.get_int_value ();
|
this.duration = reader.get_int_value ();
|
||||||
reader.end_member ();
|
reader.end_member ();
|
||||||
|
|
||||||
reader.read_member ("playCount");
|
|
||||||
this.play_count = reader.get_int_value ();
|
|
||||||
reader.end_member ();
|
|
||||||
|
|
||||||
reader.read_member ("genre");
|
|
||||||
this.genre = reader.get_string_value ();
|
|
||||||
reader.end_member ();
|
|
||||||
|
|
||||||
reader.read_member ("coverArt");
|
|
||||||
this.cover_art = reader.get_string_value ();
|
|
||||||
reader.end_member ();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Audrey.Subsonic.Client : Object {
|
public struct Subsonic.PlayQueue {
|
||||||
|
public string current;
|
||||||
|
public int64 position;
|
||||||
|
public DateTime changed;
|
||||||
|
public string changed_by;
|
||||||
|
public ListStore songs;
|
||||||
|
|
||||||
|
internal PlayQueue.from_reader (Json.Reader reader) {
|
||||||
|
reader.read_member ("current");
|
||||||
|
this.current = reader.get_string_value ();
|
||||||
|
reader.end_member ();
|
||||||
|
|
||||||
|
reader.read_member ("position");
|
||||||
|
this.position = reader.get_int_value ();
|
||||||
|
reader.end_member ();
|
||||||
|
|
||||||
|
reader.read_member ("changed");
|
||||||
|
this.changed = new DateTime.from_iso8601 (reader.get_string_value (), null);
|
||||||
|
reader.end_member ();
|
||||||
|
|
||||||
|
reader.read_member ("changed_by");
|
||||||
|
this.changed_by = reader.get_string_value ();
|
||||||
|
reader.end_member ();
|
||||||
|
|
||||||
|
//print("%s %lli %s %s\n",this.current, this.position, this.changed, this.changed_by);
|
||||||
|
|
||||||
|
this.songs = new ListStore (typeof (Song));
|
||||||
|
|
||||||
|
reader.read_member ("song");
|
||||||
|
for (int i = 0; i < reader.count_elements (); i += 1) {
|
||||||
|
reader.read_element (i);
|
||||||
|
this.songs.append (new Song (reader));
|
||||||
|
reader.end_element ();
|
||||||
|
}
|
||||||
|
reader.end_member ();
|
||||||
|
assert (reader.get_error () == null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Subsonic.Client : Object {
|
||||||
|
public ListStore artist_list;
|
||||||
|
public ListStore album_list;
|
||||||
|
public ListStore song_list;
|
||||||
|
|
||||||
private Soup.Session session;
|
private Soup.Session session;
|
||||||
private string url;
|
private string url;
|
||||||
private string parameters;
|
private string parameters;
|
||||||
|
@ -120,6 +148,10 @@ public class Audrey.Subsonic.Client : Object {
|
||||||
|
|
||||||
this.session = new Soup.Session ();
|
this.session = new Soup.Session ();
|
||||||
this.session.user_agent = Audrey.Const.user_agent;
|
this.session.user_agent = Audrey.Const.user_agent;
|
||||||
|
|
||||||
|
this.artist_list = new ListStore (typeof (Artist));
|
||||||
|
this.album_list = new ListStore (typeof (Album));
|
||||||
|
this.song_list = new ListStore (typeof (Song));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unwrap_response (Json.Reader reader) throws GLib.Error {
|
private void unwrap_response (Json.Reader reader) throws GLib.Error {
|
||||||
|
@ -190,11 +222,8 @@ public class Audrey.Subsonic.Client : Object {
|
||||||
|
|
||||||
for (int i = 0; i < reader.count_elements (); i += 1) {
|
for (int i = 0; i < reader.count_elements (); i += 1) {
|
||||||
reader.read_element (i);
|
reader.read_element (i);
|
||||||
callback (Song (reader));
|
callback (new Song (reader));
|
||||||
reader.end_element ();
|
reader.end_element ();
|
||||||
|
|
||||||
Idle.add (this.get_random_songs.callback);
|
|
||||||
yield;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert (reader.get_error () == null);
|
assert (reader.get_error () == null);
|
||||||
|
@ -204,33 +233,16 @@ public class Audrey.Subsonic.Client : Object {
|
||||||
return @"$(this.url)/rest/stream?id=$(Uri.escape_string(id))&$(this.parameters)";
|
return @"$(this.url)/rest/stream?id=$(Uri.escape_string(id))&$(this.parameters)";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string cover_art_uri (string id, int size = -1) {
|
public string cover_art_uri (string id) {
|
||||||
if (size >= 0) {
|
|
||||||
return @"$(this.url)/rest/getCoverArt?id=$(Uri.escape_string(id))&$(this.parameters)";
|
return @"$(this.url)/rest/getCoverArt?id=$(Uri.escape_string(id))&$(this.parameters)";
|
||||||
} else {
|
|
||||||
return @"$(this.url)/rest/getCoverArt?size=$size&id=$(Uri.escape_string(id))&$(this.parameters)";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Gdk.Pixbuf cover_art (
|
public async Gdk.Pixbuf cover_art (string id, Cancellable cancellable) throws GLib.Error {
|
||||||
string id,
|
var msg = new Soup.Message("GET", this.cover_art_uri (id));
|
||||||
int size = -1,
|
|
||||||
int priority = Priority.DEFAULT,
|
|
||||||
Cancellable? cancellable = null
|
|
||||||
)
|
|
||||||
throws GLib.Error
|
|
||||||
{
|
|
||||||
var msg = new Soup.Message("GET", this.cover_art_uri (id, size));
|
|
||||||
assert (msg != null);
|
assert (msg != null);
|
||||||
|
|
||||||
var stream = yield this.session.send_async (msg, priority, cancellable);
|
var stream = yield this.session.send_async (msg, Priority.DEFAULT, cancellable);
|
||||||
if (msg.get_status () != Soup.Status.OK) {
|
assert (msg.get_status () == Soup.Status.OK);
|
||||||
warning ("could not load cover art for %s: %s", id, Soup.Status.get_phrase (msg.get_status ()));
|
|
||||||
}
|
|
||||||
return yield new Gdk.Pixbuf.from_stream_async (stream, cancellable);
|
return yield new Gdk.Pixbuf.from_stream_async (stream, cancellable);
|
||||||
}
|
}
|
||||||
|
|
||||||
~Client () {
|
|
||||||
debug ("destroying subsonic client");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
mod ffi {
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreySubsonicClient {
|
|
||||||
parent_instance: glib::gobject_ffi::GObject,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreySubsonicClientClass {
|
|
||||||
parent_class: glib::gobject_ffi::GObjectClass,
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
pub fn audrey_subsonic_client_get_type() -> glib::ffi::GType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct Client(Object<ffi::AudreySubsonicClient, ffi::AudreySubsonicClientClass>);
|
|
||||||
|
|
||||||
match fn {
|
|
||||||
type_ => || ffi::audrey_subsonic_client_get_type(),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
mod ffi {
|
|
||||||
use gtk::glib;
|
|
||||||
use std::ffi::*;
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
pub struct AudreySubsonicSong {
|
|
||||||
pub id: *mut c_char,
|
|
||||||
pub title: *mut c_char,
|
|
||||||
pub album: *mut c_char,
|
|
||||||
pub artist: *mut c_char,
|
|
||||||
pub track: i64,
|
|
||||||
pub year: i64,
|
|
||||||
pub starred: *mut glib::ffi::GDateTime,
|
|
||||||
pub duration: i64,
|
|
||||||
pub play_count: i64,
|
|
||||||
pub genre: *mut c_char,
|
|
||||||
pub cover_art: *mut c_char,
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
pub fn audrey_subsonic_song_copy(ptr: *const AudreySubsonicSong)
|
|
||||||
-> *mut AudreySubsonicSong;
|
|
||||||
pub fn audrey_subsonic_song_free(ptr: *mut AudreySubsonicSong);
|
|
||||||
pub fn audrey_subsonic_song_get_type() -> glib::ffi::GType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct Song(BoxedInline<ffi::AudreySubsonicSong>);
|
|
||||||
|
|
||||||
match fn {
|
|
||||||
copy => |ptr| ffi::audrey_subsonic_song_copy(ptr),
|
|
||||||
free => |ptr| ffi::audrey_subsonic_song_free(ptr),
|
|
||||||
type_ => || ffi::audrey_subsonic_song_get_type(),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
mod window;
|
|
||||||
pub use window::Window;
|
|
||||||
|
|
||||||
mod playbar;
|
|
||||||
pub use playbar::Playbar;
|
|
12
src/ui/meson.build
Normal file
12
src/ui/meson.build
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
blueprints = custom_target(
|
||||||
|
'blueprints',
|
||||||
|
input: files('play_queue.blp', 'play_queue_song.blp', 'setup.blp', 'window.blp'),
|
||||||
|
output: ['play_queue.ui', 'play_queue_song.ui', 'setup.ui', 'window.ui'],
|
||||||
|
command: [
|
||||||
|
find_program('blueprint-compiler'),
|
||||||
|
'batch-compile',
|
||||||
|
'@OUTDIR@',
|
||||||
|
'@CURRENT_SOURCE_DIR@',
|
||||||
|
'@INPUT@',
|
||||||
|
],
|
||||||
|
)
|
36
src/ui/play_queue.blp
Normal file
36
src/ui/play_queue.blp
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
using Adw 1;
|
||||||
|
|
||||||
|
template $UiPlayQueue: Adw.NavigationPage {
|
||||||
|
title: _("Play queue");
|
||||||
|
|
||||||
|
Adw.ToolbarView {
|
||||||
|
[top]
|
||||||
|
Adw.HeaderBar {
|
||||||
|
Button {
|
||||||
|
icon-name: "edit-clear-all";
|
||||||
|
clicked => $on_clear ();
|
||||||
|
sensitive: bind template.can_clear_all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrolledWindow {
|
||||||
|
ListView view {
|
||||||
|
show-separators: true;
|
||||||
|
single-click-activate: true;
|
||||||
|
|
||||||
|
activate => $on_row_activated ();
|
||||||
|
|
||||||
|
model: NoSelection {
|
||||||
|
model: bind template.playbin as <$Playbin>.play_queue;
|
||||||
|
};
|
||||||
|
|
||||||
|
factory: SignalListItemFactory {
|
||||||
|
setup => $on_song_list_setup ();
|
||||||
|
bind => $on_song_list_bind ();
|
||||||
|
unbind => $on_song_list_unbind ();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +0,0 @@
|
||||||
mod song;
|
|
||||||
pub use song::Song;
|
|
|
@ -1,28 +1,22 @@
|
||||||
// song widget+drag behavior taken from gnome music
|
[GtkTemplate (ui = "/eu/callcc/audrey/ui/play_queue_song.ui")]
|
||||||
|
class Ui.PlayQueueSong : Gtk.ListBoxRow {
|
||||||
[GtkTemplate (ui = "/eu/callcc/audrey/play_queue_song.ui")]
|
|
||||||
class Audrey.Ui.PlayQueueSong : Gtk.Box {
|
|
||||||
public bool draggable { get; set; default = false; }
|
|
||||||
public bool show_position { get; set; default = false; }
|
|
||||||
public bool show_artist { get; set; default = false; }
|
|
||||||
public bool show_cover { get; set; default = false; }
|
|
||||||
|
|
||||||
private bool _current = false;
|
|
||||||
public bool current {
|
public bool current {
|
||||||
get { return _current; }
|
|
||||||
set {
|
set {
|
||||||
this._current = value;
|
|
||||||
if (value) {
|
if (value) {
|
||||||
|
this.play_icon_name = "media-playback-start";
|
||||||
this.add_css_class ("playing");
|
this.add_css_class ("playing");
|
||||||
} else {
|
} else {
|
||||||
|
this.play_icon_name = "";
|
||||||
this.remove_css_class ("playing");
|
this.remove_css_class ("playing");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public uint displayed_position { get; set; }
|
public uint displayed_position { get; set; }
|
||||||
public PlaybinSong song { get; set; }
|
public Subsonic.Song song { get; set; }
|
||||||
|
|
||||||
private weak Playbin playbin;
|
public string play_icon_name { get; set; default = ""; }
|
||||||
|
|
||||||
|
private Playbin playbin;
|
||||||
public PlayQueueSong (Playbin playbin) {
|
public PlayQueueSong (Playbin playbin) {
|
||||||
this.playbin = playbin;
|
this.playbin = playbin;
|
||||||
|
|
||||||
|
@ -30,7 +24,7 @@ class Audrey.Ui.PlayQueueSong : Gtk.Box {
|
||||||
|
|
||||||
var remove = new SimpleAction ("remove", null);
|
var remove = new SimpleAction ("remove", null);
|
||||||
remove.activate.connect (() => {
|
remove.activate.connect (() => {
|
||||||
this.playbin.remove_track (this.displayed_position-1);
|
this.playbin.play_queue.remove (this.displayed_position-1);
|
||||||
});
|
});
|
||||||
action_group.add_action (remove);
|
action_group.add_action (remove);
|
||||||
|
|
||||||
|
@ -38,15 +32,13 @@ class Audrey.Ui.PlayQueueSong : Gtk.Box {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ulong connection;
|
private ulong connection;
|
||||||
public void bind (uint position, PlaybinSong song) {
|
public void bind (uint position, Subsonic.Song song) {
|
||||||
this.displayed_position = position+1;
|
this.displayed_position = position+1;
|
||||||
this.song = song;
|
this.song = song;
|
||||||
this.current = this.playbin.play_queue_position == position;
|
this.current = this.playbin.play_queue_position == position;
|
||||||
this.connection = this.playbin.notify["play-queue-position"].connect (() => {
|
this.connection = this.playbin.notify["play-queue-position"].connect (() => {
|
||||||
this.current = this.playbin.play_queue_position == position;
|
this.current = this.playbin.play_queue_position == position;
|
||||||
});
|
});
|
||||||
|
|
||||||
song.need_cover_art ();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unbind () {
|
public void unbind () {
|
||||||
|
@ -61,59 +53,20 @@ class Audrey.Ui.PlayQueueSong : Gtk.Box {
|
||||||
return starred == null ? "non-starred" : "starred";
|
return starred == null ? "non-starred" : "starred";
|
||||||
}
|
}
|
||||||
|
|
||||||
private double drag_x;
|
|
||||||
private double drag_y;
|
|
||||||
|
|
||||||
[GtkCallback] private Gdk.ContentProvider? on_drag_prepare (double x, double y) {
|
[GtkCallback] private Gdk.ContentProvider? on_drag_prepare (double x, double y) {
|
||||||
if (this.draggable) {
|
|
||||||
this.drag_x = x;
|
|
||||||
this.drag_y = y;
|
|
||||||
return new Gdk.ContentProvider.for_value (this);
|
return new Gdk.ContentProvider.for_value (this);
|
||||||
}
|
}
|
||||||
else return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Gtk.ListBox? drag_widget;
|
|
||||||
|
|
||||||
[GtkCallback] private void on_drag_begin (Gtk.DragSource source, Gdk.Drag drag) {
|
|
||||||
this.drag_widget = new Gtk.ListBox ();
|
|
||||||
|
|
||||||
var drag_row = new PlayQueueSong (this.playbin);
|
|
||||||
drag_row.draggable = false;
|
|
||||||
drag_row.show_position = this.show_position;
|
|
||||||
drag_row.show_artist = this.show_artist;
|
|
||||||
drag_row.show_cover = this.show_cover;
|
|
||||||
drag_row.current = false;
|
|
||||||
drag_row.displayed_position = this.displayed_position;
|
|
||||||
drag_row.song = this.song;
|
|
||||||
drag_row.set_size_request (this.get_width (), this.get_height ());
|
|
||||||
|
|
||||||
var drag_row_real = new Gtk.ListBoxRow ();
|
|
||||||
drag_row_real.child = drag_row;
|
|
||||||
|
|
||||||
this.drag_widget.append (drag_row_real);
|
|
||||||
this.drag_widget.drag_highlight_row (drag_row_real);
|
|
||||||
|
|
||||||
var drag_icon = Gtk.DragIcon.get_for_drag (drag);
|
|
||||||
drag_icon.set("child", this.drag_widget);
|
|
||||||
drag.set_hotspot ((int) this.drag_x, (int) this.drag_y);
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private bool on_drop (Value value, double x, double y) {
|
[GtkCallback] private bool on_drop (Value value, double x, double y) {
|
||||||
this.drag_widget = null;
|
|
||||||
this.drag_x = 0.0;
|
|
||||||
this.drag_y = 0.0;
|
|
||||||
|
|
||||||
var source = value as PlayQueueSong;
|
var source = value as PlayQueueSong;
|
||||||
debug ("dropped %u on %u", source.displayed_position, this.displayed_position);
|
print ("dropped %u on %u", source.displayed_position, this.displayed_position);
|
||||||
this.playbin.move_track (source.displayed_position-1, this.displayed_position-1);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkTemplate (ui = "/eu/callcc/audrey/play_queue.ui")]
|
[GtkTemplate (ui = "/eu/callcc/audrey/ui/play_queue.ui")]
|
||||||
public class Audrey.Ui.PlayQueue : Adw.Bin {
|
public class Ui.PlayQueue : Adw.NavigationPage {
|
||||||
private weak Playbin _playbin;
|
private Playbin _playbin;
|
||||||
public Playbin playbin {
|
public Playbin playbin {
|
||||||
get { return _playbin; }
|
get { return _playbin; }
|
||||||
set {
|
set {
|
||||||
|
@ -122,14 +75,17 @@ public class Audrey.Ui.PlayQueue : Adw.Bin {
|
||||||
|
|
||||||
_playbin.play_queue.items_changed.connect (this.on_store_items_changed);
|
_playbin.play_queue.items_changed.connect (this.on_store_items_changed);
|
||||||
this.can_clear_all = _playbin.play_queue.get_n_items () > 0;
|
this.can_clear_all = _playbin.play_queue.get_n_items () > 0;
|
||||||
|
|
||||||
|
_playbin.notify["play-queue-position"].connect (() => {
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool can_clear_all { get; private set; }
|
public bool can_clear_all { get; private set; }
|
||||||
|
|
||||||
/*[GtkCallback] private void on_clear () {
|
[GtkCallback] private void on_clear () {
|
||||||
this.playbin.clear ();
|
this.playbin.play_queue.remove_all ();
|
||||||
}*/
|
}
|
||||||
|
|
||||||
private void on_store_items_changed (GLib.ListModel store, uint position, uint removed, uint added) {
|
private void on_store_items_changed (GLib.ListModel store, uint position, uint removed, uint added) {
|
||||||
this.can_clear_all = store.get_n_items () > 0;
|
this.can_clear_all = store.get_n_items () > 0;
|
||||||
|
@ -139,11 +95,6 @@ public class Audrey.Ui.PlayQueue : Adw.Bin {
|
||||||
var item = object as Gtk.ListItem;
|
var item = object as Gtk.ListItem;
|
||||||
var child = new PlayQueueSong (this.playbin);
|
var child = new PlayQueueSong (this.playbin);
|
||||||
|
|
||||||
child.draggable = true;
|
|
||||||
child.show_position = true;
|
|
||||||
child.show_artist = true;
|
|
||||||
child.show_cover = true;
|
|
||||||
|
|
||||||
item.child = child;
|
item.child = child;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +102,7 @@ public class Audrey.Ui.PlayQueue : Adw.Bin {
|
||||||
var item = object as Gtk.ListItem;
|
var item = object as Gtk.ListItem;
|
||||||
var child = item.child as PlayQueueSong;
|
var child = item.child as PlayQueueSong;
|
||||||
|
|
||||||
child.bind (item.position, item.item as PlaybinSong);
|
child.bind (item.position, item.item as Subsonic.Song);
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private void on_song_list_unbind (Gtk.SignalListItemFactory factory, Object object) {
|
[GtkCallback] private void on_song_list_unbind (Gtk.SignalListItemFactory factory, Object object) {
|
||||||
|
@ -164,12 +115,4 @@ public class Audrey.Ui.PlayQueue : Adw.Bin {
|
||||||
[GtkCallback] private void on_row_activated (uint position) {
|
[GtkCallback] private void on_row_activated (uint position) {
|
||||||
playbin.select_track (position);
|
playbin.select_track (position);
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private string visible_child_name (uint n_items) {
|
|
||||||
return n_items > 0 ? "not-empty" : "empty";
|
|
||||||
}
|
|
||||||
|
|
||||||
~PlayQueue () {
|
|
||||||
debug ("destroying play queue widget");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
mod imp {
|
|
||||||
use std::cell::RefCell;
|
|
||||||
//use crate::playbin;
|
|
||||||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
|
||||||
|
|
||||||
#[derive(gtk::CompositeTemplate, glib::Properties, Default)]
|
|
||||||
#[template(resource = "/eu/callcc/audrey/play_queue_song.ui")]
|
|
||||||
#[properties(wrapper_type = super::Song)]
|
|
||||||
pub struct Song {
|
|
||||||
#[property(get, set, default = false)]
|
|
||||||
draggable: RefCell<bool>,
|
|
||||||
#[property(get, set, default = false)]
|
|
||||||
show_position: RefCell<bool>,
|
|
||||||
#[property(get, set, default = false)]
|
|
||||||
show_artist: RefCell<bool>,
|
|
||||||
#[property(get, set, default = false)]
|
|
||||||
show_cover: RefCell<bool>,
|
|
||||||
#[property(get, set = Self::set_current, default = false)]
|
|
||||||
current: RefCell<bool>,
|
|
||||||
#[property(get, set)]
|
|
||||||
displayed_position: RefCell<u32>,
|
|
||||||
//#[property(get, set)]
|
|
||||||
//song: playbin::Song,
|
|
||||||
|
|
||||||
//playbin: playbin::Playbin,
|
|
||||||
connection: RefCell<u64>,
|
|
||||||
drag_x: RefCell<f64>,
|
|
||||||
drag_y: RefCell<f64>,
|
|
||||||
drag_widget: RefCell<Option<gtk::ListBox>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[glib::object_subclass]
|
|
||||||
impl ObjectSubclass for Song {
|
|
||||||
const NAME: &'static str = "AudreyUiPlayQueueSong";
|
|
||||||
type Type = super::Song;
|
|
||||||
type ParentType = gtk::Box;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[glib::derived_properties]
|
|
||||||
impl ObjectImpl for Song {}
|
|
||||||
|
|
||||||
impl WidgetImpl for Song {}
|
|
||||||
|
|
||||||
impl BoxImpl for Song {}
|
|
||||||
|
|
||||||
#[gtk::template_callbacks]
|
|
||||||
impl Song {
|
|
||||||
fn set_current(&self, value: bool) {
|
|
||||||
*self.current.borrow_mut() = value;
|
|
||||||
if value {
|
|
||||||
self.obj().add_css_class("playing");
|
|
||||||
} else {
|
|
||||||
self.obj().remove_css_class("playing");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct Song(ObjectSubclass<imp::Song>)
|
|
||||||
@extends gtk::Box, gtk::Widget,
|
|
||||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
|
|
||||||
}
|
|
145
src/ui/play_queue_song.blp
Normal file
145
src/ui/play_queue_song.blp
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
|
||||||
|
template $UiPlayQueueSong: ListBoxRow {
|
||||||
|
selectable: false;
|
||||||
|
|
||||||
|
Box {
|
||||||
|
focusable: false;
|
||||||
|
spacing: 6;
|
||||||
|
|
||||||
|
Image {
|
||||||
|
//visible: false;
|
||||||
|
icon-name: "list-drag-handle";
|
||||||
|
styles [ "drag-handle" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
Box {
|
||||||
|
width-request: 48;
|
||||||
|
focusable: false;
|
||||||
|
homogeneous: true;
|
||||||
|
|
||||||
|
Image {
|
||||||
|
focusable: false;
|
||||||
|
icon-size: normal;
|
||||||
|
icon-name: bind template.play-icon-name;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
focusable: false;
|
||||||
|
halign: end;
|
||||||
|
justify: right;
|
||||||
|
styles [ "dim-label", "numeric" ]
|
||||||
|
|
||||||
|
label: bind template.displayed_position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box title_box {
|
||||||
|
focusable: false;
|
||||||
|
hexpand: true;
|
||||||
|
|
||||||
|
Label {
|
||||||
|
focusable: false;
|
||||||
|
xalign: 0;
|
||||||
|
halign: start;
|
||||||
|
hexpand: true;
|
||||||
|
ellipsize: end;
|
||||||
|
max-width-chars: 90;
|
||||||
|
justify: fill;
|
||||||
|
margin-start: 9;
|
||||||
|
|
||||||
|
label: bind template.song as <$SubsonicSong>.title;
|
||||||
|
styles [ "title-label" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box artist_box {
|
||||||
|
//visible: false;
|
||||||
|
focusable: false;
|
||||||
|
hexpand: true;
|
||||||
|
|
||||||
|
Label {
|
||||||
|
focusable: false;
|
||||||
|
xalign: 0;
|
||||||
|
halign: start;
|
||||||
|
hexpand: true;
|
||||||
|
ellipsize: end;
|
||||||
|
max-width-chars: 90;
|
||||||
|
justify: fill;
|
||||||
|
margin-start: 9;
|
||||||
|
|
||||||
|
label: bind template.song as <$SubsonicSong>.artist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box album_duration_box {
|
||||||
|
focusable: false;
|
||||||
|
hexpand: true;
|
||||||
|
spacing: 6;
|
||||||
|
|
||||||
|
Label {
|
||||||
|
//visible: false;
|
||||||
|
focusable: false;
|
||||||
|
xalign: 0;
|
||||||
|
halign: start;
|
||||||
|
hexpand: true;
|
||||||
|
ellipsize: end;
|
||||||
|
max-width-chars: 90;
|
||||||
|
justify: fill;
|
||||||
|
|
||||||
|
label: bind template.song as <$SubsonicSong>.album;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
focusable: false;
|
||||||
|
halign: end;
|
||||||
|
hexpand: true;
|
||||||
|
single-line-mode: true;
|
||||||
|
styles [ "numeric" ]
|
||||||
|
|
||||||
|
label: bind $format_duration (template.song as <$SubsonicSong>.duration) as <string>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
focusable: true;
|
||||||
|
icon-name: bind $star_button_icon_name (template.song as <$SubsonicSong>.starred) as <string>;
|
||||||
|
styles [ "flat" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuButton {
|
||||||
|
//visible: false;
|
||||||
|
focusable: true;
|
||||||
|
icon-name: "view-more";
|
||||||
|
styles [ "flat" ]
|
||||||
|
|
||||||
|
popover: PopoverMenu {
|
||||||
|
menu-model: song-menu;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DragSource {
|
||||||
|
actions: move;
|
||||||
|
propagation-phase: capture;
|
||||||
|
|
||||||
|
prepare => $on_drag_prepare ();
|
||||||
|
}
|
||||||
|
|
||||||
|
DropTarget {
|
||||||
|
actions: move;
|
||||||
|
formats: UiPlayQueueSong;
|
||||||
|
preload: true;
|
||||||
|
|
||||||
|
drop => $on_drop ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeGroup {
|
||||||
|
mode: horizontal;
|
||||||
|
widgets [title_box, artist_box, album_duration_box]
|
||||||
|
}
|
||||||
|
|
||||||
|
menu song-menu {
|
||||||
|
item ("Remove", "song.remove")
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
typedef struct _AudreyUiPlaybar AudreyUiPlaybar;
|
|
|
@ -1,203 +0,0 @@
|
||||||
mod imp {
|
|
||||||
use adw::prelude::*;
|
|
||||||
use adw::subclass::prelude::*;
|
|
||||||
use glib::subclass::InitializingObject;
|
|
||||||
use gtk::{gdk, glib};
|
|
||||||
use std::cell::{Cell, RefCell};
|
|
||||||
|
|
||||||
#[derive(glib::Properties, gtk::CompositeTemplate, Default)]
|
|
||||||
#[properties(wrapper_type = super::Playbar)]
|
|
||||||
#[template(resource = "/eu/callcc/audrey/playbar.ui")]
|
|
||||||
pub struct Playbar {
|
|
||||||
#[property(get, set)]
|
|
||||||
song: RefCell<Option<crate::playbin::Song>>,
|
|
||||||
#[property(get, set)]
|
|
||||||
playing_cover_art: RefCell<Option<gdk::Paintable>>,
|
|
||||||
#[property(get, set)]
|
|
||||||
playbin: RefCell<Option<crate::Playbin>>, // TODO: weak
|
|
||||||
#[property(get, set, default = true)]
|
|
||||||
show_cover_art: Cell<bool>,
|
|
||||||
|
|
||||||
#[property(get = Self::get_volume, set = Self::set_volume)]
|
|
||||||
_volume: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[glib::object_subclass]
|
|
||||||
impl ObjectSubclass for Playbar {
|
|
||||||
const NAME: &'static str = "AudreyUiPlaybar";
|
|
||||||
type Type = super::Playbar;
|
|
||||||
type ParentType = adw::Bin;
|
|
||||||
|
|
||||||
fn class_init(klass: &mut Self::Class) {
|
|
||||||
klass.bind_template();
|
|
||||||
klass.bind_template_callbacks();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn instance_init(obj: &InitializingObject<Self>) {
|
|
||||||
obj.init_template();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[glib::derived_properties]
|
|
||||||
impl ObjectImpl for Playbar {}
|
|
||||||
|
|
||||||
impl WidgetImpl for Playbar {}
|
|
||||||
|
|
||||||
impl BinImpl for Playbar {}
|
|
||||||
|
|
||||||
#[gtk::template_callbacks]
|
|
||||||
impl Playbar {
|
|
||||||
#[template_callback]
|
|
||||||
fn song_title(&self, song: Option<&crate::playbin::Song>) -> String {
|
|
||||||
match song {
|
|
||||||
None => "".to_owned(),
|
|
||||||
Some(song) => song.get_title().to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn song_artist(&self, song: Option<&crate::playbin::Song>) -> String {
|
|
||||||
match song {
|
|
||||||
None => "".to_owned(),
|
|
||||||
Some(song) => song.get_artist().to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn song_album(&self, song: Option<&crate::playbin::Song>) -> String {
|
|
||||||
match song {
|
|
||||||
None => "".to_owned(),
|
|
||||||
Some(song) => song.get_album().to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn format_timestamp(&self, s: f64) -> String {
|
|
||||||
format!("{:02}:{:02}", (s as i64) / 64, (s as i64) % 60)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn playbin_active(&self, state: crate::playbin::State) -> bool {
|
|
||||||
state != crate::playbin::State::Stopped
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn can_press_play(&self, state: crate::playbin::State, n_items: u32) -> bool {
|
|
||||||
!(state == crate::playbin::State::Stopped && n_items == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn play_pause_icon_name(&self, state: crate::playbin::State) -> &'static str {
|
|
||||||
match state {
|
|
||||||
crate::playbin::State::Playing => "media-playback-pause",
|
|
||||||
_ => "media-playback-start",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn mute_button_icon_name(&self, mute: bool) -> &'static str {
|
|
||||||
if mute {
|
|
||||||
"audio-volume-muted"
|
|
||||||
} else {
|
|
||||||
"audio-volume-high"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn on_play_position_seek(
|
|
||||||
&self,
|
|
||||||
_scroll_type: gtk::ScrollType,
|
|
||||||
value: f64,
|
|
||||||
range: >k::Range,
|
|
||||||
) -> bool {
|
|
||||||
let playbin = self.playbin.borrow();
|
|
||||||
let playbin = playbin.as_ref().unwrap();
|
|
||||||
if range.adjustment().lower() < range.adjustment().upper() {
|
|
||||||
playbin.seek(value);
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn on_skip_forward_clicked(&self) {
|
|
||||||
// this.playbin.go_to_next_track ();
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn on_skip_backward_clicked(&self) {
|
|
||||||
// this.playbin.go_to_prev_track ();
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn seek_backward(&self) {
|
|
||||||
// 10 seconds
|
|
||||||
// double new_position = playbin.position - 10.0;
|
|
||||||
// if (new_position < 0.0) new_position = 0.0;
|
|
||||||
// this.playbin.seek (new_position);
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn seek_forward(&self) {
|
|
||||||
// 10 seconds
|
|
||||||
// double new_position = playbin.position + 10.0;
|
|
||||||
// if (new_position > this.playbin.duration) new_position = this.playbin.duration;
|
|
||||||
// this.playbin.seek (new_position);
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn on_play_pause_clicked(&self, _button: >k::Button) {
|
|
||||||
let playbin = self.playbin.borrow();
|
|
||||||
let playbin = playbin.as_ref().unwrap();
|
|
||||||
|
|
||||||
if playbin.get_state() == crate::playbin::State::Playing {
|
|
||||||
playbin.pause();
|
|
||||||
} else {
|
|
||||||
playbin.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[template_callback]
|
|
||||||
fn on_mute_toggle(&self) {
|
|
||||||
//this.playbin.mute = !this.playbin.mute;
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_volume(&self) -> i32 {
|
|
||||||
let playbin = self.playbin.borrow();
|
|
||||||
|
|
||||||
match playbin.as_ref() {
|
|
||||||
None => 100,
|
|
||||||
Some(playbin) => playbin.get_volume(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_volume(&self, value: i32) {
|
|
||||||
let playbin = self.playbin.borrow();
|
|
||||||
let playbin = playbin.as_ref().unwrap();
|
|
||||||
playbin.set_volume(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct Playbar(ObjectSubclass<imp::Playbar>)
|
|
||||||
@extends adw::Bin, gtk::Widget,
|
|
||||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
mod ffi {
|
|
||||||
use glib::translate::IntoGlib;
|
|
||||||
use gtk::glib;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn audrey_ui_playbar_get_type() -> glib::ffi::GType {
|
|
||||||
super::Playbar::static_type().into_glib()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
// [GtkTemplate (ui = "/eu/callcc/audrey/playbar.ui")]
|
|
||||||
[CCode (cheader_filename = "ui/playbar.h")]
|
|
||||||
public class Audrey.Ui.Playbar : Adw.Bin {
|
|
||||||
public PlaybinSong? song {
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
public Gdk.Paintable? playing_cover_art { get; set; }
|
|
||||||
public weak Playbin playbin { get; set; }
|
|
||||||
public bool show_cover_art { get; set; /*default = true;*/ }
|
|
||||||
|
|
||||||
public int volume { get; set; }
|
|
||||||
/*{
|
|
||||||
get { return playbin == null ? 100 : playbin.volume; }
|
|
||||||
set { playbin.volume = value; }
|
|
||||||
}*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
[GtkCallback] private string format_timestamp (double s) {
|
|
||||||
return "%02d:%02d".printf (((int) s)/60, ((int) s)%60);
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private bool on_play_position_seek (Gtk.Range range, Gtk.ScrollType scroll_type, double value) {
|
|
||||||
if (range.adjustment.lower < range.adjustment.upper) {
|
|
||||||
this.playbin.seek ((int64) value);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private void on_play_pause_clicked () {
|
|
||||||
if (this.playbin.state == PlaybinState.PLAYING) {
|
|
||||||
this.playbin.pause();
|
|
||||||
} else {
|
|
||||||
this.playbin.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private string play_pause_icon_name (PlaybinState state) {
|
|
||||||
if (state == PlaybinState.PLAYING) {
|
|
||||||
return "media-playback-pause";
|
|
||||||
} else {
|
|
||||||
return "media-playback-start";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private bool playbin_active (PlaybinState state) {
|
|
||||||
return state != PlaybinState.STOPPED;
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private bool can_press_play (PlaybinState state, uint n_items) {
|
|
||||||
return !(state == PlaybinState.STOPPED && n_items == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private string mute_button_icon_name (bool mute) {
|
|
||||||
return mute ? "audio-volume-muted" : "audio-volume-high";
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private void on_mute_toggle () {
|
|
||||||
this.playbin.mute = !this.playbin.mute;
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private void on_skip_forward_clicked () {
|
|
||||||
this.playbin.go_to_next_track ();
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private void on_skip_backward_clicked () {
|
|
||||||
this.playbin.go_to_prev_track ();
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private void seek_backward () {
|
|
||||||
// 10 seconds
|
|
||||||
double new_position = playbin.position - 10.0;
|
|
||||||
if (new_position < 0.0) new_position = 0.0;
|
|
||||||
this.playbin.seek (new_position);
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private void seek_forward () {
|
|
||||||
// 10 seconds
|
|
||||||
double new_position = playbin.position + 10.0;
|
|
||||||
if (new_position > this.playbin.duration) new_position = this.playbin.duration;
|
|
||||||
this.playbin.seek (new_position);
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private string song_title (PlaybinSong? song) {
|
|
||||||
return song == null ? "" : song.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private string song_artist (PlaybinSong? song) {
|
|
||||||
return song == null ? "" : song.artist;
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private string song_album (PlaybinSong? song) {
|
|
||||||
return song == null ? "" : song.album;
|
|
||||||
}
|
|
||||||
|
|
||||||
~Playbar () {
|
|
||||||
debug ("destroying playbar widget");
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
using Gtk 4.0;
|
using Gtk 4.0;
|
||||||
using Adw 1;
|
using Adw 1;
|
||||||
|
|
||||||
template $AudreyUiSetup: Adw.PreferencesDialog {
|
template $UiSetup: Adw.PreferencesDialog {
|
||||||
title: _("Setup");
|
title: _("Setup");
|
||||||
|
|
||||||
Adw.ToolbarView {
|
Adw.ToolbarView {
|
|
@ -16,8 +16,8 @@ static void salt_password (string password, out string token, out string salt) {
|
||||||
salt = (string) salt_chars;
|
salt = (string) salt_chars;
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkTemplate (ui = "/eu/callcc/audrey/setup.ui")]
|
[GtkTemplate (ui = "/eu/callcc/audrey/ui/setup.ui")]
|
||||||
public class Audrey.Ui.Setup : Adw.PreferencesDialog {
|
public class Ui.Setup : Adw.PreferencesDialog {
|
||||||
public string status { get; private set; default = _("Not connected"); }
|
public string status { get; private set; default = _("Not connected"); }
|
||||||
|
|
||||||
public bool authn_can_edit { get; private set; default = true; }
|
public bool authn_can_edit { get; private set; default = true; }
|
||||||
|
@ -78,7 +78,7 @@ public class Audrey.Ui.Setup : Adw.PreferencesDialog {
|
||||||
Secret.password_searchv.begin (
|
Secret.password_searchv.begin (
|
||||||
secret_schema,
|
secret_schema,
|
||||||
new HashTable<string, string> (null, null),
|
new HashTable<string, string> (null, null),
|
||||||
Secret.SearchFlags.UNLOCK,
|
Secret.SearchFlags.NONE,
|
||||||
null,
|
null,
|
||||||
(obj, res) => {
|
(obj, res) => {
|
||||||
try {
|
try {
|
||||||
|
@ -128,8 +128,4 @@ public class Audrey.Ui.Setup : Adw.PreferencesDialog {
|
||||||
this.authn_can_edit = true;
|
this.authn_can_edit = true;
|
||||||
}, "server-url", this.server_url, "username", this.username);
|
}, "server-url", this.server_url, "username", this.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
~Setup () {
|
|
||||||
debug ("destroying setup dialog");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
250
src/ui/window.blp
Normal file
250
src/ui/window.blp
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
using Adw 1;
|
||||||
|
|
||||||
|
template $UiWindow: Adw.ApplicationWindow {
|
||||||
|
title: _("audrey");
|
||||||
|
default-width: 800;
|
||||||
|
default-height: 600;
|
||||||
|
|
||||||
|
content: Adw.BottomSheet bottom_sheet {
|
||||||
|
can-open: false; // broken in libadwaita
|
||||||
|
|
||||||
|
[content]
|
||||||
|
Adw.OverlaySplitView {
|
||||||
|
margin-bottom: bind bottom_sheet.bottom-bar-height;
|
||||||
|
|
||||||
|
vexpand: true;
|
||||||
|
|
||||||
|
[sidebar]
|
||||||
|
Adw.NavigationPage {
|
||||||
|
width-request: 100;
|
||||||
|
title: _("audrey");
|
||||||
|
|
||||||
|
Adw.ToolbarView {
|
||||||
|
[top]
|
||||||
|
Adw.HeaderBar {
|
||||||
|
[end]
|
||||||
|
Button {
|
||||||
|
icon-name: "applications-system";
|
||||||
|
clicked => $show_setup_dialog ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Box {
|
||||||
|
orientation: vertical;
|
||||||
|
|
||||||
|
ListBox sidebar {
|
||||||
|
styles [
|
||||||
|
"navigation-sidebar",
|
||||||
|
]
|
||||||
|
|
||||||
|
row-activated => $on_sidebar_row_activated();
|
||||||
|
|
||||||
|
ListBoxRow sidebar_play_queue {
|
||||||
|
Label {
|
||||||
|
xalign: 0;
|
||||||
|
label: _("Play queue");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Separator {}
|
||||||
|
|
||||||
|
ListBox {
|
||||||
|
selection-mode: none;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"navigation-sidebar",
|
||||||
|
]
|
||||||
|
|
||||||
|
Adw.ButtonRow shuffle_all_tracks {
|
||||||
|
title: _("Shuffle all tracks");
|
||||||
|
start-icon-name: "media-playlist-shuffle";
|
||||||
|
sensitive: false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Separator {
|
||||||
|
styles [
|
||||||
|
"spacer",
|
||||||
|
]
|
||||||
|
vexpand: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Picture {
|
||||||
|
paintable: bind template.playing_cover_art;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[content]
|
||||||
|
Stack stack {
|
||||||
|
StackPage {
|
||||||
|
name: "play_queue";
|
||||||
|
title: _("Play queue");
|
||||||
|
|
||||||
|
child: $UiPlayQueue play_queue {
|
||||||
|
playbin: bind template.playbin;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[sheet]
|
||||||
|
Box {}
|
||||||
|
|
||||||
|
[bottom-bar]
|
||||||
|
CenterBox {
|
||||||
|
styles [
|
||||||
|
"toolbar",
|
||||||
|
]
|
||||||
|
|
||||||
|
[start]
|
||||||
|
Box {
|
||||||
|
orientation: vertical;
|
||||||
|
valign: center;
|
||||||
|
|
||||||
|
Label {
|
||||||
|
styles [ "heading" ]
|
||||||
|
xalign: 0;
|
||||||
|
halign: start;
|
||||||
|
label: bind $song_title (template.song) as <string>;
|
||||||
|
ellipsize: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
styles [ "caption" ]
|
||||||
|
xalign: 0;
|
||||||
|
label: bind $song_artist (template.song) as <string>;
|
||||||
|
ellipsize: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
styles [ "caption" ]
|
||||||
|
xalign: 0;
|
||||||
|
label: bind $song_album (template.song) as <string>;
|
||||||
|
ellipsize: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[center]
|
||||||
|
Box {
|
||||||
|
orientation: vertical;
|
||||||
|
halign: center;
|
||||||
|
hexpand: true;
|
||||||
|
|
||||||
|
CenterBox {
|
||||||
|
[start]
|
||||||
|
Label play_position_label {
|
||||||
|
styles [
|
||||||
|
"caption",
|
||||||
|
"numeric",
|
||||||
|
]
|
||||||
|
|
||||||
|
label: bind $format_timestamp (template.playbin as <$Playbin>.position) as <string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
[center]
|
||||||
|
Scale play_position {
|
||||||
|
name: "seek-scale";
|
||||||
|
orientation: horizontal;
|
||||||
|
width-request: 400;
|
||||||
|
sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as <bool>;
|
||||||
|
|
||||||
|
adjustment: Adjustment {
|
||||||
|
lower: 0;
|
||||||
|
value: bind template.playbin as <$Playbin>.position;
|
||||||
|
upper: bind template.playbin as <$Playbin>.duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
change-value => $on_play_position_seek ();
|
||||||
|
}
|
||||||
|
|
||||||
|
[end]
|
||||||
|
Label play_duration {
|
||||||
|
styles [
|
||||||
|
"caption",
|
||||||
|
"numeric",
|
||||||
|
]
|
||||||
|
|
||||||
|
label: bind $format_timestamp (template.playbin as <$Playbin>.duration) as <string>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box {
|
||||||
|
halign: center;
|
||||||
|
orientation: horizontal;
|
||||||
|
|
||||||
|
Button {
|
||||||
|
icon-name: "media-skip-backward";
|
||||||
|
valign: center;
|
||||||
|
sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as <bool>;
|
||||||
|
|
||||||
|
clicked => $on_skip_backward_clicked ();
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
icon-name: "media-seek-backward";
|
||||||
|
valign: center;
|
||||||
|
sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as <bool>;
|
||||||
|
|
||||||
|
clicked => $seek_backward ();
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
icon-name: bind $play_pause_icon_name (template.playbin as <$Playbin>.state as <$PlaybinState>) as <string>;
|
||||||
|
valign: center;
|
||||||
|
sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as <bool>;
|
||||||
|
|
||||||
|
clicked => $on_play_pause_clicked ();
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
icon-name: "media-seek-forward";
|
||||||
|
valign: center;
|
||||||
|
sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as <bool>;
|
||||||
|
|
||||||
|
clicked => $seek_forward ();
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
icon-name: "media-skip-forward";
|
||||||
|
valign: center;
|
||||||
|
sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as <bool>;
|
||||||
|
|
||||||
|
clicked => $on_skip_forward_clicked ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[end]
|
||||||
|
Box {
|
||||||
|
Button {
|
||||||
|
icon-name: "non-starred";
|
||||||
|
valign: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
icon-name: bind $mute_button_icon_name (template.mute) as <string>;
|
||||||
|
valign: center;
|
||||||
|
|
||||||
|
clicked => $on_mute_toggle ();
|
||||||
|
}
|
||||||
|
|
||||||
|
Scale {
|
||||||
|
name: "volume-scale";
|
||||||
|
orientation: horizontal;
|
||||||
|
width-request: 130;
|
||||||
|
|
||||||
|
adjustment: Adjustment {
|
||||||
|
lower: 0;
|
||||||
|
value: bind template.volume bidirectional;
|
||||||
|
upper: 100;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
mod ffi {
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreyUiWindow {
|
|
||||||
parent_instance: adw::ffi::AdwApplicationWindow,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct AudreyUiWindowClass {
|
|
||||||
parent_class: adw::ffi::AdwApplicationWindowClass,
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
pub fn audrey_ui_window_get_type() -> glib::ffi::GType;
|
|
||||||
pub fn audrey_ui_window_new(app: *mut gtk::ffi::GtkApplication) -> *mut AudreyUiWindow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use adw::prelude::*;
|
|
||||||
use gtk::{gio, glib};
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct Window(Object<ffi::AudreyUiWindow, ffi::AudreyUiWindowClass>)
|
|
||||||
@extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,
|
|
||||||
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
|
|
||||||
|
|
||||||
match fn {
|
|
||||||
type_ => || ffi::audrey_ui_window_get_type(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Window {
|
|
||||||
pub fn new(app: &impl IsA<gtk::Application>) -> Self {
|
|
||||||
use glib::translate::*;
|
|
||||||
|
|
||||||
unsafe { from_glib_none(ffi::audrey_ui_window_new(app.as_ref().to_glib_none().0)) }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,11 @@
|
||||||
[GtkTemplate (ui = "/eu/callcc/audrey/window.ui")]
|
[GtkTemplate (ui = "/eu/callcc/audrey/ui/window.ui")]
|
||||||
class Audrey.Ui.Window : Adw.ApplicationWindow {
|
class Ui.Window : Adw.ApplicationWindow {
|
||||||
[GtkChild] public unowned PlayQueue play_queue;
|
[GtkChild] private unowned Gtk.ListBox sidebar;
|
||||||
[GtkChild] public unowned Playbar playbar;
|
[GtkChild] private unowned Gtk.ListBoxRow sidebar_play_queue;
|
||||||
//[GtkChild] public unowned Adw.ButtonRow shuffle_all_tracks;
|
[GtkChild] private unowned Gtk.Stack stack;
|
||||||
|
|
||||||
|
[GtkChild] public unowned Ui.PlayQueue play_queue;
|
||||||
|
[GtkChild] public unowned Adw.ButtonRow shuffle_all_tracks;
|
||||||
|
|
||||||
private Setup setup;
|
private Setup setup;
|
||||||
|
|
||||||
|
@ -17,11 +20,11 @@ class Audrey.Ui.Window : Adw.ApplicationWindow {
|
||||||
set { this.playbin.mute = value; }
|
set { this.playbin.mute = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlaybinSong? song { get; private set; }
|
public Subsonic.Song? song { get; private set; }
|
||||||
public Gdk.Paintable? playing_cover_art { get; set; default = null; }
|
|
||||||
|
|
||||||
private Cancellable cancel_loading_art;
|
private Cancellable cancel_loading_art;
|
||||||
public bool cover_art_loading { get; set; default = false; }
|
public bool cover_art_loading { get; set; default = false; }
|
||||||
|
public Gdk.Paintable playing_cover_art { get; set; }
|
||||||
|
|
||||||
public Playbin playbin { get; private set; default = new Playbin (); }
|
public Playbin playbin { get; private set; default = new Playbin (); }
|
||||||
|
|
||||||
|
@ -29,23 +32,42 @@ class Audrey.Ui.Window : Adw.ApplicationWindow {
|
||||||
Object (application: app);
|
Object (application: app);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mpris mpris;
|
construct {
|
||||||
private MprisPlayer mpris_player;
|
// TODO: mpris
|
||||||
|
// Bus.own_name (
|
||||||
|
// BusType.SESSION,
|
||||||
|
// "org.mpris.MediaPlayer2.audrey",
|
||||||
|
// BusNameOwnerFlags.NONE,
|
||||||
|
// (conn) => {
|
||||||
|
// try {
|
||||||
|
// } catch (IOError e) {
|
||||||
|
// error ("could not register dbus service: %s", e.message);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// () => {},
|
||||||
|
// () => { error ("could not acquire dbus name"); });
|
||||||
|
|
||||||
private void now_playing (PlaybinSong song) {
|
this.setup = new Setup ();
|
||||||
this.song = song;
|
|
||||||
// api.scrobble.begin (this.song.id); TODO
|
this.setup.connected.connect ((api) => {
|
||||||
|
this.api = api;
|
||||||
|
this.playbin.api = api;
|
||||||
|
|
||||||
|
this.playbin.now_playing.connect ((playbin, now, next) => {
|
||||||
|
this.song = now;
|
||||||
|
api.scrobble.begin (this.song.id);
|
||||||
|
|
||||||
if (this.cancel_loading_art != null) {
|
if (this.cancel_loading_art != null) {
|
||||||
this.cancel_loading_art.cancel ();
|
this.cancel_loading_art.cancel ();
|
||||||
}
|
}
|
||||||
this.cancel_loading_art = new GLib.Cancellable ();
|
this.cancel_loading_art = new GLib.Cancellable ();
|
||||||
|
|
||||||
this.playing_cover_art = null; // TODO: preload next art somehow
|
this.playing_cover_art = Gdk.Paintable.empty (1, 1);
|
||||||
|
if (this.song != null) {
|
||||||
this.cover_art_loading = true;
|
this.cover_art_loading = true;
|
||||||
|
|
||||||
string song_id = this.song.id;
|
string song_id = this.song.id;
|
||||||
this.api.cover_art.begin (song_id, -1, Priority.DEFAULT, this.cancel_loading_art, (obj, res) => {
|
this.api.cover_art.begin (song_id, this.cancel_loading_art, (obj, res) => {
|
||||||
try {
|
try {
|
||||||
this.playing_cover_art = Gdk.Texture.for_pixbuf (this.api.cover_art.end (res));
|
this.playing_cover_art = Gdk.Texture.for_pixbuf (this.api.cover_art.end (res));
|
||||||
this.cover_art_loading = false;
|
this.cover_art_loading = false;
|
||||||
|
@ -57,80 +79,114 @@ class Audrey.Ui.Window : Adw.ApplicationWindow {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
construct {
|
|
||||||
Bus.own_name (
|
|
||||||
BusType.SESSION,
|
|
||||||
"org.mpris.MediaPlayer2.audrey",
|
|
||||||
BusNameOwnerFlags.NONE,
|
|
||||||
(conn) => {
|
|
||||||
try {
|
|
||||||
this.mpris = new Mpris (this);
|
|
||||||
this.mpris_player = new MprisPlayer (conn, this.playbin);
|
|
||||||
|
|
||||||
conn.register_object ("/org/mpris/MediaPlayer2", this.mpris);
|
|
||||||
conn.register_object ("/org/mpris/MediaPlayer2", this.mpris_player);
|
|
||||||
} catch (IOError e) {
|
|
||||||
error ("could not register dbus service: %s", e.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
() => {},
|
|
||||||
() => { error ("could not acquire dbus name"); });
|
|
||||||
|
|
||||||
this.setup = new Setup ();
|
|
||||||
|
|
||||||
this.setup.connected.connect ((api) => {
|
|
||||||
this.api = api;
|
|
||||||
this.playbin.api = api;
|
|
||||||
this.mpris_player.api = api;
|
|
||||||
this.can_click_shuffle_all = true;
|
|
||||||
});
|
|
||||||
this.setup.load ();
|
|
||||||
|
|
||||||
this.playbin.new_track.connect (() => {
|
|
||||||
this.now_playing (this.playbin.play_queue.get_item (this.playbin.play_queue_position) as PlaybinSong);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.playbin.stopped.connect (() => {
|
this.playbin.stopped.connect (() => {
|
||||||
this.playing_cover_art = null;
|
this.playing_cover_art = Gdk.Paintable.empty (1, 1);
|
||||||
this.song = null;
|
this.song = null;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private void show_setup_dialog () {
|
this.shuffle_all_tracks.sensitive = true;
|
||||||
this.setup.present (this);
|
this.shuffle_all_tracks.activated.connect (() => {
|
||||||
}
|
this.shuffle_all_tracks.sensitive = false;
|
||||||
|
this.playbin.play_queue.remove_all ();
|
||||||
public bool can_click_shuffle_all { get; private set; default = false; }
|
|
||||||
|
|
||||||
[GtkCallback] private void shuffle_all () {
|
|
||||||
this.can_click_shuffle_all = false;
|
|
||||||
this.playbin.clear ();
|
|
||||||
api.get_random_songs.begin (null, (song) => {
|
api.get_random_songs.begin (null, (song) => {
|
||||||
this.playbin.append_track (song);
|
this.playbin.play_queue.append (song);
|
||||||
}, (obj, res) => {
|
}, (obj, res) => {
|
||||||
try {
|
try {
|
||||||
api.get_random_songs.end (res);
|
api.get_random_songs.end (res);
|
||||||
} catch (Error e) {
|
} catch (Error e) {
|
||||||
error ("could not get random songs: %s", e.message);
|
error ("could not get random songs: %s", e.message);
|
||||||
}
|
}
|
||||||
this.can_click_shuffle_all = true;
|
this.shuffle_all_tracks.sensitive = true;
|
||||||
|
|
||||||
this.playbin.select_track (0);
|
this.playbin.select_track (0);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.setup.load ();
|
||||||
|
|
||||||
|
this.sidebar.select_row (this.sidebar.get_row_at_index (0));
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private bool show_playbar_cover_art (string? stack_child) {
|
[GtkCallback] private void on_sidebar_row_activated (Gtk.ListBoxRow row) {
|
||||||
return stack_child != "play-queue";
|
if (row == this.sidebar_play_queue) {
|
||||||
|
this.stack.set_visible_child_name("play_queue");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool close_request () {
|
[GtkCallback] private string format_timestamp (double s) {
|
||||||
// stop playback on close
|
return "%02d:%02d".printf (((int) s)/60, ((int) s)%60);
|
||||||
this.playbin.stop ();
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private bool on_play_position_seek (Gtk.Range range, Gtk.ScrollType scroll_type, double value) {
|
||||||
|
this.playbin.seek ((int64) value);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
~Window () {
|
[GtkCallback] private void on_play_pause_clicked () {
|
||||||
debug ("destroying main window");
|
if (this.playbin.state == PlaybinState.PLAYING) {
|
||||||
|
this.playbin.pause();
|
||||||
|
} else {
|
||||||
|
this.playbin.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private string play_pause_icon_name (PlaybinState state) {
|
||||||
|
if (state == PlaybinState.PLAYING) {
|
||||||
|
return "media-playback-pause";
|
||||||
|
} else {
|
||||||
|
return "media-playback-start";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private bool playbin_active (PlaybinState state) {
|
||||||
|
return state != PlaybinState.STOPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private string mute_button_icon_name (bool mute) {
|
||||||
|
return mute ? "audio-volume-muted" : "audio-volume-high";
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private void on_mute_toggle () {
|
||||||
|
this.mute = !this.mute;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private void on_skip_forward_clicked () {
|
||||||
|
this.playbin.next_track ();
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private void on_skip_backward_clicked () {
|
||||||
|
this.playbin.prev_track ();
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private void show_setup_dialog () {
|
||||||
|
this.setup.present (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private void seek_backward () {
|
||||||
|
// 10 seconds
|
||||||
|
double new_position = playbin.position - 10.0;
|
||||||
|
if (new_position < 0.0) new_position = 0.0;
|
||||||
|
this.playbin.seek (new_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private void seek_forward () {
|
||||||
|
// 10 seconds
|
||||||
|
double new_position = playbin.position + 10.0;
|
||||||
|
if (new_position > this.playbin.duration) new_position = this.playbin.duration;
|
||||||
|
this.playbin.seek (new_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private string song_title (Subsonic.Song? song) {
|
||||||
|
return song == null ? "" : song.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private string song_artist (Subsonic.Song? song) {
|
||||||
|
return song == null ? "" : song.artist;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private string song_album (Subsonic.Song? song) {
|
||||||
|
return song == null ? "" : song.album;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ namespace Mpv {
|
||||||
|
|
||||||
public delegate void WakeupCallback ();
|
public delegate void WakeupCallback ();
|
||||||
|
|
||||||
[CCode (cname = "mpv_handle", free_function = "mpv_terminate_destroy")]
|
[CCode (cname = "mpv_handle", free_function = "mpv_destroy")]
|
||||||
[Compact]
|
[Compact]
|
||||||
public class Handle {
|
public class Handle {
|
||||||
[CCode (cname = "mpv_create")]
|
[CCode (cname = "mpv_create")]
|
||||||
|
@ -65,9 +65,6 @@ namespace Mpv {
|
||||||
[CCode (cname = "mpv_command")]
|
[CCode (cname = "mpv_command")]
|
||||||
public Error command ([CCode (array_length = false)] string[] args);
|
public Error command ([CCode (array_length = false)] string[] args);
|
||||||
|
|
||||||
[CCode (cname = "mpv_command_async")]
|
|
||||||
public Error command_async (uint64 reply_userdata, [CCode (array_length = false)] string[] args);
|
|
||||||
|
|
||||||
[CCode (cname = "mpv_observe_property")]
|
[CCode (cname = "mpv_observe_property")]
|
||||||
public Error observe_property (uint64 reply_userdata, string name, Format format);
|
public Error observe_property (uint64 reply_userdata, string name, Format format);
|
||||||
}
|
}
|
||||||
|
@ -104,10 +101,7 @@ namespace Mpv {
|
||||||
PLAYBACK_RESTART,
|
PLAYBACK_RESTART,
|
||||||
PROPERTY_CHANGE,
|
PROPERTY_CHANGE,
|
||||||
QUEUE_OVERFLOW,
|
QUEUE_OVERFLOW,
|
||||||
HOOK;
|
HOOK,
|
||||||
|
|
||||||
[CCode (cname = "mpv_event_name")]
|
|
||||||
public unowned string to_string ();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[CCode (cname = "mpv_event", destroy_function = "", has_type_id = false, has_copy_function = false)]
|
[CCode (cname = "mpv_event", destroy_function = "", has_type_id = false, has_copy_function = false)]
|
||||||
|
@ -149,12 +143,6 @@ namespace Mpv {
|
||||||
{
|
{
|
||||||
return * (double *) data;
|
return * (double *) data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool parse_flag ()
|
|
||||||
requires (format == Format.FLAG)
|
|
||||||
{
|
|
||||||
return (* (int *) data) == 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[CCode (cname = "mpv_event_end_file", destroy_function = "", has_type_id = false, has_copy_function = false)]
|
[CCode (cname = "mpv_event_end_file", destroy_function = "", has_type_id = false, has_copy_function = false)]
|
||||||
|
|
Loading…
Reference in a new issue