diff --git a/approot/explore.xml b/approot/explore.xml index a53455fb..f8edae9a 100644 --- a/approot/explore.xml +++ b/approot/explore.xml @@ -21,8 +21,6 @@ ${clusters} -${} ${}${name} -
diff --git a/approot/messages.xml b/approot/messages.xml index 0a29ffb5..f515240b 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -124,6 +124,7 @@ Similar albums Copyright +Disc {1} Recently added diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 934cd400..742251af 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -124,6 +124,7 @@ Albums similaires Copyright +Disque {1} Ajouts récents @@ -155,8 +156,10 @@ +Apparence Audio Changement de mot de passe +Mode sombre Impossible de sauvegarder en utilisant un compte de démo ! Mauvais mot de passe L'ancien mot de passe doit être renseigné diff --git a/approot/release.xml b/approot/release.xml index 57fc3439..3e311468 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -15,17 +15,22 @@ + ${container} + + + +

${disc-title}

${tracks}
- ${}${disc-number}-${}${}${track-number}${} + ${}${track-number}${}
${name}
diff --git a/approot/templates.xml b/approot/templates.xml index 321420a5..bc95730e 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -6,7 +6,7 @@
${contents class="Lms-contents"}
- ${player class="Lms-player well well-sm"} + ${player class="Lms-player"} diff --git a/docroot/css/lms-darkly.css b/docroot/css/lms-darkly.css index 8cbb660a..904cc8c1 100644 --- a/docroot/css/lms-darkly.css +++ b/docroot/css/lms-darkly.css @@ -1,8 +1,9 @@ :root { + --body-color: #ffffff; --hover-color: #00bc8c; - --body-color: #cacaca; --playing-background-color: #375a7f4f; --slider-background-color: #5a5a5a; + --player-background-color: #303030; } .Lms-artistname.text-muted { diff --git a/docroot/css/lms-flatly.css b/docroot/css/lms-flatly.css index 9e9e817a..9848fd7e 100644 --- a/docroot/css/lms-flatly.css +++ b/docroot/css/lms-flatly.css @@ -3,6 +3,7 @@ --hover-color: #18bc9c; --playing-background-color: #2c3e5029; --slider-background-color: #2c3e5029; + --player-background-color: #ecf0f1; } .Lms-artistname.text-muted { diff --git a/docroot/css/lms.css b/docroot/css/lms.css index d2b1fbff..5d89d714 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -1,3 +1,4 @@ +/* Stylized for flaty/darkly themes */ a, a:link, a:active, a:visited { text-decoration: none; cursor: pointer; @@ -51,66 +52,17 @@ a.Lms-artistname:hover, a.Lms-artistname:focus { } .Lms-cluster { - display: inline; + display: inline-block; cursor: pointer; - line-height: 32px; - margin: 4px; + line-height: 1.3; + margin-right: 4px; + margin-bottom: 4px; } .Lms-cluster:hover { filter: brightness(130%); } -.Lms-cluster-type-0 { - background-color: darkslategrey; - margin-bottom: 4px; -} - -.Lms-cluster-type-1 { - background-color: darkblue; - margin-bottom: 4px; -} - -.Lms-cluster-type-2 { - background-color: darkgrey; - margin-bottom: 4px; -} - -.Lms-cluster-type-3 { - background-color: darkorchid; - margin-bottom: 4px; -} - -.Lms-cluster-type-4 { - background-color: darkgoldenrod; - margin-bottom: 4px; -} - -.Lms-cluster-type-5 { - background-color: darkkhaki; - margin-bottom: 4px; -} - -.Lms-cluster-type-6 { - background-color: darkolivegreen; - margin-bottom: 4px; -} - -.Lms-cluster-type-7 { - background-color: darkred; - margin-bottom: 4px; -} - -.Lms-cluster-type-8 { - background-color: darkviolet; - margin-bottom: 4px; -} - -.Lms-cluster-type-9 { - background-color: darkturquoise; - margin-bottom: 4px; -} - .Lms-control-btn:hover { color: var(--hover-color); } @@ -121,24 +73,24 @@ a.Lms-artistname:hover, a.Lms-artistname:focus { .Lms-cover-smaller { width: 64px; /* some cover may not be square */ - border-radius: 10px; + border-radius: 3px; box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } .Lms-cover-small { width: 96px; /* some cover may not be square */ - border-radius: 12px; + border-radius: 3px; box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } .Lms-cover-medium { width: 128px; - border-radius: 16px; + border-radius: 3px; box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } .Lms-cover-large { - border-radius: 16px; + border-radius: 3px; box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } @@ -162,7 +114,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus { } .Lms-explore-release-entry-tracknumber { - text-align: center; + text-align: right; white-space: nowrap; } @@ -333,6 +285,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus { height: 70px; padding: 0px; margin: 0px; + background-color: var(--player-background-color); } .Lms-player-btn { @@ -397,7 +350,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus { top: 0px; left: 0px; height: 8px; - border-radius: 4px; + border-radius: 3px; } .Lms-player-seek::-moz-range-thumb { @@ -448,7 +401,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus { height: 8px; outline: none; cursor: pointer; - border-radius: 4px; + border-radius: 3px; border-style: solid; border-width: thin; background-color: var(--slider-background-color); diff --git a/src/libs/cover/impl/CoverArtGrabber.cpp b/src/libs/cover/impl/CoverArtGrabber.cpp index 38781cbc..8646d8bf 100644 --- a/src/libs/cover/impl/CoverArtGrabber.cpp +++ b/src/libs/cover/impl/CoverArtGrabber.cpp @@ -189,7 +189,7 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, st trackPath = track->getPath(); auto release {track->getRelease()}; - if (release && release->getTotalDiscNumber() > 1) + if (release && release->getTotalDisc() > 1) isMultiDisc = true; } } diff --git a/src/libs/database/impl/Db.cpp b/src/libs/database/impl/Db.cpp index c3ec1d60..dd6b8d0e 100644 --- a/src/libs/database/impl/Db.cpp +++ b/src/libs/database/impl/Db.cpp @@ -33,9 +33,9 @@ Db::Db(const std::filesystem::path& dbPath) LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string(); std::unique_ptr connection {std::make_unique(dbPath.string())}; +// connection->setProperty("show-queries", "true"); connection->executeSql("pragma journal_mode=WAL"); connection->executeSql("pragma synchronous=normal"); -// connection->setProperty("show-queries", "true"); auto connectionPool = std::make_unique(std::move(connection), 10); connectionPool->setTimeout(std::chrono::seconds(10)); @@ -43,6 +43,14 @@ Db::Db(const std::filesystem::path& dbPath) _connectionPool = std::move(connectionPool); } +void +Db::executeSql(const std::string& sql) +{ + auto connection {_connectionPool->getConnection()}; + connection->executeSql(sql); + _connectionPool->returnConnection(std::move(connection)); +} + } // namespace Database diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 7fdd639a..a8535f49 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -268,15 +268,29 @@ Release::getAllIdsWithClusters(Session& session, std::optional limi std::optional -Release::getTotalTrackNumber(void) const +Release::getTotalTrack(void) const { - return (_totalTrackNumber > 0) ? std::make_optional(_totalTrackNumber) : std::nullopt; + assert(session()); + assert(IdIsValid(self()->id())); + + int res = session()->query("SELECT COALESCE(MAX(total_track),0) FROM track t INNER JOIN release r ON r.id = t.release_id") + .where("r.id = ?") + .bind(this->id()); + + return (res > 0) ? std::make_optional(res) : std::nullopt; } std::optional -Release::getTotalDiscNumber(void) const +Release::getTotalDisc(void) const { - return (_totalDiscNumber > 0) ? std::make_optional(_totalDiscNumber) : std::nullopt; + assert(session()); + assert(IdIsValid(self()->id())); + + int res = session()->query("SELECT COALESCE(MAX(total_disc),0) FROM track t INNER JOIN release r ON r.id = t.release_id") + .where("r.id = ?") + .bind(this->id()); + + return (res > 0) ? std::make_optional(res) : std::nullopt; } std::optional @@ -287,7 +301,7 @@ Release::getReleaseYear(bool original) const const std::string field {original ? "original_year" : "year"}; Wt::Dbo::collection dates = session()->query( - std::string{"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") + std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") .where("r.id = ?") .groupBy(field) .bind(this->id()); diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 6270c7df..70ebabea 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -40,7 +40,7 @@ namespace Database { -#define LMS_DATABASE_VERSION 16 +#define LMS_DATABASE_VERSION 18 using Version = std::size_t; @@ -83,26 +83,31 @@ class VersionInfo void Session::doDatabaseMigrationIfNeeded() { - auto uniqueTransaction {createUniqueTransaction()}; - static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"}; - Version version; - try - { - version = VersionInfo::getOrCreate(*this)->getVersion(); - LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION; - if (version == LMS_DATABASE_VERSION) - return; - } - catch (std::exception& e) - { - LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what(); - throw LmsException {outdatedMsg}; - } + Db::ScopedNoForeignKeys noPragmaKeys {_db}; - while (version < LMS_DATABASE_VERSION) + while (1) { + auto uniqueTransaction {createUniqueTransaction()}; + + Version version; + try + { + version = VersionInfo::getOrCreate(*this)->getVersion(); + LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION; + if (version == LMS_DATABASE_VERSION) + { + LMS_LOG(DB, DEBUG) << "Lms database version " << LMS_DATABASE_VERSION << ": up to date!"; + return; + } + } + catch (std::exception& e) + { + LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what(); + throw LmsException {outdatedMsg}; + } + LMS_LOG(DB, INFO) << "Migrating database from version " << version << "..."; if (version == 5) @@ -173,15 +178,41 @@ CREATE TABLE IF NOT EXISTS "track_bookmark" ( { _session.execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultUITheme)) + ")"); } + else if (version == 16) + { + _session.execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)"); + _session.execute("ALTER TABLE track ADD total_track INTEGER NOT NULL DEFAULT(0)"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(*this).modify()->incScanVersion(); + } + else if (version == 17) + { + // Drop colums total_disc/total_track from release + _session.execute(R"( +CREATE TABLE "release_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "name" text not null, + "mbid" text not null +))"); + _session.execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release"); + _session.execute("DROP TABLE release;"); + _session.execute("ALTER TABLE release_backup RENAME TO release"); + _session.execute("CREATE INDEX release_name_idx ON release(name)"); + _session.execute("CREATE INDEX release_name_nocase_idx ON release(name COLLATE NOCASE)"); + _session.execute("CREATE INDEX release_mbid_idx ON release(mbid)"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(*this).modify()->incScanVersion(); + } else { LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; throw LmsException { LMS_DATABASE_VERSION > version ? outdatedMsg : "Server binary outdated, please upgrade it to handle this database"}; } - ++version; - - VersionInfo::get(*this).modify()->setVersion(LMS_DATABASE_VERSION); + VersionInfo::get(*this).modify()->setVersion(++version); } } diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 2acff171..e62f875f 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -177,7 +177,7 @@ Track::getAllIdsWithClusters(Session& session, std::optional limit) } std::vector -Track::getClusters(void) const +Track::getClusters() const { std::vector< Cluster::pointer > clusters; std::copy(_clusters.begin(), _clusters.end(), std::back_inserter(clusters)); @@ -185,7 +185,7 @@ Track::getClusters(void) const } std::vector -Track::getClusterIds(void) const +Track::getClusterIds() const { assert(self()); assert(IdIsValid(self()->id())); @@ -355,17 +355,29 @@ Track::setFeatures(const Wt::Dbo::ptr& features) } std::optional -Track::getTrackNumber(void) const +Track::getTrackNumber() const { return (_trackNumber > 0) ? std::make_optional(_trackNumber) : std::nullopt; } std::optional -Track::getDiscNumber(void) const +Track::getTotalTrack() const +{ + return (_totalTrack > 0) ? std::make_optional(_totalTrack) : std::nullopt; +} + +std::optional +Track::getDiscNumber() const { return (_discNumber > 0) ? std::make_optional(_discNumber) : std::nullopt; } +std::optional +Track::getTotalDisc() const +{ + return (_totalDisc > 0) ? std::make_optional(_totalDisc) : std::nullopt; +} + std::optional Track::getYear() const { diff --git a/src/libs/database/include/database/Db.hpp b/src/libs/database/include/database/Db.hpp index fa07af26..17470c19 100644 --- a/src/libs/database/include/database/Db.hpp +++ b/src/libs/database/include/database/Db.hpp @@ -39,6 +39,25 @@ class Db std::shared_mutex& getMutex() { return _sharedMutex; } Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; } + class ScopedNoForeignKeys + { + public: + ScopedNoForeignKeys(Db& db) : _db {db} + { + _db.executeSql("PRAGMA foreign_keys=OFF"); + } + ~ScopedNoForeignKeys() + { + _db.executeSql("PRAGMA foreign_keys=ON"); + } + + private: + Db& _db; + + }; + + void executeSql(const std::string& sql); + std::shared_mutex _sharedMutex; std::unique_ptr _connectionPool; }; diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index 6360b90e..29b93c58 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -84,15 +84,11 @@ class Release : public Wt::Dbo::Dbo std::optional getCopyright() const; std::optional getCopyrightURL() const; - // Modifiers - void setTotalDiscNumber(std::size_t num) { _totalDiscNumber = static_cast(num); } - void setTotalTrackNumber(std::size_t num) { _totalTrackNumber = static_cast(num); } - // Accessors const std::string& getName() const { return _name; } std::optional getMBID() const { return UUID::fromString(_MBID); } - std::optional getTotalTrackNumber() const; - std::optional getTotalDiscNumber() const; + std::optional getTotalTrack() const; + std::optional getTotalDisc() const; std::chrono::milliseconds getDuration() const; // Get the artists of this release @@ -109,8 +105,6 @@ class Release : public Wt::Dbo::Dbo { Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _MBID, "mbid"); - Wt::Dbo::field(a, _totalDiscNumber, "total_disc_number"); - Wt::Dbo::field(a, _totalTrackNumber, "total_track_number"); Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release"); Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade); @@ -121,8 +115,6 @@ class Release : public Wt::Dbo::Dbo std::string _name; std::string _MBID; - int _totalDiscNumber {}; - int _totalTrackNumber {}; Wt::Dbo::collection> _tracks; // Tracks in the release Wt::Dbo::collection> _starringUsers; // Users that starred this release diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 24113f21..2bbeb9ee 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -88,6 +88,8 @@ class Track : public Wt::Dbo::Dbo void setScanVersion(std::size_t version) { _scanVersion = version; } void setTrackNumber(int num) { _trackNumber = num; } void setDiscNumber(int num) { _discNumber = num; } + void setTotalTrack(std::optional totalTrack) { totalTrack ? _totalTrack = *totalTrack : 0; } + void setTotalDisc(std::optional totalDisc) { totalDisc ? _totalDisc = *totalDisc : 0; } void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); } void setDuration(std::chrono::milliseconds duration) { _duration = duration; } void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } @@ -106,7 +108,9 @@ class Track : public Wt::Dbo::Dbo std::size_t getScanVersion() const { return _scanVersion; } std::optional getTrackNumber() const; + std::optional getTotalTrack() const; std::optional getDiscNumber() const; + std::optional getTotalDisc() const; std::string getName() const { return _name; } std::filesystem::path getPath() const { return _filePath; } std::chrono::milliseconds getDuration() const { return _duration; } @@ -135,6 +139,8 @@ class Track : public Wt::Dbo::Dbo Wt::Dbo::field(a, _scanVersion, "scan_version"); Wt::Dbo::field(a, _trackNumber, "track_number"); Wt::Dbo::field(a, _discNumber, "disc_number"); + Wt::Dbo::field(a, _totalTrack, "total_track"); + Wt::Dbo::field(a, _totalDisc, "total_disc"); Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _duration, "duration"); Wt::Dbo::field(a, _year, "year"); @@ -160,19 +166,21 @@ class Track : public Wt::Dbo::Dbo static const std::size_t _maxCopyrightLength = 128; static const std::size_t _maxCopyrightURLLength = 128; - int _scanVersion = 0; - int _trackNumber = 0; - int _discNumber = 0; + int _scanVersion {}; + int _trackNumber {}; + int _discNumber {}; + int _totalTrack {}; + int _totalDisc {}; std::string _name; std::string _artistName; std::string _releaseName; std::chrono::duration _duration; - int _year = 0; - int _originalYear = 0; + int _year {}; + int _originalYear {}; std::string _filePath; Wt::WDateTime _fileLastWrite; Wt::WDateTime _fileAdded; - bool _hasCover = false; + bool _hasCover {}; std::string _MBID; // Musicbrainz Identifier std::string _copyright; std::string _copyrightURL; diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index ca0c4715..f8a8fa66 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -718,13 +718,6 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S stats.updates++; } - // Release related data - if (release) - { - release.modify()->setTotalTrackNumber(trackInfo->totalTrack ? *trackInfo->totalTrack : 0); - release.modify()->setTotalDiscNumber(trackInfo->totalDisc ? *trackInfo->totalDisc : 0); - } - // Track related data assert(track); @@ -744,6 +737,8 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S track.modify()->setAddedTime(Wt::WLocalDateTime::currentServerDateTime().toUTC()); track.modify()->setTrackNumber(trackInfo->trackNumber ? *trackInfo->trackNumber : 0); track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0); + track.modify()->setTotalTrack(trackInfo->totalTrack); + track.modify()->setTotalDisc(trackInfo->totalDisc); track.modify()->setYear(trackInfo->year ? *trackInfo->year : 0); track.modify()->setOriginalYear(trackInfo->originalYear ? *trackInfo->originalYear : 0); diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 6cdfdddc..bac11ebd 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable(lms ui/Auth.cpp ui/LmsApplication.cpp ui/LmsApplicationGroup.cpp + ui/LmsTheme.cpp ui/MediaPlayer.cpp ui/PlayQueueView.cpp ui/PlayHistoryView.cpp diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index d115cf46..addce9dd 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -20,9 +20,7 @@ #include "LmsApplication.hpp" #include -#include #include -#include #include #include #include @@ -50,6 +48,7 @@ #include "resource/ImageResource.hpp" #include "Auth.hpp" #include "LmsApplicationException.hpp" +#include "LmsTheme.hpp" #include "MediaPlayer.hpp" #include "PlayHistoryView.hpp" #include "PlayQueueView.hpp" @@ -58,40 +57,6 @@ namespace UserInterface { -class LmsBooststrapTheme : public Wt::WBootstrapTheme -{ - public: - LmsBooststrapTheme(Database::User::UITheme theme) : _theme {theme} {} - - private: - std::vector styleSheets() const override - { - switch (_theme) - { - case Database::User::UITheme::Dark: - return - { - Wt::WLinkedCssStyleSheet {"css/bootstrap-darkly.min.css"}, - Wt::WLinkedCssStyleSheet {"resources/themes/bootstrap/3/wt.css"}, - Wt::WLinkedCssStyleSheet {"css/lms.css"}, - Wt::WLinkedCssStyleSheet {"css/lms-darkly.css"}, - }; - - case Database::User::UITheme::Light: - return - { - Wt::WLinkedCssStyleSheet {"css/bootstrap-flatly.min.css"}, - Wt::WLinkedCssStyleSheet {"resources/themes/bootstrap/3/wt.css"}, - Wt::WLinkedCssStyleSheet {"css/lms.css"}, - Wt::WLinkedCssStyleSheet {"css/lms-flatly.css"}, - }; - } - return {}; - } - - Database::User::UITheme _theme; -}; - std::unique_ptr LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups) { @@ -202,6 +167,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, if (firstConnection) { + setTheme(std::make_unique(Database::User::defaultUITheme)); root()->addWidget(std::make_unique()); return; } @@ -217,10 +183,8 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, if (user) theme = user->getUITheme(); } - auto LmsTheme {std::make_unique(theme)}; - LmsTheme->setVersion(Wt::BootstrapVersion::v3); - LmsTheme->setResponsive(true); - setTheme(std::move(LmsTheme)); + + setTheme(std::make_unique(theme)); } if (userId) @@ -245,6 +209,16 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Auth* auth {root()->addNew()}; auth->userLoggedIn.connect(this, [this](Database::IdType userId) { + { + auto transaction {_dbSession.createSharedTransaction()}; + const auto user {Database::User::getById(_dbSession, userId)}; + if (user) + { + LmsTheme* lmsTheme {static_cast(LmsApp->theme().get())}; + lmsTheme->setTheme(user->getUITheme()); + } + } + handleUserLoggedIn(userId, true); }); } @@ -308,18 +282,27 @@ LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool add return res; } -std::unique_ptr +std::unique_ptr LmsApplication::createCluster(Database::Cluster::pointer cluster, bool canDelete) { - std::string styleClass = "Lms-cluster-type-" + std::to_string(cluster->getType().id() % 10); + auto getStyleClass = [](const Database::Cluster::pointer cluster) + { + switch (cluster->getType().id() % 6) + { + case 0: return "label-primary"; + case 1: return "label-default"; + case 2: return "label-info"; + case 3: return "label-warning"; + case 4: return "label-success"; + case 5: return "label-danger"; + } + return "label-default"; + }; - auto res = std::make_unique(Wt::WString::tr("Lms.Explore.template.cluster-entry").arg(styleClass)); - res->addFunction("tr", &Wt::WTemplate::Functions::tr); + const std::string styleClass {getStyleClass(cluster)}; + auto res {std::make_unique(std::string {} + (canDelete ? " " : "") + Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::UnsafeXHTML)}; - res->bindString("name", Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::Plain); - res->setCondition("if-can-delete", canDelete); - - res->setStyleClass("Lms-cluster"); + res->setStyleClass("Lms-cluster label " + styleClass); res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain); res->setInline(true); diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 474562d3..19928d31 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -103,7 +103,7 @@ class LmsApplication : public Wt::WApplication static std::unique_ptr createArtistAnchor(Wt::Dbo::ptr artist, bool addText = true); static Wt::WLink createReleaseLink(Wt::Dbo::ptr release); static std::unique_ptr createReleaseAnchor(Wt::Dbo::ptr release, bool addText = true); - static std::unique_ptr createCluster(Wt::Dbo::ptr cluster, bool canDelete = false); + static std::unique_ptr createCluster(Wt::Dbo::ptr cluster, bool canDelete = false); // Signal emitted just before the session ends (user may already be logged out) Wt::Signal<>& preQuit() { return _preQuit; } diff --git a/src/lms/ui/LmsTheme.cpp b/src/lms/ui/LmsTheme.cpp new file mode 100644 index 00000000..9fcc0df1 --- /dev/null +++ b/src/lms/ui/LmsTheme.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#include "LmsTheme.hpp" + +#include "utils/Logger.hpp" +#include "LmsApplication.hpp" + +namespace UserInterface { + +LmsTheme::LmsTheme(Database::User::UITheme theme) +: _theme {theme} +{ + setVersion(Wt::BootstrapVersion::v3); + setResponsive(true); + applyMetaHeader(); +} + +void +LmsTheme::applyMetaHeader() const +{ + switch (_theme) + { + case Database::User::UITheme::Dark: + LmsApp->addMetaHeader("theme-color", "#303030"); + break; + case Database::User::UITheme::Light: + LmsApp->removeMetaHeader(Wt::MetaHeaderType::Meta, "theme-color"); + break; + } +} + +void +LmsTheme::setTheme(Database::User::UITheme theme) +{ + // Hack, use the application interface directly since changing theme is not allowed + + const auto currentStyleSheets {getStyleSheets(_theme)}; + for (auto it {std::crbegin(currentStyleSheets)}; it != std::crend(currentStyleSheets); ++it) + { + LMS_LOG(UI, DEBUG) << "Removing css file '" << it->url() << "'"; + LmsApp->removeStyleSheet(*it); + } + + _theme = theme; + for (const auto& styleSheet : getStyleSheets(theme)) + { + LMS_LOG(UI, DEBUG) << "Adding css file '" << styleSheet.url() << "'"; + LmsApp->useStyleSheet(styleSheet); + } + applyMetaHeader(); +} + +std::vector +LmsTheme::styleSheets() const +{ + const std::vector styleSheets {getStyleSheets(_theme)}; + std::vector res; + res.reserve(styleSheets.size()); + + std::transform(std::cbegin(styleSheets), std::cend(styleSheets), std::back_inserter(res), [](const Wt::WLink& styleSheet) { return Wt::WLinkedCssStyleSheet {styleSheet}; }); + return res; +} + +std::vector +LmsTheme::getStyleSheets(Database::User::UITheme theme) +{ + switch (theme) + { + case Database::User::UITheme::Dark: + return + { + {"css/bootstrap-darkly.min.css"}, + {"resources/themes/bootstrap/3/wt.css"}, + {"css/lms.css"}, + {"css/lms-darkly.css"}, + }; + + case Database::User::UITheme::Light: + return + { + {"css/bootstrap-flatly.min.css"}, + {"resources/themes/bootstrap/3/wt.css"}, + {"css/lms.css"}, + {"css/lms-flatly.css"}, + }; + } + return {}; +} + +} diff --git a/src/lms/ui/LmsTheme.hpp b/src/lms/ui/LmsTheme.hpp new file mode 100644 index 00000000..c72ffd71 --- /dev/null +++ b/src/lms/ui/LmsTheme.hpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#pragma once + +#include + +#include +#include + +#include "database/User.hpp" + +namespace UserInterface { + +class LmsTheme : public Wt::WBootstrapTheme +{ + public: + LmsTheme(Database::User::UITheme theme); + + void setTheme(Database::User::UITheme theme); + + private: + + void applyMetaHeader() const; + std::vector styleSheets() const override; + static std::vector getStyleSheets(Database::User::UITheme theme); + + Database::User::UITheme _theme; +}; + +} diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 9b90e040..dae47c9c 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -35,6 +35,7 @@ #include "utils/Service.hpp" #include "LmsApplication.hpp" +#include "LmsTheme.hpp" namespace UserInterface { @@ -88,7 +89,14 @@ class SettingsModel : public Wt::WFormModel User::pointer user {LmsApp->getUser()}; - user.modify()->setUITheme(Wt::asNumber(value(DarkModeField)) ? User::UITheme::Dark : User::UITheme::Light); + { + const User::UITheme newTheme {Wt::asNumber(value(DarkModeField)) ? User::UITheme::Dark : User::UITheme::Light}; + LmsTheme* lmsTheme {static_cast(LmsApp->theme().get())}; + lmsTheme->setTheme(newTheme); + + user.modify()->setUITheme(newTheme); + } + user.modify()->setAudioTranscodeEnable(Wt::asNumber(value(TranscodeEnableField))); auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))}; diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index 078f48a3..231ee305 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -126,7 +126,7 @@ void Filters::add(Database::IdType clusterId) { - Wt::WTemplate* filter {}; + Wt::WInteractWidget* filter {}; { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index f47bd7d7..5df3ba4c 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -77,7 +77,6 @@ Release::refresh() throw ReleaseNotFoundException {*releaseId}; Wt::WTemplate* t {addNew(Wt::WString::tr("Lms.Explore.Release.template"))}; - t->addFunction("tr", &Wt::WTemplate::Functions::tr); t->bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain); @@ -154,18 +153,47 @@ Release::refresh() }); } - Wt::WContainerWidget* tracksContainer {t->bindNew("tracks")}; + Wt::WContainerWidget* rootContainer {t->bindNew("container")}; - auto clusterIds {_filters->getClusterIds()}; - auto tracks {release->getTracks(clusterIds)}; + const bool variousArtists {release->hasVariousArtists()}; + const auto totalDisc {release->getTotalDisc()}; + const bool isReleaseMultiDisc {totalDisc && *totalDisc > 1}; - bool variousArtists {release->hasVariousArtists()}; + // Expect to be call in asc order + std::map trackContainers; + auto getOrAddDiscContainer = [&](std::size_t discNumber) -> Wt::WContainerWidget* + { + { + auto it = trackContainers.find(discNumber); + if (it != std::cend(trackContainers)) + return it->second; + } + + Wt::WTemplate* disc {rootContainer->addNew(Wt::WString::tr("Lms.Explore.Release.template.disc-entry"))}; + disc->bindNew("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber)); + disc->addFunction("tr", &Wt::WTemplate::Functions::tr); + + Wt::WContainerWidget* tracksContainer {disc->bindNew("tracks")}; + trackContainers[discNumber] = tracksContainer; + + return tracksContainer; + }; + + const auto clusterIds {_filters->getClusterIds()}; + const auto tracks {release->getTracks(clusterIds)}; for (const auto& track : tracks) { auto trackId {track.id()}; - Wt::WTemplate* entry {tracksContainer->addNew(Wt::WString::tr("Lms.Explore.Release.template.entry"))}; + const auto discNumber {track->getDiscNumber()}; + + Wt::WContainerWidget* container {rootContainer}; + if (isReleaseMultiDisc && discNumber) + container = getOrAddDiscContainer(*discNumber); + + Wt::WTemplate* entry {container->addNew(Wt::WString::tr("Lms.Explore.Release.template.entry"))}; + entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); auto artists {track->getArtists()}; @@ -188,14 +216,6 @@ Release::refresh() entry->bindInt("track-number", *trackNumber); } - auto discNumber {track->getDiscNumber()}; - auto totalDiscNumber {release->getTotalDiscNumber()}; - if (discNumber && totalDiscNumber && *totalDiscNumber > 1) - { - entry->setCondition("if-has-disc-number", true); - entry->bindInt("disc-number", *discNumber); - } - Wt::WText* playBtn {entry->bindNew("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)}; playBtn->clicked().connect(std::bind([=] { diff --git a/src/test/database/DatabaseTest.cpp b/src/test/database/DatabaseTest.cpp index 68e19288..42c39505 100644 --- a/src/test/database/DatabaseTest.cpp +++ b/src/test/database/DatabaseTest.cpp @@ -487,6 +487,89 @@ testSingleTrackSingleRelease(Session& session) } } +static +void +testMultiTracksSingleReleaseTotalDiscTrack(Session& session) +{ + ScopedRelease release1 {session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + + CHECK(!release1->getTotalTrack()); + CHECK(!release1->getTotalDisc()); + } + + ScopedTrack track1 {session, "MyTrack"}; + { + auto transaction {session.createUniqueTransaction()}; + + track1.get().modify()->setRelease(release1.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + CHECK(!release1->getTotalTrack()); + CHECK(!release1->getTotalDisc()); + } + + { + auto transaction {session.createUniqueTransaction()}; + + track1.get().modify()->setTotalTrack(36); + track1.get().modify()->setTotalDisc(6); + } + + { + auto transaction {session.createSharedTransaction()}; + + CHECK(release1->getTotalTrack() && *release1->getTotalTrack() == 36); + CHECK(release1->getTotalDisc() && *release1->getTotalDisc() == 6); + } + + ScopedTrack track2 {session, "MyTrack2"}; + { + auto transaction {session.createUniqueTransaction()}; + + track2.get().modify()->setRelease(release1.get()); + track2.get().modify()->setTotalTrack(37); + track2.get().modify()->setTotalDisc(67); + } + + { + auto transaction {session.createSharedTransaction()}; + + CHECK(release1->getTotalTrack() && *release1->getTotalTrack() == 37); + CHECK(release1->getTotalDisc() && *release1->getTotalDisc() == 67); + } + + ScopedRelease release2 {session, "MyRelease2"}; + { + auto transaction {session.createSharedTransaction()}; + + CHECK(!release2->getTotalTrack()); + CHECK(!release2->getTotalDisc()); + } + + ScopedTrack track3 {session, "MyTrack3"}; + { + auto transaction {session.createUniqueTransaction()}; + + track3.get().modify()->setRelease(release2.get()); + track3.get().modify()->setTotalTrack(7); + track3.get().modify()->setTotalDisc(5); + } + { + auto transaction {session.createSharedTransaction()}; + + CHECK(release1->getTotalTrack() && *release1->getTotalTrack() == 37); + CHECK(release1->getTotalDisc() && *release1->getTotalDisc() == 67); + CHECK(release2->getTotalTrack() && *release2->getTotalTrack() == 7); + CHECK(release2->getTotalDisc() && *release2->getTotalDisc() == 5); + } +} + static void testSingleTrackSingleCluster(Session& session) @@ -1461,6 +1544,7 @@ int main() RUN_TEST(testMultiArtistsSortMethod); RUN_TEST(testSingleTrackSingleRelease); + RUN_TEST(testMultiTracksSingleReleaseTotalDiscTrack); RUN_TEST(testSingleTrackSingleCluster); RUN_TEST(testMultipleTracksSingleCluster);