Compare commits

...

48 Commits

Author SHA1 Message Date
Jonas Kvinge
464fde1851 Release 1.1.1 2024-07-22 23:44:17 +02:00
Jonas Kvinge
2639498642 Update Changelog 2024-07-22 23:37:46 +02:00
Jonas Kvinge
49f074737c Remove placeholder text 2024-07-22 20:48:46 +02:00
Jonas Kvinge
3d53a8b434 AppearanceSettingsPage: Remove translatable 2024-07-22 19:00:24 +02:00
Jonas Kvinge
e260433c7a settings: Remove translatable 2024-07-22 18:58:18 +02:00
Jonas Kvinge
88ef8bff0b Update .gitignore 2024-07-22 18:29:43 +02:00
Jonas Kvinge
fbdac36f6f PlaylistView: Adjust initial header layout 2024-07-20 15:19:50 +02:00
Jonas Kvinge
da3876bd83 StretchHeaderView: Properly implement reset 2024-07-20 15:19:28 +02:00
Jonas Kvinge
d303e700ae CollectionFilter: Override mimedata function 2024-07-20 01:55:53 +02:00
Jonas Kvinge
92a1173b9e Remove unused FilterParserRatingComparatorDecorator 2024-07-19 18:18:02 +02:00
Jonas Kvinge
9f7ebb1ac7 CI: Remove Ubuntu mantic and add oracular for PPA 2024-07-19 18:14:10 +02:00
Jonas Kvinge
1a8690e1f2 StretchHeaderView: Make sure section size never is zero
Fixes #1085
2024-07-19 17:51:49 +02:00
Jonas Kvinge
6543e4c5da Use common filter parser for collection and playlist 2024-07-19 17:29:05 +02:00
Jonas Kvinge
dd904fe3c2 Remove unused CollectionQueryOptions class 2024-07-18 02:06:48 +02:00
ajtribick
c14cc6bf0b CMake: Use result of find_program instead of calling xgettext directly 2024-07-18 00:20:25 +02:00
Jonas Kvinge
95c265ffd3 CollectionFilter: Match individual words 2024-07-17 01:41:25 +02:00
Jonas Kvinge
31c1ae68df EditTagDialog: Fix build without MusicBrainz
Fixes #1492
2024-07-17 00:00:17 +02:00
Jonas Kvinge
f2eb0c3b6b CollectionModel: Add ItemNeverHasChildren 2024-07-15 14:28:29 +02:00
Jonas Kvinge
32be33847c CollectionFilter: Move early return 2024-07-15 14:16:56 +02:00
Jonas Kvinge
3100b0c044 CollectionFilter: Use recursive filtering
Fixes #1486
Fixes #1487
2024-07-15 13:44:50 +02:00
Jonas Kvinge
f4ec3ab379 CollectionModel: Don't append artist if song is compilation 2024-07-14 20:21:08 +02:00
Jonas Kvinge
cdd7faa9bb CI: Add Fedora 41 and Ubuntu Oracular 2024-07-14 17:47:43 +02:00
Jonas Kvinge
e7b35aeaf7 CMake: Use find_package CONFIG for Boost 2024-07-14 17:47:43 +02:00
Jonas Kvinge
696256eb5b README: Update macOS and Windows download info 2024-07-14 17:46:11 +02:00
Mikel Pérez
8ad560ce0e simplify CreateElementForMimeType + good practices
suggestions from gstreamer dev slomo on gst's matrix:
- whole static_pad_templates loop can be avoided with
  gst_element_factory_can_src_any_caps
- ffmpeg elements have been av* prefixed for a while now
- should be looking for Muxer instead of Codec/Muxer,
  Encoder/Audio instead of Codec/Encoder/Audio,
  and there are constants for that
2024-07-14 17:24:42 +02:00
Jonas Kvinge
1c71506f62 Turn off git revision 2024-07-14 17:20:58 +02:00
Jonas Kvinge
8bea6ec5b0 Release 1.1.0 2024-07-14 14:55:14 +02:00
Jonas Kvinge
e8144487ee Update Changelog 2024-07-14 14:48:29 +02:00
Jonas Kvinge
41d9d15dda MainWindow: Only show sponsor dialog if update dialog is answered 2024-07-13 18:24:47 +02:00
Jonas Kvinge
124b97c024 Turn on git revision 2024-07-10 21:58:29 +02:00
Jonas Kvinge
98e0b45403 Release 1.1.0-rc4 2024-07-10 20:07:01 +02:00
Jonas Kvinge
1f2b8d8bf6 Rename playlist filter classes 2024-07-10 18:27:17 +02:00
Jonas Kvinge
8327751b91 CollectionFilter: Optimize use of QRegularExpression
Possible fix for #1482
2024-07-09 22:06:42 +02:00
Jonas Kvinge
6417f89596 CollectionFilter: Add std::as_const 2024-07-09 18:06:46 +02:00
Jonas Kvinge
2e53656f44 Turn on git revision 2024-07-09 17:52:21 +02:00
Jonas Kvinge
822cf0ad07 Release 1.1.0-rc3 2024-07-09 16:23:10 +02:00
Jonas Kvinge
67f04a81b3 Playlist: Add data changed when setting current row 2024-07-09 16:21:09 +02:00
Jonas Kvinge
9232ad0125 TagReaderTagLib: Use QString for converting TagLib::String
Converting directly to std::string does not seem to work correctly.
2024-07-09 15:56:07 +02:00
Jonas Kvinge
0de87b3e1e TagReaderTagLib: Use UTF-8 when converting to CString
Fixes #1481
2024-07-09 15:06:07 +02:00
Jonas Kvinge
74b8cd6156 StretchHeaderView: Formatting 2024-07-09 11:35:01 +02:00
Jonas Kvinge
ac959387fe StretchHeaderView: Fix infinite loop
Fixes #1480
2024-07-09 11:14:31 +02:00
Jonas Kvinge
ffd8ce9281 Turn on git revision 2024-07-09 10:44:11 +02:00
Jonas Kvinge
d8052b295f Release 1.1.0-rc2 2024-07-09 04:39:48 +02:00
Jonas Kvinge
625929133c Rename analyzers and add turbine analyzer 2024-07-09 04:39:48 +02:00
Jonas Kvinge
79c28e7e1d sqlite_test: Remove fts5 2024-07-09 03:24:13 +02:00
Jonas Kvinge
01f4a79f07 schema: Bump schema version to 20 2024-07-09 03:19:29 +02:00
Jonas Kvinge
47c5a2215e device-schema: Remove fts5 table 2024-07-09 03:18:43 +02:00
Jonas Kvinge
bb6e38630f Turn on git revision 2024-07-09 03:18:04 +02:00
63 changed files with 1695 additions and 1715 deletions

View File

@@ -171,7 +171,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
fedora_version: [ '39', '40' ] fedora_version: [ '39', '40', '41' ]
container: container:
image: fedora:${{matrix.fedora_version}} image: fedora:${{matrix.fedora_version}}
steps: steps:
@@ -544,7 +544,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
ubuntu_version: [ 'focal', 'jammy', 'mantic', 'noble' ] ubuntu_version: [ 'focal', 'jammy', 'noble', 'oracular' ]
container: container:
image: ubuntu:${{matrix.ubuntu_version}} image: ubuntu:${{matrix.ubuntu_version}}
steps: steps:
@@ -633,7 +633,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
ubuntu_version: [ 'focal', 'jammy', 'mantic', 'noble' ] ubuntu_version: [ 'focal', 'jammy', 'noble', 'oracular' ]
container: container:
image: ubuntu:${{matrix.ubuntu_version}} image: ubuntu:${{matrix.ubuntu_version}}
steps: steps:

132
.gitignore vendored
View File

@@ -1,120 +1,16 @@
# This file is used to ignore files which are generated /build
# ---------------------------------------------------------------------------- /bin
/CMakeLists.txt.user
# Build /.kdev4
build/ /strawberry.kdev4
bin/ /.vscode
/.code-workspace
# CMake /.sublime-workspace
CMakeLists.txt.user /.idea
CMakeCache.txt
CMakeFiles
CMakeScripts
Makefile*
Testing
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.so.*
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Dump files
*.core
*.stackdump
# Qt
*build-*
moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
*.moc
*.qm
# Temporary files
*~
*.autosave
*.orig
*.rej
.*.kate-swp
.swp.*
.*.swp
*.flc
# Directory files
.directory
.DS_Store
Thumbs.db
# MinGW generated files
*.Debug
*.Release
# Package files
*.spec
*.nsi
*.plist
# Stuff in dist
maketarball.sh
changelog
# Translations
translations.pot
zanata.xml
.zanata-cache/
# QtCreator
CMakeLists.txt.user*
*.pro.user
*.pro.user.*
*creator.user*
target_wrapper.*
compile_commands.json
*.kdev4
*.vscode
*.code-workspace
*.sublime-workspace
# MSVC
CMakeSettings.json
/.vs /.vs
/out /out
/CMakeSettings.json
# CLion /dist/scripts/maketarball.sh
/.idea /dist/unix/strawberry.spec
/dist/windows/strawberry.nsi
src/translations/translations.pot

View File

@@ -105,7 +105,10 @@ find_package(Backtrace)
if(Backtrace_FOUND) if(Backtrace_FOUND)
set(HAVE_BACKTRACE ON) set(HAVE_BACKTRACE ON)
endif() endif()
find_package(Boost REQUIRED) find_package(Boost CONFIG)
if(NOT Boost_FOUND)
find_package(Boost REQUIRED)
endif()
find_package(ICU COMPONENTS uc i18n REQUIRED) find_package(ICU COMPONENTS uc i18n REQUIRED)
find_package(Protobuf CONFIG) find_package(Protobuf CONFIG)
if(NOT Protobuf_FOUND) if(NOT Protobuf_FOUND)

View File

