diff --git a/include/bar/bar.hpp b/include/bar/bar.hpp index f8ea38f..5c410fd 100644 --- a/include/bar/bar.hpp +++ b/include/bar/bar.hpp @@ -27,8 +27,8 @@ class Bar : public Gtk::Window { Clock clock; Date date; - WebWidget homeAssistant{"\ue88a", "Home Assistant", "https://home.rivercry.com"}; - ControlCenter controlCenter{"\ue88a", "Control Center"}; + WebWidget homeAssistant{"\uf024", "Home Assistant", "https://home.rivercry.com"}; + ControlCenter controlCenter{"\ue062", "Control Center"}; std::shared_ptr trayWidget = nullptr; std::shared_ptr volumeWidget = nullptr; diff --git a/include/services/dbus/messages.hpp b/include/services/dbus/messages.hpp index 330b35b..0ea41d6 100644 --- a/include/services/dbus/messages.hpp +++ b/include/services/dbus/messages.hpp @@ -16,6 +16,7 @@ struct MprisPlayer2Message { std::string title; std::vector artist; std::string artwork_url; + std::string track_id; int64_t length_ms; std::function play_pause; diff --git a/include/services/dbus/mpris.hpp b/include/services/dbus/mpris.hpp index 0fbf2cf..b5d2376 100644 --- a/include/services/dbus/mpris.hpp +++ b/include/services/dbus/mpris.hpp @@ -26,10 +26,13 @@ class MprisController { void next_song(); void previous_song(); void emit_seeked(int64_t position_us); + void set_position(const std::string &track_id, int64_t position_us); sigc::signal &signal_mpris_updated(); sigc::signal &signal_playback_status_changed(); sigc::signal &signal_playback_position_changed(); + sigc::signal &signal_player_registered(); + sigc::signal &signal_player_deregistered(); private: MprisController(); @@ -43,13 +46,22 @@ class MprisController { Glib::RefPtr m_connection; Glib::RefPtr m_proxy; + Glib::RefPtr m_dbus_proxy; + std::string m_player_bus_name = "org.mpris.MediaPlayer2.spotify"; sigc::signal mprisUpdatedSignal; sigc::signal playbackStatusChangedSignal; sigc::signal playbackPositionChangedSignal; + sigc::signal playerRegisteredSignal; + sigc::signal playerDeregisteredSignal; void on_bus_connected(const Glib::RefPtr &result); void signalNotification(); + void on_dbus_signal(const Glib::ustring &sender_name, + const Glib::ustring &signal_name, + const Glib::VariantContainerBase ¶meters); + void handle_player_registered(const std::string &bus_name); + void handle_player_deregistered(const std::string &bus_name); // Called when the song changes void on_properties_changed(const Gio::DBus::Proxy::MapChangedProperties &changed_properties, diff --git a/include/widgets/controlCenter/mediaControl.hpp b/include/widgets/controlCenter/mediaControl.hpp index e9fd7de..51cddcd 100644 --- a/include/widgets/controlCenter/mediaControl.hpp +++ b/include/widgets/controlCenter/mediaControl.hpp @@ -19,6 +19,8 @@ class MediaControlWidget : public Gtk::Box { int64_t currentPositionUs = 0; int64_t totalLengthUs = 0; sigc::connection seekTimerConnection; + bool suppressSeekSignal = false; + std::string currentTrackId; void setCurrentPosition(int64_t position_us); void setTotalLength(int64_t length_us); diff --git a/include/widgets/notification/baseNotification.hpp b/include/widgets/notification/baseNotification.hpp index bf7d16b..63ae365 100644 --- a/include/widgets/notification/baseNotification.hpp +++ b/include/widgets/notification/baseNotification.hpp @@ -9,7 +9,7 @@ #include "gdkmm/monitor.h" #include "gtkmm/window.h" -#define DEFAULT_NOTIFICATION_TIMEOUT 5000 +#define DEFAULT_NOTIFICATION_TIMEOUT 6700 class BaseNotification : public Gtk::Window { diff --git a/resources/bar.css b/resources/bar.css index a79cb94..9605f98 100644 --- a/resources/bar.css +++ b/resources/bar.css @@ -21,6 +21,11 @@ window { font-family: var(--text-font); } +.material-icons { + font-family: var(--icon-font-material); + font-size: 18px; +} + popover { margin-top: 4px; font-family: var(--text-font); diff --git a/src/components/popover.cpp b/src/components/popover.cpp index f4c1d6f..e007334 100644 --- a/src/components/popover.cpp +++ b/src/components/popover.cpp @@ -5,6 +5,8 @@ Popover::Popover(const std::string icon, std::string name): Button(icon) { set_name(name); + this->add_css_class("material-icons"); + popover = new Gtk::Popover(); popover->set_parent(*this); popover->set_autohide(true); diff --git a/src/services/dbus/mpris.cpp b/src/services/dbus/mpris.cpp index 9be52a1..ff05c54 100644 --- a/src/services/dbus/mpris.cpp +++ b/src/services/dbus/mpris.cpp @@ -32,6 +32,14 @@ sigc::signal &MprisController::signal_playback_position_changed() return playbackPositionChangedSignal; } +sigc::signal &MprisController::signal_player_registered() { + return playerRegisteredSignal; +} + +sigc::signal &MprisController::signal_player_deregistered() { + return playerDeregisteredSignal; +} + void MprisController::on_bus_connected(const Glib::RefPtr &result) { if (!result) { spdlog::error("DBus Connection Error: null async result"); @@ -40,22 +48,32 @@ void MprisController::on_bus_connected(const Glib::RefPtr &res try { m_connection = Gio::DBus::Connection::get_finish(result); - std::string player_bus_name = "org.mpris.MediaPlayer2.spotify"; + if (!m_dbus_proxy) { + m_dbus_proxy = Gio::DBus::Proxy::create_sync( + m_connection, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus"); - m_proxy = Gio::DBus::Proxy::create_sync( - m_connection, - player_bus_name, // The Bus Name - "/org/mpris/MediaPlayer2", // The Object Path - "org.mpris.MediaPlayer2.Player" // The Interface - ); + if (m_dbus_proxy) { + m_dbus_proxy->signal_signal().connect( + sigc::mem_fun(*this, &MprisController::on_dbus_signal)); - if (m_proxy) { - spdlog::info("Connected to: {}", player_bus_name); + try { + auto list_names_result = m_dbus_proxy->call_sync("ListNames"); + auto names_variant = Glib::VariantBase::cast_dynamic< + Glib::Variant>>(list_names_result.get_child(0)); - signalNotification(); - - m_proxy->signal_properties_changed().connect( - sigc::mem_fun(*this, &MprisController::on_properties_changed)); + for (const auto &name : names_variant.get()) { + const std::string bus_name = name; + if (bus_name.rfind("org.mpris.MediaPlayer2.", 0) == 0) { + handle_player_registered(bus_name); + } + } + } catch (const Glib::Error &ex) { + spdlog::error("DBus ListNames Error: {}", ex.what()); + } + } } } catch (const Glib::Error &ex) { @@ -63,6 +81,66 @@ void MprisController::on_bus_connected(const Glib::RefPtr &res } } +void MprisController::on_dbus_signal(const Glib::ustring &, + const Glib::ustring &signal_name, + const Glib::VariantContainerBase ¶meters) { + if (signal_name != "NameOwnerChanged") { + return; + } + + auto name_var = Glib::VariantBase::cast_dynamic>(parameters.get_child(0)); + auto old_owner_var = Glib::VariantBase::cast_dynamic>(parameters.get_child(1)); + auto new_owner_var = Glib::VariantBase::cast_dynamic>(parameters.get_child(2)); + + const std::string bus_name = name_var.get(); + if (bus_name.rfind("org.mpris.MediaPlayer2.", 0) != 0) { + return; + } + + const bool had_owner = !old_owner_var.get().empty(); + const bool has_owner = !new_owner_var.get().empty(); + + if (!had_owner && has_owner) { + handle_player_registered(bus_name); + } else if (had_owner && !has_owner) { + handle_player_deregistered(bus_name); + } +} + +void MprisController::handle_player_registered(const std::string &bus_name) { + spdlog::info("MPRIS player registered: {}", bus_name); + + if (bus_name == m_player_bus_name && !m_proxy && m_connection) { + try { + m_proxy = Gio::DBus::Proxy::create_sync( + m_connection, + m_player_bus_name, + "/org/mpris/MediaPlayer2", + "org.mpris.MediaPlayer2.Player"); + + if (m_proxy) { + m_proxy->signal_properties_changed().connect( + sigc::mem_fun(*this, &MprisController::on_properties_changed)); + signalNotification(); + } + } catch (const Glib::Error &ex) { + spdlog::error("DBus Connection Error: {}", ex.what()); + } + } + + playerRegisteredSignal.emit(bus_name); +} + +void MprisController::handle_player_deregistered(const std::string &bus_name) { + spdlog::info("MPRIS player deregistered: {}", bus_name); + + if (bus_name == m_player_bus_name) { + m_proxy.reset(); + } + + playerDeregisteredSignal.emit(bus_name); +} + void MprisController::signalNotification() { if (!m_proxy) { return; @@ -89,7 +167,7 @@ void MprisController::signalNotification() { metadata_map = variant_dict.get(); - std::string title, artwork_url; + std::string title, artwork_url, track_id; std::vector artist; if (metadata_map.count("xesam:title")) { @@ -124,6 +202,17 @@ void MprisController::signalNotification() { } } + if (metadata_map.count("mpris:trackid")) { + const auto &track_base = metadata_map["mpris:trackid"]; + if (track_base.is_of_type(Glib::VariantType("o"))) { + auto track_var = Glib::VariantBase::cast_dynamic>(track_base); + track_id = track_var.get(); + } else if (track_base.is_of_type(Glib::VariantType("s"))) { + auto track_var = Glib::VariantBase::cast_dynamic>(track_base); + track_id = track_var.get(); + } + } + int64_t length_us = 0; if (metadata_map.count("mpris:length")) { const auto &length_base = metadata_map["mpris:length"]; @@ -140,10 +229,11 @@ void MprisController::signalNotification() { mpris.title = StringHelper::trimToSize(title, 30); mpris.artist = artist; mpris.artwork_url = artwork_url; + mpris.track_id = track_id; mpris.play_pause = [this]() { this->toggle_play(); }; mpris.next = [this]() { this->next_song(); }; mpris.previous = [this]() { this->previous_song(); }; - mpris.length_ms = length_us; // Convert microseconds to milliseconds + mpris.length_ms = length_us / 1000; // Convert microseconds to milliseconds mprisUpdatedSignal.emit(mpris); } @@ -177,7 +267,7 @@ void MprisController::on_properties_changed(const Gio::DBus::Proxy::MapChangedPr spdlog::info("Position changed to: {}", position); playbackPositionChangedSignal.emit(static_cast(position)); } - } + } } void MprisController::previous_song() { @@ -199,6 +289,8 @@ void MprisController::next_song() { } void MprisController::emit_seeked(int64_t position_us) { + spdlog::debug("Seeking to position: {}", position_us); + if (!m_proxy) { return; } @@ -212,3 +304,20 @@ void MprisController::emit_seeked(int64_t position_us) { spdlog::error("Error seeking: {}", ex.what()); } } + +void MprisController::set_position(const std::string &track_id, int64_t position_us) { + if (!m_proxy || track_id.empty()) { + return; + } + + try { + Glib::VariantContainerBase params = Glib::VariantContainerBase::create_tuple({ + Glib::Variant::create(track_id), + Glib::Variant::create(position_us) + }); + + m_proxy->call("SetPosition", params); + } catch (const Glib::Error &ex) { + spdlog::error("Error setting position: {}", ex.what()); + } +} diff --git a/src/widgets/controlCenter/mediaControl.cpp b/src/widgets/controlCenter/mediaControl.cpp index 9246549..5a53e04 100644 --- a/src/widgets/controlCenter/mediaControl.cpp +++ b/src/widgets/controlCenter/mediaControl.cpp @@ -60,10 +60,20 @@ MediaControlWidget::MediaControlWidget() this->seekBar.add_css_class("control-center-seek-bar"); this->seekBar.signal_value_changed().connect([this]() { + if (this->suppressSeekSignal || this->totalLengthUs <= 0) { + return; + } double fraction = this->seekBar.get_value() / 100.0; int64_t new_position_us = static_cast(fraction * static_cast(this->totalLengthUs)); - this->mprisController->emit_seeked(new_position_us - this->currentPositionUs); // in us + if (new_position_us == this->currentPositionUs) { + return; + } + if (!this->currentTrackId.empty()) { + this->mprisController->set_position(this->currentTrackId, new_position_us); + } else { + this->mprisController->emit_seeked(new_position_us - this->currentPositionUs); // in us + } this->resetSeekTimer(new_position_us); }); @@ -126,12 +136,13 @@ void MediaControlWidget::onSpotifyMprisUpdated(const MprisPlayer2Message &messag } this->artistLabel.set_text(artistText); this->titleLabel.set_text(message.title); + this->currentTrackId = message.track_id; if (auto texture = TextureCacheService::getInstance()->getTexture(message.artwork_url)) { this->backgroundImage.set_paintable(texture); } - this->setTotalLength(message.length_ms); + this->setTotalLength(message.length_ms * 1000); this->setCurrentPosition(0); this->resetSeekTimer(0); } @@ -144,7 +155,9 @@ void MediaControlWidget::setCurrentPosition(int64_t position_us) { std::to_string(minutes) + ":" + (seconds < 10 ? "0" : "") + std::to_string(seconds)); if (totalLengthUs > 0) { double fraction = static_cast(currentPositionUs) / static_cast(totalLengthUs); + this->suppressSeekSignal = true; this->seekBar.set_value(fraction * 100); + this->suppressSeekSignal = false; } } diff --git a/src/widgets/notification/spotifyNotification.cpp b/src/widgets/notification/spotifyNotification.cpp index b0f1dc3..9bd4de7 100644 --- a/src/widgets/notification/spotifyNotification.cpp +++ b/src/widgets/notification/spotifyNotification.cpp @@ -33,7 +33,11 @@ SpotifyNotification::SpotifyNotification(uint64_t notificationId, std::shared_pt title_label->set_ellipsize(Pango::EllipsizeMode::END); auto artistLabel = Gtk::make_managed(); - artistLabel->set_text(StringHelper::trimToSize(mpris.artist[0], 30)); + if (!mpris.artist.empty()) { + artistLabel->set_text(StringHelper::trimToSize(mpris.artist[0], 30)); + } else { + artistLabel->set_text("Unknown Artist"); + } artistLabel->set_hexpand(true); artistLabel->set_halign(Gtk::Align::CENTER);