Compare commits
22 Commits
xcb_qpa
...
l10n_maste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
610b458196 | ||
|
|
ad285a91f2 | ||
|
|
6400f903e8 | ||
|
|
83d5f3d8f2 | ||
|
|
582b8e8076 | ||
|
|
030908f6ac | ||
|
|
34ae443548 | ||
|
|
1c9e99e776 | ||
|
|
4e6459b977 | ||
|
|
d2b5359fa9 | ||
|
|
1d82977441 | ||
|
|
17519076f5 | ||
|
|
e8d9e1172f | ||
|
|
aac8d4e68b | ||
|
|
0e28e800b3 | ||
|
|
cf84bc29ab | ||
|
|
afc3effc9d | ||
|
|
370bebff5f | ||
|
|
db410cc257 | ||
|
|
20a9946e51 | ||
|
|
b6c8ff19af | ||
|
|
80d058af10 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
175
src/core/unixsignalwatcher.cpp
Normal file
175
src/core/unixsignalwatcher.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
53
src/core/unixsignalwatcher.h
Normal file
53
src/core/unixsignalwatcher.h
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
205
src/fileview/fileviewtree.cpp
Normal file
205
src/fileview/fileviewtree.cpp
Normal 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());
|
||||
}
|
||||
78
src/fileview/fileviewtree.h
Normal file
78
src/fileview/fileviewtree.h
Normal 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
|
||||
52
src/fileview/fileviewtreeitem.h
Normal file
52
src/fileview/fileviewtreeitem.h
Normal 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
|
||||
246
src/fileview/fileviewtreemodel.cpp
Normal file
246
src/fileview/fileviewtreemodel.cpp
Normal 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();
|
||||
|
||||
}
|
||||
72
src/fileview/fileviewtreemodel.h
Normal file
72
src/fileview/fileviewtreemodel.h
Normal 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
|
||||
12
src/main.cpp
12
src/main.cpp
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -1227,7 +1227,7 @@ If there are no matches then it will use the largest image in the directory.</so
|
||||
</message>
|
||||
<message>
|
||||
<source>Don't show in various artists</source>
|
||||
<translation type="unfinished">Don'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't create playlist</source>
|
||||
<translation type="unfinished">Couldn'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 "Behavior" preferences</source>
|
||||
<translation type="unfinished">This option can be changed in the "Behavior" preferences</translation>
|
||||
<translation/>
|
||||
</message>
|
||||
<message>
|
||||
<source>Double-click here to favorite this playlist so it will be saved and remain accessible through the "Playlists" 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 <n>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 "Save" to copy the URL to clipboard and manually open it in a web browser.</source>
|
||||
<translation type="unfinished">Press "Save" to copy the URL to clipboard and manually open it in a web browser.</translation>
|
||||
<translation>Πατήστε "Save" για να αντιγράψετε το 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>
|
||||
|
||||
@@ -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&lughw</source>
|
||||
<translation type="unfinished">p&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 ("the", "a", "an") when sorting artist names</source>
|
||||
<translation type="unfinished">Skip leading articles ("the", "a", "an") 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 "Previous" 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>
|
||||
|
||||
Reference in New Issue
Block a user