@@ -2,7 +2,19 @@ Strawberry Music Player
======================= =======================
ChangeLog ChangeLog
Version 1.1.0-rc1: Version 1.1.1 (2024.07.22):
Bugfixes:
* Fixed compilation songs being split into different albums when using album grouping.
* Fixed adding playlist columns not working when stretch mode is disabled (#1085).
* Fixed resetting playlist columns.
* Fixed adding songs to playlist adding all songs instead of filtered songs.
* Fixed collection filter matching entire text instead of individual words.
Enhancements:
* Use same code for collection and playlist filter search.
Version 1.1.0 (2024.07.14):
Bugfixes: Bugfixes:
* Fixed crash when pressing CTRL + C (#1359). * Fixed crash when pressing CTRL + C (#1359).
@@ -28,14 +40,13 @@ Version 1.1.0-rc1:
* Only use playbin3 with GStreamer 1.24 and higher, not with GStreamer 1.22 or lower. * Only use playbin3 with GStreamer 1.24 and higher, not with GStreamer 1.22 or lower.
* (macOS/Windows) Fixed dash and hls streaming, plugins were missing. * (macOS/Windows) Fixed dash and hls streaming, plugins were missing.
* (Windows) Fixed incorrect colors in smart playlist wizard with Fusion in dark mode (#1399). * (Windows) Fixed incorrect colors in smart playlist wizard with Fusion in dark mode (#1399).
* (Windows) Fixed update window blocking sponsor window on startup.
Enhancements: Enhancements:
* Improve error messages when connecting and copying to devices. * Improve error messages when connecting and copying to devices.
* Allow enter to be used with multiselection to add songs to playlist (#1360) * Allow enter to be used with multiselection to add songs to playlist (#1360)
* Add song progress to taskbar using D-Bus. * Add song progress to taskbar using D-Bus.
* Use API to receive Radio Paradise channels. * Use API to receive Radio Paradise channels.
* Added letras lyrics provider.
* Added Open Tidal API (openapi.tidal.com) cover provider.
* Added button for fetching lyrics to tag editor (#1391). * Added button for fetching lyrics to tag editor (#1391).
* Added option not to skip "A", "An" and "The” when sorting artist names in collection (#1393). * Added option not to skip "A", "An" and "The” when sorting artist names in collection (#1393).
* Improved album and title disc, remastered, etc matching and stripping (#1387). * Improved album and title disc, remastered, etc matching and stripping (#1387).
@@ -54,6 +65,9 @@ Version 1.1.0-rc1:
* (Windows MSVC) Add back WASAPI2. * (Windows MSVC) Add back WASAPI2.
New features: New features:
* Letras lyrics provider.
* Open Tidal API (openapi.tidal.com) cover provider.
* Turbine analyzer.
* WaveRubber analyzer. * WaveRubber analyzer.
* Spotify streaming support. * Spotify streaming support.

View File

@@ -64,7 +64,7 @@ Funding developers is a way to contribute to open source projects you appreciate
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows. It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
**macOS releases are currently limited to sponsors. This is because Strawberry mainly has one contributor/developer and supporting macOS requires Apple hardware, building libraries Strawberry depends and a Apple developer account for signing releases. If you are sponsoring strawberry through Patreon, releases are available directly on Patreon, if you are sponsoring through GitHub, Ko-fi or Paypal, please e-mail support@strawberrymusicplayer.org for access to downloads.** **Access to macOS and Windows releases are currently restricted to sponsors, a 5 USD monthly sponsorship is required. You can sponsor strawberry through <a href="https://www.patreon.com/jonaskvinge">Patreon</a> for direct access to new releases. If you are sponsoring through GitHub, Ko-fi or PayPal, please e-mail support AT strawberrymusicplayer.org for access to downloads.**
### :heavy_exclamation_mark: Requirements ### :heavy_exclamation_mark: Requirements

View File

@@ -44,7 +44,7 @@ macro(add_pot outfiles header pot)
add_custom_command( add_custom_command(
OUTPUT ${pot} OUTPUT ${pot}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND xgettext ${XGETTEXT_OPTIONS} -s -C --omit-header --output="${CMAKE_CURRENT_BINARY_DIR}/pot.temp" ${add_pot_sources} COMMAND ${GETTEXT_XGETTEXT_EXECUTABLE} ${XGETTEXT_OPTIONS} -s -C --omit-header --output="${CMAKE_CURRENT_BINARY_DIR}/pot.temp" ${add_pot_sources}
COMMAND cat ${header} ${CMAKE_CURRENT_BINARY_DIR}/pot.temp > ${pot} COMMAND cat ${header} ${CMAKE_CURRENT_BINARY_DIR}/pot.temp > ${pot}
DEPENDS ${add_pot_sources} ${header} DEPENDS ${add_pot_sources} ${header}
) )

View File

@@ -1,7 +1,7 @@
set(STRAWBERRY_VERSION_MAJOR 1) set(STRAWBERRY_VERSION_MAJOR 1)
set(STRAWBERRY_VERSION_MINOR 1) set(STRAWBERRY_VERSION_MINOR 1)
set(STRAWBERRY_VERSION_PATCH 0) set(STRAWBERRY_VERSION_PATCH 1)
set(STRAWBERRY_VERSION_PRERELEASE rc1) #set(STRAWBERRY_VERSION_PRERELEASE rc1)
set(INCLUDE_GIT_REVISION OFF) set(INCLUDE_GIT_REVISION OFF)

View File

@@ -94,9 +94,4 @@ CREATE INDEX idx_device_%deviceid_songs_album ON device_%deviceid_songs (album);
CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist); CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist);
CREATE VIRTUAL TABLE device_%deviceid_fts USING fts5(
ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
UPDATE devices SET schema_version=5 WHERE ROWID=%deviceid; UPDATE devices SET schema_version=5 WHERE ROWID=%deviceid;

View File

@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
DELETE FROM schema_version; DELETE FROM schema_version;
INSERT INTO schema_version (version) VALUES (19); INSERT INTO schema_version (version) VALUES (20);
CREATE TABLE IF NOT EXISTS directories ( CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL, path TEXT NOT NULL,

View File

@@ -50,6 +50,8 @@
</screenshots> </screenshots>
<update_contact>eclipseo@fedoraproject.org</update_contact> <update_contact>eclipseo@fedoraproject.org</update_contact>
<releases> <releases>
<release version="1.1.1" date="2024-07-22"/>
<release version="1.1.0" date="2024-07-14"/>
<release version="1.0.23" date="2024-01-11"/> <release version="1.0.23" date="2024-01-11"/>
<release version="1.0.22" date="2023-12-09"/> <release version="1.0.22" date="2023-12-09"/>
<release version="1.0.21" date="2023-10-21"/> <release version="1.0.21" date="2023-10-21"/>

View File

@@ -23,8 +23,6 @@
#include <string> #include <string>
#include <boost/algorithm/string/trim.hpp>
#include <QByteArray> #include <QByteArray>
#include <QString> #include <QString>
@@ -64,7 +62,7 @@ class TagReaderTagLib : public TagReaderBase {
} }
static inline std::string TagLibStringToStdString(const TagLib::String &s) { static inline std::string TagLibStringToStdString(const TagLib::String &s) {
return std::string(s.toCString(), s.length()); return std::string(s.toCString(true), s.length());
} }
static inline TagLib::String QStringToTagLibString(const QString &s) { static inline TagLib::String QStringToTagLibString(const QString &s) {
@@ -77,9 +75,9 @@ class TagReaderTagLib : public TagReaderBase {
static inline void AssignTagLibStringToStdString(const TagLib::String &tstr, std::string *output) { static inline void AssignTagLibStringToStdString(const TagLib::String &tstr, std::string *output) {
std::string stdstr = TagLibStringToStdString(tstr); const QString qstr = TagLibStringToQString(tstr).trimmed();
boost::trim(stdstr); const QByteArray data = qstr.toUtf8();
output->assign(stdstr); output->assign(data.constData(), data.size());
} }

View File

@@ -58,9 +58,11 @@ set(SOURCES
utilities/filemanagerutils.cpp utilities/filemanagerutils.cpp
utilities/coverutils.cpp utilities/coverutils.cpp
utilities/screenutils.cpp utilities/screenutils.cpp
utilities/searchparserutils.cpp
utilities/textencodingutils.cpp utilities/textencodingutils.cpp
filterparser/filterparser.cpp
filterparser/filtertree.cpp
engine/enginebase.cpp engine/enginebase.cpp
engine/enginedevice.cpp engine/enginedevice.cpp
engine/devicefinders.cpp engine/devicefinders.cpp
@@ -72,9 +74,10 @@ set(SOURCES
analyzer/analyzercontainer.cpp analyzer/analyzercontainer.cpp
analyzer/blockanalyzer.cpp analyzer/blockanalyzer.cpp
analyzer/boomanalyzer.cpp analyzer/boomanalyzer.cpp
analyzer/turbineanalyzer.cpp
analyzer/sonogramanalyzer.cpp
analyzer/waverubberanalyzer.cpp
analyzer/rainbowanalyzer.cpp analyzer/rainbowanalyzer.cpp
analyzer/sonogram.cpp
analyzer/waverubber.cpp
equalizer/equalizer.cpp equalizer/equalizer.cpp
equalizer/equalizerslider.cpp equalizer/equalizerslider.cpp
@@ -95,7 +98,6 @@ set(SOURCES
collection/collectionfilter.cpp collection/collectionfilter.cpp
collection/collectionplaylistitem.cpp collection/collectionplaylistitem.cpp
collection/collectionquery.cpp collection/collectionquery.cpp
collection/collectionqueryoptions.cpp
collection/savedgroupingmanager.cpp collection/savedgroupingmanager.cpp
collection/groupbydialog.cpp collection/groupbydialog.cpp
collection/collectiontask.cpp collection/collectiontask.cpp
@@ -106,7 +108,6 @@ set(SOURCES
playlist/playlistcontainer.cpp playlist/playlistcontainer.cpp
playlist/playlistdelegates.cpp playlist/playlistdelegates.cpp
playlist/playlistfilter.cpp playlist/playlistfilter.cpp
playlist/playlistfilterparser.cpp
playlist/playlistheader.cpp playlist/playlistheader.cpp
playlist/playlistitem.cpp playlist/playlistitem.cpp
playlist/playlistlistcontainer.cpp playlist/playlistlistcontainer.cpp
@@ -331,9 +332,10 @@ set(HEADERS
analyzer/analyzercontainer.h analyzer/analyzercontainer.h
analyzer/blockanalyzer.h analyzer/blockanalyzer.h
analyzer/boomanalyzer.h analyzer/boomanalyzer.h
analyzer/turbineanalyzer.h
analyzer/sonogramanalyzer.h
analyzer/waverubberanalyzer.h
analyzer/rainbowanalyzer.h analyzer/rainbowanalyzer.h
analyzer/sonogram.h
analyzer/waverubber.h
equalizer/equalizer.h equalizer/equalizer.h
equalizer/equalizerslider.h equalizer/equalizerslider.h

View File

@@ -39,9 +39,10 @@
#include "analyzerbase.h" #include "analyzerbase.h"
#include "blockanalyzer.h" #include "blockanalyzer.h"
#include "boomanalyzer.h" #include "boomanalyzer.h"
#include "turbineanalyzer.h"
#include "sonogramanalyzer.h"
#include "waverubberanalyzer.h"
#include "rainbowanalyzer.h" #include "rainbowanalyzer.h"
#include "sonogram.h"
#include "waverubber.h"
#include "core/logging.h" #include "core/logging.h"
#include "core/shared_ptr.h" #include "core/shared_ptr.h"
@@ -88,10 +89,11 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
AddAnalyzerType<BlockAnalyzer>(); AddAnalyzerType<BlockAnalyzer>();
AddAnalyzerType<BoomAnalyzer>(); AddAnalyzerType<BoomAnalyzer>();
AddAnalyzerType<NyanCatAnalyzer>(); AddAnalyzerType<TurbineAnalyzer>();
AddAnalyzerType<SonogramAnalyzer>();
AddAnalyzerType<WaveRubberAnalyzer>();
AddAnalyzerType<RainbowDashAnalyzer>(); AddAnalyzerType<RainbowDashAnalyzer>();
AddAnalyzerType<Sonogram>(); AddAnalyzerType<NyanCatAnalyzer>();
AddAnalyzerType<WaveRubber>();
disable_action_ = context_menu_->addAction(tr("No analyzer"), this, &AnalyzerContainer::DisableAnalyzer); disable_action_ = context_menu_->addAction(tr("No analyzer"), this, &AnalyzerContainer::DisableAnalyzer);
disable_action_->setCheckable(true); disable_action_->setCheckable(true);

View File

@@ -136,7 +136,7 @@ void BlockAnalyzer::transform(Scope &s) {
} }
void BlockAnalyzer::analyze(QPainter &p, const Scope &s, bool new_frame) { void BlockAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame) {
// y = 2 3 2 1 0 2 // y = 2 3 2 1 0 2
// . . . . # . // . . . . # .

View File

@@ -54,7 +54,7 @@ class BlockAnalyzer : public AnalyzerBase {
protected: protected:
void transform(Scope&) override; void transform(Scope&) override;
void analyze(QPainter &p, const Scope&, bool new_frame) override; void analyze(QPainter &p, const Scope &s, const bool new_frame) override;
void resizeEvent(QResizeEvent*) override; void resizeEvent(QResizeEvent*) override;
virtual void paletteChange(const QPalette&); virtual void paletteChange(const QPalette&);
void framerateChanged() override; void framerateChanged() override;

View File

@@ -45,7 +45,7 @@ class BoomAnalyzer : public AnalyzerBase {
static const char *kName; static const char *kName;
void transform(Scope &s) override; void transform(Scope &s) override;
void analyze(QPainter &p, const Scope&, const bool new_frame) override; void analyze(QPainter &p, const Scope &scope, const bool new_frame) override;
public slots: public slots:
void changeK_barHeight(int); void changeK_barHeight(int);

View File

@@ -41,18 +41,21 @@
#include "fht.h" #include "fht.h"
#include "analyzerbase.h" #include "analyzerbase.h"
const char *NyanCatAnalyzer::kName = "Nyanalyzer Cat";
const char *RainbowDashAnalyzer::kName = "Rainbow Dash";
RainbowAnalyzer::RainbowType RainbowAnalyzer::rainbowtype;
const int RainbowAnalyzer::kHeight[] = { 21, 33 }; const int RainbowAnalyzer::kHeight[] = { 21, 33 };
const int RainbowAnalyzer::kWidth[] = { 34, 53 }; const int RainbowAnalyzer::kWidth[] = { 34, 53 };
const int RainbowAnalyzer::kFrameCount[] = { 6, 16 }; const int RainbowAnalyzer::kFrameCount[] = { 6, 16 };
const int RainbowAnalyzer::kRainbowHeight[] = { 21, 16 };
const int RainbowAnalyzer::kRainbowOverlap[] = { 13, 15 };
const int RainbowAnalyzer::kSleepingHeight[] = { 24, 33 }; const int RainbowAnalyzer::kSleepingHeight[] = { 24, 33 };
const char *NyanCatAnalyzer::kName = "Nyanalyzer Cat"; namespace {
const char *RainbowDashAnalyzer::kName = "Rainbow Dash"; constexpr int kFrameIntervalMs = 150;
const float RainbowAnalyzer::kPixelScale = 0.02F; constexpr int kRainbowHeight[] = { 21, 16 };
constexpr int kRainbowOverlap[] = { 13, 15 };
RainbowAnalyzer::RainbowType RainbowAnalyzer::rainbowtype; constexpr float kPixelScale = 0.02F;
} // namespace
RainbowAnalyzer::RainbowAnalyzer(const RainbowType rbtype, QWidget *parent) RainbowAnalyzer::RainbowAnalyzer(const RainbowType rbtype, QWidget *parent)
: AnalyzerBase(parent, 9), : AnalyzerBase(parent, 9),
@@ -106,7 +109,7 @@ void RainbowAnalyzer::resizeEvent(QResizeEvent *e) {
} }
void RainbowAnalyzer::analyze(QPainter &p, const Scope &s, bool new_frame) { void RainbowAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame) {
// Discard the second half of the transform // Discard the second half of the transform
const int scope_size = static_cast<int>(s.size() / 2); const int scope_size = static_cast<int>(s.size() / 2);

View File

@@ -49,44 +49,37 @@ class RainbowAnalyzer : public AnalyzerBase {
Dash = 1 Dash = 1
}; };
RainbowAnalyzer(const RainbowType rbtype, QWidget *parent); explicit RainbowAnalyzer(const RainbowType rbtype, QWidget *parent);
protected: protected:
void transform(Scope&) override; void transform(Scope &s) override;
void analyze(QPainter &p, const Scope&, bool new_frame) override; void analyze(QPainter &p, const Scope &s, const bool new_frame) override;
void timerEvent(QTimerEvent *e) override; void timerEvent(QTimerEvent *e) override;
void resizeEvent(QResizeEvent *e) override; void resizeEvent(QResizeEvent *e) override;
private: private:
static const int kRainbowBands = 6;
static const int kHistorySize = 128;
static RainbowType rainbowtype;
static const int kHeight[]; static const int kHeight[];
static const int kWidth[]; static const int kWidth[];
static const int kFrameCount[]; static const int kFrameCount[];
static const int kRainbowHeight[];
static const int kRainbowOverlap[];
static const int kSleepingHeight[]; static const int kSleepingHeight[];
static const int kHistorySize = 128; inline QRect SourceRect(const RainbowType _rainbowtype) const {
static const int kRainbowBands = 6;
static const float kPixelScale;
static const int kFrameIntervalMs = 150;
static RainbowType rainbowtype;
inline QRect SourceRect(RainbowType _rainbowtype) const {
return QRect(0, kHeight[_rainbowtype] * frame_, kWidth[_rainbowtype], kHeight[_rainbowtype]); return QRect(0, kHeight[_rainbowtype] * frame_, kWidth[_rainbowtype], kHeight[_rainbowtype]);
} }
inline QRect SleepingSourceRect(RainbowType _rainbowtype) const { inline QRect SleepingSourceRect(const RainbowType _rainbowtype) const {
return QRect(0, kHeight[_rainbowtype] * kFrameCount[_rainbowtype], kWidth[_rainbowtype], kSleepingHeight[_rainbowtype]); return QRect(0, kHeight[_rainbowtype] * kFrameCount[_rainbowtype], kWidth[_rainbowtype], kSleepingHeight[_rainbowtype]);
} }
inline QRect DestRect(RainbowType _rainbowtype) const { inline QRect DestRect(const RainbowType _rainbowtype) const {
return QRect(width() - kWidth[_rainbowtype], (height() - kHeight[_rainbowtype]) / 2, kWidth[_rainbowtype], kHeight[_rainbowtype]); return QRect(width() - kWidth[_rainbowtype], (height() - kHeight[_rainbowtype]) / 2, kWidth[_rainbowtype], kHeight[_rainbowtype]);
} }
inline QRect SleepingDestRect(RainbowType _rainbowtype) const { inline QRect SleepingDestRect(const RainbowType _rainbowtype) const {
return QRect(width() - kWidth[_rainbowtype], (height() - kSleepingHeight[_rainbowtype]) / 2, kWidth[_rainbowtype], kSleepingHeight[_rainbowtype]); return QRect(width() - kWidth[_rainbowtype], (height() - kSleepingHeight[_rainbowtype]) / 2, kWidth[_rainbowtype], kSleepingHeight[_rainbowtype]);
} }

View File

@@ -26,14 +26,14 @@
#include "engine/enginebase.h" #include "engine/enginebase.h"
#include "sonogram.h" #include "sonogramanalyzer.h"
const char *Sonogram::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "Sonogram"); const char *SonogramAnalyzer::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "Sonogram");
Sonogram::Sonogram(QWidget *parent) SonogramAnalyzer::SonogramAnalyzer(QWidget *parent)
: AnalyzerBase(parent, 9) {} : AnalyzerBase(parent, 9) {}
void Sonogram::resizeEvent(QResizeEvent *e) { void SonogramAnalyzer::resizeEvent(QResizeEvent *e) {
Q_UNUSED(e) Q_UNUSED(e)
@@ -42,7 +42,7 @@ void Sonogram::resizeEvent(QResizeEvent *e) {
} }
void Sonogram::analyze(QPainter &p, const Scope &s, bool new_frame) { void SonogramAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame) {
if (!new_frame || engine_->state() == EngineBase::State::Paused) { if (!new_frame || engine_->state() == EngineBase::State::Paused) {
p.drawPixmap(0, 0, canvas_); p.drawPixmap(0, 0, canvas_);
@@ -81,7 +81,7 @@ void Sonogram::analyze(QPainter &p, const Scope &s, bool new_frame) {
} }
void Sonogram::transform(Scope &scope) { void SonogramAnalyzer::transform(Scope &scope) {
fht_->power2(scope.data()); fht_->power2(scope.data());
fht_->scale(scope.data(), 1.0 / 256); fht_->scale(scope.data(), 1.0 / 256);
@@ -89,6 +89,6 @@ void Sonogram::transform(Scope &scope) {
} }
void Sonogram::demo(QPainter &p) { void SonogramAnalyzer::demo(QPainter &p) {
analyze(p, Scope(fht_->size(), 0), new_frame_); analyze(p, Scope(fht_->size(), 0), new_frame_);
} }

View File

@@ -21,24 +21,25 @@
along with Strawberry. If not, see <http://www.gnu.org/licenses/>. along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef SONOGRAM_H #ifndef SONOGRAMANALYZER_H
#define SONOGRAM_H #define SONOGRAMANALYZER_H
#include <QPixmap> #include <QPixmap>
#include <QPainter> #include <QPainter>
#include "analyzerbase.h" #include "analyzerbase.h"
class Sonogram : public AnalyzerBase { class SonogramAnalyzer : public AnalyzerBase {
Q_OBJECT Q_OBJECT
public: public:
Q_INVOKABLE explicit Sonogram(QWidget *parent); Q_INVOKABLE explicit SonogramAnalyzer(QWidget *parent);
static const char *kName; static const char *kName;
protected: protected:
void resizeEvent(QResizeEvent *e) override; void resizeEvent(QResizeEvent *e) override;
void analyze(QPainter &p, const Scope &s, bool new_frame) override; void analyze(QPainter &p, const Scope &s, const bool new_frame) override;
void transform(Scope &scope) override; void transform(Scope &scope) override;
void demo(QPainter &p) override; void demo(QPainter &p) override;
@@ -46,4 +47,4 @@ class Sonogram : public AnalyzerBase {
QPixmap canvas_; QPixmap canvas_;
}; };
#endif // SONOGRAM_H #endif // SONOGRAMANALYZER_H

View File

@@ -0,0 +1,100 @@
/*
Strawberry Music Player
This file was part of Clementine.
Copyright 2003, Stanislav Karchebny <berkus@users.sf.net>
Copyright 2003, Max Howell <max.howell@methylblue.com>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <cmath>
#include <algorithm>
#include <QPainter>
#include "turbineanalyzer.h"
#include "engine/enginebase.h"
const char *TurbineAnalyzer::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "Turbine");
TurbineAnalyzer::TurbineAnalyzer(QWidget *parent) : BoomAnalyzer(parent) {}
void TurbineAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame) {
if (!new_frame || engine_->state() == EngineBase::State::Paused) {
p.drawPixmap(0, 0, canvas_);
return;
}
const uint hd2 = height() / 2;
const uint kMaxHeight = hd2 - 1;
QPainter canvas_painter(&canvas_);
canvas_.fill(palette().color(QPalette::Window));
AnalyzerBase::interpolate(scope, scope_);
for (uint i = 0, x = 0, y = 0; i < static_cast<uint>(bands_); ++i, x += kColumnWidth + 1) {
float h = static_cast<float>(std::min(log10(scope_[i] * 256.0) * F_ * 0.5, kMaxHeight * 1.0));
if (h > bar_height_[i]) {
bar_height_[i] = h;
if (h > peak_height_[i]) {
peak_height_[i] = h;
peak_speed_[i] = 0.01;
}
else {
goto peak_handling;
}
}
else {
if (bar_height_[i] > 0.0) {
bar_height_[i] -= K_barHeight_; // 1.4
if (bar_height_[i] < 0.0) bar_height_[i] = 0.0;
}
peak_handling:
if (peak_height_[i] > 0.0) {
peak_height_[i] -= peak_speed_[i];
peak_speed_[i] *= F_peakSpeed_; // 1.12
peak_height_[i] = std::max(0.0, std::max(bar_height_[i], peak_height_[i]));
}
}
y = hd2 - static_cast<uint>(bar_height_[i]);
canvas_painter.drawPixmap(static_cast<int>(x + 1), static_cast<int>(y), barPixmap_, 0, static_cast<int>(y), -1, -1);
canvas_painter.drawPixmap(static_cast<int>(x + 1), static_cast<int>(hd2), barPixmap_, 0, static_cast<int>(bar_height_[i]), -1, -1);
canvas_painter.setPen(fg_);
if (bar_height_[i] > 0) {
canvas_painter.drawRect(static_cast<int>(x), static_cast<int>(y), kColumnWidth - 1, static_cast<int>(bar_height_[i]) * 2 - 1);
}
const uint x2 = x + kColumnWidth - 1;
canvas_painter.setPen(palette().color(QPalette::Midlight));
y = hd2 - static_cast<uint>(peak_height_[i]);
canvas_painter.drawLine(static_cast<int>(x), static_cast<int>(y), static_cast<int>(x2), static_cast<int>(y));
y = hd2 + static_cast<uint>(peak_height_[i]);
canvas_painter.drawLine(static_cast<int>(x), static_cast<int>(y), static_cast<int>(x2), static_cast<int>(y));
}
p.drawPixmap(0, 0, canvas_);
}

View File

@@ -0,0 +1,41 @@
/*
Strawberry Music Player
This file was part of Clementine.
Copyright 2003, Stanislav Karchebny <berkus@users.sf.net>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TURBINEANALYZER_H
#define TURBINEANALYZER_H
#include "boomanalyzer.h"
class QPainter;
class TurbineAnalyzer : public BoomAnalyzer {
Q_OBJECT
public:
Q_INVOKABLE explicit TurbineAnalyzer(QWidget *parent);
void analyze(QPainter &p, const Scope &scope, const bool new_frame);
static const char *kName;
};
#endif // TURBINEANALYZER_H

View File

@@ -19,14 +19,14 @@
#include <QPainter> #include <QPainter>
#include <QResizeEvent> #include <QResizeEvent>
#include "engine/enginebase.h" #include "engine/enginebase.h"
#include "waverubber.h" #include "waverubberanalyzer.h"
const char *WaveRubber::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "WaveRubber"); const char *WaveRubberAnalyzer::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "WaveRubber");
WaveRubber::WaveRubber(QWidget *parent) WaveRubberAnalyzer::WaveRubberAnalyzer(QWidget *parent)
: AnalyzerBase(parent, 9) {} : AnalyzerBase(parent, 9) {}
void WaveRubber::resizeEvent(QResizeEvent *e) { void WaveRubberAnalyzer::resizeEvent(QResizeEvent *e) {
Q_UNUSED(e) Q_UNUSED(e)
@@ -35,7 +35,7 @@ void WaveRubber::resizeEvent(QResizeEvent *e) {
} }
void WaveRubber::analyze(QPainter &p, const Scope &s, const bool new_frame) { void WaveRubberAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame) {
if (!new_frame || engine_->state() == EngineBase::State::Paused) { if (!new_frame || engine_->state() == EngineBase::State::Paused) {
p.drawPixmap(0, 0, canvas_); p.drawPixmap(0, 0, canvas_);
@@ -82,11 +82,11 @@ void WaveRubber::analyze(QPainter &p, const Scope &s, const bool new_frame) {
} }
void WaveRubber::transform(Scope &s) { void WaveRubberAnalyzer::transform(Scope &s) {
// No need transformation for waveform analyzer // No need transformation for waveform analyzer
Q_UNUSED(s); Q_UNUSED(s);
} }
void WaveRubber::demo(QPainter &p) { void WaveRubberAnalyzer::demo(QPainter &p) {
analyze(p, Scope(fht_->size(), 0), new_frame_); analyze(p, Scope(fht_->size(), 0), new_frame_);
} }

View File

@@ -22,11 +22,11 @@
#include "analyzerbase.h" #include "analyzerbase.h"
class WaveRubber : public AnalyzerBase { class WaveRubberAnalyzer : public AnalyzerBase {
Q_OBJECT Q_OBJECT
public: public:
Q_INVOKABLE explicit WaveRubber(QWidget *parent); Q_INVOKABLE explicit WaveRubberAnalyzer(QWidget *parent);
static const char *kName; static const char *kName;

View File

@@ -19,29 +19,31 @@
#include "config.h" #include "config.h"
#include <QSortFilterProxyModel> #include <algorithm>
#include <QVariant> #include <functional>
#include <QSet>
#include <QList>
#include <QString> #include <QString>
#include <QStringList> #include <QUrl>
#include "core/logging.h"
#include "utilities/timeconstants.h"
#include "utilities/searchparserutils.h"
#include "core/song.h"
#include "filterparser/filterparser.h"
#include "filterparser/filtertree.h"
#include "playlist/songmimedata.h"
#include "playlist/playlistmanager.h"
#include "collectionbackend.h"
#include "collectionfilter.h" #include "collectionfilter.h"
#include "collectionmodel.h" #include "collectionmodel.h"
#include "collectionitem.h" #include "collectionitem.h"
const QStringList CollectionFilter::Operators = QStringList() << QStringLiteral(":") CollectionFilter::CollectionFilter(QObject *parent) : QSortFilterProxyModel(parent), query_hash_(0) {
<< QStringLiteral("=")
<< QStringLiteral("==")
<< QStringLiteral("<>")
<< QStringLiteral("<")
<< QStringLiteral("<=")
<< QStringLiteral(">")
<< QStringLiteral(">=");
CollectionFilter::CollectionFilter(QObject *parent) : QSortFilterProxyModel(parent) {} setSortLocaleAware(true);
setDynamicSortFilter(true);
setRecursiveFilteringEnabled(true);
}
bool CollectionFilter::filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const { bool CollectionFilter::filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const {
@@ -52,284 +54,83 @@ bool CollectionFilter::filterAcceptsRow(const int source_row, const QModelIndex
CollectionItem *item = model->IndexToItem(idx); CollectionItem *item = model->IndexToItem(idx);
if (!item) return false; if (!item) return false;
if (item->type == CollectionItem::Type::LoadingIndicator) return true; if (filter_string_.isEmpty()) return true;
if (item->type != CollectionItem::Type::Song) {
return item->type == CollectionItem::Type::LoadingIndicator;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QString filter_text = filterRegularExpression().pattern().remove(QLatin1Char('\\')); const size_t hash = qHash(filter_string_);
#else #else
QString filter_text = filterRegExp().pattern(); const uint hash = qHash(filter_string_);
#endif #endif
if (hash != query_hash_) {
FilterParser p(filter_string_);
filter_tree_.reset(p.parse());
query_hash_ = hash;
}
if (filter_text.isEmpty()) return true; return item->metadata.is_valid() && filter_tree_->accept(item->metadata);
filter_text = filter_text.replace(QRegularExpression(QStringLiteral("\\s*:\\s*")), QStringLiteral(":")) }
.replace(QRegularExpression(QStringLiteral("\\s*=\\s*")), QStringLiteral("="))
.replace(QRegularExpression(QStringLiteral("\\s*==\\s*")), QStringLiteral("=="))
.replace(QRegularExpression(QStringLiteral("\\s*<>\\s*")), QStringLiteral("<>"))
.replace(QRegularExpression(QStringLiteral("\\s*<\\s*")), QStringLiteral("<"))
.replace(QRegularExpression(QStringLiteral("\\s*>\\s*")), QStringLiteral(">"))
.replace(QRegularExpression(QStringLiteral("\\s*<=\\s*")), QStringLiteral("<="))
.replace(QRegularExpression(QStringLiteral("\\s*>=\\s*")), QStringLiteral(">="));
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) void CollectionFilter::SetFilterString(const QString &filter_string) {
const QStringList tokens = filter_text.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
#else
const QStringList tokens = filter_text.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts);
#endif
filter_text.clear(); filter_string_ = filter_string;
setFilterFixedString(filter_string);
FilterList filters; }
static QRegularExpression operator_regex(QStringLiteral("(=|<[>=]?|>=?|!=)"));
for (int i = 0; i < tokens.count(); ++i) { QMimeData *CollectionFilter::mimeData(const QModelIndexList &indexes) const {
const QString &token = tokens[i];
if (token.contains(QLatin1Char(':'))) { if (indexes.isEmpty()) return nullptr;
QString field = token.section(QLatin1Char(':'), 0, 0).remove(QLatin1Char(':')).trimmed();
QString value = token.section(QLatin1Char(':'), 1, -1).remove(QLatin1Char(':')).trimmed(); CollectionModel *collection_model = qobject_cast<CollectionModel*>(sourceModel());
if (field.isEmpty() || value.isEmpty()) continue; SongMimeData *data = new SongMimeData;
if (Song::kTextSearchColumns.contains(field, Qt::CaseInsensitive) && value.count(QLatin1Char('"')) <= 2) { data->backend = collection_model->backend();
bool quotation_mark_start = false;
bool quotation_mark_end = false; QSet<int> song_ids;
if (value.left(1) == QLatin1Char('"')) { QList<QUrl> urls;
value.remove(0, 1); for (const QModelIndex &idx : indexes) {
quotation_mark_start = true; const QModelIndex source_index = mapToSource(idx);
if (value.length() >= 1 && value.count(QLatin1Char('"')) == 1) { CollectionItem *item = collection_model->IndexToItem(source_index);
value = value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed(); GetChildSongs(item, song_ids, urls, data->songs);
quotation_mark_end = true; }
}
} data->setUrls(urls);
for (int y = i + 1; y < tokens.count() && !quotation_mark_end; ++y) { data->name_for_new_playlist_ = PlaylistManager::GetNameForNewPlaylist(data->songs);
QString next_value = tokens[y];
if (!quotation_mark_start && ContainsOperators(next_value)) { return data;
break;
} }
if (quotation_mark_start && next_value.contains(QLatin1Char('"'))) {
next_value = next_value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed(); void CollectionFilter::GetChildSongs(CollectionItem *item, QSet<int> &song_ids, QList<QUrl> &urls, SongList &songs) const {
quotation_mark_end = true;
} CollectionModel *collection_model = qobject_cast<CollectionModel*>(sourceModel());
value.append(QLatin1Char(' ') + next_value);
i = y; switch (item->type) {
} case CollectionItem::Type::Container:{
if (!field.isEmpty() && !value.isEmpty()) { QList<CollectionItem*> children = item->children;
filters.insert(field, Filter(field, value)); std::sort(children.begin(), children.end(), std::bind(&CollectionModel::CompareItems, collection_model, std::placeholders::_1, std::placeholders::_2));
} for (CollectionItem *child : children) {
continue; GetChildSongs(child, song_ids, urls, songs);
} }
break;
} }
else if (token.contains(operator_regex)) { case CollectionItem::Type::Song:{
QRegularExpressionMatch re_match = operator_regex.match(token); const QModelIndex idx = collection_model->ItemToIndex(item);
if (re_match.hasMatch()) { if (filterAcceptsRow(idx.row(), idx.parent())) {
const QString foperator = re_match.captured(0); urls << item->metadata.url();
const QString field = token.section(foperator, 0, 0).remove(foperator).trimmed(); if (!song_ids.contains(item->metadata.id())) {
const QString value = token.section(foperator, 1, -1).remove(foperator).trimmed(); song_ids.insert(item->metadata.id());
if (value.isEmpty()) continue; songs << item->metadata;
if (Song::kNumericalSearchColumns.contains(field, Qt::CaseInsensitive)) {
if (Song::kIntSearchColumns.contains(field, Qt::CaseInsensitive)) {
bool ok = false;
const int value_int = value.toInt(&ok);
if (ok) {
filters.insert(field, Filter(field, value_int, foperator));
continue;
}
}
else if (Song::kUIntSearchColumns.contains(field, Qt::CaseInsensitive)) {
bool ok = false;
const uint value_uint = value.toUInt(&ok);
if (ok) {
filters.insert(field, Filter(field, value_uint, foperator));
continue;
}
}
else if (field.compare(QLatin1String("length"), Qt::CaseInsensitive) == 0) {
filters.insert(field, Filter(field, static_cast<qint64>(Utilities::ParseSearchTime(value)) * kNsecPerSec, foperator));
continue;
}
else if (field.compare(QLatin1String("rating"), Qt::CaseInsensitive) == 0) {
filters.insert(field, Filter(field, Utilities::ParseSearchRating(value), foperator));
}
} }
} }
break;
} }
if (!filter_text.isEmpty()) filter_text.append(QLatin1Char(' ')); default:
filter_text += token; break;
} }
if (filter_text.isEmpty() && filters.isEmpty()) return true;
return ItemMatchesFilters(item, filters, filter_text);
}
bool CollectionFilter::ItemMatchesFilters(CollectionItem *item, const FilterList &filters, const QString &filter_text) {
if (item->type == CollectionItem::Type::Song &&
item->metadata.is_valid() &&
ItemMetadataMatchesFilters(item->metadata, filters, filter_text)) {
return true;
}
for (CollectionItem *child : std::as_const(item->children)) {
if (ItemMatchesFilters(child, filters, filter_text)) return true;
}
return false;
}
bool CollectionFilter::ItemMetadataMatchesFilters(const Song &metadata, const FilterList &filters, const QString &filter_text) {
for (FilterList::const_iterator it = filters.begin() ; it != filters.end() ; ++it) {
const QString &field = it.key();
const Filter &filter = it.value();
const QVariant &value = filter.value;
const QString &foperator = filter.foperator;
if (field.isEmpty() || !value.isValid()) {
continue;
}
const QVariant data = DataFromField(field, metadata);
if (
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
value.metaType() != data.metaType()
#else
value.type() != data.type()
#endif
|| !FieldValueMatchesData(value, data, foperator)) {
return false;
}
}
return filter_text.isEmpty() || ItemMetadataMatchesFilterText(metadata, filter_text);
}
bool CollectionFilter::ItemMetadataMatchesFilterText(const Song &metadata, const QString &filter_text) {
return metadata.effective_albumartist().contains(filter_text, Qt::CaseInsensitive) ||
metadata.artist().contains(filter_text, Qt::CaseInsensitive) ||
metadata.album().contains(filter_text, Qt::CaseInsensitive) ||
metadata.title().contains(filter_text, Qt::CaseInsensitive) ||
metadata.composer().contains(filter_text, Qt::CaseInsensitive) ||
metadata.performer().contains(filter_text, Qt::CaseInsensitive) ||
metadata.grouping().contains(filter_text, Qt::CaseInsensitive) ||
metadata.genre().contains(filter_text, Qt::CaseInsensitive) ||
metadata.comment().contains(filter_text, Qt::CaseInsensitive);
}
QVariant CollectionFilter::DataFromField(const QString &field, const Song &metadata) {
if (field == QLatin1String("albumartist")) return metadata.effective_albumartist();
if (field == QLatin1String("artist")) return metadata.artist();
if (field == QLatin1String("album")) return metadata.album();
if (field == QLatin1String("title")) return metadata.title();
if (field == QLatin1String("composer")) return metadata.composer();
if (field == QLatin1String("performer")) return metadata.performer();
if (field == QLatin1String("grouping")) return metadata.grouping();
if (field == QLatin1String("genre")) return metadata.genre();
if (field == QLatin1String("comment")) return metadata.comment();
if (field == QLatin1String("track")) return metadata.track();
if (field == QLatin1String("year")) return metadata.year();
if (field == QLatin1String("length")) return metadata.length_nanosec();
if (field == QLatin1String("samplerate")) return metadata.samplerate();
if (field == QLatin1String("bitdepth")) return metadata.bitdepth();
if (field == QLatin1String("bitrate")) return metadata.bitrate();
if (field == QLatin1String("rating")) return metadata.rating();
if (field == QLatin1String("playcount")) return metadata.playcount();
if (field == QLatin1String("skipcount")) return metadata.skipcount();
return QVariant();
}
bool CollectionFilter::FieldValueMatchesData(const QVariant &value, const QVariant &data, const QString &foperator) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
switch (value.metaType().id()) {
#else
switch (value.userType()) {
#endif
case QMetaType::QString:{
const QString str_value = value.toString();
const QString str_data = data.toString();
return str_data.contains(str_value, Qt::CaseInsensitive);
}
case QMetaType::Int:{
return FieldIntValueMatchesData(value.toInt(), foperator, data.toInt());
}
case QMetaType::UInt:{
return FieldUIntValueMatchesData(value.toUInt(), foperator, data.toUInt());
}
case QMetaType::LongLong:{
return FieldLongLongValueMatchesData(value.toLongLong(), foperator, data.toLongLong());
}
case QMetaType::Float:{
return FieldFloatValueMatchesData(value.toFloat(), foperator, data.toFloat());
}
default:{
return false;
}
}
return false;
}
template<typename T>
bool CollectionFilter::FieldNumericalValueMatchesData(const T value, const QString &foperator, const T data) {
if (foperator == QLatin1Char('=') || foperator == QLatin1String("==")) {
return data == value;
}
if (foperator == QLatin1String("!=") || foperator == QLatin1String("<>")) {
return data != value;
}
if (foperator == QLatin1Char('<')) {
return data < value;
}
if (foperator == QLatin1Char('>')) {
return data > value;
}
if (foperator == QLatin1String(">=")) {
return data >= value;
}
if (foperator == QLatin1String("<=")) {
return data <= value;
}
return false;
}
bool CollectionFilter::FieldIntValueMatchesData(const int value, const QString &foperator, const int data) {
return FieldNumericalValueMatchesData(value, foperator, data);
}
bool CollectionFilter::FieldUIntValueMatchesData(const uint value, const QString &foperator, const uint data) {
return FieldNumericalValueMatchesData(value, foperator, data);
}
bool CollectionFilter::FieldLongLongValueMatchesData(const qint64 value, const QString &foperator, const qint64 data) {
return FieldNumericalValueMatchesData(value, foperator, data);
}
bool CollectionFilter::FieldFloatValueMatchesData(const float value, const QString &foperator, const float data) {
return FieldNumericalValueMatchesData(value, foperator, data);
}
bool CollectionFilter::ContainsOperators(const QString &token) {
for (const QString &foperator : Operators) {
if (token.contains(foperator, Qt::CaseInsensitive)) return true;
}
return false;
} }

View File

@@ -22,14 +22,14 @@
#include "config.h" #include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <QVariant> #include <QScopedPointer>
#include <QString> #include <QSet>
#include <QStringList> #include <QList>
#include <QUrl>
#include "core/song.h" #include "core/song.h"
#include "filterparser/filtertree.h"
class CollectionItem; class CollectionItem;
@@ -39,31 +39,24 @@ class CollectionFilter : public QSortFilterProxyModel {
public: public:
explicit CollectionFilter(QObject *parent = nullptr); explicit CollectionFilter(QObject *parent = nullptr);
void SetFilterString(const QString &filter_string);
QString filter_string() const { return filter_string_; }
protected: protected:
bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override; bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
private: private:
static const QStringList Operators; void GetChildSongs(CollectionItem *item, QSet<int> &song_ids, QList<QUrl> &urls, SongList &songs) const;
struct Filter {
public: private:
Filter(const QString &_field = QString(), const QVariant &_value = QVariant(), const QString &_foperator = QString()) : field(_field), value(_value), foperator(_foperator) {} mutable QScopedPointer<FilterTree> filter_tree_;
QString field; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QVariant value; mutable size_t query_hash_;
QString foperator; #else
}; mutable uint query_hash_;
using FilterList = QMap<QString, Filter>; #endif
static bool ItemMatchesFilters(CollectionItem *item, const FilterList &filters, const QString &filter_text); QString filter_string_;
static bool ItemMetadataMatchesFilters(const Song &metadata, const FilterList &filters, const QString &filter_text);
static bool ItemMetadataMatchesFilterText(const Song &metadata, const QString &filter_text);
static QVariant DataFromField(const QString &field, const Song &metadata);
static bool FieldValueMatchesData(const QVariant &value, const QVariant &data, const QString &foperator);
template<typename T>
static bool FieldNumericalValueMatchesData(const T value, const QString &foperator, const T data);
static bool FieldIntValueMatchesData(const int value, const QString &foperator, const int data);
static bool FieldUIntValueMatchesData(const uint value, const QString &foperator, const uint data);
static bool FieldLongLongValueMatchesData(const qint64 value, const QString &foperator, const qint64 data);
static bool FieldFloatValueMatchesData(const float value, const QString &foperator, const float data);
static bool ContainsOperators(const QString &token);
}; };
#endif // COLLECTIONFILTER_H #endif // COLLECTIONFILTER_H

View File

@@ -52,6 +52,7 @@
#include "collectionmodel.h" #include "collectionmodel.h"
#include "collectionfilter.h" #include "collectionfilter.h"
#include "collectionquery.h" #include "collectionquery.h"
#include "filterparser/filterparser.h"
#include "savedgroupingmanager.h" #include "savedgroupingmanager.h"
#include "collectionfilterwidget.h" #include "collectionfilterwidget.h"
#include "groupbydialog.h" #include "groupbydialog.h"
@@ -71,47 +72,19 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
group_by_menu_(nullptr), group_by_menu_(nullptr),
collection_menu_(nullptr), collection_menu_(nullptr),
group_by_group_(nullptr), group_by_group_(nullptr),
filter_delay_(new QTimer(this)), timer_filter_delay_(new QTimer(this)),
filter_applies_to_model_(true), filter_applies_to_model_(true),
delay_behaviour_(DelayBehaviour::DelayedOnLargeLibraries) { delay_behaviour_(DelayBehaviour::DelayedOnLargeLibraries) {
ui_->setupUi(this); ui_->setupUi(this);
QString available_fields = Song::kTextSearchColumns.join(QLatin1String(", ")); ui_->search_field->setToolTip(FilterParser::ToolTip());
available_fields += QLatin1String(", ") + Song::kNumericalSearchColumns.join(QLatin1String(", "));
ui_->search_field->setToolTip(
QLatin1String("<html><head/><body><p>") +
tr("Prefix a word with a field name to limit the search to that field, e.g.:") +
QLatin1Char(' ') +
QLatin1String("<span style=\"font-weight:600;\">") +
tr("artist") +
QLatin1String(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
tr("searches the collection for all artists that contain the word %1. ").arg(QLatin1String("Strawbs")) +
QLatin1String("</p><p>") +
tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
.arg(QLatin1String(" =, !=, &lt;, &gt;, &lt;="), QLatin1String("&gt;=")) +
QLatin1String("<span style=\"font-weight:600;\">") +
tr("rating") +
QLatin1String("</span>") +
QLatin1String(":>=") +
QLatin1String("<span style=\"font-weight:italic;\">4</span>") +
QLatin1String("</p><p><span style=\"font-weight:600;\">") +
tr("Available fields") +
QLatin1String(": ") +
QLatin1String("</span>") +
QLatin1String("<span style=\"font-style:italic;\">") +
available_fields +
QLatin1String("</span>.") +
QLatin1String("</p></body></html>")
);
QObject::connect(ui_->search_field, &QSearchField::returnPressed, this, &CollectionFilterWidget::ReturnPressed); QObject::connect(ui_->search_field, &QSearchField::returnPressed, this, &CollectionFilterWidget::ReturnPressed);
QObject::connect(filter_delay_, &QTimer::timeout, this, &CollectionFilterWidget::FilterDelayTimeout); QObject::connect(timer_filter_delay_, &QTimer::timeout, this, &CollectionFilterWidget::FilterDelayTimeout);
filter_delay_->setInterval(kFilterDelay); timer_filter_delay_->setInterval(kFilterDelay);
filter_delay_->setSingleShot(true); timer_filter_delay_->setSingleShot(true);
// Icons // Icons
ui_->options->setIcon(IconLoader::Load(QStringLiteral("configure"))); ui_->options->setIcon(IconLoader::Load(QStringLiteral("configure")));
@@ -529,10 +502,10 @@ void CollectionFilterWidget::FilterTextChanged(const QString &text) {
const bool delay = (delay_behaviour_ == DelayBehaviour::AlwaysDelayed) || (delay_behaviour_ == DelayBehaviour::DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000); const bool delay = (delay_behaviour_ == DelayBehaviour::AlwaysDelayed) || (delay_behaviour_ == DelayBehaviour::DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
if (delay) { if (delay) {
filter_delay_->start(); timer_filter_delay_->start();
} }
else { else {
filter_delay_->stop(); timer_filter_delay_->stop();
FilterDelayTimeout(); FilterDelayTimeout();
} }
@@ -541,7 +514,7 @@ void CollectionFilterWidget::FilterTextChanged(const QString &text) {
void CollectionFilterWidget::FilterDelayTimeout() { void CollectionFilterWidget::FilterDelayTimeout() {
if (filter_applies_to_model_) { if (filter_applies_to_model_) {
filter_->setFilterFixedString(ui_->search_field->text()); filter_->SetFilterString(ui_->search_field->text());
} }
} }

View File

@@ -128,7 +128,7 @@ class CollectionFilterWidget : public QWidget {
QActionGroup *group_by_group_; QActionGroup *group_by_group_;
QHash<QAction*, int> filter_max_ages_; QHash<QAction*, int> filter_max_ages_;
QTimer *filter_delay_; QTimer *timer_filter_delay_;
bool filter_applies_to_model_; bool filter_applies_to_model_;
DelayBehaviour delay_behaviour_; DelayBehaviour delay_behaviour_;

View File

@@ -100,8 +100,6 @@ CollectionModel::CollectionModel(SharedPtr<CollectionBackend> backend, Applicati
filter_->setSourceModel(this); filter_->setSourceModel(this);
filter_->setSortRole(Role_SortText); filter_->setSortRole(Role_SortText);
filter_->setDynamicSortFilter(true);
filter_->setSortLocaleAware(true);
filter_->sort(0); filter_->sort(0);
if (app_) { if (app_) {
@@ -391,12 +389,13 @@ QVariant CollectionModel::data(const CollectionItem *item, const int role) const
Qt::ItemFlags CollectionModel::flags(const QModelIndex &idx) const { Qt::ItemFlags CollectionModel::flags(const QModelIndex &idx) const {
switch (IndexToItem(idx)->type) { switch (IndexToItem(idx)->type) {
case CollectionItem::Type::Song:
case CollectionItem::Type::Container:
return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
case CollectionItem::Type::Divider:
case CollectionItem::Type::Root:
case CollectionItem::Type::LoadingIndicator: case CollectionItem::Type::LoadingIndicator:
case CollectionItem::Type::Divider:
return Qt::ItemIsEnabled | Qt::ItemNeverHasChildren;
case CollectionItem::Type::Container:
case CollectionItem::Type::Song:
return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
case CollectionItem::Type::Root:
default: default:
return Qt::ItemIsEnabled; return Qt::ItemIsEnabled;
} }
@@ -1316,7 +1315,7 @@ QString CollectionModel::ContainerKey(const GroupBy group_by, const Song &song,
} }
// Make sure we distinguish albums by different artists if the parent group by is not including artist. // Make sure we distinguish albums by different artists if the parent group by is not including artist.
if (IsAlbumGroupBy(group_by) && !has_unique_album_identifier && !song.effective_albumartist().isEmpty()) { if (IsAlbumGroupBy(group_by) && !has_unique_album_identifier && !song.is_compilation() && !song.effective_albumartist().isEmpty()) {
key.prepend(QLatin1Char('-')); key.prepend(QLatin1Char('-'));
key.prepend(TextOrUnknown(song.effective_albumartist())); key.prepend(TextOrUnknown(song.effective_albumartist()));
has_unique_album_identifier = true; has_unique_album_identifier = true;

View File

@@ -142,6 +142,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
CollectionFilterOptions filter_options; CollectionFilterOptions filter_options;
}; };
SharedPtr<CollectionBackend> backend() const { return backend_; }
CollectionFilter *filter() const { return filter_; } CollectionFilter *filter() const { return filter_; }
void Init(); void Init();
@@ -200,6 +201,8 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
void ExpandAll(CollectionItem *item = nullptr) const; void ExpandAll(CollectionItem *item = nullptr) const;
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
signals: signals:
void TotalSongCountUpdated(const int count); void TotalSongCountUpdated(const int count);
void TotalArtistCountUpdated(const int count); void TotalArtistCountUpdated(const int count);
@@ -250,7 +253,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
static QUrl AlbumIconPixmapDiskCacheKey(const QString &cache_key); static QUrl AlbumIconPixmapDiskCacheKey(const QString &cache_key);
QVariant AlbumIcon(const QModelIndex &idx); QVariant AlbumIcon(const QModelIndex &idx);
void ClearItemPixmapCache(CollectionItem *item); void ClearItemPixmapCache(CollectionItem *item);
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default); static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);
private slots: private slots:

View File

@@ -35,7 +35,6 @@
#include "collectionquery.h" #include "collectionquery.h"
#include "collectionfilteroptions.h" #include "collectionfilteroptions.h"
#include "utilities/searchparserutils.h"
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options) CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options)
: SqlQuery(db), : SqlQuery(db),

View File

@@ -1,33 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2023, 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 <QVariant>
#include <QString>
#include "collectionqueryoptions.h"
CollectionQueryOptions::CollectionQueryOptions()
: compilation_requirement_(CollectionQueryOptions::CompilationRequirement::None),
query_have_compilations_(false) {}
void CollectionQueryOptions::AddWhere(const QString &column, const QVariant &value, const QString &op) {
where_clauses_ << Where(column, value, op);
}

View File

@@ -1,63 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2023, 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 COLLECTIONQUERYOPTIONS_H
#define COLLECTIONQUERYOPTIONS_H
#include <QList>
#include <QVariant>
#include <QString>
class CollectionQueryOptions {
public:
explicit CollectionQueryOptions();
struct Where {
explicit Where(const QString &_column = QString(), const QVariant &_value = QString(), const QString &_op = QString()) : column(_column), value(_value), op(_op) {}
QString column;
QVariant value;
QString op;
};
enum class CompilationRequirement {
None,
On,
Off
};
QString column_spec() const { return column_spec_; }
CompilationRequirement compilation_requirement() const { return compilation_requirement_; }
bool query_have_compilations() const { return query_have_compilations_; }
void set_column_spec(const QString &column_spec) { column_spec_ = column_spec; }
void set_compilation_requirement(const CompilationRequirement compilation_requirement) { compilation_requirement_ = compilation_requirement; }
void set_query_have_compilations(const bool query_have_compilations) { query_have_compilations_ = query_have_compilations; }
QList<Where> where_clauses() const { return where_clauses_; }
void AddWhere(const QString &column, const QVariant &value, const QString &op = QStringLiteral("="));
private:
QString column_spec_;
CompilationRequirement compilation_requirement_;
bool query_have_compilations_;
QList<Where> where_clauses_;
};
#endif // COLLECTIONQUERYOPTIONS_H

View File

@@ -1071,17 +1071,25 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
#endif #endif
{ {
bool asked_permission = true;
Settings s; Settings s;
s.beginGroup(kSettingsGroup); #ifdef HAVE_QTSPARKLE
constexpr char do_not_show_sponsor_message_key[] = "do_not_show_sponsor_message"; s.beginGroup("QtSparkle");
const bool do_not_show_sponsor_message = s.value(do_not_show_sponsor_message_key, false).toBool(); asked_permission = s.value("asked_permission", false).toBool();
s.endGroup(); s.endGroup();
if (!do_not_show_sponsor_message) { #endif
MessageDialog *sponsor_message = new MessageDialog(this); if (asked_permission) {
sponsor_message->set_settings_group(QLatin1String(kSettingsGroup)); s.beginGroup(kSettingsGroup);
sponsor_message->set_do_not_show_message_again(QLatin1String(do_not_show_sponsor_message_key)); constexpr char do_not_show_sponsor_message_key[] = "do_not_show_sponsor_message";
sponsor_message->setAttribute(Qt::WA_DeleteOnClose); const bool do_not_show_sponsor_message = s.value(do_not_show_sponsor_message_key, false).toBool();
sponsor_message->ShowMessage(tr("Sponsoring Strawberry"), tr("Strawberry is free and open source software. If you like Strawberry, please consider sponsoring the project. For more information about sponsorship see our website %1").arg(QStringLiteral("<a href= \"https://www.strawberrymusicplayer.org/\">www.strawberrymusicplayer.org</a>")), IconLoader::Load(QStringLiteral("dialog-information"))); s.endGroup();
if (!do_not_show_sponsor_message) {
MessageDialog *sponsor_message = new MessageDialog(this);
sponsor_message->set_settings_group(QLatin1String(kSettingsGroup));
sponsor_message->set_do_not_show_message_again(QLatin1String(do_not_show_sponsor_message_key));
sponsor_message->setAttribute(Qt::WA_DeleteOnClose);
sponsor_message->ShowMessage(tr("Sponsoring Strawberry"), tr("Strawberry is free and open source software. If you like Strawberry, please consider sponsoring the project. For more information about sponsorship see our website %1").arg(QStringLiteral("<a href= \"https://www.strawberrymusicplayer.org/\">www.strawberrymusicplayer.org</a>")), IconLoader::Load(QStringLiteral("dialog-information")));
}
} }
} }

View File

@@ -57,8 +57,8 @@ class Ui_EditTagDialog;
#ifdef HAVE_MUSICBRAINZ #ifdef HAVE_MUSICBRAINZ
class TrackSelectionDialog; class TrackSelectionDialog;
class TagFetcher; class TagFetcher;
class LyricsFetcher;
#endif #endif
class LyricsFetcher;
class EditTagDialog : public QDialog { class EditTagDialog : public QDialog {
Q_OBJECT Q_OBJECT

View File

@@ -0,0 +1,487 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
*
* 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 "config.h"
#include <QString>
#include "filterparser.h"
#include "filtertree.h"
#include "filterparsersearchcomparators.h"
FilterParser::FilterParser(const QString &filter_string) : filter_string_(filter_string), iter_{}, end_{} {}
FilterTree *FilterParser::parse() {
iter_ = filter_string_.constBegin();
end_ = filter_string_.constEnd();
return parseOrGroup();
}
void FilterParser::advance() {
while (iter_ != end_ && iter_->isSpace()) {
++iter_;
}
}
FilterTree *FilterParser::parseOrGroup() {
advance();
if (iter_ == end_) return new NopFilter;
OrFilter *group = new OrFilter;
group->add(parseAndGroup());
advance();
while (checkOr()) {
group->add(parseAndGroup());
advance();
}
return group;
}
FilterTree *FilterParser::parseAndGroup() {
advance();
if (iter_ == end_) return new NopFilter;
AndFilter *group = new AndFilter();
do {
group->add(parseSearchExpression());
advance();
if (iter_ != end_ && *iter_ == QLatin1Char(')')) break;
if (checkOr(false)) {
break;
}
checkAnd(); // If there's no 'AND', we'll add the term anyway...
} while (iter_ != end_);
return group;
}
bool FilterParser::checkAnd() {
if (iter_ != end_) {
if (*iter_ == QLatin1Char('A')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && *iter_ == QLatin1Char('N')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && *iter_ == QLatin1Char('D')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QLatin1Char('-') || *iter_ == QLatin1Char('('))) {
advance();
buf_.clear();
return true;
}
}
}
}
}
return false;
}
bool FilterParser::checkOr(const bool step_over) {
if (!buf_.isEmpty()) {
if (buf_ == QLatin1String("OR")) {
if (step_over) {
buf_.clear();
advance();
}
return true;
}
}
else {
if (iter_ != end_) {
if (*iter_ == QLatin1Char('O')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && *iter_ == QLatin1Char('R')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QLatin1Char('-') || *iter_ == QLatin1Char('('))) {
if (step_over) {
buf_.clear();
advance();
}
return true;
}
}
}
}
}
return false;
}
FilterTree *FilterParser::parseSearchExpression() {
advance();
if (iter_ == end_) return new NopFilter;
if (*iter_ == QLatin1Char('(')) {
++iter_;
advance();
FilterTree *tree = parseOrGroup();
advance();
if (iter_ != end_) {
if (*iter_ == QLatin1Char(')')) {
++iter_;
}
}
return tree;
}
else if (*iter_ == QLatin1Char('-')) {
++iter_;
FilterTree *tree = parseSearchExpression();
if (tree->type() != FilterTree::FilterType::Nop) return new NotFilter(tree);
return tree;
}
else {
return parseSearchTerm();
}
}
FilterTree *FilterParser::parseSearchTerm() {
QString column;
QString prefix;
QString value;
bool in_quotes = false;
for (; iter_ != end_; ++iter_) {
if (in_quotes) {
if (*iter_ == QLatin1Char('"')) {
in_quotes = false;
}
else {
buf_ += *iter_;
}
}
else {
if (*iter_ == QLatin1Char('"')) {
in_quotes = true;
}
else if (column.isEmpty() && *iter_ == QLatin1Char(':')) {
column = buf_.toLower();
buf_.clear();
prefix.clear(); // Prefix isn't allowed here - let's ignore it
}
else if (iter_->isSpace() || *iter_ == QLatin1Char('(') || *iter_ == QLatin1Char(')') || *iter_ == QLatin1Char('-')) {
break;
}
else if (buf_.isEmpty()) {
// We don't know whether there is a column part in this search term thus we assume the latter and just try and read a prefix
if (prefix.isEmpty() && (*iter_ == QLatin1Char('>') || *iter_ == QLatin1Char('<') || *iter_ == QLatin1Char('=') || *iter_ == QLatin1Char('!'))) {
prefix += *iter_;
}
else if (prefix != QLatin1Char('=') && *iter_ == QLatin1Char('=')) {
prefix += *iter_;
}
else {
buf_ += *iter_;
}
}
else {
buf_ += *iter_;
}
}
}
value = buf_.toLower();
buf_.clear();
return createSearchTermTreeNode(column, prefix, value);
}
FilterTree *FilterParser::createSearchTermTreeNode(const QString &column, const QString &prefix, const QString &value) const {
if (value.isEmpty() && prefix != QLatin1Char('=')) {
return new NopFilter;
}
FilterParserSearchTermComparator *cmp = nullptr;
if (Song::kTextSearchColumns.contains(column, Qt::CaseInsensitive)) {
if (prefix == QLatin1Char('=') || prefix == QLatin1String("==")) {
cmp = new FilterParserTextEqComparator(value);
}
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
cmp = new FilterParserTextNeComparator(value);
}
else {
cmp = new FilterParserDefaultComparator(value);
}
}
else if (Song::kIntSearchColumns.contains(column, Qt::CaseInsensitive)) {
bool ok = false;
int number = value.toInt(&ok);
if (ok) {
if (prefix == QLatin1Char('=') || prefix == QLatin1String("==")) {
cmp = new FilterParserIntEqComparator(number);
}
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
cmp = new FilterParserIntNeComparator(number);
}
else if (prefix == QLatin1Char('>')) {
cmp = new FilterParserIntGtComparator(number);
}
else if (prefix == QLatin1String(">=")) {
cmp = new FilterParserIntGeComparator(number);
}
else if (prefix == QLatin1Char('<')) {
cmp = new FilterParserIntLtComparator(number);
}
else if (prefix == QLatin1String("<=")) {
cmp = new FilterParserIntLeComparator(number);
}
else {
cmp = new FilterParserIntEqComparator(number);
}
}
}
else if (Song::kUIntSearchColumns.contains(column, Qt::CaseInsensitive)) {
bool ok = false;
uint number = value.toUInt(&ok);
if (ok) {
if (prefix == QLatin1Char('=') || prefix == QLatin1String("==")) {
cmp = new FilterParserUIntEqComparator(number);
}
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
cmp = new FilterParserUIntNeComparator(number);
}
else if (prefix == QLatin1Char('>')) {
cmp = new FilterParserUIntGtComparator(number);
}
else if (prefix == QLatin1String(">=")) {
cmp = new FilterParserUIntGeComparator(number);
}
else if (prefix == QLatin1Char('<')) {
cmp = new FilterParserUIntLtComparator(number);
}
else if (prefix == QLatin1String("<=")) {
cmp = new FilterParserUIntLeComparator(number);
}
else {
cmp = new FilterParserUIntEqComparator(number);
}
}
}
else if (Song::kInt64SearchColumns.contains(column, Qt::CaseInsensitive)) {
qint64 number = 0;
if (column == QLatin1String("length")) {
number = ParseTime(value);
}
else {
number = value.toLongLong();
}
if (prefix == QLatin1Char('=') || prefix == QLatin1String("==")) {
cmp = new FilterParserInt64EqComparator(number);
}
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
cmp = new FilterParserInt64NeComparator(number);
}
else if (prefix == QLatin1Char('>')) {
cmp = new FilterParserInt64GtComparator(number);
}
else if (prefix == QLatin1String(">=")) {
cmp = new FilterParserInt64GeComparator(number);
}
else if (prefix == QLatin1Char('<')) {
cmp = new FilterParserInt64LtComparator(number);
}
else if (prefix == QLatin1String("<=")) {
cmp = new FilterParserInt64LeComparator(number);
}
else {
cmp = new FilterParserInt64EqComparator(number);
}
}
else if (Song::kFloatSearchColumns.contains(column, Qt::CaseInsensitive)) {
const float rating = ParseRating(value);
if (prefix == QLatin1Char('=') || prefix == QLatin1String("==")) {
cmp = new FilterParserFloatEqComparator(rating);
}
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
cmp = new FilterParserFloatNeComparator(rating);
}
else if (prefix == QLatin1Char('>')) {
cmp = new FilterParserFloatGtComparator(rating);
}
else if (prefix == QLatin1String(">=")) {
cmp = new FilterParserFloatGeComparator(rating);
}
else if (prefix == QLatin1Char('<')) {
cmp = new FilterParserFloatLtComparator(rating);
}
else if (prefix == QLatin1String("<=")) {
cmp = new FilterParserFloatLeComparator(rating);
}
else {
cmp = new FilterParserFloatEqComparator(rating);
}
}
if (cmp) {
return new FilterColumnTerm(column, cmp);
}
return new FilterTerm(Song::kTextSearchColumns, new FilterParserDefaultComparator(value));
}
// Try and parse the string as '[[h:]m:]s' (ignoring all spaces),
// and return the number of seconds if it parses correctly.
// If not, the original string is returned.
// The 'h', 'm' and 's' components can have any length (including 0).
// A few examples:
// "::" is parsed to "0"
// "1::" is parsed to "3600"
// "3:45" is parsed to "225"
// "1:165" is parsed to "225"
// "225" is parsed to "225" (srsly! ^.^)
// "2:3:4:5" is parsed to "2:3:4:5"
// "25m" is parsed to "25m"
qint64 FilterParser::ParseTime(const QString &time_str) {
qint64 seconds = 0;
qint64 accum = 0;
qint64 colon_count = 0;
for (const QChar &c : time_str) {
if (c.isDigit()) {
accum = accum * 10LL + static_cast<qint64>(c.digitValue());
}
else if (c == QLatin1Char(':')) {
seconds = seconds * 60LL + accum;
accum = 0LL;
++colon_count;
if (colon_count > 2) {
return 0LL;
}
}
else if (!c.isSpace()) {
return 0LL;
}
}
seconds = seconds * 60LL + accum;
return seconds;
}
// Parses a rating search term to float.
// If the rating is a number from 0-5, map it to 0-1
// To use float values directly, the search term can be prefixed with "f" (rating:>f0.2)
// If search string is 0, or by default, uses -1
// @param rating_str: Rating search 0-5, or "f0.2"
// @return float: rating from 0-1 or -1 if not rated.
float FilterParser::ParseRating(const QString &rating_str) {
if (rating_str.isEmpty()) {
return -1;
}
float rating = -1.0F;
// Check if the search is a float
if (rating_str.contains(QLatin1Char('f'), Qt::CaseInsensitive)) {
if (rating_str.count(QLatin1Char('f'), Qt::CaseInsensitive) > 1) {
return rating;
}
QString rating_float_str = rating_str;
if (rating_str.at(0) == QLatin1Char('f') || rating_str.at(0) == QLatin1Char('F')) {
rating_float_str = rating_float_str.remove(0, 1);
}
if (rating_str.right(1) == QLatin1Char('f') || rating_str.right(1) == QLatin1Char('F')) {
rating_float_str.chop(1);
}
bool ok = false;
const float rating_input = rating_float_str.toFloat(&ok);
if (ok) {
rating = rating_input;
}
}
else {
bool ok = false;
const int rating_input = rating_str.toInt(&ok);
// Is valid int from 0-5: convert to float
if (ok && rating_input >= 0 && rating_input <= 5) {
rating = static_cast<float>(rating_input) / 5.0F;
}
}
// Songs with zero rating have -1 in the DB
if (rating == 0) {
rating = -1;
}
return rating;
}
QString FilterParser::ToolTip() {
return QLatin1String("<html><head/><body><p>") +
QObject::tr("Prefix a search term with a field name to limit the search to that field, e.g.:") +
QLatin1Char(' ') +
QLatin1String("<span style=\"font-weight:600;\">") +
QObject::tr("artist") +
QLatin1String(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
QObject::tr("searches for all artists containing the word %1. ").arg(QLatin1String("Strawbs")) +
QLatin1String("</p><p>") +
QObject::tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
.arg(QLatin1String(" =, !=, &lt;, &gt;, &lt;="), QLatin1String("&gt;=")) +
QLatin1String("<span style=\"font-weight:600;\">") +
QObject::tr("rating") +
QLatin1String("</span>") +
QLatin1String(":>=") +
QLatin1String("<span style=\"font-weight:italic;\">4</span>") +
QLatin1String("</p><p>") +
QObject::tr("Multiple search terms can also be combined with \"%1\" (default) and \"%2\", as well as grouped with parentheses. ")
.arg(QLatin1String("AND"), QLatin1String("OR")) +
QLatin1String("</p><p><span style=\"font-weight:600;\">") +
QObject::tr("Available fields") +
QLatin1String(": ") + QLatin1String("</span><span style=\"font-style:italic;\">") +
Song::kSearchColumns.join(QLatin1String(", ")) +
QLatin1String("</span>.") +
QLatin1String("</p></body></html>");
}

View File

@@ -2,6 +2,8 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com> * Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -18,47 +20,17 @@
* *
*/ */
#ifndef PLAYLISTFILTERPARSER_H #ifndef FILTERPARSER_H
#define PLAYLISTFILTERPARSER_H #define FILTERPARSER_H
#include "config.h" #include "config.h"
#include <QSet>
#include <QMap>
#include <QString> #include <QString>
class QAbstractItemModel; class FilterTree;
class QModelIndex;
// Structure for filter parse tree
class FilterTree {
public:
FilterTree() = default;
virtual ~FilterTree() {}
virtual bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const = 0;
enum class FilterType {
Nop = 0,
Or,
And,
Not,
Column,
Term
};
virtual FilterType type() = 0;
private:
Q_DISABLE_COPY(FilterTree)
};
// Trivial filter that accepts *anything*
class NopFilter : public FilterTree {
public:
bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override { Q_UNUSED(row); Q_UNUSED(parent); Q_UNUSED(model); return true; }
FilterType type() override { return FilterType::Nop; }
};
// A utility class to parse search filter strings into a decision tree // A utility class to parse search filter strings into a decision tree
// that can decide whether a playlist entry matches the filter. // that can decide whether a song matches the filter.
// //
// Here's a grammar describing the filters we expect: // Here's a grammar describing the filters we expect:
//  expr ::= or-group //  expr ::= or-group
@@ -72,29 +44,33 @@ class NopFilter : public FilterTree {
// col ::= "title" | "artist" | ... // col ::= "title" | "artist" | ...
class FilterParser { class FilterParser {
public: public:
explicit FilterParser(const QString &filter, const QMap<QString, int> &columns, const QSet<int> &numerical_cols); explicit FilterParser(const QString &filter_string);
FilterTree *parse(); FilterTree *parse();
private: static QString ToolTip();
protected:
void advance(); void advance();
FilterTree *parseOrGroup();
FilterTree *parseAndGroup();
// Check if iter is at the start of 'AND' if so, step over it and return true if not, return false and leave iter where it was // Check if iter is at the start of 'AND' if so, step over it and return true if not, return false and leave iter where it was
bool checkAnd(); bool checkAnd();
// Check if iter is at the start of 'OR' // Check if iter is at the start of 'OR'
bool checkOr(bool step_over = true); bool checkOr(const bool step_over = true);
FilterTree *parseOrGroup();
FilterTree *parseAndGroup();
FilterTree *parseSearchExpression(); FilterTree *parseSearchExpression();
FilterTree *parseSearchTerm(); FilterTree *parseSearchTerm();
FilterTree *createSearchTermTreeNode(const QString &col, const QString &prefix, const QString &search) const; FilterTree *createSearchTermTreeNode(const QString &column, const QString &prefix, const QString &value) const;
static qint64 ParseTime(const QString &time_str);
static float ParseRating(const QString &rating_str);
const QString filter_string_;
QString::const_iterator iter_; QString::const_iterator iter_;
QString::const_iterator end_; QString::const_iterator end_;
QString buf_; QString buf_;
const QString filterstring_;
const QMap<QString, int> columns_;
const QSet<int> numerical_columns_;
}; };
#endif // PLAYLISTFILTERPARSER_H #endif // FILTERPARSER_H

View File

@@ -0,0 +1,314 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, 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 FILTERPARSERSEARCHCOMPARATORS_H
#define FILTERPARSERSEARCHCOMPARATORS_H
#include "config.h"
#include <QVariant>
#include <QString>
#include <QScopedPointer>
class FilterParserSearchTermComparator {
public:
FilterParserSearchTermComparator() = default;
virtual ~FilterParserSearchTermComparator() = default;
virtual bool Matches(const QVariant &value) const = 0;
private:
Q_DISABLE_COPY(FilterParserSearchTermComparator)
};
// "compares" by checking if the field contains the search term
class FilterParserDefaultComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserDefaultComparator(const QString &search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toString().contains(search_term_, Qt::CaseInsensitive);
}
private:
QString search_term_;
Q_DISABLE_COPY(FilterParserDefaultComparator)
};
class FilterParserTextEqComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserTextEqComparator(const QString &search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return search_term_.compare(value.toString(), Qt::CaseInsensitive) == 0;
}
private:
QString search_term_;
};
class FilterParserTextNeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserTextNeComparator(const QString &search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return search_term_.compare(value.toString(), Qt::CaseInsensitive) != 0;
}
private:
QString search_term_;
};
class FilterParserIntEqComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntEqComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() == search_term_;
}
private:
int search_term_;
};
class FilterParserIntNeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntNeComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() != search_term_;
}
private:
int search_term_;
};
class FilterParserIntGtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntGtComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() > search_term_;
}
private:
int search_term_;
};
class FilterParserIntGeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntGeComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() >= search_term_;
}
private:
int search_term_;
};
class FilterParserIntLtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntLtComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() < search_term_;
}
private:
int search_term_;
};
class FilterParserIntLeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntLeComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() <= search_term_;
}
private:
int search_term_;
};
class FilterParserUIntEqComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntEqComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() == search_term_;
}
private:
uint search_term_;
};
class FilterParserUIntNeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntNeComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() != search_term_;
}
private:
uint search_term_;
};
class FilterParserUIntGtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntGtComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() > search_term_;
}
private:
uint search_term_;
};
class FilterParserUIntGeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntGeComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() >= search_term_;
}
private:
uint search_term_;
};
class FilterParserUIntLtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntLtComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() < search_term_;
}
private:
uint search_term_;
};
class FilterParserUIntLeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntLeComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() <= search_term_;
}
private:
uint search_term_;
};
class FilterParserInt64EqComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64EqComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() == search_term_;
}
private:
qint64 search_term_;
};
class FilterParserInt64NeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64NeComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() != search_term_;
}
private:
qint64 search_term_;
};
class FilterParserInt64GtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64GtComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() > search_term_;
}
private:
qint64 search_term_;
};
class FilterParserInt64GeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64GeComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() >= search_term_;
}
private:
qint64 search_term_;
};
class FilterParserInt64LtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64LtComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() < search_term_;
}
private:
qint64 search_term_;
};
class FilterParserInt64LeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64LeComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() <= search_term_;
}
private:
qint64 search_term_;
};
// Float Comparators are for the rating
class FilterParserFloatEqComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatEqComparator(const float search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() == search_term_;
}
private:
float search_term_;
};
class FilterParserFloatNeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatNeComparator(const float value) : search_term_(value) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() != search_term_;
}
private:
float search_term_;
};
class FilterParserFloatGtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatGtComparator(const float search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() > search_term_;
}
private:
float search_term_;
};
class FilterParserFloatGeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatGeComparator(const float search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() >= search_term_;
}
private:
float search_term_;
};
class FilterParserFloatLtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatLtComparator(const float search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() < search_term_;
}
private:
float search_term_;
};
class FilterParserFloatLeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatLeComparator(const float search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() <= search_term_;
}
private:
float search_term_;
};
#endif // FILTERPARSERSEARCHCOMPARATORS_H

