Merge branch 'develop' for release v3.18.0

This commit is contained in:
emeric
2020-08-21 10:54:19 +02:00
56 changed files with 923 additions and 405 deletions
+5
View File
@@ -60,6 +60,11 @@ The Subsonic API is enabled by default.
__Note__: since _LMS_ stores hashed and salted passwords, it cannot handle the __token authentication__ method defined from version 1.13.0. __Note__: since _LMS_ stores hashed and salted passwords, it cannot handle the __token authentication__ method defined from version 1.13.0.
## Keyboard shortcuts
* Play/pause: <kbd>Space</bbd>
* Previous track: <kbd>Ctrl</kbd> + <kbd>Left</kbd>
* Next track: <kbd>Ctrl</kbd> + <kbd>Right</kbd>
## Installation ## Installation
### Docker ### Docker
+2 -37
View File
@@ -60,7 +60,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-lg-offset-3 col-lg-9"> <div class="col-lg-offset-3 col-lg-9">
${apply-btn class="btn-primary"} ${discard-btn} ${immediate-scan-btn class="btn-info"} ${apply-btn class="btn-primary"} ${discard-btn}
</div> </div>
</div> </div>
</form> </form>
@@ -69,42 +69,7 @@
<br/> <br/>
<div class="row"> <div class="row">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="panel panel-info"> ${scanner-controller}
<div class="panel-heading">${tr:Lms.Admin.Database.Status.status}</div>
<div class="panel-body">
${status}
</div>
</div>
</div>
</div>
</message>
<message id="Lms.Admin.Database.Status.template">
<div class="form-horizontal">
<div class="form-group">
<label class="col-lg-3 control-label">
${tr:Lms.Admin.Database.Status.status}
</label>
<div class="col-lg-9">
<div class="well well-sm">
${status}
</div>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">
${tr:Lms.Admin.Database.Status.last-scan}
</label>
<div class="col-lg-9">
<div class="well well-sm">
${last-scan}
</div>
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-3 col-lg-9">
${btn-report class="btn"}
</div>
</div> </div>
</div> </div>
</message> </message>
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Admin.ScannerController.template">
<div class="panel panel-info">
<div class="panel-heading">${tr:Lms.Admin.ScannerController.scanner}</div>
<div class="panel-body">
<div class="form-horizontal">
<div class="form-group">
<label class="col-lg-3 control-label">
${tr:Lms.Admin.ScannerController.status}
</label>
<div class="col-lg-9">
<div class="well well-sm">
${status}
<div>${step-status}</div>
</div>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">
${tr:Lms.Admin.ScannerController.last-scan}
</label>
<div class="col-lg-9">
<div class="well well-sm">
${last-scan}
</div>
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-3 col-lg-9">
${btn-report class="btn btn-xs"}
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-3 col-lg-9">
${btn-action}
</div>
</div>
</div>
</div>
</div>
</message>
</messages>
+3 -1
View File
@@ -6,18 +6,20 @@
<div class="Lms-header"> <div class="Lms-header">
<h2><div class="Lms-artistname text-primary">${name}</div></h2> <h2><div class="Lms-artistname text-primary">${name}</div></h2>
${clusters} ${clusters}
${play-btn}${add-btn} ${play-btn}${more-btn}
</div> </div>
<div class="row"> <div class="row">
${releases class="Lms-explore-artist-entries-container"} ${releases class="Lms-explore-artist-entries-container"}
</div> </div>
<div class="row"> <div class="row">
${<if-has-similar-artists>}
<div class="col-md-4"> <div class="col-md-4">
<div class="well well-sm"> <div class="well well-sm">
<h3>${tr:Lms.Explore.Artist.similar-artists}</h3> <h3>${tr:Lms.Explore.Artist.similar-artists}</h3>
${similar-artists} ${similar-artists}
</div> </div>
</div> </div>
${</if-has-similar-artists>}
${<if-has-mbid>} ${<if-has-mbid>}
<div class="col-md-4"> <div class="col-md-4">
<div class="well well-sm"> <div class="well well-sm">
+1 -3
View File
@@ -13,9 +13,7 @@
</div> </div>
</div> </div>
${artists} ${artists}
<div class="Lms-horizontal-center"> ${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
${show-more class="btn-primary Lms-show-more"}
</div>
</message> </message>
<message id="Lms.Explore.Artists.template.entry"> <message id="Lms.Explore.Artists.template.entry">
+1 -1
View File
@@ -5,7 +5,7 @@
${contents class="Lms-explore-contents"} ${contents class="Lms-explore-contents"}
</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.more-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">
+27 -21
View File
@@ -9,6 +9,7 @@
<message id="Lms.discard">Discard</message> <message id="Lms.discard">Discard</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.loading">Loading...</message>
<message id="Lms.login">Login</message> <message id="Lms.login">Login</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> 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>
@@ -53,22 +54,29 @@
<message id="Lms.Admin.Database.update-start-time">Update start time</message> <message id="Lms.Admin.Database.update-start-time">Update start time</message>
<message id="Lms.Admin.Database.weekly">Weekly</message> <message id="Lms.Admin.Database.weekly">Weekly</message>
<message id="Lms.Admin.Database.Status.bad-duration">Cannot get track duration</message> <message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message>
<message id="Lms.Admin.Database.Status.cannot-parse-file">Cannot parse file</message> <message id="Lms.Admin.ScannerController.cannot-parse-file">Cannot parse file</message>
<message id="Lms.Admin.Database.Status.cannot-read-file">Cannot read file</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read file</message>
<message id="Lms.Admin.Database.Status.duplicates-header">{1} duplicate files:</message> <message id="Lms.Admin.ScannerController.duplicates-header">{1} duplicate files:</message>
<message id="Lms.Admin.Database.Status.errors-header">{1} errors:</message> <message id="Lms.Admin.ScannerController.errors-header">{1} errors:</message>
<message id="Lms.Admin.Database.Status.get-report">Get report</message> <message id="Lms.Admin.ScannerController.force-scan-now">Force scan now</message>
<message id="Lms.Admin.Database.Status.last-scan">Last scan</message> <message id="Lms.Admin.ScannerController.get-report">Get report</message>
<message id="Lms.Admin.Database.Status.last-scan-not-available">Not available</message> <message id="Lms.Admin.ScannerController.last-scan">Last scan</message>
<message id="Lms.Admin.Database.Status.last-scan-status">Scanned {1} files in {2} on {3} ({4} errors, {5} duplicates)</message> <message id="Lms.Admin.ScannerController.last-scan-not-available">Not available</message>
<message id="Lms.Admin.Database.Status.no-audio-track">No audio track</message> <message id="Lms.Admin.ScannerController.last-scan-status">Scanned {1} files in {2} on {3} ({4} errors, {5} duplicates)</message>
<message id="Lms.Admin.Database.Status.same-hash">Duplicated file hash</message> <message id="Lms.Admin.ScannerController.no-audio-track">No audio track</message>
<message id="Lms.Admin.Database.Status.same-mbid">Duplicated MBID</message> <message id="Lms.Admin.ScannerController.same-hash">Duplicated file hash</message>
<message id="Lms.Admin.Database.Status.status">Status</message> <message id="Lms.Admin.ScannerController.same-mbid">Duplicated MBID</message>
<message id="Lms.Admin.Database.Status.status-not-scheduled">Not scheduled</message> <message id="Lms.Admin.ScannerController.scan-now">Scan now</message>
<message id="Lms.Admin.Database.Status.status-scheduled">Scheduled on {1}</message> <message id="Lms.Admin.ScannerController.scanner">Scanner</message>
<message id="Lms.Admin.Database.Status.status-in-progress">Scanning {1} of {2} files ({3} %)</message> <message id="Lms.Admin.ScannerController.status">Status</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Not scheduled</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Scheduled on {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Checking for missing files... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files">Discovering files: {1} files</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Scanning files: {1}/{2} files ({3}%)...</message>
<!--Users--> <!--Users-->
<message id="Lms.Admin.Users.add">New user</message> <message id="Lms.Admin.Users.add">New user</message>
@@ -113,11 +121,12 @@
<message id="Lms.Explore.most-played">Most played</message> <message id="Lms.Explore.most-played">Most played</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message> <message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
<message id="Lms.Explore.play-last">Play last</message>
<message id="Lms.Explore.play-shuffled">Play shuffled</message>
<message id="Lms.Explore.random">Random</message> <message id="Lms.Explore.random">Random</message>
<message id="Lms.Explore.recently-added">Recently added</message> <message id="Lms.Explore.recently-added">Recently added</message>
<message id="Lms.Explore.recently-played">Recently played</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.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>
<message id="Lms.Explore.value">Value</message> <message id="Lms.Explore.value">Value</message>
@@ -150,11 +159,8 @@
<plural case="0">Added {1} track</plural> <plural case="0">Added {1} track</plural>
<plural case="1">Added {1} tracks</plural> <plural case="1">Added {1} tracks</plural>
</message> </message>
<message id="Lms.PlayQueue.nb-tracks-playing">
<plural case="0">Playing {1} track</plural>
<plural case="1">Playing {1} tracks</plural>
</message>
<message id="Lms.PlayQueue.playqueue">Play Queue</message> <message id="Lms.PlayQueue.playqueue">Play Queue</message>
<message id="Lms.PlayQueue.playqueue-full">Play Queue full!</message>
<message id="Lms.PlayQueue.radio-mode">Radio mode</message> <message id="Lms.PlayQueue.radio-mode">Radio mode</message>
<message id="Lms.PlayQueue.repeat">Repeat</message> <message id="Lms.PlayQueue.repeat">Repeat</message>
<message id="Lms.PlayQueue.shuffle">Shuffle</message> <message id="Lms.PlayQueue.shuffle">Shuffle</message>
+28 -22
View File
@@ -9,6 +9,7 @@
<message id="Lms.discard">Annuler</message> <message id="Lms.discard">Annuler</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.loading">Chargement...</message>
<message id="Lms.login">Login</message> <message id="Lms.login">Login</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Quitter</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>
@@ -53,22 +54,29 @@
<message id="Lms.Admin.Database.update-start-time">Heure de départ de la mise à jour</message> <message id="Lms.Admin.Database.update-start-time">Heure de départ de la mise à jour</message>
<message id="Lms.Admin.Database.weekly">Toutes les semaines</message> <message id="Lms.Admin.Database.weekly">Toutes les semaines</message>
<message id="Lms.Admin.Database.Status.bad-duration">Impossible de récupérer la durée de la piste</message> <message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message>
<message id="Lms.Admin.Database.Status.cannot-parse-file">Impossible d'analyser le fichier</message> <message id="Lms.Admin.ScannerController.cannot-parse-file">Impossible d'analyser le fichier</message>
<message id="Lms.Admin.Database.Status.cannot-read-file">Impossible de lire le fichier</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier</message>
<message id="Lms.Admin.Database.Status.duplicates-header">{1} fichiers dupliqués :</message> <message id="Lms.Admin.ScannerController.duplicates-header">{1} fichiers dupliqués :</message>
<message id="Lms.Admin.Database.Status.errors-header">{1} erreurs :</message> <message id="Lms.Admin.ScannerController.errors-header">{1} erreurs :</message>
<message id="Lms.Admin.Database.Status.get-report">Rapport</message> <message id="Lms.Admin.ScannerController.force-scan-now">Lancer un scan forcé</message>
<message id="Lms.Admin.Database.Status.last-scan">Dernier scan</message> <message id="Lms.Admin.ScannerController.get-report">Rapport</message>
<message id="Lms.Admin.Database.Status.last-scan-not-available">Non disponible</message> <message id="Lms.Admin.ScannerController.last-scan">Dernier scan</message>
<message id="Lms.Admin.Database.Status.last-scan-status">{1} fichiers scannés en {2} le {3} ({4} erreurs, {5} duplicatas)</message> <message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponible</message>
<message id="Lms.Admin.Database.Status.no-audio-track">Pas de piste audio</message> <message id="Lms.Admin.ScannerController.last-scan-status">{1} fichiers scannés en {2} le {3} ({4} erreurs, {5} duplicatas)</message>
<message id="Lms.Admin.Database.Status.same-hash">Hash dupliqué</message> <message id="Lms.Admin.ScannerController.no-audio-track">Pas de piste audio</message>
<message id="Lms.Admin.Database.Status.same-mbid">MBID dupliqué</message> <message id="Lms.Admin.ScannerController.same-hash">Hash dupliqué</message>
<message id="Lms.Admin.Database.Status.status">Statut</message> <message id="Lms.Admin.ScannerController.same-mbid">MBID dupliqué</message>
<message id="Lms.Admin.Database.Status.status-not-scheduled">Non planifié</message> <message id="Lms.Admin.ScannerController.scan-now">Lancer un scan</message>
<message id="Lms.Admin.Database.Status.status-scheduled">Planifié le {1}</message> <message id="Lms.Admin.ScannerController.scanner">Scanner</message>
<message id="Lms.Admin.Database.Status.status-in-progress">Scan de {1} fichiers sur {2} ({3} %)</message> <message id="Lms.Admin.ScannerController.status">Statut</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Non planifié</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Planifié le {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Vérification des fichiers supprimés... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files">Découverte des fichiers: {1} fichiers</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Récupération des métadonnées AcousticBrainz: {1}/{2} fichiers ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Scan des fichiers: {1}/{2} fichiers ({3}%)...</message>
<!--Users--> <!--Users-->
<message id="Lms.Admin.Users.add">Ajouter</message> <message id="Lms.Admin.Users.add">Ajouter</message>
@@ -113,11 +121,12 @@
<message id="Lms.Explore.most-played">Plus joués</message> <message id="Lms.Explore.most-played">Plus joués</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message> <message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
<message id="Lms.Explore.play-last">Jouer en dernier</message>
<message id="Lms.Explore.play-shuffled">Jouer aléatoirement</message>
<message id="Lms.Explore.random">Aléatoire</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-added">Ajoutés récemment</message>
<message id="Lms.Explore.recently-played">Joué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.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>
<message id="Lms.Explore.value">Valeur</message> <message id="Lms.Explore.value">Valeur</message>
@@ -132,7 +141,7 @@
<message id="Lms.Explore.Artists.linktype-releaseartist">Artistes d'album</message> <message id="Lms.Explore.Artists.linktype-releaseartist">Artistes d'album</message>
<!--Explore:Release--> <!--Explore:Release-->
<message id="Lms.Explore.ReleaseInfo.similar-releases">Albums similaires</message> <message id="Lms.Explore.Release.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>
@@ -150,11 +159,8 @@
<plural case="0">{1} piste ajoutée</plural> <plural case="0">{1} piste ajoutée</plural>
<plural case="1">{1} pistes ajoutées</plural> <plural case="1">{1} pistes ajoutées</plural>
</message> </message>
<message id="Lms.PlayQueue.nb-tracks-playing">
<plural case="0">{1} piste ajoutée</plural>
<plural case="1">{1} pistes ajoutées</plural>
</message>
<message id="Lms.PlayQueue.playqueue">Liste de lecture</message> <message id="Lms.PlayQueue.playqueue">Liste de lecture</message>
<message id="Lms.PlayQueue.playqueue-full">Liste de lecture pleine!</message>
<message id="Lms.PlayQueue.radio-mode">Mode radio</message> <message id="Lms.PlayQueue.radio-mode">Mode radio</message>
<message id="Lms.PlayQueue.repeat">Repéter</message> <message id="Lms.PlayQueue.repeat">Repéter</message>
<message id="Lms.PlayQueue.shuffle">Mélanger</message> <message id="Lms.PlayQueue.shuffle">Mélanger</message>
+1 -3
View File
@@ -16,9 +16,7 @@
<h4><small>${nb-tracks}</small></h4> <h4><small>${nb-tracks}</small></h4>
</div> </div>
${entries} ${entries}
<div class="Lms-horizontal-center"> ${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
${show-more class="btn-primary Lms-show-more"}
</div>
</div> </div>
</div> </div>
</message> </message>
+4 -2
View File
@@ -17,12 +17,13 @@
${<if-has-artist>}<h4>${artist class="Lms-artistname text-muted"}</h4>${</if-has-artist>} ${<if-has-artist>}<h4>${artist class="Lms-artistname text-muted"}</h4>${</if-has-artist>}
<div class="text-muted"><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></div> <div class="text-muted"><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></div>
${clusters} ${clusters}
${play-btn}${add-btn} ${play-btn}${more-btn}
</div> </div>
${container} ${container}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
${<if-has-similar-releases>}
<div class="col-xs-12 col-md-8"> <div class="col-xs-12 col-md-8">
<div class="well well-sm"> <div class="well well-sm">
<h3>${tr:Lms.Explore.Release.similar-releases}</h3> <h3>${tr:Lms.Explore.Release.similar-releases}</h3>
@@ -31,6 +32,7 @@
</div> </div>
</div> </div>
</div> </div>
${</if-has-similar-releases>}
${<if-has-copyright>} ${<if-has-copyright>}
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<div class="well well-sm"> <div class="well well-sm">
@@ -66,7 +68,7 @@
</div> </div>
<div class="col-xs-3"> <div class="col-xs-3">
<div class="Lms-explore-release-entry-controls"> <div class="Lms-explore-release-entry-controls">
${play-btn}${add-btn} ${play-btn}${more-btn}
</div> </div>
<div class="Lms-explore-release-entry-track-duration text-muted"> <div class="Lms-explore-release-entry-track-duration text-muted">
${duration} ${duration}
+2 -4
View File
@@ -6,14 +6,12 @@
<div class="Lms-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"} ${mode class="nav nav-pills Lms-explore-mode-container"}
${play-btn}${add-btn} ${play-btn}${more-btn}
</div> </div>
<div class="row"> <div class="row">
${releases} ${releases}
</div> </div>
<div class="Lms-horizontal-center"> ${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
${show-more class="btn-primary Lms-show-more"}
</div>
</message> </message>
<message id="Lms.Explore.Releases.template.entry-grid"> <message id="Lms.Explore.Releases.template.entry-grid">
+5
View File
@@ -56,4 +56,9 @@
${player class="Lms-player"} ${player class="Lms-player"}
</message> </message>
<message id="Lms.LoadingIndicator.template">
<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i>
<span class="sr-only">${tr:Lms.loading}</span>
</message>
</messages> </messages>
+3 -5
View File
@@ -9,12 +9,10 @@
<div class="Lms-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"} ${mode class="nav nav-pills Lms-explore-mode-container"}
${play-btn}${add-btn} ${play-btn}${more-btn}
</div> </div>
${tracks} ${tracks}
<div class="Lms-horizontal-center"> ${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
${show-more class="btn-primary Lms-show-more"}
</div>
</div> </div>
</div> </div>
</message> </message>
@@ -35,7 +33,7 @@
</div> </div>
<div class="col-xs-2 col-sm-2 col-md-2"> <div class="col-xs-2 col-sm-2 col-md-2">
<div class="Lms-explore-tracks-entry-controls"> <div class="Lms-explore-tracks-entry-controls">
${play-btn}${add-btn} ${play-btn}${more-btn}
</div> </div>
<div class="Lms-explore-tracks-entry-duration text-muted"> <div class="Lms-explore-tracks-entry-duration text-muted">
${duration} ${duration}
+5 -5
View File
@@ -259,6 +259,11 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
justify-content: center; justify-content: center;
} }
.Lms-loading-indicator {
margin-top: 8px;
margin-bottom: 8px;
}
.Lms-navbar-search { .Lms-navbar-search {
max-width: 175px; max-width: 175px;
padding-top: 7.5px; padding-top: 7.5px;
@@ -478,9 +483,4 @@ a.Lms-releasename:hover, a.Lms-releasename:focus {
padding-bottom: 100%; padding-bottom: 100%;
} }
.Lms-show-more {
margin-top: 8px;
margin-bottom: 8px;
}
+42 -10
View File
@@ -65,6 +65,25 @@ LMS.mediaplayer = function () {
} }
} }
var _playPause = function() {
_audioCtx.resume();
if (_elems.audio.paused && _elems.audio.children.length > 0) {
_playTrack();
}
else
_elems.audio.pause();
}
var _playPrevious = function() {
_audioCtx.resume();
_requestPreviousTrack();
}
var _playNext = function() {
_audioCtx.resume();
_requestNextTrack();
}
var _requestPreviousTrack = function() { var _requestPreviousTrack = function() {
Wt.emit(_root, "playPrevious"); Wt.emit(_root, "playPrevious");
} }
@@ -146,21 +165,14 @@ LMS.mediaplayer = function () {
_gainNode.connect(_audioCtx.destination); _gainNode.connect(_audioCtx.destination);
_elems.playpause.addEventListener("click", function() { _elems.playpause.addEventListener("click", function() {
_audioCtx.resume(); _playPause();
if (_elems.audio.paused && _elems.audio.children.length > 0) {
_playTrack();
}
else
_elems.audio.pause();
}); });
_elems.previous.addEventListener("click", function() { _elems.previous.addEventListener("click", function() {
_audioCtx.resume(); _playPrevious();
_requestPreviousTrack();
}); });
_elems.next.addEventListener("click", function() { _elems.next.addEventListener("click", function() {
_audioCtx.resume(); _playNext();
_requestNextTrack();
}); });
_elems.seek.addEventListener("change", function() { _elems.seek.addEventListener("change", function() {
_audioCtx.resume(); _audioCtx.resume();
@@ -225,6 +237,26 @@ LMS.mediaplayer = function () {
} }
}); });
document.addEventListener("keydown", function(event) {
let handled = false;
if (event.keyCode == 32 && !(event.target instanceof HTMLInputElement)) {
_playPause();
handled = true;
}
else if (event.ctrlKey && event.keyCode == 37) {
_playPrevious();
handled = true;
}
else if (event.ctrlKey && event.keyCode == 39) {
_playNext();
handled = true;
}
if (handled)
event.preventDefault();
});
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler("previoustrack", function() { navigator.mediaSession.setActionHandler("previoustrack", function() {
_requestPreviousTrack(); _requestPreviousTrack();
+6 -4
View File
@@ -6,16 +6,15 @@ add_library(lmsav SHARED
impl/AvTypes.cpp impl/AvTypes.cpp
) )
# TODO make av libs private
target_include_directories(lmsav INTERFACE target_include_directories(lmsav INTERFACE
include include
${AVCODEC_INCLUDE_DIR}
${AVFORMAT_INCLUDE_DIR}
${AVUTIL_INCLUDE_DIR}
) )
target_include_directories(lmsav PRIVATE target_include_directories(lmsav PRIVATE
include/ include/
${AVCODEC_INCLUDE_DIR}
${AVFORMAT_INCLUDE_DIR}
${AVUTIL_INCLUDE_DIR}
) )
target_link_libraries(lmsav PUBLIC target_link_libraries(lmsav PUBLIC
@@ -24,6 +23,9 @@ target_link_libraries(lmsav PUBLIC
avutil avutil
std::filesystem std::filesystem
wt wt
)
target_link_libraries(lmsav PRIVATE
${AVFORMAT_LIBRARY} ${AVFORMAT_LIBRARY}
${AVUTIL_LIBRARY} ${AVUTIL_LIBRARY}
) )
+8
View File
@@ -19,6 +19,14 @@
#include "av/AvInfo.hpp" #include "av/AvInfo.hpp"
extern "C"
{
#define __STDC_CONSTANT_MACROS
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/error.h>
}
#include <array> #include <array>
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
@@ -39,7 +39,6 @@ namespace Av
static constexpr std::size_t _chunkSize {262144}; static constexpr std::size_t _chunkSize {262144};
const std::filesystem::path _trackPath; const std::filesystem::path _trackPath;
Transcoder _transcoder; Transcoder _transcoder;
}; };
} }
+14 -13
View File
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2020 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -59,17 +59,20 @@ Transcoder::start()
if (!std::filesystem::exists(_filePath)) if (!std::filesystem::exists(_filePath))
{ {
LOG(ERROR) << "File '" << _filePath << "' does not exist!"; LOG(ERROR) << "File '" << _filePath << "' does not exist!";
_isComplete = true;
return false; return false;
} }
else if (!std::filesystem::is_regular_file( _filePath) ) else if (!std::filesystem::is_regular_file( _filePath) )
{ {
LOG(ERROR) << "File '" << _filePath << "' is not regular!"; LOG(ERROR) << "File '" << _filePath << "' is not regular!";
_isComplete = true;
return false; return false;
} }
} }
catch (const std::filesystem::filesystem_error& e) catch (const std::filesystem::filesystem_error& e)
{ {
LOG(ERROR) << "File error on '" << _filePath.string() << "': " << e.what(); LOG(ERROR) << "File error on '" << _filePath.string() << "': " << e.what();
_isComplete = true;
return false; return false;
} }
@@ -154,6 +157,7 @@ Transcoder::start()
break; break;
default: default:
_isComplete = true;
return false; return false;
} }
@@ -178,12 +182,14 @@ Transcoder::start()
if (!_child->is_open()) if (!_child->is_open())
{ {
LOG(DEBUG) << "Exec failed!"; LOG(DEBUG) << "Exec failed!";
_isComplete = true;
return false; return false;
} }
if (_child->out().eof()) if (_child->out().eof())
{ {
LOG(DEBUG) << "Early end of file!"; LOG(DEBUG) << "Early end of file!";
_isComplete = true;
return false; return false;
} }
@@ -199,16 +205,6 @@ Transcoder::process(std::vector<unsigned char>& output, std::size_t maxSize)
if (!_child || _isComplete) if (!_child || _isComplete)
return; return;
if (_child->out().fail())
{
LOG(DEBUG) << "Stdout FAILED 2";
}
if (_child->out().eof())
{
LOG(DEBUG) << "Stdout ENDED 2";
}
output.resize(maxSize); output.resize(maxSize);
//Read on the output stream //Read on the output stream
@@ -218,14 +214,19 @@ Transcoder::process(std::vector<unsigned char>& output, std::size_t maxSize)
if (_child->out().fail()) if (_child->out().fail())
{ {
LOG(DEBUG) << "Stdout FAILED"; LOG(DEBUG) << "Stdout FAILED";
_isComplete = true;
} }
if (_child->out().eof()) if (_child->out().eof())
{ {
LOG(DEBUG) << "Stdout EOF!"; LOG(DEBUG) << "Stdout EOF!";
_child->clear();
_isComplete = true; _isComplete = true;
}
if (_isComplete)
{
LOG(DEBUG) << "Transcode complete!";
_child->clear();
_child.reset(); _child.reset();
} }
+2 -8
View File
@@ -21,14 +21,6 @@
#pragma once #pragma once
extern "C"
{
#define __STDC_CONSTANT_MACROS
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/error.h>
}
#include <chrono> #include <chrono>
#include <filesystem> #include <filesystem>
#include <map> #include <map>
@@ -38,6 +30,8 @@ extern "C"
#include "AvTypes.hpp" #include "AvTypes.hpp"
struct AVFormatContext;
namespace Av namespace Av
{ {
+91 -56
View File
@@ -257,22 +257,14 @@ MediaScanner::MediaScanner(Database::Db& db)
} }
MediaScanner::~MediaScanner() MediaScanner::~MediaScanner()
{
if (_running)
stop();
}
void
MediaScanner::restart(void)
{ {
stop(); stop();
start();
} }
void void
MediaScanner::start(void) MediaScanner::start()
{ {
_running = true; std::scoped_lock lock {_controlMutex};
scheduleNextScan(); scheduleNextScan();
@@ -280,27 +272,47 @@ MediaScanner::start(void)
} }
void void
MediaScanner::stop(void) MediaScanner::stop()
{ {
_running = false; std::scoped_lock lock {_controlMutex};
_abortScan = true;
_scheduleTimer.cancel(); _scheduleTimer.cancel();
_ioService.stop(); _ioService.stop();
} }
void void
MediaScanner::requestImmediateScan() MediaScanner::abortScan()
{ {
LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan...";
std::scoped_lock lock {_controlMutex};
LMS_LOG(DBUPDATER, DEBUG) << "Waiting for the scan to abort...";
_abortScan = true;
_scheduleTimer.cancel();
_ioService.stop();
LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!";
_abortScan = false;
_ioService.start();
}
void
MediaScanner::requestImmediateScan(bool force)
{
abortScan();
_ioService.post([=]() _ioService.post([=]()
{ {
scheduleScan(); scheduleScan(force);
}); });
} }
void void
MediaScanner::requestReschedule() MediaScanner::requestReload()
{ {
abortScan();
_ioService.post([=]() _ioService.post([=]()
{ {
scheduleNextScan(); scheduleNextScan();
@@ -308,7 +320,7 @@ MediaScanner::requestReschedule()
} }
MediaScanner::Status MediaScanner::Status
MediaScanner::getStatus() MediaScanner::getStatus() const
{ {
Status res; Status res;
@@ -317,7 +329,7 @@ MediaScanner::getStatus()
res.currentState = _curState; res.currentState = _curState;
res.nextScheduledScan = _nextScheduledScan; res.nextScheduledScan = _nextScheduledScan;
res.lastCompleteScanStats = _lastCompleteScanStats; res.lastCompleteScanStats = _lastCompleteScanStats;
res.inProgressScanStats = _inProgressScanStats; res.currentScanStepStats = _currentScanStepStats;
return res; return res;
} }
@@ -365,7 +377,7 @@ MediaScanner::scheduleNextScan()
if (nextScanDate.isValid()) if (nextScanDate.isValid())
{ {
nextScanDateTime = Wt::WDateTime {nextScanDate, _startTime}; nextScanDateTime = Wt::WDateTime {nextScanDate, _startTime};
scheduleScan(nextScanDateTime); scheduleScan(false, nextScanDateTime);
} }
{ {
@@ -380,17 +392,21 @@ MediaScanner::scheduleNextScan()
void void
MediaScanner::countAllFiles(ScanStats& stats) MediaScanner::countAllFiles(ScanStats& stats)
{ {
stats.filesToScan = 0; ScanStepStats stepStats{stats.startTime, ScanProgressStep::DiscoveringFiles};
stats.filesScanned = 0;
notifyInProgress(stepStats);
exploreFilesRecursive(_mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path) exploreFilesRecursive(_mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path)
{ {
if (!_running) if (_abortScan)
return false; return false;
if (!ec && isFileSupported(path, _fileExtensions)) if (!ec && isFileSupported(path, _fileExtensions))
{ {
stats.filesToScan++; stats.filesScanned++;
notifyInProgressIfNeeded(stats); stepStats.processedFiles++;
notifyInProgressIfNeeded(stepStats);
} }
return true; return true;
@@ -398,13 +414,21 @@ MediaScanner::countAllFiles(ScanStats& stats)
} }
void void
MediaScanner::scheduleScan(const Wt::WDateTime& dateTime) MediaScanner::scheduleScan(bool force, const Wt::WDateTime& dateTime)
{ {
auto cb {[=](boost::system::error_code ec)
{
if (ec)
return;
scan(force);
}};
if (dateTime.isNull()) if (dateTime.isNull())
{ {
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now"; LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now";
_scheduleTimer.expires_from_now(std::chrono::seconds(0)); _scheduleTimer.expires_from_now(std::chrono::seconds {0});
_scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1)); _scheduleTimer.async_wait(cb);
} }
else else
{ {
@@ -413,16 +437,13 @@ MediaScanner::scheduleScan(const Wt::WDateTime& dateTime)
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(std::ctime(&t)); LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(std::ctime(&t));
_scheduleTimer.expires_at(timePoint); _scheduleTimer.expires_at(timePoint);
_scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1)); _scheduleTimer.async_wait(cb);
} }
} }
void void
MediaScanner::scan(boost::system::error_code err) MediaScanner::scan(bool forceScan)
{ {
if (err)
return;
scanStarted().emit(); scanStarted().emit();
{ {
@@ -438,13 +459,11 @@ MediaScanner::scan(boost::system::error_code err)
refreshScanSettings(); refreshScanSettings();
bool forceScan {false}; removeMissingTracks(stats);
LMS_LOG(DBUPDATER, DEBUG) << "Counting files in media directory '" << _mediaDirectory.string() << "'..."; LMS_LOG(DBUPDATER, DEBUG) << "Counting files in media directory '" << _mediaDirectory.string() << "'...";
countAllFiles(stats); countAllFiles(stats);
LMS_LOG(DBUPDATER, DEBUG) << "-> Nb files = " << stats.filesToScan; LMS_LOG(DBUPDATER, DEBUG) << "-> Nb files = " << stats.filesScanned;
removeMissingTracks(stats);
LMS_LOG(UI, INFO) << "Checks complete, force scan = " << forceScan; LMS_LOG(UI, INFO) << "Checks complete, force scan = " << forceScan;
@@ -454,36 +473,39 @@ MediaScanner::scan(boost::system::error_code err)
removeOrphanEntries(); removeOrphanEntries();
if (_running) if (!_abortScan)
checkDuplicatedAudioFiles(stats); checkDuplicatedAudioFiles(stats);
// Now update all the track features if needed // Now update all the track features if needed
fetchTrackFeatures(stats); fetchTrackFeatures(stats);
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << "/" << stats.featuresToFetch <<", duplicates = " << stats.duplicates.size(); LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size();
_dbSession.optimize(); _dbSession.optimize();
if (_running) if (!_abortScan)
{ {
stats.stopTime = Wt::WLocalDateTime::currentDateTime().toUTC(); stats.stopTime = Wt::WLocalDateTime::currentDateTime().toUTC();
{ {
std::unique_lock lock {_statusMutex}; std::unique_lock lock {_statusMutex};
_lastCompleteScanStats = std::move(stats); _lastCompleteScanStats = std::move(stats);
_inProgressScanStats.reset(); _currentScanStepStats.reset();
} }
LMS_LOG(DBUPDATER, DEBUG) << "Scan not aborted, scheduling next scan!";
scheduleNextScan(); scheduleNextScan();
scanComplete().emit(); scanComplete().emit();
} }
else else
{ {
LMS_LOG(DBUPDATER, DEBUG) << "Scan aborted, not scheduling next scan!";
std::unique_lock lock {_statusMutex}; std::unique_lock lock {_statusMutex};
_curState = State::NotScheduled; _curState = State::NotScheduled;
_inProgressScanStats.reset(); _currentScanStepStats.reset();
} }
} }
@@ -519,6 +541,8 @@ MediaScanner::fetchTrackFeatures(ScanStats& stats)
if (_recommendationEngineType != ScanSettings::RecommendationEngineType::Features) if (_recommendationEngineType != ScanSettings::RecommendationEngineType::Features)
return; return;
ScanStepStats stepStats{stats.startTime, ScanProgressStep::FetchingTrackFeatures};
LMS_LOG(DBUPDATER, INFO) << "Fetching missing track features..."; LMS_LOG(DBUPDATER, INFO) << "Fetching missing track features...";
struct TrackInfo struct TrackInfo
@@ -540,18 +564,21 @@ MediaScanner::fetchTrackFeatures(ScanStats& stats)
return res; return res;
}()}; }()};
stats.featuresToFetch = tracksToFetch.size(); stepStats.filesToProcess = tracksToFetch.size();
notifyInProgress(stepStats);
LMS_LOG(DBUPDATER, INFO) << "Found " << tracksToFetch.size() << " track(s) to fetch!"; LMS_LOG(DBUPDATER, INFO) << "Found " << tracksToFetch.size() << " track(s) to fetch!";
for (const TrackInfo& trackToFetch : tracksToFetch) for (const TrackInfo& trackToFetch : tracksToFetch)
{ {
if (!_running) if (_abortScan)
return; return;
if (fetchTrackFeatures(trackToFetch.id, trackToFetch.mbid)) if (fetchTrackFeatures(trackToFetch.id, trackToFetch.mbid))
stats.featuresFetched++; stats.featuresFetched++;
stepStats.processedFiles++;
notifyInProgressIfNeeded(stepStats);
} }
LMS_LOG(DBUPDATER, INFO) << "Track features fetched!"; LMS_LOG(DBUPDATER, INFO) << "Track features fetched!";
@@ -591,27 +618,25 @@ MediaScanner::refreshScanSettings()
} }
void void
MediaScanner::notifyInProgress(const ScanStats& stats) MediaScanner::notifyInProgress(const ScanStepStats& stepStats)
{ {
const ScanProgressStats progressStats {stats.toProgressStats()};
{ {
std::unique_lock lock {_statusMutex}; std::unique_lock lock {_statusMutex};
_inProgressScanStats = progressStats; _currentScanStepStats = stepStats;
} }
const std::chrono::system_clock::time_point now {std::chrono::system_clock::now()}; const std::chrono::system_clock::time_point now {std::chrono::system_clock::now()};
_sigScanInProgress(progressStats); _sigScanInProgress(stepStats);
_lastScanInProgressEmit = now; _lastScanInProgressEmit = now;
} }
void void
MediaScanner::notifyInProgressIfNeeded(const ScanStats& stats) MediaScanner::notifyInProgressIfNeeded(const ScanStepStats& stepStats)
{ {
std::chrono::system_clock::time_point now {std::chrono::system_clock::now()}; std::chrono::system_clock::time_point now {std::chrono::system_clock::now()};
if (std::chrono::duration_cast<std::chrono::seconds>(now - _lastScanInProgressEmit).count() > 2) if (std::chrono::duration_cast<std::chrono::seconds>(now - _lastScanInProgressEmit).count() > 1)
notifyInProgress(stats); notifyInProgress(stepStats);
} }
void void
@@ -772,9 +797,13 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
void void
MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats) MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats)
{ {
ScanStepStats stepStats{stats.startTime, ScanProgressStep::ScanningFiles};
stepStats.filesToProcess = stats.filesScanned;
notifyInProgress(stepStats);
exploreFilesRecursive(mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path) exploreFilesRecursive(mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path)
{ {
if (!_running) if (_abortScan)
return false; return false;
if (ec) if (ec)
@@ -786,13 +815,12 @@ MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bo
{ {
scanAudioFile(path, forceScan, stats ); scanAudioFile(path, forceScan, stats );
notifyInProgressIfNeeded(stats); stepStats.processedFiles++;
notifyInProgressIfNeeded(stepStats);
} }
return true; return true;
}); });
notifyInProgress(stats);
} }
// Check if a file exists and is still in a media directory // Check if a file exists and is still in a media directory
@@ -837,6 +865,8 @@ MediaScanner::removeMissingTracks(ScanStats& stats)
{ {
static constexpr std::size_t batchSize {50}; static constexpr std::size_t batchSize {50};
ScanStepStats stepStats{stats.startTime, ScanProgressStep::ChekingForMissingFiles};
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed..."; LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed...";
std::size_t trackCount {}; std::size_t trackCount {};
@@ -846,6 +876,9 @@ MediaScanner::removeMissingTracks(ScanStats& stats)
} }
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked..."; LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked...";
stepStats.filesToProcess = trackCount;
notifyInProgress(stepStats);
std::vector<std::pair<Database::IdType, std::filesystem::path>> trackPaths; std::vector<std::pair<Database::IdType, std::filesystem::path>> trackPaths;
std::vector<IdType> tracksToRemove; std::vector<IdType> tracksToRemove;
@@ -861,11 +894,13 @@ MediaScanner::removeMissingTracks(ScanStats& stats)
for (const auto& [trackId, trackPath] : trackPaths) for (const auto& [trackId, trackPath] : trackPaths)
{ {
if (!_running) if (_abortScan)
return; return;
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions)) if (!checkFile(trackPath, _mediaDirectory, _fileExtensions))
tracksToRemove.push_back(trackId); tracksToRemove.push_back(trackId);
stepStats.processedFiles++;
} }
if (!tracksToRemove.empty()) if (!tracksToRemove.empty())
@@ -883,7 +918,7 @@ MediaScanner::removeMissingTracks(ScanStats& stats)
} }
} }
notifyInProgressIfNeeded(stats); notifyInProgressIfNeeded(stepStats);
if (i == 0) if (i == 0)
break; break;
+15 -14
View File
@@ -52,26 +52,26 @@ class MediaScanner : public IMediaScanner
void start() override; void start() override;
void stop() override; void stop() override;
void restart() override; void requestReload() override;
void requestImmediateScan(bool force) override;
void requestImmediateScan() override; Status getStatus() const override;
void requestReschedule() override ;
Status getStatus() override;
Wt::Signal<>& scanStarted() override { return _sigScanStarted; } Wt::Signal<>& scanStarted() override { return _sigScanStarted; }
Wt::Signal<>& scanComplete() override { return _sigScanComplete; } Wt::Signal<>& scanComplete() override { return _sigScanComplete; }
Wt::Signal<ScanProgressStats>& scanInProgress() override { return _sigScanInProgress; } Wt::Signal<ScanStepStats>& scanInProgress() override { return _sigScanInProgress; }
Wt::Signal<Wt::WDateTime>& scheduled() override { return _sigScheduled; } Wt::Signal<Wt::WDateTime>& scheduled() override { return _sigScheduled; }
private: private:
// Job handling // Job handling
void scheduleNextScan(); void scheduleNextScan();
void scheduleScan(const Wt::WDateTime& dateTime = {}); void scheduleScan(bool force, const Wt::WDateTime& dateTime = {});
void abortScan();
// Update database (scheduled callback) // Update database (scheduled callback)
void scan(boost::system::error_code ec); void scan(bool force);
void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats); void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats);
bool fetchTrackFeatures(Database::IdType trackId, const UUID& MBID); bool fetchTrackFeatures(Database::IdType trackId, const UUID& MBID);
@@ -86,24 +86,25 @@ class MediaScanner : public IMediaScanner
void checkDuplicatedAudioFiles(ScanStats& stats); void checkDuplicatedAudioFiles(ScanStats& stats);
void scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats); void scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats);
Database::IdType doScanAudioFile(const std::filesystem::path& file, ScanStats& stats); Database::IdType doScanAudioFile(const std::filesystem::path& file, ScanStats& stats);
void notifyInProgressIfNeeded(const ScanStats& stats); void notifyInProgressIfNeeded(const ScanStepStats& stats);
void notifyInProgress(const ScanStats& stats); void notifyInProgress(const ScanStepStats& stats);
bool _running {}; std::mutex _controlMutex;
std::atomic<bool> _abortScan {};
Wt::WIOService _ioService; Wt::WIOService _ioService;
boost::asio::system_timer _scheduleTimer {_ioService}; boost::asio::system_timer _scheduleTimer {_ioService};
Wt::Signal<> _sigScanStarted; Wt::Signal<> _sigScanStarted;
Wt::Signal<> _sigScanComplete; Wt::Signal<> _sigScanComplete;
Wt::Signal<ScanProgressStats> _sigScanInProgress; Wt::Signal<ScanStepStats> _sigScanInProgress;
std::chrono::system_clock::time_point _lastScanInProgressEmit {}; std::chrono::system_clock::time_point _lastScanInProgressEmit {};
Wt::Signal<Wt::WDateTime> _sigScheduled; Wt::Signal<Wt::WDateTime> _sigScheduled;
Database::Session _dbSession; Database::Session _dbSession;
std::unique_ptr<MetaData::IParser> _metadataParser; std::unique_ptr<MetaData::IParser> _metadataParser;
std::shared_mutex _statusMutex; mutable std::shared_mutex _statusMutex;
State _curState {State::NotScheduled}; State _curState {State::NotScheduled};
std::optional<ScanStats> _lastCompleteScanStats; std::optional<ScanStats> _lastCompleteScanStats;
std::optional<ScanProgressStats> _inProgressScanStats; std::optional<ScanStepStats> _currentScanStepStats;
Wt::WDateTime _nextScheduledScan; Wt::WDateTime _nextScheduledScan;
// Current scan settings // Current scan settings
+2 -8
View File
@@ -40,16 +40,10 @@ ScanStats::nbChanges() const
return additions + deletions + updates; return additions + deletions + updates;
} }
ScanProgressStats
ScanStats::toProgressStats() const
{
return ScanProgressStats {startTime, filesToScan, nbFiles()};
}
unsigned unsigned
ScanProgressStats::progress() const ScanStepStats::progress() const
{ {
return (processedFiles / static_cast<float>(filesToScan ? filesToScan : 1)) * 100; return (processedFiles / static_cast<float>(filesToProcess ? filesToProcess : 1)) * 100;
} }
} // namespace Scanner } // namespace Scanner
@@ -40,11 +40,10 @@ class IMediaScanner
virtual void start() = 0; virtual void start() = 0;
virtual void stop() = 0; virtual void stop() = 0;
virtual void restart() = 0;
// Async requests // Async requests
virtual void requestImmediateScan() = 0; virtual void requestReload() = 0;
virtual void requestReschedule() = 0; virtual void requestImmediateScan(bool force) = 0;
enum class State enum class State
@@ -56,13 +55,13 @@ class IMediaScanner
struct Status struct Status
{ {
State currentState {State::NotScheduled}; State currentState {State::NotScheduled};
Wt::WDateTime nextScheduledScan; Wt::WDateTime nextScheduledScan;
std::optional<ScanStats> lastCompleteScanStats; std::optional<ScanStats> lastCompleteScanStats;
std::optional<ScanProgressStats> inProgressScanStats; std::optional<ScanStepStats> currentScanStepStats;
}; };
virtual Status getStatus() = 0; virtual Status getStatus() const = 0;
// Called just after scan start // Called just after scan start
virtual Wt::Signal<>& scanStarted() = 0; virtual Wt::Signal<>& scanStarted() = 0;
@@ -71,7 +70,7 @@ class IMediaScanner
virtual Wt::Signal<>& scanComplete() = 0; virtual Wt::Signal<>& scanComplete() = 0;
// Called during scan in progress // Called during scan in progress
virtual Wt::Signal<ScanProgressStats>& scanInProgress() = 0; virtual Wt::Signal<ScanStepStats>& scanInProgress() = 0;
// Called after a schedule // Called after a schedule
virtual Wt::Signal<Wt::WDateTime>& scheduled() = 0; virtual Wt::Signal<Wt::WDateTime>& scheduled() = 0;
@@ -56,15 +56,26 @@ namespace Scanner {
}; };
enum class ScanProgressStep : unsigned
{
ChekingForMissingFiles = 0,
DiscoveringFiles,
ScanningFiles,
FetchingTrackFeatures,
};
static inline constexpr unsigned ScanProgressStepCount {4};
// reduced scan stats // reduced scan stats
struct ScanProgressStats struct ScanStepStats
{ {
Wt::WDateTime startTime; Wt::WDateTime startTime;
std::size_t filesToScan {}; ScanProgressStep currentStep;
std::size_t processedFiles {};
unsigned progress() const; std::size_t filesToProcess {};
std::size_t processedFiles {};
unsigned progress() const;
}; };
struct ScanStats struct ScanStats
@@ -72,25 +83,22 @@ namespace Scanner {
Wt::WDateTime startTime; Wt::WDateTime startTime;
Wt::WDateTime stopTime; Wt::WDateTime stopTime;
std::size_t filesToScan {}; // Total number of files to be scanned (estimated) std::size_t filesScanned {}; // Total number of files scanned (estimated)
std::size_t skips {}; // no change since last scan std::size_t skips {}; // no change since last scan
std::size_t scans {}; // actually scanned filed std::size_t scans {}; // actually scanned filed
std::size_t additions {}; // added in DB std::size_t additions {}; // added in DB
std::size_t deletions {}; // removed from DB std::size_t deletions {}; // removed from DB
std::size_t updates {}; // updated file in DB std::size_t updates {}; // updated file in DB
std::size_t featuresFetched {}; // features fetched in DB std::size_t featuresFetched {}; // features fetched in DB
std::size_t featuresToFetch {}; // features to be fetched in DB
std::vector<ScanError> errors; std::vector<ScanError> errors;
std::vector<ScanDuplicate> duplicates; std::vector<ScanDuplicate> duplicates;
std::size_t nbFiles() const; std::size_t nbFiles() const;
std::size_t nbChanges() const; std::size_t nbChanges() const;
ScanProgressStats toProgressStats() const;
}; };
} }
+10 -3
View File
@@ -35,8 +35,15 @@ namespace API::Subsonic::Scan
const IMediaScanner::Status scanStatus {ServiceProvider<IMediaScanner>::get()->getStatus()}; const IMediaScanner::Status scanStatus {ServiceProvider<IMediaScanner>::get()->getStatus()};
statusResponse.setAttribute("scanning", scanStatus.currentState == IMediaScanner::State::InProgress); statusResponse.setAttribute("scanning", scanStatus.currentState == IMediaScanner::State::InProgress);
if (scanStatus.currentState == IMediaScanner::State::InProgress && scanStatus.inProgressScanStats) if (scanStatus.currentState == IMediaScanner::State::InProgress)
statusResponse.setAttribute("count", scanStatus.inProgressScanStats->processedFiles); {
std::size_t count{};
if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanProgressStep::ScanningFiles)
count = scanStatus.currentScanStepStats->processedFiles;
statusResponse.setAttribute("count", count);
}
return statusResponse; return statusResponse;
} }
@@ -54,7 +61,7 @@ namespace API::Subsonic::Scan
Response Response
handleStartScan(RequestContext& context) handleStartScan(RequestContext& context)
{ {
ServiceProvider<IMediaScanner>::get()->requestImmediateScan(); ServiceProvider<IMediaScanner>::get()->requestImmediateScan(false);
Response response {Response::createOkResponse(context)}; Response response {Response::createOkResponse(context)};
response.addNode("scanStatus", createStatusResponseNode()); response.addNode("scanStatus", createStatusResponseNode());
+3 -1
View File
@@ -10,11 +10,12 @@ add_executable(lms
ui/SettingsView.cpp ui/SettingsView.cpp
ui/TrackStringUtils.cpp ui/TrackStringUtils.cpp
ui/admin/DatabaseSettingsView.cpp ui/admin/DatabaseSettingsView.cpp
ui/admin/DatabaseStatus.cpp ui/admin/ScannerController.cpp
ui/admin/InitWizardView.cpp ui/admin/InitWizardView.cpp
ui/admin/UserView.cpp ui/admin/UserView.cpp
ui/admin/UsersView.cpp ui/admin/UsersView.cpp
ui/common/AuthModeModel.cpp ui/common/AuthModeModel.cpp
ui/common/LoadingIndicator.cpp
ui/common/Validators.cpp ui/common/Validators.cpp
ui/explore/ArtistListHelpers.cpp ui/explore/ArtistListHelpers.cpp
ui/explore/ArtistView.cpp ui/explore/ArtistView.cpp
@@ -22,6 +23,7 @@ add_executable(lms
ui/explore/Explore.cpp ui/explore/Explore.cpp
ui/explore/Filters.cpp ui/explore/Filters.cpp
ui/explore/ReleaseListHelpers.cpp ui/explore/ReleaseListHelpers.cpp
ui/explore/ReleasePopup.cpp
ui/explore/ReleasesView.cpp ui/explore/ReleasesView.cpp
ui/explore/ReleaseView.cpp ui/explore/ReleaseView.cpp
ui/explore/SearchView.cpp ui/explore/SearchView.cpp
+13 -3
View File
@@ -36,7 +36,9 @@
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "utils/WtLogger.hpp" #include "utils/WtLogger.hpp"
std::vector<std::string> generateWtConfig(std::string execPath) static
std::vector<std::string>
generateWtConfig(std::string execPath)
{ {
std::vector<std::string> args; std::vector<std::string> args;
@@ -92,8 +94,16 @@ std::vector<std::string> generateWtConfig(std::string execPath)
} }
std::ofstream oss(wtConfigPath.string().c_str(), std::ios::out); {
boost::property_tree::xml_parser::write_xml(oss, pt); std::ofstream oss {wtConfigPath.string().c_str(), std::ios::out};
if (!oss)
throw LmsException {"Can't open '" + wtConfigPath.string() + "' for writing!"};
boost::property_tree::xml_parser::write_xml(oss, pt);
if (!oss)
throw LmsException {"Can't write in file '" + wtConfigPath.string() + "', no space left?"};
}
return args; return args;
} }
+13 -7
View File
@@ -22,6 +22,7 @@
#include <Wt/WAnchor.h> #include <Wt/WAnchor.h>
#include <Wt/WEnvironment.h> #include <Wt/WEnvironment.h>
#include <Wt/WLineEdit.h> #include <Wt/WLineEdit.h>
#include <Wt/WPushButton.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>
@@ -34,7 +35,6 @@
#include "database/User.hpp" #include "database/User.hpp"
#include "explore/Explore.hpp" #include "explore/Explore.hpp"
#include "explore/Filters.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"
@@ -122,6 +122,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
// Add a resource bundle // Add a resource bundle
messageResourceBundle().use(appRoot() + "admin-database"); messageResourceBundle().use(appRoot() + "admin-database");
messageResourceBundle().use(appRoot() + "admin-initwizard"); messageResourceBundle().use(appRoot() + "admin-initwizard");
messageResourceBundle().use(appRoot() + "admin-scannercontroller");
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");
@@ -306,6 +307,13 @@ LmsApplication::createCluster(Database::Cluster::pointer cluster, bool canDelete
return res; return res;
} }
Wt::WPopupMenu*
LmsApplication::createPopupMenu()
{
_popupMenu = std::make_unique<Wt::WPopupMenu>();
return _popupMenu.get();
}
void void
LmsApplication::handleException(LmsApplicationException& e) LmsApplication::handleException(LmsApplicationException& e)
{ {
@@ -332,7 +340,6 @@ enum IdxRoot
{ {
IdxExplore = 0, IdxExplore = 0,
IdxPlayQueue, IdxPlayQueue,
IdxSearch,
IdxSettings, IdxSettings,
IdxAdminDatabase, IdxAdminDatabase,
IdxAdminUsers, IdxAdminUsers,
@@ -353,9 +360,9 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
{ "/artist", IdxExplore, false }, { "/artist", IdxExplore, false },
{ "/releases", IdxExplore, false }, { "/releases", IdxExplore, false },
{ "/release", IdxExplore, false }, { "/release", IdxExplore, false },
{ "/search", IdxExplore, false },
{ "/tracks", IdxExplore, false }, { "/tracks", IdxExplore, false },
{ "/playqueue", IdxPlayQueue, false }, { "/playqueue", IdxPlayQueue, 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 },
@@ -469,7 +476,6 @@ LmsApplication::createHome()
Explore* explore = mainStack->addNew<Explore>(filters); Explore* explore = mainStack->addNew<Explore>(filters);
_playQueue = mainStack->addNew<PlayQueue>(); _playQueue = mainStack->addNew<PlayQueue>();
auto* search {mainStack->addNew<SearchView>(filters)};
mainStack->addNew<SettingsView>(); mainStack->addNew<SettingsView>();
searchEdit->enterPressed().connect([=] searchEdit->enterPressed().connect([=]
@@ -480,7 +486,7 @@ LmsApplication::createHome()
searchEdit->textInput().connect([=] searchEdit->textInput().connect([=]
{ {
setInternalPath("/search", true); setInternalPath("/search", true);
search->refreshView(searchEdit->text().toUTF8()); explore->search(searchEdit->text());
}); });
// Admin stuff // Admin stuff
@@ -542,11 +548,11 @@ LmsApplication::createHome()
}); });
}); });
ServiceProvider<Scanner::IMediaScanner>::get()->scanInProgress().connect(this, [=] (Scanner::ScanProgressStats stats) ServiceProvider<Scanner::IMediaScanner>::get()->scanInProgress().connect(this, [=] (Scanner::ScanStepStats stepStats)
{ {
Wt::WServer::instance()->post(sessionId, [=] Wt::WServer::instance()->post(sessionId, [=]
{ {
_events.dbScanInProgress.emit(stats); _events.dbScanInProgress.emit(stepStats);
triggerUpdate(); triggerUpdate();
}); });
}); });
+14 -11
View File
@@ -22,6 +22,7 @@
#include <optional> #include <optional>
#include <Wt/WApplication.h> #include <Wt/WApplication.h>
#include <Wt/WPopupMenu.h>
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
@@ -57,7 +58,7 @@ struct Events
// Database events // Database events
Wt::Signal<> dbScanStarted; Wt::Signal<> dbScanStarted;
Wt::Signal<> dbScanned; Wt::Signal<> dbScanned;
Wt::Signal<Scanner::ScanProgressStats> dbScanInProgress; Wt::Signal<Scanner::ScanStepStats> dbScanInProgress;
Wt::Signal<Wt::WDateTime> dbScanScheduled; Wt::Signal<Wt::WDateTime> dbScanScheduled;
}; };
@@ -101,6 +102,7 @@ class LmsApplication : public Wt::WApplication
static Wt::WLink createReleaseLink(Wt::Dbo::ptr<Database::Release> release); 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::WAnchor> createReleaseAnchor(Wt::Dbo::ptr<Database::Release> release, bool addText = true);
static std::unique_ptr<Wt::WText> 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);
Wt::WPopupMenu* createPopupMenu();
MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; } MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; }
PlayQueue& getPlayQueue() const { return *_playQueue; } PlayQueue& getPlayQueue() const { return *_playQueue; }
@@ -124,17 +126,18 @@ class LmsApplication : public Wt::WApplication
void createHome(); void createHome();
Wt::Signal<> _preQuit; Wt::Signal<> _preQuit;
Database::Session _dbSession; Database::Session _dbSession;
LmsApplicationGroupContainer& _appGroups; LmsApplicationGroupContainer& _appGroups;
Events _events; Events _events;
std::optional<Database::IdType> _userId; std::optional<Database::IdType> _userId;
std::optional<bool> _userAuthStrong; std::optional<bool> _userAuthStrong;
std::shared_ptr<AudioTranscodeResource> _audioTranscodeResource; std::shared_ptr<AudioTranscodeResource> _audioTranscodeResource;
std::shared_ptr<AudioFileResource> _audioFileResource; std::shared_ptr<AudioFileResource> _audioFileResource;
std::shared_ptr<ImageResource> _imageResource; std::shared_ptr<ImageResource> _imageResource;
MediaPlayer* _mediaPlayer {}; MediaPlayer* _mediaPlayer {};
PlayQueue* _playQueue {}; PlayQueue* _playQueue {};
std::unique_ptr<Wt::WPopupMenu> _popupMenu {};
}; };
+72 -25
View File
@@ -32,6 +32,7 @@
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
#include "common/LoadingIndicator.hpp"
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
@@ -65,14 +66,7 @@ PlayQueue::PlayQueue()
}); });
_entriesContainer = bindNew<Wt::WContainerWidget>("entries"); _entriesContainer = bindNew<Wt::WContainerWidget>("entries");
hideLoadingIndicator();
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
_showMore->setHidden(true);
_showMore->clicked().connect([=]
{
addSome();
updateCurrentTrack(true);
});
Wt::WText* shuffleBtn = bindNew<Wt::WText>("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.template.shuffle-btn"), Wt::TextFormat::XHTML); Wt::WText* shuffleBtn = bindNew<Wt::WText>("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.template.shuffle-btn"), Wt::TextFormat::XHTML);
setToolTip(*shuffleBtn, Wt::WString::tr("Lms.PlayQueue.shuffle")); setToolTip(*shuffleBtn, Wt::WString::tr("Lms.PlayQueue.shuffle"));
@@ -188,12 +182,40 @@ PlayQueue::updateRadioBtn()
_radioBtn->toggleStyleClass("text-muted", !_radioMode); _radioBtn->toggleStyleClass("text-muted", !_radioMode);
} }
void
PlayQueue::displayLoadingIndicator()
{
_loadingIndicator = bindWidget<Wt::WTemplate>("loading-indicator", createLoadingIndicator());
_loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
{
if (!visible)
return;
addSome();
updateCurrentTrack(true);
});
}
void
PlayQueue::hideLoadingIndicator()
{
_loadingIndicator = nullptr;
bindEmpty("loading-indicator");
}
Database::TrackList::pointer Database::TrackList::pointer
PlayQueue::getTrackList() const PlayQueue::getTrackList() const
{ {
return Database::TrackList::getById(LmsApp->getDbSession(), _tracklistId); return Database::TrackList::getById(LmsApp->getDbSession(), _tracklistId);
} }
bool
PlayQueue::isFull() const
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
return getTrackList()->getCount() == _nbMaxEntries;
}
void void
PlayQueue::clearTracks() PlayQueue::clearTracks()
{ {
@@ -202,7 +224,7 @@ PlayQueue::clearTracks()
getTrackList().modify()->clear(); getTrackList().modify()->clear();
} }
_showMore->setHidden(true); hideLoadingIndicator();
_entriesContainer->clear(); _entriesContainer->clear();
updateInfo(); updateInfo();
} }
@@ -306,55 +328,77 @@ PlayQueue::updateCurrentTrack(bool selected)
entry->bindString("is-selected", selected ? "Lms-playqueue-selected" : ""); entry->bindString("is-selected", selected ? "Lms-playqueue-selected" : "");
} }
void std::size_t
PlayQueue::enqueueTracks(const std::vector<Database::IdType>& trackIds) PlayQueue::enqueueTracks(const std::vector<Database::IdType>& trackIds)
{ {
std::size_t nbTracksQueued {};
{ {
auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
auto tracklist = getTrackList(); auto tracklist {getTrackList()};
std::size_t nbTracksToEnqueue {tracklist->getCount() + trackIds.size() > _nbMaxEntries ? _nbMaxEntries - tracklist->getCount() : trackIds.size()};
for (Database::IdType trackId : trackIds) for (Database::IdType trackId : trackIds)
{ {
Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
if (!track) if (!track)
continue; continue;
if (nbTracksQueued == nbTracksToEnqueue)
break;
Database::TrackListEntry::create(LmsApp->getDbSession(), track, tracklist); Database::TrackListEntry::create(LmsApp->getDbSession(), track, tracklist);
nbTracksQueued++;
} }
} }
updateInfo(); updateInfo();
addSome(); addSome();
}
void return nbTracksQueued;
PlayQueue::enqueueTrack(Database::IdType trackId)
{
enqueueTracks({trackId});
} }
void void
PlayQueue::processTracks(PlayQueueAction action, const std::vector<Database::IdType>& trackIds) PlayQueue::processTracks(PlayQueueAction action, const std::vector<Database::IdType>& trackIds)
{ {
std::size_t nbAddedTracks {};
switch (action) switch (action)
{ {
case PlayQueueAction::AddLast: case PlayQueueAction::PlayLast:
enqueueTracks(trackIds); nbAddedTracks = enqueueTracks(trackIds);
LmsApp->notifyMsg(MsgType::Info, Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", trackIds.size()).arg(trackIds.size()), std::chrono::milliseconds(2000)); if (!_trackPos)
break; loadTrack(0, true);
case PlayQueueAction::AddNext:
break; break;
case PlayQueueAction::Play: case PlayQueueAction::Play:
clearTracks(); clearTracks();
enqueueTracks(trackIds); nbAddedTracks = enqueueTracks(trackIds);
loadTrack(0, true); loadTrack(0, true);
LmsApp->notifyMsg(MsgType::Info, Wt::WString::trn("Lms.PlayQueue.nb-tracks-playing", trackIds.size()).arg(trackIds.size()), std::chrono::milliseconds(2000));
break; break;
case PlayQueueAction::PlayShuffled:
{
clearTracks();
{
std::vector<Database::IdType> shuffledTrackIds {trackIds};
Random::shuffleContainer(shuffledTrackIds);
nbAddedTracks = enqueueTracks(shuffledTrackIds);
}
loadTrack(0, true);
break;
}
} }
if (nbAddedTracks > 0)
LmsApp->notifyMsg(MsgType::Info, Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::milliseconds(2000));
if (isFull())
LmsApp->notifyMsg(MsgType::Warning, Wt::WString::tr("Lms.PlayQueue.playqueue-full"), std::chrono::milliseconds(2000));
} }
void void
@@ -447,7 +491,10 @@ PlayQueue::addSome()
} }
_showMore->setHidden(static_cast<std::size_t>(_entriesContainer->count()) >= tracklist->getCount()); if (static_cast<std::size_t>(_entriesContainer->count()) < tracklist->getCount())
displayLoadingIndicator();
else
hideLoadingIndicator();
} }
void void
+7 -4
View File
@@ -22,7 +22,6 @@
#include <optional> #include <optional>
#include <Wt/WContainerWidget.h> #include <Wt/WContainerWidget.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include <Wt/WText.h> #include <Wt/WText.h>
@@ -61,16 +60,18 @@ class PlayQueue : public Wt::WTemplate
private: private:
Wt::Dbo::ptr<Database::TrackList> getTrackList() const; Wt::Dbo::ptr<Database::TrackList> getTrackList() const;
bool isFull() const;
void clearTracks(); void clearTracks();
void enqueueTracks(const std::vector<Database::IdType>& trackIds); std::size_t enqueueTracks(const std::vector<Database::IdType>& trackIds);
void enqueueTrack(Database::IdType trackId);
void addSome(); void addSome();
void enqueueRadioTrack(); void enqueueRadioTrack();
void updateInfo(); void updateInfo();
void updateCurrentTrack(bool selected); void updateCurrentTrack(bool selected);
void updateRepeatBtn(); void updateRepeatBtn();
void updateRadioBtn(); void updateRadioBtn();
void displayLoadingIndicator();
void hideLoadingIndicator();
void loadTrack(std::size_t pos, bool play); void loadTrack(std::size_t pos, bool play);
void stop(); void stop();
@@ -79,12 +80,14 @@ class PlayQueue : public Wt::WTemplate
void addRadioTrackFromClusters(); void addRadioTrackFromClusters();
std::optional<float> getReplayGain(std::size_t pos, const Wt::Dbo::ptr<Database::Track>& track) const; std::optional<float> getReplayGain(std::size_t pos, const Wt::Dbo::ptr<Database::Track>& track) const;
static inline constexpr std::size_t _nbMaxEntries {1000};
bool _repeatAll {}; bool _repeatAll {};
bool _radioMode {}; bool _radioMode {};
bool _mediaPlayerSettingsLoaded {}; bool _mediaPlayerSettingsLoaded {};
Database::IdType _tracklistId {}; Database::IdType _tracklistId {};
Wt::WContainerWidget* _entriesContainer {}; Wt::WContainerWidget* _entriesContainer {};
Wt::WPushButton* _showMore {}; Wt::WTemplate* _loadingIndicator {};
Wt::WText* _nbTracks {}; Wt::WText* _nbTracks {};
Wt::WText* _repeatBtn {}; Wt::WText* _repeatBtn {};
Wt::WText* _radioBtn {}; Wt::WText* _radioBtn {};
+9 -2
View File
@@ -19,15 +19,22 @@
#pragma once #pragma once
#include <vector>
#include <Wt/WSignal.h>
#include "database/Types.hpp"
namespace UserInterface namespace UserInterface
{ {
enum class PlayQueueAction enum class PlayQueueAction
{ {
Play, Play,
AddNext, PlayLast,
AddLast, PlayShuffled,
}; };
using PlayQueueActionSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::IdType>&>;
} }
+4 -4
View File
@@ -34,7 +34,7 @@
#include "common/Validators.hpp" #include "common/Validators.hpp"
#include "common/ValueStringModel.hpp" #include "common/ValueStringModel.hpp"
#include "DatabaseStatus.hpp" #include "ScannerController.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
namespace UserInterface { namespace UserInterface {
@@ -220,7 +220,7 @@ DatabaseSettingsView::refreshView()
Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard"))); Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard")));
Wt::WPushButton *immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.Admin.Database.immediate-scan"))); Wt::WPushButton *immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.Admin.Database.immediate-scan")));
t->bindNew<DatabaseStatus>("status"); t->bindNew<ScannerController>("scanner-controller");
saveBtn->clicked().connect([=] () saveBtn->clicked().connect([=] ()
{ {
@@ -230,7 +230,7 @@ DatabaseSettingsView::refreshView()
{ {
model->saveData(); model->saveData();
ServiceProvider<Scanner::IMediaScanner>::get()->requestReschedule(); ServiceProvider<Scanner::IMediaScanner>::get()->requestReload();
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved")); LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved"));
} }
@@ -247,7 +247,7 @@ DatabaseSettingsView::refreshView()
immScanBtn->clicked().connect([=] () immScanBtn->clicked().connect([=] ()
{ {
ServiceProvider<Scanner::IMediaScanner>::get()->requestImmediateScan(); ServiceProvider<Scanner::IMediaScanner>::get()->requestImmediateScan(false);
}); });
t->updateView(model.get()); t->updateView(model.get());
@@ -17,14 +17,16 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "DatabaseStatus.hpp" #include "ScannerController.hpp"
#include <iomanip> #include <iomanip>
#include <Wt/Http/Response.h> #include <Wt/Http/Response.h>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include <Wt/WPopupMenu.h>
#include <Wt/WPushButton.h> #include <Wt/WPushButton.h>
#include <Wt/WResource.h> #include <Wt/WResource.h>
#include <Wt/WSplitButton.h>
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
@@ -66,7 +68,7 @@ class ReportResource : public Wt::WResource
void handleRequest(const Wt::Http::Request&, Wt::Http::Response& response) void handleRequest(const Wt::Http::Request&, Wt::Http::Response& response)
{ {
response.out() << Wt::WString::tr("Lms.Admin.Database.Status.errors-header").arg(_stats.errors.size()).toUTF8() << std::endl; response.out() << Wt::WString::tr("Lms.Admin.ScannerController.errors-header").arg(_stats.errors.size()).toUTF8() << std::endl;
for (const auto& error : _stats.errors) for (const auto& error : _stats.errors)
{ {
@@ -78,7 +80,7 @@ class ReportResource : public Wt::WResource
response.out() << std::endl; response.out() << std::endl;
response.out() << Wt::WString::tr("Lms.Admin.Database.Status.duplicates-header").arg(_stats.duplicates.size()).toUTF8() << std::endl; response.out() << Wt::WString::tr("Lms.Admin.ScannerController.duplicates-header").arg(_stats.duplicates.size()).toUTF8() << std::endl;
for (const auto& duplicate : _stats.duplicates) for (const auto& duplicate : _stats.duplicates)
response.out() << duplicate.file.string() << " - " << duplicateReasonToWString(duplicate.reason).toUTF8() << std::endl; response.out() << duplicate.file.string() << " - " << duplicateReasonToWString(duplicate.reason).toUTF8() << std::endl;
@@ -90,10 +92,10 @@ class ReportResource : public Wt::WResource
{ {
switch (error) switch (error)
{ {
case Scanner::ScanErrorType::CannotReadFile: return Wt::WString::tr("Lms.Admin.Database.Status.cannot-read-file"); case Scanner::ScanErrorType::CannotReadFile: return Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-file");
case Scanner::ScanErrorType::CannotParseFile: return Wt::WString::tr("Lms.Admin.Database.Status.cannot-parse-file"); case Scanner::ScanErrorType::CannotParseFile: return Wt::WString::tr("Lms.Admin.ScannerController.cannot-parse-file");
case Scanner::ScanErrorType::NoAudioTrack: return Wt::WString::tr("Lms.Admin.Database.Status.no-audio-track"); case Scanner::ScanErrorType::NoAudioTrack: return Wt::WString::tr("Lms.Admin.ScannerController.no-audio-track");
case Scanner::ScanErrorType::BadDuration: return Wt::WString::tr("Lms.Admin.Database.Status.bad-duration"); case Scanner::ScanErrorType::BadDuration: return Wt::WString::tr("Lms.Admin.ScannerController.bad-duration");
} }
return "?"; return "?";
} }
@@ -102,8 +104,8 @@ class ReportResource : public Wt::WResource
{ {
switch (reason) switch (reason)
{ {
case Scanner::DuplicateReason::SameHash: return Wt::WString::tr("Lms.Admin.Database.Status.same-hash"); case Scanner::DuplicateReason::SameHash: return Wt::WString::tr("Lms.Admin.ScannerController.same-hash");
case Scanner::DuplicateReason::SameMBID: return Wt::WString::tr("Lms.Admin.Database.Status.same-mbid"); case Scanner::DuplicateReason::SameMBID: return Wt::WString::tr("Lms.Admin.ScannerController.same-mbid");
} }
return "?"; return "?";
} }
@@ -112,8 +114,8 @@ class ReportResource : public Wt::WResource
}; };
DatabaseStatus::DatabaseStatus() ScannerController::ScannerController()
: WTemplate {Wt::WString::tr("Lms.Admin.Database.Status.template")} : WTemplate {Wt::WString::tr("Lms.Admin.ScannerController.template")}
{ {
addFunction("tr", &Wt::WTemplate::Functions::tr); addFunction("tr", &Wt::WTemplate::Functions::tr);
@@ -133,16 +135,34 @@ DatabaseStatus::DatabaseStatus()
} }
void void
DatabaseStatus::refreshContents() ScannerController::refreshContents()
{ {
using namespace Scanner; using namespace Scanner;
Wt::WPushButton* reportBtn {bindNew<Wt::WPushButton>("btn-report", Wt::WString::tr("Lms.Admin.Database.Status.get-report"))}; Wt::WPushButton* reportBtn {bindNew<Wt::WPushButton>("btn-report", Wt::WString::tr("Lms.Admin.ScannerController.get-report"))};
Wt::WSplitButton* actionBtn {bindNew<Wt::WSplitButton>("btn-action")};
actionBtn->actionButton()->addStyleClass("btn-primary");
actionBtn->actionButton()->setText(Wt::WString::tr("Lms.Admin.ScannerController.scan-now"));
actionBtn->actionButton()->clicked().connect([]
{
ServiceProvider<Scanner::IMediaScanner>::get()->requestImmediateScan(false);
});
auto popup = std::make_unique<Wt::WPopupMenu>();
popup->addItem(Wt::WString::tr("Lms.Admin.ScannerController.force-scan-now"));
popup->itemSelected().connect([]
{
ServiceProvider<Scanner::IMediaScanner>::get()->requestImmediateScan(true);
});
actionBtn->dropDownButton()->setMenu(std::move(popup));
actionBtn->dropDownButton()->addStyleClass("btn-primary");
const IMediaScanner::Status status {ServiceProvider<IMediaScanner>::get()->getStatus()}; const IMediaScanner::Status status {ServiceProvider<IMediaScanner>::get()->getStatus()};
if (status.lastCompleteScanStats) if (status.lastCompleteScanStats)
{ {
bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-status") bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-status")
.arg(status.lastCompleteScanStats->nbFiles()) .arg(status.lastCompleteScanStats->nbFiles())
.arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime)) .arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime))
.arg(status.lastCompleteScanStats->stopTime.toString()) .arg(status.lastCompleteScanStats->stopTime.toString())
@@ -156,7 +176,7 @@ DatabaseStatus::refreshContents()
} }
else else
{ {
bindString("last-scan", Wt::WString::tr("Lms.Admin.Database.Status.last-scan-not-available")); bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-not-available"));
reportBtn->setEnabled(false); reportBtn->setEnabled(false);
} }
@@ -164,19 +184,44 @@ DatabaseStatus::refreshContents()
switch (status.currentState) switch (status.currentState)
{ {
case IMediaScanner::State::NotScheduled: case IMediaScanner::State::NotScheduled:
bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-not-scheduled")); bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-not-scheduled"));
bindEmpty("step-status");
break; break;
case IMediaScanner::State::Scheduled: case IMediaScanner::State::Scheduled:
bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-scheduled") bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-scheduled")
.arg(status.nextScheduledScan.toString())); .arg(status.nextScheduledScan.toString()));
bindEmpty("step-status");
break; break;
case IMediaScanner::State::InProgress: case IMediaScanner::State::InProgress:
bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress")
.arg(static_cast<int>(status.currentScanStepStats->currentStep) + 1)
.arg(Scanner::ScanProgressStepCount));
switch (status.currentScanStepStats->currentStep)
{ {
std::ostringstream oss; case Scanner::ScanProgressStep::ChekingForMissingFiles:
bindString("status", Wt::WString::tr("Lms.Admin.Database.Status.status-in-progress") bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-missing-files")
.arg(status.inProgressScanStats->processedFiles) .arg(status.currentScanStepStats->progress()));
.arg(status.inProgressScanStats->filesToScan) break;
.arg(status.inProgressScanStats->progress()));
case Scanner::ScanProgressStep::DiscoveringFiles:
bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-discovering-files")
.arg(status.currentScanStepStats->processedFiles));
break;
case Scanner::ScanProgressStep::ScanningFiles:
bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-files")
.arg(status.currentScanStepStats->processedFiles)
.arg(status.currentScanStepStats->filesToProcess)
.arg(status.currentScanStepStats->progress()));
break;
case Scanner::ScanProgressStep::FetchingTrackFeatures:
bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-fetching-track-features")
.arg(status.currentScanStepStats->processedFiles)
.arg(status.currentScanStepStats->filesToProcess)
.arg(status.currentScanStepStats->progress()));
break;
} }
break; break;
} }
@@ -21,16 +21,17 @@
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
namespace UserInterface { namespace UserInterface
class DatabaseStatus : public Wt::WTemplate
{ {
public:
DatabaseStatus();
private: class ScannerController : public Wt::WTemplate
void refreshContents(); {
}; public:
ScannerController();
private:
void refreshContents();
};
} // namespace DatabaseStatus } // namespace DatabaseStatus
+36
View File
@@ -0,0 +1,36 @@
/*
* 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 "LoadingIndicator.hpp"
namespace UserInterface
{
std::unique_ptr<Wt::WTemplate>
createLoadingIndicator()
{
auto res {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.LoadingIndicator.template"))};
res->addFunction("tr", &Wt::WTemplate::Functions::tr);
res->setScrollVisibilityMargin(200);
res->setScrollVisibilityEnabled(true);
return res;
}
}
+28
View File
@@ -0,0 +1,28 @@
/*
* 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/WTemplate.h>
namespace UserInterface
{
std::unique_ptr<Wt::WTemplate> createLoadingIndicator();
}
+20 -3
View File
@@ -115,11 +115,24 @@ Artist::refreshView()
} }
{ {
Wt::WText* addBtn = bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML); Wt::WText* moreBtn = bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML);
addBtn->clicked().connect([=] moreBtn->clicked().connect([=]
{ {
artistsAction.emit(PlayQueueAction::AddLast, {*artistId}); Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
popup->addItem(Wt::WString::tr("Lms.Explore.play-shuffled"))
->triggered().connect(this, [=]
{
artistsAction.emit(PlayQueueAction::PlayShuffled, {*artistId});
});
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
->triggered().connect(this, [=]
{
artistsAction.emit(PlayQueueAction::PlayLast, {*artistId});
});
popup->exec(moreBtn);
}); });
} }
@@ -135,6 +148,10 @@ Artist::refreshView()
void void
Artist::refreshSimilarArtists(const std::vector<Database::IdType>& similarArtistsId) Artist::refreshSimilarArtists(const std::vector<Database::IdType>& similarArtistsId)
{ {
if (similarArtistsId.empty())
return;
setCondition("if-has-similar-artists", true);
Wt::WContainerWidget* similarArtistsContainer {bindNew<Wt::WContainerWidget>("similar-artists")}; Wt::WContainerWidget* similarArtistsContainer {bindNew<Wt::WContainerWidget>("similar-artists")};
for (Database::IdType artistId : similarArtistsId) for (Database::IdType artistId : similarArtistsId)
+1 -1
View File
@@ -42,7 +42,7 @@ class Artist : public Wt::WTemplate
public: public:
Artist(Filters* filters); Artist(Filters* filters);
Wt::Signal<PlayQueueAction, const std::vector<Database::IdType>&> artistsAction; PlayQueueActionSignal artistsAction;
private: private:
void refreshView(); void refreshView();
+26 -7
View File
@@ -29,6 +29,7 @@
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "common/LoadingIndicator.hpp"
#include "ArtistListHelpers.hpp" #include "ArtistListHelpers.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "Filters.hpp" #include "Filters.hpp"
@@ -75,12 +76,7 @@ Artists::Artists(Filters* filters)
_linkType->changed().connect([this] { refreshView(); }); _linkType->changed().connect([this] { refreshView(); });
_container = bindNew<Wt::WContainerWidget>("artists"); _container = bindNew<Wt::WContainerWidget>("artists");
hideLoadingIndicator();
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
_showMore->clicked().connect([=]
{
addSome();
});
refreshView(); refreshView();
@@ -102,6 +98,26 @@ Artists::refreshView(Mode mode)
refreshView(); refreshView();
} }
void
Artists::displayLoadingIndicator()
{
_loadingIndicator = bindWidget<Wt::WTemplate>("loading-indicator", createLoadingIndicator());
_loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
{
if (!visible)
return;
addSome();
});
}
void
Artists::hideLoadingIndicator()
{
_loadingIndicator = nullptr;
bindEmpty("loading-indicator");
}
std::vector<Artist::pointer> std::vector<Artist::pointer>
Artists::getRandomArtists(std::optional<Range> range, bool& moreResults) Artists::getRandomArtists(std::optional<Range> range, bool& moreResults)
{ {
@@ -203,7 +219,10 @@ Artists::addSome()
_container->addWidget(ArtistListHelpers::createEntry(artist)); _container->addWidget(ArtistListHelpers::createEntry(artist));
} }
_showMore->setHidden(!moreResults); if (moreResults)
displayLoadingIndicator();
else
hideLoadingIndicator();
} }
} // namespace UserInterface } // namespace UserInterface
+5 -5
View File
@@ -24,9 +24,7 @@
#include <Wt/WComboBox.h> #include <Wt/WComboBox.h>
#include <Wt/WContainerWidget.h> #include <Wt/WContainerWidget.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include <Wt/WSignal.h>
#include "database/Types.hpp" #include "database/Types.hpp"
@@ -56,6 +54,8 @@ class Artists : public Wt::WTemplate
void refreshView(); void refreshView();
void refreshView(Mode mode); void refreshView(Mode mode);
void displayLoadingIndicator();
void hideLoadingIndicator();
void addSome(); void addSome();
std::vector<Wt::Dbo::ptr<Database::Artist>> getArtists(std::optional<Database::Range> range, bool& moreResults); std::vector<Wt::Dbo::ptr<Database::Artist>> getArtists(std::optional<Database::Range> range, bool& moreResults);
@@ -67,15 +67,15 @@ class Artists : public Wt::WTemplate
{ {
{Mode::Random, batchSize * 4}, {Mode::Random, batchSize * 4},
{Mode::RecentlyPlayed, batchSize * 4}, {Mode::RecentlyPlayed, batchSize * 4},
{Mode::RecentlyAdded, batchSize * 2}, {Mode::RecentlyAdded, batchSize * 4},
{Mode::MostPlayed, batchSize * 2}, {Mode::MostPlayed, batchSize * 4},
{Mode::All, batchSize * 50}, {Mode::All, batchSize * 50},
}; };
Mode _mode {defaultMode}; Mode _mode {defaultMode};
std::vector<Database::IdType> _randomArtists; std::vector<Database::IdType> _randomArtists;
Filters* _filters {}; Filters* _filters {};
Wt::WPushButton* _showMore {}; Wt::WTemplate* _loadingIndicator {};
Wt::WContainerWidget* _container {}; Wt::WContainerWidget* _container {};
Wt::WComboBox* _linkType {}; Wt::WComboBox* _linkType {};
}; };
+14
View File
@@ -35,6 +35,7 @@
#include "Filters.hpp" #include "Filters.hpp"
#include "ReleasesView.hpp" #include "ReleasesView.hpp"
#include "ReleaseView.hpp" #include "ReleaseView.hpp"
#include "SearchView.hpp"
#include "TracksView.hpp" #include "TracksView.hpp"
namespace UserInterface { namespace UserInterface {
@@ -50,6 +51,7 @@ handleContentsPathChange(Wt::WStackedWidget* stack)
IdxArtist, IdxArtist,
IdxReleases, IdxReleases,
IdxRelease, IdxRelease,
IdxSearch,
IdxTracks, IdxTracks,
}; };
@@ -59,6 +61,7 @@ handleContentsPathChange(Wt::WStackedWidget* stack)
{ "/artist", IdxArtist }, { "/artist", IdxArtist },
{ "/releases", IdxReleases }, { "/releases", IdxReleases },
{ "/release", IdxRelease }, { "/release", IdxRelease },
{ "/search", IdxSearch },
{ "/tracks", IdxTracks }, { "/tracks", IdxTracks },
}; };
@@ -100,6 +103,11 @@ Explore::Explore(Filters* filters)
release->tracksAction.connect(this, &Explore::handleTracksAction); release->tracksAction.connect(this, &Explore::handleTracksAction);
contentsStack->addWidget(std::move(release)); contentsStack->addWidget(std::move(release));
auto search = std::make_unique<SearchView>(_filters);
search->tracksAction.connect(this, &Explore::handleTracksAction);
_search = search.get();
contentsStack->addWidget(std::move(search));
auto tracks = std::make_unique<Tracks>(_filters); auto tracks = std::make_unique<Tracks>(_filters);
tracks->tracksAction.connect(this, &Explore::handleTracksAction); tracks->tracksAction.connect(this, &Explore::handleTracksAction);
contentsStack->addWidget(std::move(tracks)); contentsStack->addWidget(std::move(tracks));
@@ -112,6 +120,12 @@ Explore::Explore(Filters* filters)
handleContentsPathChange(contentsStack); handleContentsPathChange(contentsStack);
} }
void
Explore::search(const Wt::WString& searchText)
{
_search->refreshView(searchText);
}
static static
std::vector<Database::IdType> std::vector<Database::IdType>
getArtistsTracks(Database::Session& session, const std::vector<Database::IdType>& artistsId, const std::set<Database::IdType>&) getArtistsTracks(Database::Session& session, const std::vector<Database::IdType>& artistsId, const std::set<Database::IdType>&)
+5 -1
View File
@@ -27,13 +27,16 @@
namespace UserInterface { namespace UserInterface {
class Filters; class Filters;
class SearchView;
class Explore : public Wt::WTemplate class Explore : public Wt::WTemplate
{ {
public: public:
Explore(Filters* filters); Explore(Filters* filters);
Wt::Signal<PlayQueueAction, const std::vector<Database::IdType>&> tracksAction; void search(const Wt::WString& searchText);
PlayQueueActionSignal tracksAction;
private: private:
@@ -42,6 +45,7 @@ class Explore : public Wt::WTemplate
void handleTracksAction(PlayQueueAction action, const std::vector<Database::IdType>& tracksId); void handleTracksAction(PlayQueueAction action, const std::vector<Database::IdType>& tracksId);
Filters* _filters {}; Filters* _filters {};
SearchView* _search {};
}; };
} // namespace UserInterface } // namespace UserInterface
+49
View File
@@ -0,0 +1,49 @@
/*
* 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 "ReleasePopup.hpp"
#include "LmsApplication.hpp"
namespace UserInterface
{
void
displayReleasePopupMenu(Wt::WInteractWidget& target,
Database::IdType releaseId,
PlayQueueActionSignal& releasesAction)
{
Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
popup->addItem(Wt::WString::tr("Lms.Explore.play-shuffled"))
->triggered().connect(&target, [&releasesAction, releaseId]
{
releasesAction.emit(PlayQueueAction::PlayShuffled, {releaseId});
});
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
->triggered().connect(&target, [&releasesAction, releaseId]
{
releasesAction.emit(PlayQueueAction::PlayLast, {releaseId});
});
popup->popup(&target);
}
} // namespace UserInterface
+34
View File
@@ -0,0 +1,34 @@
/*
* 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/WWidget.h>
#include <Wt/WSignal.h>
#include "database/Types.hpp"
#include "PlayQueueAction.hpp"
namespace UserInterface
{
void displayReleasePopupMenu(Wt::WInteractWidget& target,
Database::IdType releaseId,
PlayQueueActionSignal& releasesAction);
} // namespace UserInterface
+19 -7
View File
@@ -22,7 +22,6 @@
#include <Wt/WAnchor.h> #include <Wt/WAnchor.h>
#include <Wt/WApplication.h> #include <Wt/WApplication.h>
#include <Wt/WImage.h> #include <Wt/WImage.h>
#include <Wt/WPopupMenu.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include <Wt/WText.h> #include <Wt/WText.h>
@@ -39,6 +38,7 @@
#include "LmsApplicationException.hpp" #include "LmsApplicationException.hpp"
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
#include "ReleaseListHelpers.hpp" #include "ReleaseListHelpers.hpp"
#include "ReleasePopup.hpp"
#include "TrackStringUtils.hpp" #include "TrackStringUtils.hpp"
using namespace Database; using namespace Database;
@@ -157,10 +157,10 @@ Release::refreshView()
} }
{ {
Wt::WText* addBtn {bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)}; Wt::WText* moreBtn {bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML)};
addBtn->clicked().connect([=] moreBtn->clicked().connect([=]
{ {
releasesAction.emit(PlayQueueAction::AddLast, {*releaseId}); displayReleasePopupMenu(*moreBtn, *releaseId, releasesAction);
}); });
} }
@@ -237,10 +237,18 @@ Release::refreshView()
tracksAction.emit(PlayQueueAction::Play, {trackId}); tracksAction.emit(PlayQueueAction::Play, {trackId});
}); });
Wt::WText* addBtn {entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)}; Wt::WText* moreBtn {entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML)};
addBtn->clicked().connect([=]() moreBtn->clicked().connect([=]()
{ {
tracksAction.emit(PlayQueueAction::AddLast, {trackId}); Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
->triggered().connect(moreBtn, [=]
{
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
});
popup->popup(moreBtn);
}); });
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
@@ -306,6 +314,10 @@ Release::refreshLinks(const Database::Release::pointer& release)
void void
Release::refreshSimilarReleases(const std::vector<Database::IdType>& similarReleasesId) Release::refreshSimilarReleases(const std::vector<Database::IdType>& similarReleasesId)
{ {
if (similarReleasesId.empty())
return;
setCondition("if-has-similar-releases", true);
auto* similarReleasesContainer {bindNew<Wt::WContainerWidget>("similar-releases")}; auto* similarReleasesContainer {bindNew<Wt::WContainerWidget>("similar-releases")};
for (Database::IdType id : similarReleasesId) for (Database::IdType id : similarReleasesId)
+2 -6
View File
@@ -19,7 +19,6 @@
#pragma once #pragma once
#include <Wt/WSignal.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "database/Types.hpp" #include "database/Types.hpp"
@@ -39,11 +38,8 @@ namespace UserInterface
public: public:
Release(Filters* filters); Release(Filters* filters);
Wt::Signal<PlayQueueAction, const std::vector<Database::IdType>&> releasesAction; PlayQueueActionSignal releasesAction;
Wt::Signal<PlayQueueAction, const std::vector<Database::IdType>&> tracksAction; PlayQueueActionSignal tracksAction;
Wt::Signal<Wt::WWidget&, Database::IdType> createReleaseActionMenu;
Wt::Signal<Wt::WWidget&, Database::IdType> createTrackActionMenu;
private: private:
void refreshView(); void refreshView();
+42 -10
View File
@@ -32,6 +32,7 @@
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
#include "common/LoadingIndicator.hpp"
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
#include "ReleaseListHelpers.hpp" #include "ReleaseListHelpers.hpp"
#include "Filters.hpp" #include "Filters.hpp"
@@ -71,19 +72,27 @@ _filters {filters}
{ {
releasesAction.emit(PlayQueueAction::Play, getAllReleases()); releasesAction.emit(PlayQueueAction::Play, getAllReleases());
}); });
Wt::WText* addBtn {bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)}; Wt::WText* moreBtn {bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML)};
addBtn->clicked().connect([this] moreBtn->clicked().connect([=]
{ {
releasesAction.emit(PlayQueueAction::AddLast, getAllReleases()); Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
popup->addItem(Wt::WString::tr("Lms.Explore.play-shuffled"))
->triggered().connect([this]
{
releasesAction.emit(PlayQueueAction::PlayShuffled, getAllReleases());
});
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
->triggered().connect([this]
{
releasesAction.emit(PlayQueueAction::PlayLast, getAllReleases());
});
popup->popup(moreBtn);
}); });
_container = bindNew<Wt::WContainerWidget>("releases"); _container = bindNew<Wt::WContainerWidget>("releases");
hideLoadingIndicator();
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
_showMore->clicked().connect([this]
{
addSome();
});
refreshView(defaultMode); refreshView(defaultMode);
@@ -105,6 +114,26 @@ Releases::refreshView(Mode mode)
refreshView(); refreshView();
} }
void
Releases::displayLoadingIndicator()
{
_loadingIndicator = bindWidget<Wt::WTemplate>("loading-indicator", createLoadingIndicator());
_loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
{
if (!visible)
return;
addSome();
});
}
void
Releases::hideLoadingIndicator()
{
_loadingIndicator = nullptr;
bindEmpty("loading-indicator");
}
void void
Releases::addSome() Releases::addSome()
{ {
@@ -118,7 +147,10 @@ Releases::addSome()
_container->addWidget(ReleaseListHelpers::createEntry(release)); _container->addWidget(ReleaseListHelpers::createEntry(release));
} }
_showMore->setHidden(!moreResults); if (moreResults)
displayLoadingIndicator();
else
hideLoadingIndicator();
} }
std::vector<Database::Release::pointer> std::vector<Database::Release::pointer>
+11 -8
View File
@@ -22,7 +22,6 @@
#include <optional> #include <optional>
#include <Wt/WContainerWidget.h> #include <Wt/WContainerWidget.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "database/Types.hpp" #include "database/Types.hpp"
@@ -42,7 +41,7 @@ class Releases : public Wt::WTemplate
public: public:
Releases(Filters* filters); Releases(Filters* filters);
Wt::Signal<PlayQueueAction, const std::vector<Database::IdType>&> releasesAction; PlayQueueActionSignal releasesAction;
private: private:
@@ -57,19 +56,23 @@ class Releases : public Wt::WTemplate
void refreshView(); void refreshView();
void refreshView(Mode mode); void refreshView(Mode mode);
void displayLoadingIndicator();
void hideLoadingIndicator();
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>> 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<Wt::Dbo::ptr<Database::Release>> getRandomReleases(std::optional<Database::Range> range, bool& moreResults);
std::vector<Database::IdType> getAllReleases(); std::vector<Database::IdType> getAllReleases();
static constexpr Mode defaultMode {Mode::Random}; static constexpr Mode defaultMode {Mode::Random};
static constexpr std::size_t batchSize {18}; static constexpr std::size_t maxItemsPerLine {6};
static constexpr std::size_t batchSize {maxItemsPerLine * 3};
static inline std::unordered_map<Mode, std::optional<std::size_t>> maxItemsPerMode static inline std::unordered_map<Mode, std::optional<std::size_t>> maxItemsPerMode
{ {
{Mode::Random, batchSize * 6}, {Mode::Random, batchSize * 10},
{Mode::RecentlyPlayed, batchSize * 3}, {Mode::RecentlyPlayed, batchSize * 10},
{Mode::RecentlyAdded, batchSize * 2}, {Mode::RecentlyAdded, batchSize * 10},
{Mode::MostPlayed, batchSize * 2}, {Mode::MostPlayed, batchSize * 10},
{Mode::All, batchSize * 30}, {Mode::All, batchSize * 30},
}; };
@@ -77,7 +80,7 @@ class Releases : public Wt::WTemplate
Filters* _filters {}; Filters* _filters {};
std::vector<Database::IdType> _randomReleases; std::vector<Database::IdType> _randomReleases;
Wt::WContainerWidget* _container {}; Wt::WContainerWidget* _container {};
Wt::WPushButton* _showMore {}; Wt::WTemplate* _loadingIndicator {};
}; };
} // namespace UserInterface } // namespace UserInterface
+3 -3
View File
@@ -51,9 +51,9 @@ namespace UserInterface
} }
void void
SearchView::refreshView(const std::string& searchText) SearchView::refreshView(const Wt::WString& searchText)
{ {
_keywords = StringUtils::splitString(searchText, " "); _keywords = StringUtils::splitString(searchText.toUTF8(), " ");
refreshView(); refreshView();
} }
@@ -127,7 +127,7 @@ namespace UserInterface
auto* container {bindNew<Wt::WContainerWidget>("tracks")}; auto* container {bindNew<Wt::WContainerWidget>("tracks")};
for (const Database::Track::pointer& track : tracks) for (const Database::Track::pointer& track : tracks)
container->addWidget(TrackListHelpers::createEntry(track)); container->addWidget(TrackListHelpers::createEntry(track, tracksAction));
} }
} }
+5 -1
View File
@@ -24,6 +24,8 @@
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "PlayQueueAction.hpp"
namespace UserInterface { namespace UserInterface {
class Filters; class Filters;
@@ -33,7 +35,9 @@ class SearchView : public Wt::WTemplate
public: public:
SearchView(Filters* filters); SearchView(Filters* filters);
void refreshView(const std::string& searchText); PlayQueueActionSignal tracksAction;
void refreshView(const Wt::WString& searchText);
private: private:
void refreshView(); void refreshView();
+14 -7
View File
@@ -30,7 +30,6 @@
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
#include "PlayQueue.hpp"
#include "TrackStringUtils.hpp" #include "TrackStringUtils.hpp"
using namespace Database; using namespace Database;
@@ -38,7 +37,7 @@ using namespace Database;
namespace UserInterface::TrackListHelpers namespace UserInterface::TrackListHelpers
{ {
std::unique_ptr<Wt::WTemplate> std::unique_ptr<Wt::WTemplate>
createEntry(const Wt::Dbo::ptr<Database::Track>& track) createEntry(const Wt::Dbo::ptr<Database::Track>& track, PlayQueueActionSignal& tracksAction)
{ {
auto entry {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))}; auto entry {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))};
auto* entryPtr {entry.get()}; auto* entryPtr {entry.get()};
@@ -89,15 +88,23 @@ namespace UserInterface::TrackListHelpers
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML); Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
playBtn->clicked().connect([=] playBtn->clicked().connect([trackId, &tracksAction]
{ {
LmsApp->getPlayQueue().processTracks(PlayQueueAction::Play, {trackId}); tracksAction.emit(PlayQueueAction::Play, {trackId});
}); });
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML); Wt::WText* moreBtn = entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML);
addBtn->clicked().connect([=] moreBtn->clicked().connect([=, &tracksAction]
{ {
LmsApp->getPlayQueue().processTracks(PlayQueueAction::AddLast, {trackId}); Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
->triggered().connect(moreBtn, [=, &tracksAction]
{
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
});
popup->popup(moreBtn);
}); });
LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=] (Database::IdType loadedTrackId) LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=] (Database::IdType loadedTrackId)
+2 -1
View File
@@ -22,6 +22,7 @@
#include <memory> #include <memory>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "PlayQueueAction.hpp"
namespace Database namespace Database
{ {
@@ -30,6 +31,6 @@ namespace Database
namespace UserInterface::TrackListHelpers namespace UserInterface::TrackListHelpers
{ {
std::unique_ptr<Wt::WTemplate> createEntry(const Wt::Dbo::ptr<Database::Track>& track); std::unique_ptr<Wt::WTemplate> createEntry(const Wt::Dbo::ptr<Database::Track>& track, PlayQueueActionSignal& tracksAction);
} // namespace UserInterface } // namespace UserInterface
+43 -12
View File
@@ -32,8 +32,8 @@
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
#include "common/LoadingIndicator.hpp"
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
#include "Filters.hpp" #include "Filters.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
@@ -75,19 +75,27 @@ _filters {filters}
tracksAction.emit(PlayQueueAction::Play, getAllTracks()); tracksAction.emit(PlayQueueAction::Play, getAllTracks());
}); });
Wt::WText* addBtn = bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML); Wt::WText* moreBtn = bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML);
addBtn->clicked().connect([=] moreBtn->clicked().connect([=]
{ {
tracksAction.emit(PlayQueueAction::AddLast, getAllTracks()); Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
popup->addItem(Wt::WString::tr("Lms.Explore.play-shuffled"))
->triggered().connect(this, [this]
{
tracksAction.emit(PlayQueueAction::PlayShuffled, getAllTracks());
});
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
->triggered().connect(this, [this]
{
tracksAction.emit(PlayQueueAction::PlayLast, getAllTracks());
});
popup->popup(moreBtn);
}); });
_tracksContainer = bindNew<Wt::WContainerWidget>("tracks"); _tracksContainer = bindNew<Wt::WContainerWidget>("tracks");
hideLoadingIndicator();
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
_showMore->clicked().connect([this]
{
addSome();
});
filters->updated().connect([this] filters->updated().connect([this]
{ {
@@ -112,6 +120,26 @@ Tracks::refreshView(Mode mode)
refreshView(); refreshView();
} }
void
Tracks::displayLoadingIndicator()
{
_loadingIndicator = bindWidget<Wt::WTemplate>("loading-indicator", createLoadingIndicator());
_loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
{
if (!visible)
return;
addSome();
});
}
void
Tracks::hideLoadingIndicator()
{
_loadingIndicator = nullptr;
bindEmpty("loading-indicator");
}
std::vector<Database::Track::pointer> std::vector<Database::Track::pointer>
Tracks::getRandomTracks(std::optional<Range> range, bool& moreResults) Tracks::getRandomTracks(std::optional<Range> range, bool& moreResults)
{ {
@@ -203,10 +231,13 @@ Tracks::addSome()
bool moreResults; bool moreResults;
for (const Track::pointer& track : getTracks(Range {static_cast<std::size_t>(_tracksContainer->count()), batchSize}, moreResults)) for (const Track::pointer& track : getTracks(Range {static_cast<std::size_t>(_tracksContainer->count()), batchSize}, moreResults))
{ {
_tracksContainer->addWidget(TrackListHelpers::createEntry(track)); _tracksContainer->addWidget(TrackListHelpers::createEntry(track, tracksAction));
} }
_showMore->setHidden(!moreResults); if (moreResults)
displayLoadingIndicator();
else
hideLoadingIndicator();
} }
} // namespace UserInterface } // namespace UserInterface
+4 -5
View File
@@ -22,8 +22,6 @@
#include <optional> #include <optional>
#include <Wt/WContainerWidget.h> #include <Wt/WContainerWidget.h>
#include <Wt/WLineEdit.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "database/Types.hpp" #include "database/Types.hpp"
@@ -42,7 +40,7 @@ class Tracks : public Wt::WTemplate
public: public:
Tracks(Filters* filters); Tracks(Filters* filters);
Wt::Signal<PlayQueueAction, const std::vector<Database::IdType>&> tracksAction; PlayQueueActionSignal tracksAction;
private: private:
@@ -57,6 +55,8 @@ class Tracks : public Wt::WTemplate
void refreshView(); void refreshView();
void refreshView(Mode mode); void refreshView(Mode mode);
void displayLoadingIndicator();
void hideLoadingIndicator();
void addSome(); void addSome();
std::vector<Wt::Dbo::ptr<Database::Track>> getRandomTracks(std::optional<Database::Range> range, bool& moreResults); std::vector<Wt::Dbo::ptr<Database::Track>> getRandomTracks(std::optional<Database::Range> range, bool& moreResults);
@@ -78,8 +78,7 @@ class Tracks : public Wt::WTemplate
Filters* _filters {}; Filters* _filters {};
std::vector<Database::IdType> _randomTracks; std::vector<Database::IdType> _randomTracks;
Wt::WContainerWidget* _tracksContainer {}; Wt::WContainerWidget* _tracksContainer {};
Wt::WPushButton* _showMore {}; Wt::WTemplate* _loadingIndicator {};
Wt::WLineEdit* _search {};
}; };
} // namespace UserInterface } // namespace UserInterface