#include "widgets/controlCenter/mediaControl.hpp" #include "helpers/string.hpp" #include "services/textureCache.hpp" MediaControlWidget::MediaControlWidget() : Gtk::Box(Gtk::Orientation::VERTICAL) { this->set_orientation(Gtk::Orientation::VERTICAL); this->set_size_request(200, 240); this->set_hexpand(false); this->set_vexpand(false); this->add_css_class("control-center-spotify-container"); this->append(this->topContainer); this->append(this->seekBarContainer); this->append(this->bottomContainer); this->backgroundImage.set_content_fit(Gtk::ContentFit::COVER); this->backgroundImage.set_can_shrink(true); this->imageWrapper.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::NEVER); this->imageWrapper.set_child(this->backgroundImage); this->topContainer.set_child(this->imageWrapper); this->topContainer.set_size_request(200, 100); this->topContainer.set_vexpand(false); this->topContainer.set_hexpand(true); this->infoContainer.set_orientation(Gtk::Orientation::VERTICAL); this->infoContainer.set_valign(Gtk::Align::END); this->infoContainer.set_halign(Gtk::Align::START); this->infoContainer.append(this->artistLabel); this->infoContainer.append(this->titleLabel); this->topContainer.add_overlay(this->infoContainer); this->artistLabel.set_halign(Gtk::Align::START); this->titleLabel.set_halign(Gtk::Align::START); this->seekBarContainer.set_orientation(Gtk::Orientation::HORIZONTAL); this->seekBarContainer.set_vexpand(false); this->seekBarContainer.set_hexpand(true); this->seekBarContainer.set_halign(Gtk::Align::CENTER); this->seekBarContainer.append(this->currentTimeLabel); this->seekBarContainer.append(this->seekBar); this->seekBarContainer.append(this->totalTimeLabel); this->currentTimeLabel.set_text("0:00"); this->currentTimeLabel.set_halign(Gtk::Align::START); this->totalTimeLabel.set_text("0:00"); this->totalTimeLabel.set_halign(Gtk::Align::END); this->seekBar.set_range(0, 100); 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"); this->seekBar.signal_value_changed().connect([this]() { 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 this->resetSeekTimer(new_position_us); }); this->bottomContainer.set_orientation(Gtk::Orientation::HORIZONTAL); this->bottomContainer.set_vexpand(false); this->bottomContainer.set_hexpand(false); this->bottomContainer.set_valign(Gtk::Align::START); this->bottomContainer.set_homogeneous(true); this->topContainer.set_vexpand(false); this->topContainer.set_hexpand(true); this->bottomContainer.append(this->previousButton); this->bottomContainer.append(this->playPauseButton); 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->playPauseButton.set_label("\u23EF"); // Play/Pause symbol this->playPauseButton.add_css_class("notification-button"); this->playPauseButton.add_css_class("notification-icon-button"); this->nextButton.set_label("\u23ED"); // Next track symbol this->nextButton.add_css_class("notification-button"); this->nextButton.add_css_class("notification-icon-button"); this->previousButton.signal_clicked().connect([this]() { this->mprisController->previous_song(); }); this->playPauseButton.signal_clicked().connect([this]() { this->mprisController->toggle_play(); }); this->nextButton.signal_clicked().connect([this]() { this->mprisController->next_song(); }); this->mprisController->signal_mpris_updated().connect( sigc::mem_fun(*this, &MediaControlWidget::onSpotifyMprisUpdated)); this->mprisController->signal_playback_status_changed().connect( [this](MprisController::PlaybackStatus status) { this->onRunningStateChanged(status); }); this->mprisController->signal_playback_position_changed().connect( [this](int64_t position_us) { this->setCurrentPosition(position_us); }); this->artistLabel.set_text("Artist Name"); this->artistLabel.add_css_class("control-center-spotify-artist-label"); this->titleLabel.set_text("Song Title"); this->titleLabel.add_css_class("control-center-spotify-title-label"); this->resetSeekTimer(0); } void MediaControlWidget::onSpotifyMprisUpdated(const MprisPlayer2Message &message) { std::string artistText = "Unknown Artist"; if (!message.artist.empty()) { artistText = StringHelper::trimToSize(message.artist[0], 30); } this->artistLabel.set_text(artistText); this->titleLabel.set_text(message.title); if (auto texture = TextureCacheService::getInstance()->getTexture(message.artwork_url)) { this->backgroundImage.set_paintable(texture); } this->setTotalLength(message.length_ms); this->setCurrentPosition(0); this->resetSeekTimer(0); } void MediaControlWidget::setCurrentPosition(int64_t position_us) { this->currentPositionUs = position_us; int64_t seconds = (position_us / 1000000) % 60; int64_t minutes = (position_us / (1000000 * 60)) % 60; this->currentTimeLabel.set_text( std::to_string(minutes) + ":" + (seconds < 10 ? "0" : "") + std::to_string(seconds)); if (totalLengthUs > 0) { double fraction = static_cast(currentPositionUs) / static_cast(totalLengthUs); this->seekBar.set_value(fraction * 100); } } void MediaControlWidget::setTotalLength(int64_t length_us) { this->totalLengthUs = length_us; int64_t seconds = (length_us / 1000000) % 60; int64_t minutes = (length_us / (1000000 * 60)) % 60; this->totalTimeLabel.set_text( std::to_string(minutes) + ":" + (seconds < 10 ? "0" : "") + std::to_string(seconds)); } void MediaControlWidget::resetSeekTimer(int64_t start_position_us) { if (seekTimerConnection.connected()) { seekTimerConnection.disconnect(); } setCurrentPosition(start_position_us); seekTimerConnection = Glib::signal_timeout().connect( sigc::mem_fun(*this, &MediaControlWidget::onSeekTick), 1000); } bool MediaControlWidget::onSeekTick() { if (totalLengthUs <= 0) { return true; } int64_t nextPosition = currentPositionUs + 1000000; if (nextPosition > totalLengthUs) { nextPosition = totalLengthUs; } setCurrentPosition(nextPosition); return true; } void MediaControlWidget::onRunningStateChanged(MprisController::PlaybackStatus status) { switch (status) { case MprisController::PlaybackStatus::Playing: this->onPlay(); break; case MprisController::PlaybackStatus::Paused: this->onPause(); break; case MprisController::PlaybackStatus::Stopped: this->onStop(); break; } } void MediaControlWidget::onPlay() { this->playPauseButton.set_label("\u23F8"); // Pause symbol // strart seek timer if not already running this->resetSeekTimer(currentPositionUs); } void MediaControlWidget::onPause() { this->playPauseButton.set_label("\u23EF"); // Play symbol if (seekTimerConnection.connected()) { seekTimerConnection.disconnect(); } } void MediaControlWidget::onStop() { this->playPauseButton.set_label("\u23EF"); // Play symbol if (seekTimerConnection.connected()) { seekTimerConnection.disconnect(); } this->setCurrentPosition(0); }