View File

@@ -0,0 +1,52 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, 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 <QString>
#include "filtertree.h"
FilterTree::FilterTree() = default;
FilterTree::~FilterTree() = default;
QVariant FilterTree::DataFromColumn(const QString &column, const Song &metadata) {
if (column == QLatin1String("albumartist")) return metadata.effective_albumartist();
if (column == QLatin1String("artist")) return metadata.artist();
if (column == QLatin1String("album")) return metadata.album();
if (column == QLatin1String("title")) return metadata.title();
if (column == QLatin1String("composer")) return metadata.composer();
if (column == QLatin1String("performer")) return metadata.performer();
if (column == QLatin1String("grouping")) return metadata.grouping();
if (column == QLatin1String("genre")) return metadata.genre();
if (column == QLatin1String("comment")) return metadata.comment();
if (column == QLatin1String("track")) return metadata.track();
if (column == QLatin1String("year")) return metadata.year();
if (column == QLatin1String("length")) return metadata.length_nanosec();
if (column == QLatin1String("samplerate")) return metadata.samplerate();
if (column == QLatin1String("bitdepth")) return metadata.bitdepth();
if (column == QLatin1String("bitrate")) return metadata.bitrate();
if (column == QLatin1String("rating")) return metadata.rating();
if (column == QLatin1String("playcount")) return metadata.playcount();
if (column == QLatin1String("skipcount")) return metadata.skipcount();
return QVariant();
}

