Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c658a77b05 | ||
|
|
1880dc8153 | ||
|
|
b5fd3d5717 | ||
|
|
3c3480fb84 | ||
|
|
f628914173 | ||
|
|
c100fb1bb8 | ||
|
|
8c804c4fba | ||
|
|
912a7c7da9 | ||
|
|
0a5815c82e | ||
|
|
6513b3032b | ||
|
|
8c51401bdc | ||
|
|
45fc9c83d4 | ||
|
|
be57d8147a | ||
|
|
a97908fb6b | ||
|
|
c0417d4bb3 | ||
|
|
062e2cfb84 |
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
@@ -733,7 +733,7 @@ jobs:
|
||||
submodules: recursive
|
||||
- name: Build FreeBSD
|
||||
id: build-freebsd
|
||||
uses: vmactions/freebsd-vm@v1.2.1
|
||||
uses: vmactions/freebsd-vm@v1.2.3
|
||||
with:
|
||||
usesh: true
|
||||
mem: 4096
|
||||
@@ -758,7 +758,7 @@ jobs:
|
||||
submodules: recursive
|
||||
- name: Build OpenBSD
|
||||
id: build-openbsd
|
||||
uses: vmactions/openbsd-vm@v1.1.8
|
||||
uses: vmactions/openbsd-vm@v1.2.0
|
||||
with:
|
||||
usesh: true
|
||||
mem: 4096
|
||||
@@ -1295,6 +1295,20 @@ jobs:
|
||||
shell: bash
|
||||
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
||||
|
||||
- name: Show SDK versions
|
||||
shell: bash
|
||||
run: ls -la "c:/Program Files (x86)/Windows Kits/10/include"
|
||||
|
||||
- name: Set SDK version
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
run: echo "sdk_version=10.0.19041.0" >> $GITHUB_ENV
|
||||
|
||||
- name: Set SDK version
|
||||
if: matrix.arch == 'arm64'
|
||||
shell: bash
|
||||
run: echo "sdk_version=10.0.26100.0" >> $GITHUB_ENV
|
||||
|
||||
- name: Install rsync
|
||||
shell: cmd
|
||||
run: choco install --no-progress rsync
|
||||
@@ -1377,7 +1391,7 @@ jobs:
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: ${{matrix.arch}}
|
||||
sdk: 10.0.20348.0
|
||||
sdk: ${{env.sdk_version}}
|
||||
vsversion: 2022
|
||||
|
||||
- name: Checkout
|
||||
|
||||
1
3rdparty/discord-rpc/discord_rpc.h
vendored
1
3rdparty/discord-rpc/discord_rpc.h
vendored
@@ -32,6 +32,7 @@ extern "C" {
|
||||
|
||||
typedef struct DiscordRichPresence {
|
||||
int type;
|
||||
int status_display_type;
|
||||
const char *name; /* max 128 bytes */
|
||||
const char *state; /* max 128 bytes */
|
||||
const char *details; /* max 128 bytes */
|
||||
|
||||
@@ -128,6 +128,9 @@ size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce
|
||||
if (presence->type >= 0 && presence->type <= 5) {
|
||||
WriteKey(writer, "type");
|
||||
writer.Int(presence->type);
|
||||
|
||||
WriteKey(writer, "status_display_type");
|
||||
writer.Int(presence->status_display_type);
|
||||
}
|
||||
|
||||
WriteOptionalString(writer, "name", presence->name);
|
||||
|
||||
@@ -259,7 +259,16 @@ if(APPLE)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
find_package(getopt-win REQUIRED)
|
||||
find_package(getopt NAMES getopt getopt-win unofficial-getopt-win32 REQUIRED)
|
||||
if(TARGET getopt::getopt)
|
||||
set(GETOPT_LIBRARIES getopt::getopt)
|
||||
elseif(TARGET getopt-win::getopt)
|
||||
set(GETOPT_LIBRARIES getopt-win::getopt)
|
||||
elseif(TARGET getopt::getopt_shared)
|
||||
set(GETOPT_LIBRARIES getopt::getopt_shared)
|
||||
else()
|
||||
message(FATAL_ERROR "Missing getopt")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(APPLE OR WIN32)
|
||||
@@ -1554,7 +1563,7 @@ target_link_libraries(strawberry_lib PUBLIC
|
||||
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
|
||||
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
|
||||
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
|
||||
$<$<BOOL:${WIN32}>:dsound dwmapi getopt-win::getopt>
|
||||
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
|
||||
$<$<BOOL:${MSVC}>:WindowsApp>
|
||||
KDAB::kdsingleapplication
|
||||
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
|
||||
|
||||
13
Changelog
13
Changelog
@@ -2,6 +2,19 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
Version 1.2.13 (2025.08.31):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed playlist alternating row colors no longer working with some styles (#1806)
|
||||
* Fixed "Open Audio CD" no longer working (#1803)
|
||||
* Fixed systemtray icon playback status not working with scaling (#1782)
|
||||
* Fixed build without MusicBrainz (#1799)
|
||||
* Fixed build without MTP (#1804)
|
||||
|
||||
Enhancements:
|
||||
* Added Discord status text option (#1796)
|
||||
* Read Vorbis/FLAC "Other" embedded covers if front cover is not available (#1793)
|
||||
|
||||
Version 1.2.12 (2025.08.12):
|
||||
|
||||
Bugfixes:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 2)
|
||||
set(STRAWBERRY_VERSION_PATCH 12)
|
||||
set(STRAWBERRY_VERSION_PATCH 13)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
</screenshots>
|
||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||
<releases>
|
||||
<release version="1.2.13" date="2025-08-31"/>
|
||||
<release version="1.2.12" date="2025-08-12"/>
|
||||
<release version="1.2.11" date="2025-05-15"/>
|
||||
<release version="1.2.10" date="2025-04-18"/>
|
||||
|
||||
@@ -71,6 +71,14 @@ constexpr char kSettingsGroup[] = "DiscordRPC";
|
||||
|
||||
constexpr char kEnabled[] = "enabled";
|
||||
|
||||
constexpr char kStatusDisplayType[] = "StatusDisplayType";
|
||||
|
||||
enum class StatusDisplayType {
|
||||
App = 0,
|
||||
Artist,
|
||||
Song
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // NOTIFICATIONSSETTINGS_H
|
||||
|
||||
@@ -279,7 +279,7 @@ constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-
|
||||
#endif // HAVE_QTSPARKLE
|
||||
|
||||
MainWindow::MainWindow(Application *app,
|
||||
SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd,
|
||||
SharedPtr<SystemTrayIcon> systemtrayicon, OSDBase *osd,
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence,
|
||||
#endif
|
||||
@@ -291,7 +291,7 @@ MainWindow::MainWindow(Application *app,
|
||||
thumbbar_(new Windows7ThumbBar(this)),
|
||||
#endif
|
||||
app_(app),
|
||||
tray_icon_(tray_icon),
|
||||
systemtrayicon_(systemtrayicon),
|
||||
osd_(osd),
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord_rich_presence_(discord_rich_presence),
|
||||
@@ -408,6 +408,8 @@ MainWindow::MainWindow(Application *app,
|
||||
setWindowIcon(IconLoader::Load(u"strawberry"_s));
|
||||
}
|
||||
|
||||
systemtrayicon_->SetDevicePixelRatioF(devicePixelRatioF());
|
||||
|
||||
QObject::connect(&*app->database(), &Database::Error, this, &MainWindow::ShowErrorDialog);
|
||||
|
||||
album_cover_choice_controller_->Init(app->network(), app->tagreader_client(), app->collection()->backend(), app->albumcover_loader(), app->current_albumcover_loader(), app->cover_providers(), app->streaming_services());
|
||||
@@ -846,14 +848,14 @@ MainWindow::MainWindow(Application *app,
|
||||
mac::SetApplicationHandler(this);
|
||||
#endif
|
||||
// Tray icon
|
||||
tray_icon_->SetupMenu(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_stop_after_this_track, ui_->action_next_track, ui_->action_mute, ui_->action_love, ui_->action_quit);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
|
||||
systemtrayicon_->SetupMenu(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_stop_after_this_track, ui_->action_next_track, ui_->action_mute, ui_->action_love, ui_->action_quit);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
|
||||
|
||||
// Windows 7 thumbbar buttons
|
||||
#ifdef Q_OS_WIN32
|
||||
@@ -1031,7 +1033,7 @@ MainWindow::MainWindow(Application *app,
|
||||
show();
|
||||
break;
|
||||
case BehaviourSettings::StartupBehaviour::Hide:
|
||||
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible()) {
|
||||
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible()) {
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
@@ -1044,7 +1046,7 @@ MainWindow::MainWindow(Application *app,
|
||||
was_minimized_ = settings_.value(MainWindowSettings::kMinimized, false).toBool();
|
||||
if (was_minimized_) setWindowState(windowState() | Qt::WindowMinimized);
|
||||
|
||||
if (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) {
|
||||
if (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) {
|
||||
show();
|
||||
}
|
||||
break;
|
||||
@@ -1156,13 +1158,13 @@ void MainWindow::ReloadSettings() {
|
||||
#ifdef Q_OS_MACOS
|
||||
constexpr bool keeprunning_available = true;
|
||||
#else
|
||||
const bool systemtray_available = tray_icon_->IsSystemTrayAvailable();
|
||||
const bool systemtray_available = systemtrayicon_->IsSystemTrayAvailable();
|
||||
s.beginGroup(BehaviourSettings::kSettingsGroup);
|
||||
const bool showtrayicon = s.value(BehaviourSettings::kShowTrayIcon, systemtray_available).toBool();
|
||||
s.endGroup();
|
||||
const bool keeprunning_available = systemtray_available && showtrayicon;
|
||||
if (systemtray_available) {
|
||||
tray_icon_->setVisible(showtrayicon);
|
||||
systemtrayicon_->setVisible(showtrayicon);
|
||||
}
|
||||
if ((!showtrayicon || !systemtray_available) && !isVisible()) {
|
||||
show();
|
||||
@@ -1187,7 +1189,7 @@ void MainWindow::ReloadSettings() {
|
||||
int iconsize = s.value(AppearanceSettings::kIconSizePlayControlButtons, 32).toInt();
|
||||
s.endGroup();
|
||||
|
||||
tray_icon_->SetTrayiconProgress(trayicon_progress);
|
||||
systemtrayicon_->SetTrayiconProgress(trayicon_progress);
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_ && !taskbar_progress) {
|
||||
@@ -1209,11 +1211,11 @@ void MainWindow::ReloadSettings() {
|
||||
ui_->volume->SetEnabled(volume_control);
|
||||
if (volume_control) {
|
||||
if (!ui_->action_mute->isVisible()) ui_->action_mute->setVisible(true);
|
||||
if (!tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(true);
|
||||
if (!systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(true);
|
||||
}
|
||||
else {
|
||||
if (ui_->action_mute->isVisible()) ui_->action_mute->setVisible(false);
|
||||
if (tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(false);
|
||||
if (systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1365,8 +1367,8 @@ void MainWindow::Exit() {
|
||||
if (app_->player()->GetState() == EngineBase::State::Playing) {
|
||||
app_->player()->Stop();
|
||||
hide();
|
||||
if (tray_icon_->IsSystemTrayAvailable()) {
|
||||
tray_icon_->setVisible(false);
|
||||
if (systemtrayicon_->IsSystemTrayAvailable()) {
|
||||
systemtrayicon_->setVisible(false);
|
||||
}
|
||||
return; // Don't quit the application now: wait for the fadeout finished signal
|
||||
}
|
||||
@@ -1423,7 +1425,7 @@ void MainWindow::MediaStopped() {
|
||||
|
||||
ui_->action_love->setEnabled(false);
|
||||
ui_->button_love->setEnabled(false);
|
||||
tray_icon_->LoveStateChanged(false);
|
||||
systemtrayicon_->LoveStateChanged(false);
|
||||
|
||||
if (track_position_timer_->isActive()) {
|
||||
track_position_timer_->stop();
|
||||
@@ -1432,8 +1434,8 @@ void MainWindow::MediaStopped() {
|
||||
track_slider_timer_->stop();
|
||||
}
|
||||
ui_->track_slider->SetStopped();
|
||||
tray_icon_->SetProgress(0);
|
||||
tray_icon_->SetStopped();
|
||||
systemtrayicon_->SetProgress(0);
|
||||
systemtrayicon_->SetStopped();
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -1465,7 +1467,7 @@ void MainWindow::MediaPaused() {
|
||||
track_slider_timer_->start();
|
||||
}
|
||||
|
||||
tray_icon_->SetPaused();
|
||||
systemtrayicon_->SetPaused();
|
||||
|
||||
}
|
||||
|
||||
@@ -1486,7 +1488,7 @@ void MainWindow::MediaPlaying() {
|
||||
}
|
||||
ui_->action_play_pause->setEnabled(enable_play_pause);
|
||||
ui_->track_slider->SetCanSeek(can_seek);
|
||||
tray_icon_->SetPlaying(enable_play_pause);
|
||||
systemtrayicon_->SetPlaying(enable_play_pause);
|
||||
|
||||
if (!track_position_timer_->isActive()) {
|
||||
track_position_timer_->start();
|
||||
@@ -1507,14 +1509,14 @@ void MainWindow::SendNowPlaying() {
|
||||
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->EffectiveMetadata());
|
||||
ui_->action_love->setEnabled(true);
|
||||
ui_->button_love->setEnabled(true);
|
||||
tray_icon_->LoveStateChanged(true);
|
||||
systemtrayicon_->LoveStateChanged(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::VolumeChanged(const uint volume) {
|
||||
ui_->action_mute->setChecked(volume == 0);
|
||||
tray_icon_->MuteButtonStateChanged(volume == 0);
|
||||
systemtrayicon_->MuteButtonStateChanged(volume == 0);
|
||||
}
|
||||
|
||||
void MainWindow::SongChanged(const Song &song) {
|
||||
@@ -1524,7 +1526,7 @@ void MainWindow::SongChanged(const Song &song) {
|
||||
song_playing_ = song;
|
||||
song_ = song;
|
||||
setWindowTitle(song.PrettyTitleWithArtist());
|
||||
tray_icon_->SetProgress(0);
|
||||
systemtrayicon_->SetProgress(0);
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -1707,7 +1709,7 @@ void MainWindow::hideEvent(QHideEvent *e) {
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *e) {
|
||||
|
||||
if (!exit_ && (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !keep_running_)) {
|
||||
if (!exit_ && (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !keep_running_)) {
|
||||
Exit();
|
||||
}
|
||||
|
||||
@@ -1718,7 +1720,7 @@ void MainWindow::closeEvent(QCloseEvent *e) {
|
||||
void MainWindow::SetHiddenInTray(const bool hidden) {
|
||||
|
||||
if (hidden && isVisible()) {
|
||||
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible() && keep_running_) {
|
||||
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible() && keep_running_) {
|
||||
close();
|
||||
}
|
||||
else {
|
||||
@@ -1747,7 +1749,7 @@ void MainWindow::Seeked(const qint64 microseconds) {
|
||||
|
||||
const qint64 position = microseconds / kUsecPerSec;
|
||||
const qint64 length = app_->player()->GetCurrentItem()->EffectiveMetadata().length_nanosec() / kNsecPerSec;
|
||||
tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||
systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -1767,7 +1769,7 @@ void MainWindow::UpdateTrackPosition() {
|
||||
const int position = std::floor(static_cast<float>(app_->player()->engine()->position_nanosec()) / static_cast<float>(kNsecPerSec) + 0.5);
|
||||
|
||||
// Update the tray icon every 10 seconds
|
||||
if (position % 10 == 0) tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||
if (position % 10 == 0) systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -3259,7 +3261,7 @@ void MainWindow::LoveButtonVisibilityChanged(const bool value) {
|
||||
else
|
||||
ui_->widget_love->hide();
|
||||
|
||||
tray_icon_->LoveVisibilityChanged(value);
|
||||
systemtrayicon_->LoveVisibilityChanged(value);
|
||||
|
||||
}
|
||||
|
||||
@@ -3282,7 +3284,7 @@ void MainWindow::Love() {
|
||||
app_->scrobbler()->Love();
|
||||
ui_->button_love->setEnabled(false);
|
||||
ui_->action_love->setEnabled(false);
|
||||
tray_icon_->LoveStateChanged(false);
|
||||
systemtrayicon_->LoveStateChanged(false);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
|
||||
public:
|
||||
explicit MainWindow(Application *app,
|
||||
SharedPtr<SystemTrayIcon> tray_icon,
|
||||
SharedPtr<SystemTrayIcon> systemtrayicon,
|
||||
OSDBase *osd,
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence,
|
||||
@@ -310,7 +310,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
#endif
|
||||
|
||||
Application *app_;
|
||||
SharedPtr<SystemTrayIcon> tray_icon_;
|
||||
SharedPtr<SystemTrayIcon> systemtrayicon_;
|
||||
OSDBase *osd_;
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence_;
|
||||
|
||||
@@ -179,8 +179,10 @@ SongLoader::Result SongLoader::LoadAudioCD() {
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
CDDASongLoader *cdda_song_loader = new CDDASongLoader(QUrl(), this);
|
||||
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsDurationLoaded, this, &SongLoader::AudioCDTracksLoadFinishedSlot);
|
||||
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsMetadataLoaded, this, &SongLoader::AudioCDTracksTagsLoaded);
|
||||
QObject::connect(cdda_song_loader, &CDDASongLoader::LoadError, this, &SongLoader::AudioCDTracksLoadErrorSlot);
|
||||
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsLoaded, this, &SongLoader::AudioCDTracksLoadedSlot);
|
||||
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsUpdated, this, &SongLoader::AudioCDTracksUpdatedSlot);
|
||||
QObject::connect(cdda_song_loader, &CDDASongLoader::LoadingFinished, this, &SongLoader::AudioCDLoadingFinishedSlot);
|
||||
cdda_song_loader->LoadSongs();
|
||||
return Result::Success;
|
||||
#else
|
||||
@@ -192,23 +194,38 @@ SongLoader::Result SongLoader::LoadAudioCD() {
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
|
||||
void SongLoader::AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error) {
|
||||
void SongLoader::AudioCDTracksLoadErrorSlot(const QString &error) {
|
||||
|
||||
songs_ = songs;
|
||||
errors_ << error;
|
||||
Q_EMIT AudioCDTracksLoadFinished();
|
||||
|
||||
}
|
||||
|
||||
void SongLoader::AudioCDTracksTagsLoaded(const SongList &songs) {
|
||||
void SongLoader::AudioCDTracksLoadedSlot(const SongList &songs) {
|
||||
|
||||
songs_ = songs;
|
||||
|
||||
Q_EMIT AudioCDTracksLoaded();
|
||||
|
||||
}
|
||||
|
||||
void SongLoader::AudioCDTracksUpdatedSlot(const SongList &songs) {
|
||||
|
||||
songs_ = songs;
|
||||
|
||||
Q_EMIT AudioCDTracksUpdated();
|
||||
|
||||
}
|
||||
|
||||
void SongLoader::AudioCDLoadingFinishedSlot() {
|
||||
|
||||
CDDASongLoader *cdda_song_loader = qobject_cast<CDDASongLoader*>(sender());
|
||||
cdda_song_loader->deleteLater();
|
||||
songs_ = songs;
|
||||
Q_EMIT LoadAudioCDFinished(true);
|
||||
|
||||
Q_EMIT AudioCDLoadingFinished(true);
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // HAVE_AUDIOCD
|
||||
|
||||
SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
|
||||
|
||||
|
||||
@@ -90,17 +90,21 @@ class SongLoader : public QObject {
|
||||
QStringList errors() { return errors_; }
|
||||
|
||||
Q_SIGNALS:
|
||||
void AudioCDTracksLoadFinished();
|
||||
void LoadAudioCDFinished(const bool success);
|
||||
void AudioCDTracksLoaded();
|
||||
void AudioCDTracksUpdated();
|
||||
void AudioCDLoadingFinished(const bool success);
|
||||
void LoadRemoteFinished();
|
||||
|
||||
private Q_SLOTS:
|
||||
void ScheduleTimeout();
|
||||
void Timeout();
|
||||
void StopTypefind();
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
void AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error);
|
||||
void AudioCDTracksTagsLoaded(const SongList &songs);
|
||||
void AudioCDTracksLoadErrorSlot(const QString &error);
|
||||
void AudioCDTracksLoadedSlot(const SongList &songs);
|
||||
void AudioCDTracksUpdatedSlot(const SongList &songs);
|
||||
void AudioCDLoadingFinishedSlot();
|
||||
#endif // HAVE_AUDIOCD
|
||||
|
||||
private:
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <QFile>
|
||||
#include <QString>
|
||||
#include <QPalette>
|
||||
#include <QColor>
|
||||
#include <QEvent>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
@@ -79,13 +80,13 @@ void StyleSheetLoader::UpdateStyleSheet(QWidget *widget, SharedPtr<StyleSheetDat
|
||||
// Replace %palette-role with actual colours
|
||||
QPalette p(widget->palette());
|
||||
|
||||
{
|
||||
QColor color_altbase = p.color(QPalette::AlternateBase);
|
||||
QColor color_altbase = p.color(QPalette::AlternateBase);
|
||||
#ifdef Q_OS_MACOS
|
||||
color_altbase.setAlpha(color_altbase.lightness() > 180 ? 130 : 16);
|
||||
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? (color_altbase.lightness() > 180 ? 130 : 16) : color_altbase.alpha());
|
||||
#else
|
||||
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? 116 : color_altbase.alpha());
|
||||
#endif
|
||||
stylesheet.replace("%palette-alternate-base"_L1, QStringLiteral("rgba(%1,%2,%3,%4)").arg(color_altbase.red()).arg(color_altbase.green()).arg(color_altbase.blue()).arg(color_altbase.alpha()));
|
||||
}
|
||||
stylesheet.replace("%palette-alternate-base"_L1, QStringLiteral("rgba(%1,%2,%3,%4)").arg(color_altbase.red()).arg(color_altbase.green()).arg(color_altbase.blue()).arg(color_altbase.alpha()));
|
||||
|
||||
ReplaceColor(&stylesheet, u"Window"_s, p, QPalette::Window);
|
||||
ReplaceColor(&stylesheet, u"Background"_s, p, QPalette::Window);
|
||||
|
||||
@@ -63,9 +63,8 @@ CDDADevice::CDDADevice(const QUrl &url,
|
||||
timer_disc_changed_->setInterval(1s);
|
||||
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsLoaded, this, &CDDADevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsDurationLoaded, this, &CDDADevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsMetadataLoaded, this, &CDDADevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongLoadingFinished, this, &CDDADevice::SongLoadingFinished);
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsUpdated, this, &CDDADevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::LoadingFinished, this, &CDDADevice::SongLoadingFinished);
|
||||
QObject::connect(this, &CDDADevice::SongsDiscovered, collection_model_, &CollectionModel::AddReAddOrUpdate);
|
||||
QObject::connect(timer_disc_changed_, &QTimer::timeout, this, &CDDADevice::CheckDiscChanged);
|
||||
|
||||
|
||||
@@ -55,8 +55,9 @@ CDDASongLoader::CDDASongLoader(const QUrl &url, QObject *parent)
|
||||
url_(url),
|
||||
network_(make_shared<NetworkAccessManager>()) {
|
||||
|
||||
QObject::connect(this, &CDDASongLoader::MusicBrainzDiscIdLoaded, this, &CDDASongLoader::LoadMusicBrainzCDTags);
|
||||
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
QObject::connect(this, &CDDASongLoader::LoadTagsFromMusicBrainz, this, &CDDASongLoader::LoadTagsFromMusicBrainzSlot);
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
}
|
||||
|
||||
CDDASongLoader::~CDDASongLoader() {
|
||||
@@ -93,7 +94,7 @@ void CDDASongLoader::LoadSongsFromCDDA() {
|
||||
Error(QStringLiteral("%1: %2").arg(error->code).arg(QString::fromUtf8(error->message)));
|
||||
}
|
||||
if (!cdda) {
|
||||
Q_EMIT SongLoadingFinished();
|
||||
Error(tr("Could not create cdiocddasrc"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,7 +111,6 @@ void CDDASongLoader::LoadSongsFromCDDA() {
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while setting CDDA device to ready state."));
|
||||
Q_EMIT SongLoadingFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -119,7 +119,6 @@ void CDDASongLoader::LoadSongsFromCDDA() {
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while setting CDDA device to pause state."));
|
||||
Q_EMIT SongLoadingFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,7 +131,6 @@ void CDDASongLoader::LoadSongsFromCDDA() {
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while querying CDDA tracks."));
|
||||
Q_EMIT SongLoadingFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,7 +140,6 @@ void CDDASongLoader::LoadSongsFromCDDA() {
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while querying CDDA tracks."));
|
||||
Q_EMIT SongLoadingFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,9 +154,12 @@ void CDDASongLoader::LoadSongsFromCDDA() {
|
||||
song.set_track(track_number);
|
||||
songs.insert(track_number, song);
|
||||
}
|
||||
|
||||
Q_EMIT SongsLoaded(songs.values());
|
||||
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
gst_tag_register_musicbrainz_tags();
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
|
||||
GstElement *pipeline = gst_pipeline_new("pipeline");
|
||||
GstElement *sink = gst_element_factory_make("fakesink", nullptr);
|
||||
@@ -172,7 +172,9 @@ void CDDASongLoader::LoadSongsFromCDDA() {
|
||||
int track_artist_tags = 0;
|
||||
int track_album_tags = 0;
|
||||
int track_title_tags = 0;
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
QString musicbrainz_discid;
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
GstMessageType msg_filter = static_cast<GstMessageType>(GST_MESSAGE_TOC|GST_MESSAGE_TAG);
|
||||
while (msg_filter != 0 && (msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND * 5, msg_filter))) {
|
||||
|
||||
@@ -334,41 +336,50 @@ void CDDASongLoader::LoadSongsFromCDDA() {
|
||||
// This will also cause cdda to be unref'd.
|
||||
gst_object_unref(pipeline);
|
||||
|
||||
Q_EMIT SongsMetadataLoaded(songs.values());
|
||||
|
||||
if ((track_artist_tags >= total_tracks && track_album_tags >= total_tracks && track_title_tags >= total_tracks)) {
|
||||
qLog(Info) << "Songs loaded from CD-Text";
|
||||
Q_EMIT SongLoadingFinished();
|
||||
Q_EMIT SongsUpdated(songs.values());
|
||||
Q_EMIT LoadingFinished();
|
||||
}
|
||||
else {
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
if (musicbrainz_discid.isEmpty()) {
|
||||
qLog(Info) << "CD is missing tags";
|
||||
Q_EMIT LoadingFinished();
|
||||
}
|
||||
else {
|
||||
qLog(Info) << "MusicBrainz Disc ID:" << musicbrainz_discid;
|
||||
Q_EMIT MusicBrainzDiscIdLoaded(musicbrainz_discid);
|
||||
Q_EMIT LoadTagsFromMusicBrainz(musicbrainz_discid);
|
||||
}
|
||||
#else
|
||||
Q_EMIT LoadingFinished();
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
|
||||
void CDDASongLoader::LoadMusicBrainzCDTags(const QString &musicbrainz_discid) const {
|
||||
void CDDASongLoader::LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid) const {
|
||||
|
||||
MusicBrainzClient *musicbrainz_client = new MusicBrainzClient(network_);
|
||||
QObject::connect(musicbrainz_client, &MusicBrainzClient::DiscIdFinished, this, &CDDASongLoader::MusicBrainzCDTagsLoaded);
|
||||
QObject::connect(musicbrainz_client, &MusicBrainzClient::DiscIdFinished, this, &CDDASongLoader::LoadTagsFromMusicBrainzFinished);
|
||||
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
|
||||
|
||||
}
|
||||
|
||||
void CDDASongLoader::MusicBrainzCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results) {
|
||||
void CDDASongLoader::LoadTagsFromMusicBrainzFinished(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results, const QString &error) {
|
||||
|
||||
MusicBrainzClient *musicbrainz_client = qobject_cast<MusicBrainzClient*>(sender());
|
||||
musicbrainz_client->deleteLater();
|
||||
|
||||
if (!error.isEmpty()) {
|
||||
Error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.empty()) {
|
||||
Q_EMIT SongLoadingFinished();
|
||||
Q_EMIT LoadingFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -392,8 +403,8 @@ void CDDASongLoader::MusicBrainzCDTagsLoaded(const QString &artist, const QStrin
|
||||
songs << song;
|
||||
}
|
||||
|
||||
Q_EMIT SongsMetadataLoaded(songs);
|
||||
Q_EMIT SongLoadingFinished();
|
||||
Q_EMIT SongsUpdated(songs);
|
||||
Q_EMIT LoadingFinished();
|
||||
|
||||
}
|
||||
|
||||
@@ -402,6 +413,8 @@ void CDDASongLoader::MusicBrainzCDTagsLoaded(const QString &artist, const QStrin
|
||||
void CDDASongLoader::Error(const QString &error) {
|
||||
|
||||
qLog(Error) << error;
|
||||
Q_EMIT SongsDurationLoaded(SongList(), error);
|
||||
|
||||
Q_EMIT LoadError(error);
|
||||
Q_EMIT LoadingFinished();
|
||||
|
||||
}
|
||||
|
||||
@@ -58,17 +58,16 @@ class CDDASongLoader : public QObject {
|
||||
QUrl GetUrlFromTrack(const int track_number) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void SongsLoadError(const QString &error);
|
||||
void SongsLoaded(const SongList &songs);
|
||||
void SongLoadingFinished();
|
||||
void SongsDurationLoaded(const SongList &songs, const QString &error = QString());
|
||||
void SongsMetadataLoaded(const SongList &songs);
|
||||
void MusicBrainzDiscIdLoaded(const QString &musicbrainz_discid);
|
||||
void SongsUpdated(const SongList &songs);
|
||||
void LoadError(const QString &error);
|
||||
void LoadingFinished();
|
||||
void LoadTagsFromMusicBrainz(const QString &musicbrainz_discid);
|
||||
|
||||
private Q_SLOTS:
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
void LoadMusicBrainzCDTags(const QString &musicbrainz_discid) const;
|
||||
void MusicBrainzCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results);
|
||||
void LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid) const;
|
||||
void LoadTagsFromMusicBrainzFinished(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results, const QString &error);
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -60,6 +60,7 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
|
||||
void UpdateDeviceFreeSpace(const QString &id);
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
struct MTPDevice {
|
||||
MTPDevice() : capacity(0), free_space(0) {}
|
||||
QString vendor;
|
||||
@@ -74,6 +75,7 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
quint64 capacity;
|
||||
quint64 free_space;
|
||||
};
|
||||
#endif // HAVE_MTP
|
||||
|
||||
void ExitAsync();
|
||||
|
||||
@@ -91,11 +93,12 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
|
||||
static void DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context);
|
||||
|
||||
void FoundMTPDevice(const MTPDevice &device, const QString &serial);
|
||||
#ifdef HAVE_MTP
|
||||
void FoundMTPDevice(const MTPDevice &mtp_device, const QString &serial);
|
||||
void RemovedMTPDevice(const QString &serial);
|
||||
|
||||
quint64 GetFreeSpace(const QUrl &url);
|
||||
quint64 GetCapacity(const QUrl &url);
|
||||
#endif // HAVE_MTP
|
||||
|
||||
bool IsCDDevice(const QString &serial) const;
|
||||
|
||||
@@ -103,18 +106,23 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
CFRunLoopRef run_loop_;
|
||||
|
||||
QMap<QString, QString> current_devices_;
|
||||
#ifdef HAVE_MTP
|
||||
QMap<QString, MTPDevice> mtp_devices_;
|
||||
#endif
|
||||
QSet<QString> cd_devices_;
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
QMutex libmtp_mutex_;
|
||||
|
||||
static QSet<MTPDevice> sMTPDeviceList;
|
||||
#endif
|
||||
};
|
||||
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &device);
|
||||
#ifdef HAVE_MTP
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &mtp_device);
|
||||
|
||||
inline bool operator==(const MacOsDeviceLister::MTPDevice &a, const MacOsDeviceLister::MTPDevice &b) {
|
||||
return (a.vendor_id == b.vendor_id) && (a.product_id == b.product_id);
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
#endif // MACDEVICELISTER_H
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,7 +21,9 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <libmtp.h>
|
||||
#ifdef HAVE_MTP
|
||||
# include <libmtp.h>
|
||||
#endif
|
||||
|
||||
#include <AvailabilityMacros.h>
|
||||
#include <CoreFoundation/CFRunLoop.h>
|
||||
@@ -41,12 +43,15 @@
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "macosdevicelister.h"
|
||||
#include "mtpconnection.h"
|
||||
#include "includes/scoped_cftyperef.h"
|
||||
#include "includes/scoped_nsobject.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/scoped_nsautorelease_pool.h"
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
# include "mtpconnection.h"
|
||||
#endif
|
||||
|
||||
#import <AppKit/NSWorkspace.h>
|
||||
#import <Foundation/NSDictionary.h>
|
||||
#import <Foundation/NSNotification.h>
|
||||
@@ -102,11 +107,15 @@ class ScopedIOObject {
|
||||
// Libgphoto2 MTP detection code:
|
||||
// http://www.sfr-fresh.com/unix/privat/libgphoto2-2.4.10.1.tar.gz:a/libgphoto2-2.4.10.1/libgphoto2_port/usb/check-mtp-device.c
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
QSet<MacOsDeviceLister::MTPDevice> MacOsDeviceLister::sMTPDeviceList;
|
||||
#endif
|
||||
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &d) {
|
||||
return qHash(d.vendor_id) ^ qHash(d.product_id);
|
||||
#ifdef HAVE_MTP
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &mtp_device) {
|
||||
return qHash(mtp_device.vendor_id) ^ qHash(mtp_device.product_id);
|
||||
}
|
||||
#endif
|
||||
|
||||
MacOsDeviceLister::MacOsDeviceLister(QObject *parent) : DeviceLister(parent) {}
|
||||
|
||||
@@ -116,6 +125,7 @@ bool MacOsDeviceLister::Init() {
|
||||
|
||||
ScopedNSAutoreleasePool pool;
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
// Populate MTP Device list.
|
||||
if (sMTPDeviceList.empty()) {
|
||||
LIBMTP_device_entry_t *devices = nullptr;
|
||||
@@ -126,25 +136,26 @@ bool MacOsDeviceLister::Init() {
|
||||
else {
|
||||
for (int i = 0; i < num; ++i) {
|
||||
LIBMTP_device_entry_t device = devices[i];
|
||||
MTPDevice d;
|
||||
d.vendor = QString::fromLatin1(device.vendor);
|
||||
d.vendor_id = device.vendor_id;
|
||||
d.product = QString::fromLatin1(device.product);
|
||||
d.product_id = device.product_id;
|
||||
d.quirks = device.device_flags;
|
||||
sMTPDeviceList << d;
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = QString::fromLatin1(device.vendor);
|
||||
mtp_device.vendor_id = device.vendor_id;
|
||||
mtp_device.product = QString::fromLatin1(device.product);
|
||||
mtp_device.product_id = device.product_id;
|
||||
mtp_device.quirks = device.device_flags;
|
||||
sMTPDeviceList << mtp_device;
|
||||
}
|
||||
}
|
||||
|
||||
MTPDevice d;
|
||||
d.vendor = "SanDisk"_L1;
|
||||
d.vendor_id = 0x781;
|
||||
d.product = "Sansa Clip+"_L1;
|
||||
d.product_id = 0x74d0;
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = "SanDisk"_L1;
|
||||
mtp_device.vendor_id = 0x781;
|
||||
mtp_device.product = "Sansa Clip+"_L1;
|
||||
mtp_device.product_id = 0x74d0;
|
||||
|
||||
d.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
|
||||
sMTPDeviceList << d;
|
||||
mtp_device.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
|
||||
sMTPDeviceList << mtp_device;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
run_loop_ = CFRunLoopGetCurrent();
|
||||
|
||||
@@ -240,12 +251,14 @@ CFTypeRef GetUSBRegistryEntry(io_object_t device, CFStringRef key) {
|
||||
}
|
||||
|
||||
QString GetUSBRegistryEntryString(io_object_t device, CFStringRef key) {
|
||||
|
||||
ScopedCFTypeRef<CFStringRef> registry_string(reinterpret_cast<CFStringRef>(GetUSBRegistryEntry(device, key)));
|
||||
if (registry_string) {
|
||||
return QString::fromUtf8([reinterpret_cast<NSString*>(registry_string.get()) UTF8String]);
|
||||
}
|
||||
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) {
|
||||
@@ -277,17 +290,13 @@ NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) {
|
||||
|
||||
int GetUSBDeviceClass(io_object_t device) {
|
||||
|
||||
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(
|
||||
device,
|
||||
kIOServicePlane,
|
||||
CFSTR(kUSBInterfaceClass),
|
||||
kCFAllocatorDefault,
|
||||
kIORegistryIterateRecursively));
|
||||
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(device, kIOServicePlane, CFSTR(kUSBInterfaceClass), kCFAllocatorDefault, kIORegistryIterateRecursively));
|
||||
NSNumber *number = reinterpret_cast<NSNumber*>(interface_class.get());
|
||||
if (number) {
|
||||
int ret = [number unsignedShortValue];
|
||||
return ret;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
@@ -322,12 +331,14 @@ QString GetSerialForDevice(io_object_t device) {
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
QString GetSerialForMTPDevice(io_object_t device) {
|
||||
|
||||
scoped_nsobject<NSString> serial(reinterpret_cast<NSString*>(GetPropertyForDevice(device, CFSTR(kUSBSerialNumberString))));
|
||||
return "MTP/"_L1 + QString::fromUtf8([serial UTF8String]);
|
||||
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
|
||||
|
||||
@@ -343,6 +354,7 @@ QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
|
||||
|
||||
} // namespace
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
quint64 MacOsDeviceLister::GetFreeSpace(const QUrl &url) {
|
||||
|
||||
QMutexLocker l(&libmtp_mutex_);
|
||||
@@ -380,6 +392,8 @@ quint64 MacOsDeviceLister::GetCapacity(const QUrl &url) {
|
||||
|
||||
}
|
||||
|
||||
#endif // HAVE_MTP
|
||||
|
||||
void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
|
||||
@@ -390,12 +404,12 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
NSString *kind = [properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionMediaKindKey)];
|
||||
if (kind && strcmp([kind UTF8String], kIOCDMediaClass) == 0) {
|
||||
// CD inserted.
|
||||
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
me->cd_devices_ << bsd_name;
|
||||
Q_EMIT me->DeviceAdded(bsd_name);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#endif // HAVE_AUDIOCD
|
||||
|
||||
NSURL *volume_path = [[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy];
|
||||
|
||||
@@ -403,8 +417,8 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk));
|
||||
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(device.get()));
|
||||
if (class_name && CFStringCompare(class_name.get(), CFSTR(kIOMediaClass), 0) == kCFCompareEqualTo) {
|
||||
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
|
||||
CFMutableDictionaryRef cf_properties;
|
||||
kern_return_t ret = IORegistryEntryCreateCFProperties(device.get(), &cf_properties, kCFAllocatorDefault, 0);
|
||||
@@ -412,7 +426,7 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
if (ret == KERN_SUCCESS) {
|
||||
scoped_nsobject<NSDictionary> dict(reinterpret_cast<NSDictionary*>(cf_properties)); // Takes ownership.
|
||||
if ([[dict objectForKey:@"Removable"] intValue] == 1) {
|
||||
QString serial = GetSerialForDevice(device.get());
|
||||
const QString serial = GetSerialForDevice(device.get());
|
||||
if (!serial.isEmpty()) {
|
||||
me->current_devices_[serial] = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
Q_EMIT me->DeviceAdded(serial);
|
||||
@@ -427,10 +441,9 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
void MacOsDeviceLister::DiskRemovedCallback(DADiskRef disk, void *context) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
|
||||
// We cannot access the USB tree when the disk is removed but we still get
|
||||
// the BSD disk name.
|
||||
// We cannot access the USB tree when the disk is removed but we still get the BSD disk name.
|
||||
|
||||
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
if (me->cd_devices_.remove(bsd_name)) {
|
||||
Q_EMIT me->DeviceRemoved(bsd_name);
|
||||
return;
|
||||
@@ -496,6 +509,7 @@ int GetBusNumber(io_object_t o) {
|
||||
void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
|
||||
Q_UNUSED(me)
|
||||
|
||||
io_object_t object;
|
||||
while ((object = IOIteratorNext(it))) {
|
||||
@@ -503,30 +517,34 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
|
||||
|
||||
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
|
||||
|
||||
const int interface_class = GetUSBDeviceClass(object);
|
||||
qLog(Debug) << "Interface class:" << interface_class;
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
|
||||
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
|
||||
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
|
||||
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
|
||||
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
|
||||
int interface_class = GetUSBDeviceClass(object);
|
||||
qLog(Debug) << "Interface class:" << interface_class;
|
||||
|
||||
QString serial = GetSerialForMTPDevice(object);
|
||||
const QString serial = GetSerialForMTPDevice(object);
|
||||
|
||||
MTPDevice device;
|
||||
device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
device.product = QString::fromUtf8([product UTF8String]);
|
||||
device.vendor_id = [vendor_id unsignedShortValue];
|
||||
device.product_id = [product_id unsignedShortValue];
|
||||
device.quirks = 0;
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
mtp_device.product = QString::fromUtf8([product UTF8String]);
|
||||
mtp_device.vendor_id = [vendor_id unsignedShortValue];
|
||||
mtp_device.product_id = [product_id unsignedShortValue];
|
||||
mtp_device.quirks = 0;
|
||||
|
||||
device.bus = -1;
|
||||
device.address = -1;
|
||||
mtp_device.bus = -1;
|
||||
mtp_device.address = -1;
|
||||
|
||||
if (device.vendor_id == kAppleVendorID || // I think we can safely skip Apple products.
|
||||
if (mtp_device.vendor_id == kAppleVendorID || // I think we can safely skip Apple products.
|
||||
// Blacklist ilok2 as this probe may be breaking it.
|
||||
(device.vendor_id == 0x088e && device.product_id == 0x5036) ||
|
||||
(mtp_device.vendor_id == 0x088e && mtp_device.product_id == 0x5036) ||
|
||||
// Blacklist eLicenser
|
||||
(device.vendor_id == 0x0819 && device.product_id == 0x0101) ||
|
||||
(mtp_device.vendor_id == 0x0819 && mtp_device.product_id == 0x0101) ||
|
||||
// Skip HID devices, printers and hubs.
|
||||
interface_class == kUSBHIDInterfaceClass ||
|
||||
interface_class == kUSBPrintingInterfaceClass ||
|
||||
@@ -535,31 +553,28 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
}
|
||||
|
||||
NSNumber *addr = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR("USB Address")));
|
||||
int bus = GetBusNumber(object);
|
||||
const int bus = GetBusNumber(object);
|
||||
if (!addr || bus == -1) {
|
||||
// Failed to get bus or address number.
|
||||
continue;
|
||||
}
|
||||
device.bus = bus;
|
||||
device.address = [addr intValue];
|
||||
mtp_device.bus = bus;
|
||||
mtp_device.address = [addr intValue];
|
||||
|
||||
// First check the libmtp device list.
|
||||
QSet<MTPDevice>::const_iterator it2 = sMTPDeviceList.find(device);
|
||||
QSet<MTPDevice>::const_iterator it2 = sMTPDeviceList.find(mtp_device);
|
||||
if (it2 != sMTPDeviceList.end()) {
|
||||
// Fill in quirks flags from libmtp.
|
||||
device.quirks = it2->quirks;
|
||||
me->FoundMTPDevice(device, GetSerialForMTPDevice(object));
|
||||
mtp_device.quirks = it2->quirks;
|
||||
me->FoundMTPDevice(mtp_device, GetSerialForMTPDevice(object));
|
||||
continue;
|
||||
}
|
||||
|
||||
#endif // HAVE_MTP
|
||||
|
||||
IOCFPlugInInterface **plugin_interface = nullptr;
|
||||
SInt32 score;
|
||||
kern_return_t err = IOCreatePlugInInterfaceForService(
|
||||
object,
|
||||
kIOUSBDeviceUserClientTypeID,
|
||||
kIOCFPlugInInterfaceID,
|
||||
&plugin_interface,
|
||||
&score);
|
||||
kern_return_t err = IOCreatePlugInInterfaceForService(object, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin_interface, &score);
|
||||
if (err != KERN_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
@@ -590,7 +605,7 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
bool ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, 2, &data);
|
||||
if (!ret) continue;
|
||||
|
||||
UInt8 string_len = data[0];
|
||||
const UInt8 string_len = data[0];
|
||||
|
||||
ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, string_len, &data);
|
||||
if (!ret) continue;
|
||||
@@ -599,6 +614,7 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
// Because this was designed by MS, the characters are in UTF-16 (LE?).
|
||||
QString str = QString::fromUtf16(reinterpret_cast<char16_t*>(data.data() + 2), (data.size() / 2) - 2);
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (str.startsWith("MSFT100"_L1)) {
|
||||
// We got the OS descriptor!
|
||||
char vendor_code = data[16];
|
||||
@@ -621,8 +637,10 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
continue;
|
||||
}
|
||||
// Hurray! We made it!
|
||||
me->FoundMTPDevice(device, serial);
|
||||
me->FoundMTPDevice(mtp_device, serial);
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,30 +649,39 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
void MacOsDeviceLister::USBDeviceRemovedCallback(void *refcon, io_iterator_t it) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
|
||||
Q_UNUSED(me)
|
||||
|
||||
io_object_t object;
|
||||
while ((object = IOIteratorNext(it))) {
|
||||
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(object));
|
||||
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
|
||||
|
||||
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
|
||||
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
|
||||
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
|
||||
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
|
||||
QString serial = GetSerialForMTPDevice(object);
|
||||
|
||||
MTPDevice device;
|
||||
device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
device.product = QString::fromUtf8([product UTF8String]);
|
||||
device.vendor_id = [vendor_id unsignedShortValue];
|
||||
device.product_id = [product_id unsignedShortValue];
|
||||
const QString serial = GetSerialForMTPDevice(object);
|
||||
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
mtp_device.product = QString::fromUtf8([product UTF8String]);
|
||||
mtp_device.vendor_id = [vendor_id unsignedShortValue];
|
||||
mtp_device.product_id = [product_id unsignedShortValue];
|
||||
|
||||
me->RemovedMTPDevice(serial);
|
||||
#endif // HAVE_MTP
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
|
||||
void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
|
||||
|
||||
int count = mtp_devices_.remove(serial);
|
||||
@@ -668,34 +695,40 @@ void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
|
||||
void MacOsDeviceLister::FoundMTPDevice(const MTPDevice &device, const QString &serial) {
|
||||
|
||||
qLog(Debug) << "New MTP device detected!" << device.bus << device.address;
|
||||
|
||||
mtp_devices_[serial] = device;
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
MTPDevice *d = &mtp_devices_[serial];
|
||||
d->capacity = GetCapacity(urls[0]);
|
||||
d->free_space = GetFreeSpace(urls[0]);
|
||||
const QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
MTPDevice *mtp_device = &mtp_devices_[serial];
|
||||
mtp_device->capacity = GetCapacity(urls[0]);
|
||||
mtp_device->free_space = GetFreeSpace(urls[0]);
|
||||
|
||||
Q_EMIT DeviceAdded(serial);
|
||||
|
||||
}
|
||||
|
||||
bool IsMTPSerial(const QString &serial) { return serial.startsWith("MTP"_L1); }
|
||||
|
||||
#endif // HAVE_MTP
|
||||
|
||||
bool MacOsDeviceLister::IsCDDevice(const QString &serial) const {
|
||||
return cd_devices_.contains(serial);
|
||||
}
|
||||
|
||||
QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
const MTPDevice &device = mtp_devices_[serial];
|
||||
if (device.vendor.isEmpty()) {
|
||||
return device.product;
|
||||
const MTPDevice &mtp_device = mtp_devices_[serial];
|
||||
if (mtp_device.vendor.isEmpty()) {
|
||||
return mtp_device.product;
|
||||
}
|
||||
else {
|
||||
return device.vendor + QLatin1Char(' ') + device.product;
|
||||
return mtp_device.vendor + QLatin1Char(' ') + mtp_device.product;
|
||||
}
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
QString bsd_name = IsCDDevice(serial) ? *cd_devices_.find(serial) : current_devices_[serial];
|
||||
const QString bsd_name = IsCDDevice(serial) ? *cd_devices_.find(serial) : current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
@@ -708,75 +741,86 @@ QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
|
||||
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk));
|
||||
|
||||
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
|
||||
if (vendor.isEmpty()) {
|
||||
return product;
|
||||
}
|
||||
|
||||
return vendor + QLatin1Char(' ') + product;
|
||||
|
||||
}
|
||||
|
||||
QList<QUrl> MacOsDeviceLister::MakeDeviceUrls(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
const MTPDevice &device = mtp_devices_[serial];
|
||||
QString str = QString::asprintf("gphoto2://usb-%d-%d/", device.bus, device.address);
|
||||
const MTPDevice &mtp_device = mtp_devices_[serial];
|
||||
const QString str = QString::asprintf("gphoto2://usb-%d-%d/", mtp_device.bus, mtp_device.address);
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem(u"vendor"_s, device.vendor);
|
||||
url_query.addQueryItem(u"vendor_id"_s, QString::number(device.vendor_id));
|
||||
url_query.addQueryItem(u"product"_s, device.product);
|
||||
url_query.addQueryItem(u"product_id"_s, QString::number(device.product_id));
|
||||
url_query.addQueryItem(u"quirks"_s, QString::number(device.quirks));
|
||||
url_query.addQueryItem(u"vendor"_s, mtp_device.vendor);
|
||||
url_query.addQueryItem(u"vendor_id"_s, QString::number(mtp_device.vendor_id));
|
||||
url_query.addQueryItem(u"product"_s, mtp_device.product);
|
||||
url_query.addQueryItem(u"product_id"_s, QString::number(mtp_device.product_id));
|
||||
url_query.addQueryItem(u"quirks"_s, QString::number(mtp_device.quirks));
|
||||
QUrl url(str);
|
||||
url.setQuery(url_query);
|
||||
return QList<QUrl>() << url;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
if (IsCDDevice(serial)) {
|
||||
return QList<QUrl>() << QUrl(u"cdda:///dev/r"_s + serial);
|
||||
}
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk.get())));
|
||||
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
|
||||
|
||||
QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
QUrl ret = MakeUrlFromLocalPath(path);
|
||||
const QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
const QUrl ret = MakeUrlFromLocalPath(path);
|
||||
|
||||
return QList<QUrl>() << ret;
|
||||
|
||||
}
|
||||
|
||||
QStringList MacOsDeviceLister::DeviceUniqueIDs() {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
return current_devices_.keys() + mtp_devices_.keys();
|
||||
#else
|
||||
return current_devices_.keys();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
return QVariantList();
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
if (IsCDDevice(serial)) {
|
||||
return QVariantList() << u"media-optical"_s;
|
||||
}
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk.get()));
|
||||
QString icon = GetIconForDevice(device.get());
|
||||
const QString icon = GetIconForDevice(device.get());
|
||||
|
||||
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk)));
|
||||
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
|
||||
|
||||
QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
const QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
|
||||
QVariantList ret;
|
||||
ret << GuessIconForPath(path);
|
||||
@@ -784,31 +828,45 @@ QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
|
||||
if (!icon.isEmpty()) {
|
||||
ret << icon;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QString MacOsDeviceLister::DeviceManufacturer(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
return mtp_devices_[serial].vendor;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBVendorString));
|
||||
|
||||
}
|
||||
|
||||
QString MacOsDeviceLister::DeviceModel(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
return mtp_devices_[serial].product;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBProductString));
|
||||
|
||||
}
|
||||
|
||||
quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
return mtp_devices_[serial].capacity;
|
||||
}
|
||||
QString bsd_name = current_devices_[serial];
|
||||
#endif // HAVE_MTP
|
||||
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
@@ -816,7 +874,7 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
||||
|
||||
NSNumber *capacity = reinterpret_cast<NSNumber*>(GetPropertyForDevice(device, CFSTR("Size")));
|
||||
|
||||
quint64 ret = [capacity unsignedLongLongValue];
|
||||
const quint64 ret = [capacity unsignedLongLongValue];
|
||||
|
||||
IOObjectRelease(device);
|
||||
|
||||
@@ -826,10 +884,13 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
||||
|
||||
quint64 MacOsDeviceLister::DeviceFreeSpace(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
return mtp_devices_[serial].free_space;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
@@ -857,9 +918,11 @@ bool MacOsDeviceLister::AskForScan(const QString &serial) const {
|
||||
|
||||
void MacOsDeviceLister::UnmountDevice(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) return;
|
||||
#endif
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, loop_session_, bsd_name.toLatin1().constData()));
|
||||
|
||||
DADiskUnmount(disk, kDADiskUnmountOptionDefault, &DiskUnmountCallback, this);
|
||||
@@ -879,13 +942,16 @@ void MacOsDeviceLister::DiskUnmountCallback(DADiskRef disk, DADissenterRef disse
|
||||
|
||||
void MacOsDeviceLister::UpdateDeviceFreeSpace(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
if (mtp_devices_.contains(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
MTPDevice *d = &mtp_devices_[serial];
|
||||
d->free_space = GetFreeSpace(urls[0]);
|
||||
MTPDevice *mtp_device = &mtp_devices_[serial];
|
||||
mtp_device->free_space = GetFreeSpace(urls[0]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Q_EMIT DeviceChanged(serial);
|
||||
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
|
||||
: QObject(parent),
|
||||
player_(player),
|
||||
playlist_manager_(playlist_manager),
|
||||
initialized_(false) {
|
||||
initialized_(false),
|
||||
status_display_type_(0) {
|
||||
|
||||
QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &RichPresence::EngineStateChanged);
|
||||
QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &RichPresence::CurrentSongChanged);
|
||||
@@ -69,6 +70,7 @@ void RichPresence::ReloadSettings() {
|
||||
Settings s;
|
||||
s.beginGroup(DiscordRPCSettings::kSettingsGroup);
|
||||
const bool enabled = s.value(DiscordRPCSettings::kEnabled, false).toBool();
|
||||
status_display_type_ = s.value(DiscordRPCSettings::kStatusDisplayType, static_cast<int>(DiscordRPCSettings::StatusDisplayType::App)).toInt();
|
||||
s.endGroup();
|
||||
|
||||
if (enabled && !initialized_) {
|
||||
@@ -117,7 +119,11 @@ void RichPresence::SendPresenceUpdate() {
|
||||
|
||||
::DiscordRichPresence presence_data{};
|
||||
memset(&presence_data, 0, sizeof(presence_data));
|
||||
presence_data.type = 2; // Listening
|
||||
|
||||
// Listening to
|
||||
presence_data.type = 2;
|
||||
presence_data.status_display_type = status_display_type_;
|
||||
|
||||
presence_data.largeImageKey = kStrawberryIconResourceName;
|
||||
presence_data.smallImageKey = kStrawberryIconResourceName;
|
||||
presence_data.smallImageText = kStrawberryIconDescription;
|
||||
@@ -126,7 +132,9 @@ void RichPresence::SendPresenceUpdate() {
|
||||
QByteArray artist;
|
||||
if (!activity_.artist.isEmpty()) {
|
||||
artist = activity_.artist.toUtf8();
|
||||
artist.prepend(tr("by ").toUtf8());
|
||||
if (artist.size() < 2) { // Discord activity 2 char min. fix
|
||||
artist.append(" ");
|
||||
}
|
||||
presence_data.state = artist.constData();
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ class RichPresence : public QObject {
|
||||
};
|
||||
Activity activity_;
|
||||
bool initialized_;
|
||||
int status_display_type_;
|
||||
};
|
||||
|
||||
} // namespace discord
|
||||
|
||||
@@ -102,7 +102,7 @@ class MusicBrainzClient : public QObject {
|
||||
void CancelAll();
|
||||
|
||||
Q_SIGNALS:
|
||||
// Finished signal emitted when fechting songs tags
|
||||
// Finished signal emitted when feching songs tags
|
||||
void Finished(const int id, const MusicBrainzClient::ResultList &result, const QString &error = QString());
|
||||
// Finished signal emitted when fechting album's songs tags using DiscId
|
||||
void DiscIdFinished(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &result, const QString &error = QString());
|
||||
|
||||
@@ -96,6 +96,7 @@ void SongLoaderInserter::Load(Playlist *destination, const int row, const bool p
|
||||
else {
|
||||
(void)QtConcurrent::run(&SongLoaderInserter::AsyncLoad, this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Load audio CD tracks:
|
||||
@@ -111,13 +112,15 @@ void SongLoaderInserter::LoadAudioCD(Playlist *destination, const int row, const
|
||||
enqueue_next_ = enqueue_next;
|
||||
|
||||
SongLoader *loader = new SongLoader(url_handlers_, collection_backend_, tagreader_client_, this);
|
||||
QObject::connect(loader, &SongLoader::AudioCDTracksLoadFinished, this, [this, loader]() { AudioCDTracksLoadFinished(loader); });
|
||||
QObject::connect(loader, &SongLoader::LoadAudioCDFinished, this, &SongLoaderInserter::AudioCDTagsLoaded);
|
||||
QObject::connect(loader, &SongLoader::AudioCDTracksLoaded, this, &SongLoaderInserter::AudioCDTracksLoadedSlot);
|
||||
QObject::connect(loader, &SongLoader::AudioCDTracksUpdated, this, &SongLoaderInserter::AudioCDTracksUpdatedSlot);
|
||||
QObject::connect(loader, &SongLoader::AudioCDLoadingFinished, this, &SongLoaderInserter::AudioCDLoadingFinishedSlot);
|
||||
qLog(Info) << "Loading audio CD...";
|
||||
const SongLoader::Result result = loader->LoadAudioCD();
|
||||
if (result == SongLoader::Result::Error) {
|
||||
if (loader->errors().isEmpty())
|
||||
if (loader->errors().isEmpty()) {
|
||||
Q_EMIT Error(tr("Error while loading audio CD."));
|
||||
}
|
||||
else {
|
||||
const QStringList errors = loader->errors();
|
||||
for (const QString &error : errors) {
|
||||
@@ -126,13 +129,17 @@ void SongLoaderInserter::LoadAudioCD(Playlist *destination, const int row, const
|
||||
}
|
||||
delete loader;
|
||||
}
|
||||
|
||||
// Songs will be loaded later: see AudioCDTracksLoadFinished and AudioCDTagsLoaded slots
|
||||
|
||||
}
|
||||
|
||||
void SongLoaderInserter::DestinationDestroyed() { destination_ = nullptr; }
|
||||
|
||||
void SongLoaderInserter::AudioCDTracksLoadFinished(SongLoader *loader) {
|
||||
void SongLoaderInserter::AudioCDTracksLoadedSlot() {
|
||||
|
||||
SongLoader *loader = qobject_cast<SongLoader*>(sender());
|
||||
if (!loader) return;
|
||||
|
||||
songs_ = loader->songs();
|
||||
if (songs_.isEmpty()) {
|
||||
@@ -147,17 +154,18 @@ void SongLoaderInserter::AudioCDTracksLoadFinished(SongLoader *loader) {
|
||||
|
||||
}
|
||||
|
||||
void SongLoaderInserter::AudioCDTagsLoaded(const bool success) {
|
||||
void SongLoaderInserter::AudioCDTracksUpdatedSlot() {
|
||||
|
||||
SongLoader *loader = qobject_cast<SongLoader*>(sender());
|
||||
if (!loader || !destination_) return;
|
||||
if (!loader || loader->songs().isEmpty() || !destination_) return;
|
||||
|
||||
if (success) {
|
||||
destination_->UpdateItems(loader->songs());
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Error while getting audio CD metadata from MusicBrainz";
|
||||
}
|
||||
destination_->UpdateItems(loader->songs());
|
||||
|
||||
}
|
||||
|
||||
void SongLoaderInserter::AudioCDLoadingFinishedSlot(const bool success) {
|
||||
|
||||
Q_UNUSED(success)
|
||||
|
||||
deleteLater();
|
||||
|
||||
|
||||
@@ -62,8 +62,9 @@ class SongLoaderInserter : public QObject {
|
||||
|
||||
private Q_SLOTS:
|
||||
void DestinationDestroyed();
|
||||
void AudioCDTracksLoadFinished(SongLoader *loader);
|
||||
void AudioCDTagsLoaded(const bool success);
|
||||
void AudioCDTracksLoadedSlot();
|
||||
void AudioCDTracksUpdatedSlot();
|
||||
void AudioCDLoadingFinishedSlot(const bool success);
|
||||
void InsertSongs();
|
||||
|
||||
private:
|
||||
|
||||
@@ -209,6 +209,19 @@ void NotificationsSettingsPage::Load() {
|
||||
// Discord
|
||||
s.beginGroup(DiscordRPCSettings::kSettingsGroup);
|
||||
ui_->richpresence_enabled->setChecked(s.value(DiscordRPCSettings::kEnabled, false).toBool());
|
||||
|
||||
const DiscordRPCSettings::StatusDisplayType discord_status_display_type = static_cast<DiscordRPCSettings::StatusDisplayType>(s.value(DiscordRPCSettings::kStatusDisplayType, static_cast<int>(DiscordRPCSettings::StatusDisplayType::App)).toInt());
|
||||
switch (discord_status_display_type) {
|
||||
case DiscordRPCSettings::StatusDisplayType::App:
|
||||
ui_->richpresence_listening_to_app->setChecked(true);
|
||||
break;
|
||||
case DiscordRPCSettings::StatusDisplayType::Artist:
|
||||
ui_->richpresence_listening_to_artist->setChecked(true);
|
||||
break;
|
||||
case DiscordRPCSettings::StatusDisplayType::Song:
|
||||
ui_->richpresence_listening_to_song->setChecked(true);
|
||||
break;
|
||||
}
|
||||
s.endGroup();
|
||||
|
||||
UpdatePopupVisible();
|
||||
@@ -229,6 +242,11 @@ void NotificationsSettingsPage::Save() {
|
||||
else if (osd_->SupportsTrayPopups() && ui_->notifications_tray->isChecked()) osd_type = OSDSettings::Type::TrayPopup;
|
||||
else if (osd_->SupportsOSDPretty() && ui_->notifications_pretty->isChecked()) osd_type = OSDSettings::Type::Pretty;
|
||||
|
||||
DiscordRPCSettings::StatusDisplayType discord_status_display_type = DiscordRPCSettings::StatusDisplayType::App;
|
||||
if (ui_->richpresence_listening_to_app->isChecked()) discord_status_display_type = DiscordRPCSettings::StatusDisplayType::App;
|
||||
else if (ui_->richpresence_listening_to_artist->isChecked()) discord_status_display_type = DiscordRPCSettings::StatusDisplayType::Artist;
|
||||
else if (ui_->richpresence_listening_to_song->isChecked()) discord_status_display_type = DiscordRPCSettings::StatusDisplayType::Song;
|
||||
|
||||
s.beginGroup(OSDSettings::kSettingsGroup);
|
||||
s.setValue(OSDSettings::kType, static_cast<int>(osd_type));
|
||||
s.setValue(OSDSettings::kTimeout, ui_->notifications_duration->value() * 1000);
|
||||
@@ -255,7 +273,9 @@ void NotificationsSettingsPage::Save() {
|
||||
|
||||
s.beginGroup(DiscordRPCSettings::kSettingsGroup);
|
||||
s.setValue(DiscordRPCSettings::kEnabled, ui_->richpresence_enabled->isChecked());
|
||||
s.setValue(DiscordRPCSettings::kStatusDisplayType, static_cast<int>(discord_status_display_type));
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void NotificationsSettingsPage::PrettyOpacityChanged(int value) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>518</width>
|
||||
<height>778</height>
|
||||
<height>844</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -380,6 +380,36 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="richpresence_listening_to">
|
||||
<property name="title">
|
||||
<string>"Listening to..."</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_11">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="richpresence_listening_to_app">
|
||||
<property name="text">
|
||||
<string>Strawberry</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="richpresence_listening_to_artist">
|
||||
<property name="text">
|
||||
<string>Artist name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="richpresence_listening_to_song">
|
||||
<property name="text">
|
||||
<string>Song title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
*Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -47,6 +47,7 @@ class SystemTrayIcon : public QObject {
|
||||
bool isVisible() const { return true; }
|
||||
void setVisible(const bool) {}
|
||||
|
||||
void SetDevicePixelRatioF(const qreal device_pixel_ratio);
|
||||
void SetTrayiconProgress(const bool enabled);
|
||||
|
||||
void SetupMenu(QAction *previous, QAction *play, QAction *stop, QAction *stop_after, QAction *next, QAction *mute, QAction *love, QAction *quit);
|
||||
@@ -93,6 +94,7 @@ class SystemTrayIcon : public QObject {
|
||||
QPixmap playing_icon_;
|
||||
QPixmap paused_icon_;
|
||||
QPixmap current_state_icon_;
|
||||
qreal device_pixel_ratio_;
|
||||
bool trayicon_progress_;
|
||||
int song_progress_;
|
||||
Q_DISABLE_COPY(SystemTrayIcon);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -159,6 +159,7 @@ SystemTrayIcon::SystemTrayIcon(QObject *parent)
|
||||
grey_icon_(QPixmap(u":/pictures/strawberry-grey.png"_s).scaled(128, 128, Qt::KeepAspectRatio, Qt::SmoothTransformation)),
|
||||
playing_icon_(u":/pictures/tiny-play.png"_s),
|
||||
paused_icon_(u":/pictures/tiny-pause.png"_s),
|
||||
device_pixel_ratio_(1.0),
|
||||
trayicon_progress_(false),
|
||||
song_progress_(0) {
|
||||
|
||||
@@ -168,6 +169,12 @@ SystemTrayIcon::SystemTrayIcon(QObject *parent)
|
||||
|
||||
SystemTrayIcon::~SystemTrayIcon() {}
|
||||
|
||||
void SystemTrayIcon::SetDevicePixelRatioF(const qreal device_pixel_ratio) {
|
||||
|
||||
device_pixel_ratio_ = device_pixel_ratio;
|
||||
|
||||
}
|
||||
|
||||
void SystemTrayIcon::SetTrayiconProgress(const bool enabled) {
|
||||
|
||||
trayicon_progress_ = enabled;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -36,33 +36,30 @@
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace {
|
||||
constexpr int kSystemTrayIconSize = 48;
|
||||
}
|
||||
|
||||
SystemTrayIcon::SystemTrayIcon(QObject *parent)
|
||||
: QSystemTrayIcon(parent),
|
||||
menu_(new QMenu),
|
||||
pixmap_playing_(u":/pictures/tiny-play.png"_s),
|
||||
pixmap_paused_(u":/pictures/tiny-pause.png"_s),
|
||||
icon_normal_(IconLoader::Load(u"strawberry"_s)),
|
||||
icon_grey_(IconLoader::Load(u"strawberry-grey"_s)),
|
||||
icon_playing_(QIcon(u":/pictures/tiny-play.png"_s)),
|
||||
icon_paused_(QIcon(u":/pictures/tiny-pause.png"_s)),
|
||||
action_play_pause_(nullptr),
|
||||
action_stop_(nullptr),
|
||||
action_stop_after_this_track_(nullptr),
|
||||
action_mute_(nullptr),
|
||||
action_love_(nullptr),
|
||||
available_(false),
|
||||
device_pixel_ratio_(1.0),
|
||||
trayicon_progress_(false),
|
||||
song_progress_(0) {
|
||||
|
||||
const QIcon icon = IconLoader::Load(u"strawberry"_s);
|
||||
const QIcon icon_grey = IconLoader::Load(u"strawberry-grey"_s);
|
||||
pixmap_normal_ = icon.pixmap(48, QIcon::Normal);
|
||||
if (icon_grey.isNull()) {
|
||||
pixmap_grey_ = icon.pixmap(48, QIcon::Disabled);
|
||||
}
|
||||
else {
|
||||
pixmap_grey_ = icon_grey.pixmap(48, QIcon::Disabled);
|
||||
}
|
||||
|
||||
if (isSystemTrayAvailable()) {
|
||||
available_ = true;
|
||||
setIcon(icon);
|
||||
setIcon(icon_normal_);
|
||||
setToolTip(QCoreApplication::applicationName());
|
||||
}
|
||||
|
||||
@@ -74,6 +71,41 @@ SystemTrayIcon::~SystemTrayIcon() {
|
||||
delete menu_;
|
||||
}
|
||||
|
||||
void SystemTrayIcon::InitPixmaps() {
|
||||
|
||||
if (pixmap_normal_.isNull() || pixmap_normal_.devicePixelRatioF() != device_pixel_ratio_) {
|
||||
pixmap_normal_ = icon_normal_.pixmap(QSize(kSystemTrayIconSize, kSystemTrayIconSize), device_pixel_ratio_, QIcon::Normal);
|
||||
}
|
||||
|
||||
if (pixmap_grey_.isNull() || pixmap_grey_.devicePixelRatioF() != device_pixel_ratio_) {
|
||||
if (icon_grey_.isNull()) {
|
||||
pixmap_grey_ = icon_normal_.pixmap(QSize(kSystemTrayIconSize, kSystemTrayIconSize), device_pixel_ratio_, QIcon::Disabled);
|
||||
}
|
||||
else {
|
||||
pixmap_grey_ = icon_grey_.pixmap(QSize(kSystemTrayIconSize, kSystemTrayIconSize), device_pixel_ratio_, QIcon::Disabled);
|
||||
}
|
||||
}
|
||||
|
||||
if (pixmap_playing_.isNull() || pixmap_playing_.devicePixelRatioF() != device_pixel_ratio_) {
|
||||
pixmap_playing_ = icon_playing_.pixmap(icon_playing_.availableSizes().at(0));
|
||||
pixmap_playing_.setDevicePixelRatio(device_pixel_ratio_);
|
||||
}
|
||||
|
||||
if (pixmap_paused_.isNull() || pixmap_paused_.devicePixelRatioF() != device_pixel_ratio_) {
|
||||
pixmap_paused_ = icon_paused_.pixmap(icon_paused_.availableSizes().at(0));
|
||||
pixmap_paused_.setDevicePixelRatio(device_pixel_ratio_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SystemTrayIcon::SetDevicePixelRatioF(const qreal device_pixel_ratio) {
|
||||
|
||||
device_pixel_ratio_ = device_pixel_ratio;
|
||||
|
||||
InitPixmaps();
|
||||
|
||||
}
|
||||
|
||||
void SystemTrayIcon::SetTrayiconProgress(const bool enabled) {
|
||||
|
||||
trayicon_progress_ = enabled;
|
||||
@@ -131,13 +163,17 @@ void SystemTrayIcon::ShowPopup(const QString &summary, const QString &message, c
|
||||
|
||||
void SystemTrayIcon::UpdateIcon() {
|
||||
|
||||
if (available_) setIcon(CreateIcon(pixmap_normal_, pixmap_grey_));
|
||||
if (available_) {
|
||||
InitPixmaps();
|
||||
setIcon(CreateIcon(pixmap_normal_, pixmap_grey_));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SystemTrayIcon::SetPlaying(bool enable_play_pause) {
|
||||
void SystemTrayIcon::SetPlaying(const bool enable_play_pause) {
|
||||
|
||||
current_state_icon_ = pixmap_playing_;
|
||||
|
||||
UpdateIcon();
|
||||
|
||||
action_stop_->setEnabled(true);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -46,6 +46,9 @@ class SystemTrayIcon : public QSystemTrayIcon {
|
||||
|
||||
bool IsSystemTrayAvailable() const { return available_; }
|
||||
|
||||
void InitPixmaps();
|
||||
|
||||
void SetDevicePixelRatioF(const qreal device_pixel_ratio);
|
||||
void SetTrayiconProgress(const bool enabled);
|
||||
|
||||
void SetupMenu(QAction *previous, QAction *play, QAction *stop, QAction *stop_after, QAction *next, QAction *mute, QAction *love, QAction *quit);
|
||||
@@ -82,6 +85,12 @@ class SystemTrayIcon : public QSystemTrayIcon {
|
||||
|
||||
private:
|
||||
QMenu *menu_;
|
||||
|
||||
QIcon icon_normal_;
|
||||
QIcon icon_grey_;
|
||||
QIcon icon_playing_;
|
||||
QIcon icon_paused_;
|
||||
|
||||
QPixmap pixmap_normal_;
|
||||
QPixmap pixmap_grey_;
|
||||
QPixmap pixmap_playing_;
|
||||
@@ -94,6 +103,7 @@ class SystemTrayIcon : public QSystemTrayIcon {
|
||||
QAction *action_love_;
|
||||
|
||||
bool available_;
|
||||
qreal device_pixel_ratio_;
|
||||
bool trayicon_progress_;
|
||||
int song_progress_;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -35,45 +35,54 @@
|
||||
# include "qtsystemtrayicon.h"
|
||||
#endif
|
||||
|
||||
QPixmap SystemTrayIcon::CreateIcon(const QPixmap &icon, const QPixmap &grey_icon) {
|
||||
QPixmap SystemTrayIcon::CreateIcon(const QPixmap &pixmap_icon_normal, const QPixmap &pixmap_icon_grey) {
|
||||
|
||||
QRect rect(icon.rect());
|
||||
const qreal dpr = pixmap_icon_normal.devicePixelRatio();
|
||||
const QRect pixmap_rect = pixmap_icon_normal.rect();
|
||||
|
||||
QPixmap ret(icon);
|
||||
QPainter p(&ret);
|
||||
QPixmap pixmap_drawn(pixmap_icon_normal.size());
|
||||
pixmap_drawn.setDevicePixelRatio(dpr);
|
||||
pixmap_drawn.fill(Qt::transparent);
|
||||
|
||||
QPainter p(&pixmap_drawn);
|
||||
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||
p.drawPixmap(0, 0, pixmap_icon_normal);
|
||||
|
||||
if (trayicon_progress_) {
|
||||
// The angle of the line that's used to cover the icon.
|
||||
// Centered on rect.topLeft()
|
||||
double angle = static_cast<double>(100 - song_progress_) / 100.0 * M_PI_2;
|
||||
double length = sqrt(pow(rect.width(), 2.0) + pow(rect.height(), 2.0));
|
||||
const double angle = static_cast<double>(100 - song_progress_) / 100.0 * M_PI_2;
|
||||
const double length = std::sqrt(std::pow(pixmap_rect.width(), 2.0) + std::pow(pixmap_rect.height(), 2.0));
|
||||
|
||||
QPolygon mask;
|
||||
mask << rect.topLeft();
|
||||
mask << rect.topLeft() + QPoint(static_cast<int>(length * sin(angle)), static_cast<int>(length * cos(angle)));
|
||||
mask << pixmap_rect.topLeft();
|
||||
mask << pixmap_rect.topLeft() + QPoint(static_cast<int>(length * std::sin(angle)), static_cast<int>(length * std::cos(angle)));
|
||||
|
||||
if (song_progress_ > 50) mask << rect.bottomRight();
|
||||
if (song_progress_ > 50) {
|
||||
mask << pixmap_rect.bottomRight();
|
||||
}
|
||||
|
||||
mask << rect.topRight();
|
||||
mask << rect.topLeft();
|
||||
mask << pixmap_rect.topRight();
|
||||
mask << pixmap_rect.topLeft();
|
||||
|
||||
// Draw the grey bit
|
||||
p.setClipRegion(mask);
|
||||
p.drawPixmap(0, 0, grey_icon);
|
||||
p.drawPixmap(0, 0, pixmap_icon_grey);
|
||||
p.setClipping(false);
|
||||
}
|
||||
|
||||
// Draw the playing or paused icon in the top-right
|
||||
if (!current_state_icon_.isNull()) {
|
||||
int height = rect.height() / 2;
|
||||
QPixmap scaled(current_state_icon_.scaledToHeight(height, Qt::SmoothTransformation));
|
||||
const int height = pixmap_rect.height() / 2;
|
||||
QPixmap current_state_scaled = current_state_icon_.scaledToHeight(height, Qt::SmoothTransformation);
|
||||
current_state_scaled.setDevicePixelRatio(dpr);
|
||||
|
||||
QRect state_rect(rect.width() - scaled.width(), 0, scaled.width(), scaled.height());
|
||||
p.drawPixmap(state_rect, scaled);
|
||||
const QRect state_rect(static_cast<int>((pixmap_rect.width() - current_state_scaled.width()) / dpr), 0, static_cast<int>(current_state_scaled.width() / dpr), static_cast<int>(current_state_scaled.height() / dpr));
|
||||
p.drawPixmap(state_rect, current_state_scaled);
|
||||
}
|
||||
|
||||
p.end();
|
||||
|
||||
return ret;
|
||||
return pixmap_drawn;
|
||||
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ TagReaderResult TagReaderTagLib::Read(SharedPtr<TagLib::FileRef> fileref, Song *
|
||||
TagLib::List<TagLib::FLAC::Picture*> pictures = vorbis_comment->pictureList();
|
||||
if (!pictures.isEmpty()) {
|
||||
for (TagLib::FLAC::Picture *picture : pictures) {
|
||||
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
|
||||
if ((picture->type() == TagLib::FLAC::Picture::FrontCover || picture->type() == TagLib::FLAC::Picture::Other) && picture->data().size() > 0) {
|
||||
song->set_art_embedded(true);
|
||||
break;
|
||||
}
|
||||
@@ -388,7 +388,7 @@ TagReaderResult TagReaderTagLib::Read(SharedPtr<TagLib::FileRef> fileref, Song *
|
||||
TagLib::List<TagLib::FLAC::Picture*> pictures = file_flac->pictureList();
|
||||
if (!pictures.isEmpty()) {
|
||||
for (TagLib::FLAC::Picture *picture : pictures) {
|
||||
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
|
||||
if ((picture->type() == TagLib::FLAC::Picture::FrontCover || picture->type() == TagLib::FLAC::Picture::Other) && picture->data().size() > 0) {
|
||||
song->set_art_embedded(true);
|
||||
break;
|
||||
}
|
||||
@@ -1472,12 +1472,19 @@ TagReaderResult TagReaderTagLib::LoadEmbeddedCover(const QString &filename, QByt
|
||||
TagLib::List<TagLib::FLAC::Picture*> pictures = file_flac->pictureList();
|
||||
if (!pictures.isEmpty()) {
|
||||
for (TagLib::FLAC::Picture *picture : pictures) {
|
||||
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
|
||||
data = QByteArray(picture->data().data(), picture->data().size());
|
||||
if (!data.isEmpty()) {
|
||||
return TagReaderResult::ErrorCode::Success;
|
||||
}
|
||||
if (picture->data().size() <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (picture->type() == TagLib::FLAC::Picture::FrontCover) {
|
||||
data = QByteArray(picture->data().data(), picture->data().size());
|
||||
return TagReaderResult::ErrorCode::Success;
|
||||
}
|
||||
else if (picture->type() == TagLib::FLAC::Picture::Other) {
|
||||
data = QByteArray(picture->data().data(), picture->data().size());
|
||||
}
|
||||
}
|
||||
if (!data.isEmpty()) {
|
||||
return TagReaderResult::ErrorCode::Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1515,21 +1522,27 @@ TagReaderResult TagReaderTagLib::LoadEmbeddedCover(const QString &filename, QByt
|
||||
|
||||
// Ogg Vorbis / Opus / Speex
|
||||
if (TagLib::Ogg::XiphComment *vorbis_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
|
||||
TagLib::Ogg::FieldListMap map = vorbis_comment->fieldListMap();
|
||||
|
||||
TagLib::List<TagLib::FLAC::Picture*> pictures = vorbis_comment->pictureList();
|
||||
if (!pictures.isEmpty()) {
|
||||
for (TagLib::FLAC::Picture *picture : pictures) {
|
||||
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
|
||||
data = QByteArray(picture->data().data(), picture->data().size());
|
||||
if (!data.isEmpty()) {
|
||||
return TagReaderResult::ErrorCode::Success;
|
||||
}
|
||||
if (picture->data().size() <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (picture->type() == TagLib::FLAC::Picture::FrontCover) {
|
||||
data = QByteArray(picture->data().data(), picture->data().size());
|
||||
return TagReaderResult::ErrorCode::Success;
|
||||
}
|
||||
else if (picture->type() == TagLib::FLAC::Picture::Other) {
|
||||
data = QByteArray(picture->data().data(), picture->data().size());
|
||||
}
|
||||
}
|
||||
if (!data.isEmpty()) {
|
||||
return TagReaderResult::ErrorCode::Success;
|
||||
}
|
||||
}
|
||||
|
||||
// Ogg lacks a definitive standard for embedding cover art, but it seems b64 encoding a field called COVERART is the general convention
|
||||
const TagLib::Ogg::FieldListMap map = vorbis_comment->fieldListMap();
|
||||
if (map.contains(kVorbisComment_CoverArt)) {
|
||||
data = QByteArray::fromBase64(map[kVorbisComment_CoverArt].toString().toCString());
|
||||
if (!data.isEmpty()) {
|
||||
|
||||
Reference in New Issue
Block a user