diff --git a/CMakeLists.txt b/CMakeLists.txt index cf384a848..d052a9597 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ cmake_minimum_required(VERSION 3.0) project(musikcube) set (musikcube_VERSION_MAJOR 0) -set (musikcube_VERSION_MINOR 94) +set (musikcube_VERSION_MINOR 95) set (musikcube_VERSION_PATCH 0) set (musikcube_VERSION "${musikcube_VERSION_MAJOR}.${musikcube_VERSION_MINOR}.${musikcube_VERSION_PATCH}") diff --git a/musikcube.spec b/musikcube.spec index 2ef4fe4bc..d8bb9257f 100644 --- a/musikcube.spec +++ b/musikcube.spec @@ -1,6 +1,6 @@ %define name musikcube %define build_timestamp %{lua: print(os.date("%Y%m%d"))} -%define version 0.94.0 +%define version 0.95.0 Name: %{name} Version: %{version} Release: %{dist} diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index be2ba3fe4..8acfc91d8 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: musikcube # you probably want to 'snapcraft register ' -version: 0.94.0 +version: 0.95.0 summary: a terminal-based music player, metadata indexer, and server description: | musikcube is a fully functional terminal-based music player, library, and diff --git a/src/musikcore/CMakeLists.txt b/src/musikcore/CMakeLists.txt index 9e803d042..6b8bf68a5 100644 --- a/src/musikcore/CMakeLists.txt +++ b/src/musikcore/CMakeLists.txt @@ -15,6 +15,7 @@ set(CORE_SOURCES ./audio/Visualizer.cpp ./db/Connection.cpp ./db/ScopedTransaction.cpp + ./db/SqliteExtensions.cpp ./db/Statement.cpp ./i18n/Locale.cpp ./io/DataStreamFactory.cpp diff --git a/src/musikcore/db/Connection.cpp b/src/musikcore/db/Connection.cpp index 55b7f0767..d71f0d7b9 100644 --- a/src/musikcore/db/Connection.cpp +++ b/src/musikcore/db/Connection.cpp @@ -35,6 +35,7 @@ #include "pch.hpp" #include +#include #include static std::mutex globalMutex; @@ -116,6 +117,8 @@ int Connection::LastModifiedRowCount() { } void Connection::Initialize(unsigned int cache) { + SqliteExtensions::Register(this->connection); + sqlite3_enable_shared_cache(1); sqlite3_busy_timeout(this->connection, 10000); @@ -133,7 +136,7 @@ void Connection::Initialize(unsigned int cache) { sqlite3_exec(this->connection,cacheSize.c_str(), nullptr, nullptr, nullptr); // size * 1.5kb = 6Mb cache } - sqlite3_exec(this->connection, "PRAGMA case_sensitive_like=0", nullptr, nullptr, nullptr); // More speed if case insensitive + //sqlite3_exec(this->connection, "PRAGMA case_sensitive_like=0", nullptr, nullptr, nullptr); // More speed if case insensitive sqlite3_exec(this->connection, "PRAGMA count_changes=0", nullptr, nullptr, nullptr); // If set it counts changes on SQL UPDATE. More speed when not. sqlite3_exec(this->connection, "PRAGMA legacy_file_format=OFF", nullptr, nullptr, nullptr); // No reason to be backwards compatible :) sqlite3_exec(this->connection, "PRAGMA temp_store=MEMORY", nullptr, nullptr, nullptr); // MEMORY, not file. More speed. diff --git a/src/musikcore/db/SqliteExtensions.cpp b/src/musikcore/db/SqliteExtensions.cpp new file mode 100644 index 000000000..9175276ac --- /dev/null +++ b/src/musikcore/db/SqliteExtensions.cpp @@ -0,0 +1,916 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2004-2020 musikcube team +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the author nor the names of other contributors may +// be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +////////////////////////////////////////////////////////////////////////////// + +#include "pch.hpp" +#include +#include +#include +#include + +/* NOTE: nearly all of the source here was pulled in from sqlite3.c except for the +extension registration and accentToChar mapping; this is basically a copy/paste of +the default LIKE operator that supports coercing accented characters to standard +english characters to make it easier to search. */ + +#ifndef UINT32_TYPE +# ifdef HAVE_UINT32_T +# define UINT32_TYPE uint32_t +# else +# define UINT32_TYPE unsigned int +# endif +#endif +#ifndef UINT8_TYPE +# ifdef HAVE_UINT8_T +# define UINT8_TYPE uint8_t +# else +# define UINT8_TYPE unsigned char +# endif +#endif + +typedef UINT8_TYPE u8; /* 1-byte unsigned integer */ +typedef UINT32_TYPE u32; /* 4-byte unsigned integer */ + +/* +** Mapping from accented UTF8 sequence to a regular english char +*/ +static std::unordered_map accentToChar = { + { (u32)192 /* À */ , (u32)'A' }, + { (u32)193 /* Á */ , (u32)'A' }, + { (u32)194 /* Â */ , (u32)'A' }, + { (u32)195 /* Ã */ , (u32)'A' }, + { (u32)196 /* Ä */ , (u32)'A' }, + { (u32)197 /* Å */ , (u32)'A' }, + { (u32)199 /* Ç */ , (u32)'C' }, + { (u32)200 /* È */ , (u32)'E' }, + { (u32)201 /* É */ , (u32)'E' }, + { (u32)202 /* Ê */ , (u32)'E' }, + { (u32)203 /* Ë */ , (u32)'E' }, + { (u32)204 /* Ì */ , (u32)'I' }, + { (u32)205 /* Í */ , (u32)'I' }, + { (u32)206 /* Î */ , (u32)'I' }, + { (u32)207 /* Ï */ , (u32)'I' }, + { (u32)209 /* Ñ */ , (u32)'N' }, + { (u32)210 /* Ò */ , (u32)'O' }, + { (u32)211 /* Ó */ , (u32)'O' }, + { (u32)212 /* Ô */ , (u32)'O' }, + { (u32)213 /* Õ */ , (u32)'O' }, + { (u32)214 /* Ö */ , (u32)'O' }, + { (u32)217 /* Ù */ , (u32)'U' }, + { (u32)218 /* Ú */ , (u32)'U' }, + { (u32)219 /* Û */ , (u32)'U' }, + { (u32)220 /* Ü */ , (u32)'U' }, + { (u32)221 /* Ý */ , (u32)'Y' }, + { (u32)223 /* ß */ , (u32)'s' }, + { (u32)224 /* à */ , (u32)'a' }, + { (u32)225 /* á */ , (u32)'a' }, + { (u32)226 /* â */ , (u32)'a' }, + { (u32)227 /* ã */ , (u32)'a' }, + { (u32)228 /* ä */ , (u32)'a' }, + { (u32)229 /* å */ , (u32)'a' }, + { (u32)231 /* ç */ , (u32)'c' }, + { (u32)232 /* è */ , (u32)'e' }, + { (u32)233 /* é */ , (u32)'e' }, + { (u32)234 /* ê */ , (u32)'e' }, + { (u32)235 /* ë */ , (u32)'e' }, + { (u32)236 /* ì */ , (u32)'i' }, + { (u32)237 /* í */ , (u32)'i' }, + { (u32)238 /* î */ , (u32)'i' }, + { (u32)239 /* ï */ , (u32)'i' }, + { (u32)241 /* ñ */ , (u32)'n' }, + { (u32)242 /* ò */ , (u32)'o' }, + { (u32)243 /* ó */ , (u32)'o' }, + { (u32)244 /* ô */ , (u32)'o' }, + { (u32)245 /* õ */ , (u32)'o' }, + { (u32)246 /* ö */ , (u32)'o' }, + { (u32)249 /* ù */ , (u32)'u' }, + { (u32)250 /* ú */ , (u32)'u' }, + { (u32)251 /* û */ , (u32)'u' }, + { (u32)252 /* ü */ , (u32)'u' }, + { (u32)253 /* ý */ , (u32)'y' }, + { (u32)255 /* ÿ */ , (u32)'y' }, + { (u32)256 /* Ā */ , (u32)'A' }, + { (u32)257 /* ā */ , (u32)'a' }, + { (u32)258 /* Ă */ , (u32)'A' }, + { (u32)259 /* ă */ , (u32)'a' }, + { (u32)260 /* Ą */ , (u32)'A' }, + { (u32)261 /* ą */ , (u32)'a' }, + { (u32)262 /* Ć */ , (u32)'C' }, + { (u32)263 /* ć */ , (u32)'c' }, + { (u32)264 /* Ĉ */ , (u32)'C' }, + { (u32)265 /* ĉ */ , (u32)'c' }, + { (u32)266 /* Ċ */ , (u32)'C' }, + { (u32)267 /* ċ */ , (u32)'c' }, + { (u32)268 /* Č */ , (u32)'C' }, + { (u32)269 /* č */ , (u32)'c' }, + { (u32)270 /* Ď */ , (u32)'D' }, + { (u32)271 /* ď */ , (u32)'d' }, + { (u32)272 /* Đ */ , (u32)'D' }, + { (u32)273 /* đ */ , (u32)'d' }, + { (u32)274 /* Ē */ , (u32)'E' }, + { (u32)275 /* ē */ , (u32)'e' }, + { (u32)276 /* Ĕ */ , (u32)'E' }, + { (u32)277 /* ĕ */ , (u32)'e' }, + { (u32)278 /* Ė */ , (u32)'E' }, + { (u32)279 /* ė */ , (u32)'e' }, + { (u32)280 /* Ę */ , (u32)'E' }, + { (u32)281 /* ę */ , (u32)'e' }, + { (u32)282 /* Ě */ , (u32)'E' }, + { (u32)283 /* ě */ , (u32)'e' }, + { (u32)284 /* Ĝ */ , (u32)'G' }, + { (u32)285 /* ĝ */ , (u32)'g' }, + { (u32)286 /* Ğ */ , (u32)'G' }, + { (u32)287 /* ğ */ , (u32)'g' }, + { (u32)288 /* Ġ */ , (u32)'G' }, + { (u32)289 /* ġ */ , (u32)'g' }, + { (u32)290 /* Ģ */ , (u32)'G' }, + { (u32)291 /* ģ */ , (u32)'g' }, + { (u32)292 /* Ĥ */ , (u32)'H' }, + { (u32)293 /* ĥ */ , (u32)'h' }, + { (u32)294 /* Ħ */ , (u32)'H' }, + { (u32)295 /* ħ */ , (u32)'h' }, + { (u32)296 /* Ĩ */ , (u32)'I' }, + { (u32)297 /* ĩ */ , (u32)'i' }, + { (u32)298 /* Ī */ , (u32)'I' }, + { (u32)299 /* ī */ , (u32)'i' }, + { (u32)300 /* Ĭ */ , (u32)'I' }, + { (u32)301 /* ĭ */ , (u32)'i' }, + { (u32)302 /* Į */ , (u32)'I' }, + { (u32)303 /* į */ , (u32)'i' }, + { (u32)304 /* İ */ , (u32)'I' }, + { (u32)305 /* ı */ , (u32)'i' }, + { (u32)308 /* Ĵ */ , (u32)'J' }, + { (u32)309 /* ĵ */ , (u32)'j' }, + { (u32)310 /* Ķ */ , (u32)'K' }, + { (u32)311 /* ķ */ , (u32)'k' }, + { (u32)312 /* ĸ */ , (u32)'k' }, + { (u32)313 /* Ĺ */ , (u32)'L' }, + { (u32)314 /* ĺ */ , (u32)'l' }, + { (u32)315 /* Ļ */ , (u32)'L' }, + { (u32)316 /* ļ */ , (u32)'l' }, + { (u32)317 /* Ľ */ , (u32)'L' }, + { (u32)318 /* ľ */ , (u32)'l' }, + { (u32)319 /* Ŀ */ , (u32)'L' }, + { (u32)320 /* ŀ */ , (u32)'l' }, + { (u32)321 /* Ł */ , (u32)'L' }, + { (u32)322 /* ł */ , (u32)'l' }, + { (u32)323 /* Ń */ , (u32)'N' }, + { (u32)324 /* ń */ , (u32)'n' }, + { (u32)325 /* Ņ */ , (u32)'N' }, + { (u32)326 /* ņ */ , (u32)'n' }, + { (u32)327 /* Ň */ , (u32)'N' }, + { (u32)328 /* ň */ , (u32)'n' }, + { (u32)329 /* ʼn */ , (u32)'N' }, + { (u32)330 /* Ŋ */ , (u32)'n' }, + { (u32)331 /* ŋ */ , (u32)'N' }, + { (u32)332 /* Ō */ , (u32)'O' }, + { (u32)333 /* ō */ , (u32)'o' }, + { (u32)334 /* Ŏ */ , (u32)'O' }, + { (u32)335 /* ŏ */ , (u32)'o' }, + { (u32)336 /* Ő */ , (u32)'O' }, + { (u32)337 /* ő */ , (u32)'o' }, + { (u32)340 /* Ŕ */ , (u32)'R' }, + { (u32)341 /* ŕ */ , (u32)'r' }, + { (u32)342 /* Ŗ */ , (u32)'R' }, + { (u32)343 /* ŗ */ , (u32)'r' }, + { (u32)344 /* Ř */ , (u32)'R' }, + { (u32)345 /* ř */ , (u32)'r' }, + { (u32)346 /* Ś */ , (u32)'S' }, + { (u32)347 /* ś */ , (u32)'s' }, + { (u32)348 /* Ŝ */ , (u32)'S' }, + { (u32)349 /* ŝ */ , (u32)'s' }, + { (u32)350 /* Ş */ , (u32)'S' }, + { (u32)351 /* ş */ , (u32)'s' }, + { (u32)352 /* Š */ , (u32)'S' }, + { (u32)353 /* š */ , (u32)'s' }, + { (u32)354 /* Ţ */ , (u32)'T' }, + { (u32)355 /* ţ */ , (u32)'t' }, + { (u32)356 /* Ť */ , (u32)'T' }, + { (u32)357 /* ť */ , (u32)'t' }, + { (u32)358 /* Ŧ */ , (u32)'T' }, + { (u32)359 /* ŧ */ , (u32)'t' }, + { (u32)360 /* Ũ */ , (u32)'U' }, + { (u32)361 /* ũ */ , (u32)'u' }, + { (u32)362 /* Ū */ , (u32)'U' }, + { (u32)363 /* ū */ , (u32)'u' }, + { (u32)364 /* Ŭ */ , (u32)'U' }, + { (u32)365 /* ŭ */ , (u32)'u' }, + { (u32)366 /* Ů */ , (u32)'U' }, + { (u32)367 /* ů */ , (u32)'u' }, + { (u32)368 /* Ű */ , (u32)'U' }, + { (u32)369 /* ű */ , (u32)'u' }, + { (u32)370 /* Ų */ , (u32)'U' }, + { (u32)371 /* ų */ , (u32)'u' }, + { (u32)372 /* Ŵ */ , (u32)'W' }, + { (u32)373 /* ŵ */ , (u32)'w' }, + { (u32)374 /* Ŷ */ , (u32)'Y' }, + { (u32)375 /* ŷ */ , (u32)'y' }, + { (u32)376 /* Ÿ */ , (u32)'Y' }, + { (u32)377 /* Ź */ , (u32)'Z' }, + { (u32)377 /* Ź */ , (u32)'Z' }, + { (u32)377 /* Ź */ , (u32)'Z' }, + { (u32)377 /* Ź */ , (u32)'Z' }, + { (u32)377 /* Ź */ , (u32)'Z' }, + { (u32)377 /* Ź */ , (u32)'Z' }, + { (u32)377 /* Ź */ , (u32)'Z' }, + { (u32)377 /* Ź */ , (u32)'Z' }, + { (u32)377 /* Ź */ , (u32)'Z' }, + { (u32)377 /* Ź */ , (u32)'Z' }, + { (u32)377 /* Ź */ , (u32)'Z' }, + { (u32)378 /* ź */ , (u32)'z' }, + { (u32)378 /* ź */ , (u32)'z' }, + { (u32)378 /* ź */ , (u32)'z' }, + { (u32)378 /* ź */ , (u32)'z' }, + { (u32)378 /* ź */ , (u32)'z' }, + { (u32)378 /* ź */ , (u32)'z' }, + { (u32)378 /* ź */ , (u32)'z' }, + { (u32)378 /* ź */ , (u32)'z' }, + { (u32)378 /* ź */ , (u32)'z' }, + { (u32)378 /* ź */ , (u32)'z' }, + { (u32)378 /* ź */ , (u32)'z' }, + { (u32)379 /* Ż */ , (u32)'Z' }, + { (u32)379 /* Ż */ , (u32)'Z' }, + { (u32)379 /* Ż */ , (u32)'Z' }, + { (u32)379 /* Ż */ , (u32)'Z' }, + { (u32)379 /* Ż */ , (u32)'Z' }, + { (u32)379 /* Ż */ , (u32)'Z' }, + { (u32)379 /* Ż */ , (u32)'Z' }, + { (u32)379 /* Ż */ , (u32)'Z' }, + { (u32)379 /* Ż */ , (u32)'Z' }, + { (u32)379 /* Ż */ , (u32)'Z' }, + { (u32)379 /* Ż */ , (u32)'Z' }, + { (u32)380 /* ż */ , (u32)'z' }, + { (u32)380 /* ż */ , (u32)'z' }, + { (u32)380 /* ż */ , (u32)'z' }, + { (u32)380 /* ż */ , (u32)'z' }, + { (u32)380 /* ż */ , (u32)'z' }, + { (u32)380 /* ż */ , (u32)'z' }, + { (u32)380 /* ż */ , (u32)'z' }, + { (u32)380 /* ż */ , (u32)'z' }, + { (u32)380 /* ż */ , (u32)'z' }, + { (u32)380 /* ż */ , (u32)'z' }, + { (u32)380 /* ż */ , (u32)'z' }, + { (u32)381 /* Ž */ , (u32)'Z' }, + { (u32)381 /* Ž */ , (u32)'Z' }, + { (u32)381 /* Ž */ , (u32)'Z' }, + { (u32)381 /* Ž */ , (u32)'Z' }, + { (u32)381 /* Ž */ , (u32)'Z' }, + { (u32)381 /* Ž */ , (u32)'Z' }, + { (u32)381 /* Ž */ , (u32)'Z' }, + { (u32)381 /* Ž */ , (u32)'Z' }, + { (u32)381 /* Ž */ , (u32)'Z' }, + { (u32)381 /* Ž */ , (u32)'Z' }, + { (u32)381 /* Ž */ , (u32)'Z' }, + { (u32)382 /* ž */ , (u32)'z' }, + { (u32)382 /* ž */ , (u32)'z' }, + { (u32)382 /* ž */ , (u32)'z' }, + { (u32)382 /* ž */ , (u32)'z' }, + { (u32)382 /* ž */ , (u32)'z' }, + { (u32)382 /* ž */ , (u32)'z' }, + { (u32)382 /* ž */ , (u32)'z' }, + { (u32)382 /* ž */ , (u32)'z' }, + { (u32)382 /* ž */ , (u32)'z' }, + { (u32)382 /* ž */ , (u32)'z' }, + { (u32)382 /* ž */ , (u32)'z' }, + { (u32)383 /* ſ */ , (u32)'s' }, + { (u32)383 /* ſ */ , (u32)'s' }, + { (u32)383 /* ſ */ , (u32)'s' }, + { (u32)383 /* ſ */ , (u32)'s' }, + { (u32)383 /* ſ */ , (u32)'s' }, + { (u32)383 /* ſ */ , (u32)'s' }, + { (u32)383 /* ſ */ , (u32)'s' }, + { (u32)383 /* ſ */ , (u32)'s' }, + { (u32)383 /* ſ */ , (u32)'s' }, + { (u32)383 /* ſ */ , (u32)'s' }, + { (u32)383 /* ſ */ , (u32)'s' }, +}; + +/* +** Check to see if this machine uses EBCDIC. (Yes, believe it or +** not, there are still machines out there that use EBCDIC.) +*/ +#if 'A' == '\301' +# define SQLITE_EBCDIC 1 +#else +# define SQLITE_ASCII 1 +#endif + +/* An array to map all upper-case characters into their corresponding +** lower-case character. +** +** SQLite only considers US-ASCII (or EBCDIC) characters. We do not +** handle case conversions for the UTF character set since the tables +** involved are nearly as big or bigger than SQLite itself. +*/ +const unsigned char sqlite3UpperToLower[] = { +#ifdef SQLITE_ASCII + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 97, 98, 99,100,101,102,103, + 104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121, + 122, 91, 92, 93, 94, 95, 96, 97, 98, 99,100,101,102,103,104,105,106,107, + 108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125, + 126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143, + 144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161, + 162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179, + 180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197, + 198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215, + 216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233, + 234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251, + 252,253,254,255 +#endif +#ifdef SQLITE_EBCDIC + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, /* 0x */ + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, /* 1x */ + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, /* 2x */ + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, /* 3x */ + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, /* 4x */ + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, /* 5x */ + 96, 97, 98, 99,100,101,102,103,104,105,106,107,108,109,110,111, /* 6x */ + 112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127, /* 7x */ + 128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143, /* 8x */ + 144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159, /* 9x */ + 160,161,162,163,164,165,166,167,168,169,170,171,140,141,142,175, /* Ax */ + 176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191, /* Bx */ + 192,129,130,131,132,133,134,135,136,137,202,203,204,205,206,207, /* Cx */ + 208,145,146,147,148,149,150,151,152,153,218,219,220,221,222,223, /* Dx */ + 224,225,162,163,164,165,166,167,168,169,234,235,236,237,238,239, /* Ex */ + 240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255, /* Fx */ +#endif +}; + +/* +** Maximum length (in bytes) of the pattern in a LIKE or GLOB +** operator. +*/ +#ifndef SQLITE_MAX_LIKE_PATTERN_LENGTH +# define SQLITE_MAX_LIKE_PATTERN_LENGTH 50000 +#endif + +/* +** Possible error returns from patternMatch() +*/ +#define SQLITE_MATCH 0 +#define SQLITE_NOMATCH 1 +#define SQLITE_NOWILDCARDMATCH 2 + +/* +** A structure defining how to do GLOB-style comparisons. +*/ +struct compareInfo { + u8 matchAll; /* "*" or "%" */ + u8 matchOne; /* "?" or "_" */ + u8 matchSet; /* "[" or 0 */ + u8 noCase; /* true to ignore case differences */ +}; + +static const struct compareInfo likeInfoNorm = { '%', '_', 0, 1 }; + +/* +** Assuming zIn points to the first byte of a UTF-8 character, +** advance zIn to point to the first byte of the next UTF-8 character. +*/ +#define SQLITE_SKIP_UTF8(zIn) { \ + if( (*(zIn++))>=0xc0 ){ \ + while( (*zIn & 0xc0)==0x80 ){ zIn++; } \ + } \ +} + +/* +** The following macros mimic the standard library functions toupper(), +** isspace(), isalnum(), isdigit() and isxdigit(), respectively. The +** sqlite versions only work for ASCII characters, regardless of locale. +*/ +#ifdef SQLITE_ASCII +# define sqlite3Toupper(x) ((x)&~(sqlite3CtypeMap[(unsigned char)(x)]&0x20)) +# define sqlite3Isspace(x) (sqlite3CtypeMap[(unsigned char)(x)]&0x01) +# define sqlite3Isalnum(x) (sqlite3CtypeMap[(unsigned char)(x)]&0x06) +# define sqlite3Isalpha(x) (sqlite3CtypeMap[(unsigned char)(x)]&0x02) +# define sqlite3Isdigit(x) (sqlite3CtypeMap[(unsigned char)(x)]&0x04) +# define sqlite3Isxdigit(x) (sqlite3CtypeMap[(unsigned char)(x)]&0x08) +# define sqlite3Tolower(x) (sqlite3UpperToLower[(unsigned char)(x)]) +# define sqlite3Isquote(x) (sqlite3CtypeMap[(unsigned char)(x)]&0x80) +#else +# define sqlite3Toupper(x) toupper((unsigned char)(x)) +# define sqlite3Isspace(x) isspace((unsigned char)(x)) +# define sqlite3Isalnum(x) isalnum((unsigned char)(x)) +# define sqlite3Isalpha(x) isalpha((unsigned char)(x)) +# define sqlite3Isdigit(x) isdigit((unsigned char)(x)) +# define sqlite3Isxdigit(x) isxdigit((unsigned char)(x)) +# define sqlite3Tolower(x) tolower((unsigned char)(x)) +# define sqlite3Isquote(x) ((x)=='"'||(x)=='\''||(x)=='['||(x)=='`') +#endif + +/* +** For LIKE and GLOB matching on EBCDIC machines, assume that every +** character is exactly one byte in size. Also, provde the Utf8Read() +** macro for fast reading of the next character in the common case where +** the next character is ASCII. +*/ +#if defined(SQLITE_EBCDIC) +# define sqlite3Utf8Read(A) (*((*A)++)) +# define Utf8Read(A) (*(A++)) +#else +# define Utf8Read(A) (A[0]<0x80?*(A++):sqlite3Utf8Read(&A)) +#endif + +/* +** This lookup table is used to help decode the first byte of +** a multi-byte UTF8 character. +*/ +static const unsigned char sqlite3Utf8Trans1[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x00, 0x00, +}; + +/* +** The following 256 byte lookup table is used to support SQLites built-in +** equivalents to the following standard library functions: +** +** isspace() 0x01 +** isalpha() 0x02 +** isdigit() 0x04 +** isalnum() 0x06 +** isxdigit() 0x08 +** toupper() 0x20 +** SQLite identifier character 0x40 +** Quote character 0x80 +** +** Bit 0x20 is set if the mapped character requires translation to upper +** case. i.e. if the character is a lower-case ASCII character. +** If x is a lower-case ASCII character, then its upper-case equivalent +** is (x - 0x20). Therefore toupper() can be implemented as: +** +** (x & ~(map[x]&0x20)) +** +** The equivalent of tolower() is implemented using the sqlite3UpperToLower[] +** array. tolower() is used more often than toupper() by SQLite. +** +** Bit 0x40 is set if the character is non-alphanumeric and can be used in an +** SQLite identifier. Identifiers are alphanumerics, "_", "$", and any +** non-ASCII UTF character. Hence the test for whether or not a character is +** part of an identifier is 0x46. +*/ +const unsigned char sqlite3CtypeMap[256] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 00..07 ........ */ + 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, /* 08..0f ........ */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 10..17 ........ */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 18..1f ........ */ + 0x01, 0x00, 0x80, 0x00, 0x40, 0x00, 0x00, 0x80, /* 20..27 !"#$%&' */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 28..2f ()*+,-./ */ + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, /* 30..37 01234567 */ + 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 38..3f 89:;<=>? */ + + 0x00, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x02, /* 40..47 @ABCDEFG */ + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, /* 48..4f HIJKLMNO */ + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, /* 50..57 PQRSTUVW */ + 0x02, 0x02, 0x02, 0x80, 0x00, 0x00, 0x00, 0x40, /* 58..5f XYZ[\]^_ */ + 0x80, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x22, /* 60..67 `abcdefg */ + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, /* 68..6f hijklmno */ + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, /* 70..77 pqrstuvw */ + 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, /* 78..7f xyz{|}~. */ + + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 80..87 ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 88..8f ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 90..97 ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* 98..9f ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* a0..a7 ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* a8..af ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* b0..b7 ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* b8..bf ........ */ + + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* c0..c7 ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* c8..cf ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* d0..d7 ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* d8..df ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* e0..e7 ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* e8..ef ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, /* f0..f7 ........ */ + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40 /* f8..ff ........ */ +}; + +/* +** pZ is a UTF-8 encoded unicode string. If nByte is less than zero, +** return the number of unicode characters in pZ up to (but not including) +** the first 0x00 byte. If nByte is not less than zero, return the +** number of unicode characters in the first nByte of pZ (or up to +** the first 0x00, whichever comes first). +*/ +int sqlite3Utf8CharLen(const char* zIn, int nByte) { + int r = 0; + const u8* z = (const u8*)zIn; + const u8* zTerm; + if (nByte >= 0) { + zTerm = &z[nByte]; + } + else { + zTerm = (const u8*)(-1); + } + assert(z <= zTerm); + while (*z != 0 && z < zTerm) { + SQLITE_SKIP_UTF8(z); + r++; + } + return r; +} + +u32 sqlite3Utf8Read( + const unsigned char** pz /* Pointer to string from which to read char */ +) { + unsigned int c; + + /* Same as READ_UTF8() above but without the zTerm parameter. + ** For this routine, we assume the UTF8 string is always zero-terminated. + */ + c = *((*pz)++); + if (c >= 0xc0) { + c = sqlite3Utf8Trans1[c - 0xc0]; + while ((*(*pz) & 0xc0) == 0x80) { + c = (c << 6) + (0x3f & *((*pz)++)); + } + if (c < 0x80 + || (c & 0xFFFFF800) == 0xD800 + || (c & 0xFFFFFFFE) == 0xFFFE) { + c = 0xFFFD; + } + } + auto it = accentToChar.find(c); + return it != accentToChar.end() ? it->second : c; +} + +/* +** Compare two UTF-8 strings for equality where the first string is +** a GLOB or LIKE expression. Return values: +** +** SQLITE_MATCH: Match +** SQLITE_NOMATCH: No match +** SQLITE_NOWILDCARDMATCH: No match in spite of having * or % wildcards. +** +** Globbing rules: +** +** '*' Matches any sequence of zero or more characters. +** +** '?' Matches exactly one character. +** +** [...] Matches one character from the enclosed list of +** characters. +** +** [^...] Matches one character not in the enclosed list. +** +** With the [...] and [^...] matching, a ']' character can be included +** in the list by making it the first character after '[' or '^'. A +** range of characters can be specified using '-'. Example: +** "[a-z]" matches any single lower-case letter. To match a '-', make +** it the last character in the list. +** +** Like matching rules: +** +** '%' Matches any sequence of zero or more characters +** +*** '_' Matches any one character +** +** Ec Where E is the "esc" character and c is any other +** character, including '%', '_', and esc, match exactly c. +** +** The comments within this routine usually assume glob matching. +** +** This routine is usually quick, but can be N**2 in the worst case. +*/ +static int patternCompare( + const u8* zPattern, /* The glob pattern */ + const u8* zString, /* The string to compare against the glob */ + const struct compareInfo* pInfo, /* Information about how to do the compare */ + u32 matchOther /* The escape char (LIKE) or '[' (GLOB) */ +) { + u32 c, c2; /* Next pattern and input string chars */ + u32 matchOne = pInfo->matchOne; /* "?" or "_" */ + u32 matchAll = pInfo->matchAll; /* "*" or "%" */ + u8 noCase = pInfo->noCase; /* True if uppercase==lowercase */ + const u8* zEscaped = 0; /* One past the last escaped input char */ + + while ((c = Utf8Read(zPattern)) != 0) { + if (c == matchAll) { /* Match "*" */ + /* Skip over multiple "*" characters in the pattern. If there + ** are also "?" characters, skip those as well, but consume a + ** single character of the input string for each "?" skipped */ + while ((c = Utf8Read(zPattern)) == matchAll || c == matchOne) { + if (c == matchOne && sqlite3Utf8Read(&zString) == 0) { + return SQLITE_NOWILDCARDMATCH; + } + } + if (c == 0) { + return SQLITE_MATCH; /* "*" at the end of the pattern matches */ + } + else if (c == matchOther) { + if (pInfo->matchSet == 0) { + c = sqlite3Utf8Read(&zPattern); + if (c == 0) return SQLITE_NOWILDCARDMATCH; + } + else { + /* "[...]" immediately follows the "*". We have to do a slow + ** recursive search in this case, but it is an unusual case. */ + assert(matchOther < 0x80); /* '[' is a single-byte character */ + while (*zString) { + int bMatch = patternCompare(&zPattern[-1], zString, pInfo, matchOther); + if (bMatch != SQLITE_NOMATCH) return bMatch; + SQLITE_SKIP_UTF8(zString); + } + return SQLITE_NOWILDCARDMATCH; + } + } + + /* At this point variable c contains the first character of the + ** pattern string past the "*". Search in the input string for the + ** first matching character and recursively continue the match from + ** that point. + ** + ** For a case-insensitive search, set variable cx to be the same as + ** c but in the other case and search the input string for either + ** c or cx. + */ + if (c <= 0x80) { + char zStop[3]; + int bMatch; + if (noCase) { + zStop[0] = sqlite3Toupper(c); + zStop[1] = sqlite3Tolower(c); + zStop[2] = 0; + } + else { + zStop[0] = c; + zStop[1] = 0; + } + while (1) { + zString += strcspn((const char*)zString, zStop); + if (zString[0] == 0) break; + zString++; + bMatch = patternCompare(zPattern, zString, pInfo, matchOther); + if (bMatch != SQLITE_NOMATCH) return bMatch; + } + } + else { + int bMatch; + while ((c2 = Utf8Read(zString)) != 0) { + if (c2 != c) continue; + bMatch = patternCompare(zPattern, zString, pInfo, matchOther); + if (bMatch != SQLITE_NOMATCH) return bMatch; + } + } + return SQLITE_NOWILDCARDMATCH; + } + if (c == matchOther) { + if (pInfo->matchSet == 0) { + c = sqlite3Utf8Read(&zPattern); + if (c == 0) return SQLITE_NOMATCH; + zEscaped = zPattern; + } + else { + u32 prior_c = 0; + int seen = 0; + int invert = 0; + c = sqlite3Utf8Read(&zString); + if (c == 0) return SQLITE_NOMATCH; + c2 = sqlite3Utf8Read(&zPattern); + if (c2 == '^') { + invert = 1; + c2 = sqlite3Utf8Read(&zPattern); + } + if (c2 == ']') { + if (c == ']') seen = 1; + c2 = sqlite3Utf8Read(&zPattern); + } + while (c2 && c2 != ']') { + if (c2 == '-' && zPattern[0] != ']' && zPattern[0] != 0 && prior_c > 0) { + c2 = sqlite3Utf8Read(&zPattern); + if (c >= prior_c && c <= c2) seen = 1; + prior_c = 0; + } + else { + if (c == c2) { + seen = 1; + } + prior_c = c2; + } + c2 = sqlite3Utf8Read(&zPattern); + } + if (c2 == 0 || (seen ^ invert) == 0) { + return SQLITE_NOMATCH; + } + continue; + } + } + c2 = Utf8Read(zString); + if (c == c2) continue; + if (noCase && sqlite3Tolower(c) == sqlite3Tolower(c2) && c < 0x80 && c2 < 0x80) { + continue; + } + if (c == matchOne && zPattern != zEscaped && c2 != 0) continue; + return SQLITE_NOMATCH; + } + return *zString == 0 ? SQLITE_MATCH : SQLITE_NOMATCH; +} + +/* +** Implementation of the like() SQL function. This function implements +** the build-in LIKE operator. The first argument to the function is the +** pattern and the second argument is the string. So, the SQL statements: +** +** A LIKE B +** +** is implemented as like(B,A). +** +** This same function (with a different compareInfo structure) computes +** the GLOB operator. +*/ +static void likeFunc( + sqlite3_context* context, + int argc, + sqlite3_value** argv +) { + const unsigned char* zA, * zB; + u32 escape; + sqlite3* db = sqlite3_context_db_handle(context); + struct compareInfo* pInfo = (compareInfo*) sqlite3_user_data(context); + struct compareInfo backupInfo; + +#ifdef SQLITE_LIKE_DOESNT_MATCH_BLOBS + if (sqlite3_value_type(argv[0]) == SQLITE_BLOB + || sqlite3_value_type(argv[1]) == SQLITE_BLOB + ) { + sqlite3_result_int(context, 0); + return; + } +#endif + + /* Limit the length of the LIKE or GLOB pattern to avoid problems + ** of deep recursion and N*N behavior in patternCompare(). + */ + + if (sqlite3_value_bytes(argv[0]) > SQLITE_MAX_LIKE_PATTERN_LENGTH) { + sqlite3_result_error(context, "LIKE or GLOB pattern too complex", -1); + return; + } + + if (argc == 3) { + /* The escape character string must consist of a single UTF-8 character. + ** Otherwise, return an error. + */ + const unsigned char* zEsc = sqlite3_value_text(argv[2]); + if (zEsc == 0) return; + if (sqlite3Utf8CharLen((char*)zEsc, -1) != 1) { + sqlite3_result_error(context, + "ESCAPE expression must be a single character", -1); + return; + } + escape = sqlite3Utf8Read(&zEsc); + if (escape == pInfo->matchAll || escape == pInfo->matchOne) { + memcpy(&backupInfo, pInfo, sizeof(backupInfo)); + pInfo = &backupInfo; + if (escape == pInfo->matchAll) pInfo->matchAll = 0; + if (escape == pInfo->matchOne) pInfo->matchOne = 0; + } + } + else { + escape = pInfo->matchSet; + } + zB = sqlite3_value_text(argv[0]); + zA = sqlite3_value_text(argv[1]); + if (zA && zB) { + sqlite3_result_int(context, + patternCompare(zB, zA, pInfo, escape) == SQLITE_MATCH); + } +} + +/* +** Function to delete compiled regexp objects. Registered as +** a destructor function with sqlite3_set_auxdata(). +*/ +static void regexpDelete(void* p) { + std::regex* regex = (std::regex*) p; + delete regex; +} + +/* +** Implementation of SQLite REGEXP operator. This scalar function takes +** two arguments. The first is a regular expression pattern to compile +** the second is a string to match against that pattern. If either +** argument is an SQL NULL, then NULL Is returned. Otherwise, the result +** is 1 if the string matches the pattern, or 0 otherwise. +** +** SQLite maps the regexp() function to the regexp() operator such +** that the following two are equivalent: +** +** zString REGEXP zPattern +** regexp(zPattern, zString) +** +** Uses standard C++ std::regex +*/ + +static auto kRegexFlags = + std::regex::icase | + std::regex::optimize | + std::regex::collate | + std::regex::ECMAScript; + +static auto kMatchFlags = std::regex_constants::match_any; + +static void regexpFunc(sqlite3_context* context, int nArg, sqlite3_value** apArg) { + const char* matchAgainst = (const char*) sqlite3_value_text(apArg[1]); + + if (!matchAgainst) { + return; + } + + std::regex* regex = (std::regex*) sqlite3_get_auxdata(context, 0); + if (!regex) { + const char* pattern = (const char*) sqlite3_value_text(apArg[0]); + if (!pattern) { + return; + } + + try { + regex = new std::regex(pattern, kRegexFlags); + } + catch (std::regex_error) { + return; + } + + sqlite3_set_auxdata(context, 0, regex, regexpDelete); + } + + /* Return 1 or 0. */ + sqlite3_result_int(context, std::regex_search(matchAgainst, *regex, kMatchFlags) ? 1 : 0); +} + +namespace musik { namespace core { namespace db { + + namespace SqliteExtensions { + + int Register(sqlite3* db) { + static const struct Scalar { + const char* zName; /* Function name */ + unsigned char nArg; /* Number of arguments */ + unsigned int enc; /* Optimal text encoding */ + void (*xFunc)(sqlite3_context*, int, sqlite3_value**); + } scalars[] = { + {"regexp", 2, SQLITE_ANY | SQLITE_DETERMINISTIC| SQLITE_INNOCUOUS, regexpFunc}, + {"like", 2, SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS, likeFunc}, + {"like", 3, SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS, likeFunc}, + }; + int rc = SQLITE_OK; + for (int i = 0; rc == SQLITE_OK && i < (int)(sizeof(scalars) / sizeof(scalars[0])); i++) { + const struct Scalar* p = &scalars[i]; + rc = sqlite3_create_function( + db, + p->zName, + p->nArg, + p->enc, + (void*)&likeInfoNorm, + p->xFunc, + 0, + 0); + } + return rc; + } + + } + +} } } + diff --git a/src/musikcore/db/SqliteExtensions.h b/src/musikcore/db/SqliteExtensions.h new file mode 100644 index 000000000..ecd3c2cd8 --- /dev/null +++ b/src/musikcore/db/SqliteExtensions.h @@ -0,0 +1,51 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2004-2020 musikcube team +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the author nor the names of other contributors may +// be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +struct sqlite3; + +namespace musik { namespace core { namespace db { + + namespace SqliteExtensions { + + int Register(sqlite3* db); + + } + +} } } + diff --git a/src/musikcore/library/LocalMetadataProxy.cpp b/src/musikcore/library/LocalMetadataProxy.cpp index 2486ef5bd..e213745d1 100644 --- a/src/musikcore/library/LocalMetadataProxy.cpp +++ b/src/musikcore/library/LocalMetadataProxy.cpp @@ -305,6 +305,7 @@ ITrackList* LocalMetadataProxy::QueryTracks(const char* query, int limit, int of std::shared_ptr search( new SearchTrackListQuery( this->library, + SearchTrackListQuery::MatchType::Substring, std::string(query ? query : ""), TrackSortType::Album)); @@ -456,6 +457,7 @@ IValueList* LocalMetadataProxy::QueryCategoryWithPredicate( std::shared_ptr search( new CategoryListQuery( + CategoryListQuery::MatchType::Substring, type, { field, predicateId }, std::string(filter ? filter : ""))); @@ -480,7 +482,10 @@ IValueList* LocalMetadataProxy::QueryCategoryWithPredicates( auto predicateList = toPredicateList(predicates, predicateCount); auto query = std::make_shared( - type, predicateList, std::string(filter ? filter : "")); + CategoryListQuery::MatchType::Substring, + type, + predicateList, + std::string(filter ? filter : "")); this->library->EnqueueAndWait(query); diff --git a/src/musikcore/library/QueryBase.h b/src/musikcore/library/QueryBase.h index cf72de229..83db6723b 100644 --- a/src/musikcore/library/QueryBase.h +++ b/src/musikcore/library/QueryBase.h @@ -50,6 +50,11 @@ namespace musik { namespace core { namespace library { namespace query { public sigslot::has_slots<> { public: + enum class MatchType : int { + Substring = 1, + Regex = 2 + }; + QueryBase() : status(IQuery::Idle) , options(0) diff --git a/src/musikcore/library/query/CategoryListQuery.cpp b/src/musikcore/library/query/CategoryListQuery.cpp index 29763b939..4aa35260f 100755 --- a/src/musikcore/library/query/CategoryListQuery.cpp +++ b/src/musikcore/library/query/CategoryListQuery.cpp @@ -56,35 +56,44 @@ static const std::string kUnfilteredPlaylistsQuery = static const std::string kFilteredPlaylistsQuery = "SELECT DISTINCT id, name " "FROM playlists" - "WHERE LOWER(name) LIKE LOWER(?) " + "WHERE LOWER(name) {{match_type}} ? " "ORDER BY name;"; const std::string CategoryListQuery::kQueryName = "CategoryListQuery"; +static std::string getMatchType(CategoryListQuery::MatchType matchType) { + return matchType == CategoryListQuery::MatchType::Regex ? "REGEXP" : "LIKE"; +} + CategoryListQuery::CategoryListQuery() { } CategoryListQuery::CategoryListQuery( - const std::string& trackField, const std::string& filter) -: CategoryListQuery(trackField, category::PredicateList(), filter) { + MatchType matchType, + const std::string& trackField, + const std::string& filter) +: CategoryListQuery(matchType, trackField, category::PredicateList(), filter) { } CategoryListQuery::CategoryListQuery( + MatchType matchType, const std::string& trackField, const category::Predicate predicate, const std::string& filter) -: CategoryListQuery(trackField, category::PredicateList{ predicate }, filter) { +: CategoryListQuery(matchType, trackField, category::PredicateList{ predicate }, filter) { } CategoryListQuery::CategoryListQuery( + MatchType matchType, const std::string& trackField, const category::PredicateList predicates, const std::string& filter) -: trackField(trackField) +: matchType(matchType) +, trackField(trackField) , filter(filter) { result.reset(new SdkValueList()); - if (this->filter.size()) { + if (this->filter.size() && this->matchType == MatchType::Substring) { /* transform "FilteR" => "%filter%" */ std::string wild = this->filter; std::transform(wild.begin(), wild.end(), wild.begin(), tolower); @@ -134,6 +143,8 @@ void CategoryListQuery::QueryPlaylist(musik::core::db::Connection& db) { ? kFilteredPlaylistsQuery : kUnfilteredPlaylistsQuery; + category::ReplaceAll(query, "{{match_type}}", getMatchType(matchType)); + Statement stmt(query.c_str() , db); if (filtered) { @@ -157,6 +168,7 @@ void CategoryListQuery::QueryRegular(musik::core::db::Connection &db) { if (this->filter.size()) { regularFilter = category::REGULAR_FILTER; category::ReplaceAll(regularFilter, "{{table}}", prop.first); + category::ReplaceAll(regularFilter, "{{match_type}}", getMatchType(matchType)); args.push_back(category::StringArgument(this->filter)); } @@ -184,6 +196,7 @@ void CategoryListQuery::QueryExtended(musik::core::db::Connection &db) { if (this->filter.size()) { extendedFilter = category::EXTENDED_FILTER; args.push_back(category::StringArgument(this->filter)); + category::ReplaceAll(extendedFilter, "{{match_type}}", getMatchType(matchType)); } category::ReplaceAll(query, "{{regular_predicates}}", regular); @@ -242,6 +255,7 @@ std::string CategoryListQuery::SerializeQuery() { query["options"] = { { "trackField", this->trackField }, { "filter", this->filter }, + { "matchType", this->matchType }, { "outputType", this->outputType }, { "regularPredicateList", PredicateListToJson(this->regular) }, { "extendedPredicateList", PredicateListToJson(this->extended) } @@ -269,6 +283,7 @@ std::shared_ptr CategoryListQuery::DeserializeQuery(const std std::shared_ptr result(new CategoryListQuery()); result->trackField = options.value("trackField", ""); result->filter = options.value("filter", ""); + result->matchType = options.value("matchType", MatchType::Substring); result->outputType = (OutputType)options.value("outputType", OutputType::Regular); PredicateListFromJson(options["regularPredicateList"], result->regular); PredicateListFromJson(options["extendedPredicateList"], result->extended); diff --git a/src/musikcore/library/query/CategoryListQuery.h b/src/musikcore/library/query/CategoryListQuery.h index c59872a29..3cd0bfe02 100755 --- a/src/musikcore/library/query/CategoryListQuery.h +++ b/src/musikcore/library/query/CategoryListQuery.h @@ -52,15 +52,18 @@ namespace musik { namespace core { namespace library { namespace query { using Result = SdkValueList::Shared; CategoryListQuery( + MatchType matchType, const std::string& trackField, const std::string& filter = ""); CategoryListQuery( + MatchType matchType, const std::string& trackField, const category::Predicate predicate, const std::string& filter = ""); CategoryListQuery( + MatchType matchType, const std::string& trackField, const category::PredicateList predicate, const std::string& filter = ""); @@ -100,6 +103,7 @@ namespace musik { namespace core { namespace library { namespace query { std::string trackField; std::string filter; + MatchType matchType; OutputType outputType; category::PredicateList regular, extended; Result result; diff --git a/src/musikcore/library/query/SearchTrackListQuery.cpp b/src/musikcore/library/query/SearchTrackListQuery.cpp index fe5a21f09..592d02c4f 100755 --- a/src/musikcore/library/query/SearchTrackListQuery.cpp +++ b/src/musikcore/library/query/SearchTrackListQuery.cpp @@ -61,9 +61,13 @@ using namespace boost::algorithm; const std::string SearchTrackListQuery::kQueryName = "SearchTrackListQuery"; SearchTrackListQuery::SearchTrackListQuery( - ILibraryPtr library, const std::string& filter, TrackSortType sort) + ILibraryPtr library, + MatchType matchType, + const std::string& filter, + TrackSortType sort) { this->library = library; + this->matchType = matchType; this->sortType = sort; this->filter = filter; @@ -105,11 +109,10 @@ bool SearchTrackListQuery::OnRun(Connection& db) { headers.reset(new std::set()); } + bool useRegex = (matchType == MatchType::Regex); bool hasFilter = (this->filter.size() > 0); - std::string lastAlbum; size_t index = 0; - std::string query; if (hasFilter) { @@ -119,9 +122,11 @@ bool SearchTrackListQuery::OnRun(Connection& db) { "WHERE " " tracks.visible=1 AND " + this->orderByPredicate + - "(tracks.title LIKE ? OR al.name LIKE ? OR ar.name LIKE ? OR gn.name LIKE ?) " + "(tracks.title {{match_type}} ? OR al.name {{match_type}} ? OR ar.name {{match_type}} ? OR gn.name {{match_type}} ?) " " AND tracks.album_id=al.id AND tracks.visual_genre_id=gn.id AND tracks.visual_artist_id=ar.id " "ORDER BY " + this->orderBy + " "; + + ReplaceAll(query, "{{match_type}}", useRegex ? "REGEXP" : "LIKE"); } else { query = @@ -139,11 +144,13 @@ bool SearchTrackListQuery::OnRun(Connection& db) { Statement trackQuery(query.c_str(), db); if (hasFilter) { - std::string filterWithWildcard = "%" + trim_copy(to_lower_copy(filter)) + "%"; - trackQuery.BindText(0, filterWithWildcard); - trackQuery.BindText(1, filterWithWildcard); - trackQuery.BindText(2, filterWithWildcard); - trackQuery.BindText(3, filterWithWildcard); + std::string patternToMatch = useRegex + ? filter : "%" + trim_copy(to_lower_copy(filter)) + "%"; + + trackQuery.BindText(0, patternToMatch); + trackQuery.BindText(1, patternToMatch); + trackQuery.BindText(2, patternToMatch); + trackQuery.BindText(3, patternToMatch); } while (trackQuery.Step() == Row) { @@ -173,7 +180,8 @@ std::string SearchTrackListQuery::SerializeQuery() { { "name", kQueryName }, { "options", { { "filter", filter }, - { "sortType", sortType} + { "matchType", matchType }, + { "sortType", sortType } }} }; return FinalizeSerializedQueryWithLimitAndOffset(output); @@ -194,6 +202,7 @@ std::shared_ptr SearchTrackListQuery::DeserializeQuery(mus auto options = nlohmann::json::parse(data)["options"]; auto result = std::make_shared( library, + options.value("matchType", MatchType::Substring), options["filter"].get(), options["sortType"].get()); result->ExtractLimitAndOffsetFromDeserializedQuery(options); diff --git a/src/musikcore/library/query/SearchTrackListQuery.h b/src/musikcore/library/query/SearchTrackListQuery.h index ebe7c34c0..39c82123c 100755 --- a/src/musikcore/library/query/SearchTrackListQuery.h +++ b/src/musikcore/library/query/SearchTrackListQuery.h @@ -45,6 +45,7 @@ namespace musik { namespace core { namespace library { namespace query { SearchTrackListQuery( musik::core::ILibraryPtr library, + MatchType matchType, const std::string& filter, TrackSortType sort); @@ -70,6 +71,7 @@ namespace musik { namespace core { namespace library { namespace query { private: musik::core::ILibraryPtr library; + MatchType matchType; bool parseHeaders; std::string orderBy; std::string orderByPredicate; diff --git a/src/musikcore/library/query/util/CategoryQueryUtil.h b/src/musikcore/library/query/util/CategoryQueryUtil.h index 5bbaa4e10..e41bd95ba 100644 --- a/src/musikcore/library/query/util/CategoryQueryUtil.h +++ b/src/musikcore/library/query/util/CategoryQueryUtil.h @@ -71,10 +71,10 @@ namespace musik { namespace core { namespace library { namespace query { }; static const std::string REGULAR_PREDICATE = " tracks.{{fk_id}}=? "; - static const std::string REGULAR_FILTER = " AND LOWER({{table}}.name) LIKE ? "; + static const std::string REGULAR_FILTER = " AND LOWER({{table}}.name) {{match_type}} ? "; static const std::string EXTENDED_PREDICATE = " (key=? AND meta_value_id=?) "; - static const std::string EXTENDED_FILTER = " AND LOWER(extended_metadata.value) LIKE ?"; + static const std::string EXTENDED_FILTER = " AND LOWER(extended_metadata.value) {{match_type}} ?"; static const std::string EXTENDED_INNER_JOIN = "INNER JOIN ( " @@ -193,7 +193,7 @@ namespace musik { namespace core { namespace library { namespace query { and other supplementary information. */ static const std::string ALBUM_LIST_FILTER = - " AND (LOWER(album) like ? OR LOWER(album_artist) like ?) "; + " AND (LOWER(album) LIKE ? OR LOWER(album_artist) LIKE ?) "; static const std::string ALBUM_LIST_QUERY = "SELECT DISTINCT " diff --git a/src/musikcore/musikcore.vcxproj b/src/musikcore/musikcore.vcxproj index 35bd36510..10412e12f 100644 --- a/src/musikcore/musikcore.vcxproj +++ b/src/musikcore/musikcore.vcxproj @@ -514,6 +514,7 @@ + @@ -595,6 +596,7 @@ + diff --git a/src/musikcore/musikcore.vcxproj.filters b/src/musikcore/musikcore.vcxproj.filters index da06b69c5..b6b407d56 100644 --- a/src/musikcore/musikcore.vcxproj.filters +++ b/src/musikcore/musikcore.vcxproj.filters @@ -262,6 +262,9 @@ src\net + + src\db + @@ -642,5 +645,8 @@ src\net + + src\db + \ No newline at end of file diff --git a/src/musikcore/version.h b/src/musikcore/version.h index cee3c57fa..dbc9c6705 100644 --- a/src/musikcore/version.h +++ b/src/musikcore/version.h @@ -37,10 +37,10 @@ #include #define VERSION_MAJOR 0 -#define VERSION_MINOR 94 +#define VERSION_MINOR 95 #define VERSION_PATCH 0 -#define VERSION_COMMIT_HASH "#d85c1002" -#define VERSION "0.94.0" +#define VERSION_COMMIT_HASH "#79637136" +#define VERSION "0.95.0" namespace musik { namespace cube { diff --git a/src/musikcube/app/layout/CategorySearchLayout.cpp b/src/musikcube/app/layout/CategorySearchLayout.cpp index 77a7ecc75..dfde0ff1d 100644 --- a/src/musikcube/app/layout/CategorySearchLayout.cpp +++ b/src/musikcube/app/layout/CategorySearchLayout.cpp @@ -88,15 +88,21 @@ CategorySearchLayout::~CategorySearchLayout() { void CategorySearchLayout::LoadLastSession() { auto session = Preferences::ForComponent(components::Session); + const std::string lastFilter = session->GetString(keys::LastCategoryFilter); if (lastFilter.size()) { this->input->SetText(lastFilter); } + + this->matchType = static_cast(session->GetInt( + keys::LastCategoryFilterMatchType, + static_cast(MatchType::Substring))); } void CategorySearchLayout::SaveSession() { auto session = Preferences::ForComponent(components::Session); session->SetString(keys::LastCategoryFilter, this->input->GetText().c_str()); + session->SetInt(keys::LastCategoryFilterMatchType, static_cast(this->matchType)); } void CategorySearchLayout::OnLayout() { @@ -106,7 +112,10 @@ void CategorySearchLayout::OnLayout() { size_t inputWidth = cx / 2; size_t inputX = x + ((cx - inputWidth) / 2); this->input->MoveAndResize(inputX, 0, cx / 2, SEARCH_HEIGHT); - this->input->SetHint(_TSTR("search_filter_hint")); + + bool inputIsRegex = this->matchType == MatchType::Regex; + this->input->SetHint(_TSTR(inputIsRegex ? "search_regex_hint" : "search_filter_hint")); + this->input->SetFocusedFrameColor(inputIsRegex ? Color::FrameImportant : Color::FrameFocused); size_t labelY = SEARCH_HEIGHT; size_t categoryWidth = cx / 3; @@ -134,9 +143,9 @@ void CategorySearchLayout::InitializeWindows(musik::core::audio::PlaybackService void CategorySearchLayout::Requery() { const std::string& value = this->input->GetText(); - this->albums->Requery(value); - this->artists->Requery(value); - this->genres->Requery(value); + this->albums->Requery(this->matchType, value); + this->artists->Requery(this->matchType, value); + this->genres->Requery(this->matchType, value); } void CategorySearchLayout::FocusInput() { @@ -198,6 +207,10 @@ bool CategorySearchLayout::KeyPress(const std::string& key) { return true; } } + else if (Hotkeys::Is(Hotkeys::SearchInputToggleMatchType, key)) { + this->ToggleMatchType(); + return true; + } return LayoutBase::KeyPress(key); } @@ -211,3 +224,16 @@ void CategorySearchLayout::ProcessMessage(IMessage &message) { } } +void CategorySearchLayout::ToggleMatchType() { + const bool isRegex = this->matchType == MatchType::Regex; + this->SetMatchType(isRegex ? MatchType::Substring : MatchType::Regex); +} + +void CategorySearchLayout::SetMatchType(MatchType matchType) { + if (matchType != this->matchType) { + this->matchType = matchType; + this->Layout(); + this->Requery(); + } +} + diff --git a/src/musikcube/app/layout/CategorySearchLayout.h b/src/musikcube/app/layout/CategorySearchLayout.h index da820da94..487a8ca34 100644 --- a/src/musikcube/app/layout/CategorySearchLayout.h +++ b/src/musikcube/app/layout/CategorySearchLayout.h @@ -74,6 +74,8 @@ namespace musik { virtual void ProcessMessage(musik::core::runtime::IMessage &message); private: + using MatchType = musik::core::library::query::QueryBase::MatchType; + void InitializeWindows(musik::core::audio::PlaybackService& playback); void OnCategoryEntryActivated(cursespp::ListWindow* sender, size_t index); void Requery(); @@ -85,7 +87,11 @@ namespace musik { cursespp::TextInput* sender, std::string value); + void ToggleMatchType(); + void SetMatchType(MatchType matchType); + musik::core::ILibraryPtr library; + MatchType matchType{ MatchType::Substring }; std::shared_ptr prefs; std::shared_ptr albums; std::shared_ptr artists; diff --git a/src/musikcube/app/layout/TrackSearchLayout.cpp b/src/musikcube/app/layout/TrackSearchLayout.cpp index d4a882794..a53ea3f1a 100755 --- a/src/musikcube/app/layout/TrackSearchLayout.cpp +++ b/src/musikcube/app/layout/TrackSearchLayout.cpp @@ -86,15 +86,21 @@ TrackSearchLayout::~TrackSearchLayout() { void TrackSearchLayout::LoadLastSession() { auto session = Preferences::ForComponent(components::Session); + const std::string lastFilter = session->GetString(keys::LastTrackFilter); if (lastFilter.size()) { this->input->SetText(lastFilter); } + + this->matchType = static_cast(session->GetInt( + keys::LastTrackFilterMatchType, + static_cast(MatchType::Substring))); } void TrackSearchLayout::SaveSession() { auto session = Preferences::ForComponent(components::Session); session->SetString(keys::LastTrackFilter, this->input->GetText().c_str()); + session->SetInt(keys::LastTrackFilterMatchType, static_cast(this->matchType)); } void TrackSearchLayout::OnLayout() { @@ -104,7 +110,10 @@ void TrackSearchLayout::OnLayout() { size_t inputWidth = cx / 2; size_t inputX = x + ((cx - inputWidth) / 2); this->input->MoveAndResize(inputX, y, cx / 2, SEARCH_HEIGHT); - this->input->SetHint(_TSTR("search_filter_hint")); + + bool inputIsRegex = this->matchType == MatchType::Regex; + this->input->SetHint(_TSTR(inputIsRegex ? "search_regex_hint" : "search_filter_hint")); + this->input->SetFocusedFrameColor(inputIsRegex ? Color::FrameImportant : Color::FrameFocused); this->trackList->MoveAndResize( x, @@ -149,7 +158,8 @@ void TrackSearchLayout::Requery() { const std::string& filter = this->input->GetText(); const TrackSortType sortOrder = getDefaultTrackSort(this->prefs); this->trackList->Requery(std::shared_ptr( - new SearchTrackListQuery(this->library, filter, sortOrder))); + new SearchTrackListQuery( + this->library, this->matchType, filter, sortOrder))); } void TrackSearchLayout::PlayFromTop() { @@ -187,6 +197,19 @@ void TrackSearchLayout::OnEnterPressed(cursespp::TextInput* sender) { } } +void TrackSearchLayout::ToggleMatchType() { + const bool isRegex = this->matchType == MatchType::Regex; + this->SetMatchType(isRegex ? MatchType::Substring : MatchType::Regex); +} + +void TrackSearchLayout::SetMatchType(MatchType matchType) { + if (matchType != this->matchType) { + this->matchType = matchType; + this->Layout(); + this->Requery(); + } +} + bool TrackSearchLayout::KeyPress(const std::string& key) { if (Hotkeys::Is(Hotkeys::Down, key)) { if (this->GetFocus() == this->input) { @@ -210,5 +233,9 @@ bool TrackSearchLayout::KeyPress(const std::string& key) { }); return true; } + else if (Hotkeys::Is(Hotkeys::SearchInputToggleMatchType, key)) { + this->ToggleMatchType(); + return true; + } return LayoutBase::KeyPress(key); } diff --git a/src/musikcube/app/layout/TrackSearchLayout.h b/src/musikcube/app/layout/TrackSearchLayout.h index c5506f99d..752796ec1 100755 --- a/src/musikcube/app/layout/TrackSearchLayout.h +++ b/src/musikcube/app/layout/TrackSearchLayout.h @@ -70,6 +70,8 @@ namespace musik { virtual void OnLayout(); private: + using MatchType = musik::core::library::query::QueryBase::MatchType; + void SaveSession(); void InitializeWindows(); void Requery(); @@ -82,8 +84,12 @@ namespace musik { void OnEnterPressed(cursespp::TextInput* sender); + void ToggleMatchType(); + void SetMatchType(MatchType matchType); + musik::core::audio::PlaybackService& playback; musik::core::ILibraryPtr library; + MatchType matchType{ MatchType::Substring }; std::shared_ptr prefs; std::shared_ptr trackList; std::shared_ptr input; diff --git a/src/musikcube/app/overlay/PlayQueueOverlays.cpp b/src/musikcube/app/overlay/PlayQueueOverlays.cpp index 5fecd35d7..675fcb60a 100644 --- a/src/musikcube/app/overlay/PlayQueueOverlays.cpp +++ b/src/musikcube/app/overlay/PlayQueueOverlays.cpp @@ -151,8 +151,8 @@ static void queryPlaylists( ILibraryPtr library, std::function)> callback) { - std::shared_ptr query( - new CategoryListQuery(Playlists::TABLE_NAME, "")); + std::shared_ptr query(new CategoryListQuery( + CategoryListQuery::MatchType::Substring, Playlists::TABLE_NAME, "")); library->Enqueue(query, [callback, query](auto q) { callback(query->GetStatus() == IQuery::Finished diff --git a/src/musikcube/app/util/Hotkeys.cpp b/src/musikcube/app/util/Hotkeys.cpp index bceb6a1b8..955a1ab15 100755 --- a/src/musikcube/app/util/Hotkeys.cpp +++ b/src/musikcube/app/util/Hotkeys.cpp @@ -104,6 +104,8 @@ static std::unordered_map NAME_TO_ID = { { "track_list_previous_group", Id::TrackListPreviousGroup }, { "track_list_play_from_top", Id::TrackListPlayFromTop }, + { "search_input_toggle_match_type", Id::SearchInputToggleMatchType }, + { "lyrics_retry", Id::LyricsRetry }, { "playback_toggle_mute", Id::ToggleMute }, @@ -196,6 +198,8 @@ static std::unordered_map ID_TO_DEFAULT = { { Id::TrackListPreviousGroup, "[" }, { Id::TrackListPlayFromTop, "M-P" }, + { Id::SearchInputToggleMatchType, "M-m" }, + { Id::LyricsRetry, "r" }, { Id::ToggleMute, "m" }, diff --git a/src/musikcube/app/util/Hotkeys.h b/src/musikcube/app/util/Hotkeys.h index f640ede2a..5acd4cdbd 100755 --- a/src/musikcube/app/util/Hotkeys.h +++ b/src/musikcube/app/util/Hotkeys.h @@ -115,6 +115,9 @@ namespace musik { TrackListPreviousGroup, TrackListPlayFromTop, + /* search input */ + SearchInputToggleMatchType, + /* lyrics */ LyricsRetry, diff --git a/src/musikcube/app/util/PreferenceKeys.cpp b/src/musikcube/app/util/PreferenceKeys.cpp index 3a4663c04..1b2d3620e 100644 --- a/src/musikcube/app/util/PreferenceKeys.cpp +++ b/src/musikcube/app/util/PreferenceKeys.cpp @@ -51,7 +51,9 @@ namespace musik { namespace cube { namespace prefs { const std::string keys::LastBrowseCategoryId = "LastBrowseCategoryId"; const std::string keys::LastBrowseDirectoryRoot = "LastBrowseDirectoryRoot"; const std::string keys::LastCategoryFilter = "LastCategoryFilter"; + const std::string keys::LastCategoryFilterMatchType = "LastCategoryFilterMatchType"; const std::string keys::LastTrackFilter = "LastTrackFilter"; + const std::string keys::LastTrackFilterMatchType = "LastTrackFilterMatchType"; const std::string keys::TrackSearchSortOrder = "TrackSearchSortOrder"; const std::string keys::CategoryTrackListSortOrder = "CategoryTrackListSortOrder"; const std::string keys::RatingPositiveChar = "RatingPositiveChar"; diff --git a/src/musikcube/app/util/PreferenceKeys.h b/src/musikcube/app/util/PreferenceKeys.h index 630f640c8..e143edd25 100644 --- a/src/musikcube/app/util/PreferenceKeys.h +++ b/src/musikcube/app/util/PreferenceKeys.h @@ -53,7 +53,9 @@ namespace musik { namespace cube { namespace prefs { extern const std::string LastBrowseCategoryId; extern const std::string LastBrowseDirectoryRoot; extern const std::string LastCategoryFilter; + extern const std::string LastCategoryFilterMatchType; extern const std::string LastTrackFilter; + extern const std::string LastTrackFilterMatchType; extern const std::string TrackSearchSortOrder; extern const std::string CategoryTrackListSortOrder; extern const std::string RatingPositiveChar; diff --git a/src/musikcube/app/window/CategoryListView.cpp b/src/musikcube/app/window/CategoryListView.cpp index 8b0c9817d..c2b394ed5 100755 --- a/src/musikcube/app/window/CategoryListView.cpp +++ b/src/musikcube/app/window/CategoryListView.cpp @@ -97,6 +97,7 @@ void CategoryListView::OnVisibilityChanged(bool visible) { } void CategoryListView::RequeryWithField( + MatchType matchType, const std::string& fieldName, const std::string& filter, const int64_t selectAfterQuery) @@ -105,24 +106,45 @@ void CategoryListView::RequeryWithField( this->activeQuery->Cancel(); } + this->matchType = matchType; this->fieldName = fieldName; this->fieldIdColumn = getFieldIdColumn(fieldName); this->selectAfterQuery = selectAfterQuery; this->filter = filter; - this->activeQuery.reset(new CategoryListQuery(fieldName, filter)); + this->activeQuery = std::make_shared(matchType, fieldName, filter); this->library->Enqueue(activeQuery); } +void CategoryListView::RequeryWithField( + const std::string& fieldName, + const std::string& filter, + const int64_t selectAfterQuery) +{ + this->RequeryWithField(this->matchType, fieldName, filter, selectAfterQuery); +} + +void CategoryListView::Requery( + MatchType matchType, + const std::string& filter, + const int64_t selectAfterQuery) +{ + this->RequeryWithField(matchType, this->fieldName, filter, selectAfterQuery); +} + void CategoryListView::Requery(const std::string& filter, const int64_t selectAfterQuery) { - this->RequeryWithField(this->fieldName, filter, selectAfterQuery); + this->RequeryWithField(this->matchType, this->fieldName, filter, selectAfterQuery); } void CategoryListView::Requery(const int64_t selectAfterQuery) { - this->RequeryWithField(this->fieldName, this->filter, selectAfterQuery); + this->RequeryWithField(this->matchType, this->fieldName, this->filter, selectAfterQuery); +} + +void CategoryListView::Requery() { + this->RequeryWithField(this->matchType, this->fieldName, "", -1LL); } void CategoryListView::Reset() { - this->metadata.reset(new SdkValueList()); /* ugh */ + this->metadata = std::make_shared(); /* ugh */ this->OnAdapterChanged(); } @@ -166,7 +188,7 @@ void CategoryListView::SetFieldName(const std::string& fieldName) { this->metadata.reset(); } - this->Requery(); + this->Requery(this->matchType); } } diff --git a/src/musikcube/app/window/CategoryListView.h b/src/musikcube/app/window/CategoryListView.h index 79afbbf55..f00940d92 100755 --- a/src/musikcube/app/window/CategoryListView.h +++ b/src/musikcube/app/window/CategoryListView.h @@ -55,6 +55,8 @@ namespace musik { public sigslot::has_slots<> { public: + using MatchType = musik::core::library::query::QueryBase::MatchType; + CategoryListView( musik::core::audio::PlaybackService& playback, musik::core::ILibraryPtr library, @@ -62,17 +64,30 @@ namespace musik { virtual ~CategoryListView(); + void RequeryWithField( + MatchType matchType, + const std::string& fieldName, + const std::string& filter = "", + const int64_t selectAfterQuery = -1LL); + void RequeryWithField( const std::string& fieldName, const std::string& filter = "", const int64_t selectAfterQuery = -1LL); void Requery( + MatchType matchType, const std::string& filter = "", const int64_t selectAfterQuery = -1LL); + void Requery( + const std::string& filter, + const int64_t selectAfterQuery); + void Requery(const int64_t selectAfterQuery); + void Requery(); + void Reset(); int64_t GetSelectedId(); @@ -121,6 +136,7 @@ namespace musik { std::string fieldName, fieldIdColumn; std::string filter; + MatchType matchType{ MatchType::Substring }; int64_t selectAfterQuery; musik::core::library::query::CategoryListQuery::Result metadata; }; diff --git a/src/musikcube/cursespp/Colors.cpp b/src/musikcube/cursespp/Colors.cpp index 6a4dcc1b3..11be45dc0 100755 --- a/src/musikcube/cursespp/Colors.cpp +++ b/src/musikcube/cursespp/Colors.cpp @@ -51,48 +51,50 @@ using namespace boost::filesystem; indicies we'll use to store them */ #define THEME_COLOR_BACKGROUND 16 #define THEME_COLOR_FOREGROUND 17 -#define THEME_COLOR_FOCUSED_BORDER 18 -#define THEME_COLOR_TEXT_FOCUSED 19 -#define THEME_COLOR_TEXT_ACTIVE 20 -#define THEME_COLOR_TEXT_DISABLED 21 -#define THEME_COLOR_TEXT_HIDDEN 22 -#define THEME_COLOR_TEXT_WARNING 23 -#define THEME_COLOR_TEXT_ERROR 24 -#define THEME_COLOR_OVERLAY_BACKGROUND 25 -#define THEME_COLOR_OVERLAY_FOREGROUND 26 -#define THEME_COLOR_OVERLAY_BORDER 27 -#define THEME_COLOR_OVERLAY_FOCUSED_BORDER 28 -#define THEME_COLOR_OVERLAY_FOCUSED_TEXT 29 -#define THEME_COLOR_SHORTCUTS_BACKGROUND 30 -#define THEME_COLOR_SHORTCUTS_FOREGROUND 31 -#define THEME_COLOR_SHORTCUTS_BACKGROUND_FOCUSED 32 -#define THEME_COLOR_SHORTCUTS_FOREGROUND_FOCUSED 33 -#define THEME_COLOR_BUTTON_BACKGROUND_NORMAL 34 -#define THEME_COLOR_BUTTON_FOREGROUND_NORMAL 35 -#define THEME_COLOR_BUTTON_BACKGROUND_ACTIVE 36 -#define THEME_COLOR_BUTTON_FOREGROUND_ACTIVE 37 -#define THEME_COLOR_BANNER_BACKGROUND 38 -#define THEME_COLOR_BANNER_FOREGROUND 39 -#define THEME_COLOR_LIST_HEADER_BACKGROUND 40 -#define THEME_COLOR_LIST_HEADER_FOREGROUND 41 -#define THEME_COLOR_LIST_HEADER_HIGHLIGHTED_BACKGROUND 42 -#define THEME_COLOR_LIST_HEADER_HIGHLIGHTED_FOREGROUND 43 -#define THEME_COLOR_LIST_ITEM_HIGHLIGHTED_BACKGROUND 44 -#define THEME_COLOR_LIST_ITEM_HIGHLIGHTED_FOREGROUND 45 -#define THEME_COLOR_LIST_ITEM_ACTIVE_BACKGROUND 46 -#define THEME_COLOR_LIST_ITEM_ACTIVE_FOREGROUND 47 -#define THEME_COLOR_LIST_ITEM_ACTIVE_HIGHLIGHTED_BACKGROUND 48 -#define THEME_COLOR_LIST_ITEM_ACTIVE_HIGHLIGHTED_FOREGROUND 49 -#define THEME_COLOR_FOOTER_BACKGROUND 50 -#define THEME_COLOR_FOOTER_FOREGROUND 51 -#define THEME_COLOR_HEADER_BACKGROUND 52 -#define THEME_COLOR_HEADER_FOREGROUND 53 +#define THEME_COLOR_FRAME_FOCUSED 18 +#define THEME_COLOR_FRAME_IMPORTANT 19 +#define THEME_COLOR_TEXT_FOCUSED 20 +#define THEME_COLOR_TEXT_ACTIVE 21 +#define THEME_COLOR_TEXT_DISABLED 22 +#define THEME_COLOR_TEXT_HIDDEN 23 +#define THEME_COLOR_TEXT_WARNING 24 +#define THEME_COLOR_TEXT_ERROR 25 +#define THEME_COLOR_OVERLAY_BACKGROUND 26 +#define THEME_COLOR_OVERLAY_FOREGROUND 27 +#define THEME_COLOR_OVERLAY_BORDER 28 +#define THEME_COLOR_OVERLAY_FOCUSED_FRAME 29 +#define THEME_COLOR_OVERLAY_FOCUSED_TEXT 30 +#define THEME_COLOR_SHORTCUTS_BACKGROUND 31 +#define THEME_COLOR_SHORTCUTS_FOREGROUND 32 +#define THEME_COLOR_SHORTCUTS_BACKGROUND_FOCUSED 33 +#define THEME_COLOR_SHORTCUTS_FOREGROUND_FOCUSED 34 +#define THEME_COLOR_BUTTON_BACKGROUND_NORMAL 35 +#define THEME_COLOR_BUTTON_FOREGROUND_NORMAL 36 +#define THEME_COLOR_BUTTON_BACKGROUND_ACTIVE 37 +#define THEME_COLOR_BUTTON_FOREGROUND_ACTIVE 38 +#define THEME_COLOR_BANNER_BACKGROUND 39 +#define THEME_COLOR_BANNER_FOREGROUND 40 +#define THEME_COLOR_LIST_HEADER_BACKGROUND 41 +#define THEME_COLOR_LIST_HEADER_FOREGROUND 42 +#define THEME_COLOR_LIST_HEADER_HIGHLIGHTED_BACKGROUND 43 +#define THEME_COLOR_LIST_HEADER_HIGHLIGHTED_FOREGROUND 44 +#define THEME_COLOR_LIST_ITEM_HIGHLIGHTED_BACKGROUND 45 +#define THEME_COLOR_LIST_ITEM_HIGHLIGHTED_FOREGROUND 46 +#define THEME_COLOR_LIST_ITEM_ACTIVE_BACKGROUND 47 +#define THEME_COLOR_LIST_ITEM_ACTIVE_FOREGROUND 48 +#define THEME_COLOR_LIST_ITEM_ACTIVE_HIGHLIGHTED_BACKGROUND 49 +#define THEME_COLOR_LIST_ITEM_ACTIVE_HIGHLIGHTED_FOREGROUND 50 +#define THEME_COLOR_FOOTER_BACKGROUND 51 +#define THEME_COLOR_FOOTER_FOREGROUND 52 +#define THEME_COLOR_HEADER_BACKGROUND 53 +#define THEME_COLOR_HEADER_FOREGROUND 54 /* user-readable names for the color identifiers above. these are used as key names in the config files */ #define JSON_KEY_COLOR_BACKGROUND "background" #define JSON_KEY_COLOR_FOREGROUND "foreground" -#define JSON_KEY_COLOR_FOCUSED_BORDER "focused_border" +#define JSON_KEY_COLOR_FRAME_FOCUSED "frame_focused" +#define JSON_KEY_COLOR_FRAME_IMPORTANT "frame_important" #define JSON_KEY_COLOR_TEXT_FOCUSED "text_focused" #define JSON_KEY_COLOR_TEXT_ACTIVE "text_active" #define JSON_KEY_COLOR_TEXT_DISABLED "text_disabled" @@ -222,7 +224,8 @@ struct Theme { /* main */ background.Set(THEME_COLOR_BACKGROUND, 24, 24, 20, -1); foreground.Set(THEME_COLOR_FOREGROUND, 220, 220, 220, COLOR_256_OFFWHITE); - focusedBorder.Set(THEME_COLOR_FOCUSED_BORDER, 220, 82, 86, COLOR_256_RED); + focusedFrame.Set(THEME_COLOR_FRAME_FOCUSED, 220, 82, 86, COLOR_256_RED); + importantFrame.Set(THEME_COLOR_FRAME_IMPORTANT, 102, 217, 238, COLOR_256_BLUE); /* text */ textFocused.Set(THEME_COLOR_TEXT_FOCUSED, 220, 82, 86, COLOR_256_RED); @@ -236,7 +239,7 @@ struct Theme { overlayBackground.Set(THEME_COLOR_OVERLAY_BACKGROUND, 66, 66, 56, COLOR_256_MEDIUM_GRAY); overlayForeground.Set(THEME_COLOR_OVERLAY_FOREGROUND, 220, 220, 220, COLOR_256_OFFWHITE); overlayBorder.Set(THEME_COLOR_OVERLAY_BORDER, 102, 217, 238, COLOR_256_BLUE); - overlayFocusedBorder.Set(THEME_COLOR_OVERLAY_FOCUSED_BORDER, 220, 82, 86, COLOR_256_RED); + overlayFocusedFrame.Set(THEME_COLOR_OVERLAY_FOCUSED_FRAME, 220, 82, 86, COLOR_256_RED); overlayFocusedText.Set(THEME_COLOR_OVERLAY_FOCUSED_TEXT, 220, 82, 86, COLOR_256_RED); /* shortcut bar */ @@ -299,7 +302,7 @@ struct Theme { /* actually read the theme values! */ this->background.Set(colors.value(JSON_KEY_COLOR_BACKGROUND, unset)); this->foreground.Set(colors.value(JSON_KEY_COLOR_FOREGROUND, unset)); - this->focusedBorder.Set(colors.value(JSON_KEY_COLOR_FOCUSED_BORDER, unset)); + this->importantFrame.Set(colors.value(JSON_KEY_COLOR_FRAME_IMPORTANT, unset)); this->textFocused.Set(colors.value(JSON_KEY_COLOR_TEXT_FOCUSED, unset)); this->textActive.Set(colors.value(JSON_KEY_COLOR_TEXT_ACTIVE, unset)); this->textDisabled.Set(colors.value(JSON_KEY_COLOR_TEXT_DISABLED, unset)); @@ -309,7 +312,7 @@ struct Theme { this->overlayBackground.Set(colors.value(JSON_KEY_COLOR_OVERLAY_BACKGROUND, unset)); this->overlayForeground.Set(colors.value(JSON_KEY_COLOR_OVERLAY_FOREGROUND, unset)); this->overlayBorder.Set(colors.value(JSON_KEY_COLOR_OVERLAY_BORDER, unset)); - this->overlayFocusedBorder.Set(colors.value(JSON_KEY_COLOR_OVERLAY_FOCUSED_BORDER, unset)); + this->overlayFocusedFrame.Set(colors.value(JSON_KEY_COLOR_OVERLAY_FOCUSED_BORDER, unset)); this->overlayFocusedText.Set(colors.value(JSON_KEY_COLOR_OVERLAY_FOCUSED_TEXT, unset)); this->shortcutsBackground.Set(colors.value(JSON_KEY_COLOR_SHORTCUTS_BACKGROUND, unset)); this->shortcutsForeground.Set(colors.value(JSON_KEY_COLOR_SHORTCUTS_FOREGROUND, unset)); @@ -336,6 +339,19 @@ struct Theme { this->listActiveHighlightedBackground.Set(colors.value(JSON_KEY_COLOR_LIST_ITEM_ACTIVE_HIGHLIGHTED_BACKGROUND, unset)); this->listActiveHighlightedForeground.Set(colors.value(JSON_KEY_COLOR_LIST_ITEM_ACTIVE_HIGHLIGHTED_FOREGROUND, unset)); + /* these ones require special logic because they were renamed and we + may need to load them by their original names. */ + auto keyWithFallback = [](nlohmann::json& json, const std::string& key, const std::string& fallback) -> std::string { + return json.find(key) != json.end() ? key : fallback; + }; + + const std::string focusedFrameKey = + keyWithFallback(colors, JSON_KEY_COLOR_FRAME_FOCUSED, "focused_border"); + + this->focusedFrame.Set(colors.value(focusedFrameKey, unset)); + + /* finalize */ + this->fn = fn; this->name = data.value("name", "unnamed"); @@ -362,8 +378,9 @@ struct Theme { /* main */ init_pair(Color::ContentColorDefault, foregroundId, backgroundId); - init_pair(Color::FrameColorDefault, foregroundId, backgroundId); - init_pair(Color::FrameColorFocused, focusedBorder.Id(mode, COLOR_RED),backgroundId); + init_pair(Color::FrameDefault, foregroundId, backgroundId); + init_pair(Color::FrameFocused, focusedFrame.Id(mode, COLOR_RED),backgroundId); + init_pair(Color::FrameImportant, importantFrame.Id(mode, COLOR_BLUE), backgroundId); /* text */ init_pair(Color::TextDefault, foregroundId, backgroundId); @@ -378,10 +395,10 @@ struct Theme { int overlayBgId = overlayBackground.Id(mode, -1); init_pair(Color::OverlayFrame, overlayBorder.Id(mode, COLOR_BLUE), overlayBgId); init_pair(Color::OverlayContent, overlayForeground.Id(mode, -1), overlayBgId); - init_pair(Color::OverlayTextInputFrame, overlayFocusedBorder.Id(mode, COLOR_RED), overlayBgId); + init_pair(Color::OverlayTextInputFrame, overlayFocusedFrame.Id(mode, COLOR_RED), overlayBgId); init_pair(Color::OverlayTextFocused, overlayFocusedText.Id(mode, COLOR_RED), overlayBgId); init_pair(Color::OverlayListFrame, foregroundId, overlayBgId); - init_pair(Color::OverlayListFrameFocused, focusedBorder.Id(mode, COLOR_RED), overlayBgId); + init_pair(Color::OverlayListFrameFocused, focusedFrame.Id(mode, COLOR_RED), overlayBgId); /* shortcuts */ init_pair( @@ -461,7 +478,8 @@ struct Theme { /* main */ ThemeColor background; ThemeColor foreground; - ThemeColor focusedBorder; + ThemeColor focusedFrame; + ThemeColor importantFrame; /* text */ ThemeColor textFocused; @@ -475,7 +493,7 @@ struct Theme { ThemeColor overlayBackground; ThemeColor overlayForeground; ThemeColor overlayBorder; - ThemeColor overlayFocusedBorder; + ThemeColor overlayFocusedFrame; ThemeColor overlayFocusedText; /* shortcut bar */ diff --git a/src/musikcube/cursespp/Window.cpp b/src/musikcube/cursespp/Window.cpp index a700d3ccd..5e1957156 100755 --- a/src/musikcube/cursespp/Window.cpp +++ b/src/musikcube/cursespp/Window.cpp @@ -168,9 +168,9 @@ Window::Window(IWindow *parent) { this->lastAbsoluteX = 0; this->lastAbsoluteY = 0; this->contentColor = Color(Color::ContentColorDefault); - this->frameColor = Color(Color::FrameColorDefault); + this->frameColor = Color(Color::FrameDefault); this->focusedContentColor = Color(Color::ContentColorDefault); - this->focusedFrameColor = Color(Color::FrameColorFocused); + this->focusedFrameColor = Color(Color::FrameFocused); this->drawFrame = true; this->isVisibleInParent = false; this->isDirty = true; @@ -484,7 +484,7 @@ void Window::SetFrameColor(Color color) { ASSERT_MAIN_THREAD(); this->frameColor = (color == Color::Default) - ? Color::FrameColorDefault : color; + ? Color::FrameDefault : color; this->RepaintBackground(); this->Redraw(); @@ -494,7 +494,7 @@ void Window::SetFocusedFrameColor(Color color) { ASSERT_MAIN_THREAD(); this->focusedFrameColor = (color == Color::Default) - ? Color::FrameColorFocused : color; + ? Color::FrameFocused : color; this->RepaintBackground(); this->Redraw(); diff --git a/src/musikcube/cursespp/cursespp/Colors.h b/src/musikcube/cursespp/cursespp/Colors.h index 80abd32f1..58e6eb930 100644 --- a/src/musikcube/cursespp/cursespp/Colors.h +++ b/src/musikcube/cursespp/cursespp/Colors.h @@ -52,33 +52,34 @@ namespace cursespp { ListItemHeaderHighlighted = 6, ContentColorDefault = 7, - FrameColorDefault = 8, - FrameColorFocused = 9, + FrameDefault = 8, + FrameFocused = 9, + FrameImportant = 10, - TextDefault = 10, - TextDisabled = 11, - TextFocused = 12, - TextActive = 13, - TextWarning = 14, - TextError = 15, - TextHidden = 16, + TextDefault = 11, + TextDisabled = 12, + TextFocused = 13, + TextActive = 14, + TextWarning = 15, + TextError = 16, + TextHidden = 17, - ButtonDefault = 17, - ButtonHighlighted = 18, + ButtonDefault = 18, + ButtonHighlighted = 19, - ShortcutRowDefault = 19, - ShortcutRowFocused = 20, + ShortcutRowDefault = 20, + ShortcutRowFocused = 21, - OverlayFrame = 21, - OverlayContent = 22, - OverlayTextInputFrame = 23, - OverlayTextFocused = 24, - OverlayListFrame = 25, - OverlayListFrameFocused = 26, + OverlayFrame = 22, + OverlayContent = 23, + OverlayTextInputFrame = 24, + OverlayTextFocused = 25, + OverlayListFrame = 26, + OverlayListFrameFocused = 27, - Header = 27, - Footer = 28, - Banner = 29 + Header = 28, + Footer = 29, + Banner = 30 }; Color() { diff --git a/src/musikcube/data/locales/en_US.json b/src/musikcube/data/locales/en_US.json index 0d0632f27..ed747d044 100644 --- a/src/musikcube/data/locales/en_US.json +++ b/src/musikcube/data/locales/en_US.json @@ -37,6 +37,7 @@ "browse_no_subdirectories_toast": "there are no more sub-directories.\n\npress '%s' to play the selection.", "search_filter_hint": "search", + "search_regex_hint": "regex", "console_list_title": "debug logs", "console_version": "musikcube version %s", diff --git a/src/musikcube/data/themes/gruvbox_dark.json b/src/musikcube/data/themes/gruvbox_dark.json index 79f963240..646397e9b 100644 --- a/src/musikcube/data/themes/gruvbox_dark.json +++ b/src/musikcube/data/themes/gruvbox_dark.json @@ -10,10 +10,14 @@ "hex": "#fbf1c7", "palette": 229 }, - "focused_border": { + "frame_focused": { "hex": "#fb4934", "palette": 167 }, + "frame_important": { + "hex": "#458588", + "palette": 66 + }, "text_focused": { "hex": "#fb4934", "palette": 167 diff --git a/src/musikcube/data/themes/solarized_dark.json b/src/musikcube/data/themes/solarized_dark.json index 442081e34..a0e636971 100644 --- a/src/musikcube/data/themes/solarized_dark.json +++ b/src/musikcube/data/themes/solarized_dark.json @@ -10,10 +10,14 @@ "hex": "#eee8d5", "palette": 254 }, - "focused_border": { + "frame_focused": { "hex": "#dc322f", "palette": 160 }, + "frame_important": { + "hex": "#268bd2", + "palette": 33 + }, "text_focused": { "hex": "#dc322f", "palette": 160 diff --git a/src/musikcube/data/themes/solarized_light.json b/src/musikcube/data/themes/solarized_light.json index d0a1923b6..c7d18999b 100644 --- a/src/musikcube/data/themes/solarized_light.json +++ b/src/musikcube/data/themes/solarized_light.json @@ -10,10 +10,14 @@ "hex": "#073642", "palette": 235 }, - "focused_border": { + "frame_focused": { "hex": "#dc322f", "palette": 160 }, + "frame_important": { + "hex": "#268bd2", + "palette": 33 + }, "text_focused": { "hex": "#dc322f", "palette": 160 diff --git a/src/musikcube/musikcube.rc b/src/musikcube/musikcube.rc index 0d4403cc7..1c0c0e299 100644 Binary files a/src/musikcube/musikcube.rc and b/src/musikcube/musikcube.rc differ