Compare commits

..

22 Commits

Author SHA1 Message Date
Strawberry Bot
610b458196 New translations 2026-01-13 00:14:09 +01:00
Célestin Matte
ad285a91f2 PlaylistContainer: Remove duplicate connect 2026-01-12 21:55:58 +01:00
dependabot[bot]
6400f903e8 Bump vmactions/freebsd-vm from 1.3.6 to 1.3.7
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.3.6 to 1.3.7.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.3.6...v1.3.7)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-12 21:49:52 +01:00
Jonas Kvinge
83d5f3d8f2 Song: Remove Spotify from stream_url_can_expire
Spotify URIs don't expire and are handled directly by gst-plugin-spotify.
Only the access token needs refresh, which is handled via UpdateSpotifyAccessToken().
2026-01-09 00:27:18 +01:00
Jonas Kvinge
582b8e8076 Make sure collection directory (root) is not removed from subdirs
Fixes #1914
2026-01-08 23:40:13 +01:00
Jonas Kvinge
030908f6ac CollectionWatcher: Avoid checking for valid media file early
Optimize the collection scanning process by deferring media file validation from the initial directory scan to the actual file processing stage. Instead of calling `IsMediaFileBlocking` early to filter files, all non-rejected files are added to the scan queue and validated later during `ReadFileBlocking`. Invalid files are removed from the tracked files list, causing them to be treated as deleted from the collection.
2026-01-06 22:39:58 +01:00
Jonas Kvinge
34ae443548 CMake: Remove commented line 2026-01-06 20:40:15 +01:00
Jonas Kvinge
1c9e99e776 CMake: Remove hard-coded -std=c11 and -std=c++17 2026-01-06 20:39:37 +01:00
dependabot[bot]
4e6459b977 Bump vmactions/freebsd-vm from 1.3.5 to 1.3.6
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.3.5 to 1.3.6.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.3.5...v1.3.6)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 17:50:11 +01:00
Jonas Kvinge
d2b5359fa9 UnixSignalWatcher: Ignore -Wunused-result 2026-01-04 01:03:48 +01:00
Jonas Kvinge
1d82977441 Exit on SIGTERM 2026-01-04 00:23:13 +01:00
Marcus Müller
17519076f5 Include .webp in allowed extensions
Modern Qt can read and write webp out of the box, no use excluding that.

Signed-off-by: Marcus Müller <mueller@baseband.digital>
2026-01-03 16:55:29 +01:00
Jonas Kvinge
e8d9e1172f FileViewTreeModel: Add const 2026-01-03 16:09:56 +01:00
Alexopus
aac8d4e68b Add file tree view 2026-01-03 15:11:56 +01:00
dependabot[bot]
0e28e800b3 Bump vmactions/freebsd-vm from 1.3.4 to 1.3.5
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.3.4...v1.3.5)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 17:33:02 +01:00
Jonas Kvinge
cf84bc29ab CI: Manually codesign 2026-01-01 01:51:10 +01:00
Jonas Kvinge
afc3effc9d CI: Switch macOS dependencies repo 2025-12-30 20:01:34 +01:00
Jonas Kvinge
370bebff5f CollectionView: Fix Enter/Return behavior to respect double-click settings
Fixes #1691
2025-12-30 19:08:52 +01:00
Jonas Kvinge
db410cc257 MainWindow: Remove unused declaration 2025-12-29 22:14:08 +01:00
Jonas Kvinge
20a9946e51 Song: Prefer filenames with "front" or "cover" for art automatic
Fixes #1745
2025-12-29 21:16:06 +01:00
dependabot[bot]
b6c8ff19af Bump vmactions/freebsd-vm from 1.3.2 to 1.3.4
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.3.2 to 1.3.4.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.3.2...v1.3.4)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 18:18:55 +01:00
dependabot[bot]
80d058af10 Bump vmactions/openbsd-vm from 1.2.9 to 1.3.1
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.2.9 to 1.3.1.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.2.9...v1.3.1)

---
updated-dependencies:
- dependency-name: vmactions/openbsd-vm
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 17:23:16 +01:00
31 changed files with 1534 additions and 263 deletions

View File

