Merge branch 'develop' for release v3.14.0
This commit is contained in:
@@ -15,12 +15,13 @@ A [demo](http://lms.demo.poupon.io) instance is available, with the following li
|
||||
* User management
|
||||
* Recommendation engine
|
||||
* Audio transcode for maximum interoperability and low bandwith requirements
|
||||
* ReplayGain support
|
||||
* Persistent play queue across sessions
|
||||
* Compilation support
|
||||
* Multi-value tags: artists, genres, ...
|
||||
* Custom tags (ex: _mood_, _genre_, _albummood_, _albumgrouping_, ...)
|
||||
* MusicBrainzID support to handle duplicated artist and release names
|
||||
* ReplayGain support
|
||||
* Disc subtitles support
|
||||
* _Systemd_ integration
|
||||
* Subsonic API, with the following additional features:
|
||||
* Playlists
|
||||
|
||||
+3
-1
@@ -26,7 +26,9 @@
|
||||
<div class="col-xs-12">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle">
|
||||
${cover class="media-object Lms-cover"}
|
||||
<div class="media-object Lms-cover-container-medium">
|
||||
${cover}
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<h4 class="media-heading">
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden-xs col-sm-2 Lms-player-duration text-muted">
|
||||
<a id="lms-transcoding-active" class="Lms-player-btn Lms-control-btn" data-toggle="tooltip" title="${tr:Lms.Player.transcoding-active}" style="display:none"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></a>
|
||||
<span id="lms-mp-curtime">--</span> / <span id="lms-mp-duration">--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message>
|
||||
|
||||
<!--User-->
|
||||
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Audio transcode bitrate limit</message>
|
||||
<message id="Lms.Admin.User.demo-account">Demo account</message>
|
||||
<message id="Lms.Admin.User.demo-account-already-exists">Demo account already exists!</message>
|
||||
<message id="Lms.Admin.User.demo-password-invalid">Demo password must be the login name!</message>
|
||||
@@ -135,6 +134,9 @@
|
||||
<message id="Lms.Explore.Search.search-placeholder">Search...</message>
|
||||
<message id="Lms.Explore.Search.search">Search results</message>
|
||||
|
||||
<!--Player-->
|
||||
<message id="Lms.Player.transcoding-active">Transcoding active</message>
|
||||
|
||||
<!--Playqueue-->
|
||||
<message id="Lms.PlayQueue.clear">Clear</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks">{1} tracks</message>
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message>
|
||||
|
||||
<!--User-->
|
||||
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Limite du bitrate pour le transcodage</message>
|
||||
<message id="Lms.Admin.User.demo-account">Compte de démonstration</message>
|
||||
<message id="Lms.Admin.User.demo-account-already-exists">Le compte de démonstration existe déjà !</message>
|
||||
<message id="Lms.Admin.User.demo-password-invalid">Le password doit être égal au login !</message>
|
||||
@@ -135,6 +134,9 @@
|
||||
<message id="Lms.Explore.Search.search-placeholder">Rechercher...</message>
|
||||
<message id="Lms.Explore.Search.search">Résultats de la recherche</message>
|
||||
|
||||
<!--Player-->
|
||||
<message id="Lms.Player.transcoding-active">Transcodage actif</message>
|
||||
|
||||
<!--Playqueue-->
|
||||
<message id="Lms.PlayQueue.clear">Effacer</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks">{1} pistes</message>
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
<div class="col-xs-9 Lms-playqueue-entry-name">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle hidden-xs">
|
||||
${cover class="media-object Lms-cover Lms-cover-container-smaller"}
|
||||
<div class="media-object Lms-cover-container-small">
|
||||
${cover}
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
<div class="col-xs-12">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle">
|
||||
${cover class="media-object Lms-cover Lms-cover-container-medium"}
|
||||
<div class="media-object Lms-cover-container-medium">
|
||||
${cover}
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<h4 class="media-heading">
|
||||
@@ -40,7 +42,9 @@
|
||||
<div class="col-xs-12">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle">
|
||||
${cover class="media-object Lms-cover Lms-cover-container-smaller"}
|
||||
<div class="media-object Lms-cover-container-smaller">
|
||||
${cover}
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<div class="media-heading">
|
||||
|
||||
+3
-1
@@ -25,7 +25,9 @@
|
||||
<div class="col-xs-12 Lms-explore-tracks-entry-name">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle hidden-xs">
|
||||
${cover class="media-object Lms-cover"}
|
||||
<div class="media-object Lms-cover-container-small">
|
||||
${cover}
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
||||
|
||||
+3
-1
@@ -26,7 +26,9 @@
|
||||
<div class="col-xs-9 Lms-explore-tracks-entry-name">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle hidden-xs">
|
||||
${cover class="media-object Lms-cover"}
|
||||
<div class="media-object Lms-cover-container-small">
|
||||
${cover}
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.TracksInfo.template">
|
||||
<h3>${tr:Lms.Explore.TracksInfo.most-played}</h3>
|
||||
${most-played}
|
||||
<h3>${tr:Lms.Explore.TracksInfo.recently-added}</h3>
|
||||
${recently-added}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.TracksInfo.template.entry">
|
||||
<div class="row Lms-explore-tracksinfo-entry Lms-vertical-align">
|
||||
<div class="col-xs-12 Lms-trackname">
|
||||
${name}
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
+2
-1
@@ -177,7 +177,8 @@ RUN \
|
||||
|
||||
## Release Stage
|
||||
FROM alpine:3.10 AS release
|
||||
MAINTAINER Emeric Poupon <itmfr@yahoo.fr>
|
||||
LABEL maintainer="Emeric Poupon <itmfr@yahoo.fr>"
|
||||
LABEL version=${LMS_VERSION}
|
||||
|
||||
ARG RUNTIME_PACKAGES=" \
|
||||
openssl \
|
||||
|
||||
+25
-32
@@ -62,35 +62,41 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Lms-cover {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.Lms-cover:hover {
|
||||
filter: brightness(130%);
|
||||
}
|
||||
|
||||
.Lms-cover-container-smaller {
|
||||
}
|
||||
|
||||
.Lms-cover-smaller {
|
||||
width: 64px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||
height: 64px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Lms-cover-container-small {
|
||||
}
|
||||
|
||||
.Lms-cover-small {
|
||||
width: 96px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||
height: 96px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Lms-cover-container-medium {
|
||||
}
|
||||
|
||||
.Lms-cover-medium {
|
||||
width: 128px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||
height: 128px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Lms-cover-large {
|
||||
@@ -119,8 +125,8 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
}
|
||||
|
||||
.Lms-explore-releases-entry {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.Lms-explore-release-cover {
|
||||
@@ -181,8 +187,8 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
}
|
||||
|
||||
.Lms-explore-artist-entry {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.Lms-explore-artist-entries-container {
|
||||
@@ -193,10 +199,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.Lms-explore-tracks-entry {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.Lms-explore-tracks-entry:hover .Lms-explore-tracks-entry-controls {
|
||||
display: block;
|
||||
}
|
||||
@@ -221,11 +223,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Lms-explore-tracksinfo-entry {
|
||||
min-height: 28px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Lms-header {
|
||||
padding-bottom: 9px;
|
||||
margin-top: 20px;
|
||||
@@ -248,10 +245,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.Lms-playqueue-entry {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.Lms-playqueue-entry:hover .Lms-playqueue-entry-controls {
|
||||
display: block;
|
||||
}
|
||||
|
||||
+7
File diff suppressed because one or more lines are too long
@@ -1,211 +0,0 @@
|
||||
/* ========================================================================
|
||||
* Bootstrap: collapse.js v3.3.6
|
||||
* http://getbootstrap.com/javascript/#collapse
|
||||
* ========================================================================
|
||||
* Copyright 2011-2015 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* ======================================================================== */
|
||||
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
// COLLAPSE PUBLIC CLASS DEFINITION
|
||||
// ================================
|
||||
|
||||
var Collapse = function (element, options) {
|
||||
this.$element = $(element)
|
||||
this.options = $.extend({}, Collapse.DEFAULTS, options)
|
||||
this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' +
|
||||
'[data-toggle="collapse"][data-target="#' + element.id + '"]')
|
||||
this.transitioning = null
|
||||
|
||||
if (this.options.parent) {
|
||||
this.$parent = this.getParent()
|
||||
} else {
|
||||
this.addAriaAndCollapsedClass(this.$element, this.$trigger)
|
||||
}
|
||||
|
||||
if (this.options.toggle) this.toggle()
|
||||
}
|
||||
|
||||
Collapse.VERSION = '3.3.6'
|
||||
|
||||
Collapse.TRANSITION_DURATION = 350
|
||||
|
||||
Collapse.DEFAULTS = {
|
||||
toggle: true
|
||||
}
|
||||
|
||||
Collapse.prototype.dimension = function () {
|
||||
var hasWidth = this.$element.hasClass('width')
|
||||
return hasWidth ? 'width' : 'height'
|
||||
}
|
||||
|
||||
Collapse.prototype.show = function () {
|
||||
if (this.transitioning || this.$element.hasClass('in')) return
|
||||
|
||||
var activesData
|
||||
var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing')
|
||||
|
||||
if (actives && actives.length) {
|
||||
activesData = actives.data('bs.collapse')
|
||||
if (activesData && activesData.transitioning) return
|
||||
}
|
||||
|
||||
var startEvent = $.Event('show.bs.collapse')
|
||||
this.$element.trigger(startEvent)
|
||||
if (startEvent.isDefaultPrevented()) return
|
||||
|
||||
if (actives && actives.length) {
|
||||
Plugin.call(actives, 'hide')
|
||||
activesData || actives.data('bs.collapse', null)
|
||||
}
|
||||
|
||||
var dimension = this.dimension()
|
||||
|
||||
this.$element
|
||||
.removeClass('collapse')
|
||||
.addClass('collapsing')[dimension](0)
|
||||
.attr('aria-expanded', true)
|
||||
|
||||
this.$trigger
|
||||
.removeClass('collapsed')
|
||||
.attr('aria-expanded', true)
|
||||
|
||||
this.transitioning = 1
|
||||
|
||||
var complete = function () {
|
||||
this.$element
|
||||
.removeClass('collapsing')
|
||||
.addClass('collapse in')[dimension]('')
|
||||
this.transitioning = 0
|
||||
this.$element
|
||||
.trigger('shown.bs.collapse')
|
||||
}
|
||||
|
||||
if (!$.support.transition) return complete.call(this)
|
||||
|
||||
var scrollSize = $.camelCase(['scroll', dimension].join('-'))
|
||||
|
||||
this.$element
|
||||
.one('bsTransitionEnd', $.proxy(complete, this))
|
||||
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize])
|
||||
}
|
||||
|
||||
Collapse.prototype.hide = function () {
|
||||
if (this.transitioning || !this.$element.hasClass('in')) return
|
||||
|
||||
var startEvent = $.Event('hide.bs.collapse')
|
||||
this.$element.trigger(startEvent)
|
||||
if (startEvent.isDefaultPrevented()) return
|
||||
|
||||
var dimension = this.dimension()
|
||||
|
||||
this.$element[dimension](this.$element[dimension]())[0].offsetHeight
|
||||
|
||||
this.$element
|
||||
.addClass('collapsing')
|
||||
.removeClass('collapse in')
|
||||
.attr('aria-expanded', false)
|
||||
|
||||
this.$trigger
|
||||
.addClass('collapsed')
|
||||
.attr('aria-expanded', false)
|
||||
|
||||
this.transitioning = 1
|
||||
|
||||
var complete = function () {
|
||||
this.transitioning = 0
|
||||
this.$element
|
||||
.removeClass('collapsing')
|
||||
.addClass('collapse')
|
||||
.trigger('hidden.bs.collapse')
|
||||
}
|
||||
|
||||
if (!$.support.transition) return complete.call(this)
|
||||
|
||||
this.$element
|
||||
[dimension](0)
|
||||
.one('bsTransitionEnd', $.proxy(complete, this))
|
||||
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)
|
||||
}
|
||||
|
||||
Collapse.prototype.toggle = function () {
|
||||
this[this.$element.hasClass('in') ? 'hide' : 'show']()
|
||||
}
|
||||
|
||||
Collapse.prototype.getParent = function () {
|
||||
return $(this.options.parent)
|
||||
.find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
|
||||
.each($.proxy(function (i, element) {
|
||||
var $element = $(element)
|
||||
this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element)
|
||||
}, this))
|
||||
.end()
|
||||
}
|
||||
|
||||
Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) {
|
||||
var isOpen = $element.hasClass('in')
|
||||
|
||||
$element.attr('aria-expanded', isOpen)
|
||||
$trigger
|
||||
.toggleClass('collapsed', !isOpen)
|
||||
.attr('aria-expanded', isOpen)
|
||||
}
|
||||
|
||||
function getTargetFromTrigger($trigger) {
|
||||
var href
|
||||
var target = $trigger.attr('data-target')
|
||||
|| (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
|
||||
|
||||
return $(target)
|
||||
}
|
||||
|
||||
|
||||
// COLLAPSE PLUGIN DEFINITION
|
||||
// ==========================
|
||||
|
||||
function Plugin(option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('bs.collapse')
|
||||
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||
|
||||
if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false
|
||||
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
|
||||
if (typeof option == 'string') data[option]()
|
||||
})
|
||||
}
|
||||
|
||||
var old = $.fn.collapse
|
||||
|
||||
$.fn.collapse = Plugin
|
||||
$.fn.collapse.Constructor = Collapse
|
||||
|
||||
|
||||
// COLLAPSE NO CONFLICT
|
||||
// ====================
|
||||
|
||||
$.fn.collapse.noConflict = function () {
|
||||
$.fn.collapse = old
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
// COLLAPSE DATA-API
|
||||
// =================
|
||||
|
||||
$(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) {
|
||||
var $this = $(this)
|
||||
|
||||
if (!$this.attr('data-target')) e.preventDefault()
|
||||
|
||||
var $target = getTargetFromTrigger($this)
|
||||
var data = $target.data('bs.collapse')
|
||||
var option = data ? 'toggle' : $this.data()
|
||||
|
||||
Plugin.call($target, option)
|
||||
})
|
||||
|
||||
}(jQuery);
|
||||
@@ -1,165 +0,0 @@
|
||||
/* ========================================================================
|
||||
* Bootstrap: dropdown.js v3.3.6
|
||||
* http://getbootstrap.com/javascript/#dropdowns
|
||||
* ========================================================================
|
||||
* Copyright 2011-2015 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* ======================================================================== */
|
||||
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
// DROPDOWN CLASS DEFINITION
|
||||
// =========================
|
||||
|
||||
var backdrop = '.dropdown-backdrop'
|
||||
var toggle = '[data-toggle="dropdown"]'
|
||||
var Dropdown = function (element) {
|
||||
$(element).on('click.bs.dropdown', this.toggle)
|
||||
}
|
||||
|
||||
Dropdown.VERSION = '3.3.6'
|
||||
|
||||
function getParent($this) {
|
||||
var selector = $this.attr('data-target')
|
||||
|
||||
if (!selector) {
|
||||
selector = $this.attr('href')
|
||||
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
|
||||
}
|
||||
|
||||
var $parent = selector && $(selector)
|
||||
|
||||
return $parent && $parent.length ? $parent : $this.parent()
|
||||
}
|
||||
|
||||
function clearMenus(e) {
|
||||
if (e && e.which === 3) return
|
||||
$(backdrop).remove()
|
||||
$(toggle).each(function () {
|
||||
var $this = $(this)
|
||||
var $parent = getParent($this)
|
||||
var relatedTarget = { relatedTarget: this }
|
||||
|
||||
if (!$parent.hasClass('open')) return
|
||||
|
||||
if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return
|
||||
|
||||
$parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
|
||||
|
||||
if (e.isDefaultPrevented()) return
|
||||
|
||||
$this.attr('aria-expanded', 'false')
|
||||
$parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget))
|
||||
})
|
||||
}
|
||||
|
||||
Dropdown.prototype.toggle = function (e) {
|
||||
var $this = $(this)
|
||||
|
||||
if ($this.is('.disabled, :disabled')) return
|
||||
|
||||
var $parent = getParent($this)
|
||||
var isActive = $parent.hasClass('open')
|
||||
|
||||
clearMenus()
|
||||
|
||||
if (!isActive) {
|
||||
if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
|
||||
// if mobile we use a backdrop because click events don't delegate
|
||||
$(document.createElement('div'))
|
||||
.addClass('dropdown-backdrop')
|
||||
.insertAfter($(this))
|
||||
.on('click', clearMenus)
|
||||
}
|
||||
|
||||
var relatedTarget = { relatedTarget: this }
|
||||
$parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))
|
||||
|
||||
if (e.isDefaultPrevented()) return
|
||||
|
||||
$this
|
||||
.trigger('focus')
|
||||
.attr('aria-expanded', 'true')
|
||||
|
||||
$parent
|
||||
.toggleClass('open')
|
||||
.trigger($.Event('shown.bs.dropdown', relatedTarget))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
Dropdown.prototype.keydown = function (e) {
|
||||
if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return
|
||||
|
||||
var $this = $(this)
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if ($this.is('.disabled, :disabled')) return
|
||||
|
||||
var $parent = getParent($this)
|
||||
var isActive = $parent.hasClass('open')
|
||||
|
||||
if (!isActive && e.which != 27 || isActive && e.which == 27) {
|
||||
if (e.which == 27) $parent.find(toggle).trigger('focus')
|
||||
return $this.trigger('click')
|
||||
}
|
||||
|
||||
var desc = ' li:not(.disabled):visible a'
|
||||
var $items = $parent.find('.dropdown-menu' + desc)
|
||||
|
||||
if (!$items.length) return
|
||||
|
||||
var index = $items.index(e.target)
|
||||
|
||||
if (e.which == 38 && index > 0) index-- // up
|
||||
if (e.which == 40 && index < $items.length - 1) index++ // down
|
||||
if (!~index) index = 0
|
||||
|
||||
$items.eq(index).trigger('focus')
|
||||
}
|
||||
|
||||
|
||||
// DROPDOWN PLUGIN DEFINITION
|
||||
// ==========================
|
||||
|
||||
function Plugin(option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('bs.dropdown')
|
||||
|
||||
if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))
|
||||
if (typeof option == 'string') data[option].call($this)
|
||||
})
|
||||
}
|
||||
|
||||
var old = $.fn.dropdown
|
||||
|
||||
$.fn.dropdown = Plugin
|
||||
$.fn.dropdown.Constructor = Dropdown
|
||||
|
||||
|
||||
// DROPDOWN NO CONFLICT
|
||||
// ====================
|
||||
|
||||
$.fn.dropdown.noConflict = function () {
|
||||
$.fn.dropdown = old
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
// APPLY TO STANDARD DROPDOWN ELEMENTS
|
||||
// ===================================
|
||||
|
||||
$(document)
|
||||
.on('click.bs.dropdown.data-api', clearMenus)
|
||||
.on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
|
||||
.on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
|
||||
.on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
|
||||
.on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown)
|
||||
|
||||
}(jQuery);
|
||||
@@ -137,6 +137,9 @@ LMS.mediaplayer = function () {
|
||||
_elems.duration = document.getElementById("lms-mp-duration");
|
||||
_elems.volume = document.getElementById("lms-mp-volume");
|
||||
_elems.volumeslider = document.getElementById("lms-mp-volume-slider");
|
||||
_elems.transcodingActive = document.getElementById("lms-transcoding-active");
|
||||
|
||||
$(_elems.transcodingActive).tooltip()
|
||||
|
||||
var source = _audioCtx.createMediaElementSource(_elems.audio);
|
||||
source.connect(_gainNode);
|
||||
@@ -198,6 +201,15 @@ LMS.mediaplayer = function () {
|
||||
Wt.emit(_root, "playbackEnded");
|
||||
});
|
||||
|
||||
_elems.audio.addEventListener("canplay", function() {
|
||||
if (_getAudioMode() == Mode.Transcode) {
|
||||
_elems.transcodingActive.style.display = "inline-block";
|
||||
}
|
||||
else {
|
||||
_elems.transcodingActive.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
_initVolume();
|
||||
_initDefaultSettings(defaultSettings);
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/* ========================================================================
|
||||
* Bootstrap: transition.js v3.3.6
|
||||
* http://getbootstrap.com/javascript/#transitions
|
||||
* ========================================================================
|
||||
* Copyright 2011-2015 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* ======================================================================== */
|
||||
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
// CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
|
||||
// ============================================================
|
||||
|
||||
function transitionEnd() {
|
||||
var el = document.createElement('bootstrap')
|
||||
|
||||
var transEndEventNames = {
|
||||
WebkitTransition : 'webkitTransitionEnd',
|
||||
MozTransition : 'transitionend',
|
||||
OTransition : 'oTransitionEnd otransitionend',
|
||||
transition : 'transitionend'
|
||||
}
|
||||
|
||||
for (var name in transEndEventNames) {
|
||||
if (el.style[name] !== undefined) {
|
||||
return { end: transEndEventNames[name] }
|
||||
}
|
||||
}
|
||||
|
||||
return false // explicit for ie8 ( ._.)
|
||||
}
|
||||
|
||||
// http://blog.alexmaccaw.com/css-transitions
|
||||
$.fn.emulateTransitionEnd = function (duration) {
|
||||
var called = false
|
||||
var $el = this
|
||||
$(this).one('bsTransitionEnd', function () { called = true })
|
||||
var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
|
||||
setTimeout(callback, duration)
|
||||
return this
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$.support.transition = transitionEnd()
|
||||
|
||||
if (!$.support.transition) return
|
||||
|
||||
$.event.special.bsTransitionEnd = {
|
||||
bindType: $.support.transition.end,
|
||||
delegateType: $.support.transition.end,
|
||||
handle: function (e) {
|
||||
if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}(jQuery);
|
||||
@@ -101,7 +101,10 @@ createQuery(Session& session,
|
||||
{
|
||||
clauses.push_back("a.name LIKE ?");
|
||||
query.bind("%%" + keyword + "%%");
|
||||
}
|
||||
|
||||
for (const std::string& keyword : keywords)
|
||||
{
|
||||
sortClauses.push_back("a.sort_name LIKE ?");
|
||||
query.bind("%%" + keyword + "%%");
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
#define LMS_DATABASE_VERSION 22
|
||||
#define LMS_DATABASE_VERSION 23
|
||||
|
||||
using Version = std::size_t;
|
||||
|
||||
@@ -254,6 +254,13 @@ CREATE TABLE "user_backup" (
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 22)
|
||||
{
|
||||
_session.execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||
|
||||
@@ -91,6 +91,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
void setDiscNumber(int num) { _discNumber = num; }
|
||||
void setTotalTrack(std::optional<int> totalTrack) { totalTrack ? _totalTrack = *totalTrack : 0; }
|
||||
void setTotalDisc(std::optional<int> totalDisc) { totalDisc ? _totalDisc = *totalDisc : 0; }
|
||||
void setDiscSubtitle(const std::string& name) { _discSubtitle = name; }
|
||||
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
|
||||
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
|
||||
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
||||
@@ -113,6 +114,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
std::optional<std::size_t> getTrackNumber() const;
|
||||
std::optional<std::size_t> getTotalTrack() const;
|
||||
std::optional<std::size_t> getDiscNumber() const;
|
||||
const std::string& getDiscSubtitle() const { return _discSubtitle; }
|
||||
std::optional<std::size_t> getTotalDisc() const;
|
||||
std::string getName() const { return _name; }
|
||||
std::filesystem::path getPath() const { return _filePath; }
|
||||
@@ -145,6 +147,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||
Wt::Dbo::field(a, _trackNumber, "track_number");
|
||||
Wt::Dbo::field(a, _discNumber, "disc_number");
|
||||
Wt::Dbo::field(a, _discSubtitle, "disc_subtitle");
|
||||
Wt::Dbo::field(a, _totalTrack, "total_track");
|
||||
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
@@ -177,6 +180,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
int _scanVersion {};
|
||||
int _trackNumber {};
|
||||
int _discNumber {};
|
||||
std::string _discSubtitle;
|
||||
int _totalTrack {};
|
||||
int _totalDisc {};
|
||||
std::string _name;
|
||||
|
||||
@@ -211,6 +211,12 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
track.musicBrainzTrackID = UUID::fromString(value);
|
||||
}
|
||||
else if (tag == "TSST"
|
||||
|| tag == "DISCSUBTITLE"
|
||||
|| tag == "SETSUBTITLE")
|
||||
{
|
||||
track.discSubtitle = value;
|
||||
}
|
||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||
{
|
||||
std::vector<std::string> clusterNames {StringUtils::splitString(value, "/,;")};
|
||||
|
||||
@@ -270,6 +270,8 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|
||||
track.albumReplayGain = StringUtils::readAs<float>(value);
|
||||
else if (tag == "REPLAYGAIN_TRACK_GAIN")
|
||||
track.trackReplayGain = StringUtils::readAs<float>(value);
|
||||
else if (tag == "DISCSUBTITLE" || tag == "SETSUBTITLE")
|
||||
track.discSubtitle = value;
|
||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||
{
|
||||
std::set<std::string> clusterNames;
|
||||
@@ -372,8 +374,12 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
if (mp3File->ID3v2Tag())
|
||||
{
|
||||
if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
|
||||
const auto& frameListMap {mp3File->ID3v2Tag()->frameListMap()};
|
||||
|
||||
if (!frameListMap["APIC"].isEmpty())
|
||||
track.hasCover = true;
|
||||
if (!frameListMap["TSST"].isEmpty())
|
||||
properties.insert("DISCSUBTITLE", frameListMap["TSST"].front()->toString());
|
||||
}
|
||||
|
||||
getAPETags(mp3File->APETag());
|
||||
|
||||
@@ -58,25 +58,26 @@ namespace MetaData
|
||||
{
|
||||
std::vector<Artist> artists;
|
||||
std::vector<Artist> albumArtists;
|
||||
std::string title;
|
||||
std::string title;
|
||||
std::optional<UUID> musicBrainzTrackID;
|
||||
std::optional<UUID> musicBrainzRecordID;
|
||||
std::optional<Album> album;
|
||||
Clusters clusters;
|
||||
std::chrono::milliseconds duration {};
|
||||
std::optional<Album> album;
|
||||
Clusters clusters;
|
||||
std::chrono::milliseconds duration;
|
||||
std::optional<std::size_t> trackNumber;
|
||||
std::optional<std::size_t> totalTrack;
|
||||
std::optional<std::size_t> discNumber;
|
||||
std::optional<std::size_t> totalDisc;
|
||||
std::optional<int> year;
|
||||
std::optional<int> originalYear;
|
||||
bool hasCover {false};
|
||||
bool hasCover {};
|
||||
std::vector<AudioStream> audioStreams;
|
||||
std::optional<UUID> acoustID;
|
||||
std::string copyright;
|
||||
std::string copyrightURL;
|
||||
std::optional<float> trackReplayGain;
|
||||
std::optional<float> albumReplayGain;
|
||||
std::string copyright;
|
||||
std::string copyrightURL;
|
||||
std::optional<float> trackReplayGain;
|
||||
std::optional<float> albumReplayGain;
|
||||
std::string discSubtitle;
|
||||
};
|
||||
|
||||
class IParser
|
||||
|
||||
@@ -738,6 +738,8 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
|
||||
track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0);
|
||||
track.modify()->setTotalTrack(trackInfo->totalTrack);
|
||||
track.modify()->setTotalDisc(trackInfo->totalDisc);
|
||||
if (!trackInfo->discSubtitle.empty())
|
||||
track.modify()->setDiscSubtitle(trackInfo->discSubtitle);
|
||||
track.modify()->setYear(trackInfo->year ? *trackInfo->year : 0);
|
||||
track.modify()->setOriginalYear(trackInfo->originalYear ? *trackInfo->originalYear : 0);
|
||||
|
||||
|
||||
@@ -141,11 +141,9 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||
|
||||
// Require js here to avoid async problems
|
||||
requireJQuery("js/jquery-1.10.2.min.js");
|
||||
require("js/mediaplayer.js");
|
||||
require("js/bootstrap-notify.js");
|
||||
require("js/collapse.js");
|
||||
require("js/dropdown.js");
|
||||
require("js/transition.js");
|
||||
require("js/bootstrap.min.js");
|
||||
require("js/mediaplayer.js");
|
||||
|
||||
setTitle("LMS");
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ LmsTheme::LmsTheme(Database::User::UITheme theme)
|
||||
void
|
||||
LmsTheme::setTheme(Database::User::UITheme theme)
|
||||
{
|
||||
if (theme == _theme)
|
||||
return;
|
||||
|
||||
// Hack, use the application interface directly since changing theme is not allowed
|
||||
const auto currentStyleSheets {getStyleSheets(_theme)};
|
||||
for (auto it {std::crbegin(currentStyleSheets)}; it != std::crend(currentStyleSheets); ++it)
|
||||
|
||||
@@ -196,6 +196,8 @@ MediaPlayer::MediaPlayer()
|
||||
playNext {this, "playNext"},
|
||||
_settingsLoaded {this, "settingsLoaded"}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_title = bindNew<Wt::WText>("title");
|
||||
_artist = bindNew<Wt::WAnchor>("artist");
|
||||
_release = bindNew<Wt::WAnchor>("release");
|
||||
|
||||
@@ -144,7 +144,7 @@ Artist::createRelease(const Database::Artist::pointer& artist, const Release::po
|
||||
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
||||
cover->setStyleClass("Lms-cover-medium");
|
||||
cover->setStyleClass("Lms-cover");
|
||||
anchor->setImage(std::move(cover));
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace UserInterface::ReleaseListHelpers
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 64));
|
||||
cover->setStyleClass("Lms-cover-smaller");
|
||||
cover->setStyleClass("Lms-cover");
|
||||
anchor->setImage(std::move(cover));
|
||||
|
||||
auto artists = release->getReleaseArtists();
|
||||
|
||||
@@ -169,7 +169,7 @@ Release::refreshView()
|
||||
|
||||
// Expect to be call in asc order
|
||||
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
|
||||
auto getOrAddDiscContainer = [&](std::size_t discNumber) -> Wt::WContainerWidget*
|
||||
auto getOrAddDiscContainer = [&](std::size_t discNumber, const std::string& discSubtitle) -> Wt::WContainerWidget*
|
||||
{
|
||||
{
|
||||
auto it = trackContainers.find(discNumber);
|
||||
@@ -178,9 +178,13 @@ Release::refreshView()
|
||||
}
|
||||
|
||||
Wt::WTemplate* disc {rootContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.disc-entry"))};
|
||||
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
|
||||
disc->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
if (discSubtitle.empty())
|
||||
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
|
||||
else
|
||||
disc->bindString("disc-title", Wt::WString::fromUTF8(discSubtitle), Wt::TextFormat::Plain);
|
||||
|
||||
Wt::WContainerWidget* tracksContainer {disc->bindNew<Wt::WContainerWidget>("tracks")};
|
||||
trackContainers[discNumber] = tracksContainer;
|
||||
|
||||
@@ -198,7 +202,7 @@ Release::refreshView()
|
||||
|
||||
Wt::WContainerWidget* container {rootContainer};
|
||||
if (isReleaseMultiDisc && discNumber)
|
||||
container = getOrAddDiscContainer(*discNumber);
|
||||
container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle());
|
||||
|
||||
Wt::WTemplate* entry {container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry"))};
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ Releases::createEntry(const Release::pointer& release)
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
||||
cover->setStyleClass("Lms-cover-medium");
|
||||
cover->setStyleClass("Lms-cover");
|
||||
anchor->setImage(std::move(cover));
|
||||
|
||||
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace UserInterface
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
cover->setStyleClass("Lms-cover");
|
||||
anchor->setImage(std::move(cover));
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ namespace UserInterface
|
||||
{
|
||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
cover->setStyleClass("Lms-cover");
|
||||
}
|
||||
|
||||
return entry;
|
||||
|
||||
@@ -229,7 +229,7 @@ Tracks::createEntry(const Track::pointer& track)
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
cover->setStyleClass("Lms-cover");
|
||||
anchor->setImage(std::move(cover));
|
||||
}
|
||||
}
|
||||
@@ -237,7 +237,7 @@ Tracks::createEntry(const Track::pointer& track)
|
||||
{
|
||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
cover->setStyleClass("Lms-cover");
|
||||
}
|
||||
|
||||
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||
|
||||
@@ -110,6 +110,9 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
||||
if (track->discNumber)
|
||||
std::cout << "Disc: " << *track->discNumber << std::endl;
|
||||
|
||||
if (!track->discSubtitle.empty())
|
||||
std::cout << "Disc Subtitle: " << track->discSubtitle << std::endl;
|
||||
|
||||
if (track->totalDisc)
|
||||
std::cout << "TotalDisc: " << *track->totalDisc << std::endl;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user