View File

@@ -0,0 +1,147 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, 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 FILTERTREE_H
#define FILTERTREE_H
#include "config.h"
#include <QList>
#include <QString>
#include <QStringList>
#include <QScopedPointer>
#include "core/song.h"
#include "filterparsersearchcomparators.h"
class FilterTree {
public:
explicit FilterTree();
virtual ~FilterTree();
enum class FilterType {
Nop = 0,
Or,
And,
Not,
Column,
Term
};
virtual FilterType type() const = 0;
virtual bool accept(const Song &song) const = 0;
protected:
static QVariant DataFromColumn(const QString &column, const Song &metadata);
private:
Q_DISABLE_COPY(FilterTree)
};
// Trivial filter that accepts *anything*
class NopFilter : public FilterTree {
public:
FilterType type() const override { return FilterType::Nop; }
bool accept(const Song &song) const override { Q_UNUSED(song); return true; }
};
// Filter that applies a SearchTermComparator to all fields
class FilterTerm : public FilterTree {
public:
explicit FilterTerm(const QStringList &columns, FilterParserSearchTermComparator *comparator) : columns_(columns), cmp_(comparator) {}
FilterType type() const override { return FilterType::Term; }
bool accept(const Song &song) const override {
for (const QString &column : columns_) {
if (cmp_->Matches(DataFromColumn(column, song))) return true;
}
return false;
}
private:
const QStringList columns_;
QScopedPointer<FilterParserSearchTermComparator> cmp_;
};
class FilterColumnTerm : public FilterTree {
public:
explicit FilterColumnTerm(const QString &column, FilterParserSearchTermComparator *comparator) : column_(column), cmp_(comparator) {}
FilterType type() const override { return FilterType::Column; }
bool accept(const Song &song) const override {
return cmp_->Matches(DataFromColumn(column_, song));
}
private:
const QString column_;
QScopedPointer<FilterParserSearchTermComparator> cmp_;
};
class NotFilter : public FilterTree {
public:
explicit NotFilter(const FilterTree *inv) : child_(inv) {}
FilterType type() const override { return FilterType::Not; }
bool accept(const Song &song) const override {
return !child_->accept(song);
}
private:
QScopedPointer<const FilterTree> child_;
};
class OrFilter : public FilterTree {
public:
~OrFilter() override { qDeleteAll(children_); }
FilterType type() const override { return FilterType::Or; }
virtual void add(FilterTree *child) { children_.append(child); }
bool accept(const Song &song) const override {
return std::any_of(children_.begin(), children_.end(), [song](FilterTree *child) { return child->accept(song); });
}
private:
QList<FilterTree*> children_;
};
class AndFilter : public FilterTree {
public:
~AndFilter() override { qDeleteAll(children_); }
FilterType type() const override { return FilterType::And; }
virtual void add(FilterTree *child) { children_.append(child); }
bool accept(const Song &song) const override {
return !std::any_of(children_.begin(), children_.end(), [song](FilterTree *child) { return !child->accept(song); });
}
private:
QList<FilterTree*> children_;
};
#endif // FILTERTREE_H

