Made some more progress on theming

This commit is contained in:
emeric
2020-04-17 17:37:26 +02:00
parent 4f560fe015
commit 27320054e5
26 changed files with 476 additions and 185 deletions
-2
View File
@@ -21,8 +21,6 @@
${clusters}
</message>
<message id="Lms.Explore.template.cluster-entry"><span class="badge {1}">${<if-can-delete>}<i class="fa fa-times-circle"></i> ${</if-can-delete>}${name}</span></message>
<message id="Lms.Explore.template.add-filter">
<div class="form-horizontal">
<div class="form-group">
+1
View File
@@ -124,6 +124,7 @@
<!--Explore:Release-->
<message id="Lms.Explore.ReleaseInfo.similar-releases">Similar albums</message>
<message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disc {1}</message>
<!--Explore:Releases-->
<message id="Lms.Explore.ReleasesInfo.recently-added">Recently added</message>
+3
View File
@@ -124,6 +124,7 @@
<!--Explore:Release-->
<message id="Lms.Explore.ReleaseInfo.similar-releases">Albums similaires</message>
<message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disque {1}</message>
<!--Explore:Releases-->
<message id="Lms.Explore.ReleasesInfo.recently-added">Ajouts récents</message>
@@ -155,8 +156,10 @@
<!--Settings-->
<message id="Lms.Settings.appearance">Apparence</message>
<message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.change-password">Changement de mot de passe</message>
<message id="Lms.Settings.dark-mode">Mode sombre</message>
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
+7 -2
View File
@@ -15,17 +15,22 @@
<div class="page-header">
<h3><div class="Lms-releasename text-primary">${name}</div></h3>
${<if-has-artist>}<h4>${artist class="Lms-artistname text-muted"}</h4>${</if-has-artist>}
<h4><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></h4>
<div class="text-muted"><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></div>
${clusters}
${play-btn}${add-btn}
</div>
${container}
</message>
<message id="Lms.Explore.Release.template.disc-entry">
<h4>${disc-title}</h4>
${tracks}
</message>
<message id="Lms.Explore.Release.template.entry">
<div class="row Lms-explore-release-entry Lms-entry-hoverable Lms-vertical-align ${is-playing}">
<div class="col-xs-1 Lms-explore-release-entry-tracknumber text-muted">
${<if-has-disc-number>}${disc-number}-${</if-has-disc-number>}${<if-has-track-number>}${track-number}${</if-has-track-number>}
${<if-has-track-number>}${track-number}${</if-has-track-number>}
</div>
<div class="col-xs-8 Lms-explore-release-entry-name">
<div class="Lms-trackname text-primary">${name}</div>
+1 -1
View File
@@ -6,7 +6,7 @@
<div class="container">
${contents class="Lms-contents"}
</div>
${player class="Lms-player well well-sm"}
${player class="Lms-player"}
</message>
</messages>
+2 -1
View File
@@ -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 {
+1
View File
@@ -3,6 +3,7 @@
--hover-color: #18bc9c;
--playing-background-color: #2c3e5029;
--slider-background-color: #2c3e5029;
--player-background-color: #ecf0f1;
}
.Lms-artistname.text-muted {
+13 -60
View File
@@ -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);
+1 -1
View File
@@ -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;
}
}
+9 -1
View File
@@ -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<Wt::Dbo::backend::Sqlite3> connection {std::make_unique<Wt::Dbo::backend::Sqlite3>(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<Wt::Dbo::FixedSqlConnectionPool>(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
+19 -5
View File
@@ -268,15 +268,29 @@ Release::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limi
std::optional<std::size_t>
Release::getTotalTrackNumber(void) const
Release::getTotalTrack(void) const
{
return (_totalTrackNumber > 0) ? std::make_optional<std::size_t>(_totalTrackNumber) : std::nullopt;
assert(session());
assert(IdIsValid(self()->id()));
int res = session()->query<int>("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<std::size_t>(res) : std::nullopt;
}
std::optional<std::size_t>
Release::getTotalDiscNumber(void) const
Release::getTotalDisc(void) const
{
return (_totalDiscNumber > 0) ? std::make_optional<std::size_t>(_totalDiscNumber) : std::nullopt;
assert(session());
assert(IdIsValid(self()->id()));
int res = session()->query<int>("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<std::size_t>(res) : std::nullopt;
}
std::optional<int>
@@ -287,7 +301,7 @@ Release::getReleaseYear(bool original) const
const std::string field {original ? "original_year" : "year"};
Wt::Dbo::collection<int> dates = session()->query<int>(
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());
+51 -20
View File
@@ -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<int>(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);
}
}
+16 -4
View File
@@ -177,7 +177,7 @@ Track::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
}
std::vector<Cluster::pointer>
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<IdType>
Track::getClusterIds(void) const
Track::getClusterIds() const
{
assert(self());
assert(IdIsValid(self()->id()));
@@ -355,17 +355,29 @@ Track::setFeatures(const Wt::Dbo::ptr<TrackFeatures>& features)
}
std::optional<std::size_t>
Track::getTrackNumber(void) const
Track::getTrackNumber() const
{
return (_trackNumber > 0) ? std::make_optional<std::size_t>(_trackNumber) : std::nullopt;
}
std::optional<std::size_t>
Track::getDiscNumber(void) const
Track::getTotalTrack() const
{
return (_totalTrack > 0) ? std::make_optional<std::size_t>(_totalTrack) : std::nullopt;
}
std::optional<std::size_t>
Track::getDiscNumber() const
{
return (_discNumber > 0) ? std::make_optional<std::size_t>(_discNumber) : std::nullopt;
}
std::optional<std::size_t>
Track::getTotalDisc() const
{
return (_totalDisc > 0) ? std::make_optional<std::size_t>(_totalDisc) : std::nullopt;
}
std::optional<int>
Track::getYear() const
{
+19
View File
@@ -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<Wt::Dbo::SqlConnectionPool> _connectionPool;
};
+2 -10
View File
@@ -84,15 +84,11 @@ class Release : public Wt::Dbo::Dbo<Release>
std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const;
// Modifiers
void setTotalDiscNumber(std::size_t num) { _totalDiscNumber = static_cast<int>(num); }
void setTotalTrackNumber(std::size_t num) { _totalTrackNumber = static_cast<int>(num); }
// Accessors
const std::string& getName() const { return _name; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
std::optional<std::size_t> getTotalTrackNumber() const;
std::optional<std::size_t> getTotalDiscNumber() const;
std::optional<std::size_t> getTotalTrack() const;
std::optional<std::size_t> getTotalDisc() const;
std::chrono::milliseconds getDuration() const;
// Get the artists of this release
@@ -109,8 +105,6 @@ class Release : public Wt::Dbo::Dbo<Release>
{
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<Release>
std::string _name;
std::string _MBID;
int _totalDiscNumber {};
int _totalTrackNumber {};
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
Wt::Dbo::collection<Wt::Dbo::ptr<User>> _starringUsers; // Users that starred this release
+14 -6
View File
@@ -88,6 +88,8 @@ class Track : public Wt::Dbo::Dbo<Track>
void setScanVersion(std::size_t version) { _scanVersion = version; }
void setTrackNumber(int num) { _trackNumber = num; }
void setDiscNumber(int num) { _discNumber = num; }
void setTotalTrack(std::optional<int> totalTrack) { totalTrack ? _totalTrack = *totalTrack : 0; }
void setTotalDisc(std::optional<int> 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<Track>
std::size_t getScanVersion() const { return _scanVersion; }
std::optional<std::size_t> getTrackNumber() const;
std::optional<std::size_t> getTotalTrack() const;
std::optional<std::size_t> getDiscNumber() const;
std::optional<std::size_t> 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<Track>
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<Track>
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<int, std::milli> _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;
+2 -7
View File
@@ -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);
+1
View File
@@ -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
+31 -48
View File
@@ -20,9 +20,7 @@
#include "LmsApplication.hpp"
#include <Wt/WAnchor.h>
#include <Wt/WBootstrapTheme.h>
#include <Wt/WEnvironment.h>
#include <Wt/WLinkedCssStyleSheet.h>
#include <Wt/WMenu.h>
#include <Wt/WNavigationBar.h>
#include <Wt/WPopupMenu.h>
@@ -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<Wt::WLinkedCssStyleSheet> 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<Wt::WApplication>
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<LmsTheme>(Database::User::defaultUITheme));
root()->addWidget(std::make_unique<InitWizardView>());
return;
}
@@ -217,10 +183,8 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
if (user)
theme = user->getUITheme();
}
auto LmsTheme {std::make_unique<LmsBooststrapTheme>(theme)};
LmsTheme->setVersion(Wt::BootstrapVersion::v3);
LmsTheme->setResponsive(true);
setTheme(std::move(LmsTheme));
setTheme(std::make_unique<LmsTheme>(theme));
}
if (userId)
@@ -245,6 +209,16 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
Auth* auth {root()->addNew<Auth>()};
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<LmsTheme*>(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<Wt::WTemplate>
std::unique_ptr<Wt::WText>
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::WTemplate>(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<Wt::WText>(std::string {} + (canDelete ? "<i class=\"fa fa-times-circle\"></i> " : "") + 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);
+1 -1
View File
@@ -103,7 +103,7 @@ class LmsApplication : public Wt::WApplication
static std::unique_ptr<Wt::WAnchor> createArtistAnchor(Wt::Dbo::ptr<Database::Artist> artist, bool addText = true);
static Wt::WLink createReleaseLink(Wt::Dbo::ptr<Database::Release> release);
static std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Wt::Dbo::ptr<Database::Release> release, bool addText = true);
static std::unique_ptr<Wt::WTemplate> createCluster(Wt::Dbo::ptr<Database::Cluster> cluster, bool canDelete = false);
static std::unique_ptr<Wt::WText> createCluster(Wt::Dbo::ptr<Database::Cluster> cluster, bool canDelete = false);
// Signal emitted just before the session ends (user may already be logged out)
Wt::Signal<>& preQuit() { return _preQuit; }
+107
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<Wt::WLinkedCssStyleSheet>
LmsTheme::styleSheets() const
{
const std::vector<Wt::WLink> styleSheets {getStyleSheets(_theme)};
std::vector<Wt::WLinkedCssStyleSheet> 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<Wt::WLink>
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 {};
}
}
+47
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <vector>
#include <Wt/WBootstrapTheme.h>
#include <Wt/WLinkedCssStyleSheet.h>
#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<Wt::WLinkedCssStyleSheet> styleSheets() const override;
static std::vector<Wt::WLink> getStyleSheets(Database::User::UITheme theme);
Database::User::UITheme _theme;
};
}
+9 -1
View File
@@ -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<LmsTheme*>(LmsApp->theme().get())};
lmsTheme->setTheme(newTheme);
user.modify()->setUITheme(newTheme);
}
user.modify()->setAudioTranscodeEnable(Wt::asNumber(value(TranscodeEnableField)));
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))};
+1 -1
View File
@@ -126,7 +126,7 @@ void
Filters::add(Database::IdType clusterId)
{
Wt::WTemplate* filter {};
Wt::WInteractWidget* filter {};
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
+34 -14
View File
@@ -77,7 +77,6 @@ Release::refresh()
throw ReleaseNotFoundException {*releaseId};
Wt::WTemplate* t {addNew<Wt::WTemplate>(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<Wt::WContainerWidget>("tracks")};
Wt::WContainerWidget* rootContainer {t->bindNew<Wt::WContainerWidget>("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<std::size_t, Wt::WContainerWidget*> 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::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.disc-entry"))};
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
disc->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WContainerWidget* tracksContainer {disc->bindNew<Wt::WContainerWidget>("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::WTemplate>(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::WTemplate>(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<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
playBtn->clicked().connect(std::bind([=]
{
+84
View File
@@ -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);