Compare commits

...

26 Commits
1.1.0 ... 1.1.1

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
44 changed files with 1421 additions and 1603 deletions

View File

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

132
.gitignore vendored
View File

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

View File

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

View File

@@ -2,6 +2,18 @@ Strawberry Music Player
======================= =======================
ChangeLog ChangeLog
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): Version 1.1.0 (2024.07.14):
Bugfixes: Bugfixes:

View File

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

View File

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

View File

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

View File

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

View File

@@ -58,9 +58,11 @@ set(SOURCES
utilities/filemanagerutils.cpp utilities/filemanagerutils.cpp
utilities/coverutils.cpp utilities/coverutils.cpp
utilities/screenutils.cpp utilities/screenutils.cpp
utilities/searchparserutils.cpp
utilities/textencodingutils.cpp utilities/textencodingutils.cpp
filterparser/filterparser.cpp
filterparser/filtertree.cpp
engine/enginebase.cpp engine/enginebase.cpp
engine/enginedevice.cpp engine/enginedevice.cpp
engine/devicefinders.cpp engine/devicefinders.cpp
@@ -96,7 +98,6 @@ set(SOURCES
collection/collectionfilter.cpp collection/collectionfilter.cpp
collection/collectionplaylistitem.cpp collection/collectionplaylistitem.cpp
collection/collectionquery.cpp collection/collectionquery.cpp
collection/collectionqueryoptions.cpp
collection/savedgroupingmanager.cpp collection/savedgroupingmanager.cpp
collection/groupbydialog.cpp collection/groupbydialog.cpp
collection/collectiontask.cpp collection/collectiontask.cpp
@@ -107,7 +108,6 @@ set(SOURCES
playlist/playlistcontainer.cpp playlist/playlistcontainer.cpp
playlist/playlistdelegates.cpp playlist/playlistdelegates.cpp
playlist/playlistfilter.cpp playlist/playlistfilter.cpp
playlist/playlistfilterparser.cpp
playlist/playlistheader.cpp playlist/playlistheader.cpp
playlist/playlistitem.cpp playlist/playlistitem.cpp
playlist/playlistlistcontainer.cpp playlist/playlistlistcontainer.cpp

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,54 +23,20 @@
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QAbstractItemModel>
#include <QSortFilterProxyModel>
#include "playlist/playlist.h" #include "playlist/playlist.h"
#include "playlist/playlistitem.h"
#include "filterparser/filterparser.h"
#include "filterparser/filtertree.h"
#include "playlistfilter.h" #include "playlistfilter.h"
#include "playlistfilterparser.h"
PlaylistFilter::PlaylistFilter(QObject *parent) PlaylistFilter::PlaylistFilter(QObject *parent)
: QSortFilterProxyModel(parent), : QSortFilterProxyModel(parent),
filter_tree_(new PlaylistNopFilter), filter_tree_(new NopFilter),
query_hash_(0) { query_hash_(0) {
setDynamicSortFilter(true); setDynamicSortFilter(true);
column_names_[QStringLiteral("title")] = static_cast<int>(Playlist::Column::Title);
column_names_[QStringLiteral("name")] = static_cast<int>(Playlist::Column::Title);
column_names_[QStringLiteral("artist")] = static_cast<int>(Playlist::Column::Artist);
column_names_[QStringLiteral("album")] = static_cast<int>(Playlist::Column::Album);
column_names_[QStringLiteral("albumartist")] = static_cast<int>(Playlist::Column::AlbumArtist);
column_names_[QStringLiteral("performer")] = static_cast<int>(Playlist::Column::Performer);
column_names_[QStringLiteral("composer")] = static_cast<int>(Playlist::Column::Composer);
column_names_[QStringLiteral("year")] = static_cast<int>(Playlist::Column::Year);
column_names_[QStringLiteral("originalyear")] = static_cast<int>(Playlist::Column::OriginalYear);
column_names_[QStringLiteral("track")] = static_cast<int>(Playlist::Column::Track);
column_names_[QStringLiteral("disc")] = static_cast<int>(Playlist::Column::Disc);
column_names_[QStringLiteral("length")] = static_cast<int>(Playlist::Column::Length);
column_names_[QStringLiteral("genre")] = static_cast<int>(Playlist::Column::Genre);
column_names_[QStringLiteral("samplerate")] = static_cast<int>(Playlist::Column::Samplerate);
column_names_[QStringLiteral("bitdepth")] = static_cast<int>(Playlist::Column::Bitdepth);
column_names_[QStringLiteral("bitrate")] = static_cast<int>(Playlist::Column::Bitrate);
column_names_[QStringLiteral("filename")] = static_cast<int>(Playlist::Column::Filename);
column_names_[QStringLiteral("grouping")] = static_cast<int>(Playlist::Column::Grouping);
column_names_[QStringLiteral("comment")] = static_cast<int>(Playlist::Column::Comment);
column_names_[QStringLiteral("rating")] = static_cast<int>(Playlist::Column::Rating);
column_names_[QStringLiteral("playcount")] = static_cast<int>(Playlist::Column::PlayCount);
column_names_[QStringLiteral("skipcount")] = static_cast<int>(Playlist::Column::SkipCount);
numerical_columns_ << static_cast<int>(Playlist::Column::Year)
<< static_cast<int>(Playlist::Column::OriginalYear)
<< static_cast<int>(Playlist::Column::Track)
<< static_cast<int>(Playlist::Column::Disc)
<< static_cast<int>(Playlist::Column::Length)
<< static_cast<int>(Playlist::Column::Samplerate)
<< static_cast<int>(Playlist::Column::Bitdepth)
<< static_cast<int>(Playlist::Column::Bitrate)
<< static_cast<int>(Playlist::Column::PlayCount)
<< static_cast<int>(Playlist::Column::SkipCount);
} }
PlaylistFilter::~PlaylistFilter() = default; PlaylistFilter::~PlaylistFilter() = default;
@@ -80,29 +46,35 @@ void PlaylistFilter::sort(int column, Qt::SortOrder order) {
sourceModel()->sort(column, order); sourceModel()->sort(column, order);
} }
bool PlaylistFilter::filterAcceptsRow(const int row, const QModelIndex &parent) const { bool PlaylistFilter::filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const {
Playlist *playlist = qobject_cast<Playlist*>(sourceModel());
if (!playlist) return false;
const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
if (!idx.isValid()) return false;
PlaylistItemPtr item = playlist->item_at(idx.row());
if (!item) return false;
if (filter_string_.isEmpty()) return true;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
size_t hash = qHash(filter_text_); const size_t hash = qHash(filter_string_);
#else #else
uint hash = qHash(filter_text_); const uint hash = qHash(filter_string_);
#endif #endif
if (hash != query_hash_) { if (hash != query_hash_) {
// Parse the query FilterParser p(filter_string_);
PlaylistFilterParser p(filter_text_, column_names_, numerical_columns_);
filter_tree_.reset(p.parse()); filter_tree_.reset(p.parse());
query_hash_ = hash; query_hash_ = hash;
} }
// Test the row return filter_tree_->accept(item->Metadata());
return filter_tree_->accept(row, parent, sourceModel());
} }
void PlaylistFilter::SetFilterText(const QString &filter_text) { void PlaylistFilter::SetFilterString(const QString &filter_string) {
filter_text_ = filter_text; filter_string_ = filter_string;
setFilterFixedString(filter_text); setFilterFixedString(filter_string);
} }

View File

@@ -24,15 +24,11 @@
#include "config.h" #include "config.h"
#include <QtGlobal> #include <QSortFilterProxyModel>
#include <QObject>
#include <QMap>
#include <QSet>
#include <QScopedPointer> #include <QScopedPointer>
#include <QString> #include <QString>
#include <QSortFilterProxyModel>
class PlaylistFilterTree; #include "filterparser/filtertree.h"
class PlaylistFilter : public QSortFilterProxyModel { class PlaylistFilter : public QSortFilterProxyModel {
Q_OBJECT Q_OBJECT
@@ -48,23 +44,18 @@ class PlaylistFilter : public QSortFilterProxyModel {
// public so Playlist::NextVirtualIndex and friends can get at it // public so Playlist::NextVirtualIndex and friends can get at it
bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override; bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override;
void SetFilterText(const QString &filter_text); void SetFilterString(const QString &filter_string);
QString filter_string() const { return filter_string_; }
QString filter_text() const { return filter_text_; }
QMap<QString, int> column_names() const { return column_names_; }
private: private:
// Mutable because they're modified from filterAcceptsRow() const // Mutable because they're modified from filterAcceptsRow() const
mutable QScopedPointer<PlaylistFilterTree> filter_tree_; mutable QScopedPointer<FilterTree> filter_tree_;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
mutable size_t query_hash_; mutable size_t query_hash_;
#else #else
mutable uint query_hash_; mutable uint query_hash_;
#endif #endif
QString filter_string_;
QMap<QString, int> column_names_;
QSet<int> numerical_columns_;
QString filter_text_;
}; };
#endif // PLAYLISTFILTER_H #endif // PLAYLISTFILTER_H

View File

@@ -1,610 +0,0 @@
/*
* 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 "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 PlaylistSearchTermComparator {
public:
PlaylistSearchTermComparator() = default;
virtual ~PlaylistSearchTermComparator() = default;
virtual bool Matches(const QString &element) const = 0;
private:
Q_DISABLE_COPY(PlaylistSearchTermComparator)
};
// "compares" by checking if the field contains the search term
class PlaylistDefaultComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistDefaultComparator(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(PlaylistDefaultComparator)
};
class PlaylistEqComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistEqComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ == element;
}
private:
QString search_term_;
};
class PlaylistNeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistNeComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ != element;
}
private:
QString search_term_;
};
class PlaylistLexicalGtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLexicalGtComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element > search_term_;
}
private:
QString search_term_;
};
class PlaylistLexicalGeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLexicalGeComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element >= search_term_;
}
private:
QString search_term_;
};
class PlaylistLexicalLtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLexicalLtComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element < search_term_;
}
private:
QString search_term_;
};
class PlaylistLexicalLeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLexicalLeComparator(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 PlaylistFloatEqComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatEqComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ == element.toFloat();
}
private:
float search_term_;
};
class PlaylistFloatNeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatNeComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ != element.toFloat();
}
private:
float search_term_;
};
class PlaylistFloatGtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatGtComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() > search_term_;
}
private:
float search_term_;
};
class PlaylistFloatGeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatGeComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() >= search_term_;
}
private:
float search_term_;
};
class PlaylistFloatLtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatLtComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() < search_term_;
}
private:
float search_term_;
};
class PlaylistFloatLeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatLeComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() <= search_term_;
}
private:
float search_term_;
};
class PlaylistGtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistGtComparator(const int value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toInt() > search_term_;
}
private:
int search_term_;
};
class PlaylistGeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistGeComparator(const int value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toInt() >= search_term_;
}
private:
int search_term_;
};
class PlaylistLtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLtComparator(const int value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toInt() < search_term_;
}
private:
int search_term_;
};
class PlaylistLeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLeComparator(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 PlaylistDropTailComparatorDecorator : public PlaylistSearchTermComparator {
public:
explicit PlaylistDropTailComparatorDecorator(PlaylistSearchTermComparator *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<PlaylistSearchTermComparator> cmp_;
};
class PlaylistRatingComparatorDecorator : public PlaylistSearchTermComparator {
public:
explicit PlaylistRatingComparatorDecorator(PlaylistSearchTermComparator *cmp) : cmp_(cmp) {}
bool Matches(const QString &element) const override {
return cmp_->Matches(QString::number(lround(element.toDouble() * 10.0)));
}
private:
QScopedPointer<PlaylistSearchTermComparator> cmp_;
};
// Filter that applies a SearchTermComparator to all fields of a playlist entry
class PlaylistFilterTerm : public PlaylistFilterTree {
public:
explicit PlaylistFilterTerm(PlaylistSearchTermComparator *comparator, const QList<int> &columns) : cmp_(comparator), columns_(columns) {}
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
for (const int i : columns_) {
const 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<PlaylistSearchTermComparator> cmp_;
QList<int> columns_;
};
// Filter that applies a SearchTermComparator to one specific field of a playlist entry
class PlaylistFilterColumnTerm : public PlaylistFilterTree {
public:
PlaylistFilterColumnTerm(const int column, PlaylistSearchTermComparator *comparator) : col(column), cmp_(comparator) {}
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
const QModelIndex idx = model->index(row, col, parent);
return cmp_->Matches(idx.data().toString().toLower());
}
FilterType type() override { return FilterType::Column; }
private:
int col;
QScopedPointer<PlaylistSearchTermComparator> cmp_;
};
class PlaylistNotFilter : public PlaylistFilterTree {
public:
explicit PlaylistNotFilter(const PlaylistFilterTree *inv) : child_(inv) {}
bool accept(const 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 PlaylistFilterTree> child_;
};
class PlaylistOrFilter : public PlaylistFilterTree {
public:
~PlaylistOrFilter() override { qDeleteAll(children_); }
virtual void add(PlaylistFilterTree *child) { children_.append(child); }
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
return std::any_of(children_.begin(), children_.end(), [row, parent, model](PlaylistFilterTree *child) { return child->accept(row, parent, model); });
}
FilterType type() override { return FilterType::Or; }
private:
QList<PlaylistFilterTree*> children_;
};
class PlaylistAndFilter : public PlaylistFilterTree {
public:
~PlaylistAndFilter() override { qDeleteAll(children_); }
virtual void add(PlaylistFilterTree *child) { children_.append(child); }
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
return !std::any_of(children_.begin(), children_.end(), [row, parent, model](PlaylistFilterTree *child) { return !child->accept(row, parent, model); });
}
FilterType type() override { return FilterType::And; }
private:
QList<PlaylistFilterTree*> children_;
};
PlaylistFilterParser::PlaylistFilterParser(const QString &filter, const QMap<QString, int> &columns, const QSet<int> &numerical_cols) : iter_{}, end_{}, filterstring_(filter), columns_(columns), numerical_columns_(numerical_cols) {}
PlaylistFilterTree *PlaylistFilterParser::parse() {
iter_ = filterstring_.constBegin();
end_ = filterstring_.constEnd();
return parseOrGroup();
}
void PlaylistFilterParser::advance() {
while (iter_ != end_ && iter_->isSpace()) {
++iter_;
}
}
PlaylistFilterTree *PlaylistFilterParser::parseOrGroup() {
advance();
if (iter_ == end_) return new PlaylistNopFilter;
PlaylistOrFilter *group = new PlaylistOrFilter;
group->add(parseAndGroup());
advance();
while (checkOr()) {
group->add(parseAndGroup());
advance();
}
return group;
}
PlaylistFilterTree *PlaylistFilterParser::parseAndGroup() {
advance();
if (iter_ == end_) return new PlaylistNopFilter;
PlaylistAndFilter *group = new PlaylistAndFilter();
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 PlaylistFilterParser::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 PlaylistFilterParser::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;
}
PlaylistFilterTree *PlaylistFilterParser::parseSearchExpression() {
advance();
if (iter_ == end_) return new PlaylistNopFilter;
if (*iter_ == QLatin1Char('(')) {
++iter_;
advance();
PlaylistFilterTree *tree = parseOrGroup();
advance();
if (iter_ != end_) {
if (*iter_ == QLatin1Char(')')) {
++iter_;
}
}
return tree;
}
else if (*iter_ == QLatin1Char('-')) {
++iter_;
PlaylistFilterTree *tree = parseSearchExpression();
if (tree->type() != PlaylistFilterTree::FilterType::Nop) return new PlaylistNotFilter(tree);
return tree;
}
else {
return parseSearchTerm();
}
}
PlaylistFilterTree *PlaylistFilterParser::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);
}
PlaylistFilterTree *PlaylistFilterParser::createSearchTermTreeNode(const QString &col, const QString &prefix, const QString &search) const {
if (search.isEmpty() && prefix != QLatin1Char('=')) {
return new PlaylistNopFilter;
}
PlaylistSearchTermComparator *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 PlaylistFloatEqComparator(parsed_search);
}
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
cmp = new PlaylistFloatNeComparator(parsed_search);
}
else if (prefix == QLatin1Char('>')) {
cmp = new PlaylistFloatGtComparator(parsed_search);
}
else if (prefix == QLatin1String(">=")) {
cmp = new PlaylistFloatGeComparator(parsed_search);
}
else if (prefix == QLatin1Char('<')) {
cmp = new PlaylistFloatLtComparator(parsed_search);
}
else if (prefix == QLatin1String("<=")) {
cmp = new PlaylistFloatLeComparator(parsed_search);
}
else {
cmp = new PlaylistFloatEqComparator(parsed_search);
}
}
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
cmp = new PlaylistNeComparator(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 PlaylistGtComparator(search_value);
}
else if (prefix == QLatin1String(">=")) {
cmp = new PlaylistGeComparator(search_value);
}
else if (prefix == QLatin1Char('<')) {
cmp = new PlaylistLtComparator(search_value);
}
else if (prefix == QLatin1String("<=")) {
cmp = new PlaylistLeComparator(search_value);
}
else {
// Convert back because for time/rating
cmp = new PlaylistEqComparator(QString::number(search_value));
}
}
else {
if (prefix == QLatin1Char('=')) {
cmp = new PlaylistEqComparator(search);
}
else if (prefix == QLatin1Char('>')) {
cmp = new PlaylistLexicalGtComparator(search);
}
else if (prefix == QLatin1String(">=")) {
cmp = new PlaylistLexicalGeComparator(search);
}
else if (prefix == QLatin1Char('<')) {
cmp = new PlaylistLexicalLtComparator(search);
}
else if (prefix == QLatin1String("<=")) {
cmp = new PlaylistLexicalLeComparator(search);
}
else {
cmp = new PlaylistDefaultComparator(search);
}
}
if (columns_.contains(col)) {
if (columns_[col] == static_cast<int>(Playlist::Column::Length)) {
cmp = new PlaylistDropTailComparatorDecorator(cmp);
}
return new PlaylistFilterColumnTerm(columns_[col], cmp);
}
else {
return new PlaylistFilterTerm(cmp, columns_.values());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -145,7 +145,7 @@ bool StretchHeaderView::RestoreState(const QByteArray &state) {
if (i < visual_indices.count()) { if (i < visual_indices.count()) {
moveSection(visualIndex(visual_indices[i]), i); moveSection(visualIndex(visual_indices[i]), i);
} }
if (i < column_pixel_widths.count()) { if (i < column_pixel_widths.count() && column_pixel_widths[i] > 0) {
resizeSection(i, column_pixel_widths[i]); resizeSection(i, column_pixel_widths[i]);
} }
setSectionHidden(i, !columns_visible.contains(i)); setSectionHidden(i, !columns_visible.contains(i));
@@ -171,11 +171,19 @@ bool StretchHeaderView::RestoreState(const QByteArray &state) {
QByteArray StretchHeaderView::ResetState() { QByteArray StretchHeaderView::ResetState() {
stretch_enabled_ = true; stretch_enabled_ = false;
column_widths_.resize(count());
std::fill(column_widths_.begin(), column_widths_.end(), 1.0 / count());
setSortIndicator(0, Qt::AscendingOrder); setSortIndicator(-1, Qt::AscendingOrder);
return QByteArray(); for (int i = 0; i < count(); ++i) {
setSectionHidden(i, false);
resizeSection(i, defaultSectionSize());
moveSection(visualIndex(i), i);
}
return SaveState();
} }
@@ -284,6 +292,12 @@ void StretchHeaderView::ShowSection(const int logical_index) {
} }
else {
if (sectionSize(logical_index) == 0) {
resizeSection(logical_index, defaultSectionSize());
}
}
} }
void StretchHeaderView::HideSection(const int logical_index) { void StretchHeaderView::HideSection(const int logical_index) {