diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f46658..4410f1c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2 -DNDEBUG -Wall -Wextra -Wpedantic -Werror") -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG -Wall -Wextra -Wpedantic -Werror") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG -Wall -Wextra -Wpedantic") find_package(PkgConfig REQUIRED) @@ -37,6 +37,7 @@ target_sources(bar_lib src/services/textureCache.cpp src/services/tray.cpp src/services/dbus/notification.cpp + src/services/dbus/mpris.cpp src/widgets/tray.cpp src/widgets/controlCenter.cpp diff --git a/include/helpers/string.hpp b/include/helpers/string.hpp index 4729c69..4375324 100644 --- a/include/helpers/string.hpp +++ b/include/helpers/string.hpp @@ -37,4 +37,11 @@ public: tokens.push_back(input.substr(start)); return tokens; } + + static std::string trimToSize(const std::string &input, size_t maxSize) { + if (input.length() <= maxSize) { + return input; + } + return input.substr(0, maxSize) + "..."; + } }; \ No newline at end of file diff --git a/include/services/dbus/mpris.hpp b/include/services/dbus/mpris.hpp index d3eec24..69d7c28 100644 --- a/include/services/dbus/mpris.hpp +++ b/include/services/dbus/mpris.hpp @@ -1,136 +1,35 @@ -#include +#pragma once + #include -#include +#include #include -#include "services/notificationController.hpp" -#include "giomm/dbusconnection.h" -#include "giomm/dbusproxy.h" class MprisController { -public: - MprisController() { - // 1. Connect to the Session Bus - Gio::DBus::Connection::get( - Gio::DBus::BusType::SESSION, - sigc::mem_fun(*this, &MprisController::on_bus_connected) - ); - } + public: + struct MprisPlayer2Message { + std::string title; + std::string artist; + std::string artwork_url; -private: + std::function play_pause; + std::function next; + std::function previous; + }; + + MprisController(); + + void toggle_play(); + void next_song(); + void previous_song(); + + private: Glib::RefPtr m_connection; Glib::RefPtr m_proxy; - void on_bus_connected(const Glib::RefPtr& result) { - if (!result) { - std::cerr << "DBus Connection Error: null async result" << std::endl; - return; - } - try { - m_connection = Gio::DBus::Connection::get_finish(result); - - // 2. Create a Proxy to the media player - // NOTE: In a real app, you must find the name dynamically (see Step 2 below). - // For now, ensure a player like Spotify or VLC is running. - // Try "org.mpris.MediaPlayer2.spotify" or "org.mpris.MediaPlayer2.vlc" - std::string player_bus_name = "org.mpris.MediaPlayer2.spotify"; + void on_bus_connected(const Glib::RefPtr &result); + void launchNotification(); - 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_proxy) { - std::cout << "Connected to: " << player_bus_name << std::endl; - - // Get initial state - print_metadata(); - - // 3. Listen for changes (Song change, Pause/Play) - m_proxy->signal_properties_changed().connect( - sigc::mem_fun(*this, &MprisController::on_properties_changed) - ); - } - - } catch (const Glib::Error& ex) { - std::cerr << "DBus Connection Error: " << ex.what() << std::endl; - } - } - - void print_metadata() { - // Retrieve the cached property "Metadata" - Glib::VariantBase metadata_var; - m_proxy->get_cached_property(metadata_var, "Metadata"); - - if (!metadata_var) { - std::cout << "No metadata available." << std::endl; - return; - } - - // 4. Unpack Metadata (Type: a{sv}) - // This is a dictionary of string -> variant - using MetadataMap = std::map; - MetadataMap metadata_map; - - // Cast the variant to a container and iterate - Glib::Variant variant_dict = - Glib::VariantBase::cast_dynamic>(metadata_var); - - metadata_map = variant_dict.get(); - - std::string title, artist, artwork_url; - - if (metadata_map.count("xesam:title")) { - auto title_var = Glib::VariantBase::cast_dynamic>(metadata_map["xesam:title"]); - title = title_var.get(); - } - - if (metadata_map.count("xesam:artist")) { - auto artist_var = metadata_map["xesam:artist"]; - - if (artist_var.is_of_type(Glib::VariantType("as"))) { - auto artists = Glib::VariantBase::cast_dynamic>>(artist_var).get(); - if (!artists.empty()) { - artist = artists[0]; // Take the first artist - } - } - } - - if (metadata_map.count("mpris:artUrl")) { - auto art_var = Glib::VariantBase::cast_dynamic>(metadata_map["mpris:artUrl"]); - artwork_url = art_var.get(); - } - - auto notifactionController = NotificationController::getInstance(); - notifactionController->showSpotifyNotification(title, artist, artwork_url); - - } - // Called when the song changes - void on_properties_changed(const Gio::DBus::Proxy::MapChangedProperties& changed_properties, - const std::vector& invalidated_properties) { - (void)invalidated_properties; - - // Only refresh when Metadata is updated - if (changed_properties.find("Metadata") != changed_properties.end()) { - // You could parse 'changed_properties' directly, but it's easier to just pull the new cache - print_metadata(); - } - } - -public: - // Call this to toggle play/pause - void toggle_play() { - if(m_proxy) { - m_proxy->call("PlayPause"); - } - } - - // Call this to skip - void next_song() { - if(m_proxy) { - m_proxy->call("Next"); - } - } + void on_properties_changed(const Gio::DBus::Proxy::MapChangedProperties &changed_properties, + const std::vector &invalidated_properties); }; \ No newline at end of file diff --git a/include/services/notificationController.hpp b/include/services/notificationController.hpp index 8ed978b..ded617e 100644 --- a/include/services/notificationController.hpp +++ b/include/services/notificationController.hpp @@ -2,8 +2,10 @@ #include #include +#include "services/dbus/mpris.hpp" #include "gdkmm/monitor.h" +#include "gtkmm/window.h" class NotificationController { static std::shared_ptr instance; @@ -15,9 +17,11 @@ class NotificationController { return NotificationController::instance; } - void showSpotifyNotification(const std::string &title, const std::string &message, const std::string &artwork_url); + void showSpotifyNotification(MprisController::MprisPlayer2Message mpris); void showNotificationOnAllMonitors(const std::string &title, const std::string &message); private: NotificationController(); std::vector> activeMonitors; + + void baseWindowSetup(std::shared_ptr win, std::shared_ptr monitor); }; \ No newline at end of file diff --git a/resources/bar.css b/resources/bar.css index 423e9a6..cd17442 100644 --- a/resources/bar.css +++ b/resources/bar.css @@ -58,6 +58,13 @@ button { background-color: #111111; } +.flat-button { + background-color: #333333; + color: #ffffff; + padding: 2px 4px; + border-radius: 10px; +} + .workspace-pill { padding: 2px 5px; margin-right: 6px; @@ -151,3 +158,6 @@ button { border: 1px solid rgba(80, 80, 80, 0.8); font-size: 14px; } + +.notification-button-box { +} diff --git a/src/services/dbus/mpris.cpp b/src/services/dbus/mpris.cpp new file mode 100644 index 0000000..9f6dbba --- /dev/null +++ b/src/services/dbus/mpris.cpp @@ -0,0 +1,132 @@ +#include "services/dbus/mpris.hpp" + +#include +#include + +#include "helpers/string.hpp" +#include "services/notificationController.hpp" + +#include "giomm/dbusconnection.h" +#include "giomm/dbusproxy.h" + +MprisController::MprisController() { + // 1. Connect to the Session Bus + Gio::DBus::Connection::get( + Gio::DBus::BusType::SESSION, + sigc::mem_fun(*this, &MprisController::on_bus_connected)); +} + +void MprisController::on_bus_connected(const Glib::RefPtr &result) { + if (!result) { + std::cerr << "DBus Connection Error: null async result" << std::endl; + return; + } + try { + m_connection = Gio::DBus::Connection::get_finish(result); + + std::string player_bus_name = "org.mpris.MediaPlayer2.spotify"; + + 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_proxy) { + std::cout << "Connected to: " << player_bus_name << std::endl; + + // uncomment if launch notification on start + launchNotification(); + + m_proxy->signal_properties_changed().connect( + sigc::mem_fun(*this, &MprisController::on_properties_changed)); + } + + } catch (const Glib::Error &ex) { + std::cerr << "DBus Connection Error: " << ex.what() << std::endl; + } +} + +void MprisController::launchNotification() { + if (!m_proxy) { + return; + } + + Glib::VariantBase metadata_var; + m_proxy->get_cached_property(metadata_var, "Metadata"); + + if (!metadata_var) { + std::cout << "No metadata available." << std::endl; + return; + } + + using MetadataMap = std::map; + MetadataMap metadata_map; + + Glib::Variant variant_dict = + Glib::VariantBase::cast_dynamic>(metadata_var); + + metadata_map = variant_dict.get(); + + std::string title, artist, artwork_url; + + if (metadata_map.count("xesam:title")) { + auto title_var = Glib::VariantBase::cast_dynamic>(metadata_map["xesam:title"]); + title = title_var.get(); + } + + if (metadata_map.count("xesam:artist")) { + auto artist_var = metadata_map["xesam:artist"]; + + if (artist_var.is_of_type(Glib::VariantType("as"))) { + auto artists = Glib::VariantBase::cast_dynamic>>(artist_var).get(); + if (!artists.empty()) { + artist = artists[0]; // Take the first artist + } + } + } + + if (metadata_map.count("mpris:artUrl")) { + auto art_var = Glib::VariantBase::cast_dynamic>(metadata_map["mpris:artUrl"]); + artwork_url = art_var.get(); + } + + auto notifactionController = NotificationController::getInstance(); + + MprisController::MprisPlayer2Message mpris; + mpris.title = StringHelper::trimToSize(title, 30); + mpris.artist = StringHelper::trimToSize(artist, 30); + mpris.artwork_url = artwork_url; + mpris.play_pause = [this]() { this->toggle_play(); }; + mpris.next = [this]() { this->next_song(); }; + mpris.previous = [this]() { this->previous_song(); }; + notifactionController->showSpotifyNotification(mpris); +} + +void MprisController::on_properties_changed(const Gio::DBus::Proxy::MapChangedProperties &changed_properties, + const std::vector &) { + + if (changed_properties.find("Metadata") != changed_properties.end()) { + launchNotification(); + } +} + +void MprisController::previous_song() { + if (m_proxy) { + m_proxy->call("Previous"); + } +} + + +void MprisController::toggle_play() { + if (m_proxy) { + m_proxy->call("PlayPause"); + } +} + +void MprisController::next_song() { + if (m_proxy) { + m_proxy->call("Next"); + } +} diff --git a/src/services/notificationController.cpp b/src/services/notificationController.cpp index 8ee99ca..57a4ba6 100644 --- a/src/services/notificationController.cpp +++ b/src/services/notificationController.cpp @@ -2,19 +2,27 @@ #include +#include "services/dbus/mpris.hpp" +#include "services/textureCache.hpp" + #include "gdkmm/display.h" #include "giomm/listmodel.h" #include "glibmm/main.h" #include "gtk4-layer-shell.h" #include "gtkmm/box.h" +#include "gtkmm/button.h" +#include "gtkmm/centerbox.h" #include "gtkmm/image.h" #include "gtkmm/label.h" #include "gtkmm/window.h" -#include "services/textureCache.hpp" std::shared_ptr NotificationController::instance = nullptr; NotificationController::NotificationController() { + if (NotificationController::instance) { + throw std::runtime_error("use getInstance()!"); + } + auto display = Gdk::Display::get_default(); if (!display) { return; @@ -33,82 +41,110 @@ NotificationController::NotificationController() { } } -void NotificationController::showSpotifyNotification(const std::string &title, const std::string &message, const std::string &artwork_url) { +void NotificationController::showSpotifyNotification(MprisController::MprisPlayer2Message mpris) { for (const auto &monitor : this->activeMonitors) { - auto win = new Gtk::Window(); - win->set_title(title); - win->set_default_size(300, 100); - - gtk_layer_init_for_window(win->gobj()); - gtk_layer_set_monitor(win->gobj(), monitor->gobj()); - gtk_layer_set_layer(win->gobj(), GTK_LAYER_SHELL_LAYER_OVERLAY); - gtk_layer_set_anchor(win->gobj(), GTK_LAYER_SHELL_EDGE_TOP, TRUE); - gtk_layer_set_anchor(win->gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, TRUE); - gtk_layer_set_margin(win->gobj(), GTK_LAYER_SHELL_EDGE_TOP, 2); - - win->add_css_class("notification-popup"); + auto win = std::make_shared(); + win->set_title(mpris.title); + this->baseWindowSetup(win, monitor); auto container = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 10); - if (auto texture = TextureCacheService::getInstance()->getTexture(artwork_url)) { + if (auto texture = TextureCacheService::getInstance()->getTexture(mpris.artwork_url)) { auto img = Gtk::make_managed(texture); - // make it larger - img->set_pixel_size(64); + img->set_pixel_size(64); container->append(*img); } - - auto text_box = Gtk::make_managed(Gtk::Orientation::VERTICAL, 5); - text_box->set_halign(Gtk::Align::CENTER); - text_box->set_valign(Gtk::Align::CENTER); - auto title_label = Gtk::make_managed("" + title + ""); + auto rightArea = Gtk::make_managed(Gtk::Orientation::VERTICAL, 5); + rightArea->set_halign(Gtk::Align::CENTER); + rightArea->set_valign(Gtk::Align::CENTER); + // make sure rigfht area takes the remaining space + rightArea->set_hexpand(true); + // also set a max width + + auto title_label = Gtk::make_managed("" + mpris.title + ""); title_label->set_use_markup(true); - title_label->set_halign(Gtk::Align::START); - text_box->append(*title_label); + title_label->set_halign(Gtk::Align::START); + rightArea->append(*title_label); + auto artistLabel = Gtk::make_managed(mpris.artist); + artistLabel->set_halign(Gtk::Align::CENTER); + rightArea->append(*artistLabel); - auto message_label = Gtk::make_managed(message); - message_label->set_halign(Gtk::Align::START); - text_box->append(*message_label); + auto buttonBox = Gtk::make_managed(); + buttonBox->add_css_class("notification-button-box"); + buttonBox->set_hexpand(true); - container->append(*text_box); + auto backButton = Gtk::make_managed("<"); + backButton->add_css_class("flat-button"); + auto playPauseButton = Gtk::make_managed("=<"); + playPauseButton->add_css_class("flat-button"); + auto nextButton = Gtk::make_managed(">"); + nextButton->add_css_class("flat-button"); + + backButton->signal_clicked().connect([mpris]() { + if (mpris.previous) { + mpris.previous(); + } + }); + + playPauseButton->signal_clicked().connect([mpris]() { + if (mpris.play_pause) { + mpris.play_pause(); + } + }); + + nextButton->signal_clicked().connect([mpris]() { + if (mpris.next) { + mpris.next(); + } + }); + buttonBox->set_start_widget(*backButton); + buttonBox->set_center_widget(*playPauseButton); + buttonBox->set_end_widget(*nextButton); + + // rightArea->append(*buttonBox); + + container->append(*rightArea); win->set_child(*container); win->show(); - // Auto close after 3 seconds for demo purposes Glib::signal_timeout().connect([win]() { win->close(); - delete win; return false; // Don't repeat }, 3000); } } +void NotificationController::baseWindowSetup(std::shared_ptr win, std::shared_ptr monitor) { + win->set_default_size(300, 100); + gtk_layer_init_for_window(win->gobj()); + gtk_layer_set_monitor(win->gobj(), monitor->gobj()); + gtk_layer_set_layer(win->gobj(), GTK_LAYER_SHELL_LAYER_OVERLAY); + gtk_layer_set_anchor(win->gobj(), GTK_LAYER_SHELL_EDGE_TOP, TRUE); + gtk_layer_set_anchor(win->gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, TRUE); + gtk_layer_set_margin(win->gobj(), GTK_LAYER_SHELL_EDGE_TOP, 2); + win->add_css_class("notification-popup"); +} + void NotificationController::showNotificationOnAllMonitors(const std::string &title, const std::string &message) { for (const auto &monitor : this->activeMonitors) { - auto win = new Gtk::Window(); + auto win = std::make_shared(); win->set_title(title); - win->set_default_size(300, 100); - gtk_layer_init_for_window(win->gobj()); - gtk_layer_set_monitor(win->gobj(), monitor->gobj()); - gtk_layer_set_layer(win->gobj(), GTK_LAYER_SHELL_LAYER_OVERLAY); - gtk_layer_set_anchor(win->gobj(), GTK_LAYER_SHELL_EDGE_TOP, TRUE); - gtk_layer_set_anchor(win->gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, TRUE); - gtk_layer_set_margin(win->gobj(), GTK_LAYER_SHELL_EDGE_TOP, 2); + this->baseWindowSetup(win, monitor); - win->add_css_class("notification-popup"); auto label = Gtk::make_managed(message); label->set_use_markup(true); win->set_child(*label); + win->show(); Glib::signal_timeout().connect([win]() { win->close(); - delete win; return false; // Don't repeat }, 3000);