View File

@@ -710,6 +710,7 @@ void Playlist::set_current_row(const int i, const AutoScroll autoscroll, const b
if (current_item_index_.isValid() && !is_stopping) { if (current_item_index_.isValid() && !is_stopping) {
InformOfCurrentSongChange(false); InformOfCurrentSongChange(false);
emit dataChanged(index(current_item_index_.row(), 0), index(current_item_index_.row(), ColumnCount - 1));
emit MaybeAutoscroll(autoscroll); emit MaybeAutoscroll(autoscroll);
} }

View File

@@ -49,6 +49,7 @@
#include "core/shared_ptr.h" #include "core/shared_ptr.h"
#include "core/iconloader.h" #include "core/iconloader.h"
#include "core/settings.h" #include "core/settings.h"
#include "filterparser/filterparser.h"
#include "playlist.h" #include "playlist.h"
#include "playlisttabbar.h" #include "playlisttabbar.h"
#include "playlistview.h" #include "playlistview.h"
@@ -124,37 +125,7 @@ PlaylistContainer::PlaylistContainer(QWidget *parent)
QObject::connect(ui_->playlist, &PlaylistView::FocusOnFilterSignal, this, &PlaylistContainer::FocusOnFilter); QObject::connect(ui_->playlist, &PlaylistView::FocusOnFilterSignal, this, &PlaylistContainer::FocusOnFilter);
ui_->search_field->installEventFilter(this); ui_->search_field->installEventFilter(this);
QString available_fields = PlaylistFilter().column_names().keys().join(QLatin1String(", ")); ui_->search_field->setToolTip(FilterParser::ToolTip());
ui_->search_field->setToolTip(
QLatin1String("<html><head/><body><p>") +
tr("Prefix a search term with a field name to limit the search to that field, e.g.:") +
QLatin1Char(' ') +
QLatin1String("<span style=\"font-weight:600;\">") +
tr("artist") +
QLatin1String(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
tr("searches the playlist for all artists that contain the word %1. ").arg(QLatin1String("Strawbs")) +
QLatin1String("</p><p>") +
tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
.arg(QLatin1String(" =, !=, &lt;, &gt;, &lt;="), QLatin1String("&gt;=")) +
QLatin1String("<span style=\"font-weight:600;\">") +
tr("rating") +
QLatin1String("</span>") +
QLatin1String(":>=") +
QLatin1String("<span style=\"font-weight:italic;\">4</span>") +
QLatin1String("</p><p>") +
tr("Multiple search terms can also be combined with \"%1\" (default) and \"%2\", as well as grouped with parentheses. ")
.arg(QLatin1String("AND"), QLatin1String("OR")) +
QLatin1String("</p><p><span style=\"font-weight:600;\">") +
tr("Available fields") +
QLatin1String(": ") + QLatin1String("</span><span style=\"font-style:italic;\">") +
available_fields +
QLatin1String("</span>.") +
QLatin1String("</p></body></html>")
);
ReloadSettings(); ReloadSettings();
@@ -234,7 +205,7 @@ void PlaylistContainer::SetViewModel(Playlist *playlist, const int scroll_positi
emit ViewSelectionModelChanged(); emit ViewSelectionModelChanged();
// Update filter // Update filter
ui_->search_field->setText(playlist->filter()->filter_text()); ui_->search_field->setText(playlist->filter()->filter_string());
// Update the no matches label // Update the no matches label
QObject::connect(playlist_->filter(), &QSortFilterProxyModel::modelReset, this, &PlaylistContainer::UpdateNoMatchesLabel); QObject::connect(playlist_->filter(), &QSortFilterProxyModel::modelReset, this, &PlaylistContainer::UpdateNoMatchesLabel);
@@ -452,7 +423,7 @@ void PlaylistContainer::UpdateFilter() {
if (!ui_->toolbar->isVisible()) return; if (!ui_->toolbar->isVisible()) return;
manager_->current()->filter()->SetFilterText(ui_->search_field->text()); manager_->current()->filter()->SetFilterString(ui_->search_field->text());
ui_->playlist->JumpToCurrentlyPlayingTrack(); ui_->playlist->JumpToCurrentlyPlayingTrack();
UpdateNoMatchesLabel(); UpdateNoMatchesLabel();

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com> * Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -23,12 +23,12 @@
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QAbstractItemModel>
#include <QSortFilterProxyModel>
#include "playlist/playlist.h" #include "playlist/playlist.h"
#include "playlist/playlistitem.h"
#include "filterparser/filterparser.h"
#include "filterparser/filtertree.h"
#include "playlistfilter.h" #include "playlistfilter.h"
#include "playlistfilterparser.h"
PlaylistFilter::PlaylistFilter(QObject *parent) PlaylistFilter::PlaylistFilter(QObject *parent)
: QSortFilterProxyModel(parent), : QSortFilterProxyModel(parent),
@@ -37,40 +37,6 @@ PlaylistFilter::PlaylistFilter(QObject *parent)
setDynamicSortFilter(true); setDynamicSortFilter(true);
column_names_[QStringLiteral("title")] = static_cast<int>(Playlist::Column::Title);
column_names_[QStringLiteral("name")] = static_cast<int>(Playlist::Column::Title);
column_names_[QStringLiteral("artist")] = static_cast<int>(Playlist::Column::Artist);
column_names_[QStringLiteral("album")] = static_cast<int>(Playlist::Column::Album);
column_names_[QStringLiteral("albumartist")] = static_cast<int>(Playlist::Column::AlbumArtist);
column_names_[QStringLiteral("performer")] = static_cast<int>(Playlist::Column::Performer);
column_names_[QStringLiteral("composer")] = static_cast<int>(Playlist::Column::Composer);
column_names_[QStringLiteral("year")] = static_cast<int>(Playlist::Column::Year);
column_names_[QStringLiteral("originalyear")] = static_cast<int>(Playlist::Column::OriginalYear);
column_names_[QStringLiteral("track")] = static_cast<int>(Playlist::Column::Track);
column_names_[QStringLiteral("disc")] = static_cast<int>(Playlist::Column::Disc);
column_names_[QStringLiteral("length")] = static_cast<int>(Playlist::Column::Length);
column_names_[QStringLiteral("genre")] = static_cast<int>(Playlist::Column::Genre);
column_names_[QStringLiteral("samplerate")] = static_cast<int>(Playlist::Column::Samplerate);
column_names_[QStringLiteral("bitdepth")] = static_cast<int>(Playlist::Column::Bitdepth);
column_names_[QStringLiteral("bitrate")] = static_cast<int>(Playlist::Column::Bitrate);
column_names_[QStringLiteral("filename")] = static_cast<int>(Playlist::Column::Filename);
column_names_[QStringLiteral("grouping")] = static_cast<int>(Playlist::Column::Grouping);
column_names_[QStringLiteral("comment")] = static_cast<int>(Playlist::Column::Comment);
column_names_[QStringLiteral("rating")] = static_cast<int>(Playlist::Column::Rating);
column_names_[QStringLiteral("playcount")] = static_cast<int>(Playlist::Column::PlayCount);
column_names_[QStringLiteral("skipcount")] = static_cast<int>(Playlist::Column::SkipCount);
numerical_columns_ << static_cast<int>(Playlist::Column::Year)
<< static_cast<int>(Playlist::Column::OriginalYear)
<< static_cast<int>(Playlist::Column::Track)
<< static_cast<int>(Playlist::Column::Disc)
<< static_cast<int>(Playlist::Column::Length)
<< static_cast<int>(Playlist::Column::Samplerate)
<< static_cast<int>(Playlist::Column::Bitdepth)
<< static_cast<int>(Playlist::Column::Bitrate)
<< static_cast<int>(Playlist::Column::PlayCount)
<< static_cast<int>(Playlist::Column::SkipCount);
} }
PlaylistFilter::~PlaylistFilter() = default; PlaylistFilter::~PlaylistFilter() = default;
@@ -80,29 +46,35 @@ void PlaylistFilter::sort(int column, Qt::SortOrder order) {
sourceModel()->sort(column, order); sourceModel()->sort(column, order);
} }
bool PlaylistFilter::filterAcceptsRow(const int row, const QModelIndex &parent) const { bool PlaylistFilter::filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const {
Playlist *playlist = qobject_cast<Playlist*>(sourceModel());
if (!playlist) return false;
const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
if (!idx.isValid()) return false;
PlaylistItemPtr item = playlist->item_at(idx.row());
if (!item) return false;
if (filter_string_.isEmpty()) return true;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
size_t hash = qHash(filter_text_); const size_t hash = qHash(filter_string_);
#else #else
uint hash = qHash(filter_text_); const uint hash = qHash(filter_string_);
#endif #endif
if (hash != query_hash_) { if (hash != query_hash_) {
// Parse the query FilterParser p(filter_string_);
FilterParser p(filter_text_, column_names_, numerical_columns_);
filter_tree_.reset(p.parse()); filter_tree_.reset(p.parse());
query_hash_ = hash; query_hash_ = hash;
} }
// Test the row return filter_tree_->accept(item->Metadata());
return filter_tree_->accept(row, parent, sourceModel());
} }
void PlaylistFilter::SetFilterText(const QString &filter_text) { void PlaylistFilter::SetFilterString(const QString &filter_string) {
filter_text_ = filter_text; filter_string_ = filter_string;
setFilterFixedString(filter_text); setFilterFixedString(filter_string);
} }

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com> * Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -24,15 +24,11 @@
#include "config.h" #include "config.h"
#include <QtGlobal> #include <QSortFilterProxyModel>
#include <QObject>
#include <QMap>
#include <QSet>
#include <QScopedPointer> #include <QScopedPointer>
#include <QString> #include <QString>
#include <QSortFilterProxyModel>
class FilterTree; #include "filterparser/filtertree.h"
class PlaylistFilter : public QSortFilterProxyModel { class PlaylistFilter : public QSortFilterProxyModel {
Q_OBJECT Q_OBJECT
@@ -48,10 +44,8 @@ class PlaylistFilter : public QSortFilterProxyModel {
// public so Playlist::NextVirtualIndex and friends can get at it // public so Playlist::NextVirtualIndex and friends can get at it
bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override; bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override;
void SetFilterText(const QString &filter_text); void SetFilterString(const QString &filter_string);
QString filter_string() const { return filter_string_; }
QString filter_text() const { return filter_text_; }
QMap<QString, int> column_names() const { return column_names_; }
private: private:
// Mutable because they're modified from filterAcceptsRow() const // Mutable because they're modified from filterAcceptsRow() const
@@ -61,10 +55,7 @@ class PlaylistFilter : public QSortFilterProxyModel {
#else #else
mutable uint query_hash_; mutable uint query_hash_;
#endif #endif
QString filter_string_;
QMap<QString, int> column_names_;
QSet<int> numerical_columns_;
QString filter_text_;
}; };
#endif // PLAYLISTFILTER_H #endif // PLAYLISTFILTER_H

View File

@@ -1,607 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* 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 "config.h"
#include <algorithm>
#include <cmath>
#include <QList>
#include <QMap>
#include <QSet>
#include <QChar>
#include <QScopedPointer>
#include <QString>
#include <QtAlgorithms>
#include <QAbstractItemModel>
#include "playlist.h"
#include "playlistfilterparser.h"
#include "utilities/searchparserutils.h"
class SearchTermComparator {
public:
SearchTermComparator() = default;
virtual ~SearchTermComparator() = default;
virtual bool Matches(const QString &element) const = 0;
private:
Q_DISABLE_COPY(SearchTermComparator)
};
// "compares" by checking if the field contains the search term
class DefaultComparator : public SearchTermComparator {
public:
explicit DefaultComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.contains(search_term_);
}
private:
QString search_term_;
Q_DISABLE_COPY(DefaultComparator)
};
class EqComparator : public SearchTermComparator {
public:
explicit EqComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ == element;
}
private:
QString search_term_;
};
class NeComparator : public SearchTermComparator {
public:
explicit NeComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ != element;
}
private:
QString search_term_;
};
class LexicalGtComparator : public SearchTermComparator {
public:
explicit LexicalGtComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element > search_term_;
}
private:
QString search_term_;
};
class LexicalGeComparator : public SearchTermComparator {
public:
explicit LexicalGeComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element >= search_term_;
}
private:
QString search_term_;
};
class LexicalLtComparator : public SearchTermComparator {
public:
explicit LexicalLtComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element < search_term_;
}
private:
QString search_term_;
};
class LexicalLeComparator : public SearchTermComparator {
public:
explicit LexicalLeComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element <= search_term_;
}
private:
QString search_term_;
};
// Float Comparators are for the rating
class FloatEqComparator : public SearchTermComparator {
public:
explicit FloatEqComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ == element.toFloat();
}
private:
float search_term_;
};
class FloatNeComparator : public SearchTermComparator {
public:
explicit FloatNeComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ != element.toFloat();
}
private:
float search_term_;
};
class FloatGtComparator : public SearchTermComparator {
public:
explicit FloatGtComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() > search_term_;
}
private:
float search_term_;
};
class FloatGeComparator : public SearchTermComparator {
public:
explicit FloatGeComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() >= search_term_;
}
private:
float search_term_;
};
class FloatLtComparator : public SearchTermComparator {
public:
explicit FloatLtComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() < search_term_;
}
private:
float search_term_;
};
class FloatLeComparator : public SearchTermComparator {
public:
explicit FloatLeComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() <= search_term_;
}
private:
float search_term_;
};
class GtComparator : public SearchTermComparator {
public:
explicit GtComparator(const int value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toInt() > search_term_;
}
private:
int search_term_;
};
class GeComparator : public SearchTermComparator {
public:
explicit GeComparator(const int value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toInt() >= search_term_;
}
private:
int search_term_;
};
class LtComparator : public SearchTermComparator {
public:
explicit LtComparator(const int value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toInt() < search_term_;
}
private:
int search_term_;
};
class LeComparator : public SearchTermComparator {
public:
explicit LeComparator(const int value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toInt() <= search_term_;
}
private:
int search_term_;
};
// The length field of the playlist (entries) contains a song's running time in nanoseconds.
// However, We don't really care about nanoseconds, just seconds.
// Thus, with this decorator we drop the last 9 digits, if that many are present.
class DropTailComparatorDecorator : public SearchTermComparator {
public:
explicit DropTailComparatorDecorator(SearchTermComparator *cmp) : cmp_(cmp) {}
bool Matches(const QString &element) const override {
if (element.length() > 9) {
return cmp_->Matches(element.left(element.length() - 9));
}
else {
return cmp_->Matches(element);
}
}
private:
QScopedPointer<SearchTermComparator> cmp_;
};
class RatingComparatorDecorator : public SearchTermComparator {
public:
explicit RatingComparatorDecorator(SearchTermComparator *cmp) : cmp_(cmp) {}
bool Matches(const QString &element) const override {
return cmp_->Matches(QString::number(lround(element.toDouble() * 10.0)));
}
private:
QScopedPointer<SearchTermComparator> cmp_;
};
// filter that applies a SearchTermComparator to all fields of a playlist entry
class FilterTerm : public FilterTree {
public:
explicit FilterTerm(SearchTermComparator *comparator, const QList<int> &columns) : cmp_(comparator), columns_(columns) {}
bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
for (int i : columns_) {
QModelIndex idx(model->index(row, i, parent));
if (cmp_->Matches(idx.data().toString().toLower())) return true;
}
return false;
}
FilterType type() override { return FilterType::Term; }
private:
QScopedPointer<SearchTermComparator> cmp_;
QList<int> columns_;
};
// filter that applies a SearchTermComparator to one specific field of a playlist entry
class FilterColumnTerm : public FilterTree {
public:
FilterColumnTerm(const int column, SearchTermComparator *comparator) : col(column), cmp_(comparator) {}
bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
QModelIndex idx(model->index(row, col, parent));
return cmp_->Matches(idx.data().toString().toLower());
}
FilterType type() override { return FilterType::Column; }
private:
int col;
QScopedPointer<SearchTermComparator> cmp_;
};
class NotFilter : public FilterTree {
public:
explicit NotFilter(const FilterTree *inv) : child_(inv) {}
bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
return !child_->accept(row, parent, model);
}
FilterType type() override { return FilterType::Not; }
private:
QScopedPointer<const FilterTree> child_;
};
class OrFilter : public FilterTree {
public:
~OrFilter() override { qDeleteAll(children_); }
virtual void add(FilterTree *child) { children_.append(child); }
bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
return std::any_of(children_.begin(), children_.end(), [row, parent, model](FilterTree *child) { return child->accept(row, parent, model); });
}
FilterType type() override { return FilterType::Or; }
private:
QList<FilterTree*> children_;
};
class AndFilter : public FilterTree {
public:
~AndFilter() override { qDeleteAll(children_); }
virtual void add(FilterTree *child) { children_.append(child); }
bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
return !std::any_of(children_.begin(), children_.end(), [row, parent, model](FilterTree *child) { return !child->accept(row, parent, model); });
}
FilterType type() override { return FilterType::And; }
private:
QList<FilterTree*> children_;
};
FilterParser::FilterParser(const QString &filter, const QMap<QString, int> &columns, const QSet<int> &numerical_cols) : iter_{}, end_{}, filterstring_(filter), columns_(columns), numerical_columns_(numerical_cols) {}
FilterTree *FilterParser::parse() {
iter_ = filterstring_.constBegin();
end_ = filterstring_.constEnd();
return parseOrGroup();
}
void FilterParser::advance() {
while (iter_ != end_ && iter_->isSpace()) {
++iter_;
}
}
FilterTree *FilterParser::parseOrGroup() {
advance();
if (iter_ == end_) return new NopFilter;
OrFilter *group = new OrFilter;
group->add(parseAndGroup());
advance();
while (checkOr()) {
group->add(parseAndGroup());
advance();
}
return group;
}
FilterTree *FilterParser::parseAndGroup() {
advance();
if (iter_ == end_) return new NopFilter;
AndFilter *group = new AndFilter();
do {
group->add(parseSearchExpression());
advance();
if (iter_ != end_ && *iter_ == QLatin1Char(')')) break;
if (checkOr(false)) {
break;
}
checkAnd(); // if there's no 'AND', we'll add the term anyway...
} while (iter_ != end_);
return group;
}
bool FilterParser::checkAnd() {
if (iter_ != end_) {
if (*iter_ == QLatin1Char('A')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && *iter_ == QLatin1Char('N')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && *iter_ == QLatin1Char('D')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QLatin1Char('-') || *iter_ == QLatin1Char('('))) {
advance();
buf_.clear();
return true;
}
}
}
}
}
return false;
}
bool FilterParser::checkOr(const bool step_over) {
if (!buf_.isEmpty()) {
if (buf_ == QLatin1String("OR")) {
if (step_over) {
buf_.clear();
advance();
}
return true;
}
}
else {
if (iter_ != end_) {
if (*iter_ == QLatin1Char('O')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && *iter_ == QLatin1Char('R')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QLatin1Char('-') || *iter_ == QLatin1Char('('))) {
if (step_over) {
buf_.clear();
advance();
}
return true;
}
}
}
}
}
return false;
}
FilterTree *FilterParser::parseSearchExpression() {
advance();
if (iter_ == end_) return new NopFilter;
if (*iter_ == QLatin1Char('(')) {
++iter_;
advance();
FilterTree *tree = parseOrGroup();
advance();
if (iter_ != end_) {
if (*iter_ == QLatin1Char(')')) {
++iter_;
}
}
return tree;
}
else if (*iter_ == QLatin1Char('-')) {
++iter_;
FilterTree *tree = parseSearchExpression();
if (tree->type() != FilterTree::FilterType::Nop) return new NotFilter(tree);
return tree;
}
else {
return parseSearchTerm();
}
}
FilterTree *FilterParser::parseSearchTerm() {
QString col;
QString search;
QString prefix;
bool inQuotes = false;
for (; iter_ != end_; ++iter_) {
if (inQuotes) {
if (*iter_ == QLatin1Char('"')) {
inQuotes = false;
}
else {
buf_ += *iter_;
}
}
else {
if (*iter_ == QLatin1Char('"')) {
inQuotes = true;
}
else if (col.isEmpty() && *iter_ == QLatin1Char(':')) {
col = buf_.toLower();
buf_.clear();
prefix.clear(); // prefix isn't allowed here - let's ignore it
}
else if (iter_->isSpace() || *iter_ == QLatin1Char('(') || *iter_ == QLatin1Char(')') || *iter_ == QLatin1Char('-')) {
break;
}
else if (buf_.isEmpty()) {
// we don't know whether there is a column part in this search term thus we assume the latter and just try and read a prefix
if (prefix.isEmpty() && (*iter_ == QLatin1Char('>') || *iter_ == QLatin1Char('<') || *iter_ == QLatin1Char('=') || *iter_ == QLatin1Char('!'))) {
prefix += *iter_;
}
else if (prefix != QLatin1Char('=') && *iter_ == QLatin1Char('=')) {
prefix += *iter_;
}
else {
buf_ += *iter_;
}
}
else {
buf_ += *iter_;
}
}
}
search = buf_.toLower();
buf_.clear();
return createSearchTermTreeNode(col, prefix, search);
}
FilterTree *FilterParser::createSearchTermTreeNode(const QString &col, const QString &prefix, const QString &search) const {
if (search.isEmpty() && prefix != QLatin1Char('=')) {
return new NopFilter;
}
// here comes a mess :/
// well, not that much of a mess, but so many options -_-
SearchTermComparator *cmp = nullptr;
// Handle the float based Rating Column
if (columns_[col] == static_cast<int>(Playlist::Column::Rating)) {
float parsed_search = Utilities::ParseSearchRating(search);
if (prefix == QLatin1Char('=')) {
cmp = new FloatEqComparator(parsed_search);
}
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
cmp = new FloatNeComparator(parsed_search);
}
else if (prefix == QLatin1Char('>')) {
cmp = new FloatGtComparator(parsed_search);
}
else if (prefix == QLatin1String(">=")) {
cmp = new FloatGeComparator(parsed_search);
}
else if (prefix == QLatin1Char('<')) {
cmp = new FloatLtComparator(parsed_search);
}
else if (prefix == QLatin1String("<=")) {
cmp = new FloatLeComparator(parsed_search);
}
else {
cmp = new FloatEqComparator(parsed_search);
}
}
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
cmp = new NeComparator(search);
}
else if (!col.isEmpty() && columns_.contains(col) && numerical_columns_.contains(columns_[col])) {
// the length column contains the time in seconds (nanoseconds, actually - the "nano" part is handled by the DropTailComparatorDecorator, though).
int search_value = 0;
if (columns_[col] == static_cast<int>(Playlist::Column::Length)) {
search_value = Utilities::ParseSearchTime(search);
}
else {
search_value = search.toInt();
}
// alright, back to deciding which comparator we'll use
if (prefix == QLatin1Char('>')) {
cmp = new GtComparator(search_value);
}
else if (prefix == QLatin1String(">=")) {
cmp = new GeComparator(search_value);
}
else if (prefix == QLatin1Char('<')) {
cmp = new LtComparator(search_value);
}
else if (prefix == QLatin1String("<=")) {
cmp = new LeComparator(search_value);
}
else {
// convert back because for time/rating
cmp = new EqComparator(QString::number(search_value));
}
}
else {
if (prefix == QLatin1Char('=')) {
cmp = new EqComparator(search);
}
else if (prefix == QLatin1Char('>')) {
cmp = new LexicalGtComparator(search);
}
else if (prefix == QLatin1String(">=")) {
cmp = new LexicalGeComparator(search);
}
else if (prefix == QLatin1Char('<')) {
cmp = new LexicalLtComparator(search);
}
else if (prefix == QLatin1String("<=")) {
cmp = new LexicalLeComparator(search);
}
else {
cmp = new DefaultComparator(search);
}
}
if (columns_.contains(col)) {
if (columns_[col] == static_cast<int>(Playlist::Column::Length)) {
cmp = new DropTailComparatorDecorator(cmp);
}
return new FilterColumnTerm(columns_[col], cmp);
}
else {
return new FilterTerm(cmp, columns_.values());
}
}

