Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
464fde1851 | ||
|
|
2639498642 | ||
|
|
49f074737c | ||
|
|
3d53a8b434 | ||
|
|
e260433c7a | ||
|
|
88ef8bff0b | ||
|
|
fbdac36f6f | ||
|
|
da3876bd83 | ||
|
|
d303e700ae | ||
|
|
92a1173b9e | ||
|
|
9f7ebb1ac7 | ||
|
|
1a8690e1f2 | ||
|
|
6543e4c5da | ||
|
|
dd904fe3c2 | ||
|
|
c14cc6bf0b | ||
|
|
95c265ffd3 | ||
|
|
31c1ae68df | ||
|
|
f2eb0c3b6b | ||
|
|
32be33847c | ||
|
|
3100b0c044 | ||
|
|
f4ec3ab379 | ||
|
|
cdd7faa9bb | ||
|
|
e7b35aeaf7 | ||
|
|
696256eb5b | ||
|
|
8ad560ce0e | ||
|
|
1c71506f62 | ||
|
|
8bea6ec5b0 | ||
|
|
e8144487ee | ||
|
|
41d9d15dda | ||
|
|
124b97c024 | ||
|
|
98e0b45403 | ||
|
|
1f2b8d8bf6 | ||
|
|
8327751b91 | ||
|
|
6417f89596 | ||
|
|
2e53656f44 | ||
|
|
822cf0ad07 | ||
|
|
67f04a81b3 | ||
|
|
9232ad0125 | ||
|
|
0de87b3e1e | ||
|
|
74b8cd6156 | ||
|
|
ac959387fe | ||
|
|
ffd8ce9281 | ||
|
|
d8052b295f | ||
|
|
625929133c | ||
|
|
79c28e7e1d | ||
|
|
01f4a79f07 | ||
|
|
47c5a2215e | ||
|
|
bb6e38630f |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -171,7 +171,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
fedora_version: [ '39', '40' ]
|
||||
fedora_version: [ '39', '40', '41' ]
|
||||
container:
|
||||
image: fedora:${{matrix.fedora_version}}
|
||||
steps:
|
||||
@@ -544,7 +544,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'focal', 'jammy', 'mantic', 'noble' ]
|
||||
ubuntu_version: [ 'focal', 'jammy', 'noble', 'oracular' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -633,7 +633,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'focal', 'jammy', 'mantic', 'noble' ]
|
||||
ubuntu_version: [ 'focal', 'jammy', 'noble', 'oracular' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
|
||||
132
.gitignore
vendored
132
.gitignore
vendored
@@ -1,120 +1,16 @@
|
||||
# This file is used to ignore files which are generated
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# Build
|
||||
build/
|
||||
bin/
|
||||
|
||||
# CMake
|
||||
CMakeLists.txt.user
|
||||
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
|
||||
/build
|
||||
/bin
|
||||
/CMakeLists.txt.user
|
||||
/.kdev4
|
||||
/strawberry.kdev4
|
||||
/.vscode
|
||||
/.code-workspace
|
||||
/.sublime-workspace
|
||||
/.idea
|
||||
/.vs
|
||||
/out
|
||||
|
||||
# CLion
|
||||
/.idea
|
||||
/CMakeSettings.json
|
||||
/dist/scripts/maketarball.sh
|
||||
/dist/unix/strawberry.spec
|
||||
/dist/windows/strawberry.nsi
|
||||
src/translations/translations.pot
|
||||
|
||||
@@ -105,7 +105,10 @@ find_package(Backtrace)
|
||||
if(Backtrace_FOUND)
|
||||
set(HAVE_BACKTRACE ON)
|
||||
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(Protobuf CONFIG)
|
||||
if(NOT Protobuf_FOUND)
|
||||
|
||||
20
Changelog
20
Changelog
@@ -2,7 +2,19 @@ Strawberry Music Player
|
||||
=======================
|
||||
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:
|
||||
* 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.
|
||||
* (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 update window blocking sponsor window on startup.
|
||||
|
||||
Enhancements:
|
||||
* Improve error messages when connecting and copying to devices.
|
||||
* Allow enter to be used with multiselection to add songs to playlist (#1360)
|
||||
* Add song progress to taskbar using D-Bus.
|
||||
* 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 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).
|
||||
@@ -54,6 +65,9 @@ Version 1.1.0-rc1:
|
||||
* (Windows MSVC) Add back WASAPI2.
|
||||
|
||||
New features:
|
||||
* Letras lyrics provider.
|
||||
* Open Tidal API (openapi.tidal.com) cover provider.
|
||||
* Turbine analyzer.
|
||||
* WaveRubber analyzer.
|
||||
* Spotify streaming support.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ macro(add_pot outfiles header pot)
|
||||
add_custom_command(
|
||||
OUTPUT ${pot}
|
||||
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}
|
||||
DEPENDS ${add_pot_sources} ${header}
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 1)
|
||||
set(STRAWBERRY_VERSION_PATCH 0)
|
||||
set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
set(STRAWBERRY_VERSION_PATCH 1)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
|
||||
|
||||
@@ -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 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;
|
||||
|
||||
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS 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 (
|
||||
path TEXT NOT NULL,
|
||||
|
||||
@@ -50,6 +50,8 @@
|
||||
</screenshots>
|
||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||
<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.22" date="2023-12-09"/>
|
||||
<release version="1.0.21" date="2023-10-21"/>
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
@@ -64,7 +62,7 @@ class TagReaderTagLib : public TagReaderBase {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -77,9 +75,9 @@ class TagReaderTagLib : public TagReaderBase {
|
||||
|
||||
static inline void AssignTagLibStringToStdString(const TagLib::String &tstr, std::string *output) {
|
||||
|
||||
std::string stdstr = TagLibStringToStdString(tstr);
|
||||
boost::trim(stdstr);
|
||||
output->assign(stdstr);
|
||||
const QString qstr = TagLibStringToQString(tstr).trimmed();
|
||||
const QByteArray data = qstr.toUtf8();
|
||||
output->assign(data.constData(), data.size());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,11 @@ set(SOURCES
|
||||
utilities/filemanagerutils.cpp
|
||||
utilities/coverutils.cpp
|
||||
utilities/screenutils.cpp
|
||||
utilities/searchparserutils.cpp
|
||||
utilities/textencodingutils.cpp
|
||||
|
||||
filterparser/filterparser.cpp
|
||||
filterparser/filtertree.cpp
|
||||
|
||||
engine/enginebase.cpp
|
||||
engine/enginedevice.cpp
|
||||
engine/devicefinders.cpp
|
||||
@@ -72,9 +74,10 @@ set(SOURCES
|
||||
analyzer/analyzercontainer.cpp
|
||||
analyzer/blockanalyzer.cpp
|
||||
analyzer/boomanalyzer.cpp
|
||||
analyzer/turbineanalyzer.cpp
|
||||
analyzer/sonogramanalyzer.cpp
|
||||
analyzer/waverubberanalyzer.cpp
|
||||
analyzer/rainbowanalyzer.cpp
|
||||
analyzer/sonogram.cpp
|
||||
analyzer/waverubber.cpp
|
||||
|
||||
equalizer/equalizer.cpp
|
||||
equalizer/equalizerslider.cpp
|
||||
@@ -95,7 +98,6 @@ set(SOURCES
|
||||
collection/collectionfilter.cpp
|
||||
collection/collectionplaylistitem.cpp
|
||||
collection/collectionquery.cpp
|
||||
collection/collectionqueryoptions.cpp
|
||||
collection/savedgroupingmanager.cpp
|
||||
collection/groupbydialog.cpp
|
||||
collection/collectiontask.cpp
|
||||
@@ -106,7 +108,6 @@ set(SOURCES
|
||||
playlist/playlistcontainer.cpp
|
||||
playlist/playlistdelegates.cpp
|
||||
playlist/playlistfilter.cpp
|
||||
playlist/playlistfilterparser.cpp
|
||||
playlist/playlistheader.cpp
|
||||
playlist/playlistitem.cpp
|
||||
playlist/playlistlistcontainer.cpp
|
||||
@@ -331,9 +332,10 @@ set(HEADERS
|
||||
analyzer/analyzercontainer.h
|
||||
analyzer/blockanalyzer.h
|
||||
analyzer/boomanalyzer.h
|
||||
analyzer/turbineanalyzer.h
|
||||
analyzer/sonogramanalyzer.h
|
||||
analyzer/waverubberanalyzer.h
|
||||
analyzer/rainbowanalyzer.h
|
||||
analyzer/sonogram.h
|
||||
analyzer/waverubber.h
|
||||
|
||||
equalizer/equalizer.h
|
||||
equalizer/equalizerslider.h
|
||||
|
||||
@@ -39,9 +39,10 @@
|
||||
#include "analyzerbase.h"
|
||||
#include "blockanalyzer.h"
|
||||
#include "boomanalyzer.h"
|
||||
#include "turbineanalyzer.h"
|
||||
#include "sonogramanalyzer.h"
|
||||
#include "waverubberanalyzer.h"
|
||||
#include "rainbowanalyzer.h"
|
||||
#include "sonogram.h"
|
||||
#include "waverubber.h"
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/shared_ptr.h"
|
||||
@@ -88,10 +89,11 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
|
||||
|
||||
AddAnalyzerType<BlockAnalyzer>();
|
||||
AddAnalyzerType<BoomAnalyzer>();
|
||||
AddAnalyzerType<NyanCatAnalyzer>();
|
||||
AddAnalyzerType<TurbineAnalyzer>();
|
||||
AddAnalyzerType<SonogramAnalyzer>();
|
||||
AddAnalyzerType<WaveRubberAnalyzer>();
|
||||
AddAnalyzerType<RainbowDashAnalyzer>();
|
||||
AddAnalyzerType<Sonogram>();
|
||||
AddAnalyzerType<WaveRubber>();
|
||||
AddAnalyzerType<NyanCatAnalyzer>();
|
||||
|
||||
disable_action_ = context_menu_->addAction(tr("No analyzer"), this, &AnalyzerContainer::DisableAnalyzer);
|
||||
disable_action_->setCheckable(true);
|
||||
|
||||
@@ -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
|
||||
// . . . . # .
|
||||
|
||||
@@ -54,7 +54,7 @@ class BlockAnalyzer : public AnalyzerBase {
|
||||
|
||||
protected:
|
||||
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;
|
||||
virtual void paletteChange(const QPalette&);
|
||||
void framerateChanged() override;
|
||||
|
||||
@@ -45,7 +45,7 @@ class BoomAnalyzer : public AnalyzerBase {
|
||||
static const char *kName;
|
||||
|
||||
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:
|
||||
void changeK_barHeight(int);
|
||||
|
||||
@@ -41,18 +41,21 @@
|
||||
#include "fht.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::kWidth[] = { 34, 53 };
|
||||
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 char *NyanCatAnalyzer::kName = "Nyanalyzer Cat";
|
||||
const char *RainbowDashAnalyzer::kName = "Rainbow Dash";
|
||||
const float RainbowAnalyzer::kPixelScale = 0.02F;
|
||||
|
||||
RainbowAnalyzer::RainbowType RainbowAnalyzer::rainbowtype;
|
||||
namespace {
|
||||
constexpr int kFrameIntervalMs = 150;
|
||||
constexpr int kRainbowHeight[] = { 21, 16 };
|
||||
constexpr int kRainbowOverlap[] = { 13, 15 };
|
||||
constexpr float kPixelScale = 0.02F;
|
||||
} // namespace
|
||||
|
||||
RainbowAnalyzer::RainbowAnalyzer(const RainbowType rbtype, QWidget *parent)
|
||||
: 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
|
||||
const int scope_size = static_cast<int>(s.size() / 2);
|
||||
|
||||
@@ -49,44 +49,37 @@ class RainbowAnalyzer : public AnalyzerBase {
|
||||
Dash = 1
|
||||
};
|
||||
|
||||
RainbowAnalyzer(const RainbowType rbtype, QWidget *parent);
|
||||
explicit RainbowAnalyzer(const RainbowType rbtype, QWidget *parent);
|
||||
|
||||
protected:
|
||||
void transform(Scope&) override;
|
||||
void analyze(QPainter &p, const Scope&, bool new_frame) override;
|
||||
void transform(Scope &s) override;
|
||||
void analyze(QPainter &p, const Scope &s, const bool new_frame) override;
|
||||
|
||||
void timerEvent(QTimerEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
static const int kRainbowBands = 6;
|
||||
static const int kHistorySize = 128;
|
||||
static RainbowType rainbowtype;
|
||||
static const int kHeight[];
|
||||
static const int kWidth[];
|
||||
static const int kFrameCount[];
|
||||
static const int kRainbowHeight[];
|
||||
static const int kRainbowOverlap[];
|
||||
static const int kSleepingHeight[];
|
||||
|
||||
static const int kHistorySize = 128;
|
||||
static const int kRainbowBands = 6;
|
||||
static const float kPixelScale;
|
||||
|
||||
static const int kFrameIntervalMs = 150;
|
||||
|
||||
static RainbowType rainbowtype;
|
||||
|
||||
inline QRect SourceRect(RainbowType _rainbowtype) const {
|
||||
inline QRect SourceRect(const RainbowType _rainbowtype) const {
|
||||
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]);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,14 +26,14 @@
|
||||
|
||||
#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) {}
|
||||
|
||||
void Sonogram::resizeEvent(QResizeEvent *e) {
|
||||
void SonogramAnalyzer::resizeEvent(QResizeEvent *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) {
|
||||
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_->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_);
|
||||
}
|
||||
@@ -21,24 +21,25 @@
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SONOGRAM_H
|
||||
#define SONOGRAM_H
|
||||
#ifndef SONOGRAMANALYZER_H
|
||||
#define SONOGRAMANALYZER_H
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
|
||||
#include "analyzerbase.h"
|
||||
|
||||
class Sonogram : public AnalyzerBase {
|
||||
class SonogramAnalyzer : public AnalyzerBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE explicit Sonogram(QWidget *parent);
|
||||
Q_INVOKABLE explicit SonogramAnalyzer(QWidget *parent);
|
||||
|
||||
static const char *kName;
|
||||
|
||||
protected:
|
||||
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 demo(QPainter &p) override;
|
||||
|
||||
@@ -46,4 +47,4 @@ class Sonogram : public AnalyzerBase {
|
||||
QPixmap canvas_;
|
||||
};
|
||||
|
||||
#endif // SONOGRAM_H
|
||||
#endif // SONOGRAMANALYZER_H
|
||||
100
src/analyzer/turbineanalyzer.cpp
Normal file
100
src/analyzer/turbineanalyzer.cpp
Normal 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_);
|
||||
|
||||
}
|
||||
41
src/analyzer/turbineanalyzer.h
Normal file
41
src/analyzer/turbineanalyzer.h
Normal 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
|
||||
@@ -19,14 +19,14 @@
|
||||
#include <QPainter>
|
||||
#include <QResizeEvent>
|
||||
#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) {}
|
||||
|
||||
void WaveRubber::resizeEvent(QResizeEvent *e) {
|
||||
void WaveRubberAnalyzer::resizeEvent(QResizeEvent *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) {
|
||||
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
|
||||
Q_UNUSED(s);
|
||||
}
|
||||
|
||||
void WaveRubber::demo(QPainter &p) {
|
||||
void WaveRubberAnalyzer::demo(QPainter &p) {
|
||||
analyze(p, Scope(fht_->size(), 0), new_frame_);
|
||||
}
|
||||
@@ -22,11 +22,11 @@
|
||||
|
||||
#include "analyzerbase.h"
|
||||
|
||||
class WaveRubber : public AnalyzerBase {
|
||||
class WaveRubberAnalyzer : public AnalyzerBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE explicit WaveRubber(QWidget *parent);
|
||||
Q_INVOKABLE explicit WaveRubberAnalyzer(QWidget *parent);
|
||||
|
||||
static const char *kName;
|
||||
|
||||
@@ -19,29 +19,31 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QVariant>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "utilities/timeconstants.h"
|
||||
#include "utilities/searchparserutils.h"
|
||||
#include <QUrl>
|
||||
|
||||
#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 "collectionmodel.h"
|
||||
#include "collectionitem.h"
|
||||
|
||||
const QStringList CollectionFilter::Operators = QStringList() << QStringLiteral(":")
|
||||
<< QStringLiteral("=")
|
||||
<< QStringLiteral("==")
|
||||
<< QStringLiteral("<>")
|
||||
<< QStringLiteral("<")
|
||||
<< QStringLiteral("<=")
|
||||
<< QStringLiteral(">")
|
||||
<< QStringLiteral(">=");
|
||||
CollectionFilter::CollectionFilter(QObject *parent) : QSortFilterProxyModel(parent), query_hash_(0) {
|
||||
|
||||
CollectionFilter::CollectionFilter(QObject *parent) : QSortFilterProxyModel(parent) {}
|
||||
setSortLocaleAware(true);
|
||||
setDynamicSortFilter(true);
|
||||
setRecursiveFilteringEnabled(true);
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
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)
|
||||
QString filter_text = filterRegularExpression().pattern().remove(QLatin1Char('\\'));
|
||||
const size_t hash = qHash(filter_string_);
|
||||
#else
|
||||
QString filter_text = filterRegExp().pattern();
|
||||
const uint hash = qHash(filter_string_);
|
||||
#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)
|
||||
const QStringList tokens = filter_text.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
|
||||
#else
|
||||
const QStringList tokens = filter_text.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts);
|
||||
#endif
|
||||
void CollectionFilter::SetFilterString(const QString &filter_string) {
|
||||
|
||||
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) {
|
||||
const QString &token = tokens[i];
|
||||
if (token.contains(QLatin1Char(':'))) {
|
||||
QString field = token.section(QLatin1Char(':'), 0, 0).remove(QLatin1Char(':')).trimmed();
|
||||
QString value = token.section(QLatin1Char(':'), 1, -1).remove(QLatin1Char(':')).trimmed();
|
||||
if (field.isEmpty() || value.isEmpty()) continue;
|
||||
if (Song::kTextSearchColumns.contains(field, Qt::CaseInsensitive) && value.count(QLatin1Char('"')) <= 2) {
|
||||
bool quotation_mark_start = false;
|
||||
bool quotation_mark_end = false;
|
||||
if (value.left(1) == QLatin1Char('"')) {
|
||||
value.remove(0, 1);
|
||||
quotation_mark_start = true;
|
||||
if (value.length() >= 1 && value.count(QLatin1Char('"')) == 1) {
|
||||
value = value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed();
|
||||
quotation_mark_end = true;
|
||||
}
|
||||
}
|
||||
for (int y = i + 1; y < tokens.count() && !quotation_mark_end; ++y) {
|
||||
QString next_value = tokens[y];
|
||||
if (!quotation_mark_start && ContainsOperators(next_value)) {
|
||||
break;
|
||||
}
|
||||
if (quotation_mark_start && next_value.contains(QLatin1Char('"'))) {
|
||||
next_value = next_value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed();
|
||||
quotation_mark_end = true;
|
||||
}
|
||||
value.append(QLatin1Char(' ') + next_value);
|
||||
i = y;
|
||||
}
|
||||
if (!field.isEmpty() && !value.isEmpty()) {
|
||||
filters.insert(field, Filter(field, value));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
QMimeData *CollectionFilter::mimeData(const QModelIndexList &indexes) const {
|
||||
|
||||
if (indexes.isEmpty()) return nullptr;
|
||||
|
||||
CollectionModel *collection_model = qobject_cast<CollectionModel*>(sourceModel());
|
||||
SongMimeData *data = new SongMimeData;
|
||||
data->backend = collection_model->backend();
|
||||
|
||||
QSet<int> song_ids;
|
||||
QList<QUrl> urls;
|
||||
for (const QModelIndex &idx : indexes) {
|
||||
const QModelIndex source_index = mapToSource(idx);
|
||||
CollectionItem *item = collection_model->IndexToItem(source_index);
|
||||
GetChildSongs(item, song_ids, urls, data->songs);
|
||||
}
|
||||
|
||||
data->setUrls(urls);
|
||||
data->name_for_new_playlist_ = PlaylistManager::GetNameForNewPlaylist(data->songs);
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilter::GetChildSongs(CollectionItem *item, QSet<int> &song_ids, QList<QUrl> &urls, SongList &songs) const {
|
||||
|
||||
CollectionModel *collection_model = qobject_cast<CollectionModel*>(sourceModel());
|
||||
|
||||
switch (item->type) {
|
||||
case CollectionItem::Type::Container:{
|
||||
QList<CollectionItem*> children = item->children;
|
||||
std::sort(children.begin(), children.end(), std::bind(&CollectionModel::CompareItems, collection_model, std::placeholders::_1, std::placeholders::_2));
|
||||
for (CollectionItem *child : children) {
|
||||
GetChildSongs(child, song_ids, urls, songs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (token.contains(operator_regex)) {
|
||||
QRegularExpressionMatch re_match = operator_regex.match(token);
|
||||
if (re_match.hasMatch()) {
|
||||
const QString foperator = re_match.captured(0);
|
||||
const QString field = token.section(foperator, 0, 0).remove(foperator).trimmed();
|
||||
const QString value = token.section(foperator, 1, -1).remove(foperator).trimmed();
|
||||
if (value.isEmpty()) continue;
|
||||
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));
|
||||
}
|
||||
case CollectionItem::Type::Song:{
|
||||
const QModelIndex idx = collection_model->ItemToIndex(item);
|
||||
if (filterAcceptsRow(idx.row(), idx.parent())) {
|
||||
urls << item->metadata.url();
|
||||
if (!song_ids.contains(item->metadata.id())) {
|
||||
song_ids.insert(item->metadata.id());
|
||||
songs << item->metadata;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!filter_text.isEmpty()) filter_text.append(QLatin1Char(' '));
|
||||
filter_text += token;
|
||||
default:
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@@ -22,14 +22,14 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QScopedPointer>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "filterparser/filtertree.h"
|
||||
|
||||
class CollectionItem;
|
||||
|
||||
@@ -39,31 +39,24 @@ class CollectionFilter : public QSortFilterProxyModel {
|
||||
public:
|
||||
explicit CollectionFilter(QObject *parent = nullptr);
|
||||
|
||||
void SetFilterString(const QString &filter_string);
|
||||
QString filter_string() const { return filter_string_; }
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override;
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||
|
||||
private:
|
||||
static const QStringList Operators;
|
||||
struct Filter {
|
||||
public:
|
||||
Filter(const QString &_field = QString(), const QVariant &_value = QVariant(), const QString &_foperator = QString()) : field(_field), value(_value), foperator(_foperator) {}
|
||||
QString field;
|
||||
QVariant value;
|
||||
QString foperator;
|
||||
};
|
||||
using FilterList = QMap<QString, Filter>;
|
||||
static bool ItemMatchesFilters(CollectionItem *item, const FilterList &filters, const QString &filter_text);
|
||||
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);
|
||||
void GetChildSongs(CollectionItem *item, QSet<int> &song_ids, QList<QUrl> &urls, SongList &songs) const;
|
||||
|
||||
private:
|
||||
mutable QScopedPointer<FilterTree> filter_tree_;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
mutable size_t query_hash_;
|
||||
#else
|
||||
mutable uint query_hash_;
|
||||
#endif
|
||||
QString filter_string_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONFILTER_H
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionfilter.h"
|
||||
#include "collectionquery.h"
|
||||
#include "filterparser/filterparser.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "groupbydialog.h"
|
||||
@@ -71,47 +72,19 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
||||
group_by_menu_(nullptr),
|
||||
collection_menu_(nullptr),
|
||||
group_by_group_(nullptr),
|
||||
filter_delay_(new QTimer(this)),
|
||||
timer_filter_delay_(new QTimer(this)),
|
||||
filter_applies_to_model_(true),
|
||||
delay_behaviour_(DelayBehaviour::DelayedOnLargeLibraries) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
QString available_fields = Song::kTextSearchColumns.join(QLatin1String(", "));
|
||||
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(" =, !=, <, >, <="), QLatin1String(">=")) +
|
||||
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>")
|
||||
);
|
||||
ui_->search_field->setToolTip(FilterParser::ToolTip());
|
||||
|
||||
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);
|
||||
filter_delay_->setSingleShot(true);
|
||||
timer_filter_delay_->setInterval(kFilterDelay);
|
||||
timer_filter_delay_->setSingleShot(true);
|
||||
|
||||
// Icons
|
||||
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);
|
||||
|
||||
if (delay) {
|
||||
filter_delay_->start();
|
||||
timer_filter_delay_->start();
|
||||
}
|
||||
else {
|
||||
filter_delay_->stop();
|
||||
timer_filter_delay_->stop();
|
||||
FilterDelayTimeout();
|
||||
}
|
||||
|
||||
@@ -541,7 +514,7 @@ void CollectionFilterWidget::FilterTextChanged(const QString &text) {
|
||||
void CollectionFilterWidget::FilterDelayTimeout() {
|
||||
|
||||
if (filter_applies_to_model_) {
|
||||
filter_->setFilterFixedString(ui_->search_field->text());
|
||||
filter_->SetFilterString(ui_->search_field->text());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ class CollectionFilterWidget : public QWidget {
|
||||
QActionGroup *group_by_group_;
|
||||
QHash<QAction*, int> filter_max_ages_;
|
||||
|
||||
QTimer *filter_delay_;
|
||||
QTimer *timer_filter_delay_;
|
||||
|
||||
bool filter_applies_to_model_;
|
||||
DelayBehaviour delay_behaviour_;
|
||||
|
||||
@@ -100,8 +100,6 @@ CollectionModel::CollectionModel(SharedPtr<CollectionBackend> backend, Applicati
|
||||
|
||||
filter_->setSourceModel(this);
|
||||
filter_->setSortRole(Role_SortText);
|
||||
filter_->setDynamicSortFilter(true);
|
||||
filter_->setSortLocaleAware(true);
|
||||
filter_->sort(0);
|
||||
|
||||
if (app_) {
|
||||
@@ -391,12 +389,13 @@ QVariant CollectionModel::data(const CollectionItem *item, const int role) const
|
||||
Qt::ItemFlags CollectionModel::flags(const QModelIndex &idx) const {
|
||||
|
||||
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::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:
|
||||
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.
|
||||
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(TextOrUnknown(song.effective_albumartist()));
|
||||
has_unique_album_identifier = true;
|
||||
|
||||
@@ -142,6 +142,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
CollectionFilterOptions filter_options;
|
||||
};
|
||||
|
||||
SharedPtr<CollectionBackend> backend() const { return backend_; }
|
||||
CollectionFilter *filter() const { return filter_; }
|
||||
|
||||
void Init();
|
||||
@@ -200,6 +201,8 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
|
||||
void ExpandAll(CollectionItem *item = nullptr) const;
|
||||
|
||||
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
|
||||
|
||||
signals:
|
||||
void TotalSongCountUpdated(const int count);
|
||||
void TotalArtistCountUpdated(const int count);
|
||||
@@ -250,7 +253,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
static QUrl AlbumIconPixmapDiskCacheKey(const QString &cache_key);
|
||||
QVariant AlbumIcon(const QModelIndex &idx);
|
||||
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);
|
||||
|
||||
private slots:
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
|
||||
#include "collectionquery.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "utilities/searchparserutils.h"
|
||||
|
||||
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options)
|
||||
: SqlQuery(db),
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -1071,17 +1071,25 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
#endif
|
||||
|
||||
{
|
||||
bool asked_permission = true;
|
||||
Settings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
constexpr char do_not_show_sponsor_message_key[] = "do_not_show_sponsor_message";
|
||||
const bool do_not_show_sponsor_message = s.value(do_not_show_sponsor_message_key, false).toBool();
|
||||
#ifdef HAVE_QTSPARKLE
|
||||
s.beginGroup("QtSparkle");
|
||||
asked_permission = s.value("asked_permission", false).toBool();
|
||||
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")));
|
||||
#endif
|
||||
if (asked_permission) {
|
||||
s.beginGroup(kSettingsGroup);
|
||||
constexpr char do_not_show_sponsor_message_key[] = "do_not_show_sponsor_message";
|
||||
const bool do_not_show_sponsor_message = s.value(do_not_show_sponsor_message_key, false).toBool();
|
||||
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")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ class Ui_EditTagDialog;
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
class TrackSelectionDialog;
|
||||
class TagFetcher;
|
||||
class LyricsFetcher;
|
||||
#endif
|
||||
class LyricsFetcher;
|
||||
|
||||
class EditTagDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
487
src/filterparser/filterparser.cpp
Normal file
487
src/filterparser/filterparser.cpp
Normal 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(" =, !=, <, >, <="), QLatin1String(">=")) +
|
||||
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>");
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
* 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
|
||||
@@ -18,47 +20,17 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLAYLISTFILTERPARSER_H
|
||||
#define PLAYLISTFILTERPARSER_H
|
||||
#ifndef FILTERPARSER_H
|
||||
#define FILTERPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QSet>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
class QAbstractItemModel;
|
||||
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; }
|
||||
};
|
||||
|
||||
class FilterTree;
|
||||
|
||||
// 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:
|
||||
// expr ::= or-group
|
||||
@@ -72,29 +44,33 @@ class NopFilter : public FilterTree {
|
||||
// col ::= "title" | "artist" | ...
|
||||
class FilterParser {
|
||||
public:
|
||||
explicit FilterParser(const QString &filter, const QMap<QString, int> &columns, const QSet<int> &numerical_cols);
|
||||
explicit FilterParser(const QString &filter_string);
|
||||
|
||||
FilterTree *parse();
|
||||
|
||||
private:
|
||||
static QString ToolTip();
|
||||
|
||||
protected:
|
||||
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
|
||||
bool checkAnd();
|
||||
// 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 *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 end_;
|
||||
QString buf_;
|
||||
const QString filterstring_;
|
||||
const QMap<QString, int> columns_;
|
||||
const QSet<int> numerical_columns_;
|
||||
};
|
||||
|
||||
#endif // PLAYLISTFILTERPARSER_H
|
||||
#endif // FILTERPARSER_H
|
||||
314
src/filterparser/filterparsersearchcomparators.h
Normal file
314
src/filterparser/filterparsersearchcomparators.h
Normal 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
|
||||
52
src/filterparser/filtertree.cpp
Normal file
52
src/filterparser/filtertree.cpp
Normal 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();
|
||||
|
||||
}
|
||||
147
src/filterparser/filtertree.h
Normal file
147
src/filterparser/filtertree.h
Normal 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
|
||||
@@ -710,6 +710,7 @@ void Playlist::set_current_row(const int i, const AutoScroll autoscroll, const b
|
||||
|
||||
if (current_item_index_.isValid() && !is_stopping) {
|
||||
InformOfCurrentSongChange(false);
|
||||
emit dataChanged(index(current_item_index_.row(), 0), index(current_item_index_.row(), ColumnCount - 1));
|
||||
emit MaybeAutoscroll(autoscroll);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/settings.h"
|
||||
#include "filterparser/filterparser.h"
|
||||
#include "playlist.h"
|
||||
#include "playlisttabbar.h"
|
||||
#include "playlistview.h"
|
||||
@@ -124,37 +125,7 @@ PlaylistContainer::PlaylistContainer(QWidget *parent)
|
||||
QObject::connect(ui_->playlist, &PlaylistView::FocusOnFilterSignal, this, &PlaylistContainer::FocusOnFilter);
|
||||
ui_->search_field->installEventFilter(this);
|
||||
|
||||
QString available_fields = PlaylistFilter().column_names().keys().join(QLatin1String(", "));
|
||||
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(" =, !=, <, >, <="), QLatin1String(">=")) +
|
||||
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>")
|
||||
);
|
||||
|
||||
ui_->search_field->setToolTip(FilterParser::ToolTip());
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
@@ -234,7 +205,7 @@ void PlaylistContainer::SetViewModel(Playlist *playlist, const int scroll_positi
|
||||
emit ViewSelectionModelChanged();
|
||||
|
||||
// Update filter
|
||||
ui_->search_field->setText(playlist->filter()->filter_text());
|
||||
ui_->search_field->setText(playlist->filter()->filter_string());
|
||||
|
||||
// Update the no matches label
|
||||
QObject::connect(playlist_->filter(), &QSortFilterProxyModel::modelReset, this, &PlaylistContainer::UpdateNoMatchesLabel);
|
||||
@@ -452,7 +423,7 @@ void PlaylistContainer::UpdateFilter() {
|
||||
|
||||
if (!ui_->toolbar->isVisible()) return;
|
||||
|
||||
manager_->current()->filter()->SetFilterText(ui_->search_field->text());
|
||||
manager_->current()->filter()->SetFilterString(ui_->search_field->text());
|
||||
ui_->playlist->JumpToCurrentlyPlayingTrack();
|
||||
|
||||
UpdateNoMatchesLabel();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -23,12 +23,12 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QAbstractItemModel>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "playlist/playlist.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
#include "filterparser/filterparser.h"
|
||||
#include "filterparser/filtertree.h"
|
||||
#include "playlistfilter.h"
|
||||
#include "playlistfilterparser.h"
|
||||
|
||||
PlaylistFilter::PlaylistFilter(QObject *parent)
|
||||
: QSortFilterProxyModel(parent),
|
||||
@@ -37,40 +37,6 @@ PlaylistFilter::PlaylistFilter(QObject *parent)
|
||||
|
||||
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;
|
||||
@@ -80,29 +46,35 @@ void PlaylistFilter::sort(int column, Qt::SortOrder 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)
|
||||
size_t hash = qHash(filter_text_);
|
||||
const size_t hash = qHash(filter_string_);
|
||||
#else
|
||||
uint hash = qHash(filter_text_);
|
||||
const uint hash = qHash(filter_string_);
|
||||
#endif
|
||||
if (hash != query_hash_) {
|
||||
// Parse the query
|
||||
FilterParser p(filter_text_, column_names_, numerical_columns_);
|
||||
FilterParser p(filter_string_);
|
||||
filter_tree_.reset(p.parse());
|
||||
|
||||
query_hash_ = hash;
|
||||
}
|
||||
|
||||
// Test the row
|
||||
return filter_tree_->accept(row, parent, sourceModel());
|
||||
return filter_tree_->accept(item->Metadata());
|
||||
|
||||
}
|
||||
|
||||
void PlaylistFilter::SetFilterText(const QString &filter_text) {
|
||||
void PlaylistFilter::SetFilterString(const QString &filter_string) {
|
||||
|
||||
filter_text_ = filter_text;
|
||||
setFilterFixedString(filter_text);
|
||||
filter_string_ = filter_string;
|
||||
setFilterFixedString(filter_string);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -24,15 +24,11 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QScopedPointer>
|
||||
#include <QString>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
class FilterTree;
|
||||
#include "filterparser/filtertree.h"
|
||||
|
||||
class PlaylistFilter : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
@@ -48,10 +44,8 @@ class PlaylistFilter : public QSortFilterProxyModel {
|
||||
// public so Playlist::NextVirtualIndex and friends can get at it
|
||||
bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override;
|
||||
|
||||
void SetFilterText(const QString &filter_text);
|
||||
|
||||
QString filter_text() const { return filter_text_; }
|
||||
QMap<QString, int> column_names() const { return column_names_; }
|
||||
void SetFilterString(const QString &filter_string);
|
||||
QString filter_string() const { return filter_string_; }
|
||||
|
||||
private:
|
||||
// Mutable because they're modified from filterAcceptsRow() const
|
||||
@@ -61,10 +55,7 @@ class PlaylistFilter : public QSortFilterProxyModel {
|
||||
#else
|
||||
mutable uint query_hash_;
|
||||
#endif
|
||||
|
||||
QMap<QString, int> column_names_;
|
||||
QSet<int> numerical_columns_;
|
||||
QString filter_text_;
|
||||
QString filter_string_;
|
||||
};
|
||||
|
||||
#endif // PLAYLISTFILTER_H
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -335,6 +335,8 @@ void PlaylistView::RestoreHeaderState() {
|
||||
|
||||
if (set_initial_header_layout_) {
|
||||
|
||||
header_->SetStretchEnabled(true);
|
||||
|
||||
header_->HideSection(static_cast<int>(Playlist::Column::AlbumArtist));
|
||||
header_->HideSection(static_cast<int>(Playlist::Column::Performer));
|
||||
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::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_->SetStretchEnabled(true);
|
||||
|
||||
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Track), 0.03);
|
||||
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Title), 0.24);
|
||||
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::Track), 0.06);
|
||||
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Title), 0.23);
|
||||
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Artist), 0.23);
|
||||
header_->SetColumnWidth(static_cast<int>(Playlist::Column::Album), 0.23);
|
||||
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::Bitdepth), 0.04);
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_style">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -109,7 +109,7 @@
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -151,7 +151,7 @@
|
||||
<item>
|
||||
<widget class="QRadioButton" name="use_strawbs_background">
|
||||
<property name="text">
|
||||
<string>A Taste of Strawbs</string>
|
||||
<string notr="true">A Taste of Strawbs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -169,6 +169,9 @@
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@@ -266,7 +269,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_position">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -318,7 +321,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_background_image_stretch">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -352,10 +355,10 @@
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="tickPosition">
|
||||
<enum>QSlider::TicksBelow</enum>
|
||||
<enum>QSlider::TickPosition::TicksBelow</enum>
|
||||
</property>
|
||||
<property name="tickInterval">
|
||||
<number>10</number>
|
||||
@@ -388,10 +391,10 @@
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="tickPosition">
|
||||
<enum>QSlider::TicksBelow</enum>
|
||||
<enum>QSlider::TickPosition::TicksBelow</enum>
|
||||
</property>
|
||||
<property name="tickInterval">
|
||||
<number>10</number>
|
||||
@@ -528,7 +531,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_icon_sizes">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -585,7 +588,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
|
||||
@@ -50,6 +50,9 @@
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="currentText">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
@@ -64,6 +67,9 @@
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="currentText">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
@@ -71,6 +77,9 @@
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="currentText">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
@@ -140,7 +149,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_alsaplugin">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -181,7 +190,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_exclusive_mode">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -273,7 +282,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_channels">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -384,7 +393,7 @@
|
||||
<item row="2" column="2">
|
||||
<spacer name="spacer_buffer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -397,7 +406,7 @@
|
||||
<item row="1" column="2">
|
||||
<spacer name="spacer_buffer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -410,7 +419,7 @@
|
||||
<item row="0" column="2">
|
||||
<spacer name="spacer_buffer_1">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -434,7 +443,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_buffer_defaults">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -550,7 +559,7 @@
|
||||
<number>600</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sticky_center" stdset="0">
|
||||
<number>600</number>
|
||||
@@ -583,7 +592,7 @@
|
||||
<number>600</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sticky_center" stdset="0">
|
||||
<number>600</number>
|
||||
@@ -669,7 +678,7 @@
|
||||
<number>-230</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sticky_center" stdset="0">
|
||||
<number>-230</number>
|
||||
@@ -774,7 +783,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_fading_1">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -825,7 +834,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_fading_duration_1">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -843,7 +852,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_custom_context1">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
@@ -50,10 +50,10 @@
|
||||
<item>
|
||||
<widget class="QFrame" name="frame1">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
@@ -68,7 +68,7 @@
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="context_custom_text1">
|
||||
<property name="text">
|
||||
<string>%title - %artist%</string>
|
||||
<string notr="true">%title - %artist%</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -99,7 +99,7 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="context_custom_text2">
|
||||
<property name="text">
|
||||
<string>%album%</string>
|
||||
<string notr="true">%album%</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -128,10 +128,10 @@
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_custom_context2">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
@@ -254,7 +254,7 @@
|
||||
<item row="2" column="1">
|
||||
<widget class="QTextEdit" name="preview_headline">
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
<set>Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -313,7 +313,7 @@
|
||||
<item row="2" column="1">
|
||||
<widget class="QTextEdit" name="preview_normal">
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
<set>Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -323,7 +323,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_providers_updown">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -126,7 +126,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_button_authenticate">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -176,7 +176,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_types_updown">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -268,7 +268,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_cover_filename_bttons">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -284,7 +284,7 @@
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineedit_cover_pattern">
|
||||
<property name="text">
|
||||
<string>%albumartist-%album</string>
|
||||
<string notr="true">%albumartist-%album</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -318,7 +318,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -338,9 +338,6 @@
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
<include location="../../data/icons.qrc"/>
|
||||
</resources>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
||||
@@ -35,7 +35,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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 row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="moodbar_save">
|
||||
@@ -54,7 +58,7 @@
|
||||
<item row="4" column="0">
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
|
||||
@@ -64,7 +64,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="proxy_hostname"/>
|
||||
<widget class="QLineEdit" name="proxy_hostname">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_port">
|
||||
@@ -98,7 +102,7 @@
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_7">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
<enum>QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_username">
|
||||
@@ -108,7 +112,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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 row="1" column="0">
|
||||
<widget class="QLabel" name="label_password">
|
||||
@@ -119,8 +127,11 @@
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="proxy_password">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
<enum>QLineEdit::EchoMode::Password</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -140,7 +151,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
|
||||
@@ -159,10 +159,10 @@
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_custom_notifications">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
@@ -209,10 +209,10 @@
|
||||
<item>
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
<enum>QFrame::Shadow::Plain</enum>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
@@ -229,6 +229,9 @@
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
@@ -263,6 +266,9 @@
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
@@ -293,7 +299,7 @@
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QSlider" name="notifications_opacity">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -364,7 +370,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
|
||||
@@ -60,7 +60,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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 row="4" column="0">
|
||||
<widget class="QLabel" name="label_username">
|
||||
@@ -72,7 +76,11 @@
|
||||
<item row="4" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="username"/>
|
||||
<widget class="QLineEdit" name="username">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
@@ -85,8 +93,11 @@
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLineEdit" name="password">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
<enum>QLineEdit::EchoMode::Password</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -98,7 +109,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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>
|
||||
</layout>
|
||||
</widget>
|
||||
@@ -127,7 +142,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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 row="3" column="0">
|
||||
<widget class="QLabel" name="label_searchdelay">
|
||||
@@ -235,7 +254,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_middle">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -250,7 +269,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
|
||||
@@ -68,13 +68,20 @@
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="password">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
<enum>QLineEdit::EchoMode::Password</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="username"/>
|
||||
<widget class="QLineEdit" name="username">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
@@ -119,7 +126,7 @@
|
||||
<string><html><head/><body><p>The GStreamer Spotify plugin is not detected, you will not be able to stream songs from Spotify without it. See: <a href="https://github.com/strawberrymusicplayer/strawberry/wiki/GStreamer-Spotify-plugin"><span style=" text-decoration: underline; color:#0000ff;">https://github.com/strawberrymusicplayer/strawberry/wiki/GStreamer-Spotify-plugin</span></a> for instructions on how to install the plugin.</p></body></html></string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
@@ -247,7 +254,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_middle">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -262,7 +269,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -314,7 +321,6 @@
|
||||
<tabstop>checkbox_fetchalbums</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
<include location="../../data/icons.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
|
||||
@@ -67,7 +67,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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 row="2" column="0">
|
||||
<widget class="QLabel" name="label_api_token">
|
||||
@@ -90,6 +94,9 @@
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
@@ -102,7 +109,11 @@
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="username"/>
|
||||
<widget class="QLineEdit" name="username">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
@@ -115,8 +126,11 @@
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="password">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
<enum>QLineEdit::EchoMode::Password</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -147,7 +161,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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 row="1" column="0">
|
||||
<widget class="QLabel" name="label_searchdelay">
|
||||
@@ -237,7 +255,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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 row="7" column="0">
|
||||
<widget class="QLabel" name="label_coversize">
|
||||
@@ -267,7 +289,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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 row="9" column="0">
|
||||
<widget class="QCheckBox" name="checkbox_album_explicit">
|
||||
@@ -282,7 +308,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_middle">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -297,7 +323,7 @@
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -357,7 +383,6 @@
|
||||
<tabstop>streamurl</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
<include location="../../data/icons.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -113,31 +113,15 @@ GstElement *Transcoder::CreateElementForMimeType(const QString &element_type, co
|
||||
GstElementFactory *factory = GST_ELEMENT_FACTORY(f->data);
|
||||
|
||||
// Is this the right type of plugin?
|
||||
if (QString::fromUtf8(gst_element_factory_get_metadata(factory, GST_ELEMENT_METADATA_KLASS)).contains(element_type)) {
|
||||
const GList *const templates = gst_element_factory_get_static_pad_templates(factory);
|
||||
for (const GList *t = templates; t; t = g_list_next(t)) {
|
||||
// Only interested in source pads
|
||||
GstStaticPadTemplate *pad_template = reinterpret_cast<GstStaticPadTemplate*>(t->data);
|
||||
if (pad_template->direction != GST_PAD_SRC) continue;
|
||||
|
||||
// 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);
|
||||
if (gst_element_factory_list_is_type(factory, element_type)) {
|
||||
// check if the element factory supports the target caps
|
||||
if (gst_element_factory_can_src_any_caps(factory, target_caps)) {
|
||||
const QString name = QString::fromUtf8(GST_OBJECT_NAME(factory));
|
||||
int rank = static_cast<int>(gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(factory)));
|
||||
if (name.startsWith(QLatin1String("avmux")) || name.startsWith(QLatin1String("avenc"))) {
|
||||
rank = -1; // ffmpeg usually sucks
|
||||
}
|
||||
suitable_elements_ << SuitableElement(name, rank);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,8 +416,8 @@ bool Transcoder::StartJob(const Job &job) {
|
||||
GstElement *decode = CreateElement(QStringLiteral("decodebin"), state->pipeline_);
|
||||
GstElement *convert = CreateElement(QStringLiteral("audioconvert"), state->pipeline_);
|
||||
GstElement *resample = CreateElement(QStringLiteral("audioresample"), state->pipeline_);
|
||||
GstElement *codec = CreateElementForMimeType(QStringLiteral("Codec/Encoder/Audio"), job.preset.codec_mimetype_, state->pipeline_);
|
||||
GstElement *muxer = CreateElementForMimeType(QStringLiteral("Codec/Muxer"), job.preset.muxer_mimetype_, state->pipeline_);
|
||||
GstElement *codec = CreateElementForMimeType(GST_ELEMENT_FACTORY_TYPE_AUDIO_ENCODER, job.preset.codec_mimetype_, state->pipeline_);
|
||||
GstElement *muxer = CreateElementForMimeType(GST_ELEMENT_FACTORY_TYPE_MUXER, job.preset.muxer_mimetype_, state->pipeline_);
|
||||
GstElement *sink = CreateElement(QStringLiteral("filesink"), state->pipeline_);
|
||||
|
||||
if (!src || !decode || !convert || !sink) return false;
|
||||
|
||||
@@ -133,7 +133,7 @@ class Transcoder : public QObject {
|
||||
bool StartJob(const Job &job);
|
||||
|
||||
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);
|
||||
|
||||
static void NewPadCallback(GstElement*, GstPad *pad, gpointer data);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -42,7 +42,8 @@ constexpr int kMagicNumber = 0x502C9510;
|
||||
StretchHeaderView::StretchHeaderView(const Qt::Orientation orientation, QWidget *parent)
|
||||
: QHeaderView(orientation, parent),
|
||||
stretch_enabled_(false),
|
||||
in_mouse_move_event_(false) {
|
||||
in_mouse_move_event_(false),
|
||||
forced_resize_logical_index_(-1) {
|
||||
|
||||
setDefaultSectionSize(100);
|
||||
setMinimumSectionSize(30);
|
||||
@@ -144,7 +145,7 @@ bool StretchHeaderView::RestoreState(const QByteArray &state) {
|
||||
if (i < visual_indices.count()) {
|
||||
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]);
|
||||
}
|
||||
setSectionHidden(i, !columns_visible.contains(i));
|
||||
@@ -170,11 +171,19 @@ bool StretchHeaderView::RestoreState(const QByteArray &state) {
|
||||
|
||||
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) {
|
||||
@@ -326,39 +341,48 @@ void StretchHeaderView::SectionResized(const int logical_index, const int old_si
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_mouse_move_event_) {
|
||||
bool resized = false;
|
||||
if (new_size >= minimumSectionSize()) {
|
||||
// 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;
|
||||
int right_section_visual_index = -1;
|
||||
for (int i = 0; i <= count(); ++i) {
|
||||
if (!isSectionHidden(i) &&
|
||||
visualIndex(i) > visual_index &&
|
||||
(right_section_visual_index == -1 || visualIndex(i) < right_section_visual_index)) {
|
||||
right_section_logical_index = i;
|
||||
right_section_visual_index = visualIndex(i);
|
||||
}
|
||||
}
|
||||
if (right_section_logical_index != -1) {
|
||||
const int right_section_size = sectionSize(right_section_logical_index) + (old_size - new_size);
|
||||
if (right_section_size >= minimumSectionSize()) {
|
||||
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 (logical_index == forced_resize_logical_index_) {
|
||||
forced_resize_logical_index_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_mouse_move_event_) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool resized = false;
|
||||
if (new_size >= minimumSectionSize()) {
|
||||
// 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;
|
||||
int right_section_visual_index = -1;
|
||||
for (int i = 0; i <= count(); ++i) {
|
||||
if (!isSectionHidden(i) &&
|
||||
visualIndex(i) > visual_index &&
|
||||
(right_section_visual_index == -1 || visualIndex(i) < right_section_visual_index)) {
|
||||
right_section_logical_index = i;
|
||||
right_section_visual_index = visualIndex(i);
|
||||
}
|
||||
}
|
||||
if (!resized) {
|
||||
in_mouse_move_event_ = true;
|
||||
resizeSection(logical_index, old_size);
|
||||
in_mouse_move_event_ = false;
|
||||
if (right_section_logical_index != -1) {
|
||||
const int right_section_size = sectionSize(right_section_logical_index) + (old_size - new_size);
|
||||
if (right_section_size >= minimumSectionSize()) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ class StretchHeaderView : public QHeaderView {
|
||||
QVector<ColumnWidthType> column_widths_;
|
||||
|
||||
bool in_mouse_move_event_;
|
||||
int forced_resize_logical_index_;
|
||||
};
|
||||
|
||||
#endif // STRETCHHEADERVIEW_H
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
|
||||
// clazy:excludeall=returning-void-expression
|
||||
|
||||
TEST(SqliteTest, FTS5SupportEnabled) {
|
||||
TEST(SqliteTest, CreateTableTest) {
|
||||
|
||||
sqlite3* db = nullptr;
|
||||
sqlite3 *db = nullptr;
|
||||
int rc = sqlite3_open(":memory:", &db);
|
||||
ASSERT_EQ(0, rc);
|
||||
|
||||
char* errmsg = nullptr;
|
||||
rc = sqlite3_exec(db, "CREATE VIRTUAL TABLE foo USING fts5(content, TEXT, tokenize = 'unicode61 remove_diacritics 0')", nullptr, nullptr, &errmsg);
|
||||
char *errmsg = nullptr;
|
||||
rc = sqlite3_exec(db, "CREATE TABLE foo (content TEXT)", nullptr, nullptr, &errmsg);
|
||||
ASSERT_EQ(0, rc) << errmsg;
|
||||
|
||||
sqlite3_close(db);
|
||||
|
||||
Reference in New Issue
Block a user