Compare commits

..

48 Commits

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

View File

@@ -171,7 +171,7 @@ jobs:
strategy:
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
View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -64,7 +64,7 @@ Funding developers is a way to contribute to open source projects you appreciate
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
**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

View File

@@ -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}
)

View File

@@ -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)

View File

@@ -94,9 +94,4 @@ CREATE INDEX idx_device_%deviceid_songs_album ON device_%deviceid_songs (album);
CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist);
CREATE 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;

View File

@@ -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,

View File

@@ -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"/>

View File

@@ -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());
}

View File

@@ -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

View File

@@ -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);

View File

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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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]);
}

View File

@@ -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_);
}

View File

@@ -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

View File

@@ -0,0 +1,100 @@
/*
Strawberry Music Player
This file was part of Clementine.
Copyright 2003, Stanislav Karchebny <berkus@users.sf.net>
Copyright 2003, Max Howell <max.howell@methylblue.com>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <cmath>
#include <algorithm>
#include <QPainter>
#include "turbineanalyzer.h"
#include "engine/enginebase.h"
const char *TurbineAnalyzer::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "Turbine");
TurbineAnalyzer::TurbineAnalyzer(QWidget *parent) : BoomAnalyzer(parent) {}
void TurbineAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame) {
if (!new_frame || engine_->state() == EngineBase::State::Paused) {
p.drawPixmap(0, 0, canvas_);
return;
}
const uint hd2 = height() / 2;
const uint kMaxHeight = hd2 - 1;
QPainter canvas_painter(&canvas_);
canvas_.fill(palette().color(QPalette::Window));
AnalyzerBase::interpolate(scope, scope_);
for (uint i = 0, x = 0, y = 0; i < static_cast<uint>(bands_); ++i, x += kColumnWidth + 1) {
float h = static_cast<float>(std::min(log10(scope_[i] * 256.0) * F_ * 0.5, kMaxHeight * 1.0));
if (h > bar_height_[i]) {
bar_height_[i] = h;
if (h > peak_height_[i]) {
peak_height_[i] = h;
peak_speed_[i] = 0.01;
}
else {
goto peak_handling;
}
}
else {
if (bar_height_[i] > 0.0) {
bar_height_[i] -= K_barHeight_; // 1.4
if (bar_height_[i] < 0.0) bar_height_[i] = 0.0;
}
peak_handling:
if (peak_height_[i] > 0.0) {
peak_height_[i] -= peak_speed_[i];
peak_speed_[i] *= F_peakSpeed_; // 1.12
peak_height_[i] = std::max(0.0, std::max(bar_height_[i], peak_height_[i]));
}
}
y = hd2 - static_cast<uint>(bar_height_[i]);
canvas_painter.drawPixmap(static_cast<int>(x + 1), static_cast<int>(y), barPixmap_, 0, static_cast<int>(y), -1, -1);
canvas_painter.drawPixmap(static_cast<int>(x + 1), static_cast<int>(hd2), barPixmap_, 0, static_cast<int>(bar_height_[i]), -1, -1);
canvas_painter.setPen(fg_);
if (bar_height_[i] > 0) {
canvas_painter.drawRect(static_cast<int>(x), static_cast<int>(y), kColumnWidth - 1, static_cast<int>(bar_height_[i]) * 2 - 1);
}
const uint x2 = x + kColumnWidth - 1;
canvas_painter.setPen(palette().color(QPalette::Midlight));
y = hd2 - static_cast<uint>(peak_height_[i]);
canvas_painter.drawLine(static_cast<int>(x), static_cast<int>(y), static_cast<int>(x2), static_cast<int>(y));
y = hd2 + static_cast<uint>(peak_height_[i]);
canvas_painter.drawLine(static_cast<int>(x), static_cast<int>(y), static_cast<int>(x2), static_cast<int>(y));
}
p.drawPixmap(0, 0, canvas_);
}

View File

@@ -0,0 +1,41 @@
/*
Strawberry Music Player
This file was part of Clementine.
Copyright 2003, Stanislav Karchebny <berkus@users.sf.net>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TURBINEANALYZER_H
#define TURBINEANALYZER_H
#include "boomanalyzer.h"
class QPainter;
class TurbineAnalyzer : public BoomAnalyzer {
Q_OBJECT
public:
Q_INVOKABLE explicit TurbineAnalyzer(QWidget *parent);
void analyze(QPainter &p, const Scope &scope, const bool new_frame);
static const char *kName;
};
#endif // TURBINEANALYZER_H

View File

@@ -19,14 +19,14 @@
#include <QPainter>
#include <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_);
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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(" =, !=, &lt;, &gt;, &lt;="), QLatin1String("&gt;=")) +
QLatin1String("<span style=\"font-weight:600;\">") +
tr("rating") +
QLatin1String("</span>") +
QLatin1String(":>=") +
QLatin1String("<span style=\"font-weight:italic;\">4</span>") +
QLatin1String("</p><p><span style=\"font-weight:600;\">") +
tr("Available fields") +
QLatin1String(": ") +
QLatin1String("</span>") +
QLatin1String("<span style=\"font-style:italic;\">") +
available_fields +
QLatin1String("</span>.") +
QLatin1String("</p></body></html>")
);
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());
}
}

View File

@@ -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_;

View File

@@ -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;

View File

@@ -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:

View File

@@ -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),

View File

@@ -1,33 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QVariant>
#include <QString>
#include "collectionqueryoptions.h"
CollectionQueryOptions::CollectionQueryOptions()
: compilation_requirement_(CollectionQueryOptions::CompilationRequirement::None),
query_have_compilations_(false) {}
void CollectionQueryOptions::AddWhere(const QString &column, const QVariant &value, const QString &op) {
where_clauses_ << Where(column, value, op);
}

View File

@@ -1,63 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COLLECTIONQUERYOPTIONS_H
#define COLLECTIONQUERYOPTIONS_H
#include <QList>
#include <QVariant>
#include <QString>
class CollectionQueryOptions {
public:
explicit CollectionQueryOptions();
struct Where {
explicit Where(const QString &_column = QString(), const QVariant &_value = QString(), const QString &_op = QString()) : column(_column), value(_value), op(_op) {}
QString column;
QVariant value;
QString op;
};
enum class CompilationRequirement {
None,
On,
Off
};
QString column_spec() const { return column_spec_; }
CompilationRequirement compilation_requirement() const { return compilation_requirement_; }
bool query_have_compilations() const { return query_have_compilations_; }
void set_column_spec(const QString &column_spec) { column_spec_ = column_spec; }
void set_compilation_requirement(const CompilationRequirement compilation_requirement) { compilation_requirement_ = compilation_requirement; }
void set_query_have_compilations(const bool query_have_compilations) { query_have_compilations_ = query_have_compilations; }
QList<Where> where_clauses() const { return where_clauses_; }
void AddWhere(const QString &column, const QVariant &value, const QString &op = QStringLiteral("="));
private:
QString column_spec_;
CompilationRequirement compilation_requirement_;
bool query_have_compilations_;
QList<Where> where_clauses_;
};
#endif // COLLECTIONQUERYOPTIONS_H

View File

@@ -1071,17 +1071,25 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
#endif
{
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")));
}
}
}

View File

@@ -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

View File

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

View File

@@ -2,6 +2,8 @@
* Strawberry Music Player
* 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

View File

@@ -0,0 +1,314 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef FILTERPARSERSEARCHCOMPARATORS_H
#define FILTERPARSERSEARCHCOMPARATORS_H
#include "config.h"
#include <QVariant>
#include <QString>
#include <QScopedPointer>
class FilterParserSearchTermComparator {
public:
FilterParserSearchTermComparator() = default;
virtual ~FilterParserSearchTermComparator() = default;
virtual bool Matches(const QVariant &value) const = 0;
private:
Q_DISABLE_COPY(FilterParserSearchTermComparator)
};
// "compares" by checking if the field contains the search term
class FilterParserDefaultComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserDefaultComparator(const QString &search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toString().contains(search_term_, Qt::CaseInsensitive);
}
private:
QString search_term_;
Q_DISABLE_COPY(FilterParserDefaultComparator)
};
class FilterParserTextEqComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserTextEqComparator(const QString &search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return search_term_.compare(value.toString(), Qt::CaseInsensitive) == 0;
}
private:
QString search_term_;
};
class FilterParserTextNeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserTextNeComparator(const QString &search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return search_term_.compare(value.toString(), Qt::CaseInsensitive) != 0;
}
private:
QString search_term_;
};
class FilterParserIntEqComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntEqComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() == search_term_;
}
private:
int search_term_;
};
class FilterParserIntNeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntNeComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() != search_term_;
}
private:
int search_term_;
};
class FilterParserIntGtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntGtComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() > search_term_;
}
private:
int search_term_;
};
class FilterParserIntGeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntGeComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() >= search_term_;
}
private:
int search_term_;
};
class FilterParserIntLtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntLtComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() < search_term_;
}
private:
int search_term_;
};
class FilterParserIntLeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserIntLeComparator(const int search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toInt() <= search_term_;
}
private:
int search_term_;
};
class FilterParserUIntEqComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntEqComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() == search_term_;
}
private:
uint search_term_;
};
class FilterParserUIntNeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntNeComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() != search_term_;
}
private:
uint search_term_;
};
class FilterParserUIntGtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntGtComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() > search_term_;
}
private:
uint search_term_;
};
class FilterParserUIntGeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntGeComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() >= search_term_;
}
private:
uint search_term_;
};
class FilterParserUIntLtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntLtComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() < search_term_;
}
private:
uint search_term_;
};
class FilterParserUIntLeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserUIntLeComparator(const uint search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toUInt() <= search_term_;
}
private:
uint search_term_;
};
class FilterParserInt64EqComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64EqComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() == search_term_;
}
private:
qint64 search_term_;
};
class FilterParserInt64NeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64NeComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() != search_term_;
}
private:
qint64 search_term_;
};
class FilterParserInt64GtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64GtComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() > search_term_;
}
private:
qint64 search_term_;
};
class FilterParserInt64GeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64GeComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() >= search_term_;
}
private:
qint64 search_term_;
};
class FilterParserInt64LtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64LtComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() < search_term_;
}
private:
qint64 search_term_;
};
class FilterParserInt64LeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserInt64LeComparator(const qint64 search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toLongLong() <= search_term_;
}
private:
qint64 search_term_;
};
// Float Comparators are for the rating
class FilterParserFloatEqComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatEqComparator(const float search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() == search_term_;
}
private:
float search_term_;
};
class FilterParserFloatNeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatNeComparator(const float value) : search_term_(value) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() != search_term_;
}
private:
float search_term_;
};
class FilterParserFloatGtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatGtComparator(const float search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() > search_term_;
}
private:
float search_term_;
};
class FilterParserFloatGeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatGeComparator(const float search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() >= search_term_;
}
private:
float search_term_;
};
class FilterParserFloatLtComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatLtComparator(const float search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() < search_term_;
}
private:
float search_term_;
};
class FilterParserFloatLeComparator : public FilterParserSearchTermComparator {
public:
explicit FilterParserFloatLeComparator(const float search_term) : search_term_(search_term) {}
bool Matches(const QVariant &value) const override {
return value.toFloat() <= search_term_;
}
private:
float search_term_;
};
#endif // FILTERPARSERSEARCHCOMPARATORS_H

View File

@@ -0,0 +1,52 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QString>
#include "filtertree.h"
FilterTree::FilterTree() = default;
FilterTree::~FilterTree() = default;
QVariant FilterTree::DataFromColumn(const QString &column, const Song &metadata) {
if (column == QLatin1String("albumartist")) return metadata.effective_albumartist();
if (column == QLatin1String("artist")) return metadata.artist();
if (column == QLatin1String("album")) return metadata.album();
if (column == QLatin1String("title")) return metadata.title();
if (column == QLatin1String("composer")) return metadata.composer();
if (column == QLatin1String("performer")) return metadata.performer();
if (column == QLatin1String("grouping")) return metadata.grouping();
if (column == QLatin1String("genre")) return metadata.genre();
if (column == QLatin1String("comment")) return metadata.comment();
if (column == QLatin1String("track")) return metadata.track();
if (column == QLatin1String("year")) return metadata.year();
if (column == QLatin1String("length")) return metadata.length_nanosec();
if (column == QLatin1String("samplerate")) return metadata.samplerate();
if (column == QLatin1String("bitdepth")) return metadata.bitdepth();
if (column == QLatin1String("bitrate")) return metadata.bitrate();
if (column == QLatin1String("rating")) return metadata.rating();
if (column == QLatin1String("playcount")) return metadata.playcount();
if (column == QLatin1String("skipcount")) return metadata.skipcount();
return QVariant();
}

View File

@@ -0,0 +1,147 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef FILTERTREE_H
#define FILTERTREE_H
#include "config.h"
#include <QList>
#include <QString>
#include <QStringList>
#include <QScopedPointer>
#include "core/song.h"
#include "filterparsersearchcomparators.h"
class FilterTree {
public:
explicit FilterTree();
virtual ~FilterTree();
enum class FilterType {
Nop = 0,
Or,
And,
Not,
Column,
Term
};
virtual FilterType type() const = 0;
virtual bool accept(const Song &song) const = 0;
protected:
static QVariant DataFromColumn(const QString &column, const Song &metadata);
private:
Q_DISABLE_COPY(FilterTree)
};
// Trivial filter that accepts *anything*
class NopFilter : public FilterTree {
public:
FilterType type() const override { return FilterType::Nop; }
bool accept(const Song &song) const override { Q_UNUSED(song); return true; }
};
// Filter that applies a SearchTermComparator to all fields
class FilterTerm : public FilterTree {
public:
explicit FilterTerm(const QStringList &columns, FilterParserSearchTermComparator *comparator) : columns_(columns), cmp_(comparator) {}
FilterType type() const override { return FilterType::Term; }
bool accept(const Song &song) const override {
for (const QString &column : columns_) {
if (cmp_->Matches(DataFromColumn(column, song))) return true;
}
return false;
}
private:
const QStringList columns_;
QScopedPointer<FilterParserSearchTermComparator> cmp_;
};
class FilterColumnTerm : public FilterTree {
public:
explicit FilterColumnTerm(const QString &column, FilterParserSearchTermComparator *comparator) : column_(column), cmp_(comparator) {}
FilterType type() const override { return FilterType::Column; }
bool accept(const Song &song) const override {
return cmp_->Matches(DataFromColumn(column_, song));
}
private:
const QString column_;
QScopedPointer<FilterParserSearchTermComparator> cmp_;
};
class NotFilter : public FilterTree {
public:
explicit NotFilter(const FilterTree *inv) : child_(inv) {}
FilterType type() const override { return FilterType::Not; }
bool accept(const Song &song) const override {
return !child_->accept(song);
}
private:
QScopedPointer<const FilterTree> child_;
};
class OrFilter : public FilterTree {
public:
~OrFilter() override { qDeleteAll(children_); }
FilterType type() const override { return FilterType::Or; }
virtual void add(FilterTree *child) { children_.append(child); }
bool accept(const Song &song) const override {
return std::any_of(children_.begin(), children_.end(), [song](FilterTree *child) { return child->accept(song); });
}
private:
QList<FilterTree*> children_;
};
class AndFilter : public FilterTree {
public:
~AndFilter() override { qDeleteAll(children_); }
FilterType type() const override { return FilterType::And; }
virtual void add(FilterTree *child) { children_.append(child); }
bool accept(const Song &song) const override {
return !std::any_of(children_.begin(), children_.end(), [song](FilterTree *child) { return !child->accept(song); });
}
private:
QList<FilterTree*> children_;
};
#endif // FILTERTREE_H

View File

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

View File

@@ -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(" =, !=, &lt;, &gt;, &lt;="), QLatin1String("&gt;=")) +
QLatin1String("<span style=\"font-weight:600;\">") +
tr("rating") +
QLatin1String("</span>") +
QLatin1String(":>=") +
QLatin1String("<span style=\"font-weight:italic;\">4</span>") +
QLatin1String("</p><p>") +
tr("Multiple search terms can also be combined with \"%1\" (default) and \"%2\", as well as grouped with parentheses. ")
.arg(QLatin1String("AND"), QLatin1String("OR")) +
QLatin1String("</p><p><span style=\"font-weight:600;\">") +
tr("Available fields") +
QLatin1String(": ") + QLatin1String("</span><span style=\"font-style:italic;\">") +
available_fields +
QLatin1String("</span>.") +
QLatin1String("</p></body></html>")
);
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();

View File

@@ -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);
}

View File

@@ -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

View File

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

View File

@@ -335,6 +335,8 @@ void PlaylistView::RestoreHeaderState() {
if (set_initial_header_layout_) {
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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The GStreamer Spotify plugin is not detected, you will not be able to stream songs from Spotify without it. See: &lt;a href=&quot;https://github.com/strawberrymusicplayer/strawberry/wiki/GStreamer-Spotify-plugin&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;https://github.com/strawberrymusicplayer/strawberry/wiki/GStreamer-Spotify-plugin&lt;/span&gt;&lt;/a&gt; for instructions on how to install the plugin.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property 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/>

View File

@@ -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/>

View File

@@ -90,7 +90,7 @@ struct SuitableElement {
};
GstElement *Transcoder::CreateElementForMimeType(const QString &element_type, const QString &mime_type, GstElement *bin) {
GstElement *Transcoder::CreateElementForMimeType(GstElementFactoryListType element_type, const QString &mime_type, GstElement *bin) {
if (mime_type.isEmpty()) return nullptr;
@@ -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;

View File

@@ -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);

View File

@@ -1,119 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2019-2023, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QString>
#include "searchparserutils.h"
namespace Utilities {
// Try and parse the string as '[[h:]m:]s' (ignoring all spaces),
// and return the number of seconds if it parses correctly.
// If not, the original string is returned.
// The 'h', 'm' and 's' components can have any length (including 0).
// A few examples:
// "::" is parsed to "0"
// "1::" is parsed to "3600"
// "3:45" is parsed to "225"
// "1:165" is parsed to "225"
// "225" is parsed to "225" (srsly! ^.^)
// "2:3:4:5" is parsed to "2:3:4:5"
// "25m" is parsed to "25m"
int ParseSearchTime(const QString &time_str) {
int seconds = 0;
int accum = 0;
int colon_count = 0;
for (const QChar &c : time_str) {
if (c.isDigit()) {
accum = accum * 10 + c.digitValue();
}
else if (c == QLatin1Char(':')) {
seconds = seconds * 60 + accum;
accum = 0;
++colon_count;
if (colon_count > 2) {
return 0;
}
}
else if (!c.isSpace()) {
return 0;
}
}
seconds = seconds * 60 + accum;
return seconds;
}
// Parses a rating search term to float.
// If the rating is a number from 0-5, map it to 0-1
// To use float values directly, the search term can be prefixed with "f" (rating:>f0.2)
// If search string is 0, or by default, uses -1
// @param rating_str: Rating search 0-5, or "f0.2"
// @return float: rating from 0-1 or -1 if not rated.
float ParseSearchRating(const QString &rating_str) {
if (rating_str.isEmpty()) {
return -1;
}
float rating = -1.0F;
// Check if the search is a float
if (rating_str.contains(QLatin1Char('f'), Qt::CaseInsensitive)) {
if (rating_str.count(QLatin1Char('f'), Qt::CaseInsensitive) > 1) {
return rating;
}
QString rating_float_str = rating_str;
if (rating_str.at(0) == QLatin1Char('f') || rating_str.at(0) == QLatin1Char('F')) {
rating_float_str = rating_float_str.remove(0, 1);
}
if (rating_str.right(1) == QLatin1Char('f') || rating_str.right(1) == QLatin1Char('F')) {
rating_float_str.chop(1);
}
bool ok = false;
const float rating_input = rating_float_str.toFloat(&ok);
if (ok) {
rating = rating_input;
}
}
else {
bool ok = false;
const int rating_input = rating_str.toInt(&ok);
// Is valid int from 0-5: convert to float
if (ok && rating_input >= 0 && rating_input <= 5) {
rating = static_cast<float>(rating_input) / 5.0F;
}
}
// Songs with zero rating have -1 in the DB
if (rating == 0) {
rating = -1;
}
return rating;
}
} // namespace Utilities

View File

@@ -1,33 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2019-2023, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SEARCHPARSERUTILS_H
#define SEARCHPARSERUTILS_H
#include <QString>
namespace Utilities {
int ParseSearchTime(const QString &time_str);
float ParseSearchRating(const QString &rating_str);
} // namespace Utilities
#endif // SEARCHPARSERUTILS_H

View File

@@ -42,7 +42,8 @@ constexpr int kMagicNumber = 0x502C9510;
StretchHeaderView::StretchHeaderView(const Qt::Orientation orientation, QWidget *parent)
: 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;
}
}

View File

@@ -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

View File

@@ -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);