Merge branch 'develop' for release v3.18.0
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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,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}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 {};
|
||||||
|
|||||||
@@ -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>&>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>&)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user