Merge branch 'develop' for release 3.11.0

This commit is contained in:
emeric
2020-04-18 13:40:16 +02:00
59 changed files with 1396 additions and 498 deletions
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Admin.Subsonic.template">
<legend>${tr:Lms.Admin.Subsonic.subsonic}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:enable}">
${tr:Lms.Admin.Subsonic.enable-api}
</label>
<div class="col-sm-5">
<div class="checkbox"><label>${enable}</label></div>
</div>
<div class="help-block col-sm-5">
${enable-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:artist-list-mode}">
${tr:Lms.Admin.Subsonic.artist-list-mode}
</label>
<div class="col-sm-5">
${artist-list-mode}
</div>
<div class="help-block col-sm-5">
${artist-list-mode-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${apply-btn class="btn-primary"} ${discard-btn}
</div>
</div>
</div>
</message>
</messages>
+6 -6
View File
@@ -4,7 +4,7 @@
<message id="Lms.Explore.Artist.template">
<div class="page-header">
<h2>${name}</h2>
<h2><div class="Lms-artistname text-primary">${name}</div></h2>
${clusters}
${play-btn}${add-btn}
</div>
@@ -12,18 +12,18 @@
</message>
<message id="Lms.Explore.Artist.template.entry">
<div class="row Lms-explore-artist-entry">
<div class="row Lms-explore-artist-entry Lms-entry-hoverable">
<div class="col-xs-12">
<div class="media">
<div class="media-left media-middle">
${cover class="media-object"}
${cover class="media-object Lms-cover"}
</div>
<div class="media-body media-middle">
<h4 class="media-heading">
${name class="Lms-explore-releasename"}
${name class="Lms-releasename text-primary"}
</h4>
${<if-has-artist>}<h4><small>${artist class="Lms-explore-artistname"}</small></h4>${</if-has-artist>}
<h4><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></h4>
${<if-has-artist>}<h4><small>${artist class="Lms-artistname text-muted"}</small></h4>${</if-has-artist>}
<h4><small><div class="text-muted">${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</div></small></h4>
${play-btn}${add-btn}
</div>
</div>
+2 -2
View File
@@ -3,9 +3,9 @@
<!--FORMS message blocks-->
<message id="Lms.Explore.ArtistLink.template">
<div class="row Lms-explore-artistlink Lms-vertical-align">
<div class="row Lms-explore-artistlink Lms-entry-hoverable Lms-vertical-align">
<div class="col-xs-12">
${name class="Lms-explore-artistname Lms-explore-artistlink-name"}
${name class="Lms-artistname text-primary"}
</div>
</div>
</message>
+2 -2
View File
@@ -28,9 +28,9 @@
</message>
<message id="Lms.Explore.Artists.template.entry">
<div class="row Lms-explore-artists-entry Lms-vertical-align">
<div class="row Lms-explore-artists-entry Lms-entry-hoverable Lms-vertical-align">
<div class="col-xs-12">
<h4>${name class="Lms-explore-artistname Lms-explore-artists-entry-name"}</h4>
<h4>${name class="Lms-artistname text-primary"}</h4>
</div>
</div>
</message>
+6 -15
View File
@@ -3,33 +3,24 @@
<message id="Lms.Explore.template">
<div class="row">
<div class="col-lg-8">
<div class="col-md-8">
${contents class="Lms-explore-contents"}
</div>
<div class="col-lg-4">
${filters class="Lms-explore-filters"}
${info class="Lms-explore-info"}
<div class="col-md-4">
${filters class="well well-sm"}
${info class="well well-sm"}
</div>
</div>
</message>
<message id="Lms.Explore.template.add-btn">
<i class="fa fa-plus-circle Lms-explore-btn"></i>
</message>
<message id="Lms.Explore.template.play-btn">
<i class="fa fa-play-circle Lms-explore-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.filters">
<h3>${tr:Lms.Explore.filters} ${add-filter class="btn-primary"}</h3>
${clusters}
</message>
<message id="Lms.Explore.template.cluster-entry">
<span class="badge {1}">${<if-can-delete>}<i class="fa fa-times-circle"></i> ${</if-can-delete>}${name}</span>
</message>
<message id="Lms.Explore.template.add-filter">
<div class="form-horizontal">
<div class="form-group">
+1 -1
View File
@@ -37,7 +37,7 @@
${tr:Lms.Auth.remember-me}
</label>
<div class="col-sm-5">
${remember-me}
<div class="checkbox"><label>${remember-me}</label></div>
</div>
</div>
<div class="form-group">
+8 -11
View File
@@ -8,39 +8,36 @@
<div class="Lms-player-seek-container">
<input id="lms-mp-seek" class="Lms-player-seek-common Lms-player-seek" type="range" min="0" max="100" step="1" value="0"/>
<div class="progress Lms-player-seek-common Lms-player-progress">
<div id="lms-mp-progress" class="progress-bar progress-bar-info progress-bar-striped active" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
<div id="lms-mp-progress" class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
</div>
</div>
</div>
<div class="row">
<div class="col-xs-4 col-sm-2 Lms-player-controls">
<i id="lms-mp-previous" class="fa fa-step-backward Lms-player-btn Lms-player-btn-controls"></i>
<i id="lms-mp-playpause" class="fa fa-play Lms-player-btn Lms-player-btn-controls"></i>
<i id="lms-mp-next" class="fa fa-step-forward Lms-player-btn Lms-player-btn-controls"></i>
<div class="col-xs-5 col-sm-2 Lms-player-controls">
<i id="lms-mp-previous" class="fa fa-step-backward Lms-player-btn Lms-control-btn Lms-player-btn-controls"></i><i id="lms-mp-playpause" class="fa fa-play Lms-player-btn Lms-control-btn Lms-player-btn-controls"></i><i id="lms-mp-next" class="fa fa-step-forward Lms-player-btn Lms-control-btn Lms-player-btn-controls"></i>
</div>
<div class="hidden-xs col-sm-2">
<div class="Lms-player-volume">
<i id="lms-mp-volume" class="fa fa-fw fa-volume-up Lms-player-btn Lms-player-btn-volume"></i>
<i id="lms-mp-volume" class="fa fa-fw fa-volume-up Lms-player-btn Lms-control-btn Lms-player-btn-volume"></i>
<span class="hidden-xs Lms-player-volume-slider-container">
<input id ="lms-mp-volume-slider" type="range" min="0" max="1" step="0.01" value="0.8" class="Lms-player-volume-slider"/>
</span>
</div>
</div>
<div class="col-xs-8 col-sm-6">
<div class="col-xs-7 col-sm-6">
<div class="Lms-player-text-center">
${title class="Lms-player-text"}
${title class="Lms-player-text Lms-trackname text-primary"}
</div>
<div class="Lms-player-text-center">
<small><span class="hidden-xs">${release class="Lms-player-text Lms-explore-releasename"}</span>${artist class="Lms-player-text Lms-explore-artistname Lms-player-artistname"}</small>
<span class="hidden-xs">${release class="Lms-player-text Lms-releasename text-primary"}</span><small>${artist class="Lms-player-text Lms-artistname text-muted"}</small>
</div>
</div>
<div class="hidden-xs col-sm-2 Lms-player-duration">
<div class="hidden-xs col-sm-2 Lms-player-duration text-muted">
<span id="lms-mp-curtime">--</span> / <span id="lms-mp-duration">--</span>
</div>
</div>
</div>
</message>
</messages>
+11
View File
@@ -68,6 +68,14 @@
<message id="Lms.Admin.Database.Status.status-scheduled">Scheduled on {1}</message>
<message id="Lms.Admin.Database.Status.status-in-progress">Scanning {1} of {2} files ({3} %)</message>
<!--Subsonic-->
<message id="Lms.Admin.Subsonic.all-artists">All artists</message>
<message id="Lms.Admin.Subsonic.release-artists">Album artists</message>
<message id="Lms.Admin.Subsonic.enable-api">Enable</message>
<message id="Lms.Admin.Subsonic.artist-list-mode">Artist list mode</message>
<message id="Lms.Admin.Subsonic.need-restart">You must restart the service for this change to take effect</message>
<message id="Lms.Admin.Subsonic.subsonic">Subsonic API</message>
<!--Users-->
<message id="Lms.Admin.Users.add">New user</message>
<message id="Lms.Admin.Users.admin">Admin</message>
@@ -124,6 +132,7 @@
<!--Explore:Release-->
<message id="Lms.Explore.ReleaseInfo.similar-releases">Similar albums</message>
<message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disc {1}</message>
<!--Explore:Releases-->
<message id="Lms.Explore.ReleasesInfo.recently-added">Recently added</message>
@@ -155,8 +164,10 @@
<!--Settings-->
<message id="Lms.Settings.appearance">Appearance</message>
<message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.change-password">Change password</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.password-bad">Bad password</message>
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message>
+13 -2
View File
@@ -68,6 +68,14 @@
<message id="Lms.Admin.Database.Status.status-scheduled">Planifié le {1}</message>
<message id="Lms.Admin.Database.Status.status-in-progress">Scan de {1} fichiers sur {2} ({3} %)</message>
<!--Subsonic-->
<message id="Lms.Admin.Subsonic.all-artists">Tous les artistes</message>
<message id="Lms.Admin.Subsonic.release-artists">Artistes d'album</message>
<message id="Lms.Admin.Subsonic.enable-api">Activer</message>
<message id="Lms.Admin.Subsonic.artist-list-mode">Mode de listage des artistes</message>
<message id="Lms.Admin.Subsonic.need-restart">Vous devez redémarrer le service pour que cette option soit prise en compte</message>
<message id="Lms.Admin.Subsonic.subsonic">API Subsonic</message>
<!--Users-->
<message id="Lms.Admin.Users.add">Ajouter</message>
<message id="Lms.Admin.Users.admin">Admin</message>
@@ -116,14 +124,15 @@
<!--Explore:Artists-->
<message id="Lms.Explore.Artists.linktype-all">Tous les artistes</message>
<message id="Lms.Explore.Artists.linktype-artist">Artiste de piste</message>
<message id="Lms.Explore.Artists.linktype-releaseartist">Artiste d'album</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.ArtistsInfo.recently-added">Ajouts récents</message>
<message id="Lms.Explore.ArtistsInfo.most-played">Artistes populaires</message>
<!--Explore:Release-->
<message id="Lms.Explore.ReleaseInfo.similar-releases">Albums similaires</message>
<message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disque {1}</message>
<!--Explore:Releases-->
<message id="Lms.Explore.ReleasesInfo.recently-added">Ajouts récents</message>
@@ -155,8 +164,10 @@
<!--Settings-->
<message id="Lms.Settings.appearance">Apparence</message>
<message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.change-password">Changement de mot de passe</message>
<message id="Lms.Settings.dark-mode">Mode sombre</message>
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
+25 -13
View File
@@ -3,31 +3,43 @@
<!--FORMS message blocks-->
<message id="Lms.PlayHistory.template">
<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 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-vertical-align">
<div class="row Lms-playhistory-entry Lms-entry-hoverable Lms-vertical-align">
<div class="col-xs-12 Lms-playhistory-entry-name">
<h4>${name}</h4>
${<if-has-artists-or-release>}
${<if-has-release>}<small>${release class="Lms-explore-releasename"}</small>${</if-has-release>}
${<if-has-artists>}<small>${artists}</small>${</if-has-artists>}
${</if-has-artists-or-release>}
<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-explore-artistname"}
${artist class="Lms-artistname text-muted"}
</message>
</messages>
+34 -39
View File
@@ -2,58 +2,53 @@
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.PlayQueue.template.clear-btn">
<i class="fa fa-times-circle Lms-playqueue-btn"></i>
</message>
<message id="Lms.PlayQueue.template.shuffle-btn">
<i class="fa fa-random Lms-playqueue-btn"></i>
</message>
<message id="Lms.PlayQueue.template.repeat-btn">
<i class="fa fa-repeat Lms-playqueue-btn"></i>
</message>
<message id="Lms.PlayQueue.template.radio-btn">
<i class="fa fa-rss Lms-playqueue-btn"></i>
</message>
<message id="Lms.PlayQueue.template.clear-btn"><i class="fa fa-fw fa-times-circle Lms-playqueue-btn Lms-control-btn"></i></message>
<message id="Lms.PlayQueue.template.shuffle-btn"><i class="fa fa-fw fa-random Lms-playqueue-btn Lms-control-btn"></i></message>
<message id="Lms.PlayQueue.template.repeat-btn"><i class="fa fa-fw fa-repeat Lms-playqueue-btn Lms-control-btn"></i></message>
<message id="Lms.PlayQueue.template.radio-btn"><i class="fa fa-fw fa-rss Lms-playqueue-btn Lms-control-btn"></i></message>
<message id="Lms.PlayQueue.template">
<div class="page-header">
<h2>${tr:Lms.PlayQueue.playqueue}</h2>
${clear-btn}${shuffle-btn}${repeat-btn}${radio-btn}
<h4><small>${nb-tracks}</small></h4>
</div>
${entries}
<div class="row">
<div class="col-xs-12 Lms-horizontal-center">
${show-more class="btn-primary Lms-show-more"}
<div class="col-md-8">
<div class="page-header">
<h2>${tr:Lms.PlayQueue.playqueue}</h2>
${clear-btn}${shuffle-btn}${repeat-btn}${radio-btn}
<h4><small>${nb-tracks}</small></h4>
</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.PlayQueue.template.play-btn">
<i class="fa fa-play-circle Lms-playqueue-btn"></i>
</message>
<message id="Lms.PlayQueue.template.delete-btn">
<i class="fa fa-times-circle Lms-playqueue-btn"></i>
</message>
<message id="Lms.PlayQueue.template.play-btn"><i class="fa fa-fw fa-play-circle Lms-playqueue-btn Lms-control-btn"></i></message>
<message id="Lms.PlayQueue.template.delete-btn"><i class="fa fa-fw fa-times-circle Lms-playqueue-btn Lms-control-btn"></i></message>
<message id="Lms.PlayQueue.template.entry">
<div class="row Lms-playqueue-entry Lms-vertical-align">
<div class="row Lms-playqueue-entry Lms-entry-hoverable Lms-vertical-align ${is-selected}">
<div class="col-xs-9 Lms-playqueue-entry-name">
<h4>${name}</h4>
${<if-has-artists-or-release>}
${<if-has-release>}<small>${release class="Lms-explore-releasename"}</small>${</if-has-release>}
${<if-has-artists>}<small>${artists}</small>${</if-has-artists>}
${</if-has-artists-or-release>}
<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 class="col-xs-3">
<div class="Lms-playqueue-entry-controls">
<div class="Lms-playqueue-entry-controls Lms-vertical-align">
${play-btn}${del-btn}
</div>
<div class="Lms-playqueue-entry-duration">
<div class="Lms-playqueue-entry-duration text-muted">
${duration}
</div>
</div>
@@ -61,7 +56,7 @@
</message>
<message id="Lms.PlayQueue.template.entry-artist">
${artist class="Lms-explore-artistname"}
<small>${artist class="Lms-artistname text-muted"}</small>
</message>
</messages>
+15 -10
View File
@@ -13,29 +13,34 @@
</div>
</div>
<div class="page-header">
<h3>${name}</h3>
${<if-has-artist>}<h4>${artist class="Lms-explore-artistname"}</h4>${</if-has-artist>}
<h4><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></h4>
<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 id="Lms.Explore.Release.template.disc-entry">
<h4>${disc-title}</h4>
${tracks}
</message>
<message id="Lms.Explore.Release.template.entry">
<div class="row Lms-explore-release-entry Lms-vertical-align">
<div class="col-xs-1 Lms-explore-release-entry-tracknumber">
${<if-has-disc-number>}${disc-number}-${</if-has-disc-number>}${<if-has-track-number>}${track-number}${</if-has-track-number>}
<div class="row Lms-explore-release-entry Lms-entry-hoverable Lms-vertical-align ${is-playing}">
<div class="col-xs-1 Lms-explore-release-entry-tracknumber text-muted">
${<if-has-track-number>}${track-number}${</if-has-track-number>}
</div>
<div class="col-xs-8 Lms-explore-release-entry-name">
${name}
${<if-has-artists>}<small>${artists}</small>${</if-has-artists>}
<div class="Lms-trackname text-primary">${name}</div>
${<if-has-artists>}${artists}${</if-has-artists>}
</div>
<div class="col-xs-3">
<div class="Lms-explore-release-entry-controls">
${play-btn}${add-btn}
</div>
<div class="Lms-explore-release-entry-track-duration">
<div class="Lms-explore-release-entry-track-duration text-muted">
${duration}
</div>
</div>
@@ -43,7 +48,7 @@
</message>
<message id="Lms.Explore.Release.template.entry-artist">
${artist class="Lms-explore-artistname"}
<small>${artist class="Lms-artistname text-muted"}</small>
</message>
</messages>
+4 -4
View File
@@ -3,17 +3,17 @@
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Explore.ReleaseLink.template">
<div class="row Lms-explore-releaselink">
<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"}
${cover class="media-object Lms-cover"}
</div>
<div class="media-body media-middle">
<div class="media-heading">
${release-name class="Lms-explore-releasename"}
${release-name class="Lms-releasename text-primary"}
</div>
${<if-has-artist>}<small>${artist-name class="Lms-explore-artistname Lms-explore-releaselink-artist"}</small>${</if-has-artist>}
${<if-has-artist>}<small>${artist-name class="Lms-artistname text-muted"}</small>${</if-has-artist>}
</div>
</div>
</div>
+4 -4
View File
@@ -22,17 +22,17 @@
</message>
<message id="Lms.Explore.Releases.template.entry">
<div class="row Lms-explore-releases-entry">
<div class="row Lms-explore-releases-entry Lms-entry-hoverable">
<div class="col-xs-12">
<div class="media">
<div class="media-left media-middle">
${cover class="media-object"}
${cover class="media-object Lms-cover"}
</div>
<div class="media-body media-middle">
<h4 class="media-heading">
${release-name class="Lms-explore-releasename"}
${release-name class="Lms-releasename text-primary"}
</h4>
${<if-has-artist>}<h4><small>${artist-name class="Lms-explore-artistname"}</small></h4>${</if-has-artist>}
${<if-has-artist>}<h4><small>${artist-name class="Lms-artistname text-muted"}</small></h4>${</if-has-artist>}
${play-btn}${add-btn}
</div>
</div>
+17 -3
View File
@@ -4,16 +4,30 @@
<!--FORMS message blocks-->
<message id="Lms.Settings.template">
<legend>${tr:Lms.Settings.appearance}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label" for="${id:dark-mode}">
${tr:Lms.Settings.dark-mode}
</label>
<div class="col-sm-5">
<div class="checkbox"><label>${dark-mode}</label></div>
</div>
<div class="col-sm-5 help-block">
${dark-mode-info}
</div>
</div>
</div>
<legend>${tr:Lms.Settings.audio}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:transcoding-enable}">
<label class="col-sm-2 control-label" for="${id:transcoding-enable}">
${tr:Lms.Settings.transcoding-enable}
</label>
<div class="col-sm-5">
${transcoding-enable}
<div class="checkbox"><label>${transcoding-enable}</label></div>
</div>
<div class="help-block col-sm-5">
<div class="col-sm-5 help-block">
${transcoding-enable-info}
</div>
</div>
+1 -1
View File
@@ -2,7 +2,7 @@
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.template">
${navbar-top class="main-nav"}
${navbar-top class="Lms-navbar"}
<div class="container">
${contents class="Lms-contents"}
</div>
+15 -8
View File
@@ -23,19 +23,26 @@
</message>
<message id="Lms.Explore.Tracks.template.entry">
<div class="row Lms-explore-tracks-entry Lms-vertical-align">
<div class="row Lms-explore-tracks-entry Lms-entry-hoverable Lms-vertical-align ${is-playing}">
<div class="col-xs-9 Lms-explore-tracks-entry-name">
<h4>${name}</h4>
${<if-has-artists-or-release>}
${<if-has-release>}<small>${release class="Lms-explore-releasename"}</small>${</if-has-release>}
${<if-has-artists>}<small>${artists}</small>${</if-has-artists>}
${</if-has-artists-or-release>}
<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 class="col-xs-3">
<div class="Lms-explore-tracks-entry-controls">
${play-btn}${add-btn}
</div>
<div class="Lms-explore-tracks-entry-duration">
<div class="Lms-explore-tracks-entry-duration text-muted">
${duration}
</div>
</div>
@@ -43,7 +50,7 @@
</message>
<message id="Lms.Explore.Tracks.template.entry-artist">
${artist class="Lms-explore-artistname"}
<small>${artist class="Lms-artistname text-muted"}</small>
</message>
</messages>
+1 -1
View File
@@ -11,7 +11,7 @@
<message id="Lms.Explore.TracksInfo.template.entry">
<div class="row Lms-explore-tracksinfo-entry Lms-vertical-align">
<div class="col-xs-12">
<div class="col-xs-12 Lms-trackname">
${name}
</div>
</div>
-3
View File
@@ -34,9 +34,6 @@ deploy-path = "/";
# Acoustic brainz's root API
acousticbrainz-api-url = "https://acousticbrainz.org/api/v1/";
# API
api-subsonic = true;
# Turn on this option to allow the demo account creation/use
demo = false;
+11
View File
File diff suppressed because one or more lines are too long
+11
View File
File diff suppressed because one or more lines are too long
+41
View File
@@ -0,0 +1,41 @@
:root {
--body-color: #ffffff;
--hover-color: #00bc8c;
--playing-background-color: #375a7f4f;
--slider-background-color: #5a5a5a;
--player-background-color: #303030;
}
.Lms-artistname.text-muted {
font-style: italic;
color: #7e7e7e;
}
.Lms-artistname.text-primary, .Lms-artistname.text-primary:hover {
color: var(--body-color)
}
a.Lms-artistname.text-primary:hover {
color: var(--hover-color)
}
.Lms-control-btn {
color: var(--body-color)
}
.Lms-releasename.text-primary, .Lms-releasename.text-primary:hover {
color: var(--body-color)
}
a.Lms-releasename.text-primary:hover {
color: var(--hover-color)
}
.Lms-trackname.text-primary, .Lms-trackname.text-primary:hover {
color: var(--body-color)
}
.media, .media-body {
overflow: visible;
}
+17
View File
@@ -0,0 +1,17 @@
:root {
--body-color: #2c3e50;
--hover-color: #18bc9c;
--playing-background-color: #2c3e5029;
--slider-background-color: #2c3e5029;
--player-background-color: #ecf0f1;
}
.Lms-artistname.text-muted {
font-style: italic;
color: #7e8f9e;
}
.media, .media-body {
overflow: visible;
}
+116 -223
View File
@@ -1,26 +1,9 @@
/* Stylized for flaty/darkly themes */
a, a:link, a:active, a:visited {
text-decoration: none;
cursor: pointer;
}
a:hover {
color: #337ab7;
}
/* Base style for artist names */
.Lms-explore-artistname
{
color: darkgrey;
font-style: italic;
}
/* Base style for release names */
.Lms-explore-releasename
{
color: black;
font-style: normal;
}
.Lms-vertical-align {
display: flex;
align-items: center;
@@ -52,10 +35,6 @@ a:hover {
margin-bottom: 8px;
}
.Lms-admin-users-entry:hover {
background-color: lightgrey;
}
.Lms-admin-users-entry-role {
margin-left: 8px;
}
@@ -64,101 +43,85 @@ a:hover {
margin-right: 8px;
}
.Lms-artistname {
color: var(--body-color);
}
a.Lms-artistname:hover, a.Lms-artistname:focus {
color: var(--hover-color);
}
.Lms-cluster {
display: inline;
display: inline-block;
cursor: pointer;
line-height: 1.3;
margin-right: 4px;
margin-bottom: 4px;
}
.Lms-cluster:hover {
filter: brightness(85%);
filter: brightness(130%);
}
.Lms-cluster-type-0 {
background-color: darkslategrey;
margin-bottom: 4px;
.Lms-control-btn:hover {
color: var(--hover-color);
cursor: pointer;
}
.Lms-cluster-type-1 {
background-color: darkblue;
margin-bottom: 4px;
.Lms-cover:hover {
filter: brightness(130%);
}
.Lms-cluster-type-2 {
background-color: darkgrey;
margin-bottom: 4px;
.Lms-cover-smaller {
width: 64px; /* some cover may not be square */
border-radius: 3px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.Lms-cluster-type-3 {
background-color: darkorchid;
margin-bottom: 4px;
.Lms-cover-small {
width: 96px; /* some cover may not be square */
border-radius: 3px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.Lms-cluster-type-4 {
background-color: darkgoldenrod;
margin-bottom: 4px;
.Lms-cover-medium {
width: 128px;
border-radius: 3px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.Lms-cluster-type-5 {
background-color: darkkhaki;
margin-bottom: 4px;
.Lms-cover-large {
border-radius: 3px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.Lms-cluster-type-6 {
background-color: darkolivegreen;
margin-bottom: 4px;
.Lms-entry-playing {
font-weight: bold;
border-radius: 3px;
background-color: var(--playing-background-color);
}
.Lms-cluster-type-7 {
background-color: darkred;
margin-bottom: 4px;
}
.Lms-cluster-type-8 {
background-color: darkviolet;
margin-bottom: 4px;
}
.Lms-cluster-type-9 {
background-color: darkturquoise;
margin-bottom: 4px;
.Lms-explore-btn {
font-size: 24px !important;
text-align: left;
width: 28px;
}
.Lms-explore-contents {
margin-bottom: 24px;
}
.Lms-explore-filters {
background-color: #f5f5f5;
padding: 8px;
border-radius: 8px;
margin-bottom: 16px;
}
.Lms-explore-info {
background-color: #f5f5f5;
padding: 8px;
border-radius: 8px;
}
.Lms-explore-releases-entry {
padding-top: 8px;
padding-bottom: 8px;
margin: 0px;
}
.Lms-explore-releases-entry:hover {
background-color: lightgrey;
}
.Lms-explore-release-cover {
max-width: 100%;
border-radius: 8px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.Lms-explore-release-entry-tracknumber {
color: darkgrey;
text-align: center;
text-align: right;
white-space: nowrap;
}
@@ -169,15 +132,7 @@ a:hover {
}
.Lms-explore-release-entry {
border-bottom-style: solid;
border-bottom-color: lightgrey;
border-bottom-width: thin;
min-height: 32px;
margin: 0px;
}
.Lms-explore-release-entry:hover {
background-color: lightgrey;
}
.Lms-explore-release-entry:hover .Lms-explore-release-entry-controls {
@@ -193,12 +148,7 @@ a:hover {
display: none;
}
.Lms-explore-release-entry-playing {
font-weight: bold;
}
.Lms-explore-release-entry-track-duration {
color: darkgrey;
text-align: right;
white-space: nowrap;
display: block;
@@ -210,25 +160,11 @@ a:hover {
padding-bottom: 4px;
}
.Lms-explore-releaselink:hover {
background-color: lightgrey;
}
.Lms-explore-releaselink-artist {
color: dimgrey;
}
.Lms-explore-artists-entry {
min-height: 48px;
margin: 0px;
}
.Lms-explore-artists-entry:hover {
background-color: lightgrey;
}
.Lms-explore-artists-entry-name {
color: black;
font-style: normal;
}
@@ -239,11 +175,6 @@ a:hover {
.Lms-explore-artist-entry {
padding-top: 8px;
padding-bottom: 8px;
margin: 0px;
}
.Lms-explore-artist-entry:hover {
background-color: lightgrey;
}
.Lms-explore-artistlink {
@@ -251,25 +182,8 @@ a:hover {
margin: 0px;
}
.Lms-explore-artistlink:hover {
background-color: lightgrey;
}
.Lms-explore-artistlink-name {
color: black;
font-style: normal;
}
.Lms-explore-tracks-entry {
min-height: 32px;
border-bottom-style: solid;
border-bottom-color: lightgrey;
border-bottom-width: thin;
margin: 0px;
}
.Lms-explore-tracks-entry:hover {
background-color: lightgrey;
}
.Lms-explore-tracks-entry:hover .Lms-explore-tracks-entry-controls {
@@ -291,7 +205,6 @@ a:hover {
}
.Lms-explore-tracks-entry-duration {
color: darkgrey;
text-align: right;
white-space: nowrap;
display: block;
@@ -302,30 +215,17 @@ a:hover {
margin: 0px;
}
.Lms-explore-tracksinfo-entry:hover {
background-color: lightgrey;
.Lms-navbar {
border-radius: 0px;
}
.Lms-explore-btn {
font-size: 22px !important;
cursor: pointer;
}
.Lms-explore-btn:hover {
color: #337ab7;
.Lms-playqueue-btn {
font-size: 24px !important;
text-align: left;
}
.Lms-playqueue-entry {
border-bottom-style: solid;
border-bottom-color: lightgrey;
border-bottom-width: thin;
min-height: 32px;
margin: 0px;
}
.Lms-playqueue-entry:hover {
background-color: lightgrey;
}
.Lms-playqueue-entry:hover .Lms-playqueue-entry-controls {
@@ -336,18 +236,9 @@ a:hover {
display: none;
}
.Lms-playqueue-btn {
font-size: 22px !important;
cursor: pointer;
margin: 2px;
}
.Lms-playqueue-btn:hover {
color: #337ab7;
}
.Lms-playqueue-btn-selected {
color: #337ab7;
.text-success .Lms-playqueue-btn {
filter: brightness(130%);
filter: drop-shadow(0 0 0.65rem var(--hover-color));
}
.Lms-playqueue-entry-controls {
@@ -356,7 +247,6 @@ a:hover {
}
.Lms-playqueue-entry-duration {
color: darkgrey;
text-align: right;
white-space: nowrap;
display: block;
@@ -368,7 +258,9 @@ a:hover {
}
.Lms-playqueue-selected {
font-weight: 800;
font-weight: bold;
border-radius: 3px;
background-color: var(--playing-background-color);
}
.Lms-playqueue-selected h4 {
@@ -376,15 +268,8 @@ a:hover {
}
.Lms-playhistory-entry {
border-bottom-style: solid;
border-bottom-color: lightgrey;
border-bottom-width: thin;
min-height: 32px;
margin: 0px;
}
.Lms-playhistory-entry:hover {
background-color: lightgrey;
margin-bottom: 0px;
}
.Lms-playhistory-entry-name {
@@ -393,24 +278,14 @@ a:hover {
}
.Lms-player {
z-index: 3;
position: fixed;
bottom: 0;
width: 100%;
height: 70px;
background-color: #f5f5f5;
box-shadow: 0px -2px 8px #f5f5f5;
}
.Lms-player-artistname {
color: dimgrey;
}
.Lms-player-btn {
cursor: pointer;
}
.Lms-player-btn:hover {
color: #337ab7;
padding: 0px;
margin: 0px;
background-color: var(--player-background-color);
}
.Lms-player-btn-volume {
@@ -421,16 +296,42 @@ a:hover {
.Lms-player-btn-controls {
font-size: 40px !important;
text-align: center;
width: 0.9em;
width: 1.1em;
}
.Lms-player-controls {
text-align: center;
white-space: nowrap;
}
.Lms-player-duration {
line-height: 40px;
text-align: right;
color: darkgrey;
white-space: nowrap;
}
.Lms-player-progress {
background-color: var(--slider-background-color);
}
.Lms-player-seek {
cursor: pointer;
z-index: 1;
touch-action: manipulation;
-webkit-appearance: none;
-moz-appearance: none;
background: transparent;
border-style: solid;
border-width: thin;
}
.Lms-player-seek::-webkit-slider-thumb {
-webkit-appearance: none;
-moz-appearance: none;
height: 0px;
width: 0px;
}
.Lms-player-seek-container {
margin-top: 8px;
margin-bottom: 8px;
@@ -445,22 +346,7 @@ a:hover {
top: 0px;
left: 0px;
height: 8px;
}
.Lms-player-seek {
cursor: pointer;
z-index: 1;
touch-action: manipulation;
-webkit-appearance: none;
-moz-appearance: none;
background: transparent;
}
.Lms-player-seek::-webkit-slider-thumb {
-webkit-appearance: none;
-moz-appearance: none;
height: 0px;
width: 0px;
border-radius: 3px;
}
.Lms-player-seek::-moz-range-thumb {
@@ -470,15 +356,6 @@ a:hover {
width: 0px;
}
.Lms-player-progress {
background-color: darkgrey;
}
.Lms-player-controls {
text-align: center;
white-space: nowrap;
}
.Lms-player-text-center {
text-align: center;
white-space: nowrap;
@@ -497,10 +374,20 @@ a:hover {
display: flex;
}
.Lms-player-volume-slider-container {
display: inline-block;
width: 85px;
margin: auto;
.Lms-player-volume-slider::-webkit-slider-thumb {
background-color: var(--body-color);
}
.Lms-player-volume-slider::-webkit-slider-thumb:hover {
background-color: var(--hover-color);
}
.Lms-player-volume-slider::-moz-range-thumb {
background-color: var(--body-color);
}
.Lms-player-volume-slider::-moz-range-thumb:hover {
background-color: var(--hover-color);
}
.Lms-player-volume-slider {
@@ -508,10 +395,12 @@ a:hover {
appearance: none;
width: 100%;
height: 8px;
background: lightgrey;
outline: none;
cursor: pointer;
border-radius: 4px;
border-radius: 3px;
border-style: solid;
border-width: thin;
background-color: var(--slider-background-color);
}
.Lms-player-volume-slider::-webkit-slider-thumb {
@@ -520,21 +409,25 @@ a:hover {
width: 14px;
height: 14px;
border-radius: 7px;
background: darkgrey;
}
.Lms-player-volume-slider::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 7px;
background: darkgrey;
}
.Lms-player-volume-slider::-webkit-slider-thumb:hover {
background: #337ab7;
.Lms-player-volume-slider-container {
display: inline-block;
width: 85px;
margin: auto;
}
.Lms-player-volume-slider::-moz-range-thumb:hover {
background: #337ab7;
.Lms-releasename {
color: var(--body-color);
}
a.Lms-releasename:hover, a.Lms-releasename:focus {
color: var(--hover-color);
}
+1 -1
View File
@@ -189,7 +189,7 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, st
trackPath = track->getPath();
auto release {track->getRelease()};
if (release && release->getTotalDiscNumber() > 1)
if (release && release->getTotalDisc() > 1)
isMultiDisc = true;
}
}
+1
View File
@@ -11,6 +11,7 @@ add_library(lmsdatabase SHARED
impl/Session.cpp
impl/SessionPool.cpp
impl/SqlQuery.cpp
impl/SubsonicSettings.cpp
impl/Track.cpp
impl/TrackBookmark.cpp
impl/User.cpp
+9 -1
View File
@@ -33,9 +33,9 @@ Db::Db(const std::filesystem::path& dbPath)
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
std::unique_ptr<Wt::Dbo::backend::Sqlite3> connection {std::make_unique<Wt::Dbo::backend::Sqlite3>(dbPath.string())};
// connection->setProperty("show-queries", "true");
connection->executeSql("pragma journal_mode=WAL");
connection->executeSql("pragma synchronous=normal");
// connection->setProperty("show-queries", "true");
auto connectionPool = std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), 10);
connectionPool->setTimeout(std::chrono::seconds(10));
@@ -43,6 +43,14 @@ Db::Db(const std::filesystem::path& dbPath)
_connectionPool = std::move(connectionPool);
}
void
Db::executeSql(const std::string& sql)
{
auto connection {_connectionPool->getConnection()};
connection->executeSql(sql);
_connectionPool->returnConnection(std::move(connection));
}
} // namespace Database
+19 -5
View File
@@ -268,15 +268,29 @@ Release::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limi
std::optional<std::size_t>
Release::getTotalTrackNumber(void) const
Release::getTotalTrack(void) const
{
return (_totalTrackNumber > 0) ? std::make_optional<std::size_t>(_totalTrackNumber) : std::nullopt;
assert(session());
assert(IdIsValid(self()->id()));
int res = session()->query<int>("SELECT COALESCE(MAX(total_track),0) FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?")
.bind(this->id());
return (res > 0) ? std::make_optional<std::size_t>(res) : std::nullopt;
}
std::optional<std::size_t>
Release::getTotalDiscNumber(void) const
Release::getTotalDisc(void) const
{
return (_totalDiscNumber > 0) ? std::make_optional<std::size_t>(_totalDiscNumber) : std::nullopt;
assert(session());
assert(IdIsValid(self()->id()));
int res = session()->query<int>("SELECT COALESCE(MAX(total_disc),0) FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?")
.bind(this->id());
return (res > 0) ? std::make_optional<std::size_t>(res) : std::nullopt;
}
std::optional<int>
@@ -287,7 +301,7 @@ Release::getReleaseYear(bool original) const
const std::string field {original ? "original_year" : "year"};
Wt::Dbo::collection<int> dates = session()->query<int>(
std::string{"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?")
.groupBy(field)
.bind(this->id());
+68 -20
View File
@@ -31,6 +31,7 @@
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/ScanSettings.hpp"
#include "database/SubsonicSettings.hpp"
#include "database/Track.hpp"
#include "database/TrackBookmark.hpp"
#include "database/TrackArtistLink.hpp"
@@ -40,7 +41,7 @@
namespace Database {
#define LMS_DATABASE_VERSION 15
#define LMS_DATABASE_VERSION 19
using Version = std::size_t;
@@ -83,26 +84,31 @@ class VersionInfo
void
Session::doDatabaseMigrationIfNeeded()
{
auto uniqueTransaction {createUniqueTransaction()};
static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"};
Version version;
try
{
version = VersionInfo::getOrCreate(*this)->getVersion();
LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION;
if (version == LMS_DATABASE_VERSION)
return;
}
catch (std::exception& e)
{
LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what();
throw LmsException {outdatedMsg};
}
Db::ScopedNoForeignKeys noPragmaKeys {_db};
while (version < LMS_DATABASE_VERSION)
while (1)
{
auto uniqueTransaction {createUniqueTransaction()};
Version version;
try
{
version = VersionInfo::getOrCreate(*this)->getVersion();
LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION;
if (version == LMS_DATABASE_VERSION)
{
LMS_LOG(DB, DEBUG) << "Lms database version " << LMS_DATABASE_VERSION << ": up to date!";
return;
}
}
catch (std::exception& e)
{
LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what();
throw LmsException {outdatedMsg};
}
LMS_LOG(DB, INFO) << "Migrating database from version " << version << "...";
if (version == 5)
@@ -169,15 +175,55 @@ CREATE TABLE IF NOT EXISTS "track_bookmark" (
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 15)
{
_session.execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultUITheme)) + ")");
}
else if (version == 16)
{
_session.execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)");
_session.execute("ALTER TABLE track ADD total_track INTEGER NOT NULL DEFAULT(0)");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 17)
{
// Drop colums total_disc/total_track from release
_session.execute(R"(
CREATE TABLE "release_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null,
"mbid" text not null
))");
_session.execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release");
_session.execute("DROP TABLE release;");
_session.execute("ALTER TABLE release_backup RENAME TO release");
_session.execute("CREATE INDEX release_name_idx ON release(name)");
_session.execute("CREATE INDEX release_name_nocase_idx ON release(name COLLATE NOCASE)");
_session.execute("CREATE INDEX release_mbid_idx ON release(mbid)");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 18)
{
_session.execute(R"(
CREATE TABLE IF NOT EXISTS "subsonic_settings" (
"id" integer primary key autoincrement,
"version" integer not null,
"api_enabled" boolean not null,
"artist_list_mode" integer not null
))");
}
else
{
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
throw LmsException { LMS_DATABASE_VERSION > version ? outdatedMsg : "Server binary outdated, please upgrade it to handle this database"};
}
++version;
VersionInfo::get(*this).modify()->setVersion(LMS_DATABASE_VERSION);
VersionInfo::get(*this).modify()->setVersion(++version);
}
}
@@ -193,6 +239,7 @@ Session::Session(Db& db)
_session.mapClass<ClusterType>("cluster_type");
_session.mapClass<Release>("release");
_session.mapClass<ScanSettings>("scan_settings");
_session.mapClass<SubsonicSettings>("subsonic_settings");
_session.mapClass<Track>("track");
_session.mapClass<TrackBookmark>("track_bookmark");
_session.mapClass<TrackArtistLink>("track_artist_link");
@@ -318,6 +365,7 @@ Session::prepareTables()
auto uniqueTransaction {createUniqueTransaction()};
ScanSettings::init(*this);
SubsonicSettings::init(*this);
}
}
@@ -0,0 +1,48 @@
/*
* 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 "database/SubsonicSettings.hpp"
#include "database/Session.hpp"
namespace Database {
void
SubsonicSettings::init(Session& session)
{
session.checkUniqueLocked();
pointer settings {get(session)};
if (settings)
return;
settings = session.getDboSession().add(std::make_unique<SubsonicSettings>());
}
SubsonicSettings::pointer
SubsonicSettings::get(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().find<SubsonicSettings>();
}
} // ns Database
+16 -4
View File
@@ -177,7 +177,7 @@ Track::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
}
std::vector<Cluster::pointer>
Track::getClusters(void) const
Track::getClusters() const
{
std::vector< Cluster::pointer > clusters;
std::copy(_clusters.begin(), _clusters.end(), std::back_inserter(clusters));
@@ -185,7 +185,7 @@ Track::getClusters(void) const
}
std::vector<IdType>
Track::getClusterIds(void) const
Track::getClusterIds() const
{
assert(self());
assert(IdIsValid(self()->id()));
@@ -355,17 +355,29 @@ Track::setFeatures(const Wt::Dbo::ptr<TrackFeatures>& features)
}
std::optional<std::size_t>
Track::getTrackNumber(void) const
Track::getTrackNumber() const
{
return (_trackNumber > 0) ? std::make_optional<std::size_t>(_trackNumber) : std::nullopt;
}
std::optional<std::size_t>
Track::getDiscNumber(void) const
Track::getTotalTrack() const
{
return (_totalTrack > 0) ? std::make_optional<std::size_t>(_totalTrack) : std::nullopt;
}
std::optional<std::size_t>
Track::getDiscNumber() const
{
return (_discNumber > 0) ? std::make_optional<std::size_t>(_discNumber) : std::nullopt;
}
std::optional<std::size_t>
Track::getTotalDisc() const
{
return (_totalDisc > 0) ? std::make_optional<std::size_t>(_totalDisc) : std::nullopt;
}
std::optional<int>
Track::getYear() const
{
+19
View File
@@ -39,6 +39,25 @@ class Db
std::shared_mutex& getMutex() { return _sharedMutex; }
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
class ScopedNoForeignKeys
{
public:
ScopedNoForeignKeys(Db& db) : _db {db}
{
_db.executeSql("PRAGMA foreign_keys=OFF");
}
~ScopedNoForeignKeys()
{
_db.executeSql("PRAGMA foreign_keys=ON");
}
private:
Db& _db;
};
void executeSql(const std::string& sql);
std::shared_mutex _sharedMutex;
std::unique_ptr<Wt::Dbo::SqlConnectionPool> _connectionPool;
};
+2 -10
View File
@@ -84,15 +84,11 @@ class Release : public Wt::Dbo::Dbo<Release>
std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const;
// Modifiers
void setTotalDiscNumber(std::size_t num) { _totalDiscNumber = static_cast<int>(num); }
void setTotalTrackNumber(std::size_t num) { _totalTrackNumber = static_cast<int>(num); }
// Accessors
const std::string& getName() const { return _name; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
std::optional<std::size_t> getTotalTrackNumber() const;
std::optional<std::size_t> getTotalDiscNumber() const;
std::optional<std::size_t> getTotalTrack() const;
std::optional<std::size_t> getTotalDisc() const;
std::chrono::milliseconds getDuration() const;
// Get the artists of this release
@@ -109,8 +105,6 @@ class Release : public Wt::Dbo::Dbo<Release>
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _MBID, "mbid");
Wt::Dbo::field(a, _totalDiscNumber, "total_disc_number");
Wt::Dbo::field(a, _totalTrackNumber, "total_track_number");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
@@ -121,8 +115,6 @@ class Release : public Wt::Dbo::Dbo<Release>
std::string _name;
std::string _MBID;
int _totalDiscNumber {};
int _totalTrackNumber {};
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
Wt::Dbo::collection<Wt::Dbo::ptr<User>> _starringUsers; // Users that starred this release
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/Dbo/Dbo.h>
namespace Database {
class Session;
class SubsonicSettings : public Wt::Dbo::Dbo<SubsonicSettings>
{
public:
using pointer = Wt::Dbo::ptr<SubsonicSettings>;
enum class ArtistListMode
{
AllArtists,
ReleaseArtists,
};
static inline constexpr bool defaultSubsonicAPIEnabled {true};
static inline constexpr ArtistListMode defaultArtistListMode {ArtistListMode::AllArtists};
static void init(Session& session);
static pointer get(Session& session);
// Getters
bool isAPIEnabled() const { return _isAPIEnabled; }
ArtistListMode getArtistListMode() const { return _artistListMode; }
// Setters
void setAPIEnabled(bool enabled) { _isAPIEnabled = enabled; }
void setArtistListMode(ArtistListMode mode) {_artistListMode = mode; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _isAPIEnabled, "api_enabled");
Wt::Dbo::field(a, _artistListMode, "artist_list_mode");
}
private:
bool _isAPIEnabled {defaultSubsonicAPIEnabled};
ArtistListMode _artistListMode {defaultArtistListMode};
};
} // namespace Database
+14 -6
View File
@@ -88,6 +88,8 @@ class Track : public Wt::Dbo::Dbo<Track>
void setScanVersion(std::size_t version) { _scanVersion = version; }
void setTrackNumber(int num) { _trackNumber = num; }
void setDiscNumber(int num) { _discNumber = num; }
void setTotalTrack(std::optional<int> totalTrack) { totalTrack ? _totalTrack = *totalTrack : 0; }
void setTotalDisc(std::optional<int> totalDisc) { totalDisc ? _totalDisc = *totalDisc : 0; }
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
@@ -106,7 +108,9 @@ class Track : public Wt::Dbo::Dbo<Track>
std::size_t getScanVersion() const { return _scanVersion; }
std::optional<std::size_t> getTrackNumber() const;
std::optional<std::size_t> getTotalTrack() const;
std::optional<std::size_t> getDiscNumber() const;
std::optional<std::size_t> getTotalDisc() const;
std::string getName() const { return _name; }
std::filesystem::path getPath() const { return _filePath; }
std::chrono::milliseconds getDuration() const { return _duration; }
@@ -135,6 +139,8 @@ class Track : public Wt::Dbo::Dbo<Track>
Wt::Dbo::field(a, _scanVersion, "scan_version");
Wt::Dbo::field(a, _trackNumber, "track_number");
Wt::Dbo::field(a, _discNumber, "disc_number");
Wt::Dbo::field(a, _totalTrack, "total_track");
Wt::Dbo::field(a, _totalDisc, "total_disc");
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _year, "year");
@@ -160,19 +166,21 @@ class Track : public Wt::Dbo::Dbo<Track>
static const std::size_t _maxCopyrightLength = 128;
static const std::size_t _maxCopyrightURLLength = 128;
int _scanVersion = 0;
int _trackNumber = 0;
int _discNumber = 0;
int _scanVersion {};
int _trackNumber {};
int _discNumber {};
int _totalTrack {};
int _totalDisc {};
std::string _name;
std::string _artistName;
std::string _releaseName;
std::chrono::duration<int, std::milli> _duration;
int _year = 0;
int _originalYear = 0;
int _year {};
int _originalYear {};
std::string _filePath;
Wt::WDateTime _fileLastWrite;
Wt::WDateTime _fileAdded;
bool _hasCover = false;
bool _hasCover {};
std::string _MBID; // Musicbrainz Identifier
std::string _copyright;
std::string _copyrightURL;
+23 -11
View File
@@ -91,14 +91,13 @@ class User : public Wt::Dbo::Dbo<User>
public:
using pointer = Wt::Dbo::ptr<User>;
static const std::size_t MinNameLength = 3;
static const std::size_t MaxNameLength = 15;
// Do not change enum values!
enum class Type
{
REGULAR,
ADMIN,
DEMO
REGULAR = 0,
ADMIN = 1,
DEMO = 2,
};
struct PasswordHash
@@ -107,8 +106,21 @@ class User : public Wt::Dbo::Dbo<User>
std::string hash;
};
// list of audio parameters
static const std::set<Bitrate> audioTranscodeAllowedBitrates;
// Do not change enum values!
enum class UITheme
{
Light = 0,
Dark = 1,
};
static const std::set<Bitrate> audioTranscodeAllowedBitrates;
static inline const std::size_t MinNameLength {3};
static inline const std::size_t MaxNameLength {15};
static inline const bool defaultAudioTranscodeEnable {true};
static inline const AudioFormat defaultAudioTranscodeFormat {AudioFormat::OGG_OPUS};
static inline const Bitrate defaultAudioTranscodeBitrate {128000};
static inline const UITheme defaultUITheme {UITheme::Dark};
User();
User(const std::string& loginName, const PasswordHash& passwordHash);
@@ -138,6 +150,7 @@ class User : public Wt::Dbo::Dbo<User>
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
void setRadio(bool val) { _radio = val; }
void setRepeatAll(bool val) { _repeatAll = val; }
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void clearAuthTokens();
// read
@@ -150,6 +163,7 @@ class User : public Wt::Dbo::Dbo<User>
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
bool isRepeatAllSet() const { return _repeatAll; }
bool isRadioSet() const { return _radio; }
UITheme getUITheme() const { return _uiTheme; }
Wt::Dbo::ptr<TrackList> getPlayedTrackList(Session& session) const;
Wt::Dbo::ptr<TrackList> getQueuedTrackList(Session& session) const;
@@ -184,6 +198,7 @@ class User : public Wt::Dbo::Dbo<User>
Wt::Dbo::field(a, _audioTranscodeEnable, "audio_transcode_enable");
Wt::Dbo::field(a, _audioTranscodeBitrate, "audio_transcode_bitrate");
Wt::Dbo::field(a, _audioTranscodeFormat, "audio_transcode_format");
Wt::Dbo::field(a, _uiTheme, "ui_theme");
// User's dynamic data
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
Wt::Dbo::field(a, _repeatAll, "repeat_all");
@@ -197,14 +212,11 @@ class User : public Wt::Dbo::Dbo<User>
private:
static const bool defaultAudioTranscodeEnable {true};
static const AudioFormat defaultAudioTranscodeFormat {AudioFormat::OGG_OPUS};
static const Bitrate defaultAudioTranscodeBitrate {128000};
std::string _loginName;
std::string _passwordSalt;
std::string _passwordHash;
Wt::WDateTime _lastLogin;
UITheme _uiTheme {defaultUITheme};
// Admin defined settings
int _maxAudioTranscodeBitrate;
+2 -7
View File
@@ -718,13 +718,6 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
stats.updates++;
}
// Release related data
if (release)
{
release.modify()->setTotalTrackNumber(trackInfo->totalTrack ? *trackInfo->totalTrack : 0);
release.modify()->setTotalDiscNumber(trackInfo->totalDisc ? *trackInfo->totalDisc : 0);
}
// Track related data
assert(track);
@@ -744,6 +737,8 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
track.modify()->setAddedTime(Wt::WLocalDateTime::currentServerDateTime().toUTC());
track.modify()->setTrackNumber(trackInfo->trackNumber ? *trackInfo->trackNumber : 0);
track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0);
track.modify()->setTotalTrack(trackInfo->totalTrack);
track.modify()->setTotalDisc(trackInfo->totalDisc);
track.modify()->setYear(trackInfo->year ? *trackInfo->year : 0);
track.modify()->setOriginalYear(trackInfo->originalYear ? *trackInfo->originalYear : 0);
+37 -4
View File
@@ -30,6 +30,7 @@
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/SubsonicSettings.hpp"
#include "database/Track.hpp"
#include "database/TrackBookmark.hpp"
#include "database/TrackList.hpp"
@@ -901,7 +902,23 @@ handleGetArtistsRequest(RequestContext& context)
if (!user)
throw UserNotAuthorizedError {};
auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName)};
std::optional<TrackArtistLink::Type> linkType;
switch (SubsonicSettings::get(context.dbSession)->getArtistListMode())
{
case SubsonicSettings::ArtistListMode::AllArtists:
break;
case SubsonicSettings::ArtistListMode::ReleaseArtists:
linkType = TrackArtistLink::Type::ReleaseArtist;
break;
}
bool more {};
const std::vector<Artist::pointer> artists {Artist::getByFilter(context.dbSession,
{},
{},
linkType,
Artist::SortMethod::BySortName,
{}, {}, more)};
for (const Artist::pointer& artist : artists)
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */));
@@ -1028,7 +1045,23 @@ handleGetIndexesRequest(RequestContext& context)
if (!user)
throw UserNotAuthorizedError {};
auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName)};
std::optional<TrackArtistLink::Type> linkType;
switch (SubsonicSettings::get(context.dbSession)->getArtistListMode())
{
case SubsonicSettings::ArtistListMode::AllArtists:
break;
case SubsonicSettings::ArtistListMode::ReleaseArtists:
linkType = TrackArtistLink::Type::ReleaseArtist;
break;
}
bool more {};
const std::vector<Artist::pointer> artists {Artist::getByFilter(context.dbSession,
{},
{},
linkType,
Artist::SortMethod::BySortName,
{}, {}, more)};
for (const Artist::pointer& artist : artists)
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */));
@@ -1752,7 +1785,7 @@ static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
{"search", {handleNotImplemented, false}},
{"search2", {handleSearch2Request, false}},
{"search3", {handleSearch3Request, false}},
// Playlists
{"getPlaylists", {handleGetPlaylistsRequest, false}},
{"getPlaylist", {handleGetPlaylistRequest, false}},
@@ -1796,7 +1829,7 @@ static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
{"createInternetRadioStation", {handleNotImplemented, false}},
{"updateInternetRadioStation", {handleNotImplemented, false}},
{"deleteInternetRadioStation", {handleNotImplemented, false}},
// Chat
{"getChatMessages", {handleNotImplemented, false}},
{"addChatMessages", {handleNotImplemented, false}},
+2
View File
@@ -4,6 +4,7 @@ add_executable(lms
ui/Auth.cpp
ui/LmsApplication.cpp
ui/LmsApplicationGroup.cpp
ui/LmsTheme.cpp
ui/MediaPlayer.cpp
ui/PlayQueueView.cpp
ui/PlayHistoryView.cpp
@@ -12,6 +13,7 @@ add_executable(lms
ui/admin/DatabaseSettingsView.cpp
ui/admin/DatabaseStatus.cpp
ui/admin/InitWizardView.cpp
ui/admin/SubsonicView.cpp
ui/admin/UserView.cpp
ui/admin/UsersView.cpp
ui/common/Validators.cpp
+8 -2
View File
@@ -28,6 +28,7 @@
#include "av/AvTranscoder.hpp"
#include "cover/ICoverArtGrabber.hpp"
#include "database/Db.hpp"
#include "database/SubsonicSettings.hpp"
#include "scanner/IMediaScanner.hpp"
#include "recommendation/IEngine.hpp"
#include "subsonic/SubsonicResource.hpp"
@@ -169,8 +170,13 @@ int main(int argc, char* argv[])
API::Subsonic::SubsonicResource subsonicResource {database};
// bind API resources
if (ServiceProvider<IConfig>::get()->getBool("api-subsonic", true))
server.addResource(&subsonicResource, subsonicResource.getPath());
{
Database::Session session {database};
auto transaction {session.createSharedTransaction()};
if (Database::SubsonicSettings::get(session)->isAPIEnabled())
server.addResource(&subsonicResource, subsonicResource.getPath());
}
// bind UI entry point
server.addEntryPoint(Wt::EntryPointType::Application,
+55 -17
View File
@@ -20,7 +20,6 @@
#include "LmsApplication.hpp"
#include <Wt/WAnchor.h>
#include <Wt/WBootstrapTheme.h>
#include <Wt/WEnvironment.h>
#include <Wt/WMenu.h>
#include <Wt/WNavigationBar.h>
@@ -44,11 +43,13 @@
#include "admin/DatabaseSettingsView.hpp"
#include "admin/UserView.hpp"
#include "admin/UsersView.hpp"
#include "admin/SubsonicView.hpp"
#include "resource/AudioFileResource.hpp"
#include "resource/AudioTranscodeResource.hpp"
#include "resource/ImageResource.hpp"
#include "Auth.hpp"
#include "LmsApplicationException.hpp"
#include "LmsTheme.hpp"
#include "MediaPlayer.hpp"
#include "PlayHistoryView.hpp"
#include "PlayQueueView.hpp"
@@ -63,7 +64,7 @@ LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplica
return std::make_unique<LmsApplication>(env, db, appGroups);
}
LmsApplication*
LmsApplication*
LmsApplication::instance()
{
return reinterpret_cast<LmsApplication*>(Wt::WApplication::instance());
@@ -115,21 +116,16 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
_dbSession {db},
_appGroups {appGroups}
{
auto bootstrapTheme = std::make_unique<Wt::WBootstrapTheme>();
bootstrapTheme->setVersion(Wt::BootstrapVersion::v3);
bootstrapTheme->setResponsive(true);
setTheme(std::move(bootstrapTheme));
addMetaHeader(Wt::MetaHeaderType::Meta, "viewport", "width=device-width, user-scalable=no");
useStyleSheet("css/lms.css");
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
// Add a resource bundle
messageResourceBundle().use(appRoot() + "admin-database");
messageResourceBundle().use(appRoot() + "admin-initwizard");
messageResourceBundle().use(appRoot() + "admin-subsonic");
messageResourceBundle().use(appRoot() + "admin-user");
messageResourceBundle().use(appRoot() + "admin-users");
messageResourceBundle().use(appRoot() + "admin-initwizard");
messageResourceBundle().use(appRoot() + "artist");
messageResourceBundle().use(appRoot() + "artistinfo");
messageResourceBundle().use(appRoot() + "artistlink");
@@ -173,11 +169,26 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
if (firstConnection)
{
setTheme(std::make_unique<LmsTheme>(Database::User::defaultUITheme));
root()->addWidget(std::make_unique<InitWizardView>());
return;
}
const auto userId {processAuthToken(env)};
{
Database::User::UITheme theme {Database::User::defaultUITheme};
if (userId)
{
auto transaction {_dbSession.createSharedTransaction()};
const auto user {Database::User::getById(_dbSession, *userId)};
if (user)
theme = user->getUITheme();
}
setTheme(std::make_unique<LmsTheme>(theme));
}
if (userId)
{
try
@@ -200,6 +211,16 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
Auth* auth {root()->addNew<Auth>()};
auth->userLoggedIn.connect(this, [this](Database::IdType userId)
{
{
auto transaction {_dbSession.createSharedTransaction()};
const auto user {Database::User::getById(_dbSession, userId)};
if (user)
{
LmsTheme* lmsTheme {static_cast<LmsTheme*>(LmsApp->theme().get())};
lmsTheme->setTheme(user->getUITheme());
}
}
handleUserLoggedIn(userId, true);
});
}
@@ -263,18 +284,27 @@ LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool add
return res;
}
std::unique_ptr<Wt::WTemplate>
std::unique_ptr<Wt::WText>
LmsApplication::createCluster(Database::Cluster::pointer cluster, bool canDelete)
{
std::string styleClass = "Lms-cluster-type-" + std::to_string(cluster->getType().id() % 10);
auto getStyleClass = [](const Database::Cluster::pointer cluster)
{
switch (cluster->getType().id() % 6)
{
case 0: return "label-primary";
case 1: return "label-default";
case 2: return "label-info";
case 3: return "label-warning";
case 4: return "label-success";
case 5: return "label-danger";
}
return "label-default";
};
auto res = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.template.cluster-entry").arg(styleClass));
res->addFunction("tr", &Wt::WTemplate::Functions::tr);
const std::string styleClass {getStyleClass(cluster)};
auto res {std::make_unique<Wt::WText>(std::string {} + (canDelete ? "<i class=\"fa fa-times-circle\"></i> " : "") + Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::UnsafeXHTML)};
res->bindString("name", Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::Plain);
res->setCondition("if-can-delete", canDelete);
res->setStyleClass("Lms-cluster");
res->setStyleClass("Lms-cluster label " + styleClass);
res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain);
res->setInline(true);
@@ -314,6 +344,7 @@ enum IdxRoot
IdxAdminDatabase,
IdxAdminUsers,
IdxAdminUser,
IdxAdminSubsonic,
};
static void
@@ -337,6 +368,7 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
{ "/admin/database", IdxAdminDatabase, true },
{ "/admin/users", IdxAdminUsers, true },
{ "/admin/user", IdxAdminUser, true },
{ "/admin/subsonic", IdxAdminSubsonic, true },
};
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
@@ -456,6 +488,10 @@ LmsApplication::createHome()
usersSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/users"));
usersSettings->setSelectable(false);
auto subsonicSettings = admin->insertItem(2, Wt::WString::tr("Lms.Admin.Subsonic.subsonic"));
subsonicSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/subsonic"));
subsonicSettings->setSelectable(false);
menuItem->setMenu(std::move(admin));
}
@@ -473,6 +509,7 @@ LmsApplication::createHome()
// Contents
// Order is important in mainStack, see IdxRoot!
Wt::WStackedWidget* mainStack = main->bindNew<Wt::WStackedWidget>("contents");
mainStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
Explore* explore = mainStack->addNew<Explore>();
PlayQueue* playqueue = mainStack->addNew<PlayQueue>();
@@ -485,6 +522,7 @@ LmsApplication::createHome()
mainStack->addNew<DatabaseSettingsView>();
mainStack->addNew<UsersView>();
mainStack->addNew<UserView>();
mainStack->addNew<SubsonicView>();
}
explore->tracksAdd.connect([=] (const std::vector<Database::IdType>& trackIds)
+1 -1
View File
@@ -103,7 +103,7 @@ class LmsApplication : public Wt::WApplication
static std::unique_ptr<Wt::WAnchor> createArtistAnchor(Wt::Dbo::ptr<Database::Artist> artist, bool addText = true);
static Wt::WLink createReleaseLink(Wt::Dbo::ptr<Database::Release> release);
static std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Wt::Dbo::ptr<Database::Release> release, bool addText = true);
static std::unique_ptr<Wt::WTemplate> createCluster(Wt::Dbo::ptr<Database::Cluster> cluster, bool canDelete = false);
static std::unique_ptr<Wt::WText> createCluster(Wt::Dbo::ptr<Database::Cluster> cluster, bool canDelete = false);
// Signal emitted just before the session ends (user may already be logged out)
Wt::Signal<>& preQuit() { return _preQuit; }
+107
View File
@@ -0,0 +1,107 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "LmsTheme.hpp"
#include "utils/Logger.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
LmsTheme::LmsTheme(Database::User::UITheme theme)
: _theme {theme}
{
setVersion(Wt::BootstrapVersion::v3);
setResponsive(true);
applyMetaHeader();
}
void
LmsTheme::applyMetaHeader() const
{
switch (_theme)
{
case Database::User::UITheme::Dark:
LmsApp->addMetaHeader("theme-color", "#303030");
break;
case Database::User::UITheme::Light:
LmsApp->removeMetaHeader(Wt::MetaHeaderType::Meta, "theme-color");
break;
}
}
void
LmsTheme::setTheme(Database::User::UITheme theme)
{
// Hack, use the application interface directly since changing theme is not allowed
const auto currentStyleSheets {getStyleSheets(_theme)};
for (auto it {std::crbegin(currentStyleSheets)}; it != std::crend(currentStyleSheets); ++it)
{
LMS_LOG(UI, DEBUG) << "Removing css file '" << it->url() << "'";
LmsApp->removeStyleSheet(*it);
}
_theme = theme;
for (const auto& styleSheet : getStyleSheets(theme))
{
LMS_LOG(UI, DEBUG) << "Adding css file '" << styleSheet.url() << "'";
LmsApp->useStyleSheet(styleSheet);
}
applyMetaHeader();
}
std::vector<Wt::WLinkedCssStyleSheet>
LmsTheme::styleSheets() const
{
const std::vector<Wt::WLink> styleSheets {getStyleSheets(_theme)};
std::vector<Wt::WLinkedCssStyleSheet> res;
res.reserve(styleSheets.size());
std::transform(std::cbegin(styleSheets), std::cend(styleSheets), std::back_inserter(res), [](const Wt::WLink& styleSheet) { return Wt::WLinkedCssStyleSheet {styleSheet}; });
return res;
}
std::vector<Wt::WLink>
LmsTheme::getStyleSheets(Database::User::UITheme theme)
{
switch (theme)
{
case Database::User::UITheme::Dark:
return
{
{"css/bootstrap-darkly.min.css"},
{"resources/themes/bootstrap/3/wt.css"},
{"css/lms.css"},
{"css/lms-darkly.css"},
};
case Database::User::UITheme::Light:
return
{
{"css/bootstrap-flatly.min.css"},
{"resources/themes/bootstrap/3/wt.css"},
{"css/lms.css"},
{"css/lms-flatly.css"},
};
}
return {};
}
}
+47
View File
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <vector>
#include <Wt/WBootstrapTheme.h>
#include <Wt/WLinkedCssStyleSheet.h>
#include "database/User.hpp"
namespace UserInterface {
class LmsTheme : public Wt::WBootstrapTheme
{
public:
LmsTheme(Database::User::UITheme theme);
void setTheme(Database::User::UITheme theme);
private:
void applyMetaHeader() const;
std::vector<Wt::WLinkedCssStyleSheet> styleSheets() const override;
static std::vector<Wt::WLink> getStyleSheets(Database::User::UITheme theme);
Database::User::UITheme _theme;
};
}
+14
View File
@@ -28,6 +28,7 @@
#include "database/TrackList.hpp"
#include "database/User.hpp"
#include "resource/ImageResource.hpp"
#include "LmsApplication.hpp"
namespace {
@@ -61,6 +62,19 @@ std::unique_ptr<Wt::WTemplate> createEntry(Database::Track::pointer track)
{
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;
+26 -15
View File
@@ -32,6 +32,7 @@
#include "utils/Service.hpp"
#include "utils/String.hpp"
#include "resource/ImageResource.hpp"
#include "TrackStringUtils.hpp"
#include "LmsApplication.hpp"
@@ -163,13 +164,15 @@ PlayQueue::PlayQueue()
void
PlayQueue::updateRepeatBtn()
{
_repeatBtn->toggleStyleClass("Lms-playqueue-btn-selected", _repeatAll);
_repeatBtn->toggleStyleClass("text-success", _repeatAll);
_repeatBtn->toggleStyleClass("text-muted", !_repeatAll);
}
void
PlayQueue::updateRadioBtn()
{
_radioBtn->toggleStyleClass("Lms-playqueue-btn-selected",_radioMode);
_radioBtn->toggleStyleClass("text-success",_radioMode);
_radioBtn->toggleStyleClass("text-muted", !_radioMode);
}
Database::TrackList::pointer
@@ -282,14 +285,9 @@ PlayQueue::updateCurrentTrack(bool selected)
if (!_trackPos || *_trackPos >= static_cast<std::size_t>(_entriesContainer->count()))
return;
auto track = _entriesContainer->widget(*_trackPos);
if (track)
{
if (selected)
track->addStyleClass("Lms-playqueue-selected");
else
track->removeStyleClass("Lms-playqueue-selected");
}
Wt::WTemplate* entry {static_cast<Wt::WTemplate*>(_entriesContainer->widget(*_trackPos))};
if (entry)
entry->bindString("is-selected", selected ? "Lms-playqueue-selected" : "");
}
void
@@ -346,15 +344,15 @@ PlayQueue::addSome()
auto tracklistEntries = tracklist->getEntries(_entriesContainer->count(), 50);
for (const Database::TrackListEntry::pointer& tracklistEntry : tracklistEntries)
{
auto tracklistEntryId = tracklistEntry.id();
auto track = tracklistEntry->getTrack();
const auto tracklistEntryId {tracklistEntry.id()};
const auto track {tracklistEntry->getTrack()};
Wt::WTemplate* entry = _entriesContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.PlayQueue.template.entry"));
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
auto artists {track->getArtists()};
auto release = track->getRelease();
const auto artists {track->getArtists()};
const auto release {track->getRelease()};
if (!artists.empty() || release)
entry->setCondition("if-has-artists-or-release", true);
@@ -373,7 +371,20 @@ PlayQueue::addSome()
if (release)
{
entry->setCondition("if-has-release", true);
entry->bindWidget("release", LmsApplication::createReleaseAnchor(track->getRelease()));
entry->bindWidget("release", 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(), 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");
}
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
+27 -15
View File
@@ -35,6 +35,7 @@
#include "utils/Service.hpp"
#include "LmsApplication.hpp"
#include "LmsTheme.hpp"
namespace UserInterface {
@@ -44,18 +45,20 @@ class SettingsModel : public Wt::WFormModel
{
public:
// Associate each field with a unique string literal.
static const Field TranscodeEnableField;
static const Field TranscodeFormatField;
static const Field TranscodeBitrateField;
static const Field PasswordOldField;
static const Field PasswordField;
static const Field PasswordConfirmField;
static inline const Field DarkModeField {"dark-mode"};
static inline const Field TranscodeEnableField {"transcoding-enable"};
static inline const Field TranscodeFormatField {"transcoding-bitrate"};
static inline const Field TranscodeBitrateField {"transcoding-format"};
static inline const Field PasswordOldField {"password-old"};
static inline const Field PasswordField {"password"};
static inline const Field PasswordConfirmField {"password-confirm"};
SettingsModel(bool withOldPassword)
: _withOldPassword {withOldPassword}
{
initializeModels();
addField(DarkModeField);
addField(TranscodeEnableField);
addField(TranscodeBitrateField);
addField(TranscodeFormatField);
@@ -77,14 +80,22 @@ class SettingsModel : public Wt::WFormModel
void saveData()
{
Database::User::PasswordHash passwordHash;
User::PasswordHash passwordHash;
if (!valueText(PasswordField).empty())
passwordHash = ServiceProvider<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8());
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
Database::User::pointer user {LmsApp->getUser()};
User::pointer user {LmsApp->getUser()};
{
const User::UITheme newTheme {Wt::asNumber(value(DarkModeField)) ? User::UITheme::Dark : User::UITheme::Light};
LmsTheme* lmsTheme {static_cast<LmsTheme*>(LmsApp->theme().get())};
lmsTheme->setTheme(newTheme);
user.modify()->setUITheme(newTheme);
}
user.modify()->setAudioTranscodeEnable(Wt::asNumber(value(TranscodeEnableField)));
@@ -107,6 +118,8 @@ class SettingsModel : public Wt::WFormModel
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
setValue(DarkModeField, LmsApp->getUser()->getUITheme() == User::UITheme::Dark);
setValue(TranscodeEnableField, LmsApp->getUser()->getAudioTranscodeEnable());
if (!LmsApp->getUser()->getAudioTranscodeEnable())
{
@@ -223,13 +236,6 @@ class SettingsModel : public Wt::WFormModel
std::shared_ptr<ValueStringModel<AudioFormat>> _transcodeFormatModel;
};
const Wt::WFormModel::Field SettingsModel::TranscodeEnableField = "transcoding-enable";
const Wt::WFormModel::Field SettingsModel::TranscodeBitrateField = "transcoding-bitrate";
const Wt::WFormModel::Field SettingsModel::TranscodeFormatField = "transcoding-format";
const Wt::WFormModel::Field SettingsModel::PasswordOldField = "password-old";
const Wt::WFormModel::Field SettingsModel::PasswordField = "password";
const Wt::WFormModel::Field SettingsModel::PasswordConfirmField = "password-confirm";
SettingsView::SettingsView()
{
wApp->internalPathChanged().connect(std::bind([=]
@@ -252,6 +258,12 @@ SettingsView::refreshView()
auto model {std::make_shared<SettingsModel>(!LmsApp->isUserAuthStrong())};
// Appearance
{
auto darkMode {std::make_unique<Wt::WCheckBox>()};
t->setFormWidget(SettingsModel::DarkModeField, std::move(darkMode));
}
// Old password
if (!LmsApp->isUserAuthStrong())
{
+180
View File
@@ -0,0 +1,180 @@
/*
* 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 "SubsonicView.hpp"
#include <Wt/WCheckBox.h>
#include <Wt/WComboBox.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplateFormView.h>
#include <Wt/WFormModel.h>
#include "database/SubsonicSettings.hpp"
#include "utils/Logger.hpp"
#include "common/ValueStringModel.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
using namespace Database;
class SubsonicModel : public Wt::WFormModel
{
public:
static inline const Field EnableField {"enable"};
static inline const Field ArtistListModeField {"artist-list-mode"};
SubsonicModel()
{
addField(EnableField);
addField(ArtistListModeField);
initializeModels();
loadData();
}
std::shared_ptr<Wt::WAbstractItemModel> getArtistListModeModel() { return _artistListModeModel; }
void saveData()
{
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
auto settings {SubsonicSettings::get(LmsApp->getDbSession())};
settings.modify()->setAPIEnabled(Wt::asNumber(value(EnableField)));
auto artistListModeRow {_artistListModeModel->getRowFromString(valueText(ArtistListModeField))};
if (artistListModeRow)
settings.modify()->setArtistListMode(_artistListModeModel->getValue(*artistListModeRow));
}
void loadData()
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const auto settings {SubsonicSettings::get(LmsApp->getDbSession())};
setValue(EnableField, settings->isAPIEnabled());
auto artistListModeRow {_artistListModeModel->getRowFromValue(settings->getArtistListMode())};
if (artistListModeRow)
setValue(ArtistListModeField, _artistListModeModel->getString(*artistListModeRow));
}
void initializeModels()
{
_artistListModeModel = std::make_shared<ValueStringModel<SubsonicSettings::ArtistListMode>>();
_artistListModeModel->add(Wt::WString::tr("Lms.Admin.Subsonic.all-artists"), SubsonicSettings::ArtistListMode::AllArtists);
_artistListModeModel->add(Wt::WString::tr("Lms.Admin.Subsonic.release-artists"), SubsonicSettings::ArtistListMode::ReleaseArtists);
}
private:
bool validateField(Field field) override
{
if (field == EnableField)
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
if (SubsonicSettings::get(LmsApp->getDbSession())->isAPIEnabled() != Wt::asNumber(value(EnableField)))
setValidation(field, Wt::WValidator::Result( Wt::ValidationState::Valid, Wt::WString::tr("Lms.Admin.Subsonic.need-restart")));
else
setValidation(field, Wt::WValidator::Result( Wt::ValidationState::Valid));
return true;
}
return Wt::WFormModel::validateField(field);
}
std::shared_ptr<ValueStringModel<SubsonicSettings::ArtistListMode>> _artistListModeModel;
};
SubsonicView::SubsonicView()
{
wApp->internalPathChanged().connect([this]()
{
refreshView();
});
refreshView();
}
void
SubsonicView::refreshView()
{
if (!wApp->internalPathMatches("/admin/subsonic"))
return;
clear();
Wt::WTemplateFormView* t {addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.Subsonic.template"))};
auto model {std::make_shared<SubsonicModel>()};
// Enable
auto enable {std::make_unique<Wt::WCheckBox>()};
auto* enableRaw {enable.get()};
t->setFormWidget(SubsonicModel::EnableField, std::move(enable));
// Artis tlist mode
auto artistListMode = std::make_unique<Wt::WComboBox>();
artistListMode->setModel(model->getArtistListModeModel());
t->setFormWidget(SubsonicModel::ArtistListModeField, std::move(artistListMode));
enableRaw->changed().connect([=]()
{
bool enable {enableRaw->checkState() == Wt::CheckState::Checked};
model->setReadOnly(SubsonicModel::ArtistListModeField, !enable);
t->updateModel(model.get());
t->updateView(model.get());
});
Wt::WPushButton* saveBtn = t->bindNew<Wt::WPushButton>("apply-btn", Wt::WString::tr("Lms.apply"));
Wt::WPushButton *discardBtn {t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard")))};
saveBtn->clicked().connect([=]()
{
t->updateModel(model.get());
if (model->validate())
{
model->saveData();
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Settings.settings-saved"));
}
t->updateView(model.get());
});
discardBtn->clicked().connect([=]()
{
model->loadData();
model->validate();
t->updateView(model.get());
});
t->updateView(model.get());
}
} // namespace UserInterface
+37
View File
@@ -0,0 +1,37 @@
/*
* 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>
namespace UserInterface {
class SubsonicView : public Wt::WContainerWidget
{
public:
SubsonicView();
private:
void refreshView();
};
} // namespace UserInterface
-1
View File
@@ -19,7 +19,6 @@
#include "UserView.hpp"
#include <Wt/WApplication.h>
#include <Wt/WCheckBox.h>
#include <Wt/WComboBox.h>
#include <Wt/WLineEdit.h>
+7 -7
View File
@@ -38,15 +38,15 @@ UsersView::UsersView()
_container = bindNew<Wt::WContainerWidget>("users");
Wt::WPushButton* addBtn = bindNew<Wt::WPushButton>("add-btn", Wt::WString::tr("Lms.Admin.Users.add"));
addBtn->clicked().connect(std::bind([=]
addBtn->clicked().connect([]()
{
LmsApp->setInternalPath("/admin/user", true);
}));
});
wApp->internalPathChanged().connect(std::bind([=]
wApp->internalPathChanged().connect([this]()
{
refreshView();
}));
});
refreshView();
}
@@ -83,10 +83,10 @@ UsersView::refreshView()
entry->setCondition("if-edit", true);
Wt::WPushButton* editBtn = entry->bindNew<Wt::WPushButton>("edit-btn", Wt::WString::tr("Lms.Admin.Users.edit"));
editBtn->clicked().connect(std::bind([=]
editBtn->clicked().connect([=]()
{
LmsApp->setInternalPath("/admin/user/" + std::to_string(user.id()), true);
}));
LmsApp->setInternalPath("/admin/user/" + std::to_string(userId), true);
});
Wt::WPushButton* delBtn = entry->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.Admin.Users.del"));
delBtn->clicked().connect([=]
+1 -2
View File
@@ -132,8 +132,7 @@ Artist::refresh()
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
// Some images may not be square
cover->setWidth(128);
cover->setStyleClass("Lms-cover-medium");
anchor->setImage(std::move(cover));
}
+1
View File
@@ -119,6 +119,7 @@ Explore::Explore()
// Contents
Wt::WStackedWidget* contentsStack = bindNew<Wt::WStackedWidget>("contents");
contentsStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
auto artists = std::make_unique<Artists>(_filters);
artists->artistsAdd.connect(this, &Explore::handleArtistsAdd);
+1 -1
View File
@@ -126,7 +126,7 @@ void
Filters::add(Database::IdType clusterId)
{
Wt::WTemplate* filter {};
Wt::WInteractWidget* filter {};
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
+2 -2
View File
@@ -39,8 +39,8 @@ ReleaseLink::ReleaseLink(Database::Release::pointer release)
Wt::WAnchor* anchor = bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 48));
cover->setWidth(48);
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 64));
cover->setStyleClass("Lms-cover-smaller");
anchor->setImage(std::move(cover));
auto artists = release->getReleaseArtists();
+42 -17
View File
@@ -77,7 +77,6 @@ Release::refresh()
throw ReleaseNotFoundException {*releaseId};
Wt::WTemplate* t {addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template"))};
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
t->bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
@@ -114,7 +113,10 @@ Release::refresh()
}
}
t->bindNew<Wt::WImage>("cover", Wt::WLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 512)));
{
Wt::WImage* cover {t->bindNew<Wt::WImage>("cover", Wt::WLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 512)))};
cover->setStyleClass("Lms-cover-large");
}
Wt::WContainerWidget* clusterContainers {t->bindNew<Wt::WContainerWidget>("clusters")};
{
@@ -151,18 +153,47 @@ Release::refresh()
});
}
Wt::WContainerWidget* tracksContainer {t->bindNew<Wt::WContainerWidget>("tracks")};
Wt::WContainerWidget* rootContainer {t->bindNew<Wt::WContainerWidget>("container")};
auto clusterIds {_filters->getClusterIds()};
auto tracks {release->getTracks(clusterIds)};
const bool variousArtists {release->hasVariousArtists()};
const auto totalDisc {release->getTotalDisc()};
const bool isReleaseMultiDisc {totalDisc && *totalDisc > 1};
bool variousArtists {release->hasVariousArtists()};
// Expect to be call in asc order
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
auto getOrAddDiscContainer = [&](std::size_t discNumber) -> Wt::WContainerWidget*
{
{
auto it = trackContainers.find(discNumber);
if (it != std::cend(trackContainers))
return it->second;
}
Wt::WTemplate* disc {rootContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.disc-entry"))};
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
disc->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WContainerWidget* tracksContainer {disc->bindNew<Wt::WContainerWidget>("tracks")};
trackContainers[discNumber] = tracksContainer;
return tracksContainer;
};
const auto clusterIds {_filters->getClusterIds()};
const auto tracks {release->getTracks(clusterIds)};
for (const auto& track : tracks)
{
auto trackId {track.id()};
Wt::WTemplate* entry {tracksContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry"))};
const auto discNumber {track->getDiscNumber()};
Wt::WContainerWidget* container {rootContainer};
if (isReleaseMultiDisc && discNumber)
container = getOrAddDiscContainer(*discNumber);
Wt::WTemplate* entry {container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry"))};
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
auto artists {track->getArtists()};
@@ -185,14 +216,6 @@ Release::refresh()
entry->bindInt("track-number", *trackNumber);
}
auto discNumber {track->getDiscNumber()};
auto totalDiscNumber {release->getTotalDiscNumber()};
if (discNumber && totalDiscNumber && *totalDiscNumber > 1)
{
entry->setCondition("if-has-disc-number", true);
entry->bindInt("disc-number", *discNumber);
}
Wt::WText* playBtn {entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
playBtn->clicked().connect(std::bind([=]
{
@@ -209,11 +232,13 @@ Release::refresh()
LmsApp->getEvents().trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId, bool /*play*/)
{
entry->toggleStyleClass("Lms-explore-release-entry-playing", loadedTrackId == trackId);
entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
});
if (LmsApp->getEvents().lastLoadedTrackId && *LmsApp->getEvents().lastLoadedTrackId == trackId)
entry->addStyleClass("Lms-explore-release-entry-playing");
{
entry->bindString("is-playing", "Lms-entry-playing");
}
}
}
+1 -1
View File
@@ -98,7 +98,7 @@ Releases::addSome()
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
// Some images may not be square
cover->setWidth(128);
cover->setStyleClass("Lms-cover-medium");
anchor->setImage(std::move(cover));
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
+24
View File
@@ -144,6 +144,19 @@ Tracks::addSome()
{
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);
@@ -159,6 +172,17 @@ Tracks::addSome()
{
tracksAdd.emit({trackId});
}));
LmsApp->getEvents().trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId, bool /*play*/)
{
entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
});
if (LmsApp->getEvents().lastLoadedTrackId && *LmsApp->getEvents().lastLoadedTrackId == trackId)
{
entry->bindString("is-playing", "Lms-entry-playing");
}
}
_showMore->setHidden(!moreResults);
+84
View File
@@ -487,6 +487,89 @@ testSingleTrackSingleRelease(Session& session)
}
}
static
void
testMultiTracksSingleReleaseTotalDiscTrack(Session& session)
{
ScopedRelease release1 {session, "MyRelease"};
{
auto transaction {session.createSharedTransaction()};
CHECK(!release1->getTotalTrack());
CHECK(!release1->getTotalDisc());
}
ScopedTrack track1 {session, "MyTrack"};
{
auto transaction {session.createUniqueTransaction()};
track1.get().modify()->setRelease(release1.get());
}
{
auto transaction {session.createSharedTransaction()};
CHECK(!release1->getTotalTrack());
CHECK(!release1->getTotalDisc());
}
{
auto transaction {session.createUniqueTransaction()};
track1.get().modify()->setTotalTrack(36);
track1.get().modify()->setTotalDisc(6);
}
{
auto transaction {session.createSharedTransaction()};
CHECK(release1->getTotalTrack() && *release1->getTotalTrack() == 36);
CHECK(release1->getTotalDisc() && *release1->getTotalDisc() == 6);
}
ScopedTrack track2 {session, "MyTrack2"};
{
auto transaction {session.createUniqueTransaction()};
track2.get().modify()->setRelease(release1.get());
track2.get().modify()->setTotalTrack(37);
track2.get().modify()->setTotalDisc(67);
}
{
auto transaction {session.createSharedTransaction()};
CHECK(release1->getTotalTrack() && *release1->getTotalTrack() == 37);
CHECK(release1->getTotalDisc() && *release1->getTotalDisc() == 67);
}
ScopedRelease release2 {session, "MyRelease2"};
{
auto transaction {session.createSharedTransaction()};
CHECK(!release2->getTotalTrack());
CHECK(!release2->getTotalDisc());
}
ScopedTrack track3 {session, "MyTrack3"};
{
auto transaction {session.createUniqueTransaction()};
track3.get().modify()->setRelease(release2.get());
track3.get().modify()->setTotalTrack(7);
track3.get().modify()->setTotalDisc(5);
}
{
auto transaction {session.createSharedTransaction()};
CHECK(release1->getTotalTrack() && *release1->getTotalTrack() == 37);
CHECK(release1->getTotalDisc() && *release1->getTotalDisc() == 67);
CHECK(release2->getTotalTrack() && *release2->getTotalTrack() == 7);
CHECK(release2->getTotalDisc() && *release2->getTotalDisc() == 5);
}
}
static
void
testSingleTrackSingleCluster(Session& session)
@@ -1461,6 +1544,7 @@ int main()
RUN_TEST(testMultiArtistsSortMethod);
RUN_TEST(testSingleTrackSingleRelease);
RUN_TEST(testMultiTracksSingleReleaseTotalDiscTrack);
RUN_TEST(testSingleTrackSingleCluster);
RUN_TEST(testMultipleTracksSingleCluster);