Compare commits

..

1 Commits

Author SHA1 Message Date
Jonas Kvinge
6102679496 Change GuiPrivate to XcbQpaPrivate 2025-12-29 00:48:46 +01:00
31 changed files with 262 additions and 1533 deletions

View File

@@ -747,7 +747,7 @@ jobs:
df -h
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.3.7
uses: vmactions/freebsd-vm@v1.3.2
with:
usesh: true
mem: 8192
@@ -772,7 +772,7 @@ jobs:
submodules: recursive
- name: Build OpenBSD
id: build-openbsd
uses: vmactions/openbsd-vm@v1.3.1
uses: vmactions/openbsd-vm@v1.2.9
with:
usesh: true
mem: 4096
@@ -845,7 +845,7 @@ jobs:
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
- name: Download macOS dependencies
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies$(test "${{env.arch}}" = "x86_64" && echo "-intel" || echo "")/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
- name: Extract macOS dependencies
run: sudo tar -C / -xf strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
@@ -898,7 +898,7 @@ jobs:
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15-intel'
working-directory: build
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib,libbrotlidec.1.dylib,libsoup-3.0.0.dylib,libnghttp2.14.dylib,libpsl.5.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'

View File

@@ -84,6 +84,8 @@ if(MSVC)
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
else()
list(APPEND COMPILE_OPTIONS
$<$<COMPILE_LANGUAGE:C>:-std=c11>
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
-Wall
-Wextra
-Wpedantic
@@ -216,16 +218,16 @@ set(QT_VERSION_MAJOR 6)
set(QT_MIN_VERSION 6.4.0)
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR})
set(QT_COMPONENTS Core Concurrent Gui Widgets Network Sql)
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test)
set(QT_OPTIONAL_COMPONENTS LinguistTools Test)
if(UNIX AND NOT APPLE)
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
list(APPEND QT_OPTIONAL_COMPONENTS DBus XcbQpaPrivate)
endif()
set(QT_NO_PRIVATE_MODULE_WARNING ON)
find_package(Qt${QT_VERSION_MAJOR} ${QT_MIN_VERSION} COMPONENTS ${QT_COMPONENTS} REQUIRED OPTIONAL_COMPONENTS ${QT_OPTIONAL_COMPONENTS})
if(TARGET "Qt${QT_VERSION_MAJOR}::GuiPrivate")
set(QT_GUI_PRIVATE_FOUND ON)
if(TARGET "Qt${QT_VERSION_MAJOR}::XcbQpaPrivate")
set(QT_XCB_QPA_PRIVATE_FOUND ON)
endif()
if(Qt${QT_VERSION_MAJOR}DBus_FOUND)
@@ -253,6 +255,7 @@ find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
if(APPLE)
find_library(SPARKLE Sparkle)
#find_package(SPMediaKeyTap REQUIRED)
endif()
if(WIN32)
@@ -366,8 +369,8 @@ if(APPLE OR WIN32)
)
endif()
optional_component(QPA_QPLATFORMNATIVEINTERFACE ON "QPA Platform Native Interface"
DEPENDS "Qt Gui Private" QT_GUI_PRIVATE_FOUND
optional_component(QT_XCB_QPA_PRIVATE ON "XCB QPA Platform Native Interface"
DEPENDS "Qt XCB QPA Private" QT_XCB_QPA_PRIVATE_FOUND
)
optional_component(STREAMTAGREADER ON "Stream tagreader"
@@ -820,8 +823,6 @@ set(SOURCES
src/fileview/fileview.cpp
src/fileview/fileviewlist.cpp
src/fileview/fileviewtree.cpp
src/fileview/fileviewtreemodel.cpp
src/device/devicemanager.cpp
src/device/devicelister.cpp
@@ -1111,8 +1112,6 @@ set(HEADERS
src/fileview/fileview.h
src/fileview/fileviewlist.h
src/fileview/fileviewtree.h
src/fileview/fileviewtreemodel.h
src/device/devicemanager.h
src/device/devicelister.h
@@ -1215,10 +1214,6 @@ set(UI
src/device/deviceviewcontainer.ui
)
if(UNIX)
optional_source(UNIX SOURCES src/core/unixsignalwatcher.cpp HEADERS src/core/unixsignalwatcher.h)
endif()
if(APPLE)
optional_source(APPLE
SOURCES
@@ -1554,7 +1549,7 @@ target_link_libraries(strawberry_lib PUBLIC
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Sql
$<$<BOOL:${HAVE_DBUS}>:Qt${QT_VERSION_MAJOR}::DBus>
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
$<$<BOOL:${HAVE_QT_XCB_QPA_PRIVATE}>:Qt${QT_VERSION_MAJOR}::XcbQpaPrivate>
ICU::uc
ICU::i18n
$<$<BOOL:${HAVE_STREAMTAGREADER}>:PkgConfig::LIBSPARSEHASH>

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2026, 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
@@ -537,24 +537,10 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
ScopedTransaction transaction(&db);
for (const CollectionSubdirectory &subdir : subdirs) {
// See if this subdirectory already exists in the database
bool exists = false;
{
if (subdir.mtime == 0) {
// Delete the subdirectory
SqlQuery q(db);
q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
exists = q.next();
}
if (exists) {
SqlQuery q(db);
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":mtime"_s, subdir.mtime);
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
if (!q.Exec()) {
@@ -563,36 +549,42 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
}
}
else {
SqlQuery q(db);
q.prepare(QStringLiteral("INSERT INTO %1 (directory_id, path, mtime) VALUES (:id, :path, :mtime)").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
q.BindValue(u":mtime"_s, subdir.mtime);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
// See if this subdirectory already exists in the database
bool exists = false;
{
SqlQuery q(db);
q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
exists = q.next();
}
}
}
transaction.Commit();
}
void CollectionBackend::DeleteSubdirs(const CollectionSubdirectoryList &subdirs) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction transaction(&db);
for (const CollectionSubdirectory &subdir : subdirs) {
SqlQuery q(db);
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
if (exists) {
SqlQuery q(db);
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":mtime"_s, subdir.mtime);
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
else {
SqlQuery q(db);
q.prepare(QStringLiteral("INSERT INTO %1 (directory_id, path, mtime) VALUES (:id, :path, :mtime)").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
q.BindValue(u":mtime"_s, subdir.mtime);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
}
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2026, 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
@@ -252,7 +252,6 @@ class CollectionBackend : public CollectionBackendInterface {
void DeleteSongsByUrls(const QList<QUrl> &url);
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
void DeleteSubdirs(const CollectionSubdirectoryList &subdirs);
void CompilationsNeedUpdating();
void UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded);
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual);

View File

@@ -124,7 +124,6 @@ void CollectionLibrary::Init() {
QObject::connect(watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(watcher_, &CollectionWatcher::SubdirsDeleted, &*backend_, &CollectionBackend::DeleteSubdirs);
QObject::connect(watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);

View File

@@ -383,7 +383,7 @@ void CollectionView::keyPressEvent(QKeyEvent *e) {
case Qt::Key_Enter:
case Qt::Key_Return:
if (currentIndex().isValid()) {
Q_EMIT doubleClicked(currentIndex());
AddToPlaylist();
}
e->accept();
break;

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2026, 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
@@ -261,7 +261,7 @@ void CollectionWatcher::ReloadSettings() {
CollectionWatcher::ScanTransaction::ScanTransaction(CollectionWatcher *watcher, const int dir, const bool incremental, const bool ignores_mtime, const bool mark_songs_unavailable)
: progress_(0),
progress_max_(0),
dir_id_(dir),
dir_(dir),
incremental_(incremental),
ignores_mtime_(ignores_mtime),
mark_songs_unavailable_(mark_songs_unavailable),
@@ -313,19 +313,6 @@ void CollectionWatcher::ScanTransaction::AddToProgressMax(const quint64 n) {
void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
if (!deleted_subdirs.isEmpty()) {
Q_EMIT watcher_->SubdirsDeleted(deleted_subdirs);
}
if (!new_subdirs.isEmpty()) {
Q_EMIT watcher_->SubdirsDiscovered(new_subdirs);
}
if (!touched_subdirs.isEmpty()) {
Q_EMIT watcher_->SubdirsMTimeUpdated(touched_subdirs);
touched_subdirs.clear();
}
if (!deleted_songs.isEmpty()) {
if (mark_songs_unavailable_ && watcher_->source() == Song::Source::Collection) {
Q_EMIT watcher_->SongsUnavailable(deleted_songs);
@@ -351,24 +338,34 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
readded_songs.clear();
}
if (!new_subdirs.isEmpty()) {
Q_EMIT watcher_->SubdirsDiscovered(new_subdirs);
}
if (!touched_subdirs.isEmpty()) {
Q_EMIT watcher_->SubdirsMTimeUpdated(touched_subdirs);
touched_subdirs.clear();
}
for (const CollectionSubdirectory &subdir : std::as_const(deleted_subdirs)) {
if (watcher_->watched_dirs_.contains(dir_id_)) {
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_id_], subdir);
if (watcher_->watched_dirs_.contains(dir_)) {
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
}
}
deleted_subdirs.clear();
if (watcher_->monitor_) {
// Watch the new subdirectories
for (const CollectionSubdirectory &subdir : std::as_const(new_subdirs)) {
if (watcher_->watched_dirs_.contains(dir_id_)) {
watcher_->AddWatch(watcher_->watched_dirs_[dir_id_], subdir.path);
if (watcher_->watched_dirs_.contains(dir_)) {
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
}
}
}
new_subdirs.clear();
if (incremental_ || ignores_mtime_) {
Q_EMIT watcher_->UpdateLastSeen(dir_id_, expire_unavailable_songs_days_);
Q_EMIT watcher_->UpdateLastSeen(dir_, expire_unavailable_songs_days_);
}
}
@@ -377,7 +374,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) {
if (cached_songs_dirty_) {
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_id_);
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
cached_songs_.insert(p, song);
@@ -396,7 +393,7 @@ SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QStri
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QString &path) {
if (cached_songs_missing_fingerprint_dirty_) {
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_id_);
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
cached_songs_missing_fingerprint_.insert(p, song);
@@ -411,7 +408,7 @@ bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QS
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingLoudnessCharacteristics(const QString &path) {
if (cached_songs_missing_loudness_characteristics_dirty_) {
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_id_);
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
cached_songs_missing_loudness_characteristics_.insert(p, song);
@@ -433,7 +430,7 @@ void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const CollectionSubdire
bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
}
return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const CollectionSubdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; });
@@ -443,7 +440,7 @@ bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
}
CollectionSubdirectoryList ret;
@@ -460,7 +457,7 @@ CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdi
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
}
return known_subdirs_;
@@ -497,7 +494,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
const quint64 files_count = FilesCountForPath(&transaction, dir.path);
transaction.SetKnownSubdirs(subdirs);
transaction.AddToProgressMax(files_count);
ScanSubdirectory(dir, dir.path, CollectionSubdirectory(), files_count, &transaction);
ScanSubdirectory(dir.path, CollectionSubdirectory(), files_count, &transaction);
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
}
else {
@@ -515,7 +512,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
transaction.AddToProgressMax(files_count);
for (const CollectionSubdirectory &subdir : subdirs) {
if (stop_or_abort_requested()) break;
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
}
if (!stop_or_abort_requested()) {
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
@@ -527,7 +524,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
}
void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
const QFileInfo path_info(path);
@@ -539,8 +536,8 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
return;
}
// Do not scan symlinked dirs that are already in collection
for (const CollectionDirectory &i : std::as_const(watched_dirs_)) {
if (real_path.startsWith(i.path)) {
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
if (real_path.startsWith(dir.path)) {
return;
}
}
@@ -581,7 +578,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
const CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
for (const CollectionSubdirectory &prev_subdir : previous_subdirs) {
if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) {
ScanSubdirectory(dir, prev_subdir.path, prev_subdir, 0, t, true);
ScanSubdirectory(prev_subdir.path, prev_subdir, 0, t, true);
}
}
@@ -623,9 +620,12 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
album_art[dir_part] << child_filepath;
t->AddToProgress(1);
}
else {
else if (tagreader_client_->IsMediaFileBlocking(child_filepath)) {
files_on_disk << child_filepath;
}
else {
t->AddToProgress(1);
}
}
}
@@ -727,9 +727,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
#endif
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t)) {
files_on_disk.removeAll(file);
}
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t);
}
else { // If CUE associated.
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
@@ -786,9 +784,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
const QUrl art_automatic = ArtForSong(file, album_art);
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t)) {
files_on_disk.removeAll(file);
}
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t);
}
else { // If CUE associated.
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
@@ -799,7 +795,6 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
const SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
if (songs.isEmpty()) {
files_on_disk.removeAll(file);
t->AddToProgress(1);
continue;
}
@@ -810,7 +805,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
const QUrl art_automatic = ArtForSong(file, album_art);
for (Song song : songs) {
song.set_directory_id(t->dir_id());
song.set_directory_id(t->dir());
if (song.art_automatic().isEmpty()) song.set_art_automatic(art_automatic);
t->new_songs << song;
}
@@ -828,26 +823,27 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
}
}
// Add, update or delete subdir
// Add this subdir to the new or touched list
CollectionSubdirectory updated_subdir;
updated_subdir.directory_id = t->dir_id();
updated_subdir.directory_id = t->dir();
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0;
updated_subdir.path = path;
if (updated_subdir.mtime == 0 && updated_subdir.path != dir.path) {
t->deleted_subdirs << updated_subdir;
}
else if (subdir.directory_id == -1) {
if (subdir.directory_id == -1) {
t->new_subdirs << updated_subdir;
}
else if (subdir.mtime != updated_subdir.mtime) {
else {
t->touched_subdirs << updated_subdir;
}
if (updated_subdir.mtime == 0) { // CollectionSubdirectory deleted, mark it for removal from the watcher.
t->deleted_subdirs << updated_subdir;
}
// Recurse into the new subdirs that we found
for (const CollectionSubdirectory &my_new_subdir : std::as_const(my_new_subdirs)) {
if (stop_or_abort_requested()) return;
ScanSubdirectory(dir, my_new_subdir.path, my_new_subdir, 0, t, true);
ScanSubdirectory(my_new_subdir.path, my_new_subdir, 0, t, true);
}
}
@@ -879,7 +875,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
QSet<int> used_ids;
for (Song new_cue_song : songs) {
new_cue_song.set_source(source_);
new_cue_song.set_directory_id(t->dir_id());
new_cue_song.set_directory_id(t->dir());
PerformEBUR128Analysis(new_cue_song);
new_cue_song.set_fingerprint(fingerprint);
@@ -905,7 +901,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
}
bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
const QString &fingerprint,
const SongList &matching_songs,
const QUrl &art_automatic,
@@ -926,7 +922,7 @@ bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
const TagReaderResult result = tagreader_client_->ReadFileBlocking(file, &song_on_disk);
if (result.success() && song_on_disk.is_valid()) {
song_on_disk.set_source(source_);
song_on_disk.set_directory_id(t->dir_id());
song_on_disk.set_directory_id(t->dir());
song_on_disk.set_id(matching_song.id());
PerformEBUR128Analysis(song_on_disk);
song_on_disk.set_fingerprint(fingerprint);
@@ -935,8 +931,6 @@ bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
AddChangedSong(file, matching_song, song_on_disk, t);
}
return result.success() && song_on_disk.is_valid();
}
SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const {
@@ -1205,13 +1199,12 @@ void CollectionWatcher::DirectoryChanged(const QString &subdir) {
void CollectionWatcher::RescanPathsNow() {
const QList<int> dir_ids = rescan_queue_.keys();
for (const int dir_id : dir_ids) {
const QList<int> dirs = rescan_queue_.keys();
for (const int dir : dirs) {
if (stop_or_abort_requested()) break;
ScanTransaction transaction(this, dir_id, false, false, mark_songs_unavailable_);
ScanTransaction transaction(this, dir, false, false, mark_songs_unavailable_);
const QStringList paths = rescan_queue_.value(dir_id);
const QStringList paths = rescan_queue_.value(dir);
QMap<QString, quint64> subdir_files_count;
for (const QString &path : paths) {
@@ -1222,14 +1215,11 @@ void CollectionWatcher::RescanPathsNow() {
for (const QString &path : paths) {
if (stop_or_abort_requested()) break;
if (!subdir_mapping_.contains(path)) {
continue;
}
CollectionSubdirectory subdir;
subdir.directory_id = dir_id;
subdir.directory_id = dir;
subdir.mtime = 0;
subdir.path = path;
ScanSubdirectory(subdir_mapping_[path], path, subdir, subdir_files_count[path], &transaction);
ScanSubdirectory(path, subdir, subdir_files_count[path], &transaction);
}
}
@@ -1354,13 +1344,11 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes, mark_songs_unavailable_);
CollectionSubdirectoryList subdirs = transaction.GetAllSubdirs();
const bool has_collection_root_dir = std::any_of(subdirs.begin(), subdirs.end(), [&dir](const CollectionSubdirectory &subdir) { return subdir.path == dir.path; });
if (!has_collection_root_dir) {
qLog(Debug) << "Collection directory wasn't in subdir list, re-adding";
if (subdirs.isEmpty()) {
qLog(Debug) << "Collection directory wasn't in subdir list.";
CollectionSubdirectory subdir;
subdir.directory_id = dir.id;
subdir.path = dir.path;
subdir.mtime = 0;
subdir.directory_id = dir.id;
subdirs << subdir;
}
@@ -1370,7 +1358,7 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
for (const CollectionSubdirectory &subdir : std::as_const(subdirs)) {
if (stop_or_abort_requested()) break;
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
}
}
@@ -1471,8 +1459,6 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
QStringList scanned_paths;
for (const Song &song : songs) {
if (stop_or_abort_requested()) break;
if (!watched_dirs_.contains(song.directory_id())) continue;
const CollectionDirectory dir = watched_dirs_[song.directory_id()];
const QString song_path = song.url().toLocalFile().section(u'/', 0, -2);
if (scanned_paths.contains(song_path)) continue;
ScanTransaction transaction(this, song.directory_id(), false, true, mark_songs_unavailable_);
@@ -1482,7 +1468,7 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
if (subdir.path != song_path) continue;
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
const quint64 files_count = FilesCountForPath(&transaction, subdir.path);
ScanSubdirectory(dir, song_path, subdir, files_count, &transaction);
ScanSubdirectory(song_path, subdir, files_count, &transaction);
scanned_paths << subdir.path;
}
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2026, 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
@@ -85,7 +85,6 @@ class CollectionWatcher : public QObject {
void SongsReadded(const SongList &songs, const bool unavailable = false);
void SubdirsDiscovered(const CollectionSubdirectoryList &subdirs);
void SubdirsMTimeUpdated(const CollectionSubdirectoryList &subdirs);
void SubdirsDeleted(const CollectionSubdirectoryList &subdirs);
void CompilationsNeedUpdating();
void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days);
void ExitFinished();
@@ -123,7 +122,7 @@ class CollectionWatcher : public QObject {
// Emits the signals for new & deleted songs etc and clears the lists. This causes the new stuff to be updated on UI.
void CommitNewOrUpdatedSongs();
int dir_id() const { return dir_id_; }
int dir() const { return dir_; }
bool is_incremental() const { return incremental_; }
bool ignores_mtime() const { return ignores_mtime_; }
@@ -144,7 +143,7 @@ class CollectionWatcher : public QObject {
quint64 progress_;
quint64 progress_max_;
int dir_id_;
int dir_;
// Incremental scan enters a directory only if it has changed since the last scan.
bool incremental_;
// This type of scan updates every file in a folder that's being scanned.
@@ -180,7 +179,7 @@ class CollectionWatcher : public QObject {
void IncrementalScanNow();
void FullScanNow();
void RescanPathsNow();
void ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
void ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
void RescanSongs(const SongList &songs);
private:
@@ -203,7 +202,7 @@ class CollectionWatcher : public QObject {
// Updates the sections of a cue associated and altered (according to mtime) media file during a scan.
void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, const QUrl &art_automatic, const SongList &old_cue_songs, ScanTransaction *t) const;
// Updates a single non-cue associated and altered (according to mtime) song during a scan.
bool UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
// Scans a single media file that's present on the disk but not yet in the collection.
// It may result in a multiple files added to the collection when the media file has many sections (like a CUE related media file).
SongList ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const;

View File

@@ -43,7 +43,7 @@
#cmakedefine INSTALL_TRANSLATIONS
#define TRANSLATIONS_DIR "${CMAKE_INSTALL_PREFIX}/share/strawberry/translations"
#cmakedefine HAVE_QPA_QPLATFORMNATIVEINTERFACE
#cmakedefine HAVE_QT_XCB_QPA_PRIVATE
#cmakedefine HAVE_QX11APPLICATION
#cmakedefine ENABLE_WIN32_CONSOLE

View File

@@ -33,7 +33,7 @@ constexpr char kFileFilter[] =
"*.mod *.s3m *.xm *.it "
"*.spc *.vgm";
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm *.webp)");
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm *.webp)");
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
#endif // FILEFILTERCONSTANTS_H

View File

@@ -43,6 +43,7 @@
#include <QString>
#include <QUrl>
#include <QImage>
#include <QPixmap>
#include <QTimer>
#include <QtEvents>
@@ -245,6 +246,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void ToggleSearchCoverAuto(const bool checked);
void SaveGeometry();
void Exit();
void DoExit();
void HandleNotificationPreview(const OSDSettings::Type type, const QString &line1, const QString &line2);
@@ -279,7 +281,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
public Q_SLOTS:
void CommandlineOptionsReceived(const QByteArray &string_options);
void Raise();
void Exit();
private:
void SaveSettings();
@@ -289,6 +290,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void CheckFullRescanRevisions();
// creates the icon by painting the full one depending on the current position
QPixmap CreateOverlayedIcon(const int position, const int scrobble_point);
void GetCoverAutomatically();
void SetToggleScrobblingIcon(const bool value);

View File

@@ -690,7 +690,7 @@ bool Song::is_stream() const { return is_radio() || d->source_ == Source::Tidal
bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise; }
bool Song::is_cdda() const { return d->source_ == Source::CDDA; }
bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; }
bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz; }
bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
bool Song::is_module_music() const { return d->filetype_ == FileType::MOD || d->filetype_ == FileType::S3M || d->filetype_ == FileType::XM || d->filetype_ == FileType::IT; }
bool Song::has_cue() const { return !d->cue_path_.isEmpty(); }
@@ -1668,24 +1668,12 @@ void Song::InitArtManual() {
void Song::InitArtAutomatic() {
if (d->art_automatic_.isEmpty() && d->source_ == Source::LocalFile && d->url_.isLocalFile()) {
const QFileInfo fileinfo(d->url_.toLocalFile());
const QDir dir(fileinfo.path());
const QStringList cover_files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
QString best_cover_file;
for (const QString &cover_file : cover_files) {
if (cover_file.contains("back"_L1, Qt::CaseInsensitive)) {
continue;
}
if (cover_file.contains("front"_L1, Qt::CaseInsensitive) || cover_file.startsWith("cover"_L1, Qt::CaseInsensitive)) {
best_cover_file = cover_file;
break;
}
if (best_cover_file.isEmpty()) {
best_cover_file = cover_file;
}
}
if (!best_cover_file.isEmpty()) {
d->art_automatic_ = QUrl::fromLocalFile(fileinfo.path() + QDir::separator() + best_cover_file);
// Pick the first image file in the album directory.
QFileInfo file(d->url_.toLocalFile());
QDir dir(file.path());
QStringList files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
if (files.count() > 0) {
d->art_automatic_ = QUrl::fromLocalFile(file.path() + QDir::separator() + files.first());
}
}

View File

@@ -1,175 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2026, 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <cstring>
#include <sys/socket.h>
#include <unistd.h>
#include <csignal>
#include <cerrno>
#include <fcntl.h>
#include <QSocketNotifier>
#include "core/logging.h"
#include "unixsignalwatcher.h"
UnixSignalWatcher *UnixSignalWatcher::sInstance = nullptr;
UnixSignalWatcher::UnixSignalWatcher(QObject *parent)
: QObject(parent),
signal_fd_{-1, -1},
socket_notifier_(nullptr) {
Q_ASSERT(!sInstance);
// Create a socket pair for the self-pipe trick
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, signal_fd_) != 0) {
qLog(Error) << "Failed to create socket pair for signal handling:" << ::strerror(errno);
return;
}
Q_ASSERT(signal_fd_[0] != -1);
// Set the read end to non-blocking mode
// Non-blocking mode is important to prevent HandleSignalNotification from blocking
int flags = ::fcntl(signal_fd_[0], F_GETFL, 0);
if (flags == -1) {
qLog(Error) << "Failed to get socket flags:" << ::strerror(errno);
}
else if (::fcntl(signal_fd_[0], F_SETFL, flags | O_NONBLOCK) == -1) {
qLog(Error) << "Failed to set socket to non-blocking:" << ::strerror(errno);
}
// Set the write end to non-blocking mode as well (used in signal handler)
// Non-blocking mode prevents the signal handler from blocking if buffer is full
flags = ::fcntl(signal_fd_[1], F_GETFL, 0);
if (flags == -1) {
qLog(Error) << "Failed to get socket flags for write end:" << ::strerror(errno);
}
else if (::fcntl(signal_fd_[1], F_SETFL, flags | O_NONBLOCK) == -1) {
qLog(Error) << "Failed to set write end of socket to non-blocking:" << ::strerror(errno);
}
// Set up QSocketNotifier to monitor the read end of the socket
socket_notifier_ = new QSocketNotifier(signal_fd_[0], QSocketNotifier::Read, this);
QObject::connect(socket_notifier_, &QSocketNotifier::activated, this, &UnixSignalWatcher::HandleSignalNotification);
sInstance = this;
}
UnixSignalWatcher::~UnixSignalWatcher() {
if (socket_notifier_) {
socket_notifier_->setEnabled(false);
}
// Restore original signal handlers
for (int i = 0; i < watched_signals_.size(); ++i) {
if (::sigaction(watched_signals_[i], &original_signal_actions_[i], nullptr) != 0) {
qLog(Error) << "Failed to restore signal handler for signal" << watched_signals_[i] << ":" << ::strerror(errno);
}
}
if (signal_fd_[0] != -1) {
::close(signal_fd_[0]);
signal_fd_[0] = -1;
}
if (signal_fd_[1] != -1) {
::close(signal_fd_[1]);
signal_fd_[1] = -1;
}
sInstance = nullptr;
}
void UnixSignalWatcher::WatchForSignal(const int signal) {
// Check if socket pair was created successfully
if (signal_fd_[0] == -1 || signal_fd_[1] == -1) {
qLog(Error) << "Cannot watch for signal: socket pair not initialized";
return;
}
if (watched_signals_.contains(signal)) {
qLog(Error) << "Already watching for signal" << signal;
return;
}
struct sigaction signal_action{};
::memset(&signal_action, 0, sizeof(signal_action));
sigemptyset(&signal_action.sa_mask);
signal_action.sa_handler = UnixSignalWatcher::SignalHandler;
signal_action.sa_flags = SA_RESTART;
struct sigaction old_signal_action{};
::memset(&old_signal_action, 0, sizeof(old_signal_action));
if (::sigaction(signal, &signal_action, &old_signal_action) != 0) {
qLog(Error) << "sigaction error:" << ::strerror(errno);
return;
}
watched_signals_ << signal;
original_signal_actions_ << old_signal_action;
}
void UnixSignalWatcher::SignalHandler(const int signal) {
if (!sInstance || sInstance->signal_fd_[1] == -1) {
return;
}
// Write the signal number to the socket pair (async-signal-safe)
// This is the only operation we perform in the signal handler
// Ignore errors as there's nothing we can safely do about them in a signal handler
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
(void)::write(sInstance->signal_fd_[1], &signal, sizeof(signal));
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
}
void UnixSignalWatcher::HandleSignalNotification() {
// Read all pending signals from the socket
// Multiple signals could arrive before the notifier triggers
while (true) {
int signal = 0;
const ssize_t bytes_read = ::read(signal_fd_[0], &signal, sizeof(signal));
if (bytes_read == sizeof(signal)) {
qLog(Debug) << "Caught signal:" << signal;
Q_EMIT UnixSignal(signal);
}
else if (bytes_read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// No more data available (expected with non-blocking socket)
break;
}
else {
// Error occurred or partial read
break;
}
}
}

View File

@@ -1,53 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2026, 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef UNIXSIGNALWATCHER_H
#define UNIXSIGNALWATCHER_H
#include <csignal>
#include <QObject>
#include <QList>
class QSocketNotifier;
class UnixSignalWatcher : public QObject {
Q_OBJECT
public:
explicit UnixSignalWatcher(QObject *parent = nullptr);
~UnixSignalWatcher() override;
void WatchForSignal(const int signal);
Q_SIGNALS:
void UnixSignal(const int signal);
private:
static void SignalHandler(const int signal);
void HandleSignalNotification();
static UnixSignalWatcher *sInstance;
int signal_fd_[2];
QSocketNotifier *socket_notifier_;
QList<int> watched_signals_;
QList<struct sigaction> original_signal_actions_;
};
#endif // UNIXSIGNALWATCHER_H

View File

@@ -75,7 +75,6 @@ FilesystemDevice::FilesystemDevice(const QUrl &url,
QObject::connect(collection_watcher_, &CollectionWatcher::SongsReadded, &*collection_backend_, &CollectionBackend::MarkSongsUnavailable);
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsDiscovered, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsDeleted, &*collection_backend_, &CollectionBackend::DeleteSubdirs);
QObject::connect(collection_watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*collection_backend_, &CollectionBackend::CompilationsNeedUpdating);
QObject::connect(collection_watcher_, &CollectionWatcher::UpdateLastSeen, &*collection_backend_, &CollectionBackend::UpdateLastSeen);
QObject::connect(collection_watcher_, &CollectionWatcher::ScanStarted, this, &FilesystemDevice::TaskStarted);

View File

@@ -29,17 +29,13 @@
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QStandardPaths>
#include <QSettings>
#include <QMessageBox>
#include <QScrollBar>
#include <QLineEdit>
#include <QToolButton>
#include <QFileDialog>
#include <QSpacerItem>
#include <QtEvents>
#include "constants/appearancesettings.h"
#include "constants/filefilterconstants.h"
#include "includes/shared_ptr.h"
#include "core/deletefiles.h"
#include "core/filesystemmusicstorage.h"
@@ -49,11 +45,10 @@
#include "dialogs/deleteconfirmationdialog.h"
#include "fileview.h"
#include "fileviewlist.h"
#include "fileviewtree.h"
#include "fileviewtreemodel.h"
#include "fileviewtreeitem.h"
#include "ui_fileview.h"
#include "organize/organizeerrordialog.h"
#include "constants/appearancesettings.h"
#include "constants/filefilterconstants.h"
using std::make_unique;
using namespace Qt::Literals::StringLiterals;
@@ -62,12 +57,9 @@ FileView::FileView(QWidget *parent)
: QWidget(parent),
ui_(new Ui_FileView),
model_(nullptr),
tree_model_(nullptr),
undo_stack_(new QUndoStack(this)),
task_manager_(nullptr),
storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)),
tree_view_active_(false),
view_mode_spacer_(nullptr) {
storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)) {
ui_->setupUi(this);
@@ -76,14 +68,12 @@ FileView::FileView(QWidget *parent)
ui_->forward->setIcon(IconLoader::Load(u"go-next"_s));
ui_->home->setIcon(IconLoader::Load(u"go-home"_s));
ui_->up->setIcon(IconLoader::Load(u"go-up"_s));
ui_->toggle_view->setIcon(IconLoader::Load(u"view-choose"_s));
QObject::connect(ui_->back, &QToolButton::clicked, undo_stack_, &QUndoStack::undo);
QObject::connect(ui_->forward, &QToolButton::clicked, undo_stack_, &QUndoStack::redo);
QObject::connect(ui_->home, &QToolButton::clicked, this, &FileView::FileHome);
QObject::connect(ui_->up, &QToolButton::clicked, this, &FileView::FileUp);
QObject::connect(ui_->path, &QLineEdit::textChanged, this, &FileView::ChangeFilePath);
QObject::connect(ui_->toggle_view, &QToolButton::clicked, this, &FileView::ToggleViewMode);
QObject::connect(undo_stack_, &QUndoStack::canUndoChanged, ui_->back, &FileView::setEnabled);
QObject::connect(undo_stack_, &QUndoStack::canRedoChanged, ui_->forward, &FileView::setEnabled);
@@ -97,22 +87,6 @@ FileView::FileView(QWidget *parent)
QObject::connect(ui_->list, &FileViewList::Delete, this, &FileView::Delete);
QObject::connect(ui_->list, &FileViewList::EditTags, this, &FileView::EditTags);
// Connect tree view signals
QObject::connect(ui_->tree, &FileViewTree::AddToPlaylist, this, &FileView::AddToPlaylist);
QObject::connect(ui_->tree, &FileViewTree::CopyToCollection, this, &FileView::CopyToCollection);
QObject::connect(ui_->tree, &FileViewTree::MoveToCollection, this, &FileView::MoveToCollection);
QObject::connect(ui_->tree, &FileViewTree::CopyToDevice, this, &FileView::CopyToDevice);
QObject::connect(ui_->tree, &FileViewTree::Delete, this, &FileView::Delete);
QObject::connect(ui_->tree, &FileViewTree::EditTags, this, &FileView::EditTags);
QObject::connect(ui_->tree, &FileViewTree::activated, this, &FileView::ItemActivated);
QObject::connect(ui_->tree, &FileViewTree::doubleClicked, this, &FileView::ItemDoubleClick);
// Setup tree root management buttons
ui_->add_tree_root->setIcon(IconLoader::Load(u"folder-new"_s));
ui_->remove_tree_root->setIcon(IconLoader::Load(u"list-remove"_s));
QObject::connect(ui_->add_tree_root, &QToolButton::clicked, this, &FileView::AddRootButtonClicked);
QObject::connect(ui_->remove_tree_root, &QToolButton::clicked, this, &FileView::RemoveRootButtonClicked);
QString filter = QLatin1String(kFileFilter);
filter_list_ << filter.split(u' ');
@@ -135,19 +109,6 @@ void FileView::ReloadSettings() {
ui_->forward->setIconSize(QSize(iconsize, iconsize));
ui_->home->setIconSize(QSize(iconsize, iconsize));
ui_->up->setIconSize(QSize(iconsize, iconsize));
ui_->toggle_view->setIconSize(QSize(iconsize, iconsize));
ui_->add_tree_root->setIconSize(QSize(iconsize, iconsize));
ui_->remove_tree_root->setIconSize(QSize(iconsize, iconsize));
// Load tree root paths setting
Settings file_settings;
file_settings.beginGroup(u"FileView"_s);
tree_root_paths_ = file_settings.value(u"tree_root_paths"_s, QStandardPaths::standardLocations(QStandardPaths::StandardLocation::MusicLocation)).toStringList();
tree_view_active_ = file_settings.value(u"tree_view_active"_s, false).toBool();
file_settings.endGroup();
// Set initial view mode
UpdateViewModeUI();
}
@@ -219,46 +180,24 @@ void FileView::ChangeFilePathWithoutUndo(const QString &new_path) {
}
void FileView::ItemActivated(const QModelIndex &idx) {
// Only handle activation for list view (not tree view)
if (!tree_view_active_ && model_->isDir(idx)) {
if (model_->isDir(idx))
ChangeFilePath(model_->filePath(idx));
}
}
void FileView::ItemDoubleClick(const QModelIndex &idx) {
QString file_path;
bool is_file = false;
// Handle tree view with virtual roots
if (tree_view_active_ && tree_model_) {
QVariant type_var = tree_model_->data(idx, FileViewTreeModel::Role_Type);
if (type_var.isValid()) {
FileViewTreeItem::Type item_type = type_var.value<FileViewTreeItem::Type>();
// Only handle files, ignore directories and virtual roots
if (item_type == FileViewTreeItem::Type::File) {
file_path = tree_model_->data(idx, FileViewTreeModel::Role_FilePath).toString();
is_file = true;
}
}
}
// Handle list view with filesystem model
else if (!tree_view_active_ && model_) {
if (!model_->isDir(idx)) {
file_path = model_->filePath(idx);
is_file = true;
}
if (model_->isDir(idx)) {
return;
}
// Add file to playlist if it's a valid file
if (is_file && !file_path.isEmpty()) {
MimeData *mimedata = new MimeData;
mimedata->from_doubleclick_ = true;
mimedata->setUrls(QList<QUrl>() << QUrl::fromLocalFile(file_path));
mimedata->name_for_new_playlist_ = file_path;
QString file_path = model_->filePath(idx);
Q_EMIT AddToPlaylist(mimedata);
}
MimeData *mimedata = new MimeData;
mimedata->from_doubleclick_ = true;
mimedata->setUrls(QList<QUrl>() << QUrl::fromLocalFile(file_path));
mimedata->name_for_new_playlist_ = file_path;
Q_EMIT AddToPlaylist(mimedata);
}
@@ -333,156 +272,12 @@ void FileView::showEvent(QShowEvent *e) {
model_->setNameFilterDisables(false);
ui_->list->setModel(model_);
// Create tree model
tree_model_ = new FileViewTreeModel(this);
tree_model_->SetNameFilters(filter_list_);
SetupTreeView();
ChangeFilePathWithoutUndo(QDir::homePath());
if (!lazy_set_path_.isEmpty()) ChangeFilePathWithoutUndo(lazy_set_path_);
}
void FileView::SetupTreeView() {
// Use the new tree model with virtual roots
ui_->tree->setModel(tree_model_);
// Set the root paths in the model
tree_model_->SetRootPaths(tree_root_paths_);
// No need to set root index - the model handles virtual roots
}
void FileView::ToggleViewMode() {
tree_view_active_ = !tree_view_active_;
UpdateViewModeUI();
// Save the preference
Settings s;
s.beginGroup(u"FileView"_s);
s.setValue(u"tree_view_active"_s, tree_view_active_);
s.endGroup();
}
void FileView::UpdateViewModeUI() {
if (tree_view_active_) {
ui_->view_stack->setCurrentWidget(ui_->tree_page);
// Hide navigation controls in tree view mode
ui_->back->setVisible(false);
ui_->forward->setVisible(false);
ui_->up->setVisible(false);
ui_->home->setVisible(false);
ui_->path->setVisible(false);
// Show tree root management buttons
ui_->add_tree_root->setVisible(true);
ui_->remove_tree_root->setVisible(true);
// Insert spacer in tree view if not already present
if (!view_mode_spacer_) {
view_mode_spacer_ = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
ui_->horizontalLayout->insertSpacerItem(ui_->horizontalLayout->indexOf(ui_->toggle_view), view_mode_spacer_);
}
}
else {
ui_->view_stack->setCurrentWidget(ui_->list_page);
// Show navigation controls in list view mode
ui_->back->setVisible(true);
ui_->forward->setVisible(true);
ui_->up->setVisible(true);
ui_->home->setVisible(true);
ui_->path->setVisible(true);
// Hide tree root management buttons in list view
ui_->add_tree_root->setVisible(false);
ui_->remove_tree_root->setVisible(false);
// Remove spacer in list view
if (view_mode_spacer_) {
ui_->horizontalLayout->removeItem(view_mode_spacer_);
delete view_mode_spacer_;
view_mode_spacer_ = nullptr;
}
}
}
void FileView::AddTreeRootPath(const QString &path) {
if (!tree_root_paths_.contains(path)) {
tree_root_paths_.append(path);
SaveTreeRootPaths();
// Refresh the tree view to show the new root
if (tree_model_) {
SetupTreeView();
}
}
}
void FileView::RemoveTreeRootPath(const QString &path) {
tree_root_paths_.removeAll(path);
SaveTreeRootPaths();
// Refresh the tree view
if (tree_model_) {
SetupTreeView();
}
}
void FileView::SaveTreeRootPaths() {
Settings s;
s.beginGroup(u"FileView"_s);
s.setValue(u"tree_root_paths"_s, tree_root_paths_);
s.endGroup();
}
void FileView::AddRootButtonClicked() {
const QString dir = QFileDialog::getExistingDirectory(this, tr("Select folder to add as tree root"), tree_root_paths_.isEmpty() ? QDir::homePath() : tree_root_paths_.first(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (!dir.isEmpty()) {
AddTreeRootPath(dir);
}
}
void FileView::RemoveRootButtonClicked() {
// Get currently selected item in tree
QModelIndex current = ui_->tree->currentIndex();
if (!current.isValid()) return;
QString path;
// Get the file path from the appropriate model
if (tree_model_) {
path = tree_model_->data(current, FileViewTreeModel::Role_FilePath).toString();
}
if (path.isEmpty()) return;
const QString clean_path = QDir::cleanPath(path);
// Check if this path or any parent is a configured root
for (const QString &root : std::as_const(tree_root_paths_)) {
const QString clean_root = QDir::cleanPath(root);
if (clean_path == clean_root || clean_path.startsWith(clean_root + QDir::separator())) {
RemoveTreeRootPath(root);
return;
}
}
}
void FileView::keyPressEvent(QKeyEvent *e) {
switch (e->key()) {

View File

@@ -40,12 +40,10 @@ class QFileIconProvider;
class QUndoStack;
class QKeyEvent;
class QShowEvent;
class QSpacerItem;
class MusicStorage;
class TaskManager;
class Ui_FileView;
class FileViewTreeModel;
class FileView : public QWidget {
Q_OBJECT
@@ -78,22 +76,12 @@ class FileView : public QWidget {
void ChangeFilePath(const QString &new_path);
void ItemActivated(const QModelIndex &idx);
void ItemDoubleClick(const QModelIndex &idx);
void ToggleViewMode();
void Delete(const QStringList &filenames);
void DeleteFinished(const SongList &songs_with_errors);
public Q_SLOTS:
void AddTreeRootPath(const QString &path);
void RemoveTreeRootPath(const QString &path);
private:
void ChangeFilePathWithoutUndo(const QString &new_path);
void SetupTreeView();
void SaveTreeRootPaths();
void AddRootButtonClicked();
void RemoveRootButtonClicked();
void UpdateViewModeUI();
private:
class UndoCommand : public QUndoCommand {
@@ -122,21 +110,16 @@ class FileView : public QWidget {
Ui_FileView *ui_;
QFileSystemModel *model_;
FileViewTreeModel *tree_model_;
QUndoStack *undo_stack_;
SharedPtr<TaskManager> task_manager_;
SharedPtr<MusicStorage> storage_;
QString lazy_set_path_;
QStringList tree_root_paths_;
QStringList filter_list_;
ScopedPtr<QFileIconProvider> file_icon_provider_;
bool tree_view_active_;
QSpacerItem *view_mode_spacer_;
};
#endif // FILEVIEW_H

View File

@@ -95,143 +95,28 @@
<item>
<widget class="QLineEdit" name="path"/>
</item>
<item>
<widget class="QToolButton" name="add_tree_root">
<property name="toolTip">
<string>Add root directory</string>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="remove_tree_root">
<property name="toolTip">
<string>Remove selected root directory</string>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toggle_view">
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Toggle between list and tree view</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="view_stack">
<property name="currentIndex">
<number>0</number>
<widget class="FileViewList" name="list">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<widget class="QWidget" name="list_page">
<layout class="QVBoxLayout" name="list_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="FileViewList" name="list">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tree_page">
<layout class="QVBoxLayout" name="tree_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="FileViewTree" name="tree">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
@@ -242,11 +127,6 @@
<extends>QListView</extends>
<header>fileview/fileviewlist.h</header>
</customwidget>
<customwidget>
<class>FileViewTree</class>
<extends>QTreeView</extends>
<header>fileview/fileviewtree.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@@ -99,7 +99,7 @@ MimeData *FileViewList::MimeDataFromSelection() const {
const QStringList filenames = FilenamesFromSelection();
// If just one folder selected - use its path as the new playlist's name
// if just one folder selected - use its path as the new playlist's name
if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) {
if (filenames.first().length() > 20) {
mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName();
@@ -108,7 +108,7 @@ MimeData *FileViewList::MimeDataFromSelection() const {
mimedata->name_for_new_playlist_ = filenames.first();
}
}
// Otherwise, use the current root path
// otherwise, use the current root path
else {
QString path = qobject_cast<QFileSystemModel*>(model())->rootPath();
if (path.length() > 20) {
@@ -196,11 +196,11 @@ void FileViewList::mousePressEvent(QMouseEvent *e) {
case Qt::XButton2:
Q_EMIT Forward();
break;
// Enqueue to playlist with middleClick
// enqueue to playlist with middleClick
case Qt::MiddleButton:{
QListView::mousePressEvent(e);
// We need to update the menu selection
// we need to update the menu selection
menu_selection_ = selectionModel()->selection();
MimeData *mimedata = new MimeData;

View File

@@ -1,205 +0,0 @@
/*
* Strawberry Music Player
* Copyright 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <algorithm>
#include <utility>
#include <QWidget>
#include <QAbstractItemModel>
#include <QFileInfo>
#include <QDir>
#include <QMenu>
#include <QUrl>
#include <QCollator>
#include <QtEvents>
#include "core/iconloader.h"
#include "core/mimedata.h"
#include "utilities/filemanagerutils.h"
#include "fileviewtree.h"
#include "fileviewtreemodel.h"
using namespace Qt::Literals::StringLiterals;
FileViewTree::FileViewTree(QWidget *parent)
: QTreeView(parent),
menu_(new QMenu(this)) {
menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Append to current playlist"), this, &FileViewTree::AddToPlaylistSlot);
menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Replace current playlist"), this, &FileViewTree::LoadSlot);
menu_->addAction(IconLoader::Load(u"document-new"_s), tr("Open in new playlist"), this, &FileViewTree::OpenInNewPlaylistSlot);
menu_->addSeparator();
menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy to collection..."), this, &FileViewTree::CopyToCollectionSlot);
menu_->addAction(IconLoader::Load(u"go-jump"_s), tr("Move to collection..."), this, &FileViewTree::MoveToCollectionSlot);
menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &FileViewTree::CopyToDeviceSlot);
menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &FileViewTree::DeleteSlot);
menu_->addSeparator();
menu_->addAction(IconLoader::Load(u"edit-rename"_s), tr("Edit track information..."), this, &FileViewTree::EditTagsSlot);
menu_->addAction(IconLoader::Load(u"document-open-folder"_s), tr("Show in file browser..."), this, &FileViewTree::ShowInBrowser);
setAttribute(Qt::WA_MacShowFocusRect, false);
setHeaderHidden(true);
setUniformRowHeights(true);
}
void FileViewTree::contextMenuEvent(QContextMenuEvent *e) {
menu_selection_ = selectionModel()->selection();
menu_->popup(e->globalPos());
e->accept();
}
QStringList FileViewTree::FilenamesFromSelection() const {
QStringList filenames;
const QModelIndexList indexes = menu_selection_.indexes();
FileViewTreeModel *tree_model = qobject_cast<FileViewTreeModel*>(model());
if (tree_model) {
for (const QModelIndex &index : indexes) {
if (index.column() == 0) {
QString path = tree_model->data(index, FileViewTreeModel::Role_FilePath).toString();
if (!path.isEmpty()) {
filenames << path;
}
}
}
}
QCollator collator;
collator.setNumericMode(true);
std::sort(filenames.begin(), filenames.end(), collator);
return filenames;
}
QList<QUrl> FileViewTree::UrlListFromSelection() const {
QList<QUrl> urls;
const QStringList filenames = FilenamesFromSelection();
urls.reserve(filenames.count());
for (const QString &filename : std::as_const(filenames)) {
urls << QUrl::fromLocalFile(filename);
}
return urls;
}
MimeData *FileViewTree::MimeDataFromSelection() const {
MimeData *mimedata = new MimeData;
mimedata->setUrls(UrlListFromSelection());
const QStringList filenames = FilenamesFromSelection();
// if just one folder selected - use its path as the new playlist's name
if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) {
if (filenames.first().length() > 20) {
mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName();
}
else {
mimedata->name_for_new_playlist_ = filenames.first();
}
}
// otherwise, use "Files" as default
else {
mimedata->name_for_new_playlist_ = tr("Files");
}
return mimedata;
}
void FileViewTree::LoadSlot() {
MimeData *mimedata = MimeDataFromSelection();
mimedata->clear_first_ = true;
Q_EMIT AddToPlaylist(mimedata);
}
void FileViewTree::AddToPlaylistSlot() {
Q_EMIT AddToPlaylist(MimeDataFromSelection());
}
void FileViewTree::OpenInNewPlaylistSlot() {
MimeData *mimedata = MimeDataFromSelection();
mimedata->open_in_new_playlist_ = true;
Q_EMIT AddToPlaylist(mimedata);
}
void FileViewTree::CopyToCollectionSlot() {
Q_EMIT CopyToCollection(UrlListFromSelection());
}
void FileViewTree::MoveToCollectionSlot() {
Q_EMIT MoveToCollection(UrlListFromSelection());
}
void FileViewTree::CopyToDeviceSlot() {
Q_EMIT CopyToDevice(UrlListFromSelection());
}
void FileViewTree::DeleteSlot() {
Q_EMIT Delete(FilenamesFromSelection());
}
void FileViewTree::EditTagsSlot() {
Q_EMIT EditTags(UrlListFromSelection());
}
void FileViewTree::mousePressEvent(QMouseEvent *e) {
switch (e->button()) {
// Enqueue to playlist with middleClick
case Qt::MiddleButton:{
QTreeView::mousePressEvent(e);
// We need to update the menu selection
QItemSelectionModel *selection_model = selectionModel();
if (!selection_model) {
e->ignore();
return;
}
menu_selection_ = selection_model->selection();
MimeData *mimedata = new MimeData;
mimedata->setUrls(UrlListFromSelection());
mimedata->enqueue_now_ = true;
Q_EMIT AddToPlaylist(mimedata);
break;
}
default:
QTreeView::mousePressEvent(e);
break;
}
}
void FileViewTree::ShowInBrowser() {
Utilities::OpenInFileBrowser(UrlListFromSelection());
}

View File

@@ -1,78 +0,0 @@
/*
* Strawberry Music Player
* Copyright 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef FILEVIEWTREE_H
#define FILEVIEWTREE_H
#include <QObject>
#include <QTreeView>
#include <QList>
#include <QUrl>
#include <QString>
#include <QStringList>
class QWidget;
class QMimeData;
class QMenu;
class QMouseEvent;
class QContextMenuEvent;
class MimeData;
class FileViewTree : public QTreeView {
Q_OBJECT
public:
explicit FileViewTree(QWidget *parent = nullptr);
void mousePressEvent(QMouseEvent *e) override;
Q_SIGNALS:
void AddToPlaylist(QMimeData *data);
void CopyToCollection(const QList<QUrl> &urls);
void MoveToCollection(const QList<QUrl> &urls);
void CopyToDevice(const QList<QUrl> &urls);
void Delete(const QStringList &filenames);
void EditTags(const QList<QUrl> &urls);
protected:
void contextMenuEvent(QContextMenuEvent *e) override;
private:
QStringList FilenamesFromSelection() const;
QList<QUrl> UrlListFromSelection() const;
MimeData *MimeDataFromSelection() const;
private Q_SLOTS:
void LoadSlot();
void AddToPlaylistSlot();
void OpenInNewPlaylistSlot();
void CopyToCollectionSlot();
void MoveToCollectionSlot();
void CopyToDeviceSlot();
void DeleteSlot();
void EditTagsSlot();
void ShowInBrowser();
private:
QMenu *menu_;
QItemSelection menu_selection_;
};
#endif // FILEVIEWTREE_H

View File

@@ -1,52 +0,0 @@
/*
* Strawberry Music Player
* Copyright 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef FILEVIEWTREEITEM_H
#define FILEVIEWTREEITEM_H
#include "config.h"
#include <QFileInfo>
#include "core/simpletreeitem.h"
class FileViewTreeItem : public SimpleTreeItem<FileViewTreeItem> {
public:
enum class Type {
Root, // Hidden root
VirtualRoot, // User-configured root paths
Directory, // File system directory
File // File system file
};
explicit FileViewTreeItem(SimpleTreeModel<FileViewTreeItem> *_model) : SimpleTreeItem<FileViewTreeItem>(_model), type(Type::Root), lazy_loaded(false) {}
explicit FileViewTreeItem(const Type _type, FileViewTreeItem *_parent = nullptr) : SimpleTreeItem<FileViewTreeItem>(_parent), type(_type), lazy_loaded(false) {}
Type type;
QString file_path; // Absolute file system path
QFileInfo file_info; // Cached file info
bool lazy_loaded; // Whether children have been loaded
private:
Q_DISABLE_COPY(FileViewTreeItem)
};
Q_DECLARE_METATYPE(FileViewTreeItem::Type)
#endif // FILEVIEWTREEITEM_H

View File

@@ -1,246 +0,0 @@
/*
* Strawberry Music Player
* Copyright 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QObject>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QList>
#include <QMap>
#include <QDir>
#include <QFileInfo>
#include <QFileIconProvider>
#include <QMimeData>
#include <QUrl>
#include <QIcon>
#include "core/simpletreemodel.h"
#include "core/logging.h"
#include "fileviewtreemodel.h"
#include "fileviewtreeitem.h"
using namespace Qt::Literals::StringLiterals;
FileViewTreeModel::FileViewTreeModel(QObject *parent)
: SimpleTreeModel<FileViewTreeItem>(new FileViewTreeItem(this), parent),
icon_provider_(new QFileIconProvider()) {
}
FileViewTreeModel::~FileViewTreeModel() {
delete root_;
delete icon_provider_;
}
Qt::ItemFlags FileViewTreeModel::flags(const QModelIndex &idx) const {
const FileViewTreeItem *item = IndexToItem(idx);
if (!item) return Qt::NoItemFlags;
switch (item->type) {
case FileViewTreeItem::Type::VirtualRoot:
case FileViewTreeItem::Type::Directory:
case FileViewTreeItem::Type::File:
return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
case FileViewTreeItem::Type::Root:
default:
return Qt::ItemIsEnabled;
}
}
QVariant FileViewTreeModel::data(const QModelIndex &idx, const int role) const {
if (!idx.isValid()) return QVariant();
const FileViewTreeItem *item = IndexToItem(idx);
if (!item) return QVariant();
switch (role) {
case Qt::DisplayRole:
if (item->type == FileViewTreeItem::Type::VirtualRoot) {
return item->display_text.isEmpty() ? item->file_path : item->display_text;
}
return item->file_info.fileName();
case Qt::DecorationRole:
return GetIcon(item);
case Role_Type:
return QVariant::fromValue(item->type);
case Role_FilePath:
return item->file_path;
case Role_FileName:
return item->file_info.fileName();
default:
return QVariant();
}
}
bool FileViewTreeModel::hasChildren(const QModelIndex &parent) const {
const FileViewTreeItem *item = IndexToItem(parent);
if (!item) return false;
// Root and VirtualRoot always have children (or can have them)
if (item->type == FileViewTreeItem::Type::Root) return true;
if (item->type == FileViewTreeItem::Type::VirtualRoot) return true;
// Directories can have children
if (item->type == FileViewTreeItem::Type::Directory) {
return true;
}
// Files don't have children
return false;
}
bool FileViewTreeModel::canFetchMore(const QModelIndex &parent) const {
const FileViewTreeItem *item = IndexToItem(parent);
if (!item) return false;
// Can fetch more if not yet lazy loaded
return !item->lazy_loaded && (item->type == FileViewTreeItem::Type::VirtualRoot || item->type == FileViewTreeItem::Type::Directory);
}
void FileViewTreeModel::fetchMore(const QModelIndex &parent) {
FileViewTreeItem *item = IndexToItem(parent);
if (!item || item->lazy_loaded) return;
LazyLoad(item);
}
void FileViewTreeModel::LazyLoad(FileViewTreeItem *item) {
if (item->lazy_loaded) return;
QDir dir(item->file_path);
if (!dir.exists()) {
item->lazy_loaded = true;
return;
}
// Apply name filters
const QDir::Filters filters = QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot;
if (!name_filters_.isEmpty()) {
dir.setNameFilters(name_filters_);
}
const QFileInfoList entries = dir.entryInfoList(filters, QDir::Name | QDir::DirsFirst);
if (!entries.isEmpty()) {
BeginInsert(item, 0, static_cast<int>(entries.count()) - 1);
for (const QFileInfo &entry : entries) {
FileViewTreeItem *child = new FileViewTreeItem(
entry.isDir() ? FileViewTreeItem::Type::Directory : FileViewTreeItem::Type::File,
item
);
child->file_path = entry.absoluteFilePath();
child->file_info = entry;
child->lazy_loaded = false;
child->display_text = entry.fileName();
}
EndInsert();
}
item->lazy_loaded = true;
}
QIcon FileViewTreeModel::GetIcon(const FileViewTreeItem *item) const {
if (!item) return QIcon();
switch (item->type) {
case FileViewTreeItem::Type::VirtualRoot:
case FileViewTreeItem::Type::Directory:
return icon_provider_->icon(QFileIconProvider::Folder);
case FileViewTreeItem::Type::File:
return icon_provider_->icon(item->file_info);
default:
return QIcon();
}
}
QStringList FileViewTreeModel::mimeTypes() const {
return QStringList() << u"text/uri-list"_s;
}
QMimeData *FileViewTreeModel::mimeData(const QModelIndexList &indexes) const {
if (indexes.isEmpty()) return nullptr;
QList<QUrl> urls;
for (const QModelIndex &idx : indexes) {
const FileViewTreeItem *item = IndexToItem(idx);
if (item && (item->type == FileViewTreeItem::Type::File || item->type == FileViewTreeItem::Type::Directory || item->type == FileViewTreeItem::Type::VirtualRoot)) {
urls << QUrl::fromLocalFile(item->file_path);
}
}
if (urls.isEmpty()) return nullptr;
QMimeData *data = new QMimeData();
data->setUrls(urls);
return data;
}
void FileViewTreeModel::SetRootPaths(const QStringList &paths) {
Reset();
for (const QString &path : paths) {
QFileInfo info(path);
if (!info.exists() || !info.isDir()) continue;
FileViewTreeItem *virtual_root = new FileViewTreeItem(FileViewTreeItem::Type::VirtualRoot, root_);
virtual_root->file_path = info.absoluteFilePath();
virtual_root->file_info = info;
virtual_root->display_text = info.absoluteFilePath();
virtual_root->lazy_loaded = false;
}
}
void FileViewTreeModel::SetNameFilters(const QStringList &filters) {
name_filters_ = filters;
}
void FileViewTreeModel::Reset() {
beginResetModel();
// Clear children without notifications since we're in a reset
qDeleteAll(root_->children);
root_->children.clear();
endResetModel();
}

View File

@@ -1,72 +0,0 @@
/*
* Strawberry Music Player
* Copyright 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef FILEVIEWTREEMODEL_H
#define FILEVIEWTREEMODEL_H
#include "config.h"
#include <QObject>
#include <QVariant>
#include <QStringList>
#include <QIcon>
#include "core/simpletreemodel.h"
#include "fileviewtreeitem.h"
class QFileIconProvider;
class QMimeData;
class FileViewTreeModel : public SimpleTreeModel<FileViewTreeItem> {
Q_OBJECT
public:
explicit FileViewTreeModel(QObject *parent = nullptr);
~FileViewTreeModel() override;
enum Role {
Role_Type = Qt::UserRole + 1,
Role_FilePath,
Role_FileName,
RoleCount
};
// QAbstractItemModel
Qt::ItemFlags flags(const QModelIndex &idx) const override;
QVariant data(const QModelIndex &idx, const int role) const override;
bool hasChildren(const QModelIndex &parent) const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
void SetRootPaths(const QStringList &paths);
void SetNameFilters(const QStringList &filters);
private:
void Reset();
void LazyLoad(FileViewTreeItem *item);
QIcon GetIcon(const FileViewTreeItem *item) const;
private:
QFileIconProvider *icon_provider_;
QStringList name_filters_;
};
#endif // FILEVIEWTREEMODEL_H

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2021, 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
@@ -76,10 +76,6 @@
#include <kdsingleapplication.h>
#ifdef Q_OS_UNIX
#include "core/unixsignalwatcher.h"
#endif
#ifdef HAVE_QTSPARKLE
# include <qtsparkle-qt6/Updater>
#endif // HAVE_QTSPARKLE
@@ -369,12 +365,6 @@ int main(int argc, char *argv[]) {
#endif
options);
#ifdef Q_OS_UNIX
UnixSignalWatcher unix_signal_watcher;
unix_signal_watcher.WatchForSignal(SIGTERM);
QObject::connect(&unix_signal_watcher, &UnixSignalWatcher::UnixSignal, &w, &MainWindow::Exit);
#endif
#ifdef Q_OS_MACOS
mac::EnableFullScreen(w);
#endif // Q_OS_MACOS

View File

@@ -51,7 +51,7 @@
#include <QFlags>
#include <QtEvents>
#ifdef HAVE_QPA_QPLATFORMNATIVEINTERFACE
#ifdef HAVE_QT_XCB_QPA_PRIVATE
# include <qpa/qplatformnativeinterface.h>
#endif
@@ -215,7 +215,7 @@ void OSDPretty::ScreenRemoved(QScreen *screen) {
bool OSDPretty::IsTransparencyAvailable() {
#ifdef HAVE_QPA_QPLATFORMNATIVEINTERFACE
#ifdef HAVE_QT_XCB_QPA_PRIVATE
if (qApp && QGuiApplication::platformName() == "xcb"_L1) {
QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface();
QScreen *screen = popup_screen_ == nullptr ? QGuiApplication::primaryScreen() : popup_screen_;

View File

@@ -152,6 +152,7 @@ void PlaylistContainer::SetActions(QAction *new_playlist, QAction *load_playlist
QObject::connect(clear_playlist, &QAction::triggered, this, &PlaylistContainer::ClearPlaylist);
QObject::connect(next_playlist, &QAction::triggered, this, &PlaylistContainer::GoToNextPlaylistTab);
QObject::connect(previous_playlist, &QAction::triggered, this, &PlaylistContainer::GoToPreviousPlaylistTab);
QObject::connect(clear_playlist, &QAction::triggered, this, &PlaylistContainer::ClearPlaylist);
QObject::connect(save_all_playlists, &QAction::triggered, &*manager_, &PlaylistManager::SaveAllPlaylists);
}

View File

@@ -1227,7 +1227,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Don&apos;t show in various artists</source>
<translation>Όχι εμφάνιση σε διάφορους καλλιτέχνες</translation>
<translation type="unfinished">Don&apos;t show in various artists</translation>
</message>
<message>
<source>There are other songs in this album</source>
@@ -1726,7 +1726,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Set through album cover search (%1)</source>
<translation>Ορισμός μέσω αναζήτησης εξωφύλλου άλμπουμ (%1)</translation>
<translation type="unfinished">Set through album cover search (%1)</translation>
</message>
<message>
<source>Automatically picked up from album directory (%1)</source>
@@ -1741,7 +1741,7 @@ If there are no matches then it will use the largest image in the directory.</so
<name>CueParser</name>
<message>
<source>Saving CUE files is not supported.</source>
<translation>Η αποθήκευση αρχείων CUE δεν υποστηρίζεται.</translation>
<translation type="unfinished">Saving CUE files is not supported.</translation>
</message>
</context>
<context>
@@ -2180,7 +2180,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Different art across multiple songs.</source>
<translation>Διαφορετική τέχνη σε πολλαπλά τραγούδια.</translation>
<translation type="unfinished">Different art across multiple songs.</translation>
</message>
<message>
<source>Previous</source>
@@ -3416,7 +3416,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Toggle skip status</source>
<translation>Εναλλαγή κατάστασης παράκαμψης</translation>
<translation type="unfinished">Toggle skip status</translation>
</message>
<message>
<source>Rescan song(s)...</source>
@@ -3464,7 +3464,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>You are running Strawberry under Rosetta. Running Strawberry under Rosetta is unsupported and known to have issues. You should download Strawberry for the correct CPU architecture from %1</source>
<translation>Τρέχετε το Strawberry κάτω από Rosetta. Η χρήση του Strawberry κάτω από Rosetta δεν υποστηρίζεται και είναι γνωστό ότι παρουσιάζει προβλήματα. Θα πρέπει να λάβετε το Strawberry για τη σωστή αρχιτεκτονική CPU από %1</translation>
<translation type="unfinished">You are running Strawberry under Rosetta. Running Strawberry under Rosetta is unsupported and known to have issues. You should download Strawberry for the correct CPU architecture from %1</translation>
</message>
<message>
<source>Sponsoring Strawberry</source>
@@ -4351,7 +4351,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Loudness Range</source>
<translation>Loudness Range</translation>
<translation type="unfinished">Loudness Range</translation>
</message>
</context>
<context>
@@ -4501,7 +4501,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Couldn&apos;t create playlist</source>
<translation>Αδυναμία δημιουργίας λίστας αναπαραγωγής</translation>
<translation type="unfinished">Couldn&apos;t create playlist</translation>
</message>
<message>
<source>Save playlist</source>
@@ -4766,11 +4766,11 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Warn me when closing a playlist tab</source>
<translation>Προειδοποίηση κατά το κλείσιμο μιας καρτέλας λίστας αναπαραγωγής</translation>
<translation type="unfinished">Warn me when closing a playlist tab</translation>
</message>
<message>
<source>This option can be changed in the &quot;Behavior&quot; preferences</source>
<translation/>
<translation type="unfinished">This option can be changed in the &quot;Behavior&quot; preferences</translation>
</message>
<message>
<source>Double-click here to favorite this playlist so it will be saved and remain accessible through the &quot;Playlists&quot; panel on the left side bar</source>
@@ -4888,7 +4888,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Loads files/URLs, replacing current playlist</source>
<translation>Φόρτωση αρχείων/URL, αντικατάσταση της τρέχουσας λίστας αναπαραγωγής</translation>
<translation type="unfinished">Loads files/URLs, replacing current playlist</translation>
</message>
<message>
<source>Play the &lt;n&gt;th track in the playlist</source>
@@ -4940,11 +4940,11 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Destination file %1 exists, but not allowed to overwrite.</source>
<translation>Το αρχείο προορισμού %1 υπάρχει, αλλά δεν επιτρέπεται να αντικατασταθεί.</translation>
<translation type="unfinished">Destination file %1 exists, but not allowed to overwrite.</translation>
</message>
<message>
<source>Destination file %1 exists, but not allowed to overwrite</source>
<translation>Το αρχείο προορισμού %1 υπάρχει, αλλά δεν επιτρέπεται να αντικατασταθεί</translation>
<translation type="unfinished">Destination file %1 exists, but not allowed to overwrite</translation>
</message>
<message>
<source>Could not copy file %1 to %2.</source>
@@ -5012,7 +5012,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>%1 songs in %2 different directories selected, are you sure you want to open them all?</source>
<translation>%1 τραγούδια σε διαφορετικούς καταλόγους %2 επιλέχθηκαν, είστε σίγουροι ότι θέλετε να τα ανοίξετε όλα;</translation>
<translation type="unfinished">%1 songs in %2 different directories selected, are you sure you want to open them all?</translation>
</message>
<message>
<source>Failed to load image from data for %1</source>
@@ -5315,11 +5315,11 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Receiving album cover for %1 album...</source>
<translation>Λήψη εξωφύλλου για το άλμπουμ %1...</translation>
<translation type="unfinished">Receiving album cover for %1 album...</translation>
</message>
<message>
<source>Receiving album covers for %1 albums...</source>
<translation>Λήψη εξώφυλλων για άλμπουμ %1...</translation>
<translation type="unfinished">Receiving album covers for %1 albums...</translation>
</message>
<message>
<source>No match.</source>
@@ -5472,7 +5472,7 @@ Are you sure you want to continue?</source>
<message numerus="yes">
<source>%n track(s)</source>
<translation type="unfinished">
<numerusform>%n κομμάτι(α)</numerusform>
<numerusform>%n track(s)</numerusform>
<numerusform>%n track(s)</numerusform>
</translation>
</message>
@@ -5493,15 +5493,15 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Move up</source>
<translation>Μετακίνηση πάνω</translation>
<translation type="unfinished">Move up</translation>
</message>
<message>
<source>Ctrl+Down</source>
<translation>Ctrl+Down</translation>
<translation type="unfinished">Ctrl+Down</translation>
</message>
<message>
<source>Remove</source>
<translation>Αφαίρεση</translation>
<translation type="unfinished">Remove</translation>
</message>
<message>
<source>Clear</source>
@@ -5516,26 +5516,26 @@ Are you sure you want to continue?</source>
<name>RadioParadiseService</name>
<message>
<source>Getting %1 channels</source>
<translation>Λήψη %1 καναλιών</translation>
<translation type="unfinished">Getting %1 channels</translation>
</message>
</context>
<context>
<name>RadioView</name>
<message>
<source>Append to current playlist</source>
<translation>Προσάρτηση στην τρέχουσα λίστα</translation>
<translation type="unfinished">Append to current playlist</translation>
</message>
<message>
<source>Replace current playlist</source>
<translation>Αντικατάσταση της τρέχουσας λίστας</translation>
<translation type="unfinished">Replace current playlist</translation>
</message>
<message>
<source>Open in new playlist</source>
<translation>Άνοιγμα σε νέα λίστα</translation>
<translation type="unfinished">Open in new playlist</translation>
</message>
<message>
<source>Open homepage</source>
<translation>Άνοιγμα αρχικής σελίδας</translation>
<translation type="unfinished">Open homepage</translation>
</message>
<message>
<source>Donate</source>
@@ -5557,7 +5557,7 @@ Are you sure you want to continue?</source>
<name>SavePlaylistsDialog</name>
<message>
<source>Select directory for saving playlists</source>
<translation>Επιλέξτε φάκελο για τις λίστες αναπαραγωγής</translation>
<translation type="unfinished">Select directory for saving playlists</translation>
</message>
<message>
<source>Type</source>
@@ -5711,11 +5711,11 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Show love button</source>
<translation>Εμφάνιση πλήκτρου αγάπης</translation>
<translation type="unfinished">Show love button</translation>
</message>
<message>
<source>Submit scrobbles every</source>
<translation>Υποβολή scrobbles κάθε</translation>
<translation type="unfinished">Submit scrobbles every</translation>
</message>
<message>
<source> seconds</source>
@@ -5811,7 +5811,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Offline mode (Only cache scrobbles)</source>
<translation>Λειτουργία εκτός σύνδεσης (Μόνο cache scrobbles)</translation>
<translation type="unfinished">Offline mode (Only cache scrobbles)</translation>
</message>
<message>
<source>This is the delay between when a song is scrobbled and when scrobbles are submitted to the server. Setting the time to 0 seconds will submit scrobbles immediately.</source>
@@ -5830,11 +5830,11 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Open URL in web browser?</source>
<translation>Άνοιγμα διεύθυνσης URL στο πρόγραμμα περιήγησης;</translation>
<translation type="unfinished">Open URL in web browser?</translation>
</message>
<message>
<source>Press &quot;Save&quot; to copy the URL to clipboard and manually open it in a web browser.</source>
<translation>Πατήστε &quot;Save&quot; για να αντιγράψετε το URL στο πρόχειρο και να το ανοίξετε χειροκίνητα σε ένα πρόγραμμα περιήγησης.</translation>
<translation type="unfinished">Press &quot;Save&quot; to copy the URL to clipboard and manually open it in a web browser.</translation>
</message>
<message>
<source>Could not open URL. Please open this URL in your browser</source>
@@ -5842,19 +5842,19 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Invalid reply from web browser. Missing token.</source>
<translation>Μη έγκυρη απάντηση από το πρόγραμμα περιήγησης. Λείπει token.</translation>
<translation type="unfinished">Invalid reply from web browser. Missing token.</translation>
</message>
<message>
<source>Received invalid reply from web browser. Try another browser.</source>
<translation>Λήφθηκε μη έγκυρη απάντηση από το πρόγραμμα περιήγησης. Δοκιμάστε ένα άλλο πρόγραμμα περιήγησης.</translation>
<translation type="unfinished">Received invalid reply from web browser. Try another browser.</translation>
</message>
<message>
<source>Scrobbler %1 is not authenticated!</source>
<translation>Το Scrobbler %1 δεν είναι πιστοποιημένο!</translation>
<translation type="unfinished">Scrobbler %1 is not authenticated!</translation>
</message>
<message>
<source>Scrobbler %1 error: %2</source>
<translation>Scrobbler %1 σφάλμα: %2</translation>
<translation type="unfinished">Scrobbler %1 error: %2</translation>
</message>
</context>
<context>
@@ -5873,7 +5873,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Streaming</source>
<translation>Streaming</translation>
<translation type="unfinished">Streaming</translation>
</message>
</context>
<context>
@@ -6034,14 +6034,14 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Choose a name for your smart playlist</source>
<translation>Επιλέξτε ένα όνομα για την έξυπνη λίστα αναπαραγωγής σας</translation>
<translation type="unfinished">Choose a name for your smart playlist</translation>
</message>
</context>
<context>
<name>SmartPlaylistWizardFinishPage</name>
<message>
<source>Form</source>
<translation>Φόρμα</translation>
<translation type="unfinished">Form</translation>
</message>
<message>
<source>Name</source>
@@ -6049,7 +6049,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Use dynamic mode</source>
<translation>Χρήση δυναμικής λειτουργίας</translation>
<translation type="unfinished">Use dynamic mode</translation>
</message>
<message>
<source>In dynamic mode new tracks will be chosen and added to the playlist every time a song finishes.</source>

View File

@@ -119,11 +119,11 @@
</message>
<message>
<source>Failed to open cover file %1 for reading: %2</source>
<translation>No se pudo abrir el archivo de portada %1 para lectura: %2</translation>
<translation type="unfinished">Failed to open cover file %1 for reading: %2</translation>
</message>
<message>
<source>Cover file %1 is empty.</source>
<translation>El archivo de portada %1 está vacío.</translation>
<translation type="unfinished">Cover file %1 is empty.</translation>
</message>
<message>
<source>unknown</source>
@@ -135,27 +135,27 @@
</message>
<message>
<source>Failed to open cover file %1 for writing: %2</source>
<translation>No se pudo abrir el archivo de portada %1 para escribir: %2</translation>
<translation type="unfinished">Failed to open cover file %1 for writing: %2</translation>
</message>
<message>
<source>Failed writing cover to file %1: %2</source>
<translation>Error al escribir la portada en el archivo %1: %2</translation>
<translation type="unfinished">Failed writing cover to file %1: %2</translation>
</message>
<message>
<source>Failed writing cover to file %1.</source>
<translation>Error al escribir la portada en el archivo %1.</translation>
<translation type="unfinished">Failed writing cover to file %1.</translation>
</message>
<message>
<source>Failed to delete cover file %1: %2</source>
<translation>No se pudo eliminar el archivo de portada %1: %2</translation>
<translation type="unfinished">Failed to delete cover file %1: %2</translation>
</message>
<message>
<source>Failed to write cover to file %1: %2</source>
<translation>No se pudo escribir la portada en el archivo %1: %2</translation>
<translation type="unfinished">Failed to write cover to file %1: %2</translation>
</message>
<message>
<source>Could not save cover to file %1.</source>
<translation>No se pudo guardar la portada en el archivo %1.</translation>
<translation type="unfinished">Could not save cover to file %1.</translation>
</message>
</context>
<context>
@@ -273,7 +273,7 @@
</message>
<message>
<source>Could not save cover to file %1.</source>
<translation>No se pudo guardar la portada en el archivo %1.</translation>
<translation type="unfinished">Could not save cover to file %1.</translation>
</message>
</context>
<context>
@@ -339,7 +339,7 @@
</message>
<message>
<source>Turbine</source>
<translation>Turbina</translation>
<translation type="unfinished">Turbine</translation>
</message>
<message>
<source>Sonogram</source>
@@ -549,7 +549,7 @@
</message>
<message>
<source>p&amp;lughw</source>
<translation>enchufe</translation>
<translation type="unfinished">p&amp;lughw</translation>
</message>
<message>
<source>pcm</source>
@@ -569,7 +569,7 @@
</message>
<message>
<source>Upmix / downmix to</source>
<translation>Mezcla ascendente/descendente a</translation>
<translation type="unfinished">Upmix / downmix to</translation>
</message>
<message>
<source>channels</source>
@@ -577,15 +577,15 @@
</message>
<message>
<source>Improve headphone listening of stereo audio records (bs2b)</source>
<translation>Mejorar la escucha de grabaciones de audio estéreo con auriculares (B2B)</translation>
<translation type="unfinished">Improve headphone listening of stereo audio records (bs2b)</translation>
</message>
<message>
<source>Enable HTTP/2 for streaming</source>
<translation>Habilitar HTTP/2 para transmisión</translation>
<translation type="unfinished">Enable HTTP/2 for streaming</translation>
</message>
<message>
<source>Use strict SSL mode</source>
<translation>Utilice el modo SSL estricto</translation>
<translation type="unfinished">Use strict SSL mode</translation>
</message>
<message>
<source>Buffer</source>
@@ -649,19 +649,19 @@
</message>
<message>
<source>Fallback-gain</source>
<translation>Ganancia de respaldo</translation>
<translation type="unfinished">Fallback-gain</translation>
</message>
<message>
<source>EBU R 128 Loudness Normalization</source>
<translation>Normalización de sonoridad EBU R 128</translation>
<translation type="unfinished">EBU R 128 Loudness Normalization</translation>
</message>
<message>
<source>Perform track loudness normalization</source>
<translation>Realizar la normalización de la sonoridad de la pista</translation>
<translation type="unfinished">Perform track loudness normalization</translation>
</message>
<message>
<source>Target Level</source>
<translation>Nivel objetivo</translation>
<translation type="unfinished">Target Level</translation>
</message>
<message>
<source>Fading</source>
@@ -712,7 +712,7 @@
</message>
<message>
<source>Show song progress on taskbar</source>
<translation>Mostrar el progreso de la canción en la barra de tareas</translation>
<translation type="unfinished">Show song progress on taskbar</translation>
</message>
<message>
<source>Resume playback on start</source>
@@ -850,15 +850,15 @@
<name>CollectionBackend</name>
<message>
<source>Unable to execute collection SQL query: %1</source>
<translation>No se puede ejecutar la consulta SQL de recopilación: %1</translation>
<translation type="unfinished">Unable to execute collection SQL query: %1</translation>
</message>
<message>
<source>Failed SQL query: %1</source>
<translation>Consulta SQL fallida: %1</translation>
<translation type="unfinished">Failed SQL query: %1</translation>
</message>
<message>
<source>Updating %1 database.</source>
<translation>Actualizando la base de datos %1.</translation>
<translation type="unfinished">Updating %1 database.</translation>
</message>
</context>
<context>
@@ -873,7 +873,7 @@
</message>
<message>
<source>MenuPopupToolButton</source>
<translation>Botón de herramienta emergente de menú</translation>
<translation type="unfinished">MenuPopupToolButton</translation>
</message>
<message>
<source>Entire collection</source>
@@ -992,7 +992,7 @@
<name>CollectionLibrary</name>
<message>
<source>Saving playcounts and ratings</source>
<translation>Guardar recuentos de reproducciones y calificaciones</translation>
<translation type="unfinished">Saving playcounts and ratings</translation>
</message>
</context>
<context>
@@ -1050,11 +1050,11 @@
</message>
<message>
<source>Perform song EBU R 128 analysis (required for EBU R 128 loudness normalization)</source>
<translation>Realizar el análisis de la canción EBU R 128 (necesario para la normalización de la sonoridad EBU R 128)</translation>
<translation type="unfinished">Perform song EBU R 128 analysis (required for EBU R 128 loudness normalization)</translation>
</message>
<message>
<source>Expire unavailable songs after</source>
<translation>Las canciones no disponibles expirarán después de</translation>
<translation type="unfinished">Expire unavailable songs after</translation>
</message>
<message>
<source>days</source>
@@ -1087,11 +1087,11 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Use various artists for compilation albums</source>
<translation>Utilice varios artistas para álbumes recopilatorios</translation>
<translation type="unfinished">Use various artists for compilation albums</translation>
</message>
<message>
<source>Skip leading articles (&quot;the&quot;, &quot;a&quot;, &quot;an&quot;) when sorting artist names</source>
<translation>Omitir los artículos iniciales ("él", "un", "una") al ordenar los nombres de los artistas</translation>
<translation type="unfinished">Skip leading articles (&quot;the&quot;, &quot;a&quot;, &quot;an&quot;) when sorting artist names</translation>
</message>
<message>
<source>Album cover pixmap cache</source>
@@ -1119,27 +1119,27 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Song playcounts and ratings</source>
<translation>Número de reproducciones y calificaciones de canciones</translation>
<translation type="unfinished">Song playcounts and ratings</translation>
</message>
<message>
<source>Save playcounts to song tags when possible</source>
<translation>Guarde los recuentos de reproducciones en las etiquetas de las canciones cuando sea posible</translation>
<translation type="unfinished">Save playcounts to song tags when possible</translation>
</message>
<message>
<source>Save ratings to song tags when possible</source>
<translation>Guarde las calificaciones en las etiquetas de las canciones cuando sea posible</translation>
<translation type="unfinished">Save ratings to song tags when possible</translation>
</message>
<message>
<source>Overwrite database playcount when songs are re-read from disk</source>
<translation>Sobrescribir el recuento de reproducciones de la base de datos cuando se vuelven a leer las canciones desde el disco</translation>
<translation type="unfinished">Overwrite database playcount when songs are re-read from disk</translation>
</message>
<message>
<source>Overwrite database rating when songs are re-read from disk</source>
<translation>Sobrescribir la clasificación de la base de datos cuando se vuelven a leer las canciones desde el disco</translation>
<translation type="unfinished">Overwrite database rating when songs are re-read from disk</translation>
</message>
<message>
<source>Save playcounts and ratings to files now</source>
<translation>Guarda recuentos de reproducciones y calificaciones en archivos ahora</translation>
<translation type="unfinished">Save playcounts and ratings to files now</translation>
</message>
<message>
<source>Enable delete files in the right click context menu</source>
@@ -1151,7 +1151,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Write all playcounts and ratings to files</source>
<translation>Escribe todos los recuentos de reproducción y calificaciones en archivos</translation>
<translation type="unfinished">Write all playcounts and ratings to files</translation>
</message>
<message>
<source>Are you sure you want to write song playcounts and ratings to file for all songs in your collection?</source>
@@ -1238,7 +1238,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Error</source>
<translation>Error</translation>
<translation type="unfinished">Error</translation>
</message>
<message>
<source>None of the selected songs were suitable for copying to a device</source>
@@ -1461,11 +1461,11 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>EBU R 128 Integrated Loudness</source>
<translation>EBU R 128 Sonoridad integrada</translation>
<translation type="unfinished">EBU R 128 Integrated Loudness</translation>
</message>
<message>
<source>EBU R 128 Loudness Range</source>
<translation/>
<translation type="unfinished">EBU R 128 Loudness Range</translation>
</message>
<message>
<source>Show album cover</source>

View File

@@ -9,7 +9,7 @@
</message>
<message>
<source>About Strawberry</source>
<translation>Rakenduse teave: Strawberry</translation>
<translation>Strawberry teave</translation>
</message>
<message>
<source>Version %1</source>
@@ -45,7 +45,7 @@
</message>
<message>
<source>Contributors</source>
<translation>Kaasautorid</translation>
<translation>Toetajad</translation>
</message>
<message>
<source>Clementine authors</source>
@@ -53,7 +53,7 @@
</message>
<message>
<source>Clementine contributors</source>
<translation>Clementine'i kaasautorid</translation>
<translation>Clementine'i toetajad</translation>
</message>
<message>
<source>Thanks to</source>
@@ -233,7 +233,7 @@
</message>
<message>
<source>Really cancel?</source>
<translation>Kas tõesti katkestame?</translation>
<translation>Kas tühistada?</translation>
</message>
<message>
<source>Closing this window will stop searching for album covers.</source>

View File

@@ -9,7 +9,7 @@
</message>
<message>
<source>About Strawberry</source>
<translation>О программе Strawberry</translation>
<translation>О Strawberry</translation>
</message>
<message>
<source>Version %1</source>
@@ -95,7 +95,7 @@
</message>
<message>
<source>Unset cover</source>
<translation>Сбросить обложку</translation>
<translation>Удалить обложку</translation>
</message>
<message>
<source>Delete cover</source>
@@ -621,11 +621,11 @@
</message>
<message>
<source>Replay Gain</source>
<translation>Нормализация громкости (Replay Gain)</translation>
<translation>Нормализация громкости</translation>
</message>
<message>
<source>Use Replay Gain metadata if it is available</source>
<translation>Использовать метаданные нормализации (Replay Gain) по возможности</translation>
<translation>Использовать метаданные нормализации по возможности</translation>
</message>
<message>
<source>Replay Gain mode</source>
@@ -657,7 +657,7 @@
</message>
<message>
<source>Perform track loudness normalization</source>
<translation>Нормализовать громкость дорожки</translation>
<translation>Выполнять нормализацию громкости дорожки</translation>
</message>
<message>
<source>Target Level</source>
@@ -776,7 +776,7 @@
</message>
<message>
<source>Pressing &quot;Previous&quot; in player will...</source>
<translation>Нажатие кнопки «Предыдущий» выполнит</translation>
<translation>Нажатие кнопки «Предыдущий» осуществит</translation>
</message>
<message>
<source>Jump to previous song right away</source>
@@ -6078,7 +6078,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Ever played</source>
<translation>Когда-либо прослушивались</translation>
<translation>Любые прослушанные</translation>
</message>
<message>
<source>Never played</source>