View File

@@ -335,6 +335,8 @@ void PlaylistView::RestoreHeaderState() {
if (set_initial_header_layout_) { if (set_initial_header_layout_) {
header_->SetStretchEnabled(true);
header_->HideSection(static_cast<int>(Playlist::Column::AlbumArtist)); header_->HideSection(static_cast<int>(Playlist::Column::AlbumArtist));
header_->HideSection(static_cast<int>(Playlist::Column::Performer)); header_->HideSection(static_cast<int>(Playlist::Column::Performer));
header_->HideSection(static_cast<int>(Playlist::Column::Composer)); header_->HideSection(static_cast<int>(Playlist::Column::Composer));
@@ -358,14 +360,22 @@ void PlaylistView::RestoreHeaderState() {
header_->HideSection(static_cast<int>(Playlist::Column::EBUR128IntegratedLoudness)); header_->HideSection(static_cast<int>(Playlist::Column::EBUR128IntegratedLoudness));
header_->HideSection(static_cast<int>(Playlist::Column::EBUR128LoudnessRange)); header_->HideSection(static_cast<int>(Playlist::Column::EBUR128LoudnessRange));
header_->ShowSection(static_cast<int>(Playlist::Column::Track));
header_->ShowSection(static_cast<int>(Playlist::Column::Title));
header_->ShowSection(static_cast<int>(Playlist::Column::Artist));
header_->ShowSection(static_cast<int>(Playlist::Column::Album));
header_->ShowSection(static_cast<int>(Playlist::Column::Samplerate));
header_->ShowSection(static_cast<int>(Playlist::Column::Bitdepth));
header_->ShowSection(static_cast<int>(Playlist::Column::Bitrate));
header_->ShowSection(static_cast<int>(Playlist::Column::Filetype));
header_->ShowSection(static_cast<int>(Playlist::Column::Source));
header_->moveSection(header_->visualIndex(static_cast<int>(Playlist::Column::Track)), 0); header_->moveSection(header_->visualIndex(static_cast<int>(Playlist::Column::Track)), 0);
header_->SetStretchEnabled(true); header_->SetColumnWidth(static_cast<int>(Playlist::Column::Track), 0.06);
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Title), 0.23);
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Track), 0.03); header_->SetColumnWidth(static_cast<int>(Playlist::Column::Artist), 0.23);
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Title), 0.24); header_->SetColumnWidth(static_cast<int>(Playlist::Column::Album), 0.23);
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Artist), 0.24);
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Album), 0.24);
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Length), 0.04); header_->SetColumnWidth(static_cast<int>(Playlist::Column::Length), 0.04);
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Samplerate), 0.05); header_->SetColumnWidth(static_cast<int>(Playlist::Column::Samplerate), 0.05);
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Bitdepth), 0.04); header_->SetColumnWidth(static_cast<int>(Playlist::Column::Bitdepth), 0.04);

