diff --git a/include/services/dbus/mpris.hpp b/include/services/dbus/mpris.hpp index 82239f6..ff21fa6 100644 --- a/include/services/dbus/mpris.hpp +++ b/include/services/dbus/mpris.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "services/dbus/messages.hpp" @@ -21,6 +22,9 @@ class MprisController { }; static std::shared_ptr getInstance(); + static std::shared_ptr createForPlayer(const std::string &bus_name); + + std::vector get_registered_players() const; void toggle_play(); void pause(); @@ -32,11 +36,13 @@ class MprisController { sigc::signal &signal_mpris_updated(); sigc::signal &signal_playback_status_changed(); sigc::signal &signal_playback_position_changed(); + sigc::signal &signal_can_seek_changed(); sigc::signal &signal_player_registered(); sigc::signal &signal_player_deregistered(); private: MprisController(); + explicit MprisController(const std::string &bus_name); std::map playbackStatusMap = { {"Playing", PlaybackStatus::Playing}, {"Paused", PlaybackStatus::Paused}, @@ -49,10 +55,12 @@ class MprisController { Glib::RefPtr m_proxy; Glib::RefPtr m_dbus_proxy; std::string m_player_bus_name = "org.mpris.MediaPlayer2.spotify"; + std::set registeredPlayers; sigc::signal mprisUpdatedSignal; sigc::signal playbackStatusChangedSignal; sigc::signal playbackPositionChangedSignal; + sigc::signal canSeekChangedSignal; sigc::signal playerRegisteredSignal; sigc::signal playerDeregisteredSignal; @@ -60,6 +68,7 @@ class MprisController { void signalNotification(); void emit_cached_playback_status(); void emit_cached_position(); + void emit_cached_can_seek(); void on_dbus_signal(const Glib::ustring &sender_name, const Glib::ustring &signal_name, const Glib::VariantContainerBase ¶meters); diff --git a/include/widgets/controlCenter/controlCenter.hpp b/include/widgets/controlCenter/controlCenter.hpp index 54261f7..4045ea7 100644 --- a/include/widgets/controlCenter/controlCenter.hpp +++ b/include/widgets/controlCenter/controlCenter.hpp @@ -2,14 +2,30 @@ #include "components/popover.hpp" #include "gtkmm/box.h" +#include "gtkmm/button.h" +#include "gtkmm/label.h" +#include "gtkmm/stack.h" #include "widgets/controlCenter/mediaControl.hpp" +#include + class ControlCenter : public Popover { public: ControlCenter(std::string icon, std::string name); private: Gtk::Box container; - MediaControlWidget mediaControlWidget; + Gtk::Box tabRow; + Gtk::Stack contentStack; + Gtk::Box controlCenterContainer; + Gtk::Label testLabel; + Gtk::Button mediaControl; + Gtk::Button testTabButton; + std::shared_ptr mprisController = MprisController::getInstance(); + std::unordered_map mediaWidgets; + + void addPlayerWidget(const std::string &bus_name); + void removePlayerWidget(const std::string &bus_name); + void setActiveTab(const std::string &tab_name); }; \ No newline at end of file diff --git a/include/widgets/controlCenter/mediaControl.hpp b/include/widgets/controlCenter/mediaControl.hpp index 6d05696..92cc817 100644 --- a/include/widgets/controlCenter/mediaControl.hpp +++ b/include/widgets/controlCenter/mediaControl.hpp @@ -11,10 +11,10 @@ class MediaControlWidget : public Gtk::Box { public: - MediaControlWidget(); + explicit MediaControlWidget(std::shared_ptr controller); private: - std::shared_ptr mprisController = MprisController::getInstance(); + std::shared_ptr mprisController; int64_t currentPositionUs = 0; int64_t totalLengthUs = 0; @@ -22,12 +22,14 @@ class MediaControlWidget : public Gtk::Box { bool suppressSeekSignal = false; std::string currentTrackId; MprisController::PlaybackStatus playbackStatus = MprisController::PlaybackStatus::Stopped; + bool canSeek = true; void setCurrentPosition(int64_t position_us); void setTotalLength(int64_t length_us); void resetSeekTimer(int64_t start_position_us); bool onSeekTick(); void schedulePauseAfterSeek(); + void setCanSeek(bool can_seek); Gtk::Box spotifyContainer; diff --git a/resources/bar.css b/resources/bar.css index d212d73..75bf709 100644 --- a/resources/bar.css +++ b/resources/bar.css @@ -26,6 +26,35 @@ window { font-size: 18px; } +.tab-icon { + font-family: var(--icon-font-material); + font-size: 20px; + margin-right: 6px; + border-radius: 4px; + border: 1px solid transparent; + padding: 2px 4px; +} + +.tab-icon:hover { + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.control-center-tab-row { + /* mordern and sleek */ + background-color: rgba(50, 50, 50, 0.8); + border-radius: 8px; + padding: 2px 4px; + margin-bottom: 4px; +} + +.active-button { + background-color: #ffffff; + color: #1e1e1e; + border-radius: 4px; + box-shadow: 0 0 6px rgba(255, 255, 255, 0.8); +} + popover { margin-top: 4px; font-family: var(--text-font); @@ -39,17 +68,22 @@ popover { } .control-center-popover { - margin: 0; - background-color: rgba(25, 25, 25, 0.95); -} - -.control-center-spotify-container { - border: 1px solid rgba(80, 80, 80, 0.8); + background-color: rgba(35, 35, 35, 0.95); + padding: 12px; + padding-top : 6px; border-radius: 8px; - background: rgba(30, 30, 30, 0.95); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(57, 57, 57, 0.8); } -.control-center-spotify-artist-label { +.control-center-player-container { + border-radius: 8px; + background: rgba(35, 35, 35, 0.95); + margin-bottom: 6px; +} + + +.control-center-player-artist-label { font-size: 14px; font-weight: 600; color: #ffffff; @@ -60,7 +94,7 @@ popover { text-shadow: 0 0 5px #000000aa; } -.control-center-spotify-title-label { +.control-center-player-title-label { font-size: 12px; color: #cccccc; padding: 2px 6px; @@ -74,6 +108,7 @@ popover { min-height: 6px; min-width: 120px; margin: 0 6px; + border-bottom: 1px solid var(--color-border); } .control-center-seek-bar trough { diff --git a/src/services/dbus/mpris.cpp b/src/services/dbus/mpris.cpp index 1099305..7fb64a5 100644 --- a/src/services/dbus/mpris.cpp +++ b/src/services/dbus/mpris.cpp @@ -14,12 +14,27 @@ std::shared_ptr MprisController::getInstance() { return instance; } +std::shared_ptr MprisController::createForPlayer(const std::string &bus_name) { + return std::shared_ptr(new MprisController(bus_name)); +} + MprisController::MprisController() { Gio::DBus::Connection::get( Gio::DBus::BusType::SESSION, sigc::mem_fun(*this, &MprisController::on_bus_connected)); } +MprisController::MprisController(const std::string &bus_name) + : m_player_bus_name(bus_name) { + Gio::DBus::Connection::get( + Gio::DBus::BusType::SESSION, + sigc::mem_fun(*this, &MprisController::on_bus_connected)); +} + +std::vector MprisController::get_registered_players() const { + return std::vector(registeredPlayers.begin(), registeredPlayers.end()); +} + sigc::signal &MprisController::signal_mpris_updated() { return mprisUpdatedSignal; } @@ -32,6 +47,10 @@ sigc::signal &MprisController::signal_playback_position_changed() return playbackPositionChangedSignal; } +sigc::signal &MprisController::signal_can_seek_changed() { + return canSeekChangedSignal; +} + sigc::signal &MprisController::signal_player_registered() { return playerRegisteredSignal; } @@ -110,6 +129,8 @@ void MprisController::on_dbus_signal(const Glib::ustring &, void MprisController::handle_player_registered(const std::string &bus_name) { spdlog::info("MPRIS player registered: {}", bus_name); + registeredPlayers.insert(bus_name); + if (bus_name == m_player_bus_name && !m_proxy && m_connection) { try { m_proxy = Gio::DBus::Proxy::create_sync( @@ -124,6 +145,7 @@ void MprisController::handle_player_registered(const std::string &bus_name) { signalNotification(); emit_cached_playback_status(); emit_cached_position(); + emit_cached_can_seek(); } } catch (const Glib::Error &ex) { spdlog::error("DBus Connection Error: {}", ex.what()); @@ -136,6 +158,8 @@ void MprisController::handle_player_registered(const std::string &bus_name) { void MprisController::handle_player_deregistered(const std::string &bus_name) { spdlog::info("MPRIS player deregistered: {}", bus_name); + registeredPlayers.erase(bus_name); + if (bus_name == m_player_bus_name) { m_proxy.reset(); } @@ -275,6 +299,21 @@ void MprisController::emit_cached_position() { playbackPositionChangedSignal.emit(static_cast(position)); } +void MprisController::emit_cached_can_seek() { + if (!m_proxy) { + return; + } + + Glib::VariantBase can_seek_var; + m_proxy->get_cached_property(can_seek_var, "CanSeek"); + if (!can_seek_var || !can_seek_var.is_of_type(Glib::VariantType("b"))) { + return; + } + + auto can_seek = Glib::VariantBase::cast_dynamic>(can_seek_var).get(); + canSeekChangedSignal.emit(can_seek); +} + void MprisController::on_properties_changed(const Gio::DBus::Proxy::MapChangedProperties &changed_properties, const std::vector &) { @@ -306,6 +345,14 @@ void MprisController::on_properties_changed(const Gio::DBus::Proxy::MapChangedPr playbackPositionChangedSignal.emit(static_cast(position)); } } + + if (changed_properties.find("CanSeek") != changed_properties.end()) { + auto can_seek_var = changed_properties.at("CanSeek"); + if (can_seek_var.is_of_type(Glib::VariantType("b"))) { + auto can_seek = Glib::VariantBase::cast_dynamic>(can_seek_var).get(); + canSeekChangedSignal.emit(can_seek); + } + } } void MprisController::previous_song() { diff --git a/src/widgets/controlCenter/controlCenter.cpp b/src/widgets/controlCenter/controlCenter.cpp index a01279a..1c0c3a3 100644 --- a/src/widgets/controlCenter/controlCenter.cpp +++ b/src/widgets/controlCenter/controlCenter.cpp @@ -5,16 +5,99 @@ ControlCenter::ControlCenter(std::string icon, std::string name) : Popover(icon, name) { this->popover->add_css_class("control-center-popover"); this->container.set_orientation(Gtk::Orientation::VERTICAL); - this->container.set_spacing(0); - this->container.set_margin_top(0); - this->container.set_margin_bottom(0); - this->container.set_margin_start(0); - this->container.set_margin_end(0); + this->container.set_spacing(10); + this->popover->set_hexpand(false); + this->popover->set_size_request(240, -1); set_popover_child(this->container); + this->tabRow.set_orientation(Gtk::Orientation::HORIZONTAL); + this->tabRow.set_spacing(4); + this->tabRow.set_margin_bottom(4); + this->tabRow.add_css_class("control-center-tab-row"); - this->container.append(this->mediaControlWidget); + this->mediaControl.set_label("\uf5d3"); // control icon + this->mediaControl.add_css_class("tab-icon"); + this->testTabButton.set_label("\uE5CA"); // test icon + this->testTabButton.add_css_class("tab-icon"); + + this->tabRow.append(this->mediaControl); + this->tabRow.append(this->testTabButton); + + this->container.append(this->tabRow); + + this->contentStack.set_hhomogeneous(false); + this->contentStack.set_vhomogeneous(false); + this->contentStack.set_transition_type(Gtk::StackTransitionType::CROSSFADE); + this->contentStack.set_transition_duration(150); + + this->controlCenterContainer.set_orientation(Gtk::Orientation::VERTICAL); + this->controlCenterContainer.set_spacing(4); + + this->testLabel.set_text("Test tab"); + + this->contentStack.add(this->controlCenterContainer, "controls", "Controls"); + this->contentStack.add(this->testLabel, "test", "Test"); + this->contentStack.set_visible_child("controls"); + this->setActiveTab("controls"); + + this->container.append(this->contentStack); + + this->mediaControl.signal_clicked().connect([this]() { + this->setActiveTab("controls"); + }); + + this->testTabButton.signal_clicked().connect([this]() { + this->setActiveTab("test"); + }); + + this->mprisController->signal_player_registered().connect( + [this](const std::string &bus_name) { + this->addPlayerWidget(bus_name); + }); + + this->mprisController->signal_player_deregistered().connect( + [this](const std::string &bus_name) { + this->removePlayerWidget(bus_name); + }); + + for (const auto &bus_name : this->mprisController->get_registered_players()) { + this->addPlayerWidget(bus_name); + } } +void ControlCenter::setActiveTab(const std::string &tab_name) { + this->contentStack.set_visible_child(tab_name); + + this->mediaControl.remove_css_class("active-button"); + this->testTabButton.remove_css_class("active-button"); + + if (tab_name == "controls") { + this->mediaControl.add_css_class("active-button"); + } else if (tab_name == "test") { + this->testTabButton.add_css_class("active-button"); + } +} + +void ControlCenter::addPlayerWidget(const std::string &bus_name) { + if (this->mediaWidgets.find(bus_name) != this->mediaWidgets.end()) { + return; + } + + auto controller = MprisController::createForPlayer(bus_name); + auto widget = Gtk::make_managed(controller); + this->mediaWidgets.emplace(bus_name, widget); + this->controlCenterContainer.append(*widget); +} + +void ControlCenter::removePlayerWidget(const std::string &bus_name) { + auto it = this->mediaWidgets.find(bus_name); + if (it == this->mediaWidgets.end()) { + return; + } + + this->controlCenterContainer.remove(*it->second); + this->mediaWidgets.erase(it); +} + diff --git a/src/widgets/controlCenter/mediaControl.cpp b/src/widgets/controlCenter/mediaControl.cpp index 4d66a13..5c2dd2f 100644 --- a/src/widgets/controlCenter/mediaControl.cpp +++ b/src/widgets/controlCenter/mediaControl.cpp @@ -3,14 +3,16 @@ #include "helpers/string.hpp" #include "services/textureCache.hpp" -MediaControlWidget::MediaControlWidget() +MediaControlWidget::MediaControlWidget(std::shared_ptr controller) : Gtk::Box(Gtk::Orientation::VERTICAL) { + this->mprisController = std::move(controller); + this->set_orientation(Gtk::Orientation::VERTICAL); - this->set_size_request(200, 240); + this->set_size_request(200, -1); this->set_hexpand(false); this->set_vexpand(false); - this->add_css_class("control-center-spotify-container"); + this->add_css_class("control-center-player-container"); this->append(this->topContainer); this->append(this->seekBarContainer); @@ -24,7 +26,7 @@ MediaControlWidget::MediaControlWidget() this->topContainer.set_child(this->imageWrapper); - this->topContainer.set_size_request(200, 100); + this->topContainer.set_size_request(-1, 120); this->topContainer.set_vexpand(false); this->topContainer.set_hexpand(true); @@ -45,6 +47,7 @@ MediaControlWidget::MediaControlWidget() this->seekBarContainer.append(this->currentTimeLabel); this->seekBarContainer.append(this->seekBar); this->seekBarContainer.append(this->totalTimeLabel); + this->seekBarContainer.set_visible(true); this->currentTimeLabel.set_text("0:00"); this->currentTimeLabel.set_halign(Gtk::Align::START); @@ -54,7 +57,6 @@ MediaControlWidget::MediaControlWidget() this->seekBar.set_value(0); this->seekBar.set_orientation(Gtk::Orientation::HORIZONTAL); this->seekBar.set_draw_value(false); - this->seekBar.set_size_request(120, -1); this->seekBar.set_hexpand(true); this->seekBar.set_halign(Gtk::Align::CENTER); this->seekBar.add_css_class("control-center-seek-bar"); @@ -98,15 +100,14 @@ MediaControlWidget::MediaControlWidget() this->bottomContainer.append(this->nextButton); this->previousButton.set_label("\u23EE"); // Previous track symbol - this->previousButton.add_css_class("notification-button"); - this->previousButton.add_css_class("notification-icon-button"); + this->previousButton.add_css_class("button"); + this->previousButton.add_css_class("material-icons"); this->playPauseButton.set_label("\u23EF"); // Play/Pause symbol - this->playPauseButton.add_css_class("notification-button"); - this->playPauseButton.add_css_class("notification-icon-button"); + this->playPauseButton.add_css_class("button"); + this->playPauseButton.add_css_class("material-icons"); this->nextButton.set_label("\u23ED"); // Next track symbol - this->nextButton.add_css_class("notification-button"); - this->nextButton.add_css_class("notification-icon-button"); - + this->nextButton.add_css_class("button"); + this->nextButton.add_css_class("material-icons"); this->previousButton.signal_clicked().connect([this]() { this->mprisController->previous_song(); }); @@ -130,14 +131,24 @@ MediaControlWidget::MediaControlWidget() this->setCurrentPosition(position_us); }); + this->mprisController->signal_can_seek_changed().connect( + [this](bool can_seek) { + this->setCanSeek(can_seek); + }); + this->artistLabel.set_text("Artist Name"); - this->artistLabel.add_css_class("control-center-spotify-artist-label"); + this->artistLabel.add_css_class("control-center-player-artist-label"); this->titleLabel.set_text("Song Title"); - this->titleLabel.add_css_class("control-center-spotify-title-label"); + this->titleLabel.add_css_class("control-center-player-title-label"); this->resetSeekTimer(0); } +void MediaControlWidget::setCanSeek(bool can_seek) { + this->canSeek = can_seek; + this->seekBarContainer.set_visible(can_seek); +} + void MediaControlWidget::onSpotifyMprisUpdated(const MprisPlayer2Message &message) { std::string artistText = "Unknown Artist"; if (!message.artist.empty()) {