mpris: add progress bar with seek support

parent a67ff66e
......@@ -67,6 +67,46 @@ template $XimperShellNotificationCenterWidgetsMprisMprisPlayer: $Underlay {
}
}
Box progress_box {
orientation: horizontal;
spacing: 6;
visible: false;
styles [
"mpris-progress",
]
Label position_label {
label: "0:00";
xalign: 0;
styles [
"caption",
"dim-label",
]
}
Scale progress_scale {
hexpand: true;
draw-value: false;
adjustment: Adjustment {
lower: 0;
upper: 100;
value: 0;
};
}
Label duration_label {
label: "0:00";
xalign: 1;
styles [
"caption",
"dim-label",
]
}
}
Box {
spacing: 6;
halign: center;
......
......@@ -60,10 +60,7 @@
"text": "Label Text"
},
"mpris": {
"blacklist": [],
"autohide": false,
"show-album-art": "always",
"loop-carousel": false
"blacklist": []
}
}
}
......@@ -402,35 +402,13 @@
"description": "A widget that displays multiple music players",
"additionalProperties": false,
"properties": {
"image-size": {
"type": "integer",
"deprecated": true,
"description": "deprecated (change the CSS root variable \"--mpris-album-art-icon-size\"): The size of the album art",
"default": -1
},
"show-album-art": {
"type": "string",
"description": "Whether or not the album art should be hidden, always visible, or only visible when a valid album art is provided.",
"default": "always",
"enum": ["always", "when-available", "never"]
},
"autohide": {
"type": "boolean",
"description": "Whether to hide the widget when the player has no metadata.",
"default": false
},
"blacklist": {
"type": "array",
"description": "Audio sources for the mpris widget to ignore.",
"description": "Audio sources for the mpris widget to ignore. Regex allowed.",
"items": {
"type": "string",
"description": "Audio source/app name. Regex allowed."
}
},
"loop-carousel": {
"type": "boolean",
"description": "Whether to loop through the mpris carousel.",
"default": "false"
}
}
},
......
......@@ -76,6 +76,7 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
public abstract async void play_pause () throws Error;
public abstract async void stop () throws Error;
public abstract async void play () throws Error;
public abstract async void seek (int64 offset) throws Error;
public abstract string playback_status { owned get; }
public abstract HashTable<string, Variant> metadata { owned get; }
......
namespace XimperShellNotificationCenter.Widgets.Mpris {
public enum AlbumArtState {
ALWAYS, WHEN_AVAILABLE, NEVER;
public static AlbumArtState parse (string value) {
switch (value) {
default:
case "always":
return AlbumArtState.ALWAYS;
case "when-available":
return AlbumArtState.WHEN_AVAILABLE;
case "never":
return AlbumArtState.NEVER;
}
}
}
public struct Config {
[Version (deprecated = true, replacement = "CSS root variable")]
int image_size;
AlbumArtState show_album_art;
bool autohide;
string[] blacklist;
bool loop_carousel;
}
public class Mpris : BaseWidget {
......@@ -45,10 +24,6 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
// Default config values
Config mpris_config = Config () {
image_size = -1,
show_album_art = AlbumArtState.ALWAYS,
autohide = false,
loop_carousel = false,
};
public Mpris (string suffix) {
......@@ -83,9 +58,8 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
button_next.sensitive = false;
return;
}
button_prev.sensitive = (index > 0) || mpris_config.loop_carousel;
button_next.sensitive = (index < carousel.n_pages - 1) ||
mpris_config.loop_carousel;
button_prev.sensitive = true;
button_next.sensitive = true;
});
carousel_box.append (button_prev);
......@@ -103,46 +77,21 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
// Config
Json.Object ?config = get_config (this);
if (config != null) {
// Get image-size
bool image_size_found;
int ?image_size = get_prop<int> (config, "image-size", out image_size_found);
if (image_size_found && image_size != null) {
mpris_config.image_size = image_size;
}
bool show_art_found;
string ?show_album_art = get_prop<string> (config, "show-album-art",
out show_art_found);
if (show_art_found && show_album_art != null) {
mpris_config.show_album_art = AlbumArtState.parse (show_album_art);
}
Json.Array ?blacklist = get_prop_array (config, "blacklist");
Json.Array ?blacklist = get_prop_array (
config, "blacklist");
if (blacklist != null) {
mpris_config.blacklist = new string[blacklist.get_length ()];
mpris_config.blacklist =
new string[blacklist.get_length ()];
for (int i = 0; i < blacklist.get_length (); i++) {
if (blacklist.get_element (i).get_node_type () != Json.NodeType.VALUE) {
if (blacklist.get_element (i).get_node_type ()
!= Json.NodeType.VALUE) {
warning ("Blacklist entries should be strings");
continue;
}
mpris_config.blacklist[i] = blacklist.get_string_element (i);
mpris_config.blacklist[i] =
blacklist.get_string_element (i);
}
}
// Get autohide
bool autohide_found;
bool ?autohide = get_prop<bool> (config, "autohide", out autohide_found);
if (autohide_found) {
mpris_config.autohide = autohide;
}
// Get loop
bool loop_carousel_found;
bool ?loop_carousel = get_prop<bool> (config, "loop-carousel",
out loop_carousel_found);
if (loop_carousel_found) {
mpris_config.loop_carousel = loop_carousel;
}
}
hide ();
......@@ -249,17 +198,15 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
player.add_css_class ("%s-player".printf (css_class_name));
players.set (name, player);
if (mpris_config.autohide) {
player.content_updated.connect (() => {
if (!check_player_metadata_empty (name)) {
add_player_to_carousel (name);
} else {
remove_player_from_carousel (name);
}
});
if (check_player_metadata_empty (name)) {
return;
player.content_updated.connect (() => {
if (!check_player_metadata_empty (name)) {
add_player_to_carousel (name);
} else {
remove_player_from_carousel (name);
}
});
if (check_player_metadata_empty (name)) {
return;
}
add_player_to_carousel (name);
......@@ -303,12 +250,8 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
return;
}
uint position;
if (mpris_config.loop_carousel) {
position = ((uint) carousel.position + delta) % children_length;
} else {
position = ((uint) carousel.position + delta)
.clamp (0, (children_length - 1));
}
position = ((uint) carousel.position + delta)
% children_length;
carousel.scroll_to (carousel.get_nth_page (position), true);
}
......
......@@ -23,6 +23,18 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
[GtkChild]
unowned Gtk.Button button_repeat;
[GtkChild]
unowned Gtk.Box progress_box;
[GtkChild]
unowned Gtk.Scale progress_scale;
[GtkChild]
unowned Gtk.Label position_label;
[GtkChild]
unowned Gtk.Label duration_label;
private int64 track_length = 0;
private bool seeking = false;
public MprisSource source { construct; get; }
private const double UNSELECTED_OPACITY = 0.5;
......@@ -94,12 +106,98 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
update_buttons (source.media_player.metadata);
});
});
album_art.set_pixel_size (mpris_config.image_size);
album_art.set_visible (mpris_config.show_album_art == AlbumArtState.ALWAYS);
album_art.set_visible (true);
// Progress bar
progress_scale.change_value.connect ((type, val) => {
if (track_length <= 0) return false;
seeking = true;
int64 pos = (int64) (val * track_length / 100);
position_label.set_text (
format_time (pos));
return false;
});
progress_scale.value_changed.connect (() => {
if (!seeking || track_length <= 0) return;
seeking = false;
int64 pos = (int64) (
progress_scale.get_value ()
* track_length / 100);
seek_to.begin (pos);
});
}
private void update_position () {
if (seeking || track_length <= 0) return;
Variant ?pos_var = source.get_mpris_player_prop (
"Position");
if (pos_var == null
|| !pos_var.is_of_type (VariantType.INT64)) {
return;
}
int64 pos = pos_var.get_int64 ();
double percent = (double) pos / track_length * 100;
progress_scale.set_value (
percent.clamp (0, 100));
position_label.set_text (format_time (pos));
}
private void update_track_length (
HashTable<string, Variant> metadata) {
Variant ?length = metadata.lookup ("mpris:length");
if (length != null) {
if (length.is_of_type (VariantType.INT64)) {
track_length = length.get_int64 ();
} else if (length.is_of_type (VariantType.INT32)) {
track_length = length.get_int32 ();
} else {
track_length = 0;
}
} else {
track_length = 0;
}
bool show = track_length > 0;
progress_box.set_visible (show);
if (show) {
duration_label.set_text (
format_time (track_length));
update_position ();
}
}
private async void seek_to (int64 position_us) {
try {
// SetPosition needs track id
var metadata = source.media_player.metadata;
Variant ?trackid = metadata.lookup (
"mpris:trackid");
if (trackid != null) {
// Use Seek with offset from current
Variant ?cur = source.get_mpris_player_prop (
"Position");
if (cur != null
&& cur.is_of_type (VariantType.INT64)) {
int64 offset = position_us
- cur.get_int64 ();
yield source.media_player.seek (offset);
}
}
} catch (Error e) {
debug ("Seek failed: %s", e.message);
}
}
private static string format_time (int64 microseconds) {
int64 seconds = microseconds / 1000000;
int64 minutes = seconds / 60;
seconds = seconds % 60;
return "%d:%02d".printf (
(int) minutes, (int) seconds);
}
public void before_destroy () {
source.properties_changed.disconnect (properties_changed);
source.properties_changed.disconnect (
properties_changed);
}
private void properties_changed (string iface,
......@@ -131,6 +229,9 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
case "CanGoNext":
update_button_forward (metadata);
break;
case "Position":
update_position ();
break;
case "CanControl":
update_buttons (metadata);
break;
......@@ -154,6 +255,9 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
// Subtitle
update_sub_title (metadata);
// Progress bar
update_track_length (metadata);
// Update the buttons
update_buttons (metadata);
......@@ -314,18 +418,17 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
}
if (album_art_texture != null) {
// Set album art
int icon_size = mpris_config.image_size;
if (icon_size < 0) {
icon_size = album_art_texture.width > album_art_texture.height
? album_art_texture.height : album_art_texture.width;
}
int icon_size =
album_art_texture.width > album_art_texture.height
? album_art_texture.height
: album_art_texture.width;
Gtk.Snapshot snapshot = new Gtk.Snapshot ();
Functions.scale_texture (album_art_texture,
icon_size, icon_size,
get_scale_factor (), snapshot);
Graphene.Size size = Graphene.Size ().init (icon_size, icon_size);
album_art.set_from_paintable (snapshot.free_to_paintable (size));
album_art.set_visible (mpris_config.show_album_art != AlbumArtState.NEVER);
album_art.set_visible (true);
// Set background album art
background_picture.set_paintable (album_art_texture);
......@@ -335,7 +438,7 @@ namespace XimperShellNotificationCenter.Widgets.Mpris {
}
}
album_art.set_visible (mpris_config.show_album_art == AlbumArtState.ALWAYS);
album_art.set_visible (true);
// Get the app icon
Icon ?icon = null;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment