New UI layout and UI improvements. Fixes #50
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
|
||||||
<message id="Lms.Admin.Users.template">
|
<message id="Lms.Admin.Users.template">
|
||||||
<div class="page-header">
|
<div class="Lms-header">
|
||||||
<h2>${tr:Lms.Admin.Users.users}</h2>
|
<h2>${tr:Lms.Admin.Users.users}</h2>
|
||||||
</div>
|
</div>
|
||||||
${users}
|
${users}
|
||||||
|
|||||||
+12
-2
@@ -3,12 +3,22 @@
|
|||||||
<!--FORMS message blocks-->
|
<!--FORMS message blocks-->
|
||||||
|
|
||||||
<message id="Lms.Explore.Artist.template">
|
<message id="Lms.Explore.Artist.template">
|
||||||
<div class="page-header">
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="Lms-header">
|
||||||
<h2><div class="Lms-artistname text-primary">${name}</div></h2>
|
<h2><div class="Lms-artistname text-primary">${name}</div></h2>
|
||||||
${clusters}
|
${clusters}
|
||||||
${play-btn}${add-btn}
|
${play-btn}${add-btn}
|
||||||
</div>
|
</div>
|
||||||
${releases}
|
${releases class="Lms-explore-artist-entries-container"}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="well well-sm">
|
||||||
|
<h3>${tr:Lms.Explore.Artist.similar-artists}</h3>
|
||||||
|
${similar-artists}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.Artist.template.entry">
|
<message id="Lms.Explore.Artist.template.entry">
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
|
||||||
<!--FORMS message blocks-->
|
|
||||||
|
|
||||||
<message id="Lms.Explore.ArtistInfo.template">
|
|
||||||
<h3>${tr:Lms.Explore.ArtistInfo.similar-artists}</h3>
|
|
||||||
${similar-artists}
|
|
||||||
</message>
|
|
||||||
|
|
||||||
</messages>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
|
||||||
<!--FORMS message blocks-->
|
|
||||||
|
|
||||||
<message id="Lms.Explore.ArtistLink.template">
|
|
||||||
<div class="row Lms-explore-artistlink Lms-entry-hoverable Lms-vertical-align">
|
|
||||||
<div class="col-xs-12">
|
|
||||||
${name class="Lms-artistname text-primary"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</message>
|
|
||||||
|
|
||||||
</messages>
|
|
||||||
+11
-10
@@ -3,21 +3,14 @@
|
|||||||
<!--FORMS message blocks-->
|
<!--FORMS message blocks-->
|
||||||
|
|
||||||
<message id="Lms.Explore.Artists.template">
|
<message id="Lms.Explore.Artists.template">
|
||||||
<div class="page-header">
|
<div class="Lms-header">
|
||||||
<h2>${tr:Lms.Explore.artists}</h2>
|
<h2>${tr:Lms.Explore.artists}</h2>
|
||||||
</div>
|
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-sm-3">
|
<div class="col-sm-2">
|
||||||
<div class="Lms-explore-artists-input">
|
|
||||||
${link-type}
|
${link-type}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-9">
|
|
||||||
<div class="input-group Lms-explore-artists-input">
|
|
||||||
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
|
||||||
${search}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
${artists}
|
${artists}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -35,4 +28,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
|
<message id="Lms.Explore.Artists.template.entry-small">
|
||||||
|
<div class="row Lms-explore-artists-entry-small Lms-entry-hoverable Lms-vertical-align">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
${name class="Lms-artistname text-primary"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
</messages>
|
</messages>
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
|
||||||
<!--FORMS message blocks-->
|
|
||||||
|
|
||||||
<message id="Lms.Explore.ArtistsInfo.template">
|
|
||||||
<h3>${tr:Lms.Explore.ArtistsInfo.most-played}</h3>
|
|
||||||
${most-played}
|
|
||||||
<h3>${tr:Lms.Explore.ArtistsInfo.recently-added}</h3>
|
|
||||||
${recently-added}
|
|
||||||
</message>
|
|
||||||
|
|
||||||
</messages>
|
|
||||||
+1
-10
@@ -2,23 +2,14 @@
|
|||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
|
||||||
<message id="Lms.Explore.template">
|
<message id="Lms.Explore.template">
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8">
|
|
||||||
${contents class="Lms-explore-contents"}
|
${contents class="Lms-explore-contents"}
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
${filters class="well well-sm"}
|
|
||||||
${info class="well well-sm"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.template.add-btn"><i class="fa fa-plus-circle Lms-explore-btn Lms-control-btn"></i></message>
|
<message id="Lms.Explore.template.add-btn"><i class="fa fa-plus-circle Lms-explore-btn Lms-control-btn"></i></message>
|
||||||
<message id="Lms.Explore.template.play-btn"><i class="fa fa-play-circle Lms-explore-btn Lms-control-btn"></i></message>
|
<message id="Lms.Explore.template.play-btn"><i class="fa fa-play-circle Lms-explore-btn Lms-control-btn"></i></message>
|
||||||
|
|
||||||
<message id="Lms.Explore.template.filters">
|
<message id="Lms.Explore.template.filters">
|
||||||
<h3>${tr:Lms.Explore.filters} ${add-filter class="btn-primary"}</h3>
|
${add-filter class="btn-sm btn-info"} ${clusters class="Lms-explore-clusters"}
|
||||||
${clusters}
|
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.template.add-filter">
|
<message id="Lms.Explore.template.add-filter">
|
||||||
|
|||||||
+16
-16
@@ -4,13 +4,13 @@
|
|||||||
|
|
||||||
<!--Common-->
|
<!--Common-->
|
||||||
<message id="Lms.add">Add</message>
|
<message id="Lms.add">Add</message>
|
||||||
|
<message id="Lms.administration">Administration</message>
|
||||||
<message id="Lms.apply">Apply</message>
|
<message id="Lms.apply">Apply</message>
|
||||||
<message id="Lms.discard">Discard</message>
|
<message id="Lms.discard">Discard</message>
|
||||||
<message id="Lms.administration">Administration</message>
|
|
||||||
<message id="Lms.cancel">Cancel</message>
|
<message id="Lms.cancel">Cancel</message>
|
||||||
<message id="Lms.create">Create</message>
|
<message id="Lms.create">Create</message>
|
||||||
<message id="Lms.login">Login</message>
|
<message id="Lms.login">Login</message>
|
||||||
<message id="Lms.logout">Logout</message>
|
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
|
||||||
<message id="Lms.not-a-directory">Not a directory</message>
|
<message id="Lms.not-a-directory">Not a directory</message>
|
||||||
<message id="Lms.password">Password</message>
|
<message id="Lms.password">Password</message>
|
||||||
<message id="Lms.password-bad-login-combination">Bad login / password combination</message>
|
<message id="Lms.password-bad-login-combination">Bad login / password combination</message>
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
<message id="Lms.quit-confirm">Are you sure?</message>
|
<message id="Lms.quit-confirm">Are you sure?</message>
|
||||||
<message id="Lms.quit-other-session">Another session has been open. Reopen this one?</message>
|
<message id="Lms.quit-other-session">Another session has been open. Reopen this one?</message>
|
||||||
<message id="Lms.save">Save</message>
|
<message id="Lms.save">Save</message>
|
||||||
|
<message id="Lms.user">User</message>
|
||||||
|
|
||||||
<!--Errors-->
|
<!--Errors-->
|
||||||
<message id="Lms.Error.artist-not-found">Artist not found</message>
|
<message id="Lms.Error.artist-not-found">Artist not found</message>
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
<message id="Lms.Admin.Database.database">Music collection</message>
|
<message id="Lms.Admin.Database.database">Music collection</message>
|
||||||
<message id="Lms.Admin.Database.immediate-scan">Scan now!</message>
|
<message id="Lms.Admin.Database.immediate-scan">Scan now!</message>
|
||||||
<message id="Lms.Admin.Database.monthly">Monthly</message>
|
<message id="Lms.Admin.Database.monthly">Monthly</message>
|
||||||
|
<message id="Lms.Admin.Database.menu-database"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Music collection</message>
|
||||||
<message id="Lms.Admin.Database.never">Never</message>
|
<message id="Lms.Admin.Database.never">Never</message>
|
||||||
<message id="Lms.Admin.Database.path">Media root directory</message>
|
<message id="Lms.Admin.Database.path">Media root directory</message>
|
||||||
<message id="Lms.Admin.Database.recommendation-engine-type">Recommendation engine</message>
|
<message id="Lms.Admin.Database.recommendation-engine-type">Recommendation engine</message>
|
||||||
@@ -77,6 +79,7 @@
|
|||||||
<message id="Lms.Admin.Users.demo">Demo</message>
|
<message id="Lms.Admin.Users.demo">Demo</message>
|
||||||
<message id="Lms.Admin.Users.edit">Edit</message>
|
<message id="Lms.Admin.Users.edit">Edit</message>
|
||||||
<message id="Lms.Admin.Users.users">Users</message>
|
<message id="Lms.Admin.Users.users">Users</message>
|
||||||
|
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message>
|
||||||
|
|
||||||
<!--User-->
|
<!--User-->
|
||||||
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Audio transcode bitrate limit</message>
|
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Audio transcode bitrate limit</message>
|
||||||
@@ -100,11 +103,15 @@
|
|||||||
|
|
||||||
<!--Explore-->
|
<!--Explore-->
|
||||||
<message id="Lms.Explore.add-filter">Add filter</message>
|
<message id="Lms.Explore.add-filter">Add filter</message>
|
||||||
|
<message id="Lms.Explore.all">All</message>
|
||||||
<message id="Lms.Explore.artists">Artists</message>
|
<message id="Lms.Explore.artists">Artists</message>
|
||||||
<message id="Lms.Explore.filter-added">Filter added</message>
|
<message id="Lms.Explore.filter-added">Filter added</message>
|
||||||
<message id="Lms.Explore.filters">Filters</message>
|
<message id="Lms.Explore.filters">Filters</message>
|
||||||
|
<message id="Lms.Explore.most-played">Most played</message>
|
||||||
|
<message id="Lms.Explore.random">Random</message>
|
||||||
|
<message id="Lms.Explore.recently-added">Recently added</message>
|
||||||
|
<message id="Lms.Explore.recently-played">Recently played</message>
|
||||||
<message id="Lms.Explore.releases">Albums</message>
|
<message id="Lms.Explore.releases">Albums</message>
|
||||||
<message id="Lms.Explore.search-placeholder">Search...</message>
|
|
||||||
<message id="Lms.Explore.show-more">Show more</message>
|
<message id="Lms.Explore.show-more">Show more</message>
|
||||||
<message id="Lms.Explore.tracks">Tracks</message>
|
<message id="Lms.Explore.tracks">Tracks</message>
|
||||||
<message id="Lms.Explore.type">Type</message>
|
<message id="Lms.Explore.type">Type</message>
|
||||||
@@ -112,28 +119,21 @@
|
|||||||
<message id="Lms.Explore.various-artists">Various artists</message>
|
<message id="Lms.Explore.various-artists">Various artists</message>
|
||||||
|
|
||||||
<!--Explore:Artist-->
|
<!--Explore:Artist-->
|
||||||
<message id="Lms.Explore.ArtistInfo.similar-artists">Similar artists</message>
|
<message id="Lms.Explore.Artist.similar-artists">Similar artists</message>
|
||||||
|
|
||||||
<!--Explore:Artists-->
|
<!--Explore:Artists-->
|
||||||
<message id="Lms.Explore.Artists.linktype-all">All artists</message>
|
<message id="Lms.Explore.Artists.linktype-all">All artists</message>
|
||||||
<message id="Lms.Explore.Artists.linktype-artist">Track artists</message>
|
<message id="Lms.Explore.Artists.linktype-artist">Track artists</message>
|
||||||
<message id="Lms.Explore.Artists.linktype-releaseartist">Album artists</message>
|
<message id="Lms.Explore.Artists.linktype-releaseartist">Album artists</message>
|
||||||
<message id="Lms.Explore.ArtistsInfo.recently-added">Recently added</message>
|
|
||||||
<message id="Lms.Explore.ArtistsInfo.most-played">Top artists</message>
|
|
||||||
|
|
||||||
<!--Explore:Release-->
|
<!--Explore:Release-->
|
||||||
<message id="Lms.Explore.ReleaseInfo.similar-releases">Similar albums</message>
|
<message id="Lms.Explore.Release.similar-releases">Similar albums</message>
|
||||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||||
<message id="Lms.Explore.Release.disc">Disc {1}</message>
|
<message id="Lms.Explore.Release.disc">Disc {1}</message>
|
||||||
|
|
||||||
<!--Explore:Releases-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.ReleasesInfo.recently-added">Recently added</message>
|
<message id="Lms.Explore.Search.search-placeholder">Search...</message>
|
||||||
<message id="Lms.Explore.ReleasesInfo.most-played">Top albums</message>
|
<message id="Lms.Explore.Search.search">Search results</message>
|
||||||
|
|
||||||
<!--Explore:Tracks-->
|
|
||||||
<message id="Lms.Explore.TracksInfo.recently-added">Recently added</message>
|
|
||||||
<message id="Lms.Explore.TracksInfo.most-played">Top tracks</message>
|
|
||||||
|
|
||||||
|
|
||||||
<!--Playqueue-->
|
<!--Playqueue-->
|
||||||
<message id="Lms.PlayQueue.clear">Clear</message>
|
<message id="Lms.PlayQueue.clear">Clear</message>
|
||||||
@@ -162,6 +162,7 @@
|
|||||||
<message id="Lms.Settings.change-password">Change password</message>
|
<message id="Lms.Settings.change-password">Change password</message>
|
||||||
<message id="Lms.Settings.dark-mode">Dark mode</message>
|
<message id="Lms.Settings.dark-mode">Dark mode</message>
|
||||||
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
|
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
|
||||||
|
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
|
||||||
<message id="Lms.Settings.password-bad">Bad password</message>
|
<message id="Lms.Settings.password-bad">Bad password</message>
|
||||||
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message>
|
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message>
|
||||||
<message id="Lms.Settings.replaygain-mode">ReplayGain mode</message>
|
<message id="Lms.Settings.replaygain-mode">ReplayGain mode</message>
|
||||||
@@ -188,7 +189,6 @@
|
|||||||
<message id="Lms.Settings.transcode-mode.always">Always</message>
|
<message id="Lms.Settings.transcode-mode.always">Always</message>
|
||||||
<message id="Lms.Settings.transcode-mode.if-format-not-supported">Only when format is not supported</message>
|
<message id="Lms.Settings.transcode-mode.if-format-not-supported">Only when format is not supported</message>
|
||||||
<message id="Lms.Settings.transcode-mode.never">Never</message>
|
<message id="Lms.Settings.transcode-mode.never">Never</message>
|
||||||
<message id="Lms.Settings.settings">Settings</message>
|
|
||||||
<message id="Lms.Settings.settings-saved">New settings saved!</message>
|
<message id="Lms.Settings.settings-saved">New settings saved!</message>
|
||||||
|
|
||||||
</messages>
|
</messages>
|
||||||
|
|||||||
+15
-14
@@ -4,13 +4,13 @@
|
|||||||
|
|
||||||
<!--Common-->
|
<!--Common-->
|
||||||
<message id="Lms.add">Ajouter</message>
|
<message id="Lms.add">Ajouter</message>
|
||||||
|
<message id="Lms.administration">Administration</message>
|
||||||
<message id="Lms.apply">Appliquer</message>
|
<message id="Lms.apply">Appliquer</message>
|
||||||
<message id="Lms.discard">Annuler</message>
|
<message id="Lms.discard">Annuler</message>
|
||||||
<message id="Lms.administration">Administration</message>
|
|
||||||
<message id="Lms.cancel">Annuler</message>
|
<message id="Lms.cancel">Annuler</message>
|
||||||
<message id="Lms.create">Créer</message>
|
<message id="Lms.create">Créer</message>
|
||||||
<message id="Lms.login">Login</message>
|
<message id="Lms.login">Login</message>
|
||||||
<message id="Lms.logout">Logout</message>
|
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Quitter</message>
|
||||||
<message id="Lms.not-a-directory">N'est pas un répertoire</message>
|
<message id="Lms.not-a-directory">N'est pas un répertoire</message>
|
||||||
<message id="Lms.password">Mot de passe</message>
|
<message id="Lms.password">Mot de passe</message>
|
||||||
<message id="Lms.password-bad-login-combination">Mauvaise combinaison login / mot de passe</message>
|
<message id="Lms.password-bad-login-combination">Mauvaise combinaison login / mot de passe</message>
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
<message id="Lms.quit-confirm">Êtes-vous sûr ?</message>
|
<message id="Lms.quit-confirm">Êtes-vous sûr ?</message>
|
||||||
<message id="Lms.quit-other-session">Une autre session a été ouverte. Reouvrir celle-ci ?</message>
|
<message id="Lms.quit-other-session">Une autre session a été ouverte. Reouvrir celle-ci ?</message>
|
||||||
<message id="Lms.save">Sauvegarder</message>
|
<message id="Lms.save">Sauvegarder</message>
|
||||||
|
<message id="Lms.user">Utilisateur</message>
|
||||||
|
|
||||||
<!--Errors-->
|
<!--Errors-->
|
||||||
<message id="Lms.Error.artist-not-found">Cet artiste n'existe pas</message>
|
<message id="Lms.Error.artist-not-found">Cet artiste n'existe pas</message>
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
<message id="Lms.Admin.Database.database">Collection de musiques</message>
|
<message id="Lms.Admin.Database.database">Collection de musiques</message>
|
||||||
<message id="Lms.Admin.Database.immediate-scan">Scanner maintenant !</message>
|
<message id="Lms.Admin.Database.immediate-scan">Scanner maintenant !</message>
|
||||||
<message id="Lms.Admin.Database.monthly">Tous les mois</message>
|
<message id="Lms.Admin.Database.monthly">Tous les mois</message>
|
||||||
|
<message id="Lms.Admin.Database.menu-database"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Collection de musiques</message>
|
||||||
<message id="Lms.Admin.Database.never">Jamais</message>
|
<message id="Lms.Admin.Database.never">Jamais</message>
|
||||||
<message id="Lms.Admin.Database.path">Dossier racine des fichiers de musique</message>
|
<message id="Lms.Admin.Database.path">Dossier racine des fichiers de musique</message>
|
||||||
<message id="Lms.Admin.Database.recommendation-engine-type">Moteur de recommandation</message>
|
<message id="Lms.Admin.Database.recommendation-engine-type">Moteur de recommandation</message>
|
||||||
@@ -77,6 +79,7 @@
|
|||||||
<message id="Lms.Admin.Users.demo">Demo</message>
|
<message id="Lms.Admin.Users.demo">Demo</message>
|
||||||
<message id="Lms.Admin.Users.edit">Editer</message>
|
<message id="Lms.Admin.Users.edit">Editer</message>
|
||||||
<message id="Lms.Admin.Users.users">Utilisateurs</message>
|
<message id="Lms.Admin.Users.users">Utilisateurs</message>
|
||||||
|
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message>
|
||||||
|
|
||||||
<!--User-->
|
<!--User-->
|
||||||
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Limite du bitrate pour le transcodage</message>
|
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Limite du bitrate pour le transcodage</message>
|
||||||
@@ -100,11 +103,15 @@
|
|||||||
|
|
||||||
<!--Explore-->
|
<!--Explore-->
|
||||||
<message id="Lms.Explore.add-filter">Ajouter filtre</message>
|
<message id="Lms.Explore.add-filter">Ajouter filtre</message>
|
||||||
|
<message id="Lms.Explore.all">Tous</message>
|
||||||
<message id="Lms.Explore.artists">Artistes</message>
|
<message id="Lms.Explore.artists">Artistes</message>
|
||||||
<message id="Lms.Explore.filter-added">Filtre ajouté</message>
|
<message id="Lms.Explore.filter-added">Filtre ajouté</message>
|
||||||
<message id="Lms.Explore.filters">Filtres</message>
|
<message id="Lms.Explore.filters">Filtres</message>
|
||||||
|
<message id="Lms.Explore.mostly-played">Plus joués</message>
|
||||||
|
<message id="Lms.Explore.random">Aléatoire</message>
|
||||||
|
<message id="Lms.Explore.recently-added">Ajoutés récemment</message>
|
||||||
|
<message id="Lms.Explore.recently-played">Joués récemment</message>
|
||||||
<message id="Lms.Explore.releases">Albums</message>
|
<message id="Lms.Explore.releases">Albums</message>
|
||||||
<message id="Lms.Explore.search-placeholder">Rechercher...</message>
|
|
||||||
<message id="Lms.Explore.show-more">Voir plus</message>
|
<message id="Lms.Explore.show-more">Voir plus</message>
|
||||||
<message id="Lms.Explore.tracks">Pistes</message>
|
<message id="Lms.Explore.tracks">Pistes</message>
|
||||||
<message id="Lms.Explore.type">Type</message>
|
<message id="Lms.Explore.type">Type</message>
|
||||||
@@ -112,28 +119,21 @@
|
|||||||
<message id="Lms.Explore.various-artists">Artistes divers</message>
|
<message id="Lms.Explore.various-artists">Artistes divers</message>
|
||||||
|
|
||||||
<!--Explore:Artist-->
|
<!--Explore:Artist-->
|
||||||
<message id="Lms.Explore.ArtistInfo.similar-artists">Artistes similaires</message>
|
<message id="Lms.Explore.Artist.similar-artists">Artistes similaires</message>
|
||||||
|
|
||||||
<!--Explore:Artists-->
|
<!--Explore:Artists-->
|
||||||
<message id="Lms.Explore.Artists.linktype-all">Tous les artistes</message>
|
<message id="Lms.Explore.Artists.linktype-all">Tous les artistes</message>
|
||||||
<message id="Lms.Explore.Artists.linktype-artist">Artistes de piste</message>
|
<message id="Lms.Explore.Artists.linktype-artist">Artistes de piste</message>
|
||||||
<message id="Lms.Explore.Artists.linktype-releaseartist">Artistes d'album</message>
|
<message id="Lms.Explore.Artists.linktype-releaseartist">Artistes d'album</message>
|
||||||
<message id="Lms.Explore.ArtistsInfo.recently-added">Ajouts récents</message>
|
|
||||||
<message id="Lms.Explore.ArtistsInfo.most-played">Artistes populaires</message>
|
|
||||||
|
|
||||||
<!--Explore:Release-->
|
<!--Explore:Release-->
|
||||||
<message id="Lms.Explore.ReleaseInfo.similar-releases">Albums similaires</message>
|
<message id="Lms.Explore.ReleaseInfo.similar-releases">Albums similaires</message>
|
||||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||||
<message id="Lms.Explore.Release.disc">Disque {1}</message>
|
<message id="Lms.Explore.Release.disc">Disque {1}</message>
|
||||||
|
|
||||||
<!--Explore:Releases-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.ReleasesInfo.recently-added">Ajouts récents</message>
|
<message id="Lms.Explore.Search.search-placeholder">Rechercher...</message>
|
||||||
<message id="Lms.Explore.ReleasesInfo.most-played">Albums populaires</message>
|
<message id="Lms.Explore.Search.search">Résultats de la recherche</message>
|
||||||
|
|
||||||
<!--Explore:Tracks-->
|
|
||||||
<message id="Lms.Explore.TracksInfo.recently-added">Ajouts récents</message>
|
|
||||||
<message id="Lms.Explore.TracksInfo.most-played">Pistes populaires</message>
|
|
||||||
|
|
||||||
|
|
||||||
<!--Playqueue-->
|
<!--Playqueue-->
|
||||||
<message id="Lms.PlayQueue.clear">Effacer</message>
|
<message id="Lms.PlayQueue.clear">Effacer</message>
|
||||||
@@ -162,6 +162,7 @@
|
|||||||
<message id="Lms.Settings.change-password">Changement de mot de passe</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.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.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
|
||||||
|
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
|
||||||
<message id="Lms.Settings.password-bad">Mauvais mot de passe</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>
|
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
|
||||||
<message id="Lms.Settings.replaygain-mode">Mode ReplayGain</message>
|
<message id="Lms.Settings.replaygain-mode">Mode ReplayGain</message>
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
|
||||||
<!--FORMS message blocks-->
|
|
||||||
|
|
||||||
<message id="Lms.PlayHistory.template">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="page-header">
|
|
||||||
<h2>${tr:Lms.PlayHistory.playhistory}</h2>
|
|
||||||
</div>
|
|
||||||
${entries}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12 Lms-horizontal-center">
|
|
||||||
${show-more class="btn-primary Lms-show-more"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</message>
|
|
||||||
|
|
||||||
<message id="Lms.PlayHistory.template.entry">
|
|
||||||
<div class="row Lms-playhistory-entry Lms-entry-hoverable Lms-vertical-align">
|
|
||||||
<div class="col-xs-12 Lms-playhistory-entry-name">
|
|
||||||
<div class="media">
|
|
||||||
<div class="media-left media-middle hidden-xs">
|
|
||||||
${cover class="media-object Lms-cover"}
|
|
||||||
</div>
|
|
||||||
<div class="media-body media-middle">
|
|
||||||
|
|
||||||
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
|
||||||
${<if-has-artists-or-release>}
|
|
||||||
${<if-has-release>}<small>${release class="Lms-releasename text-primary"}</small>${</if-has-release>}
|
|
||||||
${<if-has-artists>}<small>${artists}</small>${</if-has-artists>}
|
|
||||||
${</if-has-artists-or-release>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</message>
|
|
||||||
|
|
||||||
<message id="Lms.PlayHistory.template.entry-artist">
|
|
||||||
${artist class="Lms-artistname text-muted"}
|
|
||||||
</message>
|
|
||||||
|
|
||||||
</messages>
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<message id="Lms.PlayQueue.template">
|
<message id="Lms.PlayQueue.template">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="page-header">
|
<div class="Lms-header">
|
||||||
<h2>${tr:Lms.PlayQueue.playqueue}</h2>
|
<h2>${tr:Lms.PlayQueue.playqueue}</h2>
|
||||||
${clear-btn}${shuffle-btn}${repeat-btn}${radio-btn}
|
${clear-btn}${shuffle-btn}${repeat-btn}${radio-btn}
|
||||||
<h4><small>${nb-tracks}</small></h4>
|
<h4><small>${nb-tracks}</small></h4>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<div class="col-xs-9 Lms-playqueue-entry-name">
|
<div class="col-xs-9 Lms-playqueue-entry-name">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left media-middle hidden-xs">
|
<div class="media-left media-middle hidden-xs">
|
||||||
${cover class="media-object Lms-cover"}
|
${cover class="media-object Lms-cover Lms-cover-container-smaller"}
|
||||||
</div>
|
</div>
|
||||||
<div class="media-body media-middle">
|
<div class="media-body media-middle">
|
||||||
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
||||||
|
|||||||
+17
-4
@@ -8,18 +8,31 @@
|
|||||||
|
|
||||||
<message id="Lms.Explore.Release.template">
|
<message id="Lms.Explore.Release.template">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-md-3">
|
||||||
${cover class="Lms-explore-release-cover"}
|
${cover class="Lms-explore-release-cover"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-5">
|
||||||
<div class="page-header">
|
<div class="Lms-header">
|
||||||
<h3><div class="Lms-releasename text-primary">${name}</div></h3>
|
<h3><div class="Lms-releasename text-primary">${name}</div></h3>
|
||||||
${<if-has-artist>}<h4>${artist class="Lms-artistname text-muted"}</h4>${</if-has-artist>}
|
${<if-has-artist>}<h4>${artist class="Lms-artistname text-muted"}</h4>${</if-has-artist>}
|
||||||
<div class="text-muted"><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></div>
|
<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}
|
${clusters}
|
||||||
${play-btn}${add-btn}
|
${play-btn}${add-btn}
|
||||||
</div>
|
</div>
|
||||||
${container}
|
${container class="Lms-explore-release-entries-container"}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="well well-sm">
|
||||||
|
${<if-has-copyright-or-copyright-url>}
|
||||||
|
<h3>${tr:Lms.Explore.Release.copyright}</h3>
|
||||||
|
${<if-has-copyright>}<small>${copyright}</small>${</if-has-copyright>}
|
||||||
|
${<if-has-copyright-url>}<small>${copyright-url}</small>${</if-has-copyright-url>}
|
||||||
|
${</if-has-copyright-or-copyright-url>}
|
||||||
|
<h3>${tr:Lms.Explore.Release.similar-releases}</h3>
|
||||||
|
${similar-releases}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.Release.template.disc-entry">
|
<message id="Lms.Explore.Release.template.disc-entry">
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
|
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
|
||||||
|
|
||||||
<message id="Lms.Explore.ReleaseInfo.template">
|
|
||||||
${<if-has-copyright-or-copyright-url>}
|
|
||||||
<h3>${tr:Lms.Explore.Release.copyright}</h3>
|
|
||||||
${<if-has-copyright>}<small>${copyright}</small>${</if-has-copyright>}
|
|
||||||
${<if-has-copyright-url>}<small>${copyright-url}</small>${</if-has-copyright-url>}
|
|
||||||
${</if-has-copyright-or-copyright-url>}
|
|
||||||
<h3>${tr:Lms.Explore.ReleaseInfo.similar-releases}</h3>
|
|
||||||
${similar-releases}
|
|
||||||
</message>
|
|
||||||
|
|
||||||
</messages>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
|
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
|
||||||
|
|
||||||
<message id="Lms.Explore.ReleaseLink.template">
|
|
||||||
<div class="row Lms-explore-releaselink Lms-entry-hoverable">
|
|
||||||
<div class="col-xs-12">
|
|
||||||
<div class="media">
|
|
||||||
<div class="media-left media-middle">
|
|
||||||
${cover class="media-object Lms-cover"}
|
|
||||||
</div>
|
|
||||||
<div class="media-body media-middle">
|
|
||||||
<div class="media-heading">
|
|
||||||
${release-name class="Lms-releasename text-primary"}
|
|
||||||
</div>
|
|
||||||
${<if-has-artist>}<small>${artist-name class="Lms-artistname text-muted"}</small>${</if-has-artist>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</message>
|
|
||||||
|
|
||||||
</messages>
|
|
||||||
+22
-8
@@ -3,16 +3,11 @@
|
|||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
|
||||||
<message id="Lms.Explore.Releases.template">
|
<message id="Lms.Explore.Releases.template">
|
||||||
<div class="page-header">
|
<div class="Lms-header">
|
||||||
<h2>${tr:Lms.Explore.releases}</h2>
|
<h2>${tr:Lms.Explore.releases}</h2>
|
||||||
|
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||||
${play-btn}${add-btn}
|
${play-btn}${add-btn}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
|
||||||
${search}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${releases}
|
${releases}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 Lms-horizontal-center">
|
<div class="col-xs-12 Lms-horizontal-center">
|
||||||
@@ -26,7 +21,7 @@
|
|||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left media-middle">
|
<div class="media-left media-middle">
|
||||||
${cover class="media-object Lms-cover"}
|
${cover class="media-object Lms-cover Lms-cover-container-medium"}
|
||||||
</div>
|
</div>
|
||||||
<div class="media-body media-middle">
|
<div class="media-body media-middle">
|
||||||
<h4 class="media-heading">
|
<h4 class="media-heading">
|
||||||
@@ -40,4 +35,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
|
<message id="Lms.Explore.Releases.template.entry-small">
|
||||||
|
<div class="row Lms-explore-releaselink Lms-entry-hoverable">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<div class="media">
|
||||||
|
<div class="media-left media-middle">
|
||||||
|
${cover class="media-object Lms-cover Lms-cover-container-smaller"}
|
||||||
|
</div>
|
||||||
|
<div class="media-body media-middle">
|
||||||
|
<div class="media-heading">
|
||||||
|
${release-name class="Lms-releasename text-primary"}
|
||||||
|
</div>
|
||||||
|
${<if-has-artist>}<small>${artist-name class="Lms-artistname text-muted"}</small>${</if-has-artist>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
|
||||||
</messages>
|
</messages>
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
|
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
|
||||||
|
|
||||||
<message id="Lms.Explore.ReleasesInfo.template">
|
|
||||||
<h3>${tr:Lms.Explore.ReleasesInfo.most-played}</h3>
|
|
||||||
${most-played}
|
|
||||||
<h3>${tr:Lms.Explore.ReleasesInfo.recently-added}</h3>
|
|
||||||
${recently-added}
|
|
||||||
</message>
|
|
||||||
|
|
||||||
</messages>
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
<!--FORMS message blocks-->
|
||||||
|
|
||||||
|
<message id="Lms.Explore.Search.template">
|
||||||
|
<div class="Lms-header">
|
||||||
|
<h2>${tr:Lms.Explore.Search.search}</h2>
|
||||||
|
</div>
|
||||||
|
${<if-artists>}
|
||||||
|
<h3>${tr:Lms.Explore.artists}</h3>
|
||||||
|
${artists}
|
||||||
|
${</if-artists>}
|
||||||
|
${<if-releases>}
|
||||||
|
<h3>${tr:Lms.Explore.releases}</h3>
|
||||||
|
${releases}
|
||||||
|
${</if-releases>}
|
||||||
|
${<if-tracks>}
|
||||||
|
<h3>${tr:Lms.Explore.tracks}</h3>
|
||||||
|
${tracks}
|
||||||
|
${</if-tracks>}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="Lms.Explore.Search.template.track-entry">
|
||||||
|
<div class="row Lms-explore-tracks-entry Lms-entry-hoverable Lms-vertical-align">
|
||||||
|
<div class="col-xs-12 Lms-explore-tracks-entry-name">
|
||||||
|
<div class="media">
|
||||||
|
<div class="media-left media-middle hidden-xs">
|
||||||
|
${cover class="media-object Lms-cover"}
|
||||||
|
</div>
|
||||||
|
<div class="media-body media-middle">
|
||||||
|
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
||||||
|
${<if-has-artists-or-release>}
|
||||||
|
${<if-has-release>}<small>${release class="Lms-releasename text-primary"}</small>${</if-has-release>}
|
||||||
|
${<if-has-artists>}${artists}${</if-has-artists>}
|
||||||
|
${</if-has-artists-or-release>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
</messages>
|
||||||
+48
-1
@@ -2,7 +2,54 @@
|
|||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
|
||||||
<message id="Lms.template">
|
<message id="Lms.template">
|
||||||
${navbar-top class="Lms-navbar"}
|
<nav class="navbar navbar-default">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button class="navbar-toggle" data-target=".navbar-collapse" data-toggle="collapse">
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
${title class="navbar-brand"}
|
||||||
|
<div class="Lms-navbar-search pull-left">
|
||||||
|
${search class="form-control"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse navbar-collapse">
|
||||||
|
<ul class="nav navbar-nav">
|
||||||
|
<li>${artists}</li>
|
||||||
|
<li>${releases}</li>
|
||||||
|
<li>${tracks}</li>
|
||||||
|
<li>${playqueue}</li>
|
||||||
|
</ul>
|
||||||
|
<div class="navbar-form navbar-left">
|
||||||
|
${filters class="form-group Lms-clusters-control"}
|
||||||
|
</div>
|
||||||
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
${<if-is-admin>}
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" title="${tr:Lms.administration}"><i class="fa fa-cogs" aria-hidden="true"></i> <span class="caret"></span></a>
|
||||||
|
<ul class="dropdown-menu" role="menu">
|
||||||
|
<li>${database}</li>
|
||||||
|
<li>${users}</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
${</if-is-admin>}
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" title="${tr:Lms.user}"><i class="fa fa-user" aria-hidden="true"></i>
|
||||||
|
<span class="caret"></span></a>
|
||||||
|
<ul class="dropdown-menu" role="menu">
|
||||||
|
<li class="dropdown-header">${username}</li>
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li>${settings}</li>
|
||||||
|
<li>${logout}</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
${contents class="Lms-contents"}
|
${contents class="Lms-contents"}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+8
-9
@@ -4,19 +4,18 @@
|
|||||||
<!--FORMS message blocks-->
|
<!--FORMS message blocks-->
|
||||||
|
|
||||||
<message id="Lms.Explore.Tracks.template">
|
<message id="Lms.Explore.Tracks.template">
|
||||||
<div class="page-header">
|
<div class="Lms-header">
|
||||||
<h2>${tr:Lms.Explore.tracks}</h2>
|
<h2>${tr:Lms.Explore.tracks}</h2>
|
||||||
|
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||||
${play-btn}${add-btn}
|
${play-btn}${add-btn}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
|
||||||
${search}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${tracks}
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 Lms-horizontal-center">
|
<div class="col-lg-8">
|
||||||
|
${tracks}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 col-xs-12 Lms-horizontal-center">
|
||||||
${show-more class="btn-primary Lms-show-more"}
|
${show-more class="btn-primary Lms-show-more"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+57
-27
@@ -4,25 +4,15 @@ a, a:link, a:active, a:visited {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding-bottom: 78px;
|
||||||
|
}
|
||||||
|
|
||||||
.Lms-vertical-align {
|
.Lms-vertical-align {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-horizontal-center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Lms-contents {
|
|
||||||
margin-bottom: 78px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Lms-show-more {
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Lms-admin-users-entry {
|
.Lms-admin-users-entry {
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
@@ -63,6 +53,10 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
filter: brightness(130%);
|
filter: brightness(130%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Lms-clusters-control {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.Lms-control-btn:hover {
|
.Lms-control-btn:hover {
|
||||||
color: var(--hover-color);
|
color: var(--hover-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -72,18 +66,27 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
filter: brightness(130%);
|
filter: brightness(130%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Lms-cover-container-smaller {
|
||||||
|
}
|
||||||
|
|
||||||
.Lms-cover-smaller {
|
.Lms-cover-smaller {
|
||||||
width: 64px; /* some cover may not be square */
|
width: 64px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Lms-cover-container-small {
|
||||||
|
}
|
||||||
|
|
||||||
.Lms-cover-small {
|
.Lms-cover-small {
|
||||||
width: 96px; /* some cover may not be square */
|
width: 96px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Lms-cover-container-medium {
|
||||||
|
}
|
||||||
|
|
||||||
.Lms-cover-medium {
|
.Lms-cover-medium {
|
||||||
width: 128px;
|
width: 128px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
@@ -107,6 +110,10 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
width: 28px;
|
width: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Lms-explore-clusters {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.Lms-explore-contents {
|
.Lms-explore-contents {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
@@ -120,6 +127,10 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Lms-explore-release-entries-container {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.Lms-explore-release-entry-tracknumber {
|
.Lms-explore-release-entry-tracknumber {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -160,12 +171,9 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-explore-artists-entry {
|
.Lms-explore-artists-entry-small {
|
||||||
min-height: 48px;
|
min-height: 28px;
|
||||||
}
|
margin: 0;
|
||||||
|
|
||||||
.Lms-explore-artists-entry-name {
|
|
||||||
font-style: normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-explore-artists-input {
|
.Lms-explore-artists-input {
|
||||||
@@ -177,9 +185,12 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-explore-artistlink {
|
.Lms-explore-artist-entries-container {
|
||||||
min-height: 28px;
|
margin-bottom: 15px;
|
||||||
margin: 0;
|
}
|
||||||
|
|
||||||
|
.Lms-explore-mode-container {
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-explore-tracks-entry {
|
.Lms-explore-tracks-entry {
|
||||||
@@ -215,8 +226,21 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-navbar {
|
.Lms-header {
|
||||||
border-radius: 0;
|
padding-bottom: 9px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Lms-horizontal-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Lms-navbar-search {
|
||||||
|
max-width: 175px;
|
||||||
|
padding-top: 7.5px;
|
||||||
|
padding-bottom: 7.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-playqueue-btn {
|
.Lms-playqueue-btn {
|
||||||
@@ -426,3 +450,9 @@ a.Lms-releasename:hover, a.Lms-releasename:focus {
|
|||||||
color: var(--hover-color);
|
color: var(--hover-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Lms-show-more {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Executable
+211
@@ -0,0 +1,211 @@
|
|||||||
|
/* ========================================================================
|
||||||
|
* Bootstrap: collapse.js v3.3.6
|
||||||
|
* http://getbootstrap.com/javascript/#collapse
|
||||||
|
* ========================================================================
|
||||||
|
* Copyright 2011-2015 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* ======================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
+function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// COLLAPSE PUBLIC CLASS DEFINITION
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
var Collapse = function (element, options) {
|
||||||
|
this.$element = $(element)
|
||||||
|
this.options = $.extend({}, Collapse.DEFAULTS, options)
|
||||||
|
this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' +
|
||||||
|
'[data-toggle="collapse"][data-target="#' + element.id + '"]')
|
||||||
|
this.transitioning = null
|
||||||
|
|
||||||
|
if (this.options.parent) {
|
||||||
|
this.$parent = this.getParent()
|
||||||
|
} else {
|
||||||
|
this.addAriaAndCollapsedClass(this.$element, this.$trigger)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.toggle) this.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.VERSION = '3.3.6'
|
||||||
|
|
||||||
|
Collapse.TRANSITION_DURATION = 350
|
||||||
|
|
||||||
|
Collapse.DEFAULTS = {
|
||||||
|
toggle: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.dimension = function () {
|
||||||
|
var hasWidth = this.$element.hasClass('width')
|
||||||
|
return hasWidth ? 'width' : 'height'
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.show = function () {
|
||||||
|
if (this.transitioning || this.$element.hasClass('in')) return
|
||||||
|
|
||||||
|
var activesData
|
||||||
|
var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing')
|
||||||
|
|
||||||
|
if (actives && actives.length) {
|
||||||
|
activesData = actives.data('bs.collapse')
|
||||||
|
if (activesData && activesData.transitioning) return
|
||||||
|
}
|
||||||
|
|
||||||
|
var startEvent = $.Event('show.bs.collapse')
|
||||||
|
this.$element.trigger(startEvent)
|
||||||
|
if (startEvent.isDefaultPrevented()) return
|
||||||
|
|
||||||
|
if (actives && actives.length) {
|
||||||
|
Plugin.call(actives, 'hide')
|
||||||
|
activesData || actives.data('bs.collapse', null)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dimension = this.dimension()
|
||||||
|
|
||||||
|
this.$element
|
||||||
|
.removeClass('collapse')
|
||||||
|
.addClass('collapsing')[dimension](0)
|
||||||
|
.attr('aria-expanded', true)
|
||||||
|
|
||||||
|
this.$trigger
|
||||||
|
.removeClass('collapsed')
|
||||||
|
.attr('aria-expanded', true)
|
||||||
|
|
||||||
|
this.transitioning = 1
|
||||||
|
|
||||||
|
var complete = function () {
|
||||||
|
this.$element
|
||||||
|
.removeClass('collapsing')
|
||||||
|
.addClass('collapse in')[dimension]('')
|
||||||
|
this.transitioning = 0
|
||||||
|
this.$element
|
||||||
|
.trigger('shown.bs.collapse')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$.support.transition) return complete.call(this)
|
||||||
|
|
||||||
|
var scrollSize = $.camelCase(['scroll', dimension].join('-'))
|
||||||
|
|
||||||
|
this.$element
|
||||||
|
.one('bsTransitionEnd', $.proxy(complete, this))
|
||||||
|
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize])
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.hide = function () {
|
||||||
|
if (this.transitioning || !this.$element.hasClass('in')) return
|
||||||
|
|
||||||
|
var startEvent = $.Event('hide.bs.collapse')
|
||||||
|
this.$element.trigger(startEvent)
|
||||||
|
if (startEvent.isDefaultPrevented()) return
|
||||||
|
|
||||||
|
var dimension = this.dimension()
|
||||||
|
|
||||||
|
this.$element[dimension](this.$element[dimension]())[0].offsetHeight
|
||||||
|
|
||||||
|
this.$element
|
||||||
|
.addClass('collapsing')
|
||||||
|
.removeClass('collapse in')
|
||||||
|
.attr('aria-expanded', false)
|
||||||
|
|
||||||
|
this.$trigger
|
||||||
|
.addClass('collapsed')
|
||||||
|
.attr('aria-expanded', false)
|
||||||
|
|
||||||
|
this.transitioning = 1
|
||||||
|
|
||||||
|
var complete = function () {
|
||||||
|
this.transitioning = 0
|
||||||
|
this.$element
|
||||||
|
.removeClass('collapsing')
|
||||||
|
.addClass('collapse')
|
||||||
|
.trigger('hidden.bs.collapse')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$.support.transition) return complete.call(this)
|
||||||
|
|
||||||
|
this.$element
|
||||||
|
[dimension](0)
|
||||||
|
.one('bsTransitionEnd', $.proxy(complete, this))
|
||||||
|
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.toggle = function () {
|
||||||
|
this[this.$element.hasClass('in') ? 'hide' : 'show']()
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.getParent = function () {
|
||||||
|
return $(this.options.parent)
|
||||||
|
.find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
|
||||||
|
.each($.proxy(function (i, element) {
|
||||||
|
var $element = $(element)
|
||||||
|
this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element)
|
||||||
|
}, this))
|
||||||
|
.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) {
|
||||||
|
var isOpen = $element.hasClass('in')
|
||||||
|
|
||||||
|
$element.attr('aria-expanded', isOpen)
|
||||||
|
$trigger
|
||||||
|
.toggleClass('collapsed', !isOpen)
|
||||||
|
.attr('aria-expanded', isOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetFromTrigger($trigger) {
|
||||||
|
var href
|
||||||
|
var target = $trigger.attr('data-target')
|
||||||
|
|| (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
|
||||||
|
|
||||||
|
return $(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// COLLAPSE PLUGIN DEFINITION
|
||||||
|
// ==========================
|
||||||
|
|
||||||
|
function Plugin(option) {
|
||||||
|
return this.each(function () {
|
||||||
|
var $this = $(this)
|
||||||
|
var data = $this.data('bs.collapse')
|
||||||
|
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||||
|
|
||||||
|
if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false
|
||||||
|
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
|
||||||
|
if (typeof option == 'string') data[option]()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var old = $.fn.collapse
|
||||||
|
|
||||||
|
$.fn.collapse = Plugin
|
||||||
|
$.fn.collapse.Constructor = Collapse
|
||||||
|
|
||||||
|
|
||||||
|
// COLLAPSE NO CONFLICT
|
||||||
|
// ====================
|
||||||
|
|
||||||
|
$.fn.collapse.noConflict = function () {
|
||||||
|
$.fn.collapse = old
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// COLLAPSE DATA-API
|
||||||
|
// =================
|
||||||
|
|
||||||
|
$(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) {
|
||||||
|
var $this = $(this)
|
||||||
|
|
||||||
|
if (!$this.attr('data-target')) e.preventDefault()
|
||||||
|
|
||||||
|
var $target = getTargetFromTrigger($this)
|
||||||
|
var data = $target.data('bs.collapse')
|
||||||
|
var option = data ? 'toggle' : $this.data()
|
||||||
|
|
||||||
|
Plugin.call($target, option)
|
||||||
|
})
|
||||||
|
|
||||||
|
}(jQuery);
|
||||||
Executable
+165
@@ -0,0 +1,165 @@
|
|||||||
|
/* ========================================================================
|
||||||
|
* Bootstrap: dropdown.js v3.3.6
|
||||||
|
* http://getbootstrap.com/javascript/#dropdowns
|
||||||
|
* ========================================================================
|
||||||
|
* Copyright 2011-2015 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* ======================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
+function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// DROPDOWN CLASS DEFINITION
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
var backdrop = '.dropdown-backdrop'
|
||||||
|
var toggle = '[data-toggle="dropdown"]'
|
||||||
|
var Dropdown = function (element) {
|
||||||
|
$(element).on('click.bs.dropdown', this.toggle)
|
||||||
|
}
|
||||||
|
|
||||||
|
Dropdown.VERSION = '3.3.6'
|
||||||
|
|
||||||
|
function getParent($this) {
|
||||||
|
var selector = $this.attr('data-target')
|
||||||
|
|
||||||
|
if (!selector) {
|
||||||
|
selector = $this.attr('href')
|
||||||
|
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
|
||||||
|
}
|
||||||
|
|
||||||
|
var $parent = selector && $(selector)
|
||||||
|
|
||||||
|
return $parent && $parent.length ? $parent : $this.parent()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMenus(e) {
|
||||||
|
if (e && e.which === 3) return
|
||||||
|
$(backdrop).remove()
|
||||||
|
$(toggle).each(function () {
|
||||||
|
var $this = $(this)
|
||||||
|
var $parent = getParent($this)
|
||||||
|
var relatedTarget = { relatedTarget: this }
|
||||||
|
|
||||||
|
if (!$parent.hasClass('open')) return
|
||||||
|
|
||||||
|
if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return
|
||||||
|
|
||||||
|
$parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
|
||||||
|
|
||||||
|
if (e.isDefaultPrevented()) return
|
||||||
|
|
||||||
|
$this.attr('aria-expanded', 'false')
|
||||||
|
$parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Dropdown.prototype.toggle = function (e) {
|
||||||
|
var $this = $(this)
|
||||||
|
|
||||||
|
if ($this.is('.disabled, :disabled')) return
|
||||||
|
|
||||||
|
var $parent = getParent($this)
|
||||||
|
var isActive = $parent.hasClass('open')
|
||||||
|
|
||||||
|
clearMenus()
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
|
||||||
|
// if mobile we use a backdrop because click events don't delegate
|
||||||
|
$(document.createElement('div'))
|
||||||
|
.addClass('dropdown-backdrop')
|
||||||
|
.insertAfter($(this))
|
||||||
|
.on('click', clearMenus)
|
||||||
|
}
|
||||||
|
|
||||||
|
var relatedTarget = { relatedTarget: this }
|
||||||
|
$parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))
|
||||||
|
|
||||||
|
if (e.isDefaultPrevented()) return
|
||||||
|
|
||||||
|
$this
|
||||||
|
.trigger('focus')
|
||||||
|
.attr('aria-expanded', 'true')
|
||||||
|
|
||||||
|
$parent
|
||||||
|
.toggleClass('open')
|
||||||
|
.trigger($.Event('shown.bs.dropdown', relatedTarget))
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
Dropdown.prototype.keydown = function (e) {
|
||||||
|
if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return
|
||||||
|
|
||||||
|
var $this = $(this)
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
if ($this.is('.disabled, :disabled')) return
|
||||||
|
|
||||||
|
var $parent = getParent($this)
|
||||||
|
var isActive = $parent.hasClass('open')
|
||||||
|
|
||||||
|
if (!isActive && e.which != 27 || isActive && e.which == 27) {
|
||||||
|
if (e.which == 27) $parent.find(toggle).trigger('focus')
|
||||||
|
return $this.trigger('click')
|
||||||
|
}
|
||||||
|
|
||||||
|
var desc = ' li:not(.disabled):visible a'
|
||||||
|
var $items = $parent.find('.dropdown-menu' + desc)
|
||||||
|
|
||||||
|
if (!$items.length) return
|
||||||
|
|
||||||
|
var index = $items.index(e.target)
|
||||||
|
|
||||||
|
if (e.which == 38 && index > 0) index-- // up
|
||||||
|
if (e.which == 40 && index < $items.length - 1) index++ // down
|
||||||
|
if (!~index) index = 0
|
||||||
|
|
||||||
|
$items.eq(index).trigger('focus')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// DROPDOWN PLUGIN DEFINITION
|
||||||
|
// ==========================
|
||||||
|
|
||||||
|
function Plugin(option) {
|
||||||
|
return this.each(function () {
|
||||||
|
var $this = $(this)
|
||||||
|
var data = $this.data('bs.dropdown')
|
||||||
|
|
||||||
|
if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))
|
||||||
|
if (typeof option == 'string') data[option].call($this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var old = $.fn.dropdown
|
||||||
|
|
||||||
|
$.fn.dropdown = Plugin
|
||||||
|
$.fn.dropdown.Constructor = Dropdown
|
||||||
|
|
||||||
|
|
||||||
|
// DROPDOWN NO CONFLICT
|
||||||
|
// ====================
|
||||||
|
|
||||||
|
$.fn.dropdown.noConflict = function () {
|
||||||
|
$.fn.dropdown = old
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// APPLY TO STANDARD DROPDOWN ELEMENTS
|
||||||
|
// ===================================
|
||||||
|
|
||||||
|
$(document)
|
||||||
|
.on('click.bs.dropdown.data-api', clearMenus)
|
||||||
|
.on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
|
||||||
|
.on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
|
||||||
|
.on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
|
||||||
|
.on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown)
|
||||||
|
|
||||||
|
}(jQuery);
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
/* ========================================================================
|
||||||
|
* Bootstrap: transition.js v3.3.6
|
||||||
|
* http://getbootstrap.com/javascript/#transitions
|
||||||
|
* ========================================================================
|
||||||
|
* Copyright 2011-2015 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* ======================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
+function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
function transitionEnd() {
|
||||||
|
var el = document.createElement('bootstrap')
|
||||||
|
|
||||||
|
var transEndEventNames = {
|
||||||
|
WebkitTransition : 'webkitTransitionEnd',
|
||||||
|
MozTransition : 'transitionend',
|
||||||
|
OTransition : 'oTransitionEnd otransitionend',
|
||||||
|
transition : 'transitionend'
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var name in transEndEventNames) {
|
||||||
|
if (el.style[name] !== undefined) {
|
||||||
|
return { end: transEndEventNames[name] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false // explicit for ie8 ( ._.)
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://blog.alexmaccaw.com/css-transitions
|
||||||
|
$.fn.emulateTransitionEnd = function (duration) {
|
||||||
|
var called = false
|
||||||
|
var $el = this
|
||||||
|
$(this).one('bsTransitionEnd', function () { called = true })
|
||||||
|
var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
|
||||||
|
setTimeout(callback, duration)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
$.support.transition = transitionEnd()
|
||||||
|
|
||||||
|
if (!$.support.transition) return
|
||||||
|
|
||||||
|
$.event.special.bsTransitionEnd = {
|
||||||
|
bindType: $.support.transition.end,
|
||||||
|
delegateType: $.support.transition.end,
|
||||||
|
handle: function (e) {
|
||||||
|
if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}(jQuery);
|
||||||
@@ -114,7 +114,7 @@ namespace CoverArt
|
|||||||
void saveToCache(const CacheEntryDesc& entryDesc, const Image& image);
|
void saveToCache(const CacheEntryDesc& entryDesc, const Image& image);
|
||||||
std::optional<Image> loadFromCache(const CacheEntryDesc& entryDesc);
|
std::optional<Image> loadFromCache(const CacheEntryDesc& entryDesc);
|
||||||
|
|
||||||
static inline constexpr std::size_t _maxCacheEntries {300};
|
static inline constexpr std::size_t _maxCacheEntries {1000};
|
||||||
static inline const std::vector<std::filesystem::path> _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
|
static inline const std::vector<std::filesystem::path> _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
|
||||||
static inline constexpr std::size_t _maxFileSize {10000000};
|
static inline constexpr std::size_t _maxFileSize {10000000};
|
||||||
static inline const std::vector<std::filesystem::path> _preferredFileNames {"cover", "front"}; // TODO parametrize
|
static inline const std::vector<std::filesystem::path> _preferredFileNames {"cover", "front"}; // TODO parametrize
|
||||||
|
|||||||
@@ -74,91 +74,133 @@ Artist::create(Session& session, const std::string& name, const std::optional<UU
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
static
|
static
|
||||||
Wt::Dbo::Query<Artist::pointer>
|
Wt::Dbo::Query<T>
|
||||||
getQuery(Session& session,
|
createQuery(Session& session,
|
||||||
|
const std::string& queryStr,
|
||||||
const std::set<IdType>& clusterIds,
|
const std::set<IdType>& clusterIds,
|
||||||
const std::vector<std::string>& keywords,
|
const std::vector<std::string>& keywords,
|
||||||
std::optional<TrackArtistLink::Type> linkType,
|
std::optional<TrackArtistLink::Type> linkType)
|
||||||
Artist::SortMethod sortMethod)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
WhereClause where;
|
auto query {session.getDboSession().query<T>(queryStr)};
|
||||||
|
query.join("track t ON t.id = t_a_l.track_id");
|
||||||
|
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
|
||||||
|
|
||||||
std::ostringstream oss;
|
if (linkType)
|
||||||
oss << "SELECT DISTINCT a FROM artist a";
|
query.where("t_a_l.type = ?").bind(*linkType);
|
||||||
|
|
||||||
if (!keywords.empty())
|
if (!keywords.empty())
|
||||||
{
|
{
|
||||||
WhereClause whereKeywordsName;
|
std::vector<std::string> clauses;
|
||||||
WhereClause whereKeywordsSortName;
|
std::vector<std::string> sortClauses;
|
||||||
|
|
||||||
for (auto keyword : keywords)
|
for (const std::string& keyword : keywords)
|
||||||
{
|
{
|
||||||
whereKeywordsName.And(WhereClause("a.name LIKE ?")).bind("%%" + keyword + "%%");
|
clauses.push_back("a.name LIKE ?");
|
||||||
whereKeywordsSortName.And(WhereClause("a.sort_name LIKE ?")).bind("%%" + keyword + "%%");
|
query.bind("%%" + keyword + "%%");
|
||||||
|
|
||||||
|
sortClauses.push_back("a.sort_name LIKE ?");
|
||||||
|
query.bind("%%" + keyword + "%%");
|
||||||
}
|
}
|
||||||
|
|
||||||
where.And(whereKeywordsName.Or(whereKeywordsSortName));
|
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clusterIds.empty() || linkType)
|
|
||||||
{
|
|
||||||
oss << " INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id";
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
if (!clusterIds.empty())
|
||||||
{
|
{
|
||||||
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
std::ostringstream oss;
|
||||||
|
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||||
|
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
||||||
|
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||||
|
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||||
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||||
|
|
||||||
WhereClause clusterClause;
|
WhereClause clusterClause;
|
||||||
|
for (const IdType clusterId : clusterIds)
|
||||||
for (auto id : clusterIds)
|
|
||||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
|
||||||
|
|
||||||
where.And(clusterClause);
|
|
||||||
}
|
|
||||||
if (linkType)
|
|
||||||
where.And(WhereClause {"t_a_l.type = ?"}.bind(std::to_string(static_cast<int>(*linkType))));
|
|
||||||
}
|
|
||||||
oss << " " << where.get();
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
|
||||||
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size();
|
|
||||||
|
|
||||||
switch (sortMethod)
|
|
||||||
{
|
{
|
||||||
case Artist::SortMethod::None:
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
break;
|
query.bind(clusterId);
|
||||||
case Artist::SortMethod::ByName:
|
|
||||||
oss << " ORDER BY a.name COLLATE NOCASE";
|
|
||||||
break;
|
|
||||||
case Artist::SortMethod::BySortName:
|
|
||||||
oss << " ORDER BY a.sort_name COLLATE NOCASE";
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::Dbo::Query<Artist::pointer> query = session.getDboSession().query<Artist::pointer>( oss.str() );
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
for (const std::string& bindArg : where.getBindArgs())
|
query.where(oss.str());
|
||||||
{
|
|
||||||
query.bind(bindArg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Artist::pointer>
|
std::vector<Artist::pointer>
|
||||||
Artist::getAll(Session& session, SortMethod sortMethod, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
Artist::getAll(Session& session)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<Artist::pointer> res = getQuery(session, {}, {}, std::nullopt, sortMethod)
|
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Artist>();
|
||||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
|
||||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
|
||||||
|
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
return std::vector<pointer>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<Artist::pointer>
|
||||||
|
Artist::getAll(Session& session, SortMethod sortMethod)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto query {session.getDboSession().find<Artist>()};
|
||||||
|
switch (sortMethod)
|
||||||
|
{
|
||||||
|
case Artist::SortMethod::None:
|
||||||
|
break;
|
||||||
|
case Artist::SortMethod::ByName:
|
||||||
|
query.orderBy("name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
case Artist::SortMethod::BySortName:
|
||||||
|
query.orderBy("sort_name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::Dbo::collection<pointer> res = query;
|
||||||
|
return std::vector<pointer>(res.begin(), res.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<Artist::pointer>
|
||||||
|
Artist::getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto query {createQuery<Artist::pointer>(session, "SELECT a FROM Artist a", {}, {}, std::nullopt)};
|
||||||
|
|
||||||
|
switch (sortMethod)
|
||||||
|
{
|
||||||
|
case Artist::SortMethod::None:
|
||||||
|
break;
|
||||||
|
case Artist::SortMethod::ByName:
|
||||||
|
query.orderBy("a.name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
case Artist::SortMethod::BySortName:
|
||||||
|
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||||
|
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||||
|
|
||||||
|
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||||
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<IdType>
|
std::vector<IdType>
|
||||||
Artist::getAllIds(Session& session)
|
Artist::getAllIds(Session& session)
|
||||||
{
|
{
|
||||||
@@ -168,6 +210,21 @@ Artist::getAllIds(Session& session)
|
|||||||
return std::vector<IdType>(res.begin(), res.end());
|
return std::vector<IdType>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<IdType>
|
||||||
|
Artist::getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<std::size_t> size)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto query {createQuery<IdType>(session, "SELECT DISTINCT a.id from artist a", clusters, {}, linkType)};
|
||||||
|
|
||||||
|
Wt::Dbo::collection<IdType> res = query
|
||||||
|
.orderBy("RANDOM()")
|
||||||
|
.limit(size ? static_cast<int>(*size) : -1);
|
||||||
|
|
||||||
|
return std::vector<IdType>(res.begin(), res.end());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Artist::pointer>
|
std::vector<Artist::pointer>
|
||||||
Artist::getAllOrphans(Session& session)
|
Artist::getAllOrphans(Session& session)
|
||||||
{
|
{
|
||||||
@@ -197,8 +254,8 @@ Artist::getByClusters(Session& session, const std::set<IdType>& clusters, SortMe
|
|||||||
assert(!clusters.empty());
|
assert(!clusters.empty());
|
||||||
|
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
bool more;
|
bool more{};
|
||||||
return getByFilter(session, clusters, {}, {}, sortMethod, {}, {}, more);
|
return getByFilter(session, clusters, {}, std::nullopt, sortMethod, std::nullopt, more);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Artist::pointer>
|
std::vector<Artist::pointer>
|
||||||
@@ -207,18 +264,31 @@ Artist::getByFilter(Session& session,
|
|||||||
const std::vector<std::string>& keywords,
|
const std::vector<std::string>& keywords,
|
||||||
std::optional<TrackArtistLink::Type> linkType,
|
std::optional<TrackArtistLink::Type> linkType,
|
||||||
SortMethod sortMethod,
|
SortMethod sortMethod,
|
||||||
std::optional<std::size_t> offset,
|
std::optional<Range> range,
|
||||||
std::optional<std::size_t> size,
|
|
||||||
bool& moreResults)
|
bool& moreResults)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
Wt::Dbo::collection<Artist::pointer> collection = getQuery(session, clusters, keywords, linkType, sortMethod)
|
|
||||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
auto query {createQuery<Artist::pointer>(session, "SELECT DISTINCT a from artist a", clusters, keywords, linkType)};
|
||||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
switch (sortMethod)
|
||||||
|
{
|
||||||
|
case Artist::SortMethod::None:
|
||||||
|
break;
|
||||||
|
case Artist::SortMethod::ByName:
|
||||||
|
query.orderBy("a.name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
case Artist::SortMethod::BySortName:
|
||||||
|
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||||
|
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||||
|
|
||||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||||
|
|
||||||
if (size && res.size() == static_cast<std::size_t>(*size) + 1)
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
{
|
{
|
||||||
moreResults = true;
|
moreResults = true;
|
||||||
res.pop_back();
|
res.pop_back();
|
||||||
@@ -230,14 +300,29 @@ Artist::getByFilter(Session& session,
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Artist::pointer>
|
std::vector<Artist::pointer>
|
||||||
Artist::getLastWritten(Session& session, Wt::WDateTime after, std::optional<std::size_t> limit)
|
Artist::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
Wt::Dbo::collection<Artist::pointer> res = session.getDboSession().query<Artist::pointer>("SELECT a from artist a INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id INNER JOIN track t ON t.id = t_a_l.track_id")
|
|
||||||
.where("t.file_last_write > ?").bind(after)
|
auto query {createQuery<Artist::pointer>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
|
||||||
.groupBy("a.id")
|
|
||||||
|
if (after)
|
||||||
|
query.where("t.file_last_write > ?").bind(*after);
|
||||||
|
|
||||||
|
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||||
.orderBy("t.file_last_write DESC")
|
.orderBy("t.file_last_write DESC")
|
||||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||||
|
|
||||||
|
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||||
|
|
||||||
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
return std::vector<pointer>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
@@ -427,7 +512,6 @@ void
|
|||||||
Artist::setSortName(const std::string& sortName)
|
Artist::setSortName(const std::string& sortName)
|
||||||
{
|
{
|
||||||
_sortName = std::string(sortName, 0 , _maxNameLength);
|
_sortName = std::string(sortName, 0 , _maxNameLength);
|
||||||
LMS_LOG(DB, DEBUG) << "SORT NAME = '" << _sortName << "'";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|||||||
@@ -46,9 +46,25 @@ Db::Db(const std::filesystem::path& dbPath)
|
|||||||
void
|
void
|
||||||
Db::executeSql(const std::string& sql)
|
Db::executeSql(const std::string& sql)
|
||||||
{
|
{
|
||||||
auto connection {_connectionPool->getConnection()};
|
ScopedConnection connection {*_connectionPool};
|
||||||
connection->executeSql(sql);
|
connection->executeSql(sql);
|
||||||
_connectionPool->returnConnection(std::move(connection));
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool)
|
||||||
|
: _connectionPool {pool}
|
||||||
|
, _connection {_connectionPool.getConnection()}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::ScopedConnection::~ScopedConnection()
|
||||||
|
{
|
||||||
|
_connectionPool.returnConnection(std::move(_connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::Dbo::SqlConnection* Db::ScopedConnection::operator->() const
|
||||||
|
{
|
||||||
|
return _connection.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|||||||
@@ -31,6 +31,45 @@
|
|||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static
|
||||||
|
Wt::Dbo::Query<T>
|
||||||
|
createQuery(Session& session,
|
||||||
|
const std::string& queryStr,
|
||||||
|
const std::set<IdType>& clusterIds,
|
||||||
|
const std::vector<std::string>& keywords)
|
||||||
|
{
|
||||||
|
|
||||||
|
auto query {session.getDboSession().query<T>(queryStr)};
|
||||||
|
query.join("track t ON t.release_id = r.id");
|
||||||
|
|
||||||
|
for (const std::string& keyword : keywords)
|
||||||
|
query.where("r.name LIKE ?").bind("%%" + keyword + "%%");
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||||
|
" INNER JOIN track t ON t.release_id = r.id"
|
||||||
|
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||||
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
for (const IdType clusterId : clusterIds)
|
||||||
|
{
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
|
query.bind(clusterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
|
query.where(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
|
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
|
||||||
: _name {std::string(name, 0 , _maxNameLength)},
|
: _name {std::string(name, 0 , _maxNameLength)},
|
||||||
_MBID {MBID ? MBID->getAsString() : ""}
|
_MBID {MBID ? MBID->getAsString() : ""}
|
||||||
@@ -84,13 +123,13 @@ Release::getCount(Session& session)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
std::vector<Release::pointer>
|
||||||
Release::getAll(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
Release::getAll(Session& session, std::optional<Range> range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Release>()
|
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Release>()
|
||||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||||
.limit(size ? static_cast<int>(*size) : -1)
|
.limit(range ? static_cast<int>(range->limit) : -1)
|
||||||
.orderBy("name COLLATE NOCASE");
|
.orderBy("name COLLATE NOCASE");
|
||||||
|
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
return std::vector<pointer>(res.begin(), res.end());
|
||||||
@@ -123,17 +162,34 @@ Release::getAllOrderedByArtist(Session& session, std::optional<std::size_t> offs
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
std::vector<Release::pointer>
|
||||||
Release::getAllRandom(Session& session, std::optional<std::size_t> size)
|
Release::getAllRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> size)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Release>()
|
auto query {createQuery<Release::pointer>(session, "SELECT DISTINCT r from release r", clusterIds,{})};
|
||||||
.limit(size ? static_cast<int>(*size) : -1)
|
|
||||||
.orderBy("RANDOM()");
|
Wt::Dbo::collection<pointer> res = query
|
||||||
|
.orderBy("RANDOM()")
|
||||||
|
.limit(size ? static_cast<int>(*size) : -1);
|
||||||
|
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
return std::vector<pointer>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<IdType>
|
||||||
|
Release::getAllIdsRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> size)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto query {createQuery<IdType>(session, "SELECT DISTINCT r.id from release r", clusterIds,{})};
|
||||||
|
|
||||||
|
Wt::Dbo::collection<IdType> res = query
|
||||||
|
.orderBy("RANDOM()")
|
||||||
|
.limit(size ? static_cast<int>(*size) : -1);
|
||||||
|
|
||||||
|
return std::vector<IdType>(res.begin(), res.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
std::vector<Release::pointer>
|
||||||
Release::getAllOrphans(Session& session)
|
Release::getAllOrphans(Session& session)
|
||||||
{
|
{
|
||||||
@@ -145,18 +201,34 @@ Release::getAllOrphans(Session& session)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
std::vector<Release::pointer>
|
||||||
Release::getLastWritten(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> offset, std::optional<std::size_t> limit)
|
Release::getLastWritten(Session& session,
|
||||||
|
std::optional<Wt::WDateTime> after,
|
||||||
|
const std::set<IdType>& clusterIds,
|
||||||
|
std::optional<Range> range,
|
||||||
|
bool& moreResults)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<Release::pointer> res = session.getDboSession().query<Release::pointer>("SELECT r from release r INNER JOIN track t ON r.id = t.release_id")
|
auto query {createQuery<Release::pointer>(session, "SELECT r from release r", clusterIds, {})};
|
||||||
.where("t.file_last_write > ?").bind(after)
|
if (after)
|
||||||
.groupBy("r.id")
|
query.where("t.file_last_write > ?").bind(after);
|
||||||
.orderBy("t.file_last_write DESC")
|
|
||||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
|
||||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
|
||||||
|
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
Wt::Dbo::collection<Release::pointer> collection = query
|
||||||
|
.orderBy("t.file_last_write DESC")
|
||||||
|
.groupBy("r.id")
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||||
|
.limit(range ? static_cast<int>(range->limit) + 1: -1);
|
||||||
|
|
||||||
|
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||||
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
std::vector<Release::pointer>
|
||||||
@@ -173,47 +245,6 @@ Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional<std
|
|||||||
return std::vector<pointer>(res.begin(), res.end());
|
return std::vector<pointer>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
|
||||||
Wt::Dbo::Query<Release::pointer>
|
|
||||||
getQuery(Session& session,
|
|
||||||
const std::set<IdType>& clusterIds,
|
|
||||||
const std::vector<std::string>& keywords)
|
|
||||||
{
|
|
||||||
WhereClause where;
|
|
||||||
|
|
||||||
std::ostringstream oss;
|
|
||||||
oss << "SELECT DISTINCT r FROM release r";
|
|
||||||
|
|
||||||
for (auto keyword : keywords)
|
|
||||||
where.And(WhereClause("r.name LIKE ?")).bind("%%" + keyword + "%%");
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
|
||||||
{
|
|
||||||
oss << " INNER JOIN track t ON t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
|
||||||
|
|
||||||
WhereClause clusterClause;
|
|
||||||
|
|
||||||
for (auto id : clusterIds)
|
|
||||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
|
||||||
|
|
||||||
where.And(clusterClause);
|
|
||||||
}
|
|
||||||
|
|
||||||
oss << " " << where.get();
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
|
||||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
|
||||||
|
|
||||||
oss << " ORDER BY r.name COLLATE NOCASE";
|
|
||||||
|
|
||||||
Wt::Dbo::Query<Release::pointer> query = session.getDboSession().query<Release::pointer>( oss.str() );
|
|
||||||
|
|
||||||
for (const std::string& bindArg : where.getBindArgs())
|
|
||||||
query.bind(bindArg);
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
std::vector<Release::pointer>
|
||||||
Release::getByClusters(Session& session, const std::set<IdType>& clusters)
|
Release::getByClusters(Session& session, const std::set<IdType>& clusters)
|
||||||
{
|
{
|
||||||
@@ -222,26 +253,27 @@ Release::getByClusters(Session& session, const std::set<IdType>& clusters)
|
|||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
bool moreResults;
|
bool moreResults;
|
||||||
return getByFilter(session, clusters, {}, {}, {}, moreResults);
|
return getByFilter(session, clusters, {}, std::nullopt, moreResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
std::vector<Release::pointer>
|
||||||
Release::getByFilter(Session& session,
|
Release::getByFilter(Session& session,
|
||||||
const std::set<IdType>& clusterIds,
|
const std::set<IdType>& clusterIds,
|
||||||
const std::vector<std::string>& keywords,
|
const std::vector<std::string>& keywords,
|
||||||
std::optional<std::size_t> offset,
|
std::optional<Range> range,
|
||||||
std::optional<std::size_t> size,
|
|
||||||
bool& moreResults)
|
bool& moreResults)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<pointer> collection = getQuery(session, clusterIds, keywords)
|
Wt::Dbo::collection<pointer> collection = createQuery<Release::pointer>(session, "SELECT r from release r", clusterIds, keywords)
|
||||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
.groupBy("r.id")
|
||||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
.orderBy("r.name COLLATE NOCASE")
|
||||||
|
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||||
|
|
||||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||||
|
|
||||||
if (size && res.size() == static_cast<std::size_t>(*size) + 1)
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
{
|
{
|
||||||
moreResults = true;
|
moreResults = true;
|
||||||
res.pop_back();
|
res.pop_back();
|
||||||
|
|||||||
@@ -378,6 +378,7 @@ Session::prepareTables()
|
|||||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
|
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
||||||
|
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
|
||||||
|
|||||||
@@ -32,9 +32,46 @@
|
|||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static
|
||||||
|
Wt::Dbo::Query<T>
|
||||||
|
createQuery(Session& session,
|
||||||
|
const std::string& queryStr,
|
||||||
|
const std::set<IdType>& clusterIds,
|
||||||
|
const std::vector<std::string>& keywords)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto query {session.getDboSession().query<T>(queryStr)};
|
||||||
|
|
||||||
|
for (const std::string& keyword : keywords)
|
||||||
|
query.where("t.name LIKE ?").bind("%%" + keyword + "%%");
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||||
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||||
|
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
for (const IdType clusterId : clusterIds)
|
||||||
|
{
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
|
query.bind(clusterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
|
query.where(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
Track::Track(const std::filesystem::path& p)
|
Track::Track(const std::filesystem::path& p)
|
||||||
:
|
: _filePath {p.string()}
|
||||||
_filePath( p.string() )
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,17 +95,34 @@ Track::getAll(Session& session, std::optional<std::size_t> limit)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
std::vector<Track::pointer>
|
||||||
Track::getAllRandom(Session& session, std::optional<std::size_t> limit)
|
Track::getAllRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> limit)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<Track::pointer> res {session.getDboSession().find<Track>()
|
auto query {createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, {})};
|
||||||
.limit(limit ? static_cast<int>(*limit) : -1)
|
|
||||||
.orderBy("RANDOM()")};
|
|
||||||
|
|
||||||
return std::vector<Track::pointer>(std::cbegin(res), std::cend(res));
|
Wt::Dbo::collection<Track::pointer> collection = query
|
||||||
|
.orderBy("RANDOM()")
|
||||||
|
.limit(limit ? static_cast<int>(*limit) + 1: -1);
|
||||||
|
|
||||||
|
return std::vector<pointer>(collection.begin(), collection.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<Database::IdType>
|
||||||
|
Track::getAllIdsRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> limit)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto query {createQuery<IdType>(session, "SELECT t.id from track t", clusterIds, {})};
|
||||||
|
|
||||||
|
Wt::Dbo::collection<IdType> collection = query
|
||||||
|
.orderBy("RANDOM()")
|
||||||
|
.limit(limit ? static_cast<int>(*limit) + 1: -1);
|
||||||
|
|
||||||
|
return std::vector<IdType>(collection.begin(), collection.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
std::vector<IdType>
|
std::vector<IdType>
|
||||||
Track::getAllIds(Session& session)
|
Track::getAllIds(Session& session)
|
||||||
{
|
{
|
||||||
@@ -147,16 +201,30 @@ Track::getMBIDDuplicates(Session& session)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
std::vector<Track::pointer>
|
||||||
Track::getLastWritten(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> limit)
|
Track::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<Track::pointer> res = session.getDboSession().find<Track>()
|
auto query {createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, {})};
|
||||||
.where("file_last_write > ?").bind(after)
|
if (after)
|
||||||
.orderBy("file_last_write DESC")
|
query.where("t.file_last_write > ?").bind(after);
|
||||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
|
||||||
|
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
Wt::Dbo::collection<Track::pointer> collection = query
|
||||||
|
.orderBy("t.file_last_write DESC")
|
||||||
|
.groupBy("t.id")
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||||
|
.limit(range ? static_cast<int>(range->limit) + 1: -1);
|
||||||
|
|
||||||
|
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||||
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
std::vector<Track::pointer>
|
||||||
@@ -225,66 +293,21 @@ Track::hasTrackFeatures() const
|
|||||||
return (_trackFeatures.lock() != Database::TrackFeatures::pointer());
|
return (_trackFeatures.lock() != Database::TrackFeatures::pointer());
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
|
||||||
Wt::Dbo::Query< Track::pointer >
|
|
||||||
getQuery(Session& session,
|
|
||||||
const std::set<IdType>& clusterIds,
|
|
||||||
const std::vector<std::string>& keywords)
|
|
||||||
{
|
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
WhereClause where;
|
|
||||||
|
|
||||||
std::ostringstream oss;
|
|
||||||
oss << "SELECT t FROM track t";
|
|
||||||
|
|
||||||
for (auto keyword : keywords)
|
|
||||||
where.And(WhereClause("t.name LIKE ?")).bind("%%" + keyword + "%%");
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
|
||||||
{
|
|
||||||
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
|
||||||
|
|
||||||
WhereClause clusterClause;
|
|
||||||
|
|
||||||
for (auto id : clusterIds)
|
|
||||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
|
||||||
|
|
||||||
where.And(clusterClause);
|
|
||||||
}
|
|
||||||
|
|
||||||
oss << " " << where.get();
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
|
||||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
|
||||||
|
|
||||||
oss << " ORDER BY t.name COLLATE NOCASE";
|
|
||||||
|
|
||||||
Wt::Dbo::Query<Track::pointer> query = session.getDboSession().query<Track::pointer>( oss.str() );
|
|
||||||
|
|
||||||
for (const std::string& bindArg : where.getBindArgs())
|
|
||||||
query.bind(bindArg);
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
std::vector<Track::pointer>
|
||||||
Track::getByFilter(Session& session,
|
Track::getByFilter(Session& session,
|
||||||
const std::set<IdType>& clusterIds,
|
const std::set<IdType>& clusterIds,
|
||||||
const std::vector<std::string>& keywords,
|
const std::vector<std::string>& keywords,
|
||||||
std::optional<std::size_t> offset,
|
std::optional<Range> range,
|
||||||
std::optional<std::size_t> size,
|
|
||||||
bool& moreResults)
|
bool& moreResults)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<pointer> collection = getQuery(session, clusterIds, keywords)
|
Wt::Dbo::collection<pointer> collection = createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, keywords)
|
||||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||||
|
|
||||||
std::vector<pointer> res(collection.begin(), collection.end());
|
std::vector<pointer> res(collection.begin(), collection.end());
|
||||||
|
if (range && (res.size() == static_cast<std::size_t>(range->limit) + 1))
|
||||||
if (size && (res.size() == static_cast<std::size_t>(*size) + 1))
|
|
||||||
{
|
{
|
||||||
moreResults = true;
|
moreResults = true;
|
||||||
res.pop_back();
|
res.pop_back();
|
||||||
@@ -340,12 +363,10 @@ Track::getByClusters(Session& session,
|
|||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
bool moreResults;
|
bool moreResults;
|
||||||
|
|
||||||
return getByFilter(session,
|
return getByFilter(session,
|
||||||
clusters,
|
clusters,
|
||||||
{},
|
{}, // keywords
|
||||||
{},
|
std::nullopt, // range
|
||||||
{},
|
|
||||||
moreResults);
|
moreResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
|
#include "SqlQuery.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
@@ -106,6 +107,23 @@ TrackList::getById(Session& session, IdType id)
|
|||||||
return session.getDboSession().find<TrackList>().where("id = ?").bind(id);
|
return session.getDboSession().find<TrackList>().where("id = ?").bind(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
TrackList::getCount() const
|
||||||
|
{
|
||||||
|
return _entries.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::Dbo::ptr<TrackListEntry>
|
||||||
|
TrackList::getEntry(std::size_t pos) const
|
||||||
|
{
|
||||||
|
Wt::Dbo::ptr<TrackListEntry> res;
|
||||||
|
|
||||||
|
auto entries = getEntries(pos, 1);
|
||||||
|
if (!entries.empty())
|
||||||
|
res = entries.front();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Wt::Dbo::ptr<TrackListEntry>>
|
std::vector<Wt::Dbo::ptr<TrackListEntry>>
|
||||||
TrackList::getEntries(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
|
TrackList::getEntries(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
|
||||||
@@ -139,22 +157,180 @@ TrackList::getEntriesReverse(std::optional<std::size_t> offset, std::optional<st
|
|||||||
return std::vector<Wt::Dbo::ptr<TrackListEntry>>(entries.begin(), entries.end());
|
return std::vector<Wt::Dbo::ptr<TrackListEntry>>(entries.begin(), entries.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::Dbo::ptr<TrackListEntry>
|
static
|
||||||
TrackList::getEntry(std::size_t pos) const
|
Wt::Dbo::Query<Artist::pointer>
|
||||||
|
createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType)
|
||||||
{
|
{
|
||||||
Wt::Dbo::ptr<TrackListEntry> res;
|
auto query {session.query<Artist::pointer>(queryStr)};
|
||||||
|
query.join("track t ON t.id = t_a_l.track_id");
|
||||||
|
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
|
||||||
|
query.join("tracklist_entry p_e ON p_e.track_id = t.id");
|
||||||
|
query.join("tracklist p ON p.id = p_e.tracklist_id");
|
||||||
|
|
||||||
auto entries = getEntries(pos, 1);
|
query.where("p.id = ?").bind(tracklistId);
|
||||||
if (!entries.empty())
|
|
||||||
res = entries.front();
|
if (linkType)
|
||||||
|
query.where("t_a_l.type = ?").bind(*linkType);
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||||
|
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
||||||
|
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||||
|
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||||
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
for (auto id : clusterIds)
|
||||||
|
{
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
|
query.bind(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
|
query.where(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
Wt::Dbo::Query<Release::pointer>
|
||||||
|
createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds)
|
||||||
|
{
|
||||||
|
auto query {session.query<Release::pointer>(queryStr)};
|
||||||
|
query.join("track t ON t.release_id = r.id");
|
||||||
|
query.join("tracklist_entry p_e ON p_e.track_id = t.id");
|
||||||
|
query.join("tracklist p ON p.id = p_e.tracklist_id");
|
||||||
|
|
||||||
|
query.where("p.id = ?").bind(tracklistId);
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||||
|
" INNER JOIN track t ON t.release_id = r.id"
|
||||||
|
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||||
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
for (auto id : clusterIds)
|
||||||
|
{
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
|
query.bind(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
|
query.where(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
Wt::Dbo::Query<Track::pointer>
|
||||||
|
createTracksQuery(Wt::Dbo::Session& session, IdType tracklistId, const std::set<IdType>& clusterIds)
|
||||||
|
{
|
||||||
|
auto query {session.query<Track::pointer>("SELECT t from track t INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")};
|
||||||
|
|
||||||
|
query.where("p.id = ?").bind(tracklistId);
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||||
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||||
|
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
for (auto id : clusterIds)
|
||||||
|
{
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||||
|
query.bind(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
|
query.where(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Artist::pointer>
|
||||||
|
TrackList::getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const
|
||||||
|
{
|
||||||
|
assert(session());
|
||||||
|
assert(IdIsValid(self()->id()));
|
||||||
|
|
||||||
|
Wt::Dbo::collection<Artist::pointer> collection = createArtistsQuery(*session(), "SELECT DISTINCT a from artist a", self()->id(), clusterIds, linkType)
|
||||||
|
.orderBy("p_e.id DESC")
|
||||||
|
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||||
|
|
||||||
|
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
|
||||||
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t
|
std::vector<Release::pointer>
|
||||||
TrackList::getCount() const
|
TrackList::getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||||
{
|
{
|
||||||
return _entries.size();
|
assert(session());
|
||||||
|
assert(IdIsValid(self()->id()));
|
||||||
|
|
||||||
|
Wt::Dbo::collection<Release::pointer> collection = createReleasesQuery(*session(), "SELECT DISTINCT r from release r", self()->id(), clusterIds)
|
||||||
|
.orderBy("p_e.id DESC")
|
||||||
|
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||||
|
|
||||||
|
auto res {std::vector<Release::pointer>(collection.begin(), collection.end())};
|
||||||
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Track::pointer>
|
||||||
|
TrackList::getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||||
|
{
|
||||||
|
assert(session());
|
||||||
|
assert(IdIsValid(self()->id()));
|
||||||
|
|
||||||
|
Wt::Dbo::collection<Track::pointer> collection = createTracksQuery(*session(), self()->id(), clusterIds)
|
||||||
|
.orderBy("p_e.id DESC")
|
||||||
|
.groupBy("t.id")
|
||||||
|
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||||
|
|
||||||
|
auto res {std::vector<Track::pointer>(collection.begin(), collection.end())};
|
||||||
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Wt::Dbo::ptr<Cluster>>
|
std::vector<Wt::Dbo::ptr<Cluster>>
|
||||||
@@ -235,48 +411,85 @@ TrackList::getDuration() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Artist::pointer>
|
std::vector<Artist::pointer>
|
||||||
TrackList::getTopArtists(std::size_t limit) const
|
TrackList::getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
assert(IdIsValid(self()->id()));
|
assert(IdIsValid(self()->id()));
|
||||||
|
|
||||||
Wt::Dbo::collection<Artist::pointer> res = session()->query<Artist::pointer>("SELECT a from artist a INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
|
auto query {createArtistsQuery(*session(), "SELECT a from artist a", self()->id(), clusterIds, linkType)};
|
||||||
.where("p.id = ?").bind(self()->id())
|
|
||||||
.groupBy("a.id")
|
|
||||||
.orderBy("COUNT(a.id) DESC")
|
|
||||||
.limit(static_cast<int>(limit));
|
|
||||||
|
|
||||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||||
|
.orderBy("COUNT(a.id) DESC")
|
||||||
|
.groupBy("a.id")
|
||||||
|
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||||
|
|
||||||
|
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
|
||||||
|
|
||||||
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
std::vector<Release::pointer>
|
||||||
TrackList::getTopReleases(std::size_t limit) const
|
TrackList::getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
assert(IdIsValid(self()->id()));
|
assert(IdIsValid(self()->id()));
|
||||||
|
|
||||||
Wt::Dbo::collection<Release::pointer> res = session()->query<Release::pointer>("SELECT r from release r INNER JOIN track t ON t.release_id = r.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
|
auto query {createReleasesQuery(*session(), "SELECT r from release r", self()->id(), clusterIds)};
|
||||||
.where("p.id = ?").bind(self()->id())
|
|
||||||
.groupBy("r.id")
|
|
||||||
.orderBy("COUNT(r.id) DESC")
|
|
||||||
.limit(static_cast<int>(limit));
|
|
||||||
|
|
||||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
Wt::Dbo::collection<Release::pointer> collection = query
|
||||||
|
.orderBy("COUNT(r.id) DESC")
|
||||||
|
.groupBy("r.id")
|
||||||
|
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||||
|
|
||||||
|
auto res {std::vector<Release::pointer>(collection.begin(), collection.end())};
|
||||||
|
|
||||||
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
std::vector<Track::pointer>
|
||||||
TrackList::getTopTracks(std::size_t limit) const
|
TrackList::getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
assert(IdIsValid(self()->id()));
|
assert(IdIsValid(self()->id()));
|
||||||
|
|
||||||
Wt::Dbo::collection<Track::pointer> res = session()->query<Track::pointer>("SELECT t from track t INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
|
auto query {createTracksQuery(*session(), self()->id(), clusterIds)};
|
||||||
.where("p.id = ?").bind(self()->id())
|
|
||||||
.groupBy("t.id")
|
|
||||||
.orderBy("COUNT(t.id) DESC")
|
|
||||||
.limit(static_cast<int>(limit));
|
|
||||||
|
|
||||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
Wt::Dbo::collection<Track::pointer> collection = query
|
||||||
|
.orderBy("COUNT(t.id) DESC")
|
||||||
|
.groupBy("t.id")
|
||||||
|
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||||
|
|
||||||
|
auto res {std::vector<Track::pointer>(collection.begin(), collection.end())};
|
||||||
|
|
||||||
|
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackListEntry::TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist)
|
TrackListEntry::TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist)
|
||||||
|
|||||||
@@ -70,14 +70,21 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
|||||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
|
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
|
||||||
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
||||||
SortMethod sortMethod,
|
SortMethod sortMethod,
|
||||||
std::optional<std::size_t> offset,
|
std::optional<Range> range,
|
||||||
std::optional<std::size_t> size,
|
|
||||||
bool& moreExpected);
|
bool& moreExpected);
|
||||||
|
|
||||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
static std::vector<pointer> getAll(Session& session);
|
||||||
|
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod);
|
||||||
|
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults);
|
||||||
static std::vector<IdType> getAllIds(Session& session);
|
static std::vector<IdType> getAllIds(Session& session);
|
||||||
|
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<std::size_t> size = {});
|
||||||
static std::vector<pointer> getAllOrphans(Session& session); // No track related
|
static std::vector<pointer> getAllOrphans(Session& session); // No track related
|
||||||
static std::vector<pointer> getLastWritten(Session& session, Wt::WDateTime after, std::optional<std::size_t> size = {});
|
static std::vector<pointer> getLastWritten(Session& session,
|
||||||
|
std::optional<Wt::WDateTime> after,
|
||||||
|
const std::set<IdType>& clusters,
|
||||||
|
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
||||||
|
std::optional<Range>,
|
||||||
|
bool& moreResults);
|
||||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
|
|||||||
@@ -39,6 +39,24 @@ class Db
|
|||||||
std::shared_mutex& getMutex() { return _sharedMutex; }
|
std::shared_mutex& getMutex() { return _sharedMutex; }
|
||||||
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
|
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
|
||||||
|
|
||||||
|
class ScopedConnection
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ScopedConnection(Wt::Dbo::SqlConnectionPool& pool);
|
||||||
|
~ScopedConnection();
|
||||||
|
|
||||||
|
ScopedConnection(const ScopedConnection& ) = delete;
|
||||||
|
ScopedConnection(ScopedConnection&& ) = delete;
|
||||||
|
ScopedConnection& operator=(const ScopedConnection& ) = delete;
|
||||||
|
ScopedConnection& operator=(ScopedConnection&& ) = delete;
|
||||||
|
|
||||||
|
Wt::Dbo::SqlConnection* operator->() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Wt::Dbo::SqlConnectionPool& _connectionPool;
|
||||||
|
std::unique_ptr<Wt::Dbo::SqlConnection> _connection;
|
||||||
|
};
|
||||||
|
|
||||||
class ScopedNoForeignKeys
|
class ScopedNoForeignKeys
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -51,9 +69,13 @@ class Db
|
|||||||
_db.executeSql("PRAGMA foreign_keys=ON");
|
_db.executeSql("PRAGMA foreign_keys=ON");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScopedNoForeignKeys(const ScopedNoForeignKeys&) = delete;
|
||||||
|
ScopedNoForeignKeys(ScopedNoForeignKeys&&) = delete;
|
||||||
|
ScopedNoForeignKeys& operator=(const ScopedNoForeignKeys&) = delete;
|
||||||
|
ScopedNoForeignKeys& operator=(ScopedNoForeignKeys&&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Db& _db;
|
Db& _db;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void executeSql(const std::string& sql);
|
void executeSql(const std::string& sql);
|
||||||
|
|||||||
@@ -52,19 +52,19 @@ class Release : public Wt::Dbo::Dbo<Release>
|
|||||||
static std::vector<pointer> getByName(Session& session, const std::string& name);
|
static std::vector<pointer> getByName(Session& session, const std::string& name);
|
||||||
static pointer getById(Session& session, IdType id);
|
static pointer getById(Session& session, IdType id);
|
||||||
static std::vector<pointer> getAllOrphans(Session& session); // no track related
|
static std::vector<pointer> getAllOrphans(Session& session); // no track related
|
||||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
static std::vector<pointer> getAll(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
static std::vector<IdType> getAllIds(Session& session);
|
static std::vector<IdType> getAllIds(Session& session);
|
||||||
static std::vector<pointer> getAllOrderedByArtist(Session& session, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
static std::vector<pointer> getAllOrderedByArtist(Session& session, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||||
static std::vector<pointer> getAllRandom(Session& session, std::optional<std::size_t> size = {});
|
static std::vector<pointer> getAllRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
|
||||||
static std::vector<pointer> getLastWritten(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
|
||||||
|
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||||
static std::vector<pointer> getByYear(Session& session, int yearFrom, int yearTo, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
static std::vector<pointer> getByYear(Session& session, int yearFrom, int yearTo, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||||
|
|
||||||
static std::vector<pointer> getByClusters(Session& session, const std::set<IdType>& clusters);
|
static std::vector<pointer> getByClusters(Session& session, const std::set<IdType>& clusters);
|
||||||
static std::vector<pointer> getByFilter(Session& session,
|
static std::vector<pointer> getByFilter(Session& session,
|
||||||
const std::set<IdType>& clusters, // if non empty, at least one release that belongs to these clusters
|
const std::set<IdType>& clusters, // if non empty, at least one release that belongs to these clusters
|
||||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords
|
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords
|
||||||
std::optional<std::size_t> offset,
|
std::optional<Range> range,
|
||||||
std::optional<std::size_t> size,
|
|
||||||
bool& moreExpected);
|
bool& moreExpected);
|
||||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||||
|
|
||||||
|
|||||||
@@ -68,16 +68,16 @@ class Track : public Wt::Dbo::Dbo<Track>
|
|||||||
static std::vector<pointer> getByFilter(Session& session,
|
static std::vector<pointer> getByFilter(Session& session,
|
||||||
const std::set<IdType>& clusters, // if non empty, tracks that belong to these clusters
|
const std::set<IdType>& clusters, // if non empty, tracks that belong to these clusters
|
||||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords
|
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords
|
||||||
std::optional<std::size_t> offset,
|
std::optional<Range> range,
|
||||||
std::optional<std::size_t> size,
|
|
||||||
bool& moreExpected);
|
bool& moreExpected);
|
||||||
|
|
||||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = {});
|
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = std::nullopt);
|
||||||
static std::vector<pointer> getAllRandom(Session& session, std::optional<std::size_t> limit = {});
|
static std::vector<pointer> getAllRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
||||||
|
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
||||||
static std::vector<IdType> getAllIds(Session& session);
|
static std::vector<IdType> getAllIds(Session& session);
|
||||||
static std::vector<std::pair<IdType, std::filesystem::path>> getAllPaths(Session& session, std::optional<std::size_t> offset = std::nullopt, std::optional<std::size_t> size = std::nullopt);
|
static std::vector<std::pair<IdType, std::filesystem::path>> getAllPaths(Session& session, std::optional<std::size_t> offset = std::nullopt, std::optional<std::size_t> size = std::nullopt);
|
||||||
static std::vector<pointer> getMBIDDuplicates(Session& session);
|
static std::vector<pointer> getMBIDDuplicates(Session& session);
|
||||||
static std::vector<pointer> getLastWritten(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> size = 1);
|
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||||
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
|
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
|
||||||
static std::vector<IdType> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {});
|
static std::vector<IdType> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {});
|
||||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||||
|
|||||||
@@ -21,9 +21,11 @@
|
|||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
|
#include "TrackArtistLink.hpp"
|
||||||
#include "Types.hpp"
|
#include "Types.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
@@ -51,9 +53,9 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
|
|||||||
TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
|
TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
|
||||||
|
|
||||||
// Stats utility
|
// Stats utility
|
||||||
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(std::size_t limit = 1) const;
|
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||||
std::vector<Wt::Dbo::ptr<Release>> getTopReleases(std::size_t limit = 1) const;
|
std::vector<Wt::Dbo::ptr<Release>> getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||||
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(std::size_t limit = 1) const;
|
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||||
|
|
||||||
// Search utility
|
// Search utility
|
||||||
static pointer get(Session& session, const std::string& name, Type type, Wt::Dbo::ptr<User> user);
|
static pointer get(Session& session, const std::string& name, Type type, Wt::Dbo::ptr<User> user);
|
||||||
@@ -82,6 +84,10 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
|
|||||||
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntries(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntries(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||||
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntriesReverse(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntriesReverse(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||||
|
|
||||||
|
std::vector<Wt::Dbo::ptr<Artist>> getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||||
|
std::vector<Wt::Dbo::ptr<Release>> getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||||
|
std::vector<Wt::Dbo::ptr<Track>> getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||||
|
|
||||||
std::vector<IdType> getTrackIds() const;
|
std::vector<IdType> getTrackIds() const;
|
||||||
|
|
||||||
std::chrono::milliseconds getDuration() const;
|
std::chrono::milliseconds getDuration() const;
|
||||||
|
|||||||
@@ -21,12 +21,19 @@
|
|||||||
|
|
||||||
#include <Wt/Dbo/ptr.h>
|
#include <Wt/Dbo/ptr.h>
|
||||||
|
|
||||||
namespace Database {
|
namespace Database
|
||||||
|
{
|
||||||
using IdType = Wt::Dbo::dbo_default_traits::IdType;
|
using IdType = Wt::Dbo::dbo_default_traits::IdType;
|
||||||
|
|
||||||
static inline bool IdIsValid(IdType id)
|
static inline bool IdIsValid(IdType id)
|
||||||
{
|
{
|
||||||
return id != Wt::Dbo::dbo_default_traits::invalidId();
|
return id != Wt::Dbo::dbo_default_traits::invalidId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Range
|
||||||
|
{
|
||||||
|
std::size_t offset {};
|
||||||
|
std::size_t limit {};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ handleGetRandomSongsRequest(RequestContext& context)
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw UserNotAuthorizedError {};
|
throw UserNotAuthorizedError {};
|
||||||
|
|
||||||
auto tracks {Track::getAllRandom(context.dbSession, size)};
|
auto tracks {Track::getAllRandom(context.dbSession, {}, size)};
|
||||||
|
|
||||||
Response response {Response::createOkResponse()};
|
Response response {Response::createOkResponse()};
|
||||||
|
|
||||||
@@ -710,16 +710,17 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
|||||||
if (type == "random")
|
if (type == "random")
|
||||||
{
|
{
|
||||||
// Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums
|
// Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums
|
||||||
releases = Release::getAllRandom(context.dbSession, size);
|
releases = Release::getAllRandom(context.dbSession, {}, size);
|
||||||
}
|
}
|
||||||
else if (type == "newest")
|
else if (type == "newest")
|
||||||
{
|
{
|
||||||
auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6)};
|
auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6)};
|
||||||
releases = Release::getLastWritten(context.dbSession, after, offset, size);
|
bool moreResults {};
|
||||||
|
releases = Release::getLastWritten(context.dbSession, after, {}, Range {offset, size}, moreResults);
|
||||||
}
|
}
|
||||||
else if (type == "alphabeticalByName")
|
else if (type == "alphabeticalByName")
|
||||||
{
|
{
|
||||||
releases = Release::getAll(context.dbSession, offset, size);
|
releases = Release::getAll(context.dbSession, Range {offset, size});
|
||||||
}
|
}
|
||||||
else if (type == "alphabeticalByArtist")
|
else if (type == "alphabeticalByArtist")
|
||||||
{
|
{
|
||||||
@@ -748,7 +749,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
|||||||
if (cluster)
|
if (cluster)
|
||||||
{
|
{
|
||||||
bool more;
|
bool more;
|
||||||
releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more);
|
releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, Range {offset, size}, more);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -935,7 +936,7 @@ handleGetArtistsRequest(RequestContext& context)
|
|||||||
{},
|
{},
|
||||||
linkType,
|
linkType,
|
||||||
Artist::SortMethod::BySortName,
|
Artist::SortMethod::BySortName,
|
||||||
{}, {}, more)};
|
std::nullopt, more)};
|
||||||
for (const Artist::pointer& artist : artists)
|
for (const Artist::pointer& artist : artists)
|
||||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */));
|
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */));
|
||||||
|
|
||||||
@@ -966,7 +967,8 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
|||||||
{
|
{
|
||||||
directoryNode.setAttribute("name", "Music");
|
directoryNode.setAttribute("name", "Music");
|
||||||
|
|
||||||
auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName)};
|
bool moreResults{};
|
||||||
|
auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName, std::nullopt, moreResults)};
|
||||||
for (const Artist::pointer& artist : artists)
|
for (const Artist::pointer& artist : artists)
|
||||||
directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */));
|
directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */));
|
||||||
|
|
||||||
@@ -1078,7 +1080,7 @@ handleGetIndexesRequest(RequestContext& context)
|
|||||||
{},
|
{},
|
||||||
linkType,
|
linkType,
|
||||||
Artist::SortMethod::BySortName,
|
Artist::SortMethod::BySortName,
|
||||||
{}, {}, more)};
|
std::nullopt, more)};
|
||||||
for (const Artist::pointer& artist : artists)
|
for (const Artist::pointer& artist : artists)
|
||||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */));
|
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */));
|
||||||
|
|
||||||
@@ -1296,7 +1298,7 @@ handleGetSongsByGenreRequest(RequestContext& context)
|
|||||||
Response::Node& songsByGenreNode {response.createNode("songsByGenre")};
|
Response::Node& songsByGenreNode {response.createNode("songsByGenre")};
|
||||||
|
|
||||||
bool more;
|
bool more;
|
||||||
auto tracks {Track::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more)};
|
auto tracks {Track::getByFilter(context.dbSession, {cluster.id()}, {}, Range {offset, size}, more)};
|
||||||
for (const Track::pointer& track : tracks)
|
for (const Track::pointer& track : tracks)
|
||||||
songsByGenreNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
songsByGenreNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||||
|
|
||||||
@@ -1367,19 +1369,19 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
|
|||||||
|
|
||||||
bool more;
|
bool more;
|
||||||
{
|
{
|
||||||
auto artists {Artist::getByFilter(context.dbSession, {}, keywords, std::nullopt, Artist::SortMethod::BySortName, artistOffset, artistCount, more)};
|
auto artists {Artist::getByFilter(context.dbSession, {}, keywords, std::nullopt, Artist::SortMethod::BySortName, Range {artistOffset, artistCount}, more)};
|
||||||
for (const Artist::pointer& artist : artists)
|
for (const Artist::pointer& artist : artists)
|
||||||
searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto releases {Release::getByFilter(context.dbSession, {}, keywords, albumOffset, albumCount, more)};
|
auto releases {Release::getByFilter(context.dbSession, {}, keywords, Range {albumOffset, albumCount}, more)};
|
||||||
for (const Release::pointer& release : releases)
|
for (const Release::pointer& release : releases)
|
||||||
searchResult2Node.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3));
|
searchResult2Node.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto tracks {Track::getByFilter(context.dbSession, {}, keywords, songOffset, songCount, more)};
|
auto tracks {Track::getByFilter(context.dbSession, {}, keywords, Range {songOffset, songCount}, more)};
|
||||||
for (const Track::pointer& track : tracks)
|
for (const Track::pointer& track : tracks)
|
||||||
searchResult2Node.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
searchResult2Node.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,15 @@ namespace Random {
|
|||||||
RandGenerator& getRandGenerator()
|
RandGenerator& getRandGenerator()
|
||||||
{
|
{
|
||||||
static thread_local std::random_device rd;
|
static thread_local std::random_device rd;
|
||||||
static thread_local std::mt19937 randGenerator(rd());
|
static thread_local RandGenerator randGenerator(rd());
|
||||||
|
|
||||||
return randGenerator;
|
return randGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RandGenerator createSeededGenerator(std::size_t seed)
|
||||||
|
{
|
||||||
|
return RandGenerator {seed};
|
||||||
|
}
|
||||||
|
|
||||||
} // Random
|
} // Random
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ namespace Random {
|
|||||||
using RandGenerator = std::mt19937;
|
using RandGenerator = std::mt19937;
|
||||||
RandGenerator& getRandGenerator();
|
RandGenerator& getRandGenerator();
|
||||||
|
|
||||||
|
RandGenerator createSeededGenerator(std::size_t seed);
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
T
|
T
|
||||||
getRandom(T min, T max)
|
getRandom(T min, T max)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ add_executable(lms
|
|||||||
ui/LmsTheme.cpp
|
ui/LmsTheme.cpp
|
||||||
ui/MediaPlayer.cpp
|
ui/MediaPlayer.cpp
|
||||||
ui/PlayQueueView.cpp
|
ui/PlayQueueView.cpp
|
||||||
ui/PlayHistoryView.cpp
|
|
||||||
ui/SettingsView.cpp
|
ui/SettingsView.cpp
|
||||||
ui/TrackStringUtils.cpp
|
ui/TrackStringUtils.cpp
|
||||||
ui/admin/DatabaseSettingsView.cpp
|
ui/admin/DatabaseSettingsView.cpp
|
||||||
@@ -16,19 +15,15 @@ add_executable(lms
|
|||||||
ui/admin/UserView.cpp
|
ui/admin/UserView.cpp
|
||||||
ui/admin/UsersView.cpp
|
ui/admin/UsersView.cpp
|
||||||
ui/common/Validators.cpp
|
ui/common/Validators.cpp
|
||||||
ui/explore/ArtistInfoView.cpp
|
ui/explore/ArtistListHelpers.cpp
|
||||||
ui/explore/ArtistLink.cpp
|
|
||||||
ui/explore/ArtistsInfoView.cpp
|
|
||||||
ui/explore/ArtistView.cpp
|
ui/explore/ArtistView.cpp
|
||||||
ui/explore/ArtistsView.cpp
|
ui/explore/ArtistsView.cpp
|
||||||
ui/explore/Explore.cpp
|
ui/explore/Explore.cpp
|
||||||
ui/explore/Filters.cpp
|
ui/explore/Filters.cpp
|
||||||
ui/explore/ReleaseInfoView.cpp
|
ui/explore/ReleaseListHelpers.cpp
|
||||||
ui/explore/ReleaseLink.cpp
|
|
||||||
ui/explore/ReleasesInfoView.cpp
|
|
||||||
ui/explore/ReleasesView.cpp
|
ui/explore/ReleasesView.cpp
|
||||||
ui/explore/ReleaseView.cpp
|
ui/explore/ReleaseView.cpp
|
||||||
ui/explore/TracksInfoView.cpp
|
ui/explore/SearchView.cpp
|
||||||
ui/explore/TracksView.cpp
|
ui/explore/TracksView.cpp
|
||||||
ui/resource/AudioFileResource.cpp
|
ui/resource/AudioFileResource.cpp
|
||||||
ui/resource/AudioTranscodeResource.cpp
|
ui/resource/AudioTranscodeResource.cpp
|
||||||
|
|||||||
+14
-1
@@ -77,7 +77,20 @@ std::vector<std::string> generateWtConfig(std::string execPath)
|
|||||||
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
||||||
pt.put("server.application-settings.log-config", ServiceProvider<IConfig>::get()->getString("log-config", "* -debug -info:WebRequest"));
|
pt.put("server.application-settings.log-config", ServiceProvider<IConfig>::get()->getString("log-config", "* -debug -info:WebRequest"));
|
||||||
pt.put("server.application-settings.behind-reverse-proxy", ServiceProvider<IConfig>::get()->getBool("behind-reverse-proxy", false));
|
pt.put("server.application-settings.behind-reverse-proxy", ServiceProvider<IConfig>::get()->getBool("behind-reverse-proxy", false));
|
||||||
pt.put("server.application-settings.progressive-bootstrap", true);
|
|
||||||
|
{
|
||||||
|
boost::property_tree::ptree viewport;
|
||||||
|
viewport.put("<xmlattr>.name", "viewport");
|
||||||
|
viewport.put("<xmlattr>.content", "width=device-width, initial-scale=1, user-scalable=no");
|
||||||
|
pt.add_child("server.application-settings.head-matter.meta", viewport);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
boost::property_tree::ptree themeColor;
|
||||||
|
themeColor.put("<xmlattr>.name", "theme-color");
|
||||||
|
themeColor.put("<xmlattr>.content", "#303030");
|
||||||
|
pt.add_child("server.application-settings.head-matter.meta", themeColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
std::ofstream oss(wtConfigPath.string().c_str(), std::ios::out);
|
std::ofstream oss(wtConfigPath.string().c_str(), std::ios::out);
|
||||||
boost::property_tree::xml_parser::write_xml(oss, pt);
|
boost::property_tree::xml_parser::write_xml(oss, pt);
|
||||||
|
|||||||
@@ -21,9 +21,7 @@
|
|||||||
|
|
||||||
#include <Wt/WAnchor.h>
|
#include <Wt/WAnchor.h>
|
||||||
#include <Wt/WEnvironment.h>
|
#include <Wt/WEnvironment.h>
|
||||||
#include <Wt/WMenu.h>
|
#include <Wt/WLineEdit.h>
|
||||||
#include <Wt/WNavigationBar.h>
|
|
||||||
#include <Wt/WPopupMenu.h>
|
|
||||||
#include <Wt/WServer.h>
|
#include <Wt/WServer.h>
|
||||||
#include <Wt/WStackedWidget.h>
|
#include <Wt/WStackedWidget.h>
|
||||||
#include <Wt/WText.h>
|
#include <Wt/WText.h>
|
||||||
@@ -35,6 +33,8 @@
|
|||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
#include "explore/Explore.hpp"
|
#include "explore/Explore.hpp"
|
||||||
|
#include "explore/Filters.hpp"
|
||||||
|
#include "explore/SearchView.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
@@ -50,13 +50,14 @@
|
|||||||
#include "LmsApplicationException.hpp"
|
#include "LmsApplicationException.hpp"
|
||||||
#include "LmsTheme.hpp"
|
#include "LmsTheme.hpp"
|
||||||
#include "MediaPlayer.hpp"
|
#include "MediaPlayer.hpp"
|
||||||
#include "PlayHistoryView.hpp"
|
|
||||||
#include "PlayQueueView.hpp"
|
#include "PlayQueueView.hpp"
|
||||||
#include "SettingsView.hpp"
|
#include "SettingsView.hpp"
|
||||||
|
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
|
static constexpr const char* defaultPath {"/releases"};
|
||||||
|
|
||||||
std::unique_ptr<Wt::WApplication>
|
std::unique_ptr<Wt::WApplication>
|
||||||
LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups)
|
LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups)
|
||||||
{
|
{
|
||||||
@@ -115,7 +116,6 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
|||||||
_dbSession {db},
|
_dbSession {db},
|
||||||
_appGroups {appGroups}
|
_appGroups {appGroups}
|
||||||
{
|
{
|
||||||
addMetaHeader(Wt::MetaHeaderType::Meta, "viewport", "width=device-width, user-scalable=no");
|
|
||||||
|
|
||||||
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
||||||
|
|
||||||
@@ -125,31 +125,27 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
|||||||
messageResourceBundle().use(appRoot() + "admin-user");
|
messageResourceBundle().use(appRoot() + "admin-user");
|
||||||
messageResourceBundle().use(appRoot() + "admin-users");
|
messageResourceBundle().use(appRoot() + "admin-users");
|
||||||
messageResourceBundle().use(appRoot() + "artist");
|
messageResourceBundle().use(appRoot() + "artist");
|
||||||
messageResourceBundle().use(appRoot() + "artistinfo");
|
|
||||||
messageResourceBundle().use(appRoot() + "artistlink");
|
|
||||||
messageResourceBundle().use(appRoot() + "artists");
|
messageResourceBundle().use(appRoot() + "artists");
|
||||||
messageResourceBundle().use(appRoot() + "artistsinfo");
|
|
||||||
messageResourceBundle().use(appRoot() + "error");
|
messageResourceBundle().use(appRoot() + "error");
|
||||||
messageResourceBundle().use(appRoot() + "explore");
|
messageResourceBundle().use(appRoot() + "explore");
|
||||||
messageResourceBundle().use(appRoot() + "login");
|
messageResourceBundle().use(appRoot() + "login");
|
||||||
messageResourceBundle().use(appRoot() + "mediaplayer");
|
messageResourceBundle().use(appRoot() + "mediaplayer");
|
||||||
messageResourceBundle().use(appRoot() + "messages");
|
messageResourceBundle().use(appRoot() + "messages");
|
||||||
messageResourceBundle().use(appRoot() + "playqueue");
|
messageResourceBundle().use(appRoot() + "playqueue");
|
||||||
messageResourceBundle().use(appRoot() + "playhistory");
|
|
||||||
messageResourceBundle().use(appRoot() + "release");
|
messageResourceBundle().use(appRoot() + "release");
|
||||||
messageResourceBundle().use(appRoot() + "releaseinfo");
|
|
||||||
messageResourceBundle().use(appRoot() + "releaselink");
|
|
||||||
messageResourceBundle().use(appRoot() + "releases");
|
messageResourceBundle().use(appRoot() + "releases");
|
||||||
messageResourceBundle().use(appRoot() + "releasesinfo");
|
messageResourceBundle().use(appRoot() + "search");
|
||||||
messageResourceBundle().use(appRoot() + "settings");
|
messageResourceBundle().use(appRoot() + "settings");
|
||||||
messageResourceBundle().use(appRoot() + "templates");
|
messageResourceBundle().use(appRoot() + "templates");
|
||||||
messageResourceBundle().use(appRoot() + "tracks");
|
messageResourceBundle().use(appRoot() + "tracks");
|
||||||
messageResourceBundle().use(appRoot() + "tracksinfo");
|
|
||||||
|
|
||||||
// Require js here to avoid async problems
|
// Require js here to avoid async problems
|
||||||
requireJQuery("js/jquery-1.10.2.min.js");
|
requireJQuery("js/jquery-1.10.2.min.js");
|
||||||
require("js/mediaplayer.js");
|
require("js/mediaplayer.js");
|
||||||
require("js/bootstrap-notify.js");
|
require("js/bootstrap-notify.js");
|
||||||
|
require("js/collapse.js");
|
||||||
|
require("js/dropdown.js");
|
||||||
|
require("js/transition.js");
|
||||||
|
|
||||||
setTitle("LMS");
|
setTitle("LMS");
|
||||||
|
|
||||||
@@ -245,7 +241,7 @@ LmsApplication::finalize()
|
|||||||
Wt::WLink
|
Wt::WLink
|
||||||
LmsApplication::createArtistLink(Database::Artist::pointer artist)
|
LmsApplication::createArtistLink(Database::Artist::pointer artist)
|
||||||
{
|
{
|
||||||
return Wt::WLink(Wt::LinkType::InternalPath, "/artist/" + std::to_string(artist.id()));
|
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + std::to_string(artist.id())};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WAnchor>
|
std::unique_ptr<Wt::WAnchor>
|
||||||
@@ -265,7 +261,7 @@ LmsApplication::createArtistAnchor(Database::Artist::pointer artist, bool addTex
|
|||||||
Wt::WLink
|
Wt::WLink
|
||||||
LmsApplication::createReleaseLink(Database::Release::pointer release)
|
LmsApplication::createReleaseLink(Database::Release::pointer release)
|
||||||
{
|
{
|
||||||
return Wt::WLink(Wt::LinkType::InternalPath, "/release/" + std::to_string(release.id()));
|
return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + std::to_string(release.id())};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WAnchor>
|
std::unique_ptr<Wt::WAnchor>
|
||||||
@@ -289,10 +285,10 @@ LmsApplication::createCluster(Database::Cluster::pointer cluster, bool canDelete
|
|||||||
{
|
{
|
||||||
switch (cluster->getType().id() % 6)
|
switch (cluster->getType().id() % 6)
|
||||||
{
|
{
|
||||||
case 0: return "label-primary";
|
case 0: return "label-info";
|
||||||
case 1: return "label-default";
|
case 1: return "label-warning";
|
||||||
case 2: return "label-info";
|
case 2: return "label-primary";
|
||||||
case 3: return "label-warning";
|
case 3: return "label-default";
|
||||||
case 4: return "label-success";
|
case 4: return "label-success";
|
||||||
case 5: return "label-danger";
|
case 5: return "label-danger";
|
||||||
}
|
}
|
||||||
@@ -316,7 +312,7 @@ LmsApplication::handleException(LmsApplicationException& e)
|
|||||||
Wt::WTemplate* t {root()->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Error.template"))};
|
Wt::WTemplate* t {root()->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Error.template"))};
|
||||||
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
t->bindString("error", e.what());
|
t->bindString("error", e.what(), Wt::TextFormat::Plain);
|
||||||
Wt::WPushButton* btn {t->bindNew<Wt::WPushButton>("btn-go-home", Wt::WString::tr("Lms.Error.go-home"))};
|
Wt::WPushButton* btn {t->bindNew<Wt::WPushButton>("btn-go-home", Wt::WString::tr("Lms.Error.go-home"))};
|
||||||
btn->clicked().connect([this]()
|
btn->clicked().connect([this]()
|
||||||
{
|
{
|
||||||
@@ -337,7 +333,7 @@ enum IdxRoot
|
|||||||
{
|
{
|
||||||
IdxExplore = 0,
|
IdxExplore = 0,
|
||||||
IdxPlayQueue,
|
IdxPlayQueue,
|
||||||
IdxPlayHistory,
|
IdxSearch,
|
||||||
IdxSettings,
|
IdxSettings,
|
||||||
IdxAdminDatabase,
|
IdxAdminDatabase,
|
||||||
IdxAdminUsers,
|
IdxAdminUsers,
|
||||||
@@ -360,7 +356,7 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
|
|||||||
{ "/release", IdxExplore, false },
|
{ "/release", IdxExplore, false },
|
||||||
{ "/tracks", IdxExplore, false },
|
{ "/tracks", IdxExplore, false },
|
||||||
{ "/playqueue", IdxPlayQueue, false },
|
{ "/playqueue", IdxPlayQueue, false },
|
||||||
{ "/playhistory", IdxPlayHistory, false },
|
{ "/search", IdxSearch, false },
|
||||||
{ "/settings", IdxSettings, false },
|
{ "/settings", IdxSettings, false },
|
||||||
{ "/admin/database", IdxAdminDatabase, true },
|
{ "/admin/database", IdxAdminDatabase, true },
|
||||||
{ "/admin/users", IdxAdminUsers, true },
|
{ "/admin/users", IdxAdminUsers, true },
|
||||||
@@ -381,7 +377,7 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wApp->setInternalPath("/artists", true);
|
wApp->setInternalPath(defaultPath, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
LmsApplicationGroup&
|
LmsApplicationGroup&
|
||||||
@@ -436,69 +432,35 @@ LmsApplication::createHome()
|
|||||||
|
|
||||||
Wt::WTemplate* main {root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.template")))};
|
Wt::WTemplate* main {root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.template")))};
|
||||||
|
|
||||||
// Navbar
|
main->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
Wt::WNavigationBar* navbar {main->bindNew<Wt::WNavigationBar>("navbar-top")};
|
|
||||||
navbar->setTitle("LMS", Wt::WLink {Wt::LinkType::InternalPath, "/artists"});
|
|
||||||
navbar->setResponsive(true);
|
|
||||||
|
|
||||||
// MediaPlayer
|
// MediaPlayer
|
||||||
_mediaPlayer = main->bindNew<MediaPlayer>("player");
|
_mediaPlayer = main->bindNew<MediaPlayer>("player");
|
||||||
|
|
||||||
Wt::WMenu* menu {navbar->addMenu(std::make_unique<Wt::WMenu>())};
|
main->bindNew<Wt::WAnchor>("title", Wt::WLink {Wt::LinkType::InternalPath, defaultPath}, "LMS");
|
||||||
|
main->bindNew<Wt::WAnchor>("artists", Wt::WLink {Wt::LinkType::InternalPath, "/artists"}, Wt::WString::tr("Lms.Explore.artists"));
|
||||||
|
main->bindNew<Wt::WAnchor>("releases", Wt::WLink {Wt::LinkType::InternalPath, "/releases"}, Wt::WString::tr("Lms.Explore.releases"));
|
||||||
|
main->bindNew<Wt::WAnchor>("tracks", Wt::WLink {Wt::LinkType::InternalPath, "/tracks"}, Wt::WString::tr("Lms.Explore.tracks"));
|
||||||
|
|
||||||
|
Filters* filters {main->bindNew<Filters>("filters")};
|
||||||
|
main->bindNew<Wt::WAnchor>("playqueue", Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"}, Wt::WString::tr("Lms.PlayQueue.playqueue"));
|
||||||
|
main->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
|
||||||
|
main->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings"));
|
||||||
|
|
||||||
{
|
{
|
||||||
auto menuItem = menu->insertItem(0, Wt::WString::tr("Lms.Explore.artists"));
|
auto* logout {main->bindNew<Wt::WAnchor>("logout")};
|
||||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/artists"));
|
logout->setText(Wt::WString::tr("Lms.logout"));
|
||||||
menuItem->setSelectable(false);
|
logout->clicked().connect(this, &LmsApplication::handleUserLoggedOut);
|
||||||
}
|
|
||||||
{
|
|
||||||
auto menuItem = menu->insertItem(1, Wt::WString::tr("Lms.Explore.releases"));
|
|
||||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/releases"));
|
|
||||||
menuItem->setSelectable(false);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
auto menuItem = menu->insertItem(2, Wt::WString::tr("Lms.Explore.tracks"));
|
|
||||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/tracks"));
|
|
||||||
menuItem->setSelectable(false);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
auto menuItem = menu->insertItem(3, Wt::WString::tr("Lms.PlayQueue.playqueue"));
|
|
||||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/playqueue"));
|
|
||||||
menuItem->setSelectable(false);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
auto menuItem = menu->insertItem(4, Wt::WString::tr("Lms.PlayHistory.playhistory"));
|
|
||||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/playhistory"));
|
|
||||||
menuItem->setSelectable(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WMenu* rightMenu = navbar->addMenu(std::make_unique<Wt::WMenu>(), Wt::AlignmentFlag::Right);
|
Wt::WLineEdit* searchEdit {main->bindNew<Wt::WLineEdit>("search")};
|
||||||
std::size_t itemCounter = 0;
|
searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
|
||||||
|
|
||||||
if (isUserAdmin())
|
if (isUserAdmin())
|
||||||
{
|
{
|
||||||
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.administration"));
|
main->setCondition("if-is-admin", true);
|
||||||
menuItem->setSelectable(false);
|
main->bindNew<Wt::WAnchor>("database", Wt::WLink {Wt::LinkType::InternalPath, "/admin/database"}, Wt::WString::tr("Lms.Admin.Database.menu-database"));
|
||||||
|
main->bindNew<Wt::WAnchor>("users", Wt::WLink {Wt::LinkType::InternalPath, "/admin/users"}, Wt::WString::tr("Lms.Admin.Users.menu-users"));
|
||||||
auto admin = std::make_unique<Wt::WPopupMenu>();
|
|
||||||
auto dbSettings = admin->insertItem(0, Wt::WString::tr("Lms.Admin.Database.database"));
|
|
||||||
dbSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/database"));
|
|
||||||
dbSettings->setSelectable(false);
|
|
||||||
|
|
||||||
auto usersSettings = admin->insertItem(1, Wt::WString::tr("Lms.Admin.Users.users"));
|
|
||||||
usersSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/users"));
|
|
||||||
usersSettings->setSelectable(false);
|
|
||||||
|
|
||||||
menuItem->setMenu(std::move(admin));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.Settings.settings"));
|
|
||||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/settings"));
|
|
||||||
menuItem->setSelectable(false);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.logout"));
|
|
||||||
menuItem->setSelectable(true);
|
|
||||||
menuItem->triggered().connect(this, &LmsApplication::handleUserLoggedOut);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contents
|
// Contents
|
||||||
@@ -506,11 +468,22 @@ LmsApplication::createHome()
|
|||||||
Wt::WStackedWidget* mainStack = main->bindNew<Wt::WStackedWidget>("contents");
|
Wt::WStackedWidget* mainStack = main->bindNew<Wt::WStackedWidget>("contents");
|
||||||
mainStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
|
mainStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
|
||||||
|
|
||||||
Explore* explore = mainStack->addNew<Explore>();
|
Explore* explore = mainStack->addNew<Explore>(filters);
|
||||||
PlayQueue* playqueue = mainStack->addNew<PlayQueue>();
|
PlayQueue* playqueue = mainStack->addNew<PlayQueue>();
|
||||||
mainStack->addNew<PlayHistory>();
|
auto* search {mainStack->addNew<SearchView>(filters)};
|
||||||
mainStack->addNew<SettingsView>();
|
mainStack->addNew<SettingsView>();
|
||||||
|
|
||||||
|
searchEdit->enterPressed().connect([=]
|
||||||
|
{
|
||||||
|
setInternalPath("/search", true);
|
||||||
|
});
|
||||||
|
|
||||||
|
searchEdit->textInput().connect([=]
|
||||||
|
{
|
||||||
|
setInternalPath("/search", true);
|
||||||
|
search->refreshView(searchEdit->text().toUTF8());
|
||||||
|
});
|
||||||
|
|
||||||
// Admin stuff
|
// Admin stuff
|
||||||
if (isUserAdmin())
|
if (isUserAdmin())
|
||||||
{
|
{
|
||||||
@@ -603,7 +576,7 @@ LmsApplication::createHome()
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Events from Application group
|
// Events from Application group
|
||||||
_events.appOpen.connect([=] (LmsApplicationInfo)
|
_events.appOpen.connect([=]
|
||||||
{
|
{
|
||||||
// Only one active session by user
|
// Only one active session by user
|
||||||
if (!LmsApp->isUserDemo())
|
if (!LmsApp->isUserDemo())
|
||||||
@@ -670,8 +643,9 @@ LmsApplication::notifyMsg(MsgType type, const Wt::WString& message, std::chrono:
|
|||||||
"message: '" << StringUtils::jsEscape(message.toUTF8()) << "'"
|
"message: '" << StringUtils::jsEscape(message.toUTF8()) << "'"
|
||||||
"},{"
|
"},{"
|
||||||
"type: '" << msgTypeToString(type) << "',"
|
"type: '" << msgTypeToString(type) << "',"
|
||||||
"placement: {from: 'top', align: 'center'},"
|
"placement: {from: 'bottom', align: 'right'},"
|
||||||
"timer: 250,"
|
"timer: 250,"
|
||||||
|
"offset: {x: 20, y: 80},"
|
||||||
"delay: " << duration.count() << ""
|
"delay: " << duration.count() << ""
|
||||||
"});";
|
"});";
|
||||||
|
|
||||||
|
|||||||
+3
-26
@@ -28,44 +28,21 @@ LmsTheme::LmsTheme(Database::User::UITheme theme)
|
|||||||
: _theme {theme}
|
: _theme {theme}
|
||||||
{
|
{
|
||||||
setVersion(Wt::BootstrapVersion::v3);
|
setVersion(Wt::BootstrapVersion::v3);
|
||||||
setResponsive(true);
|
setResponsive(true); // Should be not necessary since applied via head-matter
|
||||||
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
|
void
|
||||||
LmsTheme::setTheme(Database::User::UITheme theme)
|
LmsTheme::setTheme(Database::User::UITheme theme)
|
||||||
{
|
{
|
||||||
// Hack, use the application interface directly since changing theme is not allowed
|
// Hack, use the application interface directly since changing theme is not allowed
|
||||||
|
|
||||||
const auto currentStyleSheets {getStyleSheets(_theme)};
|
const auto currentStyleSheets {getStyleSheets(_theme)};
|
||||||
for (auto it {std::crbegin(currentStyleSheets)}; it != std::crend(currentStyleSheets); ++it)
|
for (auto it {std::crbegin(currentStyleSheets)}; it != std::crend(currentStyleSheets); ++it)
|
||||||
{
|
|
||||||
LMS_LOG(UI, DEBUG) << "Removing css file '" << it->url() << "'";
|
|
||||||
LmsApp->removeStyleSheet(*it);
|
LmsApp->removeStyleSheet(*it);
|
||||||
}
|
|
||||||
|
|
||||||
_theme = theme;
|
_theme = theme;
|
||||||
for (const auto& styleSheet : getStyleSheets(theme))
|
for (const auto& styleSheet : getStyleSheets(theme))
|
||||||
{
|
|
||||||
LMS_LOG(UI, DEBUG) << "Adding css file '" << styleSheet.url() << "'";
|
|
||||||
LmsApp->useStyleSheet(styleSheet);
|
LmsApp->useStyleSheet(styleSheet);
|
||||||
}
|
}
|
||||||
applyMetaHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Wt::WLinkedCssStyleSheet>
|
std::vector<Wt::WLinkedCssStyleSheet>
|
||||||
LmsTheme::styleSheets() const
|
LmsTheme::styleSheets() const
|
||||||
@@ -86,8 +63,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme)
|
|||||||
case Database::User::UITheme::Dark:
|
case Database::User::UITheme::Dark:
|
||||||
return
|
return
|
||||||
{
|
{
|
||||||
{"resources/themes/bootstrap/3/wt.css"},
|
|
||||||
{"css/bootstrap-darkly.min.css"},
|
{"css/bootstrap-darkly.min.css"},
|
||||||
|
{"resources/themes/bootstrap/3/wt.css"},
|
||||||
{"css/lms.css"},
|
{"css/lms.css"},
|
||||||
{"css/lms-darkly.css"},
|
{"css/lms-darkly.css"},
|
||||||
};
|
};
|
||||||
@@ -95,8 +72,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme)
|
|||||||
case Database::User::UITheme::Light:
|
case Database::User::UITheme::Light:
|
||||||
return
|
return
|
||||||
{
|
{
|
||||||
{"resources/themes/bootstrap/3/wt.css"},
|
|
||||||
{"css/bootstrap-flatly.min.css"},
|
{"css/bootstrap-flatly.min.css"},
|
||||||
|
{"resources/themes/bootstrap/3/wt.css"},
|
||||||
{"css/lms.css"},
|
{"css/lms.css"},
|
||||||
{"css/lms-flatly.css"},
|
{"css/lms-flatly.css"},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ class LmsTheme : public Wt::WBootstrapTheme
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
void applyMetaHeader() const;
|
|
||||||
std::vector<Wt::WLinkedCssStyleSheet> styleSheets() const override;
|
std::vector<Wt::WLinkedCssStyleSheet> styleSheets() const override;
|
||||||
static std::vector<Wt::WLink> getStyleSheets(Database::User::UITheme theme);
|
static std::vector<Wt::WLink> getStyleSheets(Database::User::UITheme theme);
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
|
#include "database/TrackList.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
|
|
||||||
#include "resource/ImageResource.hpp"
|
#include "resource/ImageResource.hpp"
|
||||||
@@ -290,6 +291,15 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain)
|
|||||||
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
|
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
|
||||||
wApp->doJavaScript(oss.str());
|
wApp->doJavaScript(oss.str());
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||||
|
|
||||||
|
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
||||||
|
if (track)
|
||||||
|
Database::TrackListEntry::create(LmsApp->getDbSession(), track, LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession()));
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
_trackIdLoaded = trackId;
|
_trackIdLoaded = trackId;
|
||||||
trackLoaded.emit(*_trackIdLoaded);
|
trackLoaded.emit(*_trackIdLoaded);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 "PlayHistoryView.hpp"
|
|
||||||
|
|
||||||
#include <Wt/WAnchor.h>
|
|
||||||
#include <Wt/WText.h>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "database/Track.hpp"
|
|
||||||
#include "database/TrackList.hpp"
|
|
||||||
#include "database/User.hpp"
|
|
||||||
|
|
||||||
#include "resource/ImageResource.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
#include "MediaPlayer.hpp"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
using namespace UserInterface;
|
|
||||||
|
|
||||||
std::unique_ptr<Wt::WTemplate> createEntry(Database::Track::pointer track)
|
|
||||||
{
|
|
||||||
auto entry = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.PlayHistory.template.entry"));
|
|
||||||
|
|
||||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
|
||||||
|
|
||||||
auto artists {track->getArtists()};
|
|
||||||
auto release = track->getRelease();
|
|
||||||
|
|
||||||
if (!artists.empty() || release)
|
|
||||||
entry->setCondition("if-has-artists-or-release", true);
|
|
||||||
|
|
||||||
if (!artists.empty())
|
|
||||||
{
|
|
||||||
entry->setCondition("if-has-artists", true);
|
|
||||||
|
|
||||||
Wt::WContainerWidget* artistContainer {entry->bindNew<Wt::WContainerWidget>("artists")};
|
|
||||||
for (const auto& artist : artists)
|
|
||||||
{
|
|
||||||
Wt::WTemplate* a {artistContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.PlayHistory.template.entry-artist"))};
|
|
||||||
a->bindWidget("artist", LmsApplication::createArtistAnchor(artist));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (release)
|
|
||||||
{
|
|
||||||
entry->setCondition("if-has-release", true);
|
|
||||||
entry->bindWidget("release", LmsApplication::createReleaseAnchor(track->getRelease()));
|
|
||||||
{
|
|
||||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
|
||||||
auto cover = std::make_unique<Wt::WImage>();
|
|
||||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
|
||||||
cover->setStyleClass("Lms-cover-small");
|
|
||||||
anchor->setImage(std::move(cover));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
|
||||||
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(track.id(), 96));
|
|
||||||
cover->setStyleClass("Lms-cover-small");
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
PlayHistory::PlayHistory()
|
|
||||||
: Wt::WTemplate {Wt::WString::tr("Lms.PlayHistory.template")}
|
|
||||||
{
|
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
|
||||||
|
|
||||||
_entriesContainer = bindNew<Wt::WContainerWidget>("entries");
|
|
||||||
|
|
||||||
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
|
|
||||||
_showMore->setHidden(true);
|
|
||||||
|
|
||||||
_showMore->clicked().connect([=]
|
|
||||||
{
|
|
||||||
addSome();
|
|
||||||
});
|
|
||||||
|
|
||||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=](Database::IdType trackId)
|
|
||||||
{
|
|
||||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
|
||||||
|
|
||||||
Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
|
||||||
if (track)
|
|
||||||
{
|
|
||||||
Database::TrackListEntry::create(LmsApp->getDbSession(), track, LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession()));
|
|
||||||
_entriesContainer->insertWidget(0, createEntry(track));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addSome();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayHistory::addSome()
|
|
||||||
{
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
|
||||||
|
|
||||||
const Database::TrackList::pointer trackList {LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())};
|
|
||||||
auto trackEntries {trackList->getEntriesReverse(_entriesContainer->count(), 50)};
|
|
||||||
for (const auto& trackEntry : trackEntries)
|
|
||||||
_entriesContainer->addWidget(createEntry(trackEntry->getTrack()));
|
|
||||||
|
|
||||||
_showMore->setHidden(static_cast<std::size_t>(_entriesContainer->count()) >= trackList->getCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -32,12 +32,12 @@ class ValueStringModel : public Wt::WStringListModel
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
||||||
T getValue(std::size_t row)
|
T getValue(std::size_t row) const
|
||||||
{
|
{
|
||||||
return Wt::cpp17::any_cast<T>(data(index(static_cast<int>(row), 0), Wt::ItemDataRole::User));
|
return Wt::cpp17::any_cast<T>(data(index(static_cast<int>(row), 0), Wt::ItemDataRole::User));
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WString getString(std::size_t row)
|
Wt::WString getString(std::size_t row) const
|
||||||
{
|
{
|
||||||
return Wt::cpp17::any_cast<Wt::WString>(data(index(static_cast<int>(row), 0), Wt::ItemDataRole::Display));
|
return Wt::cpp17::any_cast<Wt::WString>(data(index(static_cast<int>(row), 0), Wt::ItemDataRole::Display));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 "ArtistInfoView.hpp"
|
|
||||||
|
|
||||||
#include "database/Artist.hpp"
|
|
||||||
#include "recommendation/IEngine.hpp"
|
|
||||||
#include "utils/Service.hpp"
|
|
||||||
#include "utils/String.hpp"
|
|
||||||
|
|
||||||
#include "ArtistLink.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
ArtistInfo::ArtistInfo()
|
|
||||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ArtistInfo.template"))
|
|
||||||
{
|
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
|
||||||
|
|
||||||
_similarArtistsContainer = bindNew<Wt::WContainerWidget>("similar-artists");
|
|
||||||
|
|
||||||
wApp->internalPathChanged().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
refresh();
|
|
||||||
}));
|
|
||||||
|
|
||||||
LmsApp->getEvents().dbScanned.connect([=]
|
|
||||||
{
|
|
||||||
refresh();
|
|
||||||
});
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ArtistInfo::refresh()
|
|
||||||
{
|
|
||||||
_similarArtistsContainer->clear();
|
|
||||||
|
|
||||||
if (!wApp->internalPathMatches("/artist/"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto artistId = StringUtils::readAs<Database::IdType>(wApp->internalPathNextPart("/artist/"));
|
|
||||||
if (!artistId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const std::vector<Database::IdType> artistsIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)};
|
|
||||||
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
|
||||||
|
|
||||||
for (Database::IdType artistId : artistsIds)
|
|
||||||
{
|
|
||||||
Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), artistId)};
|
|
||||||
if (!artist)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
_similarArtistsContainer->addNew<ArtistLink>(artist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 <Wt/WContainerWidget.h>
|
|
||||||
#include <Wt/WTemplate.h>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
class ArtistInfo : public Wt::WTemplate
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
ArtistInfo();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void refresh();
|
|
||||||
|
|
||||||
Wt::WContainerWidget* _similarArtistsContainer;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2018 Emeric Poupon
|
* Copyright (C) 2020 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -16,20 +16,32 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
#include "ArtistListHelpers.hpp"
|
||||||
#include "ArtistLink.hpp"
|
|
||||||
|
|
||||||
#include <Wt/WAnchor.h>
|
#include <Wt/WAnchor.h>
|
||||||
|
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface::ArtistListHelpers
|
||||||
|
|
||||||
ArtistLink::ArtistLink(Database::Artist::pointer artist)
|
|
||||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ArtistLink.template"))
|
|
||||||
{
|
{
|
||||||
bindWidget("name", LmsApplication::createArtistAnchor(artist));
|
std::unique_ptr<Wt::WTemplate>
|
||||||
|
createEntry(const Wt::Dbo::ptr<Database::Artist>& artist)
|
||||||
|
{
|
||||||
|
auto res {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry"))};
|
||||||
|
res->bindWidget("name", LmsApplication::createArtistAnchor(artist));
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Wt::WTemplate>
|
||||||
|
createEntrySmall(const Wt::Dbo::ptr<Database::Artist>& artist)
|
||||||
|
{
|
||||||
|
auto res {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry-small"))};
|
||||||
|
res->bindWidget("name", LmsApplication::createArtistAnchor(artist));
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2018 Emeric Poupon
|
* Copyright (C) 2020 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -19,19 +19,20 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <Wt/WTemplate.h>
|
#include <Wt/WTemplate.h>
|
||||||
|
|
||||||
namespace Database{
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
class Artist;
|
class Artist;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface::ArtistListHelpers
|
||||||
|
|
||||||
class ArtistLink : public Wt::WTemplate
|
|
||||||
{
|
{
|
||||||
public:
|
std::unique_ptr<Wt::WTemplate> createEntry(const Wt::Dbo::ptr<Database::Artist>& artist);
|
||||||
ArtistLink(Wt::Dbo::ptr<Database::Artist> artist);
|
std::unique_ptr<Wt::WTemplate> createEntrySmall(const Wt::Dbo::ptr<Database::Artist>& artist);
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,12 +27,13 @@
|
|||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/ScanSettings.hpp"
|
#include "database/ScanSettings.hpp"
|
||||||
|
#include "recommendation/IEngine.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
#include "resource/ImageResource.hpp"
|
#include "resource/ImageResource.hpp"
|
||||||
|
|
||||||
|
#include "ArtistListHelpers.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
#include "LmsApplicationException.hpp"
|
#include "LmsApplicationException.hpp"
|
||||||
#include "Filters.hpp"
|
#include "Filters.hpp"
|
||||||
@@ -42,22 +43,26 @@ using namespace Database;
|
|||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
Artist::Artist(Filters* filters)
|
Artist::Artist(Filters* filters)
|
||||||
: _filters(filters)
|
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artist.template")}
|
||||||
|
, _filters(filters)
|
||||||
{
|
{
|
||||||
wApp->internalPathChanged().connect([=]
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
|
LmsApp->internalPathChanged().connect([=]
|
||||||
{
|
{
|
||||||
refresh();
|
refreshView();
|
||||||
});
|
});
|
||||||
|
|
||||||
refresh();
|
filters->updated().connect([=]
|
||||||
|
{
|
||||||
filters->updated().connect([=] {
|
refreshView();
|
||||||
refresh();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
refreshView();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Artist::refresh()
|
Artist::refreshView()
|
||||||
{
|
{
|
||||||
if (!wApp->internalPathMatches("/artist/"))
|
if (!wApp->internalPathMatches("/artist/"))
|
||||||
return;
|
return;
|
||||||
@@ -68,16 +73,17 @@ Artist::refresh()
|
|||||||
if (!artistId)
|
if (!artistId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const std::vector<Database::IdType> similarArtistIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)};
|
||||||
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
const Database::Artist::pointer artist = Database::Artist::getById(LmsApp->getDbSession(), *artistId);
|
const Database::Artist::pointer artist = Database::Artist::getById(LmsApp->getDbSession(), *artistId);
|
||||||
if (!artist)
|
if (!artist)
|
||||||
throw ArtistNotFoundException {*artistId};
|
throw ArtistNotFoundException {*artistId};
|
||||||
|
|
||||||
Wt::WTemplate* t {addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artist.template"))};
|
refreshSimilarArtists(similarArtistIds);
|
||||||
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
|
||||||
|
|
||||||
Wt::WContainerWidget* clusterContainers = t->bindNew<Wt::WContainerWidget>("clusters");
|
Wt::WContainerWidget* clusterContainers = bindNew<Wt::WContainerWidget>("clusters");
|
||||||
|
|
||||||
{
|
{
|
||||||
auto clusterTypes = ScanSettings::get(LmsApp->getDbSession())->getClusterTypes();
|
auto clusterTypes = ScanSettings::get(LmsApp->getDbSession())->getClusterTypes();
|
||||||
@@ -97,9 +103,9 @@ Artist::refresh()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t->bindString("name", Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain);
|
bindString("name", Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain);
|
||||||
{
|
{
|
||||||
Wt::WText* playBtn = t->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
Wt::WText* playBtn = bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||||
|
|
||||||
playBtn->clicked().connect([=]
|
playBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
@@ -108,7 +114,7 @@ Artist::refresh()
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
Wt::WText* addBtn = t->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
Wt::WText* addBtn = bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||||
|
|
||||||
addBtn->clicked().connect([=]
|
addBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
@@ -116,15 +122,21 @@ Artist::refresh()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WContainerWidget* releasesContainer = t->bindNew<Wt::WContainerWidget>("releases");
|
Wt::WContainerWidget* releasesContainer = bindNew<Wt::WContainerWidget>("releases");
|
||||||
|
|
||||||
auto releases = artist->getReleases(_filters->getClusterIds());
|
auto releases = artist->getReleases(_filters->getClusterIds());
|
||||||
|
for (const auto& release : releases)
|
||||||
|
{
|
||||||
|
releasesContainer->addWidget(createRelease(artist, release));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (auto release : releases)
|
std::unique_ptr<Wt::WTemplate>
|
||||||
|
Artist::createRelease(const Database::Artist::pointer& artist, const Release::pointer& release)
|
||||||
{
|
{
|
||||||
auto releaseId = release.id();
|
auto releaseId = release.id();
|
||||||
|
|
||||||
Wt::WTemplate* entry = releasesContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artist.template.entry"));
|
auto entry = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artist.template.entry"));
|
||||||
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
|
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -180,6 +192,22 @@ Artist::refresh()
|
|||||||
{
|
{
|
||||||
releasesAdd.emit({releaseId});
|
releasesAdd.emit({releaseId});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Artist::refreshSimilarArtists(const std::vector<Database::IdType>& similarArtistsId)
|
||||||
|
{
|
||||||
|
auto* similarArtistsContainer {bindNew<Wt::WContainerWidget>("similar-artists")};
|
||||||
|
|
||||||
|
for (Database::IdType artistId : similarArtistsId)
|
||||||
|
{
|
||||||
|
const Database::Artist::pointer similarArtist{Database::Artist::getById(LmsApp->getDbSession(), artistId)};
|
||||||
|
if (!similarArtist)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
similarArtistsContainer->addWidget(ArtistListHelpers::createEntrySmall(similarArtist));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,16 +19,24 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Wt/WContainerWidget.h>
|
#include <memory>
|
||||||
|
|
||||||
#include <Wt/WSignal.h>
|
#include <Wt/WSignal.h>
|
||||||
|
#include <Wt/WTemplate.h>
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Artist;
|
||||||
|
class Release;
|
||||||
|
}
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
class Filters;
|
class Filters;
|
||||||
|
|
||||||
class Artist : public Wt::WContainerWidget
|
class Artist : public Wt::WTemplate
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Artist(Filters* filters);
|
Artist(Filters* filters);
|
||||||
@@ -40,9 +48,12 @@ class Artist : public Wt::WContainerWidget
|
|||||||
Wt::Signal<const std::vector<Database::IdType>&> releasesPlay;
|
Wt::Signal<const std::vector<Database::IdType>&> releasesPlay;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refresh();
|
void refreshView();
|
||||||
|
void refreshSimilarArtists(const std::vector<Database::IdType>& similarArtistsId);
|
||||||
|
|
||||||
Filters* _filters;
|
std::unique_ptr<Wt::WTemplate> createRelease(const Wt::Dbo::ptr<Database::Artist>& artist, const Wt::Dbo::ptr<Database::Release>& release);
|
||||||
|
|
||||||
|
Filters* _filters {};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 "ArtistsInfoView.hpp"
|
|
||||||
|
|
||||||
#include <Wt/WLocalDateTime.h>
|
|
||||||
|
|
||||||
#include "database/Artist.hpp"
|
|
||||||
#include "database/TrackList.hpp"
|
|
||||||
#include "database/User.hpp"
|
|
||||||
#include "ArtistLink.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
#include "MediaPlayer.hpp"
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
ArtistsInfo::ArtistsInfo()
|
|
||||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ArtistsInfo.template"))
|
|
||||||
{
|
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
|
||||||
|
|
||||||
_mostPlayedContainer = bindNew<Wt::WContainerWidget>("most-played");
|
|
||||||
_recentlyAddedContainer = bindNew<Wt::WContainerWidget>("recently-added");
|
|
||||||
|
|
||||||
LmsApp->getEvents().dbScanned.connect([=]
|
|
||||||
{
|
|
||||||
refreshRecentlyAdded();
|
|
||||||
});
|
|
||||||
|
|
||||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
|
|
||||||
{
|
|
||||||
refreshMostPlayed();
|
|
||||||
});
|
|
||||||
|
|
||||||
refreshMostPlayed();
|
|
||||||
refreshRecentlyAdded();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ArtistsInfo::refreshRecentlyAdded()
|
|
||||||
{
|
|
||||||
auto after = Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6);
|
|
||||||
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
|
||||||
const std::vector<Database::Artist::pointer> artists {Artist::getLastWritten(LmsApp->getDbSession(), after, 5)};
|
|
||||||
|
|
||||||
_recentlyAddedContainer->clear();
|
|
||||||
for (const Database::Artist::pointer& artist : artists)
|
|
||||||
_recentlyAddedContainer->addNew<ArtistLink>(artist);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ArtistsInfo::refreshMostPlayed()
|
|
||||||
{
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
|
||||||
|
|
||||||
const std::vector<Database::Artist::pointer> artists {LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTopArtists(5)};
|
|
||||||
|
|
||||||
_mostPlayedContainer->clear();
|
|
||||||
for (const Database::Artist::pointer& artist : artists)
|
|
||||||
_mostPlayedContainer->addNew<ArtistLink>(artist);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 <Wt/WContainerWidget.h>
|
|
||||||
#include <Wt/WTemplate.h>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
class ArtistsInfo : public Wt::WTemplate
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
ArtistsInfo();
|
|
||||||
|
|
||||||
void refreshRecentlyAdded();
|
|
||||||
void refreshMostPlayed();
|
|
||||||
|
|
||||||
private:
|
|
||||||
Wt::WContainerWidget* _mostPlayedContainer;
|
|
||||||
Wt::WContainerWidget* _recentlyAddedContainer;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -21,16 +21,15 @@
|
|||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#include <Wt/WAnchor.h>
|
#include <Wt/WMenu.h>
|
||||||
#include <Wt/WLineEdit.h>
|
|
||||||
#include <Wt/WLocalDateTime.h>
|
|
||||||
#include <Wt/WTemplate.h>
|
|
||||||
|
|
||||||
#include "common/ValueStringModel.hpp"
|
#include "common/ValueStringModel.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/User.hpp"
|
||||||
|
#include "database/TrackList.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "utils/String.hpp"
|
|
||||||
|
|
||||||
|
#include "ArtistListHelpers.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
#include "Filters.hpp"
|
#include "Filters.hpp"
|
||||||
|
|
||||||
@@ -41,14 +40,29 @@ namespace UserInterface {
|
|||||||
using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLink::Type>>;
|
using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLink::Type>>;
|
||||||
|
|
||||||
Artists::Artists(Filters* filters)
|
Artists::Artists(Filters* filters)
|
||||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.Artists.template")),
|
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")},
|
||||||
_filters(filters)
|
_filters {filters}
|
||||||
{
|
{
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
_search = bindNew<Wt::WLineEdit>("search");
|
{
|
||||||
_search->setPlaceholderText(Wt::WString::tr("Lms.Explore.search-placeholder"));
|
auto* menu {bindNew<Wt::WMenu>("mode")};
|
||||||
_search->textInput().connect(this, &Artists::refresh);
|
|
||||||
|
auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, Mode mode)
|
||||||
|
{
|
||||||
|
auto* item {menu.addItem(str)};
|
||||||
|
item->clicked().connect([this, mode] { refreshView(mode); });
|
||||||
|
|
||||||
|
if (mode == defaultMode)
|
||||||
|
item->renderSelected(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All);
|
||||||
|
}
|
||||||
|
|
||||||
_linkType = bindNew<Wt::WComboBox>("link-type");
|
_linkType = bindNew<Wt::WComboBox>("link-type");
|
||||||
{
|
{
|
||||||
@@ -58,51 +72,135 @@ Artists::Artists(Filters* filters)
|
|||||||
linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-releaseartist"), TrackArtistLink::Type::ReleaseArtist);
|
linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-releaseartist"), TrackArtistLink::Type::ReleaseArtist);
|
||||||
_linkType->setModel(linkTypeModel);
|
_linkType->setModel(linkTypeModel);
|
||||||
}
|
}
|
||||||
_linkType->changed().connect(this, &Artists::refresh);
|
_linkType->changed().connect([this] { refreshView(); });
|
||||||
|
|
||||||
_container = bindNew<Wt::WContainerWidget>("artists");
|
_container = bindNew<Wt::WContainerWidget>("artists");
|
||||||
|
|
||||||
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
|
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
|
||||||
_showMore->clicked().connect(std::bind([=]
|
_showMore->clicked().connect([=]
|
||||||
{
|
{
|
||||||
addSome();
|
addSome();
|
||||||
}));
|
});
|
||||||
|
|
||||||
refresh();
|
refreshView();
|
||||||
|
|
||||||
filters->updated().connect(this, &Artists::refresh);
|
filters->updated().connect([this] { refreshView(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Artists::refresh()
|
Artists::refreshView()
|
||||||
{
|
{
|
||||||
_container->clear();
|
_container->clear();
|
||||||
|
_randomArtists.clear();
|
||||||
addSome();
|
addSome();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Artists::refreshView(Mode mode)
|
||||||
|
{
|
||||||
|
_mode = mode;
|
||||||
|
refreshView();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Artist::pointer>
|
||||||
|
Artists::getRandomArtists(std::optional<Range> range, bool& moreResults)
|
||||||
|
{
|
||||||
|
std::vector<Artist::pointer> artists;
|
||||||
|
|
||||||
|
const std::optional<TrackArtistLink::Type> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
|
||||||
|
|
||||||
|
if (_randomArtists.empty())
|
||||||
|
_randomArtists = Artist::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), linkType, maxItemsPerMode[Mode::Random]);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto itBegin {std::cbegin(_randomArtists) + std::min(range ? range->offset : 0, _randomArtists.size())};
|
||||||
|
auto itEnd {std::cbegin(_randomArtists) + std::min(range ? range->offset + range->limit : _randomArtists.size(), _randomArtists.size())};
|
||||||
|
|
||||||
|
for (auto it {itBegin}; it != itEnd; ++it)
|
||||||
|
{
|
||||||
|
Artist::pointer artist {Artist::getById(LmsApp->getDbSession(), *it)};
|
||||||
|
if (artist)
|
||||||
|
artists.push_back(artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
moreResults = (itEnd != std::cend(_randomArtists));
|
||||||
|
}
|
||||||
|
|
||||||
|
return artists;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Artist::pointer>
|
||||||
|
Artists::getArtists(std::optional<Range> range, bool& moreResults)
|
||||||
|
{
|
||||||
|
std::vector<Artist::pointer> artists;
|
||||||
|
|
||||||
|
const std::optional<TrackArtistLink::Type> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
|
||||||
|
|
||||||
|
const std::optional<std::size_t> modeLimit{maxItemsPerMode[_mode]};
|
||||||
|
if (modeLimit)
|
||||||
|
{
|
||||||
|
if (range)
|
||||||
|
range->limit = std::min(*modeLimit - range->offset, range->offset + range->limit);
|
||||||
|
else
|
||||||
|
range = Range {0, *modeLimit};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_mode)
|
||||||
|
{
|
||||||
|
case Mode::Random:
|
||||||
|
artists = getRandomArtists(range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::RecentlyPlayed:
|
||||||
|
artists = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())
|
||||||
|
->getArtistsReverse(_filters->getClusterIds(),
|
||||||
|
linkType,
|
||||||
|
range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::MostPlayed:
|
||||||
|
artists = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())
|
||||||
|
->getTopArtists(_filters->getClusterIds(),
|
||||||
|
linkType,
|
||||||
|
range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::RecentlyAdded:
|
||||||
|
artists = Artist::getLastWritten(LmsApp->getDbSession(),
|
||||||
|
std::nullopt,
|
||||||
|
_filters->getClusterIds(),
|
||||||
|
linkType,
|
||||||
|
range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::All:
|
||||||
|
artists = Artist::getByFilter(LmsApp->getDbSession(),
|
||||||
|
_filters->getClusterIds(),
|
||||||
|
{},
|
||||||
|
linkType,
|
||||||
|
Artist::SortMethod::BySortName,
|
||||||
|
range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range && modeLimit && (range->offset + range->limit == *modeLimit))
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return artists;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Artists::addSome()
|
Artists::addSome()
|
||||||
{
|
{
|
||||||
const auto searchKeywords {StringUtils::splitString(_search->text().toUTF8(), " ")};
|
|
||||||
|
|
||||||
auto clusterIds = _filters->getClusterIds();
|
|
||||||
auto linkModel = static_cast<ArtistLinkModel*>(_linkType->model().get());
|
|
||||||
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const std::vector<Artist::pointer> artists {Artist::getByFilter(LmsApp->getDbSession(),
|
for (const Artist::pointer& artist : getArtists(Range {static_cast<std::size_t>(_container->count()), batchSize}, moreResults))
|
||||||
clusterIds,
|
|
||||||
searchKeywords,
|
|
||||||
linkModel->getValue(_linkType->currentIndex()),
|
|
||||||
Artist::SortMethod::BySortName,
|
|
||||||
_container->count(), 20, moreResults)};
|
|
||||||
|
|
||||||
for (const auto& artist : artists)
|
|
||||||
{
|
{
|
||||||
Wt::WTemplate* entry {_container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry"))};
|
_container->addWidget(ArtistListHelpers::createEntry(artist));
|
||||||
|
|
||||||
entry->bindWidget("name", LmsApplication::createArtistAnchor(artist));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_showMore->setHidden(!moreResults);
|
_showMore->setHidden(!moreResults);
|
||||||
|
|||||||
@@ -19,15 +19,22 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <Wt/WComboBox.h>
|
#include <Wt/WComboBox.h>
|
||||||
#include <Wt/WContainerWidget.h>
|
#include <Wt/WContainerWidget.h>
|
||||||
#include <Wt/WLineEdit.h>
|
|
||||||
#include <Wt/WPushButton.h>
|
#include <Wt/WPushButton.h>
|
||||||
#include <Wt/WTemplate.h>
|
#include <Wt/WTemplate.h>
|
||||||
#include <Wt/WSignal.h>
|
#include <Wt/WSignal.h>
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Artist;
|
||||||
|
}
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
class Filters;
|
class Filters;
|
||||||
@@ -37,18 +44,40 @@ class Artists : public Wt::WTemplate
|
|||||||
public:
|
public:
|
||||||
Artists(Filters* filters);
|
Artists(Filters* filters);
|
||||||
|
|
||||||
Wt::Signal<const std::vector<Database::IdType>&> artistsAdd;
|
|
||||||
Wt::Signal<const std::vector<Database::IdType>&> artistsPlay;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refresh();
|
enum class Mode
|
||||||
|
{
|
||||||
|
Random,
|
||||||
|
RecentlyPlayed,
|
||||||
|
RecentlyAdded,
|
||||||
|
MostPlayed,
|
||||||
|
All
|
||||||
|
};
|
||||||
|
|
||||||
|
void refreshView();
|
||||||
|
void refreshView(Mode mode);
|
||||||
void addSome();
|
void addSome();
|
||||||
|
|
||||||
Filters* _filters;
|
std::vector<Wt::Dbo::ptr<Database::Artist>> getArtists(std::optional<Database::Range> range, bool& moreResults);
|
||||||
Wt::WPushButton* _showMore;
|
std::vector<Wt::Dbo::ptr<Database::Artist>> getRandomArtists(std::optional<Database::Range> range, bool& moreResults);
|
||||||
Wt::WLineEdit* _search;
|
|
||||||
Wt::WComboBox* _linkType;
|
static constexpr Mode defaultMode {Mode::Random};
|
||||||
Wt::WContainerWidget* _container;
|
static constexpr std::size_t batchSize {30};
|
||||||
|
static inline std::unordered_map<Mode, std::optional<std::size_t>> maxItemsPerMode
|
||||||
|
{
|
||||||
|
{Mode::Random, batchSize * 4},
|
||||||
|
{Mode::RecentlyPlayed, batchSize * 4},
|
||||||
|
{Mode::RecentlyAdded, batchSize * 2},
|
||||||
|
{Mode::MostPlayed, batchSize * 2},
|
||||||
|
{Mode::All, std::nullopt},
|
||||||
|
};
|
||||||
|
|
||||||
|
Mode _mode {defaultMode};
|
||||||
|
std::vector<Database::IdType> _randomArtists;
|
||||||
|
Filters* _filters {};
|
||||||
|
Wt::WPushButton* _showMore {};
|
||||||
|
Wt::WContainerWidget* _container {};
|
||||||
|
Wt::WComboBox* _linkType {};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|||||||
@@ -30,16 +30,11 @@
|
|||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
#include "ArtistInfoView.hpp"
|
|
||||||
#include "ArtistsInfoView.hpp"
|
|
||||||
#include "ArtistsView.hpp"
|
#include "ArtistsView.hpp"
|
||||||
#include "ArtistView.hpp"
|
#include "ArtistView.hpp"
|
||||||
#include "Filters.hpp"
|
#include "Filters.hpp"
|
||||||
#include "ReleaseInfoView.hpp"
|
|
||||||
#include "ReleasesInfoView.hpp"
|
|
||||||
#include "ReleasesView.hpp"
|
#include "ReleasesView.hpp"
|
||||||
#include "ReleaseView.hpp"
|
#include "ReleaseView.hpp"
|
||||||
#include "TracksInfoView.hpp"
|
|
||||||
#include "TracksView.hpp"
|
#include "TracksView.hpp"
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
@@ -77,53 +72,19 @@ handleContentsPathChange(Wt::WStackedWidget* stack)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
handleInfoPathChange(Wt::WStackedWidget* stack)
|
|
||||||
{
|
|
||||||
enum Idx
|
|
||||||
{
|
|
||||||
IdxArtist = 0,
|
|
||||||
IdxArtists,
|
|
||||||
IdxRelease,
|
|
||||||
IdxReleases,
|
|
||||||
IdxTracks,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const std::map<std::string, int> indexes =
|
|
||||||
{
|
|
||||||
{ "/artists", IdxArtists },
|
|
||||||
{ "/artist", IdxArtist },
|
|
||||||
{ "/releases", IdxReleases },
|
|
||||||
{ "/release", IdxRelease },
|
|
||||||
{ "/tracks", IdxTracks },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (auto index : indexes)
|
|
||||||
{
|
|
||||||
if (wApp->internalPathMatches(index.first))
|
|
||||||
{
|
|
||||||
stack->setCurrentIndex(index.second);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Explore::Explore()
|
Explore::Explore(Filters* filters)
|
||||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.template"))
|
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.template")}
|
||||||
|
, _filters {filters}
|
||||||
{
|
{
|
||||||
addFunction("tr", &Functions::tr);
|
addFunction("tr", &Functions::tr);
|
||||||
|
|
||||||
_filters = bindNew<Filters>("filters");
|
|
||||||
|
|
||||||
// Contents
|
// Contents
|
||||||
Wt::WStackedWidget* contentsStack = bindNew<Wt::WStackedWidget>("contents");
|
Wt::WStackedWidget* contentsStack = bindNew<Wt::WStackedWidget>("contents");
|
||||||
contentsStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
|
contentsStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
|
||||||
|
|
||||||
auto artists = std::make_unique<Artists>(_filters);
|
auto artists = std::make_unique<Artists>(_filters);
|
||||||
artists->artistsAdd.connect(this, &Explore::handleArtistsAdd);
|
|
||||||
artists->artistsPlay.connect(this, &Explore::handleArtistsPlay);
|
|
||||||
contentsStack->addWidget(std::move(artists));
|
contentsStack->addWidget(std::move(artists));
|
||||||
|
|
||||||
auto artist = std::make_unique<Artist>(_filters);
|
auto artist = std::make_unique<Artist>(_filters);
|
||||||
@@ -150,32 +111,12 @@ Explore::Explore()
|
|||||||
tracks->tracksPlay.connect(this, &Explore::handleTracksPlay);
|
tracks->tracksPlay.connect(this, &Explore::handleTracksPlay);
|
||||||
contentsStack->addWidget(std::move(tracks));
|
contentsStack->addWidget(std::move(tracks));
|
||||||
|
|
||||||
// Info
|
wApp->internalPathChanged().connect([=]
|
||||||
Wt::WStackedWidget* infoStack = bindNew<Wt::WStackedWidget>("info");
|
|
||||||
|
|
||||||
auto artistInfo = std::make_unique<ArtistInfo>();
|
|
||||||
infoStack->addWidget(std::move(artistInfo));
|
|
||||||
|
|
||||||
auto artistsInfo = std::make_unique<ArtistsInfo>();
|
|
||||||
infoStack->addWidget(std::move(artistsInfo));
|
|
||||||
|
|
||||||
auto releaseInfo = std::make_unique<ReleaseInfo>();
|
|
||||||
infoStack->addWidget(std::move(releaseInfo));
|
|
||||||
|
|
||||||
auto releasesInfo = std::make_unique<ReleasesInfo>();
|
|
||||||
infoStack->addWidget(std::move(releasesInfo));
|
|
||||||
|
|
||||||
auto tracksInfo = std::make_unique<TracksInfo>();
|
|
||||||
infoStack->addWidget(std::move(tracksInfo));
|
|
||||||
|
|
||||||
wApp->internalPathChanged().connect(std::bind([=]
|
|
||||||
{
|
{
|
||||||
handleContentsPathChange(contentsStack);
|
handleContentsPathChange(contentsStack);
|
||||||
handleInfoPathChange(infoStack);
|
});
|
||||||
}));
|
|
||||||
|
|
||||||
handleContentsPathChange(contentsStack);
|
handleContentsPathChange(contentsStack);
|
||||||
handleInfoPathChange(infoStack);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Filters;
|
|||||||
class Explore : public Wt::WTemplate
|
class Explore : public Wt::WTemplate
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Explore();
|
Explore(Filters* filters);
|
||||||
|
|
||||||
Wt::Signal<std::vector<Database::IdType>> tracksAdd;
|
Wt::Signal<std::vector<Database::IdType>> tracksAdd;
|
||||||
Wt::Signal<std::vector<Database::IdType>> tracksPlay;
|
Wt::Signal<std::vector<Database::IdType>> tracksPlay;
|
||||||
@@ -44,7 +44,7 @@ class Explore : public Wt::WTemplate
|
|||||||
void handleTracksAdd(const std::vector<Database::IdType>& tracksId);
|
void handleTracksAdd(const std::vector<Database::IdType>& tracksId);
|
||||||
void handleTracksPlay(const std::vector<Database::IdType>& tracksId);
|
void handleTracksPlay(const std::vector<Database::IdType>& tracksId);
|
||||||
|
|
||||||
Filters* _filters;
|
Filters* _filters {};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ Filters::showDialog()
|
|||||||
Wt::WComboBox* typeCombo = container->bindNew<Wt::WComboBox>("type");
|
Wt::WComboBox* typeCombo = container->bindNew<Wt::WComboBox>("type");
|
||||||
Wt::WComboBox* valueCombo = container->bindNew<Wt::WComboBox>("value");
|
Wt::WComboBox* valueCombo = container->bindNew<Wt::WComboBox>("value");
|
||||||
|
|
||||||
Wt::WPushButton* addBtn = container->bindNew<Wt::WPushButton>("add-btn", Wt::WString::tr("Lms.add"));
|
Wt::WPushButton* addBtn = container->bindNew<Wt::WPushButton>("add-btn", Wt::WString::tr("Lms.Explore.add-filter"));
|
||||||
addBtn->clicked().connect(dialog.get(), &Wt::WDialog::accept);
|
addBtn->clicked().connect(dialog.get(), &Wt::WDialog::accept);
|
||||||
|
|
||||||
Wt::WPushButton* cancelBtn = container->bindNew<Wt::WPushButton>("cancel-btn", Wt::WString::tr("Lms.cancel"));
|
Wt::WPushButton* cancelBtn = container->bindNew<Wt::WPushButton>("cancel-btn", Wt::WString::tr("Lms.cancel"));
|
||||||
@@ -155,12 +155,12 @@ Filters::add(Database::IdType clusterId)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Filters::Filters()
|
Filters::Filters()
|
||||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.template.filters"))
|
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.template.filters")}
|
||||||
{
|
{
|
||||||
addFunction("tr", &Functions::tr);
|
addFunction("tr", &Functions::tr);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
Wt::WPushButton *addFilterBtn = bindNew<Wt::WPushButton>("add-filter", Wt::WText::tr("Lms.add"));
|
Wt::WPushButton *addFilterBtn = bindNew<Wt::WPushButton>("add-filter", Wt::WText::tr("Lms.Explore.add-filter"));
|
||||||
addFilterBtn->clicked().connect(this, &Filters::showDialog);
|
addFilterBtn->clicked().connect(this, &Filters::showDialog);
|
||||||
|
|
||||||
_filters = bindNew<Wt::WContainerWidget>("clusters");
|
_filters = bindNew<Wt::WContainerWidget>("clusters");
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 "ReleaseInfoView.hpp"
|
|
||||||
|
|
||||||
#include <Wt/WAnchor.h>
|
|
||||||
|
|
||||||
#include "database/Release.hpp"
|
|
||||||
#include "recommendation/IEngine.hpp"
|
|
||||||
#include "utils/Service.hpp"
|
|
||||||
#include "utils/String.hpp"
|
|
||||||
|
|
||||||
#include "ReleaseLink.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
ReleaseInfo::ReleaseInfo()
|
|
||||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ReleaseInfo.template"))
|
|
||||||
{
|
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
|
||||||
|
|
||||||
_similarReleasesContainer = bindNew<Wt::WContainerWidget>("similar-releases");
|
|
||||||
|
|
||||||
wApp->internalPathChanged().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
refresh();
|
|
||||||
}));
|
|
||||||
|
|
||||||
LmsApp->getEvents().dbScanned.connect([=]
|
|
||||||
{
|
|
||||||
refresh();
|
|
||||||
});
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ReleaseInfo::refresh()
|
|
||||||
{
|
|
||||||
_similarReleasesContainer->clear();
|
|
||||||
setCondition("if-has-copyright-or-copyright-url", false);
|
|
||||||
setCondition("if-has-copyright-url", false);
|
|
||||||
setCondition("if-has-copyright", false);
|
|
||||||
|
|
||||||
if (!wApp->internalPathMatches("/release/"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto releaseId {StringUtils::readAs<Database::IdType>(wApp->internalPathNextPart("/release/"))};
|
|
||||||
if (!releaseId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const std::vector<Database::IdType> releasesIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 5)};
|
|
||||||
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
|
||||||
|
|
||||||
Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), *releaseId)};
|
|
||||||
if (!release)
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::optional<std::string> copyright {release->getCopyright()};
|
|
||||||
std::optional<std::string> copyrightURL {release->getCopyrightURL()};
|
|
||||||
|
|
||||||
setCondition("if-has-copyright-or-copyright-url", copyright || copyrightURL);
|
|
||||||
|
|
||||||
if (copyrightURL)
|
|
||||||
{
|
|
||||||
setCondition("if-has-copyright-url", true);
|
|
||||||
|
|
||||||
Wt::WLink link {*copyrightURL};
|
|
||||||
link.setTarget(Wt::LinkTarget::NewWindow);
|
|
||||||
|
|
||||||
Wt::WAnchor* anchor {bindNew<Wt::WAnchor>("copyright-url", link)};
|
|
||||||
anchor->setTextFormat(Wt::TextFormat::XHTML);
|
|
||||||
anchor->setText(Wt::WString::tr("Lms.Explore.Release.template.link-btn"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (copyright)
|
|
||||||
{
|
|
||||||
setCondition("if-has-copyright", true);
|
|
||||||
bindString("copyright", Wt::WString::fromUTF8(*copyright), Wt::TextFormat::Plain);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Database::Release::pointer> similarReleases;
|
|
||||||
for (Database::IdType id : releasesIds)
|
|
||||||
{
|
|
||||||
Database::Release::pointer similarRelease {Database::Release::getById(LmsApp->getDbSession(), id)};
|
|
||||||
|
|
||||||
if (similarRelease)
|
|
||||||
similarReleases.emplace_back(similarRelease);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const Database::Release::pointer& similarRelease : similarReleases)
|
|
||||||
_similarReleasesContainer->addNew<ReleaseLink>(similarRelease);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 <Wt/WContainerWidget.h>
|
|
||||||
#include <Wt/WTemplate.h>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
class ReleaseInfo : public Wt::WTemplate
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
ReleaseInfo();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void refresh();
|
|
||||||
|
|
||||||
Wt::WContainerWidget* _similarReleasesContainer;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 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 "ReleaseLink.hpp"
|
|
||||||
|
|
||||||
#include <Wt/WAnchor.h>
|
|
||||||
#include <Wt/WImage.h>
|
|
||||||
#include <Wt/WText.h>
|
|
||||||
|
|
||||||
#include "database/Release.hpp"
|
|
||||||
#include "resource/ImageResource.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
ReleaseLink::ReleaseLink(Database::Release::pointer release)
|
|
||||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ReleaseLink.template"))
|
|
||||||
{
|
|
||||||
bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
|
|
||||||
|
|
||||||
Wt::WAnchor* anchor = bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
|
||||||
auto cover = std::make_unique<Wt::WImage>();
|
|
||||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 64));
|
|
||||||
cover->setStyleClass("Lms-cover-smaller");
|
|
||||||
anchor->setImage(std::move(cover));
|
|
||||||
|
|
||||||
auto artists = release->getReleaseArtists();
|
|
||||||
if (artists.empty())
|
|
||||||
artists = release->getArtists();
|
|
||||||
|
|
||||||
if (artists.size() > 1)
|
|
||||||
{
|
|
||||||
setCondition("if-has-artist", true);
|
|
||||||
bindNew<Wt::WText>("artist-name", Wt::WString::tr("Lms.Explore.various-artists"));
|
|
||||||
}
|
|
||||||
else if (artists.size() == 1)
|
|
||||||
{
|
|
||||||
setCondition("if-has-artist", true);
|
|
||||||
bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 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 "ReleaseListHelpers.hpp"
|
||||||
|
|
||||||
|
#include <Wt/WAnchor.h>
|
||||||
|
#include <Wt/WImage.h>
|
||||||
|
#include <Wt/WText.h>
|
||||||
|
|
||||||
|
#include "database/Release.hpp"
|
||||||
|
#include "resource/ImageResource.hpp"
|
||||||
|
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
namespace UserInterface::ReleaseListHelpers
|
||||||
|
{
|
||||||
|
|
||||||
|
std::unique_ptr<Wt::WTemplate>
|
||||||
|
createEntrySmall(const Database::Release::pointer& release)
|
||||||
|
{
|
||||||
|
auto entry {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Releases.template.entry-small"))};
|
||||||
|
|
||||||
|
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
|
||||||
|
|
||||||
|
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||||
|
auto cover = std::make_unique<Wt::WImage>();
|
||||||
|
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 64));
|
||||||
|
cover->setStyleClass("Lms-cover-smaller");
|
||||||
|
anchor->setImage(std::move(cover));
|
||||||
|
|
||||||
|
auto artists = release->getReleaseArtists();
|
||||||
|
if (artists.empty())
|
||||||
|
artists = release->getArtists();
|
||||||
|
|
||||||
|
if (artists.size() > 1)
|
||||||
|
{
|
||||||
|
entry->setCondition("if-has-artist", true);
|
||||||
|
entry->bindNew<Wt::WText>("artist-name", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||||
|
}
|
||||||
|
else if (artists.size() == 1)
|
||||||
|
{
|
||||||
|
entry->setCondition("if-has-artist", true);
|
||||||
|
entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace UserInterface
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2018 Emeric Poupon
|
* Copyright (C) 2020 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -19,19 +19,19 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <Wt/WTemplate.h>
|
#include <Wt/WTemplate.h>
|
||||||
|
|
||||||
namespace Database {
|
namespace Database
|
||||||
|
{
|
||||||
class Release;
|
class Release;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface::ReleaseListHelpers
|
||||||
|
|
||||||
class ReleaseLink : public Wt::WTemplate
|
|
||||||
{
|
{
|
||||||
public:
|
|
||||||
ReleaseLink(Wt::Dbo::ptr<Database::Release>);
|
std::unique_ptr<Wt::WTemplate> createEntrySmall(const Wt::Dbo::ptr<Database::Release>& release);
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
@@ -28,16 +28,16 @@
|
|||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/ScanSettings.hpp"
|
#include "database/ScanSettings.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
|
#include "recommendation/IEngine.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
#include "resource/ImageResource.hpp"
|
#include "resource/ImageResource.hpp"
|
||||||
|
|
||||||
#include "Filters.hpp"
|
#include "Filters.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
#include "LmsApplicationException.hpp"
|
#include "LmsApplicationException.hpp"
|
||||||
#include "MediaPlayer.hpp"
|
#include "MediaPlayer.hpp"
|
||||||
|
#include "ReleaseListHelpers.hpp"
|
||||||
#include "TrackStringUtils.hpp"
|
#include "TrackStringUtils.hpp"
|
||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
@@ -45,53 +45,60 @@ using namespace Database;
|
|||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
Release::Release(Filters* filters)
|
Release::Release(Filters* filters)
|
||||||
: _filters(filters)
|
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Release.template")}
|
||||||
|
, _filters(filters)
|
||||||
{
|
{
|
||||||
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
wApp->internalPathChanged().connect([=]()
|
wApp->internalPathChanged().connect([=]()
|
||||||
{
|
{
|
||||||
refresh();
|
refreshView();
|
||||||
});
|
});
|
||||||
|
|
||||||
refresh();
|
|
||||||
|
|
||||||
filters->updated().connect([=]
|
filters->updated().connect([=]
|
||||||
{
|
{
|
||||||
refresh();
|
refreshView();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
refreshView();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Release::refresh()
|
Release::refreshView()
|
||||||
{
|
{
|
||||||
if (!wApp->internalPathMatches("/release/"))
|
if (!wApp->internalPathMatches("/release/"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
clear();
|
clear();
|
||||||
auto releaseId {StringUtils::readAs<Database::IdType>(wApp->internalPathNextPart("/release/"))};
|
|
||||||
|
const auto releaseId {StringUtils::readAs<Database::IdType>(wApp->internalPathNextPart("/release/"))};
|
||||||
if (!releaseId)
|
if (!releaseId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const std::vector<Database::IdType> similarReleasesIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 5)};
|
||||||
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), *releaseId)};
|
const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), *releaseId)};
|
||||||
if (!release)
|
if (!release)
|
||||||
throw ReleaseNotFoundException {*releaseId};
|
throw ReleaseNotFoundException {*releaseId};
|
||||||
|
|
||||||
Wt::WTemplate* t {addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template"))};
|
refreshCopyright(release);
|
||||||
|
refreshSimilarReleases(similarReleasesIds);
|
||||||
|
|
||||||
t->bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
|
bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
std::optional<int> year {release->getReleaseYear()};
|
std::optional<int> year {release->getReleaseYear()};
|
||||||
if (year)
|
if (year)
|
||||||
{
|
{
|
||||||
t->setCondition("if-has-year", true);
|
setCondition("if-has-year", true);
|
||||||
t->bindInt("year", *year);
|
bindInt("year", *year);
|
||||||
|
|
||||||
std::optional<int> originalYear {release->getReleaseYear(true)};
|
std::optional<int> originalYear {release->getReleaseYear(true)};
|
||||||
if (originalYear && *originalYear != *year)
|
if (originalYear && *originalYear != *year)
|
||||||
{
|
{
|
||||||
t->setCondition("if-has-orig-year", true);
|
setCondition("if-has-orig-year", true);
|
||||||
t->bindInt("orig-year", *originalYear);
|
bindInt("orig-year", *originalYear);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,22 +111,22 @@ Release::refresh()
|
|||||||
|
|
||||||
if (artists.size() > 1)
|
if (artists.size() > 1)
|
||||||
{
|
{
|
||||||
t->setCondition("if-has-artist", true);
|
setCondition("if-has-artist", true);
|
||||||
t->bindNew<Wt::WText>("artist", Wt::WString::tr("Lms.Explore.various-artists"));
|
bindNew<Wt::WText>("artist", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||||
}
|
}
|
||||||
else if (artists.size() == 1)
|
else if (artists.size() == 1)
|
||||||
{
|
{
|
||||||
t->setCondition("if-has-artist", true);
|
setCondition("if-has-artist", true);
|
||||||
t->bindWidget("artist", LmsApplication::createArtistAnchor(artists.front()));
|
bindWidget("artist", LmsApplication::createArtistAnchor(artists.front()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
Wt::WImage* cover {t->bindNew<Wt::WImage>("cover", Wt::WLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 512)))};
|
Wt::WImage* cover {bindNew<Wt::WImage>("cover", Wt::WLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 512)))};
|
||||||
cover->setStyleClass("Lms-cover-large");
|
cover->setStyleClass("Lms-cover-large");
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WContainerWidget* clusterContainers {t->bindNew<Wt::WContainerWidget>("clusters")};
|
Wt::WContainerWidget* clusterContainers {bindNew<Wt::WContainerWidget>("clusters")};
|
||||||
{
|
{
|
||||||
const auto clusterTypes {ScanSettings::get(LmsApp->getDbSession())->getClusterTypes()};
|
const auto clusterTypes {ScanSettings::get(LmsApp->getDbSession())->getClusterTypes()};
|
||||||
const auto clusterGroups {release->getClusterGroups(clusterTypes, 3)};
|
const auto clusterGroups {release->getClusterGroups(clusterTypes, 3)};
|
||||||
@@ -139,7 +146,7 @@ Release::refresh()
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
Wt::WText* playBtn {t->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
|
Wt::WText* playBtn {bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||||
playBtn->clicked().connect([=]
|
playBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
releasesPlay.emit({*releaseId});
|
releasesPlay.emit({*releaseId});
|
||||||
@@ -147,14 +154,14 @@ Release::refresh()
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
Wt::WText* addBtn {t->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)};
|
Wt::WText* addBtn {bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)};
|
||||||
addBtn->clicked().connect([=]
|
addBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
releasesAdd.emit({*releaseId});
|
releasesAdd.emit({*releaseId});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WContainerWidget* rootContainer {t->bindNew<Wt::WContainerWidget>("container")};
|
Wt::WContainerWidget* rootContainer {bindNew<Wt::WContainerWidget>("container")};
|
||||||
|
|
||||||
const bool variousArtists {release->hasVariousArtists()};
|
const bool variousArtists {release->hasVariousArtists()};
|
||||||
const auto totalDisc {release->getTotalDisc()};
|
const auto totalDisc {release->getTotalDisc()};
|
||||||
@@ -244,5 +251,47 @@ Release::refresh()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Release::refreshCopyright(const Database::Release::pointer& release)
|
||||||
|
{
|
||||||
|
std::optional<std::string> copyright {release->getCopyright()};
|
||||||
|
std::optional<std::string> copyrightURL {release->getCopyrightURL()};
|
||||||
|
|
||||||
|
setCondition("if-has-copyright-or-copyright-url", copyright || copyrightURL);
|
||||||
|
|
||||||
|
if (copyrightURL)
|
||||||
|
{
|
||||||
|
setCondition("if-has-copyright-url", true);
|
||||||
|
|
||||||
|
Wt::WLink link {*copyrightURL};
|
||||||
|
link.setTarget(Wt::LinkTarget::NewWindow);
|
||||||
|
|
||||||
|
Wt::WAnchor* anchor {bindNew<Wt::WAnchor>("copyright-url", link)};
|
||||||
|
anchor->setTextFormat(Wt::TextFormat::XHTML);
|
||||||
|
anchor->setText(Wt::WString::tr("Lms.Explore.Release.template.link-btn"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyright)
|
||||||
|
{
|
||||||
|
setCondition("if-has-copyright", true);
|
||||||
|
bindString("copyright", Wt::WString::fromUTF8(*copyright), Wt::TextFormat::Plain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Release::refreshSimilarReleases(const std::vector<Database::IdType>& similarReleasesId)
|
||||||
|
{
|
||||||
|
auto* similarReleasesContainer {bindNew<Wt::WContainerWidget>("similar-releases")};
|
||||||
|
|
||||||
|
for (Database::IdType id : similarReleasesId)
|
||||||
|
{
|
||||||
|
Database::Release::pointer similarRelease{Database::Release::getById(LmsApp->getDbSession(), id)};
|
||||||
|
if (!similarRelease)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
similarReleasesContainer->addWidget(ReleaseListHelpers::createEntrySmall(similarRelease));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,20 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Wt/WSignal.h>
|
#include <Wt/WSignal.h>
|
||||||
#include <Wt/WContainerWidget.h>
|
#include <Wt/WTemplate.h>
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace Database
|
||||||
|
{
|
||||||
|
class Release;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace UserInterface
|
||||||
|
{
|
||||||
|
|
||||||
class Filters;
|
class Filters;
|
||||||
class Release : public Wt::WContainerWidget
|
class Release : public Wt::WTemplate
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Release(Filters* filters);
|
Release(Filters* filters);
|
||||||
@@ -39,9 +45,11 @@ class Release : public Wt::WContainerWidget
|
|||||||
Wt::Signal<const std::vector<Database::IdType>&> tracksPlay;
|
Wt::Signal<const std::vector<Database::IdType>&> tracksPlay;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refresh();
|
void refreshView();
|
||||||
|
void refreshCopyright(const Wt::Dbo::ptr<Database::Release>& release);
|
||||||
|
void refreshSimilarReleases(const std::vector<Database::IdType>& similarReleasesId);
|
||||||
|
|
||||||
Filters* _filters;
|
Filters* _filters {};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 "ReleasesInfoView.hpp"
|
|
||||||
|
|
||||||
#include <Wt/WLocalDateTime.h>
|
|
||||||
|
|
||||||
#include "database/Release.hpp"
|
|
||||||
#include "database/TrackList.hpp"
|
|
||||||
#include "database/User.hpp"
|
|
||||||
#include "resource/ImageResource.hpp"
|
|
||||||
#include "ReleaseLink.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
#include "MediaPlayer.hpp"
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
ReleasesInfo::ReleasesInfo()
|
|
||||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ReleasesInfo.template"))
|
|
||||||
{
|
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
|
||||||
|
|
||||||
_mostPlayedContainer = bindNew<Wt::WContainerWidget>("most-played");
|
|
||||||
_recentlyAddedContainer = bindNew<Wt::WContainerWidget>("recently-added");
|
|
||||||
|
|
||||||
LmsApp->getEvents().dbScanned.connect([=]
|
|
||||||
{
|
|
||||||
refreshRecentlyAdded();
|
|
||||||
});
|
|
||||||
|
|
||||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
|
|
||||||
{
|
|
||||||
refreshMostPlayed();
|
|
||||||
});
|
|
||||||
|
|
||||||
refreshRecentlyAdded();
|
|
||||||
refreshMostPlayed();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ReleasesInfo::refreshRecentlyAdded()
|
|
||||||
{
|
|
||||||
auto after = Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6);
|
|
||||||
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
|
||||||
|
|
||||||
const auto releases {Release::getLastWritten(LmsApp->getDbSession(), after, 0, 5)};
|
|
||||||
|
|
||||||
_recentlyAddedContainer->clear();
|
|
||||||
for (const Database::Release::pointer& release : releases)
|
|
||||||
_recentlyAddedContainer->addNew<ReleaseLink>(release);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ReleasesInfo::refreshMostPlayed()
|
|
||||||
{
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
|
||||||
|
|
||||||
const auto releases {LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTopReleases(5)};
|
|
||||||
|
|
||||||
_mostPlayedContainer->clear();
|
|
||||||
for (const Database::Release::pointer& release : releases)
|
|
||||||
_mostPlayedContainer->addNew<ReleaseLink>(release);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 <Wt/WContainerWidget.h>
|
|
||||||
#include <Wt/WTemplate.h>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
class ReleasesInfo : public Wt::WTemplate
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
ReleasesInfo();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void refreshRecentlyAdded();
|
|
||||||
void refreshMostPlayed();
|
|
||||||
|
|
||||||
Wt::WContainerWidget* _mostPlayedContainer;
|
|
||||||
Wt::WContainerWidget* _recentlyAddedContainer;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -19,44 +19,63 @@
|
|||||||
|
|
||||||
#include "ReleasesView.hpp"
|
#include "ReleasesView.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include <Wt/WAnchor.h>
|
#include <Wt/WAnchor.h>
|
||||||
#include <Wt/WImage.h>
|
#include <Wt/WImage.h>
|
||||||
|
#include <Wt/WMenu.h>
|
||||||
#include <Wt/WText.h>
|
#include <Wt/WText.h>
|
||||||
#include <Wt/WTemplate.h>
|
|
||||||
|
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
|
#include "database/User.hpp"
|
||||||
|
#include "database/TrackList.hpp"
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
#include "resource/ImageResource.hpp"
|
#include "resource/ImageResource.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
#include "Filters.hpp"
|
#include "Filters.hpp"
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
Releases::Releases(Filters* filters)
|
Releases::Releases(Filters* filters)
|
||||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.Releases.template")),
|
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Releases.template")},
|
||||||
_filters(filters)
|
_filters {filters}
|
||||||
{
|
{
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
_search = bindNew<Wt::WLineEdit>("search");
|
{
|
||||||
_search->setPlaceholderText(Wt::WString::tr("Lms.Explore.search-placeholder"));
|
auto* menu {bindNew<Wt::WMenu>("mode")};
|
||||||
_search->textInput().connect(this, &Releases::refresh);
|
|
||||||
|
auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, Mode mode)
|
||||||
|
{
|
||||||
|
auto* item {menu.addItem(str)};
|
||||||
|
item->clicked().connect([this, mode] { refreshView(mode); });
|
||||||
|
|
||||||
|
if (mode == defaultMode)
|
||||||
|
item->renderSelected(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All);
|
||||||
|
}
|
||||||
|
|
||||||
Wt::WText* playBtn {bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
|
Wt::WText* playBtn {bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||||
playBtn->clicked().connect([this]
|
playBtn->clicked().connect([this]
|
||||||
{
|
{
|
||||||
releasesPlay.emit(getReleases());
|
releasesPlay.emit(getAllReleases());
|
||||||
});
|
});
|
||||||
Wt::WText* addBtn {bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)};
|
Wt::WText* addBtn {bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)};
|
||||||
addBtn->clicked().connect([this]
|
addBtn->clicked().connect([this]
|
||||||
{
|
{
|
||||||
releasesAdd.emit(getReleases());
|
releasesAdd.emit(getAllReleases());
|
||||||
});
|
});
|
||||||
|
|
||||||
_container = bindNew<Wt::WContainerWidget>("releases");
|
_container = bindNew<Wt::WContainerWidget>("releases");
|
||||||
@@ -67,37 +86,37 @@ _filters(filters)
|
|||||||
addSome();
|
addSome();
|
||||||
});
|
});
|
||||||
|
|
||||||
refresh();
|
refreshView(defaultMode);
|
||||||
|
|
||||||
filters->updated().connect(this, &Releases::refresh);
|
filters->updated().connect([this] { refreshView(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Releases::refresh()
|
Releases::refreshView()
|
||||||
{
|
{
|
||||||
_container->clear();
|
_container->clear();
|
||||||
|
_randomReleases.clear();
|
||||||
addSome();
|
addSome();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Releases::addSome()
|
Releases::refreshView(Mode mode)
|
||||||
{
|
{
|
||||||
bool moreResults {};
|
_mode = mode;
|
||||||
|
refreshView();
|
||||||
|
}
|
||||||
|
|
||||||
const auto releasesId {getReleases(_container->count(), 20, moreResults)};
|
std::unique_ptr<Wt::WTemplate>
|
||||||
for (const Database::IdType releaseId : releasesId )
|
Releases::createEntry(const Release::pointer& release)
|
||||||
{
|
{
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
const IdType releaseId {release.id()};
|
||||||
|
|
||||||
const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), releaseId)};
|
auto entry = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Releases.template.entry"));
|
||||||
|
|
||||||
Wt::WTemplate* entry = _container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Releases.template.entry"));
|
|
||||||
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
|
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||||
auto cover = std::make_unique<Wt::WImage>();
|
auto cover = std::make_unique<Wt::WImage>();
|
||||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
||||||
// Some images may not be square
|
|
||||||
cover->setStyleClass("Lms-cover-medium");
|
cover->setStyleClass("Lms-cover-medium");
|
||||||
anchor->setImage(std::move(cover));
|
anchor->setImage(std::move(cover));
|
||||||
|
|
||||||
@@ -129,33 +148,107 @@ Releases::addSome()
|
|||||||
{
|
{
|
||||||
releasesAdd.emit({releaseId});
|
releasesAdd.emit({releaseId});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Releases::addSome()
|
||||||
|
{
|
||||||
|
bool moreResults {};
|
||||||
|
|
||||||
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
|
for (const Release::pointer& release : getReleases(Range {static_cast<std::size_t>(_container->count()), batchSize}, moreResults))
|
||||||
|
{
|
||||||
|
_container->addWidget(createEntry(release));
|
||||||
}
|
}
|
||||||
|
|
||||||
_showMore->setHidden(!moreResults);
|
_showMore->setHidden(!moreResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Database::IdType>
|
std::vector<Database::Release::pointer>
|
||||||
Releases::getReleases(std::optional<std::size_t> offset, std::optional<std::size_t> limit, bool& moreResults) const
|
Releases::getRandomReleases(std::optional<Range> range, bool& moreResults)
|
||||||
{
|
{
|
||||||
const auto searchKeywords {StringUtils::splitString(_search->text().toUTF8(), " ")};
|
std::vector<Release::pointer> releases;
|
||||||
|
|
||||||
|
if (_randomReleases.empty())
|
||||||
|
_randomReleases = Release::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), maxItemsPerMode[Mode::Random]);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto itBegin {std::cbegin(_randomReleases) + std::min(range ? range->offset : 0, _randomReleases.size())};
|
||||||
|
auto itEnd {std::cbegin(_randomReleases) + std::min(range ? range->offset + range->limit : _randomReleases.size(), _randomReleases.size())};
|
||||||
|
|
||||||
|
for (auto it {itBegin}; it != itEnd; ++it)
|
||||||
|
{
|
||||||
|
Release::pointer release {Release::getById(LmsApp->getDbSession(), *it)};
|
||||||
|
if (release)
|
||||||
|
releases.push_back(release);
|
||||||
|
}
|
||||||
|
|
||||||
|
moreResults = (itEnd != std::cend(_randomReleases));
|
||||||
|
}
|
||||||
|
|
||||||
|
return releases;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Database::Release::pointer>
|
||||||
|
Releases::getReleases(std::optional<Range> range, bool& moreResults)
|
||||||
|
{
|
||||||
|
std::vector<Release::pointer> releases;
|
||||||
|
|
||||||
|
const std::optional<std::size_t> modeLimit{maxItemsPerMode[_mode]};
|
||||||
|
if (modeLimit)
|
||||||
|
{
|
||||||
|
if (range)
|
||||||
|
range->limit = std::min(*modeLimit - range->offset, range->offset + range->limit);
|
||||||
|
else
|
||||||
|
range = Range {0, *modeLimit};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_mode)
|
||||||
|
{
|
||||||
|
case Mode::Random:
|
||||||
|
releases = getRandomReleases(range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::RecentlyPlayed:
|
||||||
|
releases = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getReleasesReverse(_filters->getClusterIds(), range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::MostPlayed:
|
||||||
|
releases = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTopReleases(_filters->getClusterIds(), range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::RecentlyAdded:
|
||||||
|
releases = Release::getLastWritten(LmsApp->getDbSession(), std::nullopt, _filters->getClusterIds(), range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::All:
|
||||||
|
releases = Release::getByFilter(LmsApp->getDbSession(), _filters->getClusterIds(), {}, range, moreResults);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range && modeLimit && (range->offset + range->limit == *modeLimit))
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return releases;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Database::IdType>
|
||||||
|
Releases::getAllReleases()
|
||||||
|
{
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
const auto releases {Release::getByFilter(LmsApp->getDbSession(), _filters->getClusterIds(), searchKeywords, offset, limit, moreResults)};
|
bool moreResults;
|
||||||
|
const auto releases {getReleases(std::nullopt, moreResults)};
|
||||||
|
|
||||||
std::vector<Database::IdType> res;
|
std::vector<IdType> res;
|
||||||
for (const Database::Release::pointer& release : releases)
|
res.reserve(releases.size());
|
||||||
res.push_back(release.id());
|
std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const Release::pointer& release) { return release.id(); });
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Database::IdType>
|
|
||||||
Releases::getReleases() const
|
|
||||||
{
|
|
||||||
bool moreResults;
|
|
||||||
return getReleases({}, {}, moreResults);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,16 @@
|
|||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#include <Wt/WContainerWidget.h>
|
#include <Wt/WContainerWidget.h>
|
||||||
#include <Wt/WLineEdit.h>
|
|
||||||
#include <Wt/WPushButton.h>
|
#include <Wt/WPushButton.h>
|
||||||
#include <Wt/WTemplate.h>
|
#include <Wt/WTemplate.h>
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Release;
|
||||||
|
}
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
class Filters;
|
class Filters;
|
||||||
@@ -41,16 +45,40 @@ class Releases : public Wt::WTemplate
|
|||||||
Wt::Signal<const std::vector<Database::IdType>&> releasesPlay;
|
Wt::Signal<const std::vector<Database::IdType>&> releasesPlay;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refresh();
|
|
||||||
|
enum class Mode
|
||||||
|
{
|
||||||
|
Random,
|
||||||
|
RecentlyPlayed,
|
||||||
|
RecentlyAdded,
|
||||||
|
MostPlayed,
|
||||||
|
All
|
||||||
|
};
|
||||||
|
|
||||||
|
void refreshView();
|
||||||
|
void refreshView(Mode mode);
|
||||||
void addSome();
|
void addSome();
|
||||||
|
std::vector<Wt::Dbo::ptr<Database::Release>> getReleases(std::optional<Database::Range> range, bool& moreResults);
|
||||||
|
std::vector<Wt::Dbo::ptr<Database::Release>> getRandomReleases(std::optional<Database::Range> range, bool& moreResults);
|
||||||
|
std::vector<Database::IdType> getAllReleases();
|
||||||
|
std::unique_ptr<Wt::WTemplate> createEntry(const Wt::Dbo::ptr<Database::Release>& release);
|
||||||
|
|
||||||
std::vector<Database::IdType> getReleases(std::optional<std::size_t> offset, std::optional<std::size_t> limit, bool& moreResults) const;
|
static constexpr Mode defaultMode {Mode::Random};
|
||||||
std::vector<Database::IdType> getReleases() const;
|
static constexpr std::size_t batchSize {20};
|
||||||
|
static inline std::unordered_map<Mode, std::optional<std::size_t>> maxItemsPerMode
|
||||||
|
{
|
||||||
|
{Mode::Random, batchSize * 6},
|
||||||
|
{Mode::RecentlyPlayed, batchSize * 3},
|
||||||
|
{Mode::RecentlyAdded, batchSize * 2},
|
||||||
|
{Mode::MostPlayed, batchSize * 2},
|
||||||
|
{Mode::All, std::nullopt},
|
||||||
|
};
|
||||||
|
|
||||||
Filters* _filters;
|
Mode _mode {defaultMode};
|
||||||
Wt::WPushButton* _showMore;
|
Filters* _filters {};
|
||||||
Wt::WLineEdit* _search;
|
std::vector<Database::IdType> _randomReleases;
|
||||||
Wt::WContainerWidget* _container;
|
Wt::WContainerWidget* _container {};
|
||||||
|
Wt::WPushButton* _showMore {};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* 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 "SearchView.hpp"
|
||||||
|
|
||||||
|
#include <Wt/WAnchor.h>
|
||||||
|
#include <Wt/WImage.h>
|
||||||
|
|
||||||
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Release.hpp"
|
||||||
|
#include "database/Track.hpp"
|
||||||
|
|
||||||
|
#include "resource/ImageResource.hpp"
|
||||||
|
#include "ArtistListHelpers.hpp"
|
||||||
|
#include "Filters.hpp"
|
||||||
|
#include "ReleaseListHelpers.hpp"
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
|
static constexpr std::size_t maxEntries {5};
|
||||||
|
|
||||||
|
namespace UserInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
static
|
||||||
|
std::unique_ptr<Wt::WTemplate>
|
||||||
|
createTrack(const Database::Track::pointer& track)
|
||||||
|
{
|
||||||
|
auto entry {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Search.template.track-entry"))};
|
||||||
|
|
||||||
|
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
|
auto artists {track->getArtists()};
|
||||||
|
auto release {track->getRelease()};
|
||||||
|
const auto trackId {track.id()};
|
||||||
|
|
||||||
|
if (!artists.empty() || release)
|
||||||
|
entry->setCondition("if-has-artists-or-release", true);
|
||||||
|
|
||||||
|
if (!artists.empty())
|
||||||
|
{
|
||||||
|
entry->setCondition("if-has-artists", true);
|
||||||
|
|
||||||
|
Wt::WContainerWidget* artistContainer {entry->bindNew<Wt::WContainerWidget>("artists")};
|
||||||
|
for (const auto& artist : artists)
|
||||||
|
{
|
||||||
|
Wt::WTemplate* a {artistContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Tracks.template.entry-artist"))};
|
||||||
|
a->bindWidget("artist", LmsApplication::createArtistAnchor(artist));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track->getRelease())
|
||||||
|
{
|
||||||
|
entry->setCondition("if-has-release", true);
|
||||||
|
entry->bindWidget("release", LmsApplication::createReleaseAnchor(track->getRelease()));
|
||||||
|
{
|
||||||
|
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||||
|
auto cover = std::make_unique<Wt::WImage>();
|
||||||
|
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
||||||
|
cover->setStyleClass("Lms-cover-small");
|
||||||
|
anchor->setImage(std::move(cover));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||||
|
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, 96));
|
||||||
|
cover->setStyleClass("Lms-cover-small");
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchView::SearchView(Filters* filters)
|
||||||
|
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")}
|
||||||
|
, _filters {filters}
|
||||||
|
{
|
||||||
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
|
_filters->updated().connect([=]
|
||||||
|
{
|
||||||
|
refreshView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SearchView::refreshView(const std::string& searchText)
|
||||||
|
{
|
||||||
|
_keywords = StringUtils::splitString(searchText, " ");
|
||||||
|
refreshView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SearchView::refreshView()
|
||||||
|
{
|
||||||
|
clear();
|
||||||
|
|
||||||
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
|
searchArtists();
|
||||||
|
searchReleases();
|
||||||
|
searchTracks();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SearchView::searchArtists()
|
||||||
|
{
|
||||||
|
bool more;
|
||||||
|
const auto artists {Database::Artist::getByFilter(LmsApp->getDbSession(),
|
||||||
|
_filters->getClusterIds(),
|
||||||
|
_keywords,
|
||||||
|
std::nullopt,
|
||||||
|
Database::Artist::SortMethod::BySortName,
|
||||||
|
Database::Range {0, maxEntries}, more)};
|
||||||
|
|
||||||
|
if (!artists.empty())
|
||||||
|
{
|
||||||
|
setCondition("if-artists", true);
|
||||||
|
|
||||||
|
auto* container {bindNew<Wt::WContainerWidget>("artists")};
|
||||||
|
|
||||||
|
for (const Database::Artist::pointer& artist : artists)
|
||||||
|
container->addWidget(ArtistListHelpers::createEntrySmall(artist));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SearchView::searchReleases()
|
||||||
|
{
|
||||||
|
bool more;
|
||||||
|
const auto releases {Database::Release::getByFilter(LmsApp->getDbSession(),
|
||||||
|
_filters->getClusterIds(),
|
||||||
|
_keywords,
|
||||||
|
Database::Range {0, maxEntries}, more)};
|
||||||
|
|
||||||
|
if (!releases.empty())
|
||||||
|
{
|
||||||
|
setCondition("if-releases", true);
|
||||||
|
|
||||||
|
auto* container {bindNew<Wt::WContainerWidget>("releases")};
|
||||||
|
|
||||||
|
for (const Database::Release::pointer& release : releases)
|
||||||
|
container->addWidget(ReleaseListHelpers::createEntrySmall(release));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SearchView::searchTracks()
|
||||||
|
{
|
||||||
|
bool more;
|
||||||
|
const auto tracks {Database::Track::getByFilter(LmsApp->getDbSession(),
|
||||||
|
_filters->getClusterIds(),
|
||||||
|
_keywords,
|
||||||
|
Database::Range {0, maxEntries}, more)};
|
||||||
|
|
||||||
|
if (!tracks.empty())
|
||||||
|
{
|
||||||
|
setCondition("if-tracks", true);
|
||||||
|
|
||||||
|
auto* container {bindNew<Wt::WContainerWidget>("tracks")};
|
||||||
|
|
||||||
|
for (const Database::Track::pointer& track : tracks)
|
||||||
|
container->addWidget(createTrack(track));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace UserInterface
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2018 Emeric Poupon
|
* Copyright (C) 2020 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -19,23 +19,30 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Wt/WContainerWidget.h>
|
#include <string>
|
||||||
#include <Wt/WPushButton.h>
|
#include <vector>
|
||||||
|
|
||||||
#include <Wt/WTemplate.h>
|
#include <Wt/WTemplate.h>
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
class PlayHistory : public Wt::WTemplate
|
class Filters;
|
||||||
|
|
||||||
|
class SearchView : public Wt::WTemplate
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PlayHistory();
|
SearchView(Filters* filters);
|
||||||
|
|
||||||
|
void refreshView(const std::string& searchText);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void refreshView();
|
||||||
|
void searchArtists();
|
||||||
|
void searchReleases();
|
||||||
|
void searchTracks();
|
||||||
|
|
||||||
void addSome();
|
Filters* _filters {};
|
||||||
|
std::vector<std::string> _keywords;
|
||||||
Wt::WContainerWidget* _entriesContainer;
|
|
||||||
Wt::WPushButton* _showMore;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 "TracksInfoView.hpp"
|
|
||||||
|
|
||||||
#include <Wt/WAnchor.h>
|
|
||||||
#include <Wt/WLocalDateTime.h>
|
|
||||||
|
|
||||||
#include "database/Track.hpp"
|
|
||||||
#include "database/TrackList.hpp"
|
|
||||||
#include "database/User.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
#include "MediaPlayer.hpp"
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
using namespace UserInterface;
|
|
||||||
|
|
||||||
void addEntries(Wt::WContainerWidget *container, const std::vector<Track::pointer>& tracks)
|
|
||||||
{
|
|
||||||
for (auto track : tracks)
|
|
||||||
{
|
|
||||||
Wt::WTemplate* entry = container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.TracksInfo.template.entry"));
|
|
||||||
|
|
||||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
TracksInfo::TracksInfo()
|
|
||||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.TracksInfo.template")}
|
|
||||||
{
|
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
|
||||||
|
|
||||||
_mostPlayedContainer = bindNew<Wt::WContainerWidget>("most-played");
|
|
||||||
_recentlyAddedContainer = bindNew<Wt::WContainerWidget>("recently-added");
|
|
||||||
|
|
||||||
LmsApp->getEvents().dbScanned.connect([=]
|
|
||||||
{
|
|
||||||
refreshRecentlyAdded();
|
|
||||||
});
|
|
||||||
|
|
||||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
|
|
||||||
{
|
|
||||||
refreshMostPlayed();
|
|
||||||
});
|
|
||||||
|
|
||||||
refreshMostPlayed();
|
|
||||||
refreshRecentlyAdded();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TracksInfo::refreshRecentlyAdded()
|
|
||||||
{
|
|
||||||
const auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6)};
|
|
||||||
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
|
||||||
const auto tracks {Track::getLastWritten(LmsApp->getDbSession(), after, 5)};
|
|
||||||
|
|
||||||
_recentlyAddedContainer->clear();
|
|
||||||
addEntries(_recentlyAddedContainer, tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TracksInfo::refreshMostPlayed()
|
|
||||||
{
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
|
||||||
const auto tracks {LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTopTracks(5)};
|
|
||||||
|
|
||||||
_mostPlayedContainer->clear();
|
|
||||||
addEntries(_mostPlayedContainer, tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 <Wt/WContainerWidget.h>
|
|
||||||
#include <Wt/WTemplate.h>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
class TracksInfo : public Wt::WTemplate
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
TracksInfo();
|
|
||||||
|
|
||||||
void refreshRecentlyAdded();
|
|
||||||
void refreshMostPlayed();
|
|
||||||
|
|
||||||
private:
|
|
||||||
Wt::WContainerWidget* _mostPlayedContainer;
|
|
||||||
Wt::WContainerWidget* _recentlyAddedContainer;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -20,12 +20,14 @@
|
|||||||
#include "TracksView.hpp"
|
#include "TracksView.hpp"
|
||||||
|
|
||||||
#include <Wt/WAnchor.h>
|
#include <Wt/WAnchor.h>
|
||||||
|
#include <Wt/WMenu.h>
|
||||||
#include <Wt/WLineEdit.h>
|
#include <Wt/WLineEdit.h>
|
||||||
#include <Wt/WText.h>
|
#include <Wt/WText.h>
|
||||||
|
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
|
#include "database/TrackList.hpp"
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
@@ -47,84 +49,162 @@ _filters {filters}
|
|||||||
{
|
{
|
||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
_search = bindNew<Wt::WLineEdit>("search");
|
{
|
||||||
_search->setPlaceholderText(Wt::WString::tr("Lms.Explore.search-placeholder"));
|
auto* menu {bindNew<Wt::WMenu>("mode")};
|
||||||
_search->textInput().connect(this, &Tracks::refresh);
|
|
||||||
|
auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, Mode mode)
|
||||||
|
{
|
||||||
|
auto* item {menu.addItem(str)};
|
||||||
|
item->clicked().connect([this, mode] { refreshView(mode); });
|
||||||
|
|
||||||
|
if (mode == defaultMode)
|
||||||
|
item->renderSelected(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
|
||||||
|
addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All);
|
||||||
|
}
|
||||||
|
|
||||||
Wt::WText* playBtn = bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
Wt::WText* playBtn = bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||||
playBtn->clicked().connect([=]
|
playBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
tracksPlay.emit(getTracks());
|
tracksPlay.emit(getAllTracks());
|
||||||
});
|
});
|
||||||
|
|
||||||
Wt::WText* addBtn = bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
Wt::WText* addBtn = bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||||
addBtn->clicked().connect([=]
|
addBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
tracksAdd.emit(getTracks());
|
tracksAdd.emit(getAllTracks());
|
||||||
});
|
});
|
||||||
|
|
||||||
_tracksContainer = bindNew<Wt::WContainerWidget>("tracks");
|
_tracksContainer = bindNew<Wt::WContainerWidget>("tracks");
|
||||||
|
|
||||||
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
|
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
|
||||||
_showMore->clicked().connect([=]
|
_showMore->clicked().connect([this]
|
||||||
{
|
{
|
||||||
addSome();
|
addSome();
|
||||||
});
|
});
|
||||||
|
|
||||||
refresh();
|
filters->updated().connect([this]
|
||||||
|
{
|
||||||
|
refreshView();
|
||||||
|
});
|
||||||
|
|
||||||
filters->updated().connect(this, &Tracks::refresh);
|
refreshView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Tracks::refreshView()
|
||||||
|
{
|
||||||
|
_tracksContainer->clear();
|
||||||
|
_randomTracks.clear();
|
||||||
|
addSome();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Tracks::refreshView(Mode mode)
|
||||||
|
{
|
||||||
|
_mode = mode;
|
||||||
|
refreshView();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Database::Track::pointer>
|
||||||
|
Tracks::getRandomTracks(std::optional<Range> range, bool& moreResults)
|
||||||
|
{
|
||||||
|
std::vector<Track::pointer> tracks;
|
||||||
|
|
||||||
|
if (_randomTracks.empty())
|
||||||
|
_randomTracks = Track::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), maxItemsPerMode[Mode::Random]);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto itBegin {std::cbegin(_randomTracks) + std::min(range ? range->offset : 0, _randomTracks.size())};
|
||||||
|
auto itEnd {std::cbegin(_randomTracks) + std::min(range ? range->offset + range->limit : _randomTracks.size(), _randomTracks.size())};
|
||||||
|
|
||||||
|
for (auto it {itBegin}; it != itEnd; ++it)
|
||||||
|
{
|
||||||
|
const Track::pointer track {Track::getById(LmsApp->getDbSession(), *it)};
|
||||||
|
if (track)
|
||||||
|
tracks.push_back(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
moreResults = (itEnd != std::cend(_randomTracks));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Track::pointer>
|
||||||
|
Tracks::getTracks(std::optional<Range> range, bool& moreResults)
|
||||||
|
{
|
||||||
|
std::vector<Track::pointer> tracks;
|
||||||
|
|
||||||
|
const std::optional<std::size_t> modeLimit{maxItemsPerMode[_mode]};
|
||||||
|
if (modeLimit)
|
||||||
|
{
|
||||||
|
if (range)
|
||||||
|
range->limit = std::min(*modeLimit - range->offset, range->offset + range->limit);
|
||||||
|
else
|
||||||
|
range = Range {0, *modeLimit};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_mode)
|
||||||
|
{
|
||||||
|
case Mode::Random:
|
||||||
|
tracks = getRandomTracks(range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::RecentlyPlayed:
|
||||||
|
tracks = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTracksReverse(_filters->getClusterIds(), range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::MostPlayed:
|
||||||
|
tracks = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTopTracks(_filters->getClusterIds(), range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::RecentlyAdded:
|
||||||
|
tracks = Track::getLastWritten(LmsApp->getDbSession(), std::nullopt, _filters->getClusterIds(), range, moreResults);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mode::All:
|
||||||
|
tracks = Track::getByFilter(LmsApp->getDbSession(), _filters->getClusterIds(), {}, range, moreResults);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range && modeLimit && (range->offset + range->limit == *modeLimit))
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Database::IdType>
|
std::vector<Database::IdType>
|
||||||
Tracks::getTracks(std::optional<std::size_t> offset, std::optional<std::size_t> size, bool& moreResults)
|
Tracks::getAllTracks()
|
||||||
{
|
{
|
||||||
const auto searchKeywords {StringUtils::splitString(_search->text().toUTF8(), " ")};
|
|
||||||
const auto clusterIds {_filters->getClusterIds()};
|
|
||||||
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
const auto tracks {Track::getByFilter(LmsApp->getDbSession(), clusterIds, searchKeywords, offset, size, moreResults)};
|
bool moreResults;
|
||||||
|
const auto tracks {getTracks(std::nullopt, moreResults)};
|
||||||
|
|
||||||
std::vector<Database::IdType> res;
|
std::vector<Database::IdType> res;
|
||||||
res.reserve(tracks.size());
|
res.reserve(tracks.size());
|
||||||
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const Database::Track::pointer& track) { return track.id(); });
|
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const Database::Track::pointer& track) { return track.id(); });
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Database::IdType>
|
std::unique_ptr<Wt::WTemplate>
|
||||||
Tracks::getTracks()
|
Tracks::createEntry(const Track::pointer& track)
|
||||||
{
|
{
|
||||||
bool moreResults;
|
auto entry {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))};
|
||||||
return getTracks({}, {}, moreResults);
|
auto* entryPtr {entry.get()};
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Tracks::refresh()
|
|
||||||
{
|
|
||||||
_tracksContainer->clear();
|
|
||||||
addSome();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Tracks::addSome()
|
|
||||||
{
|
|
||||||
bool moreResults;
|
|
||||||
const std::vector<Database::IdType> trackIds {getTracks(_tracksContainer->count(), 20, moreResults)};
|
|
||||||
|
|
||||||
for (const Database::IdType trackId : trackIds)
|
|
||||||
{
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
|
||||||
|
|
||||||
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
|
||||||
|
|
||||||
Wt::WTemplate* entry {_tracksContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))};
|
|
||||||
|
|
||||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
auto artists {track->getArtists()};
|
auto artists {track->getArtists()};
|
||||||
auto release {track->getRelease()};
|
auto release {track->getRelease()};
|
||||||
|
const auto trackId {track.id()};
|
||||||
|
|
||||||
if (!artists.empty() || release)
|
if (!artists.empty() || release)
|
||||||
entry->setCondition("if-has-artists-or-release", true);
|
entry->setCondition("if-has-artists-or-release", true);
|
||||||
@@ -163,21 +243,20 @@ Tracks::addSome()
|
|||||||
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
|
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
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([=]
|
playBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
tracksPlay.emit({trackId});
|
tracksPlay.emit({trackId});
|
||||||
}));
|
});
|
||||||
|
|
||||||
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||||
addBtn->clicked().connect(std::bind([=]
|
addBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
tracksAdd.emit({trackId});
|
tracksAdd.emit({trackId});
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
LmsApp->getMediaPlayer()->trackLoaded.connect(entryPtr, [=] (Database::IdType loadedTrackId)
|
||||||
LmsApp->getMediaPlayer()->trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId)
|
|
||||||
{
|
{
|
||||||
entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
|
entryPtr->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
|
||||||
});
|
});
|
||||||
|
|
||||||
if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
|
if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
|
||||||
@@ -185,6 +264,19 @@ Tracks::addSome()
|
|||||||
if (*trackIdLoaded == trackId)
|
if (*trackIdLoaded == trackId)
|
||||||
entry->bindString("is-playing", "Lms-entry-playing");
|
entry->bindString("is-playing", "Lms-entry-playing");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Tracks::addSome()
|
||||||
|
{
|
||||||
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults;
|
||||||
|
for (const Track::pointer& track : getTracks(Range {static_cast<std::size_t>(_tracksContainer->count()), batchSize}, moreResults))
|
||||||
|
{
|
||||||
|
_tracksContainer->addWidget(createEntry(track));
|
||||||
}
|
}
|
||||||
|
|
||||||
_showMore->setHidden(!moreResults);
|
_showMore->setHidden(!moreResults);
|
||||||
|
|||||||
@@ -28,6 +28,11 @@
|
|||||||
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Track;
|
||||||
|
}
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
class Filters;
|
class Filters;
|
||||||
@@ -40,16 +45,42 @@ class Tracks : public Wt::WTemplate
|
|||||||
Wt::Signal<const std::vector<Database::IdType>&> tracksPlay;
|
Wt::Signal<const std::vector<Database::IdType>&> tracksPlay;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refresh();
|
|
||||||
|
enum class Mode
|
||||||
|
{
|
||||||
|
Random,
|
||||||
|
RecentlyPlayed,
|
||||||
|
RecentlyAdded,
|
||||||
|
MostPlayed,
|
||||||
|
All
|
||||||
|
};
|
||||||
|
|
||||||
|
void refreshView();
|
||||||
|
void refreshView(Mode mode);
|
||||||
void addSome();
|
void addSome();
|
||||||
|
|
||||||
std::vector<Database::IdType> getTracks(std::optional<std::size_t> offset, std::optional<std::size_t> size, bool& moreResults);
|
std::unique_ptr<Wt::WTemplate> createEntry(const Wt::Dbo::ptr<Database::Track>& track);
|
||||||
std::vector<Database::IdType> getTracks();
|
std::vector<Wt::Dbo::ptr<Database::Track>> getRandomTracks(std::optional<Database::Range> range, bool& moreResults);
|
||||||
|
std::vector<Wt::Dbo::ptr<Database::Track>> getTracks(std::optional<Database::Range> range, bool& moreResults);
|
||||||
|
std::vector<Database::IdType> getAllTracks();
|
||||||
|
|
||||||
Wt::WContainerWidget* _tracksContainer;
|
static constexpr Mode defaultMode {Mode::Random};
|
||||||
Wt::WPushButton* _showMore;
|
static constexpr std::size_t batchSize {20};
|
||||||
Wt::WLineEdit* _search;
|
static inline std::unordered_map<Mode, std::optional<std::size_t>> maxItemsPerMode
|
||||||
Filters* _filters;
|
{
|
||||||
|
{Mode::Random, batchSize * 20},
|
||||||
|
{Mode::RecentlyPlayed, batchSize * 10},
|
||||||
|
{Mode::RecentlyAdded, batchSize * 10},
|
||||||
|
{Mode::MostPlayed, batchSize * 10},
|
||||||
|
{Mode::All, std::nullopt},
|
||||||
|
};
|
||||||
|
|
||||||
|
Mode _mode {defaultMode};
|
||||||
|
Filters* _filters {};
|
||||||
|
std::vector<Database::IdType> _randomTracks;
|
||||||
|
Wt::WContainerWidget* _tracksContainer {};
|
||||||
|
Wt::WPushButton* _showMore {};
|
||||||
|
Wt::WLineEdit* _search {};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ using namespace Database;
|
|||||||
throw std::runtime_error {error}; \
|
throw std::runtime_error {error}; \
|
||||||
} \
|
} \
|
||||||
} \
|
} \
|
||||||
|
catch (Wt::Dbo::Exception& e) \
|
||||||
|
{ \
|
||||||
|
std::cerr << "Caught DBO exception: " <<e.code() << std::endl; \
|
||||||
|
throw; \
|
||||||
|
} \
|
||||||
catch (std::exception& e) \
|
catch (std::exception& e) \
|
||||||
{ \
|
{ \
|
||||||
std::cerr << "Exception caught: " << e.what() << std::endl; \
|
std::cerr << "Exception caught: " << e.what() << std::endl; \
|
||||||
@@ -318,11 +323,11 @@ testSingleTrackSingleArtistMultiRoles(Session& session)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
bool hasMore{};
|
bool hasMore{};
|
||||||
CHECK(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
CHECK(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
|
||||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Artist, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Artist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
|
||||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::ReleaseArtist, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::ReleaseArtist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
|
||||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Writer, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Writer, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
|
||||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Composer, Artist::SortMethod::ByName, {}, {}, hasMore).empty());
|
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Composer, Artist::SortMethod::ByName, std::nullopt, hasMore).empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -398,23 +403,25 @@ void
|
|||||||
testSingleArtistSearchByName(Session& session)
|
testSingleArtistSearchByName(Session& session)
|
||||||
{
|
{
|
||||||
ScopedArtist artist {session, "AAA"};
|
ScopedArtist artist {session, "AAA"};
|
||||||
|
ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
artist.get().modify()->setSortName("ZZZ");
|
artist.get().modify()->setSortName("ZZZ");
|
||||||
|
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool more {};
|
bool more {};
|
||||||
CHECK(Artist::getByFilter(session, {}, {"N"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, std::nullopt, more).empty());
|
CHECK(Artist::getByFilter(session, {}, {"N"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more).empty());
|
||||||
|
|
||||||
const auto artistsByAAA {Artist::Artist::getByFilter(session, {}, {"A"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, std::nullopt, more)};
|
const auto artistsByAAA {Artist::Artist::getByFilter(session, {}, {"A"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
|
||||||
CHECK(artistsByAAA.size() == 1);
|
CHECK(artistsByAAA.size() == 1);
|
||||||
CHECK(artistsByAAA.front().id() == artist.getId());
|
CHECK(artistsByAAA.front().id() == artist.getId());
|
||||||
|
|
||||||
const auto artistsByZZZ {Artist::Artist::getByFilter(session, {}, {"Z"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, std::nullopt, more)};
|
const auto artistsByZZZ {Artist::Artist::getByFilter(session, {}, {"Z"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
|
||||||
CHECK(artistsByZZZ.size() == 1);
|
CHECK(artistsByZZZ.size() == 1);
|
||||||
CHECK(artistsByZZZ.front().id() == artist.getId());
|
CHECK(artistsByZZZ.front().id() == artist.getId());
|
||||||
|
|
||||||
@@ -660,7 +667,7 @@ testMultipleTracksSingleCluster(Session& session)
|
|||||||
{
|
{
|
||||||
std::list<ScopedTrack> tracks;
|
std::list<ScopedTrack> tracks;
|
||||||
ScopedClusterType clusterType {session, "MyClusterType"};
|
ScopedClusterType clusterType {session, "MyClusterType"};
|
||||||
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyClusterType"};
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
|
||||||
for (std::size_t i {}; i < 10; ++i)
|
for (std::size_t i {}; i < 10; ++i)
|
||||||
{
|
{
|
||||||
@@ -686,6 +693,99 @@ testMultipleTracksSingleCluster(Session& session)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
void
|
||||||
|
testMultipleTracksMultipleClustersTopRelease(Session& session)
|
||||||
|
{
|
||||||
|
ScopedClusterType clusterType {session, "ClusterType"};
|
||||||
|
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "Cluster1"};
|
||||||
|
ScopedCluster cluster2 {session, clusterType.lockAndGet(), "Cluster2"};
|
||||||
|
ScopedCluster cluster3 {session, clusterType.lockAndGet(), "Cluster3"};
|
||||||
|
ScopedTrack trackA {session, "TrackA"};
|
||||||
|
ScopedTrack trackB {session, "TrackB"};
|
||||||
|
ScopedTrack trackC {session, "TrackC"};
|
||||||
|
ScopedRelease releaseA {session, "ReleaseA"};
|
||||||
|
ScopedRelease releaseB {session, "ReleaseB"};
|
||||||
|
ScopedRelease releaseC {session, "ReleaseC"};
|
||||||
|
|
||||||
|
ScopedUser user {session, "MyUser", User::PasswordHash {}};
|
||||||
|
ScopedTrackList trackList {session, "TrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
cluster1.get().modify()->addTrack(trackA.get());
|
||||||
|
cluster2.get().modify()->addTrack(trackB.get());
|
||||||
|
cluster2.get().modify()->addTrack(trackC.get());
|
||||||
|
cluster3.get().modify()->addTrack(trackC.get());
|
||||||
|
|
||||||
|
trackA.get().modify()->setRelease(releaseA.get());
|
||||||
|
trackB.get().modify()->setRelease(releaseB.get());
|
||||||
|
trackC.get().modify()->setRelease(releaseC.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
TrackListEntry::create(session, trackA.get(), trackList.get());
|
||||||
|
TrackListEntry::create(session, trackB.get(), trackList.get());
|
||||||
|
TrackListEntry::create(session, trackB.get(), trackList.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool hasMore;
|
||||||
|
const auto releases{trackList->getTopReleases({}, std::nullopt, hasMore)};
|
||||||
|
CHECK(releases.size() == 2);
|
||||||
|
CHECK(releases[0].id() == releaseB.getId());
|
||||||
|
CHECK(releases[1].id() == releaseA.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool hasMore;
|
||||||
|
auto releases{trackList->getTopReleases({cluster1.getId()}, std::nullopt, hasMore)};
|
||||||
|
CHECK(releases.size() == 1);
|
||||||
|
CHECK(releases[0].id() == releaseA.getId());
|
||||||
|
|
||||||
|
releases = trackList->getTopReleases({cluster2.getId()}, std::nullopt, hasMore);
|
||||||
|
CHECK(releases.size() == 1);
|
||||||
|
CHECK(releases[0].id() == releaseB.getId());
|
||||||
|
|
||||||
|
releases = trackList->getTopReleases({cluster2.getId(), cluster1.getId()}, std::nullopt, hasMore);
|
||||||
|
CHECK(releases.empty());
|
||||||
|
|
||||||
|
releases = trackList->getTopReleases({cluster2.getId(), cluster3.getId()}, std::nullopt, hasMore);
|
||||||
|
CHECK(releases.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
TrackListEntry::create(session, trackC.get(), trackList.get());
|
||||||
|
TrackListEntry::create(session, trackC.get(), trackList.get());
|
||||||
|
TrackListEntry::create(session, trackC.get(), trackList.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool hasMore;
|
||||||
|
auto releases {trackList->getTopReleases({cluster2.getId(), cluster3.getId()}, std::nullopt, hasMore)};
|
||||||
|
CHECK(releases.size() == 1);
|
||||||
|
CHECK(releases[0].id() == releaseC.getId());
|
||||||
|
|
||||||
|
releases = trackList->getTopReleases({cluster2.getId()}, std::nullopt, hasMore);
|
||||||
|
CHECK(releases.size() == 2);
|
||||||
|
CHECK(releases[0].id() == releaseC.getId());
|
||||||
|
CHECK(releases[1].id() == releaseB.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
void
|
void
|
||||||
@@ -1120,10 +1220,11 @@ testSingleUser(Session& session)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool hasMore {};
|
||||||
CHECK(user->getPlayedTrackList(session)->getCount() == 0);
|
CHECK(user->getPlayedTrackList(session)->getCount() == 0);
|
||||||
CHECK(user->getPlayedTrackList(session)->getTopTracks(1).empty());
|
CHECK(user->getPlayedTrackList(session)->getTopTracks({}, Range {0, 1}, hasMore).empty());
|
||||||
CHECK(user->getPlayedTrackList(session)->getTopArtists(1).empty());
|
CHECK(user->getPlayedTrackList(session)->getTopArtists({}, std::nullopt, Range {0, 1}, hasMore).empty());
|
||||||
CHECK(user->getPlayedTrackList(session)->getTopReleases(1).empty());
|
CHECK(user->getPlayedTrackList(session)->getTopReleases({}, Range {0, 1}, hasMore).empty());
|
||||||
CHECK(user->getQueuedTrackList(session)->getCount() == 0);
|
CHECK(user->getQueuedTrackList(session)->getCount() == 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1332,6 +1433,261 @@ testSingleTrackListMultipleTrackMultiClusters(Session& session)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
void
|
||||||
|
testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session)
|
||||||
|
{
|
||||||
|
ScopedUser user {session, "MyUser", User::PasswordHash {}};
|
||||||
|
ScopedTrackList trackList {session, "MyTrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||||
|
ScopedClusterType clusterType {session, "MyClusterType"};
|
||||||
|
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"};
|
||||||
|
ScopedCluster cluster2 {session, clusterType.lockAndGet(), "MyCluster2"};
|
||||||
|
ScopedCluster cluster3 {session, clusterType.lockAndGet(), "MyCluster3"};
|
||||||
|
ScopedTrack track1 {session, "MyTrack1"};
|
||||||
|
ScopedTrack track2 {session, "MyTrack1"};
|
||||||
|
ScopedArtist artist1 {session, "MyArtist1"};
|
||||||
|
ScopedArtist artist2 {session, "MyArtist2"};
|
||||||
|
ScopedRelease release1 {session, "MyRelease1"};
|
||||||
|
ScopedRelease release2 {session, "MyRelease2"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
track1.get().modify()->setRelease(release1.get());
|
||||||
|
track2.get().modify()->setRelease(release2.get());
|
||||||
|
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLink::Type::Artist);
|
||||||
|
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLink::Type::Artist);
|
||||||
|
|
||||||
|
cluster1.get().modify()->addTrack(track1.get());
|
||||||
|
cluster2.get().modify()->addTrack(track2.get());
|
||||||
|
cluster3.get().modify()->addTrack(track1.get());
|
||||||
|
cluster3.get().modify()->addTrack(track2.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
CHECK(trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults).empty());
|
||||||
|
CHECK(trackList->getReleasesReverse({}, std::nullopt, moreResults).empty());
|
||||||
|
CHECK(trackList->getTracksReverse({}, std::nullopt, moreResults).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
TrackListEntry::create(session, track1.get(), trackList.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
|
||||||
|
CHECK(artists.size() == 1);
|
||||||
|
CHECK(artists.front().id() == artist1.getId());
|
||||||
|
|
||||||
|
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
|
||||||
|
CHECK(releases.size() == 1);
|
||||||
|
CHECK(releases.front().id() == release1.getId());
|
||||||
|
|
||||||
|
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
|
||||||
|
CHECK(tracks.size() == 1);
|
||||||
|
CHECK(tracks.front().id() == track1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
|
CHECK(artists.size() == 1);
|
||||||
|
CHECK(artists.front().id() == artist1.getId());
|
||||||
|
|
||||||
|
const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(releases.size() == 1);
|
||||||
|
CHECK(releases.front().id() == release1.getId());
|
||||||
|
|
||||||
|
const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(tracks.size() == 1);
|
||||||
|
CHECK(tracks.front().id() == track1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
|
CHECK(artists.size() == 1);
|
||||||
|
CHECK(artists.front().id() == artist1.getId());
|
||||||
|
|
||||||
|
const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(releases.size() == 1);
|
||||||
|
CHECK(releases.front().id() == release1.getId());
|
||||||
|
|
||||||
|
const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(tracks.size() == 1);
|
||||||
|
CHECK(tracks.front().id() == track1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtistsReverse({cluster1.getId()}, TrackArtistLink::Type::Artist, std::nullopt, moreResults)};
|
||||||
|
CHECK(artists.size() == 1);
|
||||||
|
CHECK(artists.front().id() == artist1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtistsReverse({}, TrackArtistLink::Type::Artist, std::nullopt, moreResults)};
|
||||||
|
CHECK(artists.size() == 1);
|
||||||
|
CHECK(artists.front().id() == artist1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
CHECK(trackList->getArtistsReverse({cluster2.getId()}, std::nullopt, std::nullopt, moreResults).empty());
|
||||||
|
CHECK(trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults).empty());
|
||||||
|
CHECK(trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults).empty());
|
||||||
|
|
||||||
|
CHECK(trackList->getArtistsReverse({}, TrackArtistLink::Type::ReleaseArtist, std::nullopt, moreResults).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
TrackListEntry::create(session, track2.get(), trackList.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
|
||||||
|
CHECK(artists.size() == 2);
|
||||||
|
CHECK(artists[0].id() == artist2.getId());
|
||||||
|
CHECK(artists[1].id() == artist1.getId());
|
||||||
|
|
||||||
|
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
|
||||||
|
CHECK(releases.size() == 2);
|
||||||
|
CHECK(releases[0].id() == release2.getId());
|
||||||
|
CHECK(releases[1].id() == release1.getId());
|
||||||
|
|
||||||
|
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
|
||||||
|
CHECK(tracks.size() == 2);
|
||||||
|
CHECK(tracks[0].id() == track2.getId());
|
||||||
|
CHECK(tracks[1].id() == track1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
|
CHECK(artists.size() == 2);
|
||||||
|
CHECK(artists[0].id() == artist2.getId());
|
||||||
|
CHECK(artists[1].id() == artist1.getId());
|
||||||
|
|
||||||
|
const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(releases.size() == 2);
|
||||||
|
CHECK(releases[0].id() == release2.getId());
|
||||||
|
CHECK(releases[1].id() == release1.getId());
|
||||||
|
|
||||||
|
const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(tracks.size() == 2);
|
||||||
|
CHECK(tracks[0].id() == track2.getId());
|
||||||
|
CHECK(tracks[1].id() == track1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
|
CHECK(artists.size() == 1);
|
||||||
|
CHECK(artists.front().id() == artist1.getId());
|
||||||
|
|
||||||
|
const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(releases.size() == 1);
|
||||||
|
CHECK(releases.front().id() == release1.getId());
|
||||||
|
|
||||||
|
const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(tracks.size() == 1);
|
||||||
|
CHECK(tracks.front().id() == track1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtistsReverse({cluster2.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
|
CHECK(artists.size() == 1);
|
||||||
|
CHECK(artists.front().id() == artist2.getId());
|
||||||
|
|
||||||
|
const auto releases {trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(releases.size() == 1);
|
||||||
|
CHECK(releases.front().id() == release2.getId());
|
||||||
|
|
||||||
|
const auto tracks {trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(tracks.size() == 1);
|
||||||
|
CHECK(tracks.front().id() == track2.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
TrackListEntry::create(session, track1.get(), trackList.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
|
||||||
|
CHECK(artists.size() == 2);
|
||||||
|
CHECK(artists[0].id() == artist1.getId());
|
||||||
|
CHECK(artists[1].id() == artist2.getId());
|
||||||
|
|
||||||
|
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
|
||||||
|
CHECK(releases.size() == 2);
|
||||||
|
CHECK(releases[0].id() == release1.getId());
|
||||||
|
CHECK(releases[1].id() == release2.getId());
|
||||||
|
|
||||||
|
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
|
||||||
|
CHECK(tracks.size() == 2);
|
||||||
|
CHECK(tracks[0].id() == track1.getId());
|
||||||
|
CHECK(tracks[1].id() == track2.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
|
CHECK(artists.size() == 2);
|
||||||
|
// TODO investigate
|
||||||
|
// CHECK(artists[0].id() == artist1.getId());
|
||||||
|
// CHECK(artists[1].id() == artist2.getId());
|
||||||
|
|
||||||
|
const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(releases.size() == 2);
|
||||||
|
CHECK(releases[0].id() == release1.getId());
|
||||||
|
CHECK(releases[1].id() == release2.getId());
|
||||||
|
|
||||||
|
const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
|
CHECK(tracks.size() == 2);
|
||||||
|
CHECK(tracks[0].id() == track1.getId());
|
||||||
|
CHECK(tracks[1].id() == track2.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static
|
static
|
||||||
void
|
void
|
||||||
testMultipleTracksMultipleArtistsMultiClusters(Session& session)
|
testMultipleTracksMultipleArtistsMultiClusters(Session& session)
|
||||||
@@ -1519,8 +1875,6 @@ int main()
|
|||||||
|
|
||||||
std::cout << "Database test file: '" << tmpFile.string() << "'" << std::endl;
|
std::cout << "Database test file: '" << tmpFile.string() << "'" << std::endl;
|
||||||
|
|
||||||
for (std::size_t i = 0; i < 2; ++i)
|
|
||||||
{
|
|
||||||
Database::Db db {tmpFile};
|
Database::Db db {tmpFile};
|
||||||
Database::Session session {db};
|
Database::Session session {db};
|
||||||
session.prepareTables();
|
session.prepareTables();
|
||||||
@@ -1556,6 +1910,8 @@ int main()
|
|||||||
RUN_TEST(testSingleTrackSingleCluster);
|
RUN_TEST(testSingleTrackSingleCluster);
|
||||||
RUN_TEST(testMultipleTracksSingleCluster);
|
RUN_TEST(testMultipleTracksSingleCluster);
|
||||||
|
|
||||||
|
RUN_TEST(testMultipleTracksMultipleClustersTopRelease);
|
||||||
|
|
||||||
RUN_TEST(testMultipleTracksSingleClusterSimilarity);
|
RUN_TEST(testMultipleTracksSingleClusterSimilarity);
|
||||||
RUN_TEST(testMultipleTracksMultipleClustersSimilarity);
|
RUN_TEST(testMultipleTracksMultipleClustersSimilarity);
|
||||||
|
|
||||||
@@ -1580,12 +1936,12 @@ int main()
|
|||||||
RUN_TEST(testSingleTrackListMultipleTrack);
|
RUN_TEST(testSingleTrackListMultipleTrack);
|
||||||
RUN_TEST(testSingleTrackListMultipleTrackSingleCluster);
|
RUN_TEST(testSingleTrackListMultipleTrackSingleCluster);
|
||||||
RUN_TEST(testSingleTrackListMultipleTrackMultiClusters);
|
RUN_TEST(testSingleTrackListMultipleTrackMultiClusters);
|
||||||
|
RUN_TEST(testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed);
|
||||||
RUN_TEST(testMultipleTracksMultipleArtistsMultiClusters);
|
RUN_TEST(testMultipleTracksMultipleArtistsMultiClusters);
|
||||||
RUN_TEST(testMultipleTracksMultipleReleasesMultiClusters);
|
RUN_TEST(testMultipleTracksMultipleReleasesMultiClusters);
|
||||||
|
|
||||||
RUN_TEST(testSingleTrackSingleUserSingleBookmark);
|
RUN_TEST(testSingleTrackSingleUserSingleBookmark);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
{
|
{
|
||||||
std::cerr << "Caught exception: " << e.what() << std::endl;
|
std::cerr << "Caught exception: " << e.what() << std::endl;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
|||||||
{
|
{
|
||||||
using namespace MetaData;
|
using namespace MetaData;
|
||||||
|
|
||||||
parser.setClusterTypeNames( {"MOOD", "GENRE"} );
|
parser.setClusterTypeNames( {"ALBUMMOOD", "MOOD", "ALBUMGROUPING", "ALBUMGENRE", "GENRE"} );
|
||||||
|
|
||||||
const auto start {std::chrono::steady_clock::now()};
|
const auto start {std::chrono::steady_clock::now()};
|
||||||
std::optional<Track> track {parser.parse(file, true)};
|
std::optional<Track> track {parser.parse(file, true)};
|
||||||
|
|||||||
Reference in New Issue
Block a user