New UI layout and UI improvements. Fixes #50

This commit is contained in:
emeric
2020-05-10 13:11:43 +02:00
parent 8277b9c82b
commit 74fc4e058e
79 changed files with 3003 additions and 1987 deletions
+1 -1
View File
@@ -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}
+15 -5
View File
@@ -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">
<h2><div class="Lms-artistname text-primary">${name}</div></h2> <div class="col-md-8">
${clusters} <div class="Lms-header">
${play-btn}${add-btn} <h2><div class="Lms-artistname text-primary">${name}</div></h2>
${clusters}
${play-btn}${add-btn}
</div>
${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> </div>
${releases}
</message> </message>
<message id="Lms.Explore.Artist.template.entry"> <message id="Lms.Explore.Artist.template.entry">
-10
View File
@@ -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>
-13
View File
@@ -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>
+12 -11
View File
@@ -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>
-12
View File
@@ -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>
+2 -11
View File
@@ -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"> ${contents class="Lms-explore-contents"}
<div class="col-md-8">
${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
View File
@@ -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
View File
@@ -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>
-45
View File
@@ -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>
+2 -2
View File
@@ -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>
+23 -10
View File
@@ -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 class="col-md-5">
<div class="Lms-header">
<h3><div class="Lms-releasename text-primary">${name}</div></h3>
${<if-has-artist>}<h4>${artist class="Lms-artistname text-muted"}</h4>${</if-has-artist>}
<div class="text-muted"><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></div>
${clusters}
${play-btn}${add-btn}
</div>
${container 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> </div>
<div class="page-header">
<h3><div class="Lms-releasename text-primary">${name}</div></h3>
${<if-has-artist>}<h4>${artist class="Lms-artistname text-muted"}</h4>${</if-has-artist>}
<div class="text-muted"><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></div>
${clusters}
${play-btn}${add-btn}
</div>
${container}
</message> </message>
<message id="Lms.Explore.Release.template.disc-entry"> <message id="Lms.Explore.Release.template.disc-entry">
-15
View File
@@ -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>
-23
View File
@@ -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
View File
@@ -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>
-12
View File
@@ -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>
+42
View File
@@ -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
View File
@@ -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>
+6 -7
View File
@@ -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="row">
<div class="input-group"> <div class="col-lg-8">
<span class="input-group-addon"><i class="fa fa-search"></i></span> ${tracks}
${search}
</div> </div>
</div> </div>
${tracks}
<div class="row"> <div class="row">
<div class="col-xs-12 Lms-horizontal-center"> <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
View File
@@ -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;
}
+211
View File
@@ -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);
+165
View File
@@ -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);
+59
View File
@@ -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);
+3 -3
View File
@@ -44,9 +44,9 @@ namespace CoverArt
Release, Release,
}; };
Type type; Type type;
Database::IdType id; Database::IdType id;
std::size_t size; std::size_t size;
bool operator==(const CacheEntryDesc& other) const bool operator==(const CacheEntryDesc& other) const
{ {
@@ -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
+151 -67
View File
@@ -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())
{
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);
}
if (linkType)
where.And(WhereClause {"t_a_l.type = ?"}.bind(std::to_string(static_cast<int>(*linkType))));
}
oss << " " << where.get();
if (!clusterIds.empty()) if (!clusterIds.empty())
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size();
switch (sortMethod)
{ {
case Artist::SortMethod::None: std::ostringstream oss;
break; oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
case Artist::SortMethod::ByName: " INNER JOIN track t ON t.id = t_a_l.track_id"
oss << " ORDER BY a.name COLLATE NOCASE"; " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
break; " INNER JOIN cluster c ON c.id = t_c.cluster_id"
case Artist::SortMethod::BySortName: " INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
oss << " ORDER BY a.sort_name COLLATE NOCASE";
break;
}
Wt::Dbo::Query<Artist::pointer> query = session.getDboSession().query<Artist::pointer>( oss.str() ); WhereClause clusterClause;
for (const IdType clusterId : clusterIds)
{
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(clusterId);
}
for (const std::string& bindArg : where.getBindArgs()) oss << " " << clusterClause.get();
{ oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
query.bind(bindArg);
query.where(oss.str());
} }
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
+18 -2
View File
@@ -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
+95 -63
View File
@@ -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();
+1
View File
@@ -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)");
+88 -67
View File
@@ -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);
} }
+243 -30
View File
@@ -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)
+11 -4
View File
@@ -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
+24 -2
View File
@@ -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");
} }
private: ScopedNoForeignKeys(const ScopedNoForeignKeys&) = delete;
Db& _db; ScopedNoForeignKeys(ScopedNoForeignKeys&&) = delete;
ScopedNoForeignKeys& operator=(const ScopedNoForeignKeys&) = delete;
ScopedNoForeignKeys& operator=(ScopedNoForeignKeys&&) = delete;
private:
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 = {});
+5 -5
View File
@@ -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;
+8 -1
View File
@@ -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 {};
};
} }
+14 -12
View File
@@ -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));
} }
+6 -1
View File
@@ -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
+2
View File
@@ -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)
+3 -8
View File
@@ -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
View File
@@ -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);
+55 -81
View File
@@ -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
View File
@@ -28,43 +28,20 @@ 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>
@@ -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"},
}; };
-1
View File
@@ -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);
+10
View File
@@ -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);
} }
-132
View File
@@ -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
+2 -2
View File
@@ -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));
} }
-81
View File
@@ -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
-39
View File
@@ -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);
};
} }
+95 -67
View File
@@ -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;
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const std::vector<Database::IdType> similarArtistIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)};
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,70 +122,92 @@ 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)
for (auto release : releases)
{ {
auto releaseId = release.id(); releasesContainer->addWidget(createRelease(artist, release));
}
}
Wt::WTemplate* entry = releasesContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artist.template.entry")); std::unique_ptr<Wt::WTemplate>
entry->addFunction("tr", Wt::WTemplate::Functions::tr); Artist::createRelease(const Database::Artist::pointer& artist, const Release::pointer& release)
{
auto releaseId = release.id();
auto entry = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artist.template.entry"));
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
{
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
cover->setStyleClass("Lms-cover-medium");
anchor->setImage(std::move(cover));
}
entry->bindWidget("name", LmsApplication::createReleaseAnchor(release));
auto artists {release->getReleaseArtists()};
if (artists.empty())
artists = release->getArtists();
bool isSameArtist {(std::find(std::cbegin(artists), std::cend(artists), artist) != artists.end())};
if (artists.size() > 1)
{
entry->setCondition("if-has-artist", true);
entry->bindNew<Wt::WText>("artist", Wt::WString::tr("Lms.Explore.various-artists"));
}
else if (artists.size() == 1 && !isSameArtist)
{
entry->setCondition("if-has-artist", true);
entry->bindWidget("artist", LmsApplication::createArtistAnchor(artists.front()));
}
std::optional<int> year {release->getReleaseYear()};
if (year)
{
entry->setCondition("if-has-year", true);
entry->bindInt("year", *year);
std::optional<int> originalYear {release->getReleaseYear(true)};
if (originalYear && *originalYear != *year)
{ {
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false)); entry->setCondition("if-has-orig-year", true);
entry->bindInt("orig-year", *originalYear);
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
cover->setStyleClass("Lms-cover-medium");
anchor->setImage(std::move(cover));
} }
}
entry->bindWidget("name", LmsApplication::createReleaseAnchor(release)); Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
playBtn->clicked().connect([=]
{
releasesPlay.emit({releaseId});
});
auto artists {release->getReleaseArtists()}; Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
if (artists.empty()) addBtn->clicked().connect([=]
artists = release->getArtists(); {
releasesAdd.emit({releaseId});
});
bool isSameArtist {(std::find(std::cbegin(artists), std::cend(artists), artist) != artists.end())}; return entry;
}
if (artists.size() > 1) void
{ Artist::refreshSimilarArtists(const std::vector<Database::IdType>& similarArtistsId)
entry->setCondition("if-has-artist", true); {
entry->bindNew<Wt::WText>("artist", Wt::WString::tr("Lms.Explore.various-artists")); auto* similarArtistsContainer {bindNew<Wt::WContainerWidget>("similar-artists")};
}
else if (artists.size() == 1 && !isSameArtist)
{
entry->setCondition("if-has-artist", true);
entry->bindWidget("artist", LmsApplication::createArtistAnchor(artists.front()));
}
std::optional<int> year {release->getReleaseYear()}; for (Database::IdType artistId : similarArtistsId)
if (year) {
{ const Database::Artist::pointer similarArtist{Database::Artist::getById(LmsApp->getDbSession(), artistId)};
entry->setCondition("if-has-year", true); if (!similarArtist)
entry->bindInt("year", *year); continue;
std::optional<int> originalYear {release->getReleaseYear(true)}; similarArtistsContainer->addWidget(ArtistListHelpers::createEntrySmall(similarArtist));
if (originalYear && *originalYear != *year)
{
entry->setCondition("if-has-orig-year", true);
entry->bindInt("orig-year", *originalYear);
}
}
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
playBtn->clicked().connect([=]
{
releasesPlay.emit({releaseId});
});
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
addBtn->clicked().connect([=]
{
releasesAdd.emit({releaseId});
});
} }
} }
+15 -4
View File
@@ -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
-83
View File
@@ -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
-41
View File
@@ -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
+130 -32
View File
@@ -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);
+39 -10
View File
@@ -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
+5 -64
View File
@@ -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
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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");
-116
View File
@@ -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
-39
View File
@@ -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
-63
View File
@@ -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
+68
View File
@@ -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
+74 -25
View File
@@ -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;
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const std::vector<Database::IdType> similarReleasesIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 5)};
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
+23 -15
View File
@@ -20,29 +20,37 @@
#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 Filters;
class Release : public Wt::WContainerWidget
{ {
public: class Release;
Release(Filters* filters); }
Wt::Signal<const std::vector<Database::IdType>&> releasesAdd; namespace UserInterface
Wt::Signal<const std::vector<Database::IdType>&> releasesPlay; {
Wt::Signal<const std::vector<Database::IdType>&> tracksAdd; class Filters;
Wt::Signal<const std::vector<Database::IdType>&> tracksPlay; class Release : public Wt::WTemplate
{
public:
Release(Filters* filters);
private: Wt::Signal<const std::vector<Database::IdType>&> releasesAdd;
void refresh(); Wt::Signal<const std::vector<Database::IdType>&> releasesPlay;
Filters* _filters; Wt::Signal<const std::vector<Database::IdType>&> tracksAdd;
}; Wt::Signal<const std::vector<Database::IdType>&> tracksPlay;
private:
void refreshView();
void refreshCopyright(const Wt::Dbo::ptr<Database::Release>& release);
void refreshSimilarReleases(const std::vector<Database::IdType>& similarReleasesId);
Filters* _filters {};
};
} // namespace UserInterface } // namespace UserInterface
-85
View File
@@ -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
-41
View File
@@ -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
+163 -70
View File
@@ -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,95 +86,169 @@ _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
Releases::refreshView(Mode mode)
{
_mode = mode;
refreshView();
}
std::unique_ptr<Wt::WTemplate>
Releases::createEntry(const Release::pointer& release)
{
const IdType releaseId {release.id()};
auto entry = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Releases.template.entry"));
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
cover->setStyleClass("Lms-cover-medium");
anchor->setImage(std::move(cover));
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
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()));
}
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
playBtn->clicked().connect([=]
{
releasesPlay.emit({releaseId});
});
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
addBtn->clicked().connect([=]
{
releasesAdd.emit({releaseId});
});
return entry;
}
void void
Releases::addSome() Releases::addSome()
{ {
bool moreResults {}; bool moreResults {};
const auto releasesId {getReleases(_container->count(), 20, moreResults)}; auto transaction {LmsApp->getDbSession().createSharedTransaction()};
for (const Database::IdType releaseId : releasesId )
for (const Release::pointer& release : getReleases(Range {static_cast<std::size_t>(_container->count()), batchSize}, moreResults))
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; _container->addWidget(createEntry(release));
const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), releaseId)};
Wt::WTemplate* entry = _container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Releases.template.entry"));
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
// Some images may not be square
cover->setStyleClass("Lms-cover-medium");
anchor->setImage(std::move(cover));
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
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()));
}
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
playBtn->clicked().connect([=]
{
releasesPlay.emit({releaseId});
});
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
addBtn->clicked().connect([=]
{
releasesAdd.emit({releaseId});
});
} }
_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
+36 -8
View File
@@ -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
+183
View File
@@ -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
-97
View File
@@ -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
-41
View File
@@ -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
+189 -97
View File
@@ -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,144 +49,234 @@ _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 entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
Tracks::refresh()
{ auto artists {track->getArtists()};
_tracksContainer->clear(); auto release {track->getRelease()};
addSome(); 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");
}
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);
playBtn->clicked().connect([=]
{
tracksPlay.emit({trackId});
});
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
addBtn->clicked().connect([=]
{
tracksAdd.emit({trackId});
});
LmsApp->getMediaPlayer()->trackLoaded.connect(entryPtr, [=] (Database::IdType loadedTrackId)
{
entryPtr->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
});
if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
{
if (*trackIdLoaded == trackId)
entry->bindString("is-playing", "Lms-entry-playing");
}
return entry;
} }
void void
Tracks::addSome() Tracks::addSome()
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
bool moreResults; bool moreResults;
const std::vector<Database::IdType> trackIds {getTracks(_tracksContainer->count(), 20, moreResults)}; for (const Track::pointer& track : getTracks(Range {static_cast<std::size_t>(_tracksContainer->count()), batchSize}, moreResults))
for (const Database::IdType trackId : trackIds)
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; _tracksContainer->addWidget(createEntry(track));
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);
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.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");
}
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);
playBtn->clicked().connect(std::bind([=]
{
tracksPlay.emit({trackId});
}));
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([=]
{
tracksAdd.emit({trackId});
}));
LmsApp->getMediaPlayer()->trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId)
{
entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
});
if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
{
if (*trackIdLoaded == trackId)
entry->bindString("is-playing", "Lms-entry-playing");
}
} }
_showMore->setHidden(!moreResults); _showMore->setHidden(!moreResults);
+38 -7
View File
@@ -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
+417 -61
View File
@@ -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,72 +1875,72 @@ 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::Session session {db};
Database::Db db {tmpFile}; session.prepareTables();
Database::Session session {db};
session.prepareTables();
auto runTest = [&session](const std::string& name, std::function<void(Session&)> testFunc) auto runTest = [&session](const std::string& name, std::function<void(Session&)> testFunc)
{ {
std::cout << "Running test '" << name << "'..." << std::endl; std::cout << "Running test '" << name << "'..." << std::endl;
testFunc(session); testFunc(session);
testDatabaseEmpty(session); testDatabaseEmpty(session);
std::cout << "Running test '" << name << "': SUCCESS" << std::endl; std::cout << "Running test '" << name << "': SUCCESS" << std::endl;
}; };
#define RUN_TEST(test) runTest(#test, test) #define RUN_TEST(test) runTest(#test, test)
// Special test to remove any default created entries // Special test to remove any default created entries
RUN_TEST(testRemoveDefaultEntries); RUN_TEST(testRemoveDefaultEntries);
RUN_TEST(testSingleTrack); RUN_TEST(testSingleTrack);
RUN_TEST(testSingleArtist); RUN_TEST(testSingleArtist);
RUN_TEST(testSingleRelease); RUN_TEST(testSingleRelease);
RUN_TEST(testSingleCluster); RUN_TEST(testSingleCluster);
RUN_TEST(testSingleTrackSingleArtist); RUN_TEST(testSingleTrackSingleArtist);
RUN_TEST(testSingleTrackSingleArtistMultiRoles); RUN_TEST(testSingleTrackSingleArtistMultiRoles);
RUN_TEST(testSingleTrackMultiArtists); RUN_TEST(testSingleTrackMultiArtists);
RUN_TEST(testSingleArtistSearchByName); RUN_TEST(testSingleArtistSearchByName);
RUN_TEST(testMultiArtistsSortMethod); RUN_TEST(testMultiArtistsSortMethod);
RUN_TEST(testSingleTrackSingleRelease); RUN_TEST(testSingleTrackSingleRelease);
RUN_TEST(testMultiTracksSingleReleaseTotalDiscTrack); RUN_TEST(testMultiTracksSingleReleaseTotalDiscTrack);
RUN_TEST(testSingleTrackSingleCluster); RUN_TEST(testSingleTrackSingleCluster);
RUN_TEST(testMultipleTracksSingleCluster); RUN_TEST(testMultipleTracksSingleCluster);
RUN_TEST(testMultipleTracksSingleClusterSimilarity); RUN_TEST(testMultipleTracksMultipleClustersTopRelease);
RUN_TEST(testMultipleTracksMultipleClustersSimilarity);
RUN_TEST(testSingleTrackSingleReleaseSingleCluster); RUN_TEST(testMultipleTracksSingleClusterSimilarity);
RUN_TEST(testSingleTrackSingleArtistMultiClusters); RUN_TEST(testMultipleTracksMultipleClustersSimilarity);
RUN_TEST(testSingleTrackSingleArtistMultiRolesMultiClusters);
RUN_TEST(testMultiTracksSingleArtistMultiClusters);
RUN_TEST(testMultiTracksSingleArtistSingleRelease);
RUN_TEST(testSingleTrackSingleReleaseSingleArtist); RUN_TEST(testSingleTrackSingleReleaseSingleCluster);
RUN_TEST(testSingleTrackSingleArtistMultiClusters);
RUN_TEST(testSingleTrackSingleArtistMultiRolesMultiClusters);
RUN_TEST(testMultiTracksSingleArtistMultiClusters);
RUN_TEST(testMultiTracksSingleArtistSingleRelease);
RUN_TEST(testSingleTrackSingleReleaseSingleArtistSingleCluster); RUN_TEST(testSingleTrackSingleReleaseSingleArtist);
RUN_TEST(testSingleTrackSingleReleaseSingleArtistMultiClusters);
RUN_TEST(testSingleUser); RUN_TEST(testSingleTrackSingleReleaseSingleArtistSingleCluster);
RUN_TEST(testSingleTrackSingleReleaseSingleArtistMultiClusters);
RUN_TEST(testSingleStarredArtist); RUN_TEST(testSingleUser);
RUN_TEST(testSingleStarredRelease);
RUN_TEST(testSingleStarredTrack);
RUN_TEST(testSingleTrackList); RUN_TEST(testSingleStarredArtist);
RUN_TEST(testSingleTrackListMultipleTrack); RUN_TEST(testSingleStarredRelease);
RUN_TEST(testSingleTrackListMultipleTrackSingleCluster); RUN_TEST(testSingleStarredTrack);
RUN_TEST(testSingleTrackListMultipleTrackMultiClusters);
RUN_TEST(testMultipleTracksMultipleArtistsMultiClusters);
RUN_TEST(testMultipleTracksMultipleReleasesMultiClusters);
RUN_TEST(testSingleTrackSingleUserSingleBookmark); RUN_TEST(testSingleTrackList);
} RUN_TEST(testSingleTrackListMultipleTrack);
RUN_TEST(testSingleTrackListMultipleTrackSingleCluster);
RUN_TEST(testSingleTrackListMultipleTrackMultiClusters);
RUN_TEST(testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed);
RUN_TEST(testMultipleTracksMultipleArtistsMultiClusters);
RUN_TEST(testMultipleTracksMultipleReleasesMultiClusters);
RUN_TEST(testSingleTrackSingleUserSingleBookmark);
} }
catch (std::exception& e) catch (std::exception& e)
{ {
+1 -1
View File
@@ -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)};