@@ -747,7 +747,7 @@ jobs:
df -h
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.3.2
uses: vmactions/freebsd-vm@v1.3.7
with:
usesh: true
mem: 8192
@@ -772,7 +772,7 @@ jobs:
submodules: recursive
- name: Build OpenBSD
id: build-openbsd
uses: vmactions/openbsd-vm@v1.2.9
uses: vmactions/openbsd-vm@v1.3.1
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$(test "${{env.arch}}" = "x86_64" && echo "-intel" || echo "")/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies/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} 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,libbrotlidec.1.dylib,libsoup-3.0.0.dylib,libnghttp2.14.dylib,libpsl.5.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,8 +84,6 @@ 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
@@ -218,16 +216,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 LinguistTools Test)
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test)
if(UNIX AND NOT APPLE)
list(APPEND QT_OPTIONAL_COMPONENTS DBus XcbQpaPrivate)
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
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}::XcbQpaPrivate")
set(QT_XCB_QPA_PRIVATE_FOUND ON)
if(TARGET "Qt${QT_VERSION_MAJOR}::GuiPrivate")
set(QT_GUI_PRIVATE_FOUND ON)
endif()
if(Qt${QT_VERSION_MAJOR}DBus_FOUND)
@@ -255,7 +253,6 @@ find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
if(APPLE)
find_library(SPARKLE Sparkle)
#find_package(SPMediaKeyTap REQUIRED)
endif()
if(WIN32)
@@ -369,8 +366,8 @@ if(APPLE OR WIN32)
)
endif()
optional_component(QT_XCB_QPA_PRIVATE ON "XCB QPA Platform Native Interface"
DEPENDS "Qt XCB QPA Private" QT_XCB_QPA_PRIVATE_FOUND
optional_component(QPA_QPLATFORMNATIVEINTERFACE ON "QPA Platform Native Interface"
DEPENDS "Qt Gui Private" QT_GUI_PRIVATE_FOUND
)
optional_component(STREAMTAGREADER ON "Stream tagreader"
@@ -823,6 +820,8 @@ 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
@@ -1112,6 +1111,8 @@ 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
@@ -1214,6 +1215,10 @@ 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
@@ -1549,7 +1554,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_QT_XCB_QPA_PRIVATE}>:Qt${QT_VERSION_MAJOR}::XcbQpaPrivate>
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
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-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -537,10 +537,24 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
ScopedTransaction transaction(&db);
for (const CollectionSubdirectory &subdir : subdirs) {
if (subdir.mtime == 0) {
// Delete the subdirectory
// See if this subdirectory already exists in the database
bool exists = false;
{
SqlQuery q(db);
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
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.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
if (!q.Exec()) {
@@ -549,42 +563,36 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
}
}
else {
// 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();
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;
}
}
}
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;
}
}
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;
}
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -252,6 +252,7 @@ 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,6 +124,7 @@ 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()) {
AddToPlaylist();
Q_EMIT doubleClicked(currentIndex());
}
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-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -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_(dir),
dir_id_(dir),
incremental_(incremental),
ignores_mtime_(ignores_mtime),
mark_songs_unavailable_(mark_songs_unavailable),
@@ -313,6 +313,19 @@ 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);
@@ -338,34 +351,24 @@ 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_)) {
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
if (watcher_->watched_dirs_.contains(dir_id_)) {
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_id_], 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_)) {
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
if (watcher_->watched_dirs_.contains(dir_id_)) {
watcher_->AddWatch(watcher_->watched_dirs_[dir_id_], subdir.path);
}
}
}
new_subdirs.clear();
if (incremental_ || ignores_mtime_) {
Q_EMIT watcher_->UpdateLastSeen(dir_, expire_unavailable_songs_days_);
Q_EMIT watcher_->UpdateLastSeen(dir_id_, expire_unavailable_songs_days_);
}
}
@@ -374,7 +377,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) {
if (cached_songs_dirty_) {
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_);
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_id_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
cached_songs_.insert(p, song);
@@ -393,7 +396,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_);
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_id_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
cached_songs_missing_fingerprint_.insert(p, song);
@@ -408,7 +411,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_);
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_id_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
cached_songs_missing_loudness_characteristics_.insert(p, song);
@@ -430,7 +433,7 @@ void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const CollectionSubdire
bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
}
return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const CollectionSubdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; });
@@ -440,7 +443,7 @@ bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
}
CollectionSubdirectoryList ret;
@@ -457,7 +460,7 @@ CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdi
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
}
return known_subdirs_;
@@ -494,7 +497,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.path, CollectionSubdirectory(), files_count, &transaction);
ScanSubdirectory(dir, dir.path, CollectionSubdirectory(), files_count, &transaction);
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
}
else {
@@ -512,7 +515,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(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
}
if (!stop_or_abort_requested()) {
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
@@ -524,7 +527,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
}
void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
const QFileInfo path_info(path);
@@ -536,8 +539,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
return;
}
// Do not scan symlinked dirs that are already in collection
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
if (real_path.startsWith(dir.path)) {
for (const CollectionDirectory &i : std::as_const(watched_dirs_)) {
if (real_path.startsWith(i.path)) {
return;
}
}
@@ -578,7 +581,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
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(prev_subdir.path, prev_subdir, 0, t, true);
ScanSubdirectory(dir, prev_subdir.path, prev_subdir, 0, t, true);
}
}
@@ -620,11 +623,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
album_art[dir_part] << child_filepath;
t->AddToProgress(1);
}
else if (tagreader_client_->IsMediaFileBlocking(child_filepath)) {
files_on_disk << child_filepath;
}
else {
t->AddToProgress(1);
files_on_disk << child_filepath;
}
}
}
@@ -727,7 +727,9 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
#endif
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t);
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t)) {
files_on_disk.removeAll(file);
}
}
else { // If CUE associated.
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
@@ -784,7 +786,9 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
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.
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t);
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t)) {
files_on_disk.removeAll(file);
}
}
else { // If CUE associated.
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
@@ -795,6 +799,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
const SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
if (songs.isEmpty()) {
files_on_disk.removeAll(file);
t->AddToProgress(1);
continue;
}
@@ -805,7 +810,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
const QUrl art_automatic = ArtForSong(file, album_art);
for (Song song : songs) {
song.set_directory_id(t->dir());
song.set_directory_id(t->dir_id());
if (song.art_automatic().isEmpty()) song.set_art_automatic(art_automatic);
t->new_songs << song;
}
@@ -823,27 +828,26 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
}
}
// Add this subdir to the new or touched list
// Add, update or delete subdir
CollectionSubdirectory updated_subdir;
updated_subdir.directory_id = t->dir();
updated_subdir.directory_id = t->dir_id();
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0;
updated_subdir.path = path;
if (subdir.directory_id == -1) {
if (updated_subdir.mtime == 0 && updated_subdir.path != dir.path) {
t->deleted_subdirs << updated_subdir;
}
else if (subdir.directory_id == -1) {
t->new_subdirs << updated_subdir;
}
else {
else if (subdir.mtime != updated_subdir.mtime) {
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(my_new_subdir.path, my_new_subdir, 0, t, true);
ScanSubdirectory(dir, my_new_subdir.path, my_new_subdir, 0, t, true);
}
}
@@ -875,7 +879,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());
new_cue_song.set_directory_id(t->dir_id());
PerformEBUR128Analysis(new_cue_song);
new_cue_song.set_fingerprint(fingerprint);
@@ -901,7 +905,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
}
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
const QString &fingerprint,
const SongList &matching_songs,
const QUrl &art_automatic,
@@ -922,7 +926,7 @@ void 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());
song_on_disk.set_directory_id(t->dir_id());
song_on_disk.set_id(matching_song.id());
PerformEBUR128Analysis(song_on_disk);
song_on_disk.set_fingerprint(fingerprint);
@@ -931,6 +935,8 @@ void 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 {
@@ -1199,12 +1205,13 @@ void CollectionWatcher::DirectoryChanged(const QString &subdir) {
void CollectionWatcher::RescanPathsNow() {
const QList<int> dirs = rescan_queue_.keys();
for (const int dir : dirs) {
if (stop_or_abort_requested()) break;
ScanTransaction transaction(this, dir, false, false, mark_songs_unavailable_);
const QList<int> dir_ids = rescan_queue_.keys();
for (const int dir_id : dir_ids) {
const QStringList paths = rescan_queue_.value(dir);
if (stop_or_abort_requested()) break;
ScanTransaction transaction(this, dir_id, false, false, mark_songs_unavailable_);
const QStringList paths = rescan_queue_.value(dir_id);
QMap<QString, quint64> subdir_files_count;
for (const QString &path : paths) {
@@ -1215,11 +1222,14 @@ 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;
subdir.directory_id = dir_id;
subdir.mtime = 0;
subdir.path = path;
ScanSubdirectory(path, subdir, subdir_files_count[path], &transaction);
ScanSubdirectory(subdir_mapping_[path], path, subdir, subdir_files_count[path], &transaction);
}
}
@@ -1344,11 +1354,13 @@ 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();
if (subdirs.isEmpty()) {
qLog(Debug) << "Collection directory wasn't in subdir list.";
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";
CollectionSubdirectory subdir;
subdir.path = dir.path;
subdir.directory_id = dir.id;
subdir.path = dir.path;
subdir.mtime = 0;
subdirs << subdir;
}
@@ -1358,7 +1370,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(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
}
}
@@ -1459,6 +1471,8 @@ 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_);
@@ -1468,7 +1482,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(song_path, subdir, files_count, &transaction);
ScanSubdirectory(dir, 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-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -85,6 +85,7 @@ 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();
@@ -122,7 +123,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() const { return dir_; }
int dir_id() const { return dir_id_; }
bool is_incremental() const { return incremental_; }
bool ignores_mtime() const { return ignores_mtime_; }
@@ -143,7 +144,7 @@ class CollectionWatcher : public QObject {
quint64 progress_;
quint64 progress_max_;
int dir_;
int dir_id_;
// 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.
@@ -179,7 +180,7 @@ class CollectionWatcher : public QObject {
void IncrementalScanNow();
void FullScanNow();
void RescanPathsNow();
void ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
void ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
void RescanSongs(const SongList &songs);
private:
@@ -202,7 +203,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.
void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
bool 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_QT_XCB_QPA_PRIVATE
#cmakedefine HAVE_QPA_QPLATFORMNATIVEINTERFACE
#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)");
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
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)");
#endif // FILEFILTERCONSTANTS_H

View File

@@ -43,7 +43,6 @@
#include <QString>
#include <QUrl>
#include <QImage>
#include <QPixmap>
#include <QTimer>
#include <QtEvents>
@@ -246,7 +245,6 @@ 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);
@@ -281,6 +279,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
public Q_SLOTS:
void CommandlineOptionsReceived(const QByteArray &string_options);
void Raise();
void Exit();
private:
void SaveSettings();
@@ -290,9 +289,6 @@ 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 || d->source_ == Source::Spotify; }
bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz; }
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,12 +1668,24 @@ void Song::InitArtManual() {
void Song::InitArtAutomatic() {
if (d->art_automatic_.isEmpty() && d->source_ == Source::LocalFile && d->url_.isLocalFile()) {
// 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());
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);
}
}

View File

@@ -0,0 +1,175 @@
/*
* 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

@@ -0,0 +1,53 @@
/*
* 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,6 +75,7 @@ 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,13 +29,17 @@
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QSettings>
#include <QStandardPaths>
#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"
@@ -45,10 +49,11 @@
#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;
@@ -57,9 +62,12 @@ 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)) {
storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)),
tree_view_active_(false),
view_mode_spacer_(nullptr) {
ui_->setupUi(this);
@@ -68,12 +76,14 @@ 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);
@@ -87,6 +97,22 @@ 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' ');
@@ -109,6 +135,19 @@ 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();
}
@@ -180,24 +219,46 @@ void FileView::ChangeFilePathWithoutUndo(const QString &new_path) {
}
void FileView::ItemActivated(const QModelIndex &idx) {
if (model_->isDir(idx))
// Only handle activation for list view (not tree view)
if (!tree_view_active_ && model_->isDir(idx)) {
ChangeFilePath(model_->filePath(idx));
}
}
void FileView::ItemDoubleClick(const QModelIndex &idx) {
if (model_->isDir(idx)) {
return;
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;
}
}
QString file_path = model_->filePath(idx);
// 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;
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);
Q_EMIT AddToPlaylist(mimedata);
}
}
@@ -272,12 +333,156 @@ 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,10 +40,12 @@ 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
@@ -76,12 +78,22 @@ 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 {
@@ -110,16 +122,21 @@ 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,28 +95,143 @@
<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="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>
<widget class="QStackedWidget" name="view_stack">
<property name="currentIndex">
<number>0</number>
</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>
@@ -127,6 +242,11 @@
<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

@@ -0,0 +1,205 @@
/*
* 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

@@ -0,0 +1,78 @@
/*
* 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

@@ -0,0 +1,52 @@
/*
* 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

@@ -0,0 +1,246 @@
/*
* 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

@@ -0,0 +1,72 @@
/*
* 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-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -76,6 +76,10 @@
#include <kdsingleapplication.h>
#ifdef Q_OS_UNIX
#include "core/unixsignalwatcher.h"
#endif
#ifdef HAVE_QTSPARKLE
# include <qtsparkle-qt6/Updater>
#endif // HAVE_QTSPARKLE
@@ -365,6 +369,12 @@ 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_QT_XCB_QPA_PRIVATE
#ifdef HAVE_QPA_QPLATFORMNATIVEINTERFACE
# include <qpa/qplatformnativeinterface.h>
#endif
@@ -215,7 +215,7 @@ void OSDPretty::ScreenRemoved(QScreen *screen) {
bool OSDPretty::IsTransparencyAvailable() {
#ifdef HAVE_QT_XCB_QPA_PRIVATE
#ifdef HAVE_QPA_QPLATFORMNATIVEINTERFACE
if (qApp && QGuiApplication::platformName() == "xcb"_L1) {
QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface();
QScreen *screen = popup_screen_ == nullptr ? QGuiApplication::primaryScreen() : popup_screen_;

View File

@@ -152,7 +152,6 @@ 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 type="unfinished">Don&apos;t show in various artists</translation>
<translation>Όχι εμφάνιση σε διάφορους καλλιτέχνες</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 type="unfinished">Set through album cover search (%1)</translation>
<translation>Ορισμός μέσω αναζήτησης εξωφύλλου άλμπουμ (%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 type="unfinished">Saving CUE files is not supported.</translation>
<translation>Η αποθήκευση αρχείων CUE δεν υποστηρίζεται.</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 type="unfinished">Different art across multiple songs.</translation>
<translation>Διαφορετική τέχνη σε πολλαπλά τραγούδια.</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 type="unfinished">Toggle skip status</translation>
<translation>Εναλλαγή κατάστασης παράκαμψης</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 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>
<translation>Τρέχετε το Strawberry κάτω από Rosetta. Η χρήση του Strawberry κάτω από Rosetta δεν υποστηρίζεται και είναι γνωστό ότι παρουσιάζει προβλήματα. Θα πρέπει να λάβετε το Strawberry για τη σωστή αρχιτεκτονική CPU από %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 type="unfinished">Loudness Range</translation>
<translation>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 type="unfinished">Couldn&apos;t create playlist</translation>
<translation>Αδυναμία δημιουργίας λίστας αναπαραγωγής</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 type="unfinished">Warn me when closing a playlist tab</translation>
<translation>Προειδοποίηση κατά το κλείσιμο μιας καρτέλας λίστας αναπαραγωγής</translation>
</message>
<message>
<source>This option can be changed in the &quot;Behavior&quot; preferences</source>
<translation type="unfinished">This option can be changed in the &quot;Behavior&quot; preferences</translation>
<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 type="unfinished">Loads files/URLs, replacing current playlist</translation>
<translation>Φόρτωση αρχείων/URL, αντικατάσταση της τρέχουσας λίστας αναπαραγωγής</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 type="unfinished">Destination file %1 exists, but not allowed to overwrite.</translation>
<translation>Το αρχείο προορισμού %1 υπάρχει, αλλά δεν επιτρέπεται να αντικατασταθεί.</translation>
</message>
<message>
<source>Destination file %1 exists, but not allowed to overwrite</source>
<translation type="unfinished">Destination file %1 exists, but not allowed to overwrite</translation>
<translation>Το αρχείο προορισμού %1 υπάρχει, αλλά δεν επιτρέπεται να αντικατασταθεί</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 type="unfinished">%1 songs in %2 different directories selected, are you sure you want to open them all?</translation>
<translation>%1 τραγούδια σε διαφορετικούς καταλόγους %2 επιλέχθηκαν, είστε σίγουροι ότι θέλετε να τα ανοίξετε όλα;</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 type="unfinished">Receiving album cover for %1 album...</translation>
<translation>Λήψη εξωφύλλου για το άλμπουμ %1...</translation>
</message>
<message>
<source>Receiving album covers for %1 albums...</source>
<translation type="unfinished">Receiving album covers for %1 albums...</translation>
<translation>Λήψη εξώφυλλων για άλμπουμ %1...</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 track(s)</numerusform>
<numerusform>%n κομμάτι(α)</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 type="unfinished">Move up</translation>
<translation>Μετακίνηση πάνω</translation>
</message>
<message>
<source>Ctrl+Down</source>
<translation type="unfinished">Ctrl+Down</translation>
<translation>Ctrl+Down</translation>
</message>
<message>
<source>Remove</source>
<translation type="unfinished">Remove</translation>
<translation>Αφαίρεση</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 type="unfinished">Getting %1 channels</translation>
<translation>Λήψη %1 καναλιών</translation>
</message>
</context>
<context>
<name>RadioView</name>
<message>
<source>Append to current playlist</source>
<translation type="unfinished">Append to current playlist</translation>
<translation>Προσάρτηση στην τρέχουσα λίστα</translation>
</message>
<message>
<source>Replace current playlist</source>
<translation type="unfinished">Replace current playlist</translation>
<translation>Αντικατάσταση της τρέχουσας λίστας</translation>
</message>
<message>
<source>Open in new playlist</source>
<translation type="unfinished">Open in new playlist</translation>
<translation>Άνοιγμα σε νέα λίστα</translation>
</message>
<message>
<source>Open homepage</source>
<translation type="unfinished">Open homepage</translation>
<translation>Άνοιγμα αρχικής σελίδας</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 type="unfinished">Select directory for saving playlists</translation>
<translation>Επιλέξτε φάκελο για τις λίστες αναπαραγωγής</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 type="unfinished">Show love button</translation>
<translation>Εμφάνιση πλήκτρου αγάπης</translation>
</message>
<message>
<source>Submit scrobbles every</source>
<translation type="unfinished">Submit scrobbles every</translation>
<translation>Υποβολή scrobbles κάθε</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 type="unfinished">Offline mode (Only cache scrobbles)</translation>
<translation>Λειτουργία εκτός σύνδεσης (Μόνο 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 type="unfinished">Open URL in web browser?</translation>
<translation>Άνοιγμα διεύθυνσης URL στο πρόγραμμα περιήγησης;</translation>
</message>
<message>
<source>Press &quot;Save&quot; to copy the URL to clipboard and manually open it in a web browser.</source>
<translation type="unfinished">Press &quot;Save&quot; to copy the URL to clipboard and manually open it in a web browser.</translation>
<translation>Πατήστε &quot;Save&quot; για να αντιγράψετε το URL στο πρόχειρο και να το ανοίξετε χειροκίνητα σε ένα πρόγραμμα περιήγησης.</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 type="unfinished">Invalid reply from web browser. Missing token.</translation>
<translation>Μη έγκυρη απάντηση από το πρόγραμμα περιήγησης. Λείπει token.</translation>
</message>
<message>
<source>Received invalid reply from web browser. Try another browser.</source>
<translation type="unfinished">Received invalid reply from web browser. Try another browser.</translation>
<translation>Λήφθηκε μη έγκυρη απάντηση από το πρόγραμμα περιήγησης. Δοκιμάστε ένα άλλο πρόγραμμα περιήγησης.</translation>
</message>
<message>
<source>Scrobbler %1 is not authenticated!</source>
<translation type="unfinished">Scrobbler %1 is not authenticated!</translation>
<translation>Το Scrobbler %1 δεν είναι πιστοποιημένο!</translation>
</message>
<message>
<source>Scrobbler %1 error: %2</source>
<translation type="unfinished">Scrobbler %1 error: %2</translation>
<translation>Scrobbler %1 σφάλμα: %2</translation>
</message>
</context>
<context>
@@ -5873,7 +5873,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Streaming</source>
<translation type="unfinished">Streaming</translation>
<translation>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 type="unfinished">Choose a name for your smart playlist</translation>
<translation>Επιλέξτε ένα όνομα για την έξυπνη λίστα αναπαραγωγής σας</translation>
</message>
</context>
<context>
<name>SmartPlaylistWizardFinishPage</name>
<message>
<source>Form</source>
<translation type="unfinished">Form</translation>
<translation>Φόρμα</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 type="unfinished">Use dynamic mode</translation>
<translation>Χρήση δυναμικής λειτουργίας</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 type="unfinished">Failed to open cover file %1 for reading: %2</translation>
<translation>No se pudo abrir el archivo de portada %1 para lectura: %2</translation>
</message>
<message>
<source>Cover file %1 is empty.</source>
<translation type="unfinished">Cover file %1 is empty.</translation>
<translation>El archivo de portada %1 está vacío.</translation>
</message>
<message>
<source>unknown</source>
@@ -135,27 +135,27 @@
</message>
<message>
<source>Failed to open cover file %1 for writing: %2</source>
<translation type="unfinished">Failed to open cover file %1 for writing: %2</translation>
<translation>No se pudo abrir el archivo de portada %1 para escribir: %2</translation>
</message>
<message>
<source>Failed writing cover to file %1: %2</source>
<translation type="unfinished">Failed writing cover to file %1: %2</translation>
<translation>Error al escribir la portada en el archivo %1: %2</translation>
</message>
<message>
<source>Failed writing cover to file %1.</source>
<translation type="unfinished">Failed writing cover to file %1.</translation>
<translation>Error al escribir la portada en el archivo %1.</translation>
</message>
<message>
<source>Failed to delete cover file %1: %2</source>
<translation type="unfinished">Failed to delete cover file %1: %2</translation>
<translation>No se pudo eliminar el archivo de portada %1: %2</translation>
</message>
<message>
<source>Failed to write cover to file %1: %2</source>
<translation type="unfinished">Failed to write cover to file %1: %2</translation>
<translation>No se pudo escribir la portada en el archivo %1: %2</translation>
</message>
<message>
<source>Could not save cover to file %1.</source>
<translation type="unfinished">Could not save cover to file %1.</translation>
<translation>No se pudo guardar la portada en el archivo %1.</translation>
</message>
</context>
<context>
@@ -273,7 +273,7 @@
</message>
<message>
<source>Could not save cover to file %1.</source>
<translation type="unfinished">Could not save cover to file %1.</translation>
<translation>No se pudo guardar la portada en el archivo %1.</translation>
</message>
</context>
<context>
@@ -339,7 +339,7 @@
</message>
<message>
<source>Turbine</source>
<translation type="unfinished">Turbine</translation>
<translation>Turbina</translation>
</message>
<message>
<source>Sonogram</source>
@@ -549,7 +549,7 @@
</message>
<message>
<source>p&amp;lughw</source>
<translation type="unfinished">p&amp;lughw</translation>
<translation>enchufe</translation>
</message>
<message>
<source>pcm</source>
@@ -569,7 +569,7 @@
</message>
<message>
<source>Upmix / downmix to</source>
<translation type="unfinished">Upmix / downmix to</translation>
<translation>Mezcla ascendente/descendente a</translation>
</message>
<message>
<source>channels</source>
@@ -577,15 +577,15 @@
</message>
<message>
<source>Improve headphone listening of stereo audio records (bs2b)</source>
<translation type="unfinished">Improve headphone listening of stereo audio records (bs2b)</translation>
<translation>Mejorar la escucha de grabaciones de audio estéreo con auriculares (B2B)</translation>
</message>
<message>
<source>Enable HTTP/2 for streaming</source>
<translation type="unfinished">Enable HTTP/2 for streaming</translation>
<translation>Habilitar HTTP/2 para transmisión</translation>
</message>
<message>
<source>Use strict SSL mode</source>
<translation type="unfinished">Use strict SSL mode</translation>
<translation>Utilice el modo SSL estricto</translation>
</message>
<message>
<source>Buffer</source>
@@ -649,19 +649,19 @@
</message>
<message>
<source>Fallback-gain</source>
<translation type="unfinished">Fallback-gain</translation>
<translation>Ganancia de respaldo</translation>
</message>
<message>
<source>EBU R 128 Loudness Normalization</source>
<translation type="unfinished">EBU R 128 Loudness Normalization</translation>
<translation>Normalización de sonoridad EBU R 128</translation>
</message>
<message>
<source>Perform track loudness normalization</source>
<translation type="unfinished">Perform track loudness normalization</translation>
<translation>Realizar la normalización de la sonoridad de la pista</translation>
</message>
<message>
<source>Target Level</source>
<translation type="unfinished">Target Level</translation>
<translation>Nivel objetivo</translation>
</message>
<message>
<source>Fading</source>
@@ -712,7 +712,7 @@
</message>
<message>
<source>Show song progress on taskbar</source>
<translation type="unfinished">Show song progress on taskbar</translation>
<translation>Mostrar el progreso de la canción en la barra de tareas</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 type="unfinished">Unable to execute collection SQL query: %1</translation>
<translation>No se puede ejecutar la consulta SQL de recopilación: %1</translation>
</message>
<message>
<source>Failed SQL query: %1</source>
<translation type="unfinished">Failed SQL query: %1</translation>
<translation>Consulta SQL fallida: %1</translation>
</message>
<message>
<source>Updating %1 database.</source>
<translation type="unfinished">Updating %1 database.</translation>
<translation>Actualizando la base de datos %1.</translation>
</message>
</context>
<context>
@@ -873,7 +873,7 @@
</message>
<message>
<source>MenuPopupToolButton</source>
<translation type="unfinished">MenuPopupToolButton</translation>
<translation>Botón de herramienta emergente de menú</translation>
</message>
<message>
<source>Entire collection</source>
@@ -992,7 +992,7 @@
<name>CollectionLibrary</name>
<message>
<source>Saving playcounts and ratings</source>
<translation type="unfinished">Saving playcounts and ratings</translation>
<translation>Guardar recuentos de reproducciones y calificaciones</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 type="unfinished">Perform song EBU R 128 analysis (required for EBU R 128 loudness normalization)</translation>
<translation>Realizar el análisis de la canción EBU R 128 (necesario para la normalización de la sonoridad EBU R 128)</translation>
</message>
<message>
<source>Expire unavailable songs after</source>
<translation type="unfinished">Expire unavailable songs after</translation>
<translation>Las canciones no disponibles expirarán después de</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 type="unfinished">Use various artists for compilation albums</translation>
<translation>Utilice varios artistas para álbumes recopilatorios</translation>
</message>
<message>
<source>Skip leading articles (&quot;the&quot;, &quot;a&quot;, &quot;an&quot;) when sorting artist names</source>
<translation type="unfinished">Skip leading articles (&quot;the&quot;, &quot;a&quot;, &quot;an&quot;) when sorting artist names</translation>
<translation>Omitir los artículos iniciales ("él", "un", "una") al ordenar los nombres de los artistas</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 type="unfinished">Song playcounts and ratings</translation>
<translation>Número de reproducciones y calificaciones de canciones</translation>
</message>
<message>
<source>Save playcounts to song tags when possible</source>
<translation type="unfinished">Save playcounts to song tags when possible</translation>
<translation>Guarde los recuentos de reproducciones en las etiquetas de las canciones cuando sea posible</translation>
</message>
<message>
<source>Save ratings to song tags when possible</source>
<translation type="unfinished">Save ratings to song tags when possible</translation>
<translation>Guarde las calificaciones en las etiquetas de las canciones cuando sea posible</translation>
</message>
<message>
<source>Overwrite database playcount when songs are re-read from disk</source>
<translation type="unfinished">Overwrite database playcount when songs are re-read from disk</translation>
<translation>Sobrescribir el recuento de reproducciones de la base de datos cuando se vuelven a leer las canciones desde el disco</translation>
</message>
<message>
<source>Overwrite database rating when songs are re-read from disk</source>
<translation type="unfinished">Overwrite database rating when songs are re-read from disk</translation>
<translation>Sobrescribir la clasificación de la base de datos cuando se vuelven a leer las canciones desde el disco</translation>
</message>
<message>
<source>Save playcounts and ratings to files now</source>
<translation type="unfinished">Save playcounts and ratings to files now</translation>
<translation>Guarda recuentos de reproducciones y calificaciones en archivos ahora</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 type="unfinished">Write all playcounts and ratings to files</translation>
<translation>Escribe todos los recuentos de reproducción y calificaciones en archivos</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 type="unfinished">Error</translation>
<translation>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 type="unfinished">EBU R 128 Integrated Loudness</translation>
<translation>EBU R 128 Sonoridad integrada</translation>
</message>
<message>
<source>EBU R 128 Loudness Range</source>
<translation type="unfinished">EBU R 128 Loudness Range</translation>
<translation/>
</message>
<message>
<source>Show album cover</source>

View File

@@ -9,7 +9,7 @@
</message>
<message>
<source>About Strawberry</source>
<translation>Strawberry teave</translation>
<translation>Rakenduse teave: Strawberry</translation>
</message>
<message>
<source>Version %1</source>
@@ -45,7 +45,7 @@
</message>
<message>
<source>Contributors</source>
<translation>Toetajad</translation>
<translation>Kaasautorid</translation>
</message>
<message>
<source>Clementine authors</source>
@@ -53,7 +53,7 @@
</message>
<message>
<source>Clementine contributors</source>
<translation>Clementine'i toetajad</translation>
<translation>Clementine'i kaasautorid</translation>
</message>
<message>
<source>Thanks to</source>
@@ -233,7 +233,7 @@
</message>
<message>
<source>Really cancel?</source>
<translation>Kas tühistada?</translation>
<translation>Kas tõesti katkestame?</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>Нормализация громкости</translation>
<translation>Нормализация громкости (Replay Gain)</translation>
</message>
<message>
<source>Use Replay Gain metadata if it is available</source>
<translation>Использовать метаданные нормализации по возможности</translation>
<translation>Использовать метаданные нормализации (Replay Gain) по возможности</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>