View File

@@ -35,7 +35,7 @@
<item> <item>
<spacer name="spacer_style"> <spacer name="spacer_style">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -109,7 +109,7 @@
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="text"> <property name="text">
<string/> <string notr="true"/>
</property> </property>
</widget> </widget>
</item> </item>
@@ -151,7 +151,7 @@
<item> <item>
<widget class="QRadioButton" name="use_strawbs_background"> <widget class="QRadioButton" name="use_strawbs_background">
<property name="text"> <property name="text">
<string>A Taste of Strawbs</string> <string notr="true">A Taste of Strawbs</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -169,6 +169,9 @@
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="text">
<string notr="true"/>
</property>
</widget> </widget>
</item> </item>
<item> <item>
@@ -266,7 +269,7 @@
<item> <item>
<spacer name="spacer_position"> <spacer name="spacer_position">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -318,7 +321,7 @@
<item> <item>
<spacer name="spacer_background_image_stretch"> <spacer name="spacer_background_image_stretch">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -352,10 +355,10 @@
<number>100</number> <number>100</number>
</property> </property>
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="tickPosition"> <property name="tickPosition">
<enum>QSlider::TicksBelow</enum> <enum>QSlider::TickPosition::TicksBelow</enum>
</property> </property>
<property name="tickInterval"> <property name="tickInterval">
<number>10</number> <number>10</number>
@@ -388,10 +391,10 @@
<number>10</number> <number>10</number>
</property> </property>
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="tickPosition"> <property name="tickPosition">
<enum>QSlider::TicksBelow</enum> <enum>QSlider::TickPosition::TicksBelow</enum>
</property> </property>
<property name="tickInterval"> <property name="tickInterval">
<number>10</number> <number>10</number>
@@ -528,7 +531,7 @@
<item> <item>
<spacer name="spacer_icon_sizes"> <spacer name="spacer_icon_sizes">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -585,7 +588,7 @@
<item> <item>
<spacer name="spacer_bottom"> <spacer name="spacer_bottom">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>

View File

@@ -50,6 +50,9 @@
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="currentText">
<string notr="true"/>
</property>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="3" column="0">
@@ -64,6 +67,9 @@
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="currentText">
<string notr="true"/>
</property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
@@ -71,6 +77,9 @@
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="currentText">
<string notr="true"/>
</property>
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="0" column="0">
@@ -140,7 +149,7 @@
<item> <item>
<spacer name="spacer_alsaplugin"> <spacer name="spacer_alsaplugin">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -181,7 +190,7 @@
<item> <item>
<spacer name="spacer_exclusive_mode"> <spacer name="spacer_exclusive_mode">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -273,7 +282,7 @@
<item> <item>
<spacer name="spacer_channels"> <spacer name="spacer_channels">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -384,7 +393,7 @@
<item row="2" column="2"> <item row="2" column="2">
<spacer name="spacer_buffer_3"> <spacer name="spacer_buffer_3">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -397,7 +406,7 @@
<item row="1" column="2"> <item row="1" column="2">
<spacer name="spacer_buffer_2"> <spacer name="spacer_buffer_2">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -410,7 +419,7 @@
<item row="0" column="2"> <item row="0" column="2">
<spacer name="spacer_buffer_1"> <spacer name="spacer_buffer_1">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -434,7 +443,7 @@
<item> <item>
<spacer name="spacer_buffer_defaults"> <spacer name="spacer_buffer_defaults">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -550,7 +559,7 @@
<number>600</number> <number>600</number>
</property> </property>
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sticky_center" stdset="0"> <property name="sticky_center" stdset="0">
<number>600</number> <number>600</number>
@@ -583,7 +592,7 @@
<number>600</number> <number>600</number>
</property> </property>
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sticky_center" stdset="0"> <property name="sticky_center" stdset="0">
<number>600</number> <number>600</number>
@@ -669,7 +678,7 @@
<number>-230</number> <number>-230</number>
</property> </property>
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sticky_center" stdset="0"> <property name="sticky_center" stdset="0">
<number>-230</number> <number>-230</number>
@@ -774,7 +783,7 @@
<item> <item>
<spacer name="spacer_fading_1"> <spacer name="spacer_fading_1">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -825,7 +834,7 @@
<item> <item>
<spacer name="spacer_fading_duration_1"> <spacer name="spacer_fading_duration_1">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -843,7 +852,7 @@
<item> <item>
<spacer name="spacer_bottom"> <spacer name="spacer_bottom">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>

View File

@@ -23,10 +23,10 @@
<item> <item>
<widget class="QFrame" name="frame_custom_context1"> <widget class="QFrame" name="frame_custom_context1">
<property name="frameShape"> <property name="frameShape">
<enum>QFrame::NoFrame</enum> <enum>QFrame::Shape::NoFrame</enum>
</property> </property>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Plain</enum> <enum>QFrame::Shadow::Plain</enum>
</property> </property>
<property name="lineWidth"> <property name="lineWidth">
<number>0</number> <number>0</number>
@@ -50,10 +50,10 @@
<item> <item>
<widget class="QFrame" name="frame1"> <widget class="QFrame" name="frame1">
<property name="frameShape"> <property name="frameShape">
<enum>QFrame::NoFrame</enum> <enum>QFrame::Shape::NoFrame</enum>
</property> </property>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Plain</enum> <enum>QFrame::Shadow::Plain</enum>
</property> </property>
<property name="lineWidth"> <property name="lineWidth">
<number>0</number> <number>0</number>
@@ -68,7 +68,7 @@
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLineEdit" name="context_custom_text1"> <widget class="QLineEdit" name="context_custom_text1">
<property name="text"> <property name="text">
<string>%title - %artist%</string> <string notr="true">%title - %artist%</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -99,7 +99,7 @@
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="context_custom_text2"> <widget class="QLineEdit" name="context_custom_text2">
<property name="text"> <property name="text">
<string>%album%</string> <string notr="true">%album%</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -128,10 +128,10 @@
<item> <item>
<widget class="QFrame" name="frame_custom_context2"> <widget class="QFrame" name="frame_custom_context2">
<property name="frameShape"> <property name="frameShape">
<enum>QFrame::NoFrame</enum> <enum>QFrame::Shape::NoFrame</enum>
</property> </property>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Plain</enum> <enum>QFrame::Shadow::Plain</enum>
</property> </property>
<property name="lineWidth"> <property name="lineWidth">
<number>0</number> <number>0</number>
@@ -254,7 +254,7 @@
<item row="2" column="1"> <item row="2" column="1">
<widget class="QTextEdit" name="preview_headline"> <widget class="QTextEdit" name="preview_headline">
<property name="textInteractionFlags"> <property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> <set>Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse</set>
</property> </property>
</widget> </widget>
</item> </item>
@@ -313,7 +313,7 @@
<item row="2" column="1"> <item row="2" column="1">
<widget class="QTextEdit" name="preview_normal"> <widget class="QTextEdit" name="preview_normal">
<property name="textInteractionFlags"> <property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> <set>Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse</set>
</property> </property>
</widget> </widget>
</item> </item>
@@ -323,7 +323,7 @@
<item> <item>
<spacer name="spacer_bottom"> <spacer name="spacer_bottom">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>

View File

@@ -66,7 +66,7 @@
<item> <item>
<spacer name="spacer_providers_updown"> <spacer name="spacer_providers_updown">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -126,7 +126,7 @@
<item> <item>
<spacer name="spacer_button_authenticate"> <spacer name="spacer_button_authenticate">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -176,7 +176,7 @@
<item> <item>
<spacer name="spacer_types_updown"> <spacer name="spacer_types_updown">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -268,7 +268,7 @@
<item> <item>
<spacer name="spacer_cover_filename_bttons"> <spacer name="spacer_cover_filename_bttons">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -284,7 +284,7 @@
<item> <item>
<widget class="QLineEdit" name="lineedit_cover_pattern"> <widget class="QLineEdit" name="lineedit_cover_pattern">
<property name="text"> <property name="text">
<string>%albumartist-%album</string> <string notr="true">%albumartist-%album</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -318,7 +318,7 @@
<item> <item>
<spacer name="spacer_bottom"> <spacer name="spacer_bottom">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -338,9 +338,6 @@
<container>1</container> <container>1</container>
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<resources> <resources/>
<include location="../../data/data.qrc"/>
<include location="../../data/icons.qrc"/>
</resources>
<connections/> <connections/>
</ui> </ui>

View File

@@ -35,7 +35,11 @@
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QComboBox" name="moodbar_style"/> <widget class="QComboBox" name="moodbar_style">
<property name="currentText">
<string notr="true"/>
</property>
</widget>
</item> </item>
<item row="3" column="0" colspan="2"> <item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="moodbar_save"> <widget class="QCheckBox" name="moodbar_save">
@@ -54,7 +58,7 @@
<item row="4" column="0"> <item row="4" column="0">
<spacer name="spacer_bottom"> <spacer name="spacer_bottom">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>

View File

@@ -64,7 +64,11 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLineEdit" name="proxy_hostname"/> <widget class="QLineEdit" name="proxy_hostname">
<property name="text">
<string notr="true"/>
</property>
</widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="label_port"> <widget class="QLabel" name="label_port">
@@ -98,7 +102,7 @@
</property> </property>
<layout class="QFormLayout" name="formLayout_7"> <layout class="QFormLayout" name="formLayout_7">
<property name="fieldGrowthPolicy"> <property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum> <enum>QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow</enum>
</property> </property>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label_username"> <widget class="QLabel" name="label_username">
@@ -108,7 +112,11 @@
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLineEdit" name="proxy_username"/> <widget class="QLineEdit" name="proxy_username">
<property name="text">
<string notr="true"/>
</property>
</widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label_password"> <widget class="QLabel" name="label_password">
@@ -119,8 +127,11 @@
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="proxy_password"> <widget class="QLineEdit" name="proxy_password">
<property name="text">
<string notr="true"/>
</property>
<property name="echoMode"> <property name="echoMode">
<enum>QLineEdit::Password</enum> <enum>QLineEdit::EchoMode::Password</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -140,7 +151,7 @@
<item> <item>
<spacer name="spacer_bottom"> <spacer name="spacer_bottom">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>

View File

@@ -159,10 +159,10 @@
<item> <item>
<widget class="QFrame" name="frame_custom_notifications"> <widget class="QFrame" name="frame_custom_notifications">
<property name="frameShape"> <property name="frameShape">
<enum>QFrame::NoFrame</enum> <enum>QFrame::Shape::NoFrame</enum>
</property> </property>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Plain</enum> <enum>QFrame::Shadow::Plain</enum>
</property> </property>
<property name="lineWidth"> <property name="lineWidth">
<number>0</number> <number>0</number>
@@ -209,10 +209,10 @@
<item> <item>
<widget class="QFrame" name="frame"> <widget class="QFrame" name="frame">
<property name="frameShape"> <property name="frameShape">
<enum>QFrame::NoFrame</enum> <enum>QFrame::Shape::NoFrame</enum>
</property> </property>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Plain</enum> <enum>QFrame::Shadow::Plain</enum>
</property> </property>
<property name="lineWidth"> <property name="lineWidth">
<number>0</number> <number>0</number>
@@ -229,6 +229,9 @@
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="text">
<string notr="true"/>
</property>
</widget> </widget>
</item> </item>
<item row="0" column="2"> <item row="0" column="2">
@@ -263,6 +266,9 @@
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="text">
<string notr="true"/>
</property>
</widget> </widget>
</item> </item>
<item row="1" column="2"> <item row="1" column="2">
@@ -293,7 +299,7 @@
<item row="0" column="1" colspan="2"> <item row="0" column="1" colspan="2">
<widget class="QSlider" name="notifications_opacity"> <widget class="QSlider" name="notifications_opacity">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -364,7 +370,7 @@
<item> <item>
<spacer name="spacer_bottom"> <spacer name="spacer_bottom">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>

View File

@@ -60,7 +60,11 @@
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="app_id"/> <widget class="QLineEdit" name="app_id">
<property name="text">
<string notr="true"/>
</property>
</widget>
</item> </item>
<item row="4" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label_username"> <widget class="QLabel" name="label_username">
@@ -72,7 +76,11 @@
<item row="4" column="1"> <item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QLineEdit" name="username"/> <widget class="QLineEdit" name="username">
<property name="text">
<string notr="true"/>
</property>
</widget>
</item> </item>
</layout> </layout>
</item> </item>
@@ -85,8 +93,11 @@
</item> </item>
<item row="5" column="1"> <item row="5" column="1">
<widget class="QLineEdit" name="password"> <widget class="QLineEdit" name="password">
<property name="text">
<string notr="true"/>
</property>
<property name="echoMode"> <property name="echoMode">
<enum>QLineEdit::Password</enum> <enum>QLineEdit::EchoMode::Password</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -98,7 +109,11 @@
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QLineEdit" name="app_secret"/> <widget class="QLineEdit" name="app_secret">
<property name="text">
<string notr="true"/>
</property>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
@@ -127,7 +142,11 @@
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QComboBox" name="format"/> <widget class="QComboBox" name="format">
<property name="currentText">
<string notr="true"/>
</property>
</widget>
</item> </item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_searchdelay"> <widget class="QLabel" name="label_searchdelay">
@@ -235,7 +254,7 @@
<item> <item>
<spacer name="spacer_middle"> <spacer name="spacer_middle">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -250,7 +269,7 @@
<item> <item>
<spacer name="spacer_bottom"> <spacer name="spacer_bottom">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>

View File

@@ -68,13 +68,20 @@
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QLineEdit" name="password"> <widget class="QLineEdit" name="password">
<property name="text">
<string notr="true"/>
</property>
<property name="echoMode"> <property name="echoMode">
<enum>QLineEdit::Password</enum> <enum>QLineEdit::EchoMode::Password</enum>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="username"/> <widget class="QLineEdit" name="username">
<property name="text">
<string notr="true"/>
</property>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
@@ -119,7 +126,7 @@
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The GStreamer Spotify plugin is not detected, you will not be able to stream songs from Spotify without it. See: &lt;a href=&quot;https://github.com/strawberrymusicplayer/strawberry/wiki/GStreamer-Spotify-plugin&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;https://github.com/strawberrymusicplayer/strawberry/wiki/GStreamer-Spotify-plugin&lt;/span&gt;&lt;/a&gt; for instructions on how to install the plugin.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The GStreamer Spotify plugin is not detected, you will not be able to stream songs from Spotify without it. See: &lt;a href=&quot;https://github.com/strawberrymusicplayer/strawberry/wiki/GStreamer-Spotify-plugin&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;https://github.com/strawberrymusicplayer/strawberry/wiki/GStreamer-Spotify-plugin&lt;/span&gt;&lt;/a&gt; for instructions on how to install the plugin.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="alignment"> <property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> <set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property> </property>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>
@@ -247,7 +254,7 @@
<item> <item>
<spacer name="spacer_middle"> <spacer name="spacer_middle">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -262,7 +269,7 @@
<item> <item>
<spacer name="spacer_bottom"> <spacer name="spacer_bottom">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -314,7 +321,6 @@
<tabstop>checkbox_fetchalbums</tabstop> <tabstop>checkbox_fetchalbums</tabstop>
</tabstops> </tabstops>
<resources> <resources>
<include location="../../data/data.qrc"/>
<include location="../../data/icons.qrc"/> <include location="../../data/icons.qrc"/>
</resources> </resources>
<connections/> <connections/>

View File

@@ -67,7 +67,11 @@
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="client_id"/> <widget class="QLineEdit" name="client_id">
<property name="text">
<string notr="true"/>
</property>
</widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_api_token"> <widget class="QLabel" name="label_api_token">
@@ -90,6 +94,9 @@
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
<property name="text">
<string notr="true"/>
</property>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="3" column="0">
@@ -102,7 +109,11 @@
<item row="3" column="1"> <item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QLineEdit" name="username"/> <widget class="QLineEdit" name="username">
<property name="text">
<string notr="true"/>
</property>
</widget>
</item> </item>
</layout> </layout>
</item> </item>
@@ -115,8 +126,11 @@
</item> </item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="QLineEdit" name="password"> <widget class="QLineEdit" name="password">
<property name="text">
<string notr="true"/>
</property>
<property name="echoMode"> <property name="echoMode">
<enum>QLineEdit::Password</enum> <enum>QLineEdit::EchoMode::Password</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -147,7 +161,11 @@
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QComboBox" name="quality"/> <widget class="QComboBox" name="quality">
<property name="currentText">
<string notr="true"/>
</property>
</widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label_searchdelay"> <widget class="QLabel" name="label_searchdelay">
@@ -237,7 +255,11 @@
</widget> </widget>
</item> </item>
<item row="7" column="1"> <item row="7" column="1">
<widget class="QComboBox" name="coversize"/> <widget class="QComboBox" name="coversize">
<property name="currentText">
<string notr="true"/>
</property>
</widget>
</item> </item>
<item row="7" column="0"> <item row="7" column="0">
<widget class="QLabel" name="label_coversize"> <widget class="QLabel" name="label_coversize">
@@ -267,7 +289,11 @@
</widget> </widget>
</item> </item>
<item row="8" column="1"> <item row="8" column="1">
<widget class="QComboBox" name="streamurl"/> <widget class="QComboBox" name="streamurl">
<property name="currentText">
<string notr="true"/>
</property>
</widget>
</item> </item>
<item row="9" column="0"> <item row="9" column="0">
<widget class="QCheckBox" name="checkbox_album_explicit"> <widget class="QCheckBox" name="checkbox_album_explicit">
@@ -282,7 +308,7 @@
<item> <item>
<spacer name="spacer_middle"> <spacer name="spacer_middle">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -297,7 +323,7 @@
<item> <item>
<spacer name="spacer_bottom"> <spacer name="spacer_bottom">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -357,7 +383,6 @@
<tabstop>streamurl</tabstop> <tabstop>streamurl</tabstop>
</tabstops> </tabstops>
<resources> <resources>
<include location="../../data/data.qrc"/>
<include location="../../data/icons.qrc"/> <include location="../../data/icons.qrc"/>
</resources> </resources>
<connections/> <connections/>

View File

@@ -90,7 +90,7 @@ struct SuitableElement {
}; };
GstElement *Transcoder::CreateElementForMimeType(const QString &element_type, const QString &mime_type, GstElement *bin) { GstElement *Transcoder::CreateElementForMimeType(GstElementFactoryListType element_type, const QString &mime_type, GstElement *bin) {
if (mime_type.isEmpty()) return nullptr; if (mime_type.isEmpty()) return nullptr;
@@ -113,31 +113,15 @@ GstElement *Transcoder::CreateElementForMimeType(const QString &element_type, co
GstElementFactory *factory = GST_ELEMENT_FACTORY(f->data); GstElementFactory *factory = GST_ELEMENT_FACTORY(f->data);
// Is this the right type of plugin? // Is this the right type of plugin?
if (QString::fromUtf8(gst_element_factory_get_metadata(factory, GST_ELEMENT_METADATA_KLASS)).contains(element_type)) { if (gst_element_factory_list_is_type(factory, element_type)) {
const GList *const templates = gst_element_factory_get_static_pad_templates(factory); // check if the element factory supports the target caps
for (const GList *t = templates; t; t = g_list_next(t)) { if (gst_element_factory_can_src_any_caps(factory, target_caps)) {
// Only interested in source pads const QString name = QString::fromUtf8(GST_OBJECT_NAME(factory));
GstStaticPadTemplate *pad_template = reinterpret_cast<GstStaticPadTemplate*>(t->data); int rank = static_cast<int>(gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(factory)));
if (pad_template->direction != GST_PAD_SRC) continue; if (name.startsWith(QLatin1String("avmux")) || name.startsWith(QLatin1String("avenc"))) {
rank = -1; // ffmpeg usually sucks
// Does this pad support the mime type we want?
GstCaps *caps = gst_static_pad_template_get_caps(pad_template);
GstCaps *intersection = gst_caps_intersect(caps, target_caps);
gst_caps_unref(caps);
if (intersection) {
if (!gst_caps_is_empty(intersection)) {
int rank = static_cast<int>(gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(factory)));
const QString name = QString::fromUtf8(GST_OBJECT_NAME(factory));
if (name.startsWith(QLatin1String("ffmux")) || name.startsWith(QLatin1String("ffenc"))) {
rank = -1; // ffmpeg usually sucks
}
suitable_elements_ << SuitableElement(name, rank);
}
gst_caps_unref(intersection);
} }
suitable_elements_ << SuitableElement(name, rank);
} }
} }
} }
@@ -432,8 +416,8 @@ bool Transcoder::StartJob(const Job &job) {
GstElement *decode = CreateElement(QStringLiteral("decodebin"), state->pipeline_); GstElement *decode = CreateElement(QStringLiteral("decodebin"), state->pipeline_);
GstElement *convert = CreateElement(QStringLiteral("audioconvert"), state->pipeline_); GstElement *convert = CreateElement(QStringLiteral("audioconvert"), state->pipeline_);
GstElement *resample = CreateElement(QStringLiteral("audioresample"), state->pipeline_); GstElement *resample = CreateElement(QStringLiteral("audioresample"), state->pipeline_);
GstElement *codec = CreateElementForMimeType(QStringLiteral("Codec/Encoder/Audio"), job.preset.codec_mimetype_, state->pipeline_); GstElement *codec = CreateElementForMimeType(GST_ELEMENT_FACTORY_TYPE_AUDIO_ENCODER, job.preset.codec_mimetype_, state->pipeline_);
GstElement *muxer = CreateElementForMimeType(QStringLiteral("Codec/Muxer"), job.preset.muxer_mimetype_, state->pipeline_); GstElement *muxer = CreateElementForMimeType(GST_ELEMENT_FACTORY_TYPE_MUXER, job.preset.muxer_mimetype_, state->pipeline_);
GstElement *sink = CreateElement(QStringLiteral("filesink"), state->pipeline_); GstElement *sink = CreateElement(QStringLiteral("filesink"), state->pipeline_);
if (!src || !decode || !convert || !sink) return false; if (!src || !decode || !convert || !sink) return false;

View File

@@ -133,7 +133,7 @@ class Transcoder : public QObject {
bool StartJob(const Job &job); bool StartJob(const Job &job);
GstElement *CreateElement(const QString &factory_name, GstElement *bin = nullptr, const QString &name = QString()); GstElement *CreateElement(const QString &factory_name, GstElement *bin = nullptr, const QString &name = QString());
GstElement *CreateElementForMimeType(const QString &element_type, const QString &mime_type, GstElement *bin = nullptr); GstElement *CreateElementForMimeType(GstElementFactoryListType element_type, const QString &mime_type, GstElement *bin = nullptr);
void SetElementProperties(const QString &name, GObject *object); void SetElementProperties(const QString &name, GObject *object);
static void NewPadCallback(GstElement*, GstPad *pad, gpointer data); static void NewPadCallback(GstElement*, GstPad *pad, gpointer data);

View File

@@ -1,119 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2019-2023, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
*
* 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 <QString>
#include "searchparserutils.h"
namespace Utilities {
// Try and parse the string as '[[h:]m:]s' (ignoring all spaces),
// and return the number of seconds if it parses correctly.
// If not, the original string is returned.
// The 'h', 'm' and 's' components can have any length (including 0).
// A few examples:
// "::" is parsed to "0"
// "1::" is parsed to "3600"
// "3:45" is parsed to "225"
// "1:165" is parsed to "225"
// "225" is parsed to "225" (srsly! ^.^)
// "2:3:4:5" is parsed to "2:3:4:5"
// "25m" is parsed to "25m"
int ParseSearchTime(const QString &time_str) {
int seconds = 0;
int accum = 0;
int colon_count = 0;
for (const QChar &c : time_str) {
if (c.isDigit()) {
accum = accum * 10 + c.digitValue();
}
else if (c == QLatin1Char(':')) {
seconds = seconds * 60 + accum;
accum = 0;
++colon_count;
if (colon_count > 2) {
return 0;
}
}
else if (!c.isSpace()) {
return 0;
}
}
seconds = seconds * 60 + accum;
return seconds;
}
// Parses a rating search term to float.
// If the rating is a number from 0-5, map it to 0-1
// To use float values directly, the search term can be prefixed with "f" (rating:>f0.2)
// If search string is 0, or by default, uses -1
// @param rating_str: Rating search 0-5, or "f0.2"
// @return float: rating from 0-1 or -1 if not rated.
float ParseSearchRating(const QString &rating_str) {
if (rating_str.isEmpty()) {
return -1;
}
float rating = -1.0F;
// Check if the search is a float
if (rating_str.contains(QLatin1Char('f'), Qt::CaseInsensitive)) {
if (rating_str.count(QLatin1Char('f'), Qt::CaseInsensitive) > 1) {
return rating;
}
QString rating_float_str = rating_str;
if (rating_str.at(0) == QLatin1Char('f') || rating_str.at(0) == QLatin1Char('F')) {
rating_float_str = rating_float_str.remove(0, 1);
}
if (rating_str.right(1) == QLatin1Char('f') || rating_str.right(1) == QLatin1Char('F')) {
rating_float_str.chop(1);
}
bool ok = false;
const float rating_input = rating_float_str.toFloat(&ok);
if (ok) {
rating = rating_input;
}
}
else {
bool ok = false;
const int rating_input = rating_str.toInt(&ok);
// Is valid int from 0-5: convert to float
if (ok && rating_input >= 0 && rating_input <= 5) {
rating = static_cast<float>(rating_input) / 5.0F;
}
}
// Songs with zero rating have -1 in the DB
if (rating == 0) {
rating = -1;
}
return rating;
}
} // namespace Utilities

View File

@@ -1,33 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2019-2023, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
*
* 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 SEARCHPARSERUTILS_H
#define SEARCHPARSERUTILS_H
#include <QString>
namespace Utilities {
int ParseSearchTime(const QString &time_str);
float ParseSearchRating(const QString &rating_str);
} // namespace Utilities
#endif // SEARCHPARSERUTILS_H

View File

@@ -42,7 +42,8 @@ constexpr int kMagicNumber = 0x502C9510;
StretchHeaderView::StretchHeaderView(const Qt::Orientation orientation, QWidget *parent) StretchHeaderView::StretchHeaderView(const Qt::Orientation orientation, QWidget *parent)
: QHeaderView(orientation, parent), : QHeaderView(orientation, parent),
stretch_enabled_(false), stretch_enabled_(false),
in_mouse_move_event_(false) { in_mouse_move_event_(false),
forced_resize_logical_index_(-1) {
setDefaultSectionSize(100); setDefaultSectionSize(100);
setMinimumSectionSize(30); setMinimumSectionSize(30);
@@ -144,7 +145,7 @@ bool StretchHeaderView::RestoreState(const QByteArray &state) {
if (i < visual_indices.count()) { if (i < visual_indices.count()) {
moveSection(visualIndex(visual_indices[i]), i); moveSection(visualIndex(visual_indices[i]), i);
} }
if (i < column_pixel_widths.count()) { if (i < column_pixel_widths.count() && column_pixel_widths[i] > 0) {
resizeSection(i, column_pixel_widths[i]); resizeSection(i, column_pixel_widths[i]);
} }
setSectionHidden(i, !columns_visible.contains(i)); setSectionHidden(i, !columns_visible.contains(i));
@@ -170,11 +171,19 @@ bool StretchHeaderView::RestoreState(const QByteArray &state) {
QByteArray StretchHeaderView::ResetState() { QByteArray StretchHeaderView::ResetState() {
stretch_enabled_ = true; stretch_enabled_ = false;
column_widths_.resize(count());
std::fill(column_widths_.begin(), column_widths_.end(), 1.0 / count());
setSortIndicator(0, Qt::AscendingOrder); setSortIndicator(-1, Qt::AscendingOrder);
return QByteArray(); for (int i = 0; i < count(); ++i) {
setSectionHidden(i, false);
resizeSection(i, defaultSectionSize());
moveSection(visualIndex(i), i);
}
return SaveState();
} }
@@ -283,6 +292,12 @@ void StretchHeaderView::ShowSection(const int logical_index) {
} }
else {
if (sectionSize(logical_index) == 0) {
resizeSection(logical_index, defaultSectionSize());
}
}
} }
void StretchHeaderView::HideSection(const int logical_index) { void StretchHeaderView::HideSection(const int logical_index) {
@@ -326,39 +341,48 @@ void StretchHeaderView::SectionResized(const int logical_index, const int old_si
return; return;
} }
if (in_mouse_move_event_) { if (logical_index == forced_resize_logical_index_) {
bool resized = false; forced_resize_logical_index_ = -1;
if (new_size >= minimumSectionSize()) { return;
// Find the visible section to the right of the section that's being resized }
const int visual_index = visualIndex(logical_index);
int right_section_logical_index = -1; if (!in_mouse_move_event_) {
int right_section_visual_index = -1; return;
for (int i = 0; i <= count(); ++i) { }
if (!isSectionHidden(i) &&
visualIndex(i) > visual_index && bool resized = false;
(right_section_visual_index == -1 || visualIndex(i) < right_section_visual_index)) { if (new_size >= minimumSectionSize()) {
right_section_logical_index = i; // Find the visible section to the right of the section that's being resized
right_section_visual_index = visualIndex(i); const int visual_index = visualIndex(logical_index);
} int right_section_logical_index = -1;
} int right_section_visual_index = -1;
if (right_section_logical_index != -1) { for (int i = 0; i <= count(); ++i) {
const int right_section_size = sectionSize(right_section_logical_index) + (old_size - new_size); if (!isSectionHidden(i) &&
if (right_section_size >= minimumSectionSize()) { visualIndex(i) > visual_index &&
column_widths_[logical_index] = static_cast<ColumnWidthType>(new_size) / width(); (right_section_visual_index == -1 || visualIndex(i) < right_section_visual_index)) {
column_widths_[right_section_logical_index] = static_cast<ColumnWidthType>(right_section_size) / width(); right_section_logical_index = i;
in_mouse_move_event_ = false; right_section_visual_index = visualIndex(i);
NormaliseWidths(QList<int>() << right_section_logical_index);
ResizeSections(QList<int>() << right_section_logical_index);
in_mouse_move_event_ = true;
resized = true;
}
} }
} }
if (!resized) { if (right_section_logical_index != -1) {
in_mouse_move_event_ = true; const int right_section_size = sectionSize(right_section_logical_index) + (old_size - new_size);
resizeSection(logical_index, old_size); if (right_section_size >= minimumSectionSize()) {
in_mouse_move_event_ = false; column_widths_[logical_index] = static_cast<ColumnWidthType>(new_size) / width();
column_widths_[right_section_logical_index] = static_cast<ColumnWidthType>(right_section_size) / width();
in_mouse_move_event_ = false;
NormaliseWidths(QList<int>() << right_section_logical_index);
ResizeSections(QList<int>() << right_section_logical_index);
in_mouse_move_event_ = true;
resized = true;
}
} }
} }
if (!resized) {
forced_resize_logical_index_ = logical_index;
in_mouse_move_event_ = false;
resizeSection(logical_index, old_size);
in_mouse_move_event_ = true;
}
} }

View File

@@ -95,6 +95,7 @@ class StretchHeaderView : public QHeaderView {
QVector<ColumnWidthType> column_widths_; QVector<ColumnWidthType> column_widths_;
bool in_mouse_move_event_; bool in_mouse_move_event_;
int forced_resize_logical_index_;
}; };
#endif // STRETCHHEADERVIEW_H #endif // STRETCHHEADERVIEW_H

View File

@@ -3,14 +3,14 @@
// clazy:excludeall=returning-void-expression // clazy:excludeall=returning-void-expression
TEST(SqliteTest, FTS5SupportEnabled) { TEST(SqliteTest, CreateTableTest) {
sqlite3* db = nullptr; sqlite3 *db = nullptr;
int rc = sqlite3_open(":memory:", &db); int rc = sqlite3_open(":memory:", &db);
ASSERT_EQ(0, rc); ASSERT_EQ(0, rc);
char* errmsg = nullptr; char *errmsg = nullptr;
rc = sqlite3_exec(db, "CREATE VIRTUAL TABLE foo USING fts5(content, TEXT, tokenize = 'unicode61 remove_diacritics 0')", nullptr, nullptr, &errmsg); rc = sqlite3_exec(db, "CREATE TABLE foo (content TEXT)", nullptr, nullptr, &errmsg);
ASSERT_EQ(0, rc) << errmsg; ASSERT_EQ(0, rc) << errmsg;
sqlite3_close(db); sqlite3_close(db);