Merge branch 'develop' for release v3.29.0
This commit is contained in:
@@ -17,7 +17,7 @@ extraction:
|
||||
- libtag1-dev
|
||||
- libpam0g-dev
|
||||
after_prepare:
|
||||
- export WT_VERSION=4.5.0
|
||||
- export WT_VERSION=4.7.2
|
||||
- export WT_INSTALL_PREFIX=${LGTM_WORKSPACE}/wt-${WT_VERSION}
|
||||
- pushd ${LGTM_WORKSPACE}
|
||||
- git clone https://github.com/emweb/wt.git ${LGTM_WORKSPACE}/wt
|
||||
@@ -28,7 +28,7 @@ extraction:
|
||||
- popd
|
||||
configure:
|
||||
command:
|
||||
- export WT_VERSION=4.5.0
|
||||
- export WT_VERSION=4.7.2
|
||||
- export WT_INSTALL_PREFIX=${LGTM_WORKSPACE}/wt-${WT_VERSION}
|
||||
- cmake -DCMAKE_PREFIX_PATH=${WT_INSTALL_PREFIX} -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
arch:
|
||||
- amd64
|
||||
- arm64
|
||||
language: cpp
|
||||
os: linux
|
||||
dist: focal
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- build-essential
|
||||
- cmake
|
||||
- libboost-all-dev
|
||||
- libconfig++-dev
|
||||
- libavcodec-dev
|
||||
- libavutil-dev
|
||||
- libavformat-dev
|
||||
- ffmpeg
|
||||
- libstb-dev
|
||||
- libtag1-dev
|
||||
- libpam0g-dev
|
||||
- libgraphicsmagick++1-dev
|
||||
- libgtest-dev
|
||||
compiler:
|
||||
- clang
|
||||
- gcc
|
||||
before_install:
|
||||
- |
|
||||
if [ ! -d ${HOME}/wt-${WT_VERSION}/include ]; then
|
||||
git clone https://github.com/emweb/wt.git wt
|
||||
cd wt
|
||||
git checkout ${WT_VERSION}
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${WT_INSTALL_PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF
|
||||
make install
|
||||
cd ..
|
||||
fi
|
||||
script: mkdir build && cd build && cmake -DCMAKE_PREFIX_PATH=${WT_INSTALL_PREFIX} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DUSE_PAM=${USE_PAM} -DIMAGE_LIBRARY=${IMAGE_LIBRARY} .. && make && make test
|
||||
cache:
|
||||
directories:
|
||||
- ${HOME}/wt-${WT_VERSION}
|
||||
env:
|
||||
global:
|
||||
- MAKEFLAGS="-j 2"
|
||||
- WT_VERSION=4.5.0
|
||||
- WT_INSTALL_PREFIX=${HOME}/wt-${WT_VERSION}
|
||||
jobs:
|
||||
- BUILD_TYPE=Release USE_PAM=ON IMAGE_LIBRARY=STB
|
||||
- BUILD_TYPE=Release USE_PAM=OFF IMAGE_LIBRARY=STB
|
||||
- BUILD_TYPE=Release USE_PAM=OFF IMAGE_LIBRARY=GraphicsMagick++
|
||||
- BUILD_TYPE=Debug USE_PAM=ON IMAGE_LIBRARY=STB
|
||||
|
||||
# clang7 is not enough for full c++17 support
|
||||
jobs:
|
||||
exclude:
|
||||
- compiler: clang
|
||||
arch: amd64
|
||||
+1
-1
@@ -5,7 +5,7 @@ project(lms)
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
|
||||
if (UNIX)
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--no-undefined")
|
||||
endif ()
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
|
||||
FROM --platform=$BUILDPLATFORM alpine:3.14
|
||||
FROM --platform=$BUILDPLATFORM alpine:3.16
|
||||
|
||||
ARG BUILD_PACKAGES="\
|
||||
clang \
|
||||
@@ -30,7 +30,7 @@ RUN xx-apk add --no-scripts --no-cache ${LMS_BUILD_PACKAGES}
|
||||
ARG PREFIX="/tmp/install"
|
||||
|
||||
# STB
|
||||
ARG STB_VERSION=b42009b3b9d4ca35bc703f5310eedc74f584be58
|
||||
ARG STB_VERSION=af1a5bc352164740c1cc1354942b1c6b72eacb8a
|
||||
RUN \
|
||||
DIR=/tmp/stb && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://github.com/nothings/stb/archive/${STB_VERSION}.tar.gz && \
|
||||
@@ -47,4 +47,4 @@ RUN \
|
||||
PKG_CONFIG_PATH=/$(xx-info)/usr/lib/pkgconfig cmake /tmp/lms/ -DCMAKE_INCLUDE_PATH=${PREFIX}/include -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} $(xx-clang --print-cmake-defines) -DCMAKE_PREFIX_PATH=/$(xx-info)/usr/lib/cmake -DBUILD_TESTING=${BUILD_TESTS} && \
|
||||
VERBOSE=1 make -j$(nproc) && \
|
||||
xx-verify src/lms/lms && \
|
||||
xx-info is-cross || make test
|
||||
(xx-info is-cross || make test)
|
||||
|
||||
+8
-3
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.14 AS build
|
||||
FROM alpine:3.16 AS build
|
||||
|
||||
WORKDIR /tmp/workdir
|
||||
|
||||
@@ -77,7 +77,7 @@ RUN \
|
||||
make distclean
|
||||
|
||||
# WT
|
||||
ARG WT_VERSION=4.5.0
|
||||
ARG WT_VERSION=4.7.2
|
||||
RUN \
|
||||
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://github.com/emweb/wt/archive/${WT_VERSION}.tar.gz && \
|
||||
@@ -126,8 +126,13 @@ RUN \
|
||||
rm -rf /tmp/fakeroot/share/doc && \
|
||||
rm -rf /tmp/fakeroot/share/man
|
||||
|
||||
# Remove useless stuff
|
||||
RUN \
|
||||
rm -rf /tmp/fakeroot/share/Wt/resources/jPlayer \
|
||||
rm -rf /tmp/fakeroot/share/Wt/resources/themes
|
||||
|
||||
## Release Stage
|
||||
FROM alpine:3.14 AS release
|
||||
FROM alpine:3.16 AS release
|
||||
LABEL maintainer="Emeric Poupon <itmfr@yahoo.fr>"
|
||||
|
||||
ARG RUNTIME_PACKAGES=" \
|
||||
|
||||
+16
-45
@@ -1,6 +1,6 @@
|
||||
- [Installation](#installation)
|
||||
* [Docker](#docker)
|
||||
* [Debian Buster packages](#debian-buster-packages)
|
||||
* [Debian Bullseye packages](#debian-bullseye-packages)
|
||||
* [From source](#from-source)
|
||||
+ [Build dependencies](#build-dependencies)
|
||||
+ [Build](#build)
|
||||
@@ -12,33 +12,25 @@
|
||||
* [Deploy on non root path](#deploy-on-non-root-path)
|
||||
* [Reverse proxy settings](#reverse-proxy-settings)
|
||||
- [Run](#run)
|
||||
|
||||
# Installation
|
||||
|
||||
## Docker
|
||||
_Docker_ images are available, please see detailed instructions on https://hub.docker.com/r/epoupon/lms.
|
||||
|
||||
## Debian Buster packages
|
||||
_Buster_ packages are provided for _amd64_ and _armhf_ architectures.
|
||||
|
||||
## Debian Bullseye packages
|
||||
_Bullseye_ packages are provided for _amd64_ and _armhf_ architectures.
|
||||
As root, trust the following debian package provider and add it in your list of repositories:
|
||||
```sh
|
||||
wget -O - https://debian.poupon.dev/apt/debian/epoupon.gpg.key | apt-key add -
|
||||
echo "deb https://debian.poupon.dev/apt/debian buster main" > /etc/apt/sources.list.d/epoupon.list
|
||||
echo "deb https://debian.poupon.dev/apt/debian bullseye main" > /etc/apt/sources.list.d/epoupon.list
|
||||
```
|
||||
|
||||
To install or upgrade _LMS_:
|
||||
```sh
|
||||
apt update
|
||||
apt install lms
|
||||
```
|
||||
|
||||
The _lms_ service is started just after the package installation, run by a dedicated _lms_ system user.</br>
|
||||
Please refer to [Deployment](#deployment) for further configuration options.
|
||||
|
||||
## From source
|
||||
__Note__: this installation process and the default values of the configuration files have been written for _Debian Buster_. Therefore, you may have to adapt commands and/or paths in order to fit to your distribution.
|
||||
|
||||
__Note__: this installation process and the default values of the configuration files have been written for _Debian Bullseye_. Therefore, you may have to adapt commands and/or paths in order to fit to your distribution.
|
||||
### Build dependencies
|
||||
__Notes__:
|
||||
* a C++17 compiler is needed
|
||||
@@ -49,12 +41,9 @@ apt-get install g++ cmake libboost-program-options-dev libboost-system-dev libav
|
||||
__Notes__:
|
||||
* libpam0g-dev is optional (only for using PAM authentication)
|
||||
* libstb-dev can be replaced by libgraphicsmagick++1-dev (the latter will likely use more RAM)
|
||||
|
||||
You also need _Wt4_, which is not packaged yet on _Debian_. See [installation instructions](https://www.webtoolkit.eu/wt/doc/reference/html/InstallationUnix.html).</br>
|
||||
No optional requirement is needed, except openSSL if you plan not to deploy behind a reverse proxy (which is not recommended).
|
||||
|
||||
### Build
|
||||
|
||||
Get the latest stable release and build it:
|
||||
```sh
|
||||
git clone https://github.com/epoupon/lms.git lms
|
||||
@@ -66,80 +55,69 @@ cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
__Notes__:
|
||||
* you can customize the installation directory using `-DCMAKE_INSTALL_PREFIX=path` (defaults to `/usr/local`).
|
||||
* you can customize the image library using `-DIMAGE_LIBRARY=<STB|GraphicsMagick++>`
|
||||
|
||||
```sh
|
||||
make
|
||||
```
|
||||
__Note__: you can use `make -jN` to speed up compilation time (N is the number of compilation workers to spawn).
|
||||
|
||||
### Installation
|
||||
|
||||
__Note__: the commands of this section require root privileges.
|
||||
|
||||
```sh
|
||||
make install
|
||||
```
|
||||
|
||||
Create a dedicated system user:
|
||||
```sh
|
||||
useradd --system --group lms
|
||||
```
|
||||
|
||||
Copy the configuration files:
|
||||
```sh
|
||||
cp /usr/share/lms/lms.conf /etc/lms.conf
|
||||
cp /usr/share/lms/lms.service /lib/systemd/system/lms.service
|
||||
```
|
||||
|
||||
Create the working directory and give it access to the _lms_ user:
|
||||
```sh
|
||||
mkdir /var/lms
|
||||
chown lms:lms /var/lms
|
||||
```
|
||||
|
||||
To make _LMS_ run automatically during startup:
|
||||
```sh
|
||||
systemctl enable lms
|
||||
```
|
||||
|
||||
### Upgrade
|
||||
|
||||
To upgrade _LMS_ from sources, you need to update the master branch and rebuild/install it:
|
||||
```sh
|
||||
cd build
|
||||
git pull
|
||||
make
|
||||
```
|
||||
|
||||
Then using root privileges:
|
||||
```sh
|
||||
make install
|
||||
systemctl restart lms
|
||||
```
|
||||
|
||||
# Deployment
|
||||
|
||||
__Note__: don't forget to give the _lms_ user read access to the music directory you want to scan.
|
||||
|
||||
## Configuration
|
||||
_LMS_ uses a configuration file, installed by default in `/etc/lms.conf`. It is recommended to edit this file and change relevant settings (listen address, listen port, working directory, Subsonic API activation, deployment path, ...).
|
||||
|
||||
All other settings are set using the web interface (user management, scan settings, transcode settings, ...).
|
||||
|
||||
If a setting is not present in the configuration file, a hardcoded default value is used (the same as in the [default configuration file](conf/lms.conf))
|
||||
|
||||
## Authentication backend
|
||||
|
||||
You can define which authentication backend to be used thanks to the `authentication-backend` option:
|
||||
* `internal` (default): _LMS_ uses an internal database to store users and their associated passwords (salted and hashed using [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt)). Only the admin user can create, edit or remove other users.
|
||||
* `PAM`: the user/password authentication request is forwarded to PAM (see the default [PAM configuration file](conf/pam/lms) provided).
|
||||
* `http-headers`: _LMS_ uses a configurable HTTP header field, typically set by a reverse proxy to handle [SSO](https://en.wikipedia.org/wiki/Single_sign-on), to extract the login name. You can customize the field to be used using the `http-headers-login-field` option.
|
||||
|
||||
__Note__: the first created user is the admin user
|
||||
|
||||
### `internal` backend: reset admin password
|
||||
Open the the database file located in `/var/lms/lms.db` using `sqlite3`:
|
||||
```sh
|
||||
sqlite3 /var/lms/lms.db
|
||||
```
|
||||
Now force reset the password of the admin user to `adminadmin`:
|
||||
```sqlite3
|
||||
UPDATE user SET password_salt="sliIgHUpEoAlBMquivH4VzKXbSzexlvS", password_hash="$2y$07$a0vnQUbGTV/DZyDqOizvbOdCaytNjDhkOPcGoBZup4V0MRKDzOpEG" WHERE id=1;
|
||||
.quit
|
||||
```
|
||||
## Deploy on non root path
|
||||
If you want to deploy on non root path (e.g. https://mydomain.com/newroot/), you have to set the `deploy-path` option accordingly in `lms.conf`.
|
||||
|
||||
As static resources are __not__ related to the `deploy-path` option, you have to perform the following steps if you want them to be on a non root path too:
|
||||
* Create a new intermediary `newroot` directory in `/usr/share/lms/docroot` and move everything in it.
|
||||
* Symlink `/usr/share/lms/docroot/newroot/resources` to `/usr/share/Wt/resources`.
|
||||
@@ -149,12 +127,9 @@ wt-resources = "" # do not comment the whole line
|
||||
docroot = "/usr/share/lms/docroot/;/newroot/resources,/newroot/css,/newroot/images,/newroot/js,/newroot/favicon.ico";`
|
||||
deploy-path = "/newroot/"; # ending slash is important
|
||||
```
|
||||
|
||||
If you use nginx as a reverse proxy, you can simply replace `location /` with `location /newroot/` to achieve the same result.
|
||||
|
||||
## Reverse proxy settings
|
||||
_LMS_ is shipped with an embedded web server, but it is recommended to deploy behind a reverse proxy. You have to set the _behind-reverse-proxy_ option to _true_ in the `lms.conf` configuration file.
|
||||
|
||||
Here is an example to make _LMS_ properly work on _myserver.org_ using _nginx_:
|
||||
```
|
||||
server {
|
||||
@@ -183,16 +158,12 @@ server {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Run
|
||||
```sh
|
||||
systemctl start lms
|
||||
```
|
||||
|
||||
Log traces can be accessed using journactl:
|
||||
```sh
|
||||
journalctl -u lms.service
|
||||
```
|
||||
|
||||
To connect to _LMS_, just open your favorite browser and go to http://localhost:5082
|
||||
|
||||
To connect to _LMS_, just open your favorite browser and go to `http://localhost:5082`
|
||||
|
||||
+52
-69
@@ -4,77 +4,60 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Admin.Database.template">
|
||||
<div class ="row">
|
||||
<div class="col-lg-8">
|
||||
<form>
|
||||
<legend>${tr:Lms.Admin.Database.database}</legend>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:media-directory}">
|
||||
${tr:Lms.Admin.Database.path}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${media-directory}
|
||||
${media-directory-info class="help-block"}
|
||||
<span class="help-block">${tr:Lms.Admin.Database.path-help}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:update-period}">
|
||||
${tr:Lms.Admin.Database.update-period}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${update-period}
|
||||
${update-period-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:update-start-time}">
|
||||
${tr:Lms.Admin.Database.update-start-time}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${update-start-time}
|
||||
${update-start-time-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<legend>${tr:Lms.Admin.Database.scan-options}</legend>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:recommendation-engine-type}">
|
||||
${tr:Lms.Admin.Database.recommendation-engine-type}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${recommendation-engine-type}
|
||||
${recommendation-engine-type-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:tags}">
|
||||
${tr:Lms.Admin.Database.tags}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${tags}
|
||||
${tags-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
${apply-btn class="btn-primary"} ${discard-btn}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form class="row g-3">
|
||||
<legend>${tr:Lms.Admin.Database.database}</legend>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:media-directory}">
|
||||
${tr:Lms.Admin.Database.path}
|
||||
</label>
|
||||
${media-directory class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${media-directory-info}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:update-period}">
|
||||
${tr:Lms.Admin.Database.update-period}
|
||||
</label>
|
||||
${update-period class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${update-period-info"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:update-start-time}">
|
||||
${tr:Lms.Admin.Database.update-start-time}
|
||||
</label>
|
||||
${update-start-time class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${update-start-time-info}
|
||||
</div>
|
||||
</div>
|
||||
<legend>${tr:Lms.Admin.Database.scan-options}</legend>
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:recommendation-engine-type}">
|
||||
${tr:Lms.Admin.Database.recommendation-engine-type}
|
||||
</label>
|
||||
${recommendation-engine-type class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${recommendation-engine-type-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:tags}">
|
||||
${tr:Lms.Admin.Database.tags}
|
||||
</label>
|
||||
${tags class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${tags-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
${apply-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
</form>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
${scanner-controller}
|
||||
</div>
|
||||
</div>
|
||||
${scanner-controller}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -4,49 +4,44 @@
|
||||
|
||||
<message id="Lms.Admin.InitWizard.template">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="page-header">
|
||||
<form class="row mt-3 justify-content-md-center g-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="row g-3">
|
||||
<h2>${tr:Lms.Admin.InitWizard.header}</h2>
|
||||
</div>
|
||||
<form>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:admin-login}">
|
||||
${tr:Lms.login}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${admin-login}
|
||||
${admin-login-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:password}">
|
||||
${tr:Lms.password}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${password}
|
||||
${password-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:password-confirm}">
|
||||
${tr:Lms.password-confirm}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${password-confirm}
|
||||
${password-confirm-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
${create-btn class="btn-primary"}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:admin-login}">
|
||||
${tr:Lms.login}
|
||||
</label>
|
||||
${admin-login class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${admin-login-info}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:password}">
|
||||
${tr:Lms.password}
|
||||
</label>
|
||||
${password class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${password-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:password-confirm}">
|
||||
${tr:Lms.password-confirm}
|
||||
</label>
|
||||
${password-confirm class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${password-confirm-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
${create-btn class="btn btn-primary"}
|
||||
</div>
|
||||
${info class="alert alert-success mt-3"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
|
||||
@@ -2,42 +2,44 @@
|
||||
<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 class="card">
|
||||
<h5 class="card-header">${tr:Lms.Admin.ScannerController.scanner}</h5>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:status}">
|
||||
${tr:Lms.Admin.ScannerController.status}
|
||||
</label>
|
||||
${status class="form-control"}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:step-status}">
|
||||
${tr:Lms.Admin.ScannerController.step-status}
|
||||
</label>
|
||||
${step-status class="form-control"}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="{id:last-scan}">
|
||||
${tr:Lms.Admin.ScannerController.last-scan}
|
||||
</label>
|
||||
<div class="input-group">
|
||||
${last-scan class="form-control"}
|
||||
${report-btn class="btn btn-outline-info"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="btn-group">
|
||||
${scan-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${full-scan-btn class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
+46
-55
@@ -2,64 +2,55 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Admin.User.template">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<legend>${title}</legend>
|
||||
<form>
|
||||
<div class="form-horizontal">
|
||||
${<if-has-last-login>}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="{id:last-login}">
|
||||
${tr:Lms.Admin.User.last-login}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
<div class="well well-sm">
|
||||
${last-login}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-last-login>}
|
||||
${<if-has-login>}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="${id:login}">
|
||||
${tr:Lms.login}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${login}
|
||||
${login-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-login>}
|
||||
${<if-has-password>}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="${id:password}">
|
||||
${tr:Lms.password-new}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${password}
|
||||
${password-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-password>}
|
||||
${<if-demo>}
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-3" for="${id:demo}">
|
||||
<div class="checkbox">
|
||||
<label>${demo}${tr:Lms.Admin.User.demo-account}</label>
|
||||
${demo-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${</if-demo>}
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
${save-btn class="btn-primary"}
|
||||
</div>
|
||||
<form class="row g-3">
|
||||
<legend>${title}</legend>
|
||||
${<if-has-last-login>}
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="{id:last-login}">
|
||||
${tr:Lms.Admin.User.last-login}
|
||||
</label>
|
||||
<input type="text" readonly="readonly" class="form-control" id="status" value="${last-login}"/>
|
||||
</div>
|
||||
${</if-has-last-login>}
|
||||
${<if-has-login>}
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:login}">
|
||||
${tr:Lms.login}
|
||||
</label>
|
||||
${login class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${login-info}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-login>}
|
||||
${<if-has-password>}
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:password}">
|
||||
${tr:Lms.password-new}
|
||||
</label>
|
||||
${password class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${password-info}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-password>}
|
||||
${<if-demo>}
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
${demo class="form-check-input"}
|
||||
<label class="form-check-label" for="${id:demo}">
|
||||
${tr:Lms.Admin.User.demo-account}
|
||||
</label>
|
||||
<div class="invalid-feedback">
|
||||
${demo-info}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
${</if-demo>}
|
||||
<div class="col-12">
|
||||
${save-btn class="btn btn-primary"}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
+27
-10
@@ -2,22 +2,39 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Admin.Users.template">
|
||||
<div class="Lms-header">
|
||||
<h2>${tr:Lms.Admin.Users.users}</h2>
|
||||
</div>
|
||||
${users}
|
||||
<h3>${tr:Lms.Admin.Users.users}</h3>
|
||||
${users class="d-grid gap-1 mb-3"}
|
||||
${<if-can-create-user>}
|
||||
${add-btn class="btn-primary Lms-admin-users-add-btn"}
|
||||
${add-btn class="btn btn-primary"}
|
||||
${</if-can-create-user>}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Admin.Users.template.entry">
|
||||
<div class="row Lms-admin-users-entry Lms-vertical-align">
|
||||
<div class="col-md-5 col-xs-8 Lms-vertical-align">
|
||||
${name}${<if-tag>}<span class="label label-info Lms-admin-users-entry-role">${tag}</span>${</if-tag>}
|
||||
<div class="d-flex align-items-center Lms-bg-dark-hover rounded">
|
||||
<div class="p-2 flex-fill">
|
||||
${name}${<if-tag>}<span class="badge bg-info ms-2">${tag}</span>${</if-tag>}
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-4 Lms-horizontal-center">
|
||||
${<if-edit>}${edit-btn class="btn-warning btn-sm Lms-admin-users-entry-btn"}${del-btn class="btn-danger btn-sm Lms-admin-users-entry-btn"}${</if-edit>}
|
||||
<div class="p-2">
|
||||
${<if-edit>}${edit-btn class="btn btn-sm btn-secondary me-1"}${del-btn class="btn btn-sm btn-danger"}${</if-edit>}
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Admin.Users.template.delete-user">
|
||||
<div class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${tr:Lms.Admin.Users.del-user}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${tr:Lms.Admin.Users.del-user-name}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
${del-btn class="btn btn-danger me-1"}${cancel-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
+23
-44
@@ -3,56 +3,35 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Artist.template">
|
||||
<div class="Lms-header">
|
||||
<h2><div class="Lms-artistname text-primary">${name}</div></h2>
|
||||
${clusters}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
<div class="mb-3">
|
||||
<h3>${name}</h3>
|
||||
${clusters class="mb-2"}
|
||||
<div class="btn-group">
|
||||
${play-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${play-shuffled class="btn dropdown-item"}</li>
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${star class="btn dropdown-item"}</li>
|
||||
${<if-has-mbid>}
|
||||
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-artist}</a></li>
|
||||
${</if-has-mbid>}
|
||||
<li>${download class="btn dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
${<if-has-release>}
|
||||
<div class="row">
|
||||
${releases class="Lms-explore-artist-entries-container"}
|
||||
</div>
|
||||
${releases class="mb-3"}
|
||||
${</if-has-release>}
|
||||
${<if-has-non-release-track>}
|
||||
<div class="Lms-explore-artist-tracks-container">
|
||||
${tracks}
|
||||
</div>
|
||||
${tracks class="mb-3"}
|
||||
${</if-has-non-release-track>}
|
||||
<div class="row">
|
||||
${<if-has-similar-artists>}
|
||||
<div class="col-md-4">
|
||||
<div class="well well-sm">
|
||||
<h3>${tr:Lms.Explore.Artist.similar-artists}</h3>
|
||||
${similar-artists}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-similar-artists>}
|
||||
${<if-has-mbid>}
|
||||
<div class="col-md-4">
|
||||
<div class="well well-sm">
|
||||
<h3>${tr:Lms.Explore.links}</h3>
|
||||
${mbid-link class="Lms-btn text-muted"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-mbid>}
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Artist.template.entry">
|
||||
<div class="Lms-explore-entry-list">
|
||||
<div class="row Lms-vertical-align">
|
||||
<div class="col-xs-4 col-sm-3 col-md-2">
|
||||
<div class="Lms-responsive-square Lms-cover-container">
|
||||
${cover}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-8 col-sm-9 col-md-10 Lms-explore-list-entry-text-container">
|
||||
<h5>${name class="Lms-releasename text-primary"}</h5>
|
||||
${<if-has-artist>}<h6>${artist class="Lms-btn Lms-artistname text-muted"}</h6>${</if-has-artist>}
|
||||
<h6><small><div class="text-muted">${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</div></small></h6>
|
||||
</div>
|
||||
</div>
|
||||
${<if-has-similar-artists>}
|
||||
<div>
|
||||
<h3>${tr:Lms.Explore.Artist.similar-artists}</h3>
|
||||
${similar-artists class="d-grid gap-3"}
|
||||
</div>
|
||||
${</if-has-similar-artists>}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
+27
-11
@@ -3,11 +3,30 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Artists.template">
|
||||
<div class="Lms-header">
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
<div class="mb-3">
|
||||
<ul class="nav nav-pills mb-2">
|
||||
<li class="nav-item">
|
||||
${random class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${starred class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${most-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-added class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${all class="nav-link"}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
${link-type}
|
||||
<div class="col-lg-3">
|
||||
${link-type class="form-select"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,15 +34,12 @@
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Artists.template.entry">
|
||||
<div class="Lms-explore-artists-entry Lms-entry-hoverable Lms-vertical-align">
|
||||
<h4>${name class="Lms-btn Lms-artistname text-primary"}</h4>
|
||||
</div>
|
||||
${name class="text-decoration-none link-secondary"}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Artists.template.entry-small">
|
||||
<div class="Lms-explore-artists-entry-small Lms-entry-hoverable Lms-vertical-align">
|
||||
${name class="Lms-btn Lms-artistname text-primary"}
|
||||
</div>
|
||||
<message id="Lms.Explore.Artists.template.container">
|
||||
${elements class="d-grid gap-3"}
|
||||
${loading-indicator}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
+30
-24
@@ -2,37 +2,43 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Explore.template">
|
||||
${contents class="Lms-explore-contents"}
|
||||
${contents}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.template.more-btn"><i class="fa fa-fw fa-plus-circle"></i></message>
|
||||
<message id="Lms.Explore.template.play-btn"><i class="fa fa-fw fa-play-circle"></i></message>
|
||||
<message id="Lms.Explore.template.more-btn"><i class="fa fa-fw fa-ellipsis-v"></i></message>
|
||||
<message id="Lms.Explore.template.play-btn"><i class="fa fa-fw fa-play"></i></message>
|
||||
|
||||
<message id="Lms.Explore.template.filters">
|
||||
${add-filter class="btn-sm btn-info"} ${clusters class="Lms-explore-clusters"}
|
||||
${add-filter class="btn btn-sm btn-outline-primary me-2"}${clusters class="d-inline-block align-items-center"}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.template.add-filter">
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:type}">
|
||||
${tr:Lms.Explore.type}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
${type}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:value}">
|
||||
${tr:Lms.Explore.value}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
${value}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
${add-btn class="btn-primary"} ${cancel-btn}
|
||||
<div class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${tr:Lms.Explore.add-filter}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:type}">
|
||||
${tr:Lms.Explore.type}
|
||||
</label>
|
||||
${type class="form-control"}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:value}">
|
||||
${tr:Lms.Explore.value}
|
||||
</label>
|
||||
${value class="form-control"}
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-footer">
|
||||
${add-btn class="btn btn-primary me-1"}${cancel-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+29
-37
@@ -4,46 +4,38 @@
|
||||
|
||||
<message id="Lms.Auth.template">
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h2>${tr:Lms.Auth.welcome}</h2>
|
||||
</div>
|
||||
<form>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:login-name}">
|
||||
${tr:Lms.login}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${login-name}
|
||||
<form class="row mt-3 justify-content-md-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="row g-3">
|
||||
<h2>${tr:Lms.Auth.welcome}</h2>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:login-name}">
|
||||
${tr:Lms.login}
|
||||
</label>
|
||||
${login-name class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${login-name-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-block col-sm-5">
|
||||
${login-name-info}
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:password}">
|
||||
${tr:Lms.password}
|
||||
</label>
|
||||
${password class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${password-info}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:password}">
|
||||
${tr:Lms.password}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${password}
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
${remember-me class="form-check-input"}
|
||||
<label class="form-check-label" for="${id:remember-me}">
|
||||
${tr:Lms.Auth.remember-me}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-block col-sm-5">
|
||||
${password-info}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:remember-me}">
|
||||
${tr:Lms.Auth.remember-me}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<div class="checkbox"><label>${remember-me}</label></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
${login-btn class="btn-primary"}
|
||||
<div class="col-12">
|
||||
${login-btn class="btn btn-primary"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.main.template">
|
||||
${navbar}
|
||||
<div class="container">
|
||||
${contents}
|
||||
${notifications class="toast-container Lms-notification-container p-3"}
|
||||
${modal}
|
||||
</div>
|
||||
${player class="fixed-bottom bg-dark Lms-player"}
|
||||
</message>
|
||||
|
||||
<message id="Lms.main.template.navbar">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-3">
|
||||
<div class="container">
|
||||
<form class="d-flex">
|
||||
${search class="form-control" type="search"}
|
||||
</form>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto align-items-md-center mb-lg-0">
|
||||
<li class="nav-item">
|
||||
${releases class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${artists class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${tracks class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${playqueue class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${filters class="nav-link d-flex align-items-center"}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="navbar-nav align-items-md-center mb-lg-0">
|
||||
${<if-is-admin>}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="${tr:Lms.administration}">
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<li>${database class="dropdown-item"}</li>
|
||||
<li>${users class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</li>
|
||||
${</if-is-admin>}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="${tr:Lms.user}">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<li class="dropdown-header">${username}</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
<li>${settings class="dropdown-item"}</li>
|
||||
<li>${logout class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
+22
-23
@@ -6,36 +6,35 @@
|
||||
<audio id="lms-mp-audio">
|
||||
</audio>
|
||||
<div class="container">
|
||||
<div class="Lms-player-seek-container">
|
||||
<div class="p-1 my-1 position-relative">
|
||||
<input id="lms-mp-seek" class="Lms-player-seek-common Lms-player-seek" type="range" min="0" max="100" step="1" value="0"/>
|
||||
<div class="progress Lms-player-seek-common Lms-player-progress">
|
||||
<div id="lms-mp-progress" class="progress-bar" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
|
||||
</div>
|
||||
<div class="progress Lms-player-seek-common bg-secondary rounded">
|
||||
<div id="lms-mp-progress" class="progress-bar" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-5 col-sm-3 col-md-2 Lms-player-controls">
|
||||
<i id="lms-mp-previous" class="fa fa-fw fa-step-backward Lms-player-btn Lms-btn Lms-player-btn-controls"></i><i id="lms-mp-playpause" class="fa fa-fw fa-play Lms-player-btn Lms-btn Lms-player-btn-controls"></i><i id="lms-mp-next" class="fa fa-fw fa-step-forward Lms-player-btn Lms-btn Lms-player-btn-controls"></i>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-inline-flex me-2">
|
||||
<button id="lms-mp-previous" class="btn btn-sm btn-outline-primary border-0">
|
||||
<i class="fa fa-fw fa-step-backward"/>
|
||||
</button>
|
||||
<button id="lms-mp-playpause" class="btn btn-outline-primary border-0">
|
||||
<i class="fa fa-fw fa-play"/>
|
||||
</button>
|
||||
<button id="lms-mp-next" class="btn btn-sm btn-outline-primary border-0">
|
||||
<i class="fa fa-fw fa-step-forward"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden-xs hidden-sm col-md-2">
|
||||
<div class="Lms-player-volume">
|
||||
<i id="lms-mp-volume" class="fa fa-fw fa-volume-up Lms-player-btn Lms-btn Lms-player-btn-volume"></i>
|
||||
<span class="Lms-player-volume-slider-container">
|
||||
<input id ="lms-mp-volume-slider" type="range" min="0" max="1" step="0.01" value="0.8" class="Lms-player-volume-slider"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-fill text-center text-truncate">
|
||||
${title class="d-block text-truncate"}
|
||||
${release class="text-decoration-none text-truncate link-success"}
|
||||
</div>
|
||||
<div class="col-xs-7 col-sm-7 col-md-6">
|
||||
<div class="Lms-player-text-center">
|
||||
${title class="Lms-player-text Lms-trackname text-primary"}
|
||||
</div>
|
||||
<div class="Lms-player-text-center">
|
||||
${release class="Lms-btn Lms-player-text Lms-releasename text-primary"}<span class="hidden-xs"><small>${artist class="Lms-btn Lms-player-text Lms-artistname text-muted"}</small></span>
|
||||
</div>
|
||||
<div class="d-none d-md-flex align-items-center Lms-player-volume-container">
|
||||
<i id="lms-mp-volume" class="fa fa-fw fa-volume-up link-primary me-1"></i>
|
||||
<input id ="lms-mp-volume-slider" class="form-range" type="range" min="0" max="1" step="0.01" value="0.8"/>
|
||||
</div>
|
||||
<div class="hidden-xs col-sm-2 col-md-2 Lms-player-duration text-muted">
|
||||
<span id="lms-transcoding-active" class="Lms-player-btn Lms-btn" data-toggle="tooltip" title="${tr:Lms.Player.transcoding-active}" style="visibility:hidden"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></span>
|
||||
<div class="d-none d-sm-block text-muted text-nowrap">
|
||||
<span id="lms-transcoding-active" title="${tr:Lms.Player.transcoding-active}" class="p-2" style="visibility:hidden"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></span>
|
||||
<span id="lms-mp-curtime">--</span> / <span id="lms-mp-duration">--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
<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-reloading-similarity-engine">Reloading similarity engine: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Scanning files: {1}/{2} files ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-status">Step status</message>
|
||||
|
||||
<!--Users-->
|
||||
<message id="Lms.Admin.Users.add">New user</message>
|
||||
@@ -99,7 +100,7 @@
|
||||
<message id="Lms.Admin.User.user-already-exists">User already exists!</message>
|
||||
<message id="Lms.Admin.User.user-create">New user</message>
|
||||
<message id="Lms.Admin.User.user-created">New user created!</message>
|
||||
<message id="Lms.Admin.User.user-edit">Edit user {1}</message>
|
||||
<message id="Lms.Admin.User.user-edit">Edit user '{1}'</message>
|
||||
<message id="Lms.Admin.User.user-updated">User updated!</message>
|
||||
|
||||
<!--Wizard-->
|
||||
@@ -117,10 +118,10 @@
|
||||
<message id="Lms.Explore.download">Download</message>
|
||||
<message id="Lms.Explore.filter-added">Filter added</message>
|
||||
<message id="Lms.Explore.filters">Filters</message>
|
||||
<message id="Lms.Explore.links">Links</message>
|
||||
<message id="Lms.Explore.most-played">Most played</message>
|
||||
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
|
||||
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
|
||||
<message id="Lms.Explore.play">Play</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>
|
||||
@@ -177,11 +178,9 @@
|
||||
|
||||
|
||||
<!--Settings-->
|
||||
<message id="Lms.Settings.appearance">Appearance</message>
|
||||
<message id="Lms.Settings.audio">Audio</message>
|
||||
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message>
|
||||
<message id="Lms.Settings.change-password">Change password</message>
|
||||
<message id="Lms.Settings.dark-mode">Dark mode</message>
|
||||
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
|
||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
|
||||
<message id="Lms.Settings.password-bad">Bad password</message>
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
<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-reloading-similarity-engine">Rechargement du moteur de recommandation : {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Scan des fichiers : {1}/{2} fichiers ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message>
|
||||
|
||||
<!--Users-->
|
||||
<message id="Lms.Admin.Users.add">Ajouter</message>
|
||||
@@ -99,7 +100,7 @@
|
||||
<message id="Lms.Admin.User.user-already-exists">L'utilisateur existe déjà !</message>
|
||||
<message id="Lms.Admin.User.user-create">Nouvel utilisateur</message>
|
||||
<message id="Lms.Admin.User.user-created">Nouvel utilisateur créé !</message>
|
||||
<message id="Lms.Admin.User.user-edit">Utilisateur {1}</message>
|
||||
<message id="Lms.Admin.User.user-edit">Utilisateur '{1}'</message>
|
||||
<message id="Lms.Admin.User.user-updated">L'utilisateur a été mis à jour !</message>
|
||||
|
||||
<!--Wizard-->
|
||||
@@ -117,10 +118,10 @@
|
||||
<message id="Lms.Explore.download">Télécharger</message>
|
||||
<message id="Lms.Explore.filter-added">Filtre ajouté</message>
|
||||
<message id="Lms.Explore.filters">Filtres</message>
|
||||
<message id="Lms.Explore.links">Liens</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-release">MusicBrainz Release</message>
|
||||
<message id="Lms.Explore.play">Jouer</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>
|
||||
@@ -177,11 +178,9 @@
|
||||
|
||||
|
||||
<!--Settings-->
|
||||
<message id="Lms.Settings.appearance">Apparence</message>
|
||||
<message id="Lms.Settings.audio">Audio</message>
|
||||
<message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message>
|
||||
<message id="Lms.Settings.change-password">Changement de mot de passe</message>
|
||||
<message id="Lms.Settings.dark-mode">Mode sombre</message>
|
||||
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
|
||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
|
||||
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Ricarica motore di tracce simili: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Scansione files: {1}/{2} files ({3}%)...</message>
|
||||
|
||||
|
||||
<!--Users-->
|
||||
<message id="Lms.Admin.Users.add">Nuovo utente</message>
|
||||
<message id="Lms.Admin.Users.admin">Amministratore</message>
|
||||
@@ -116,10 +117,10 @@
|
||||
<message id="Lms.Explore.download">Download</message>
|
||||
<message id="Lms.Explore.filter-added">Filtro aggiunto</message>
|
||||
<message id="Lms.Explore.filters">Filtri</message>
|
||||
<message id="Lms.Explore.links">Collegamenti</message>
|
||||
<message id="Lms.Explore.most-played">Più ascoltati</message>
|
||||
<message id="Lms.Explore.musicbrainz-artist">Artista MusicBrainz</message>
|
||||
<message id="Lms.Explore.musicbrainz-release">Album MusicBrainz</message>
|
||||
|
||||
<message id="Lms.Explore.play-last">Aggiungi alla coda di riproduzione</message>
|
||||
<message id="Lms.Explore.play-shuffled">Aggiungi alla coda di riproduzione mischiando</message>
|
||||
<message id="Lms.Explore.random">Casuali</message>
|
||||
@@ -176,11 +177,9 @@
|
||||
|
||||
|
||||
<!--Settings-->
|
||||
<message id="Lms.Settings.appearance">Aspetto</message>
|
||||
<message id="Lms.Settings.audio">Audio</message>
|
||||
<message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message>
|
||||
<message id="Lms.Settings.change-password">Cambia password</message>
|
||||
<message id="Lms.Settings.dark-mode">Tema scuro</message>
|
||||
<message id="Lms.Settings.demo-cannot-save">Non posso salvare le impostazioni utilizzando l'account demo!</message>
|
||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
|
||||
<message id="Lms.Settings.password-bad">Password errata</message>
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 文件 ({3}%)...</message>
|
||||
|
||||
|
||||
<!--Users-->
|
||||
<message id="Lms.Admin.Users.add">新建用户</message>
|
||||
<message id="Lms.Admin.Users.admin">管理员</message>
|
||||
@@ -117,10 +118,10 @@
|
||||
<message id="Lms.Explore.download">下载</message>
|
||||
<message id="Lms.Explore.filter-added">筛选已添加</message>
|
||||
<message id="Lms.Explore.filters">筛选</message>
|
||||
<message id="Lms.Explore.links">链接</message>
|
||||
<message id="Lms.Explore.most-played">播放最多</message>
|
||||
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message>
|
||||
<message id="Lms.Explore.musicbrainz-release">MusicBrainz 专辑</message>
|
||||
|
||||
<message id="Lms.Explore.play-last">播放列表</message>
|
||||
<message id="Lms.Explore.play-shuffled">随机播放</message>
|
||||
<message id="Lms.Explore.random">随机</message>
|
||||
@@ -176,11 +177,9 @@
|
||||
<message id="Lms.PlayHistory.playhistory">播放历史</message>
|
||||
|
||||
<!--Settings-->
|
||||
<message id="Lms.Settings.appearance">外观</message>
|
||||
<message id="Lms.Settings.audio">音频</message>
|
||||
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
|
||||
<message id="Lms.Settings.change-password">更改密码</message>
|
||||
<message id="Lms.Settings.dark-mode">夜间模式</message>
|
||||
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message>
|
||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
|
||||
<message id="Lms.Settings.password-bad">无效密码</message>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.LoadingIndicator.template">
|
||||
<div class="my-5 text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">${tr:Lms.loading}</span>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.infinite-scrolling-container">
|
||||
${elements}
|
||||
${loading-indicator}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.notifications.template.entry">
|
||||
<div class="toast align-items-center ${bg-color} text-${text-color} border-0" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="${duration}">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-${text-color} me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
</messages>
|
||||
+37
-29
@@ -2,54 +2,62 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.PlayQueue.template.clear-btn"><i class="fa fa-fw fa-times-circle"></i></message>
|
||||
<message id="Lms.PlayQueue.template.clear-btn"><i class="fa fa-fw fa-times"></i></message>
|
||||
<message id="Lms.PlayQueue.template.shuffle-btn"><i class="fa fa-fw fa-random"></i></message>
|
||||
<message id="Lms.PlayQueue.template.repeat-btn"><i class="fa fa-fw fa-repeat"></i></message>
|
||||
<message id="Lms.PlayQueue.template.radio-btn"><i class="fa fa-fw fa-rss"></i></message>
|
||||
|
||||
<message id="Lms.PlayQueue.template">
|
||||
<div class="Lms-header">
|
||||
${clear-btn class="Lms-playqueue-btn Lms-btn"}${shuffle-btn class="Lms-playqueue-btn Lms-btn"}${repeat-btn class="Lms-playqueue-btn Lms-btn"}${radio-btn class="Lms-playqueue-btn Lms-btn"}
|
||||
<h4><small>${nb-tracks}</small></h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
${entries}
|
||||
<div class="mb-3">
|
||||
<div class="d-inline-block me-2" title="${tr:Lms.PlayQueue.clear}">
|
||||
${clear-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
<div class="d-inline-block me-2" title="${tr:Lms.PlayQueue.shuffle}">
|
||||
${shuffle-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
<div class="btn-group me-2">
|
||||
${repeat-btn class="btn-check"}
|
||||
<label class="btn btn-outline-info" title="${tr:Lms.PlayQueue.repeat}" for="${id:repeat-btn}">${tr:Lms.PlayQueue.template.repeat-btn}</label>
|
||||
${radio-btn class="btn-check"}
|
||||
<label class="btn btn-outline-info" title="${tr:Lms.PlayQueue.radio-mode}" for="${id:radio-btn}">${tr:Lms.PlayQueue.template.radio-btn}</label>
|
||||
</div>
|
||||
${nb-tracks}
|
||||
</div>
|
||||
${entries}
|
||||
</message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.play-btn"><i class="fa fa-fw fa-play-circle"></i></message>
|
||||
<message id="Lms.PlayQueue.template.delete-btn"><i class="fa fa-fw fa-times-circle"></i></message>
|
||||
<message id="Lms.PlayQueue.template.more-btn"><i class="fa fa-fw fa-plus-circle"></i></message>
|
||||
<message id="Lms.PlayQueue.template.play-btn"><i class="fa fa-fw fa-play"></i></message>
|
||||
<message id="Lms.PlayQueue.template.delete-btn"><i class="fa fa-fw fa-times"></i></message>
|
||||
<message id="Lms.PlayQueue.template.more-btn"><i class="fa fa-fw fa-ellipsis-v"></i></message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.entry">
|
||||
<div class="row Lms-vertical-align Lms-explore-entry-list Lms-playqueue-entry ${is-selected}">
|
||||
<div class="col-xs-3 col-sm-2 col-md-2">
|
||||
<div class="Lms-responsive-square Lms-cover-container">
|
||||
${cover}
|
||||
</div>
|
||||
<div class="d-flex align-items-center rounded Lms-bg-dark-hover mb-2">
|
||||
<div class="p-1 Lms-cover-small">
|
||||
${cover}
|
||||
</div>
|
||||
<div class="col-xs-7 col-sm-8 col-md-8 Lms-explore-entry-list-text-container">
|
||||
<div class="Lms-trackname text-primary"><h5>${name}</h5></div>
|
||||
${<if-has-artists-or-release>}
|
||||
${<if-has-release>}<small>${release class="Lms-btn Lms-releasename text-primary"}</small>${</if-has-release>}
|
||||
${<if-has-artists>}${artists}${</if-has-artists>}
|
||||
${</if-has-artists-or-release>}
|
||||
<div class="p-2 flex-fill text-truncate">
|
||||
<div class="d-block text-truncate">${name}</div>
|
||||
${<if-has-release>}${release class="text-decoration-none link-success text-truncate"}${</if-has-release>}
|
||||
</div>
|
||||
<div class="col-xs-2 col-sm-2 col-md-2">
|
||||
<div class="Lms-playqueue-entry-controls Lms-vertical-align">
|
||||
${play-btn class="Lms-playqueue-btn Lms-btn"}${del-btn class="Lms-playqueue-btn Lms-btn"}<span class="hidden-xs Lms-playqueue-btn Lms-btn">${more-btn}</span>
|
||||
</div>
|
||||
<div class="Lms-playqueue-entry-duration text-muted">
|
||||
${duration}
|
||||
<div class="p-2 d-none d-sm-block text-muted">
|
||||
${duration}
|
||||
</div>
|
||||
<div class="p-2 d-flex">
|
||||
${play-btn class="btn btn-sm btn-outline-secondary border-0"}
|
||||
${del-btn class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<div class="dropdown d-none d-sm-block">
|
||||
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
|
||||
<li>${star class="btn dropdown-item"}</li>
|
||||
<li>${download class="btn dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.entry-artist">
|
||||
<small>${artist class="Lms-btn Lms-artistname text-muted"}</small>
|
||||
${artist}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
+53
-56
@@ -7,83 +7,80 @@
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Release.template">
|
||||
<div class="row Lms-explore-release">
|
||||
<div class="col-md-4">
|
||||
${cover class="Lms-explore-release-cover"}
|
||||
<div class="row mb-3 gy-3">
|
||||
<div class="col-lg-3 col-md-4">
|
||||
${cover}
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="Lms-header">
|
||||
<h3><div class="Lms-releasename text-primary">${name}</div></h3>
|
||||
${<if-has-release-artists>}${artists}${</if-has-release-artists>}
|
||||
${<if-has-various-release-artists>}<div class="Lms-artistname text-muted">${tr:Lms.Explore.various-artists}</div>${</if-has-various-release-artists>}
|
||||
<div class="text-muted"><small>${<if-has-year>}${year}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>} · ${</if-has-year>}${duration}</small></div>
|
||||
${clusters}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
<div class="col">
|
||||
<h3>${name}</h3>
|
||||
${<if-has-release-artists>}${artists}${</if-has-release-artists>}
|
||||
${<if-has-various-release-artists>}<div class="text-secondary">${tr:Lms.Explore.various-artists}</div>${</if-has-various-release-artists>}
|
||||
<div class="small text-muted">${<if-has-year>}${year}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>} · ${</if-has-year>}${duration}</div>
|
||||
${clusters class="mb-2"}
|
||||
<div class="btn-group">
|
||||
${play-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${play-shuffled class="btn dropdown-item"}</li>
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${star class="btn dropdown-item"}</li>
|
||||
${<if-has-mbid>}
|
||||
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-release}</a></li>
|
||||
${</if-has-mbid>}
|
||||
<li>${download class="btn dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
${container}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
${container class="d-grid gap-3"}
|
||||
${<if-has-copyright>}
|
||||
<div class="pt-1 mt-1 border-top border-dark small text-muted fw-lighter fst-italic">${copyright class="text-reset"}</div>
|
||||
${</if-has-copyright>}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
${<if-has-similar-releases>}
|
||||
<div class="col-xs-12 col-md-8">
|
||||
<div class="well well-sm">
|
||||
<h3>${tr:Lms.Explore.Release.similar-releases}</h3>
|
||||
<div class="row">
|
||||
${similar-releases}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-similar-releases>}
|
||||
${<if-has-copyright>}
|
||||
<div class="col-xs-12 col-md-4">
|
||||
<div class="well well-sm">
|
||||
<h3>${tr:Lms.Explore.Release.copyright}</h3>
|
||||
${copyright class="text-muted"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-copyright>}
|
||||
${<if-has-mbid>}
|
||||
<div class="col-xs-12 col-md-4">
|
||||
<div class="well well-sm">
|
||||
<h3>${tr:Lms.Explore.links}</h3>
|
||||
${mbid-link class="Lms-btn text-muted"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-mbid>}
|
||||
</div>
|
||||
${<if-has-similar-releases>}
|
||||
<h3>${tr:Lms.Explore.Release.similar-releases}</h3>
|
||||
${similar-releases class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 gx-2 gy-4"}
|
||||
${</if-has-similar-releases>}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Release.template.entry-release-artist">
|
||||
<h4>${artist class="Lms-btn Lms-artistname text-muted"}</h4>
|
||||
${artist class="text-decoration-none link-success"}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Release.template.entry-disc">
|
||||
<h4>${disc-title}</h4>
|
||||
${tracks}
|
||||
${tracks class="d-grid gap-1"}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Release.template.entry-nodisc">
|
||||
${tracks class="d-grid gap-1"}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Release.template.entry">
|
||||
<div class="row Lms-explore-release-entry Lms-entry-hoverable Lms-vertical-align ${is-playing}">
|
||||
<div class="col-sm-1 col-xs-1 Lms-explore-release-entry-tracknumber text-muted">
|
||||
<div class="d-flex align-items-center rounded Lms-bg-dark-hover">
|
||||
<div class="p-2 text-muted text-end" style="min-width: 2.1rem;">
|
||||
${<if-has-track-number>}${track-number}${</if-has-track-number>}
|
||||
</div>
|
||||
<div class="col-sm-9 col-xs-7 Lms-explore-release-entry-name">
|
||||
<div class="Lms-trackname text-primary">${name}</div>
|
||||
<div class="p-2 flex-fill">
|
||||
${name}
|
||||
${<if-has-artists>}${artists}${</if-has-artists>}
|
||||
</div>
|
||||
<div class="col-sm-2 col-xs-4">
|
||||
<div class="Lms-explore-release-entry-controls">
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
</div>
|
||||
<div class="Lms-explore-release-entry-track-duration text-muted">
|
||||
${duration}
|
||||
<div class="p-2 d-none d-sm-block text-muted">
|
||||
${duration}
|
||||
</div>
|
||||
<div class="p-2 d-flex">
|
||||
${play-btn class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<div class="dropdown d-inline-block">
|
||||
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${star class="btn dropdown-item"}</li>
|
||||
<li>${download class="btn dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Release.template.entry-artist">
|
||||
<small>${artist class="Lms-btn Lms-artistname text-muted"}</small>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
+38
-15
@@ -3,33 +3,56 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Explore.Releases.template">
|
||||
<div class="Lms-header">
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
<div class="mb-3">
|
||||
<ul class="nav nav-pills mb-2">
|
||||
<li class="nav-item">
|
||||
${random class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${starred class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${most-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-added class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${all class="nav-link"}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="btn-group">
|
||||
${play-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${play-shuffled class="btn dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
${releases}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Releases.template.entry-grid">
|
||||
<div class="col-xs-6 col-sm-3 col-md-2 col-lg-2">
|
||||
<div class="Lms-explore-entry-grid">
|
||||
<div class="col h-100 Lms-bg-dark-hover rounded">
|
||||
<div class="p-2">
|
||||
<div class="Lms-responsive-square Lms-cover-container">
|
||||
${cover}
|
||||
</div>
|
||||
<div class="Lms-explore-entry-grid-text-container">
|
||||
<h5>${release-name class="Lms-btn Lms-releasename text-primary"}</h5>
|
||||
<div class="Lms-explore-entry-separator"><h6>${<if-has-year>}<small><span class="text-muted">${year}</span></small>${</if-has-year>}${<if-has-artist>}${artist-name class="Lms-btn Lms-artistname text-muted"}${</if-has-artist>}${<if-has-various-artists>}<div class="Lms-artistname text-muted">${tr:Lms.Explore.various-artists}</div>${</if-has-various-artists>}</h6></div>
|
||||
${cover class="shadow-sm"}
|
||||
</div>
|
||||
${release-name class="d-block text-truncate text-nowrap text-decoration-none link-success"}
|
||||
${<if-has-artist>}${artist-name class="d-block text-truncate text-nowrap text-decoration-none link-secondary"}${</if-has-artist>}
|
||||
${<if-has-various-artists>}<div class="text-truncate text-nowrap text-secondary">${tr:Lms.Explore.various-artists}</div>${</if-has-various-artists>}
|
||||
${<if-has-year>}<div class="small text-muted">${year}</div>${</if-has-year>}
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Releases.template.container">
|
||||
<div class="row">
|
||||
${elements}
|
||||
</div>
|
||||
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
|
||||
${elements class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 gx-2 gy-4"}
|
||||
${loading-indicator}
|
||||
</message>
|
||||
|
||||
|
||||
</messages>
|
||||
|
||||
+15
-3
@@ -3,10 +3,22 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Search.template">
|
||||
<div class="Lms-header">
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
<ul class="nav nav-pills mb-3" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="releases-tab" data-bs-toggle="tab" data-bs-target="#releases" type="button" role="tab" aria-controls="releases" aria-selected="true">${tr:Lms.Explore.releases}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="artists-tab" data-bs-toggle="tab" data-bs-target="#artists" type="button" role="tab" aria-controls="artists" aria-selected="false">${tr:Lms.Explore.artists}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tracks-tab" data-bs-toggle="tab" data-bs-target="#tracks" type="button" role="tab" aria-controls="tracks" aria-selected="false">${tr:Lms.Explore.tracks}</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane show active" id="releases" role="tabpanel" aria-labelledby="releases-tab">${releases}</div>
|
||||
<div class="tab-pane" id="artists" role="tabpanel" aria-labelledby="artists-tab">${artists}</div>
|
||||
<div class="tab-pane" id="tracks" role="tabpanel" aria-labelledby="tracks-tab">${tracks}</div>
|
||||
</div>
|
||||
${stack}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
+166
-191
@@ -4,198 +4,173 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Settings.template">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<form>
|
||||
<legend>${tr:Lms.Settings.appearance}</legend>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
<div class="checkbox">
|
||||
<label>${dark-mode}${tr:Lms.Settings.dark-mode}</label>
|
||||
${dark-mode-info clas="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<legend>${tr:Lms.Settings.audio}</legend>
|
||||
<div class="alert alert-info">
|
||||
${tr:Lms.Settings.audio-settings-are-local}
|
||||
</div>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:transcode-mode}">
|
||||
${tr:Lms.Settings.transcode-mode}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${transcode-mode}
|
||||
${transcode-mode-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:transcode-format}">
|
||||
${tr:Lms.Settings.transcode-format}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${transcode-format}
|
||||
${transcode-format-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:transcode-bitrate}">
|
||||
${tr:Lms.Settings.transcode-bitrate}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
<div class="input-group">
|
||||
${transcode-bitrate}
|
||||
<span class="input-group-addon">kbps</span>
|
||||
</div>
|
||||
${transcode-bitrate-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:replaygain-mode}">
|
||||
${tr:Lms.Settings.replaygain-mode}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${replaygain-mode}
|
||||
${replaygain-mode-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:replaygain-preamp}">
|
||||
${tr:Lms.Settings.replaygain-preamp}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
<div class="input-group">
|
||||
${replaygain-preamp}
|
||||
<span class="input-group-addon">dB</span>
|
||||
</div>
|
||||
${replaygain-preamp-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:replaygain-preamp-no-rg-info}">
|
||||
${tr:Lms.Settings.replaygain-preamp-no-rg-info}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
<div class="input-group">
|
||||
${replaygain-preamp-no-rg-info}
|
||||
<span class="input-group-addon">dB</span>
|
||||
</div>
|
||||
${replaygain-preamp-no-rg-info-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${<if-has-subsonic-api>}
|
||||
<legend>${tr:Lms.Settings.subsonic-api}</legend>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
<div class="checkbox">
|
||||
<label>${subsonic-transcode-enable}${tr:Lms.Settings.transcode-enable}</label>
|
||||
${subsonic-transcode-enable-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:subsonic-transcode-format}">
|
||||
${tr:Lms.Settings.transcode-format}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${subsonic-transcode-format}
|
||||
${subsonic-transcode-format-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:subsonic-transcode-bitrate}">
|
||||
${tr:Lms.Settings.transcode-bitrate}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
<div class="input-group">
|
||||
${subsonic-transcode-bitrate}
|
||||
<span class="input-group-addon">kbps</span>
|
||||
</div>
|
||||
${subsonic-transcode-bitrate-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:subsonic-artist-list-mode}">
|
||||
${tr:Lms.Settings.subsonic-artist-list-mode}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${subsonic-artist-list-mode}
|
||||
${subsonic-artist-list-mode-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-subsonic-api>}
|
||||
<legend>${tr:Lms.Settings.scrobbling}</legend>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:scrobbler}">
|
||||
${tr:Lms.Settings.scrobbling.scrobbler}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${scrobbler}
|
||||
${scrobbler-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:listenbrainz-token}">
|
||||
${tr:Lms.Settings.scrobbling.listenbrainz-token}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${listenbrainz-token}
|
||||
${listenbrainz-token-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${<if-has-change-password>}
|
||||
<legend>${tr:Lms.Settings.change-password}</legend>
|
||||
<div class="form-horizontal">
|
||||
${<if-has-old-password>}
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:password-old}">
|
||||
${tr:Lms.password-old}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${password-old}
|
||||
${password-old-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-old-password>}
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:password}">
|
||||
${tr:Lms.password-new}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${password}
|
||||
${password-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-3 control-label" for="${id:password-confirm}">
|
||||
${tr:Lms.password-confirm}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
${password-confirm}
|
||||
${password-confirm-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-change-password>}
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
${apply-btn class="btn-primary"} ${discard-btn}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form class="row g-3">
|
||||
<legend>${tr:Lms.Settings.audio}</legend>
|
||||
<div class="alert alert-info">
|
||||
${tr:Lms.Settings.audio-settings-are-local}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label" for="${id:transcode-mode}">
|
||||
${tr:Lms.Settings.transcode-mode}
|
||||
</label>
|
||||
${transcode-mode class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${transcode-mode-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label" for="${id:transcode-format}">
|
||||
${tr:Lms.Settings.transcode-format}
|
||||
</label>
|
||||
${transcode-format class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${transcode-format-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label" for="${id:transcode-bitrate}">
|
||||
${tr:Lms.Settings.transcode-bitrate}
|
||||
</label>
|
||||
<div class="input-group mb-3">
|
||||
${transcode-bitrate class="form-control"}
|
||||
<span class="input-group-text">kbps</span>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
${transcode-bitrate-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label" for="${id:replaygain-mode}">
|
||||
${tr:Lms.Settings.replaygain-mode}
|
||||
</label>
|
||||
${replaygain-mode class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${replaygain-mode-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label" for="${id:replaygain-preamp}">
|
||||
${tr:Lms.Settings.replaygain-preamp}
|
||||
</label>
|
||||
<div class="input-group mb-3">
|
||||
${replaygain-preamp class="form-control"}
|
||||
<span class="input-group-text">dB</span>
|
||||
<div class="invalid-feedback">
|
||||
${replaygain-preamp-info}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label" for="${id:replaygain-preamp-no-rg-info}">
|
||||
${tr:Lms.Settings.replaygain-preamp-no-rg-info}
|
||||
</label>
|
||||
<div class="input-group mb-3">
|
||||
${replaygain-preamp-no-rg-info class="form-control"}
|
||||
<span class="input-group-text">dB</span>
|
||||
<div class="invalid-feedback">
|
||||
${replaygain-preamp-no-rg-info-info}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${<if-has-subsonic-api>}
|
||||
<legend>${tr:Lms.Settings.subsonic-api}</legend>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
${subsonic-transcode-enable class="form-check-input"}
|
||||
<label class="form-check-label" for="${id:subsonic-transcode-enable}">
|
||||
${tr:Lms.Settings.transcode-enable class="form-control"}
|
||||
</label>
|
||||
<div class="invalid-feedback">
|
||||
${subsonic-transcode-enable-info}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:subsonic-transcode-format}">
|
||||
${tr:Lms.Settings.transcode-format}
|
||||
</label>
|
||||
${subsonic-transcode-format class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${subsonic-transcode-format-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<label class="col-lg-3 form-label" for="${id:subsonic-transcode-bitrate}">
|
||||
${tr:Lms.Settings.transcode-bitrate}
|
||||
</label>
|
||||
<div class="input-group mb-3">
|
||||
${subsonic-transcode-bitrate class="form-control"}
|
||||
<span class="input-group-text">kbps</span>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
${subsonic-transcode-bitrate-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:subsonic-artist-list-mode}">
|
||||
${tr:Lms.Settings.subsonic-artist-list-mode}
|
||||
</label>
|
||||
${subsonic-artist-list-mode class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${subsonic-artist-list-mode-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-subsonic-api>}
|
||||
<legend>${tr:Lms.Settings.scrobbling}</legend>
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:scrobbler}">
|
||||
${tr:Lms.Settings.scrobbling.scrobbler}
|
||||
</label>
|
||||
${scrobbler class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${scrobbler-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:listenbrainz-token}">
|
||||
${tr:Lms.Settings.scrobbling.listenbrainz-token}
|
||||
</label>
|
||||
${listenbrainz-token class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${listenbrainz-token-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
${<if-has-change-password>}
|
||||
<legend>${tr:Lms.Settings.change-password}</legend>
|
||||
${<if-has-old-password>}
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:password-old}">
|
||||
${tr:Lms.password-old}
|
||||
</label>
|
||||
${password-old class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${password-old-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-old-password>}
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:password}">
|
||||
${tr:Lms.password-new}
|
||||
</label>
|
||||
${password class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${password-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="${id:password-confirm}">
|
||||
${tr:Lms.password-confirm}
|
||||
</label>
|
||||
${password-confirm class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${password-confirm-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-change-password>}
|
||||
<div class="col-12">
|
||||
${apply-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
</form>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.template">
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button class="navbar-toggle" data-target=".navbar-collapse" data-toggle="collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
${title class="navbar-brand Lms-btn"}
|
||||
<div class="Lms-navbar-search pull-left">
|
||||
${search class="form-control"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>${releases class="Lms-btn"}</li>
|
||||
<li>${artists class="Lms-btn"}</li>
|
||||
<li>${tracks class="Lms-btn"}</li>
|
||||
<li>${playqueue class="Lms-btn"}</li>
|
||||
</ul>
|
||||
<div class="navbar-form navbar-left">
|
||||
${filters class="form-group Lms-clusters-control"}
|
||||
</div>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
${<if-is-admin>}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="Lms-btn dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" title="${tr:Lms.administration}"><i class="fa fa-cogs" aria-hidden="true"></i> <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li>${database}</li>
|
||||
<li>${users}</li>
|
||||
</ul>
|
||||
</li>
|
||||
${</if-is-admin>}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="Lms-btn dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" title="${tr:Lms.user}"><i class="fa fa-user" aria-hidden="true"></i>
|
||||
<span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li class="dropdown-header">${username}</li>
|
||||
<li class="divider"></li>
|
||||
<li>${settings}</li>
|
||||
<li>${logout}</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
${contents class="Lms-contents"}
|
||||
</div>
|
||||
${player class="Lms-player"}
|
||||
</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>
|
||||
|
||||
<message id="Lms.infinite-scrolling-container">
|
||||
${elements}
|
||||
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
+53
-35
@@ -4,49 +4,67 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Tracks.template">
|
||||
<div class="Lms-header">
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
<div class="mb-3">
|
||||
<ul class="nav nav-pills mb-2">
|
||||
<li class="nav-item">
|
||||
${random class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${starred class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${most-played class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${recently-added class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${all class="nav-link"}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="btn-group">
|
||||
${play-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${play-shuffled class="btn dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
${tracks}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Tracks.template.entry">
|
||||
<div class="row Lms-vertical-align Lms-explore-entry-list Lms-explore-tracks-entry ${is-playing}">
|
||||
<div class="col-xs-3 col-sm-2 col-md-2">
|
||||
<div class="Lms-responsive-square Lms-cover-container">
|
||||
${cover}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-7 col-sm-8 col-md-8 Lms-explore-entry-list-text-container">
|
||||
<div class="Lms-trackname text-primary"><h5>${name}</h5></div>
|
||||
${<if-has-artists-or-release>}
|
||||
${<if-has-release>}<small>${release class="Lms-btn Lms-releasename text-primary"}</small>${</if-has-release>}
|
||||
${<if-has-artists>}${artists}${</if-has-artists>}
|
||||
${</if-has-artists-or-release>}
|
||||
</div>
|
||||
<div class="col-xs-2 col-sm-2 col-md-2">
|
||||
<div class="Lms-explore-tracks-entry-controls">
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
</div>
|
||||
<div class="Lms-explore-tracks-entry-duration text-muted">
|
||||
${duration}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center rounded Lms-bg-dark-hover mb-2">
|
||||
<div class="p-1 Lms-cover-small">
|
||||
${cover}
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Tracks.template.entry-artist">
|
||||
<small>${artist class="Lms-btn Lms-artistname text-muted"}</small>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Tracks.template.container">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
${elements}
|
||||
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
|
||||
<div class="p-2 flex-fill text-truncate">
|
||||
<div class="d-block text-truncate">${name}</div>
|
||||
${<if-has-release>}${release class="text-decoration-none link-success text-truncate"}${</if-has-release>}
|
||||
</div>
|
||||
<div class="p-2 d-none d-sm-block text-muted">
|
||||
${duration}
|
||||
</div>
|
||||
<div class="p-2 d-flex">
|
||||
${play-btn class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<div class="dropdown d-inline-block">
|
||||
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${star class="btn dropdown-item"}</li>
|
||||
<li>${download class="btn dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Tracks.template.entry-artist">
|
||||
${artist class="text-decoration-none link-secondary"}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
find_path(STB_INCLUDE_DIR stb/stb.h)
|
||||
find_path(STB_INCLUDE_DIR stb/stb_image.h)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
-11
File diff suppressed because one or more lines are too long
Vendored
-11
File diff suppressed because one or more lines are too long
+12
File diff suppressed because one or more lines are too long
@@ -1,54 +0,0 @@
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Lato Italic'), local('Lato-Italic'), url(S6u8w4BMUTPHjxsAUi-qJCY.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Lato Italic'), local('Lato-Italic'), url(S6u8w4BMUTPHjxsAXC-q.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Lato Regular'), local('Lato-Regular'), url(S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local('Lato Regular'), local('Lato-Regular'), url(S6uyw4BMUTPHjx4wXg.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local('Lato Bold'), local('Lato-Bold'), url(S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local('Lato Bold'), local('Lato-Bold'), url(S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
:root {
|
||||
--body-color: #ffffff;
|
||||
--hover-color: #00bc8c;
|
||||
--playing-background-color: #375a7f4f;
|
||||
--slider-background-color: #5a5a5a;
|
||||
--player-background-color: #303030;
|
||||
}
|
||||
|
||||
.Lms-artistname.text-muted {
|
||||
font-style: italic;
|
||||
color: #7e7e7e;
|
||||
}
|
||||
|
||||
.Lms-artistname.text-primary, .Lms-artistname.text-primary:hover {
|
||||
color: var(--body-color)
|
||||
}
|
||||
|
||||
a.Lms-artistname.text-primary:hover {
|
||||
color: var(--hover-color)
|
||||
}
|
||||
|
||||
.Lms-releasename.text-primary, .Lms-releasename.text-primary:hover {
|
||||
color: var(--body-color)
|
||||
}
|
||||
|
||||
a.Lms-releasename.text-primary:hover {
|
||||
color: var(--hover-color)
|
||||
}
|
||||
|
||||
.Lms-trackname.text-primary, .Lms-trackname.text-primary:hover {
|
||||
color: var(--body-color)
|
||||
}
|
||||
|
||||
.media, .media-body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
:root {
|
||||
--body-color: #2c3e50;
|
||||
--hover-color: #18bc9c;
|
||||
--playing-background-color: #2c3e5029;
|
||||
--slider-background-color: #2c3e5029;
|
||||
--player-background-color: #ecf0f1;
|
||||
}
|
||||
|
||||
.Lms-artistname.text-muted {
|
||||
font-style: italic;
|
||||
color: #7e8f9e;
|
||||
}
|
||||
|
||||
.media, .media-body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
+65
-416
@@ -1,73 +1,58 @@
|
||||
/* Stylized for flaty/darkly themes */
|
||||
a, a:link, a:active, a:visited {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
/* Generic rules */
|
||||
|
||||
.link-success {
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.link-secondary {
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.Lms-bg-dark-hover {
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.Lms-bg-dark-hover:hover {
|
||||
background-color: var(--bs-dark);
|
||||
}
|
||||
|
||||
/* Specific rules */
|
||||
body {
|
||||
padding-bottom: 78px;
|
||||
}
|
||||
|
||||
.Lms-vertical-align {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Lms-admin-users-entry {
|
||||
min-height: 48px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Lms-admin-users-add-btn {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.Lms-admin-users-entry-role {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.Lms-admin-users-entry-btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.Lms-artistname {
|
||||
color: var(--body-color);
|
||||
}
|
||||
|
||||
a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.Lms-btn {
|
||||
.Lms-badge-cluster {
|
||||
transition: filter 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Lms-btn:hover {
|
||||
color: var(--hover-color);
|
||||
transition: color .20s cubic-bezier(.43,-.1,.16,1.1);
|
||||
}
|
||||
|
||||
.Lms-cluster {
|
||||
display: inline-block;
|
||||
.Lms-badge-cluster:hover {
|
||||
filter: brightness(85%);
|
||||
cursor: pointer;
|
||||
line-height: 1.3;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.Lms-cluster:hover {
|
||||
filter: brightness(130%);
|
||||
transition: filter .20s cubic-bezier(.43,-.1,.16,1.1);
|
||||
}
|
||||
|
||||
.Lms-clusters-control {
|
||||
margin-top: 4px;
|
||||
.Lms-entry-playing {
|
||||
background-color: var(--bs-dark);
|
||||
}
|
||||
|
||||
.Lms-cover {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-in, filter 0.15s ease;
|
||||
}
|
||||
|
||||
.Lms-cover-anchor:hover {
|
||||
filter: brightness(85%);
|
||||
}
|
||||
|
||||
.Lms-cover-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Lms-cover-loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.Lms-cover-release {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
@@ -78,299 +63,53 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||
opacity: 0;
|
||||
-moz-transition: opacity 1s;
|
||||
-webkit-transition: opacity 1s;
|
||||
-o-transition: opacity 1s;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
|
||||
.Lms-cover:hover {
|
||||
filter: brightness(130%);
|
||||
transition: filter .20s cubic-bezier(.43,-.1,.16,1.1);
|
||||
.Lms-cover-track {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
}
|
||||
|
||||
.Lms-cover-container {
|
||||
position: relative;
|
||||
#lms-mp-progress {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.Lms-cover-large {
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||
#lms-mp-volume {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Lms-cover-loaded {
|
||||
opacity: 1;
|
||||
#lms-mp-volume-slider::-webkit-slider-runnable-track {
|
||||
background-color: var(--bs-gray-900);
|
||||
}
|
||||
|
||||
.Lms-entry-playing {
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
background-color: var(--playing-background-color);
|
||||
#lms-mp-volume-slider::-moz-range-track {
|
||||
background-color: var(--bs-gray-900);
|
||||
}
|
||||
|
||||
.Lms-explore-btn {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.Lms-explore-clusters {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.Lms-explore-contents {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.Lms-explore-entry-list {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.Lms-explore-entry-list-text-container {
|
||||
margin-left: -15px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.Lms-explore-entry-list-text-container h5 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.Lms-explore-entry-grid {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.Lms-explore-entry-grid-text-container {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.Lms-explore-entry-grid-text-container h5, h6 {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.Lms-explore-entry-separator :nth-child(2) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.Lms-explore-release {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.Lms-explore-release-cover {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry-tracknumber {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry-name {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry:hover .Lms-explore-release-entry-controls {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry:hover .Lms-explore-release-entry-track-duration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry-controls {
|
||||
text-align: right;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry-track-duration {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Lms-explore-releaselink {
|
||||
margin: 0;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.Lms-explore-artists-entry-small {
|
||||
min-height: 28px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Lms-explore-artists-input {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.Lms-explore-artist-entry {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.Lms-explore-artist-entries-container {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.Lms-explore-artist-tracks-container {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.Lms-explore-mode-container {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.Lms-explore-tracks-entry:hover .Lms-explore-tracks-entry-controls {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Lms-explore-tracks-entry:hover .Lms-explore-tracks-entry-duration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.Lms-explore-tracks-entry-controls {
|
||||
text-align: right;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.Lms-explore-tracks-entry-duration {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Lms-header {
|
||||
padding-bottom: 9px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.Lms-horizontal-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Lms-loading-indicator {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.Lms-navbar-search {
|
||||
max-width: 175px;
|
||||
padding-top: 7.5px;
|
||||
padding-bottom: 7.5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.Lms-playqueue-btn {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.Lms-playqueue-entry:hover .Lms-playqueue-entry-controls {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Lms-playqueue-entry:hover .Lms-playqueue-entry-duration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.text-success .Lms-playqueue-btn {
|
||||
filter: brightness(130%);
|
||||
filter: drop-shadow(0 0 0.65rem var(--hover-color));
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.Lms-playqueue-entry-controls {
|
||||
text-align: right;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.Lms-playqueue-entry-duration {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Lms-playqueue-entry-name {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.Lms-playqueue-selected {
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
background-color: var(--playing-background-color);
|
||||
}
|
||||
|
||||
.Lms-playqueue-selected h4 {
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.Lms-playhistory-entry {
|
||||
min-height: 32px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.Lms-playhistory-entry-name {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
.Lms-notification-container {
|
||||
position: fixed;
|
||||
bottom: 78px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.Lms-player {
|
||||
z-index: 3;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: var(--player-background-color);
|
||||
}
|
||||
|
||||
.Lms-player-btn-volume {
|
||||
font-size: 18px !important;
|
||||
margin: auto;
|
||||
.fixed-bottom.Lms-player {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.Lms-player-btn-controls {
|
||||
font-size: 40px !important;
|
||||
.Lms-player-volume-container {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.Lms-player-controls {
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.Lms-player-duration {
|
||||
line-height: 40px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.Lms-player-progress {
|
||||
background-color: var(--slider-background-color);
|
||||
}
|
||||
|
||||
.Lms-player-progress .progress-bar {
|
||||
transition: none;
|
||||
.Lms-player-seek-common {
|
||||
position: absolute;
|
||||
width:100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.Lms-player-seek {
|
||||
@@ -380,8 +119,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background: transparent;
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
}
|
||||
|
||||
.Lms-player-seek::-webkit-slider-thumb {
|
||||
@@ -391,23 +128,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.Lms-player-seek-container {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.Lms-player-seek-common {
|
||||
position: absolute;
|
||||
width:100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.Lms-player-seek::-moz-range-thumb {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
@@ -415,75 +135,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.Lms-player-text-center {
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.Lms-player-text {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.Lms-player-volume {
|
||||
width: 125px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Lms-player-volume-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
background-color: var(--slider-background-color);
|
||||
}
|
||||
|
||||
.Lms-player-volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
background-color: var(--body-color);
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.Lms-player-volume-slider::-webkit-slider-thumb:hover {
|
||||
background-color: var(--hover-color);
|
||||
}
|
||||
|
||||
.Lms-player-volume-slider::-moz-range-thumb {
|
||||
background-color: var(--body-color);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.Lms-player-volume-slider::-moz-range-thumb:hover {
|
||||
background-color: var(--hover-color);
|
||||
}
|
||||
|
||||
.Lms-player-volume-slider-container {
|
||||
display: inline-block;
|
||||
width: 85px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.Lms-releasename {
|
||||
color: var(--body-color);
|
||||
}
|
||||
|
||||
a.Lms-releasename:hover, a.Lms-releasename:focus {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.Lms-responsive-square {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -493,5 +144,3 @@ a.Lms-releasename:hover, a.Lms-releasename:focus {
|
||||
display: block;
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Vendored
-417
@@ -1,417 +0,0 @@
|
||||
/*
|
||||
* Project: Bootstrap Notify = v3.1.5
|
||||
* Description: Turns standard Bootstrap alerts into "Growl-like" notifications.
|
||||
* Author: Mouse0270 aka Robert McIntosh
|
||||
* License: MIT License
|
||||
* Website: https://github.com/mouse0270/bootstrap-growl
|
||||
*/
|
||||
|
||||
/* global define:false, require: false, jQuery:false */
|
||||
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else if (typeof exports === 'object') {
|
||||
// Node/CommonJS
|
||||
factory(require('jquery'));
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
// Create the defaults once
|
||||
var defaults = {
|
||||
element: 'body',
|
||||
position: null,
|
||||
type: "info",
|
||||
allow_dismiss: true,
|
||||
allow_duplicates: true,
|
||||
newest_on_top: false,
|
||||
showProgressbar: false,
|
||||
placement: {
|
||||
from: "top",
|
||||
align: "right"
|
||||
},
|
||||
offset: 20,
|
||||
spacing: 10,
|
||||
z_index: 1031,
|
||||
delay: 5000,
|
||||
timer: 1000,
|
||||
url_target: '_blank',
|
||||
mouse_over: null,
|
||||
animate: {
|
||||
enter: 'animated fadeInDown',
|
||||
exit: 'animated fadeOutUp'
|
||||
},
|
||||
onShow: null,
|
||||
onShown: null,
|
||||
onClose: null,
|
||||
onClosed: null,
|
||||
onClick: null,
|
||||
icon_type: 'class',
|
||||
template: '<div data-notify="container" class="col-xs-11 col-sm-4 alert alert-{0}" role="alert"><button type="button" aria-hidden="true" class="close" data-notify="dismiss">×</button><span data-notify="icon"></span> <span data-notify="title">{1}</span> <span data-notify="message">{2}</span><div class="progress" data-notify="progressbar"><div class="progress-bar progress-bar-{0}" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"></div></div><a href="{3}" target="{4}" data-notify="url"></a></div>'
|
||||
};
|
||||
|
||||
String.format = function () {
|
||||
var args = arguments;
|
||||
var str = arguments[0];
|
||||
return str.replace(/(\{\{\d\}\}|\{\d\})/g, function (str) {
|
||||
if (str.substring(0, 2) === "{{") return str;
|
||||
var num = parseInt(str.match(/\d/)[0]);
|
||||
return args[num + 1];
|
||||
});
|
||||
};
|
||||
|
||||
function isDuplicateNotification(notification) {
|
||||
var isDupe = false;
|
||||
|
||||
$('[data-notify="container"]').each(function (i, el) {
|
||||
var $el = $(el);
|
||||
var title = $el.find('[data-notify="title"]').html().trim();
|
||||
var message = $el.find('[data-notify="message"]').html().trim();
|
||||
|
||||
// The input string might be different than the actual parsed HTML string!
|
||||
// (<br> vs <br /> for example)
|
||||
// So we have to force-parse this as HTML here!
|
||||
var isSameTitle = title === $("<div>" + notification.settings.content.title + "</div>").html().trim();
|
||||
var isSameMsg = message === $("<div>" + notification.settings.content.message + "</div>").html().trim();
|
||||
var isSameType = $el.hasClass('alert-' + notification.settings.type);
|
||||
|
||||
if (isSameTitle && isSameMsg && isSameType) {
|
||||
//we found the dupe. Set the var and stop checking.
|
||||
isDupe = true;
|
||||
}
|
||||
return !isDupe;
|
||||
});
|
||||
|
||||
return isDupe;
|
||||
}
|
||||
|
||||
function Notify(element, content, options) {
|
||||
// Setup Content of Notify
|
||||
var contentObj = {
|
||||
content: {
|
||||
message: typeof content === 'object' ? content.message : content,
|
||||
title: content.title ? content.title : '',
|
||||
icon: content.icon ? content.icon : '',
|
||||
url: content.url ? content.url : '#',
|
||||
target: content.target ? content.target : '-'
|
||||
}
|
||||
};
|
||||
|
||||
options = $.extend(true, {}, contentObj, options);
|
||||
this.settings = $.extend(true, {}, defaults, options);
|
||||
this._defaults = defaults;
|
||||
if (this.settings.content.target === "-") {
|
||||
this.settings.content.target = this.settings.url_target;
|
||||
}
|
||||
this.animations = {
|
||||
start: 'webkitAnimationStart oanimationstart MSAnimationStart animationstart',
|
||||
end: 'webkitAnimationEnd oanimationend MSAnimationEnd animationend'
|
||||
};
|
||||
|
||||
if (typeof this.settings.offset === 'number') {
|
||||
this.settings.offset = {
|
||||
x: this.settings.offset,
|
||||
y: this.settings.offset
|
||||
};
|
||||
}
|
||||
|
||||
//if duplicate messages are not allowed, then only continue if this new message is not a duplicate of one that it already showing
|
||||
if (this.settings.allow_duplicates || (!this.settings.allow_duplicates && !isDuplicateNotification(this))) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
$.extend(Notify.prototype, {
|
||||
init: function () {
|
||||
var self = this;
|
||||
|
||||
this.buildNotify();
|
||||
if (this.settings.content.icon) {
|
||||
this.setIcon();
|
||||
}
|
||||
if (this.settings.content.url != "#") {
|
||||
this.styleURL();
|
||||
}
|
||||
this.styleDismiss();
|
||||
this.placement();
|
||||
this.bind();
|
||||
|
||||
this.notify = {
|
||||
$ele: this.$ele,
|
||||
update: function (command, update) {
|
||||
var commands = {};
|
||||
if (typeof command === "string") {
|
||||
commands[command] = update;
|
||||
} else {
|
||||
commands = command;
|
||||
}
|
||||
for (var cmd in commands) {
|
||||
switch (cmd) {
|
||||
case "type":
|
||||
this.$ele.removeClass('alert-' + self.settings.type);
|
||||
this.$ele.find('[data-notify="progressbar"] > .progress-bar').removeClass('progress-bar-' + self.settings.type);
|
||||
self.settings.type = commands[cmd];
|
||||
this.$ele.addClass('alert-' + commands[cmd]).find('[data-notify="progressbar"] > .progress-bar').addClass('progress-bar-' + commands[cmd]);
|
||||
break;
|
||||
case "icon":
|
||||
var $icon = this.$ele.find('[data-notify="icon"]');
|
||||
if (self.settings.icon_type.toLowerCase() === 'class') {
|
||||
$icon.removeClass(self.settings.content.icon).addClass(commands[cmd]);
|
||||
} else {
|
||||
if (!$icon.is('img')) {
|
||||
$icon.find('img');
|
||||
}
|
||||
$icon.attr('src', commands[cmd]);
|
||||
}
|
||||
self.settings.content.icon = commands[command];
|
||||
break;
|
||||
case "progress":
|
||||
var newDelay = self.settings.delay - (self.settings.delay * (commands[cmd] / 100));
|
||||
this.$ele.data('notify-delay', newDelay);
|
||||
this.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', commands[cmd]).css('width', commands[cmd] + '%');
|
||||
break;
|
||||
case "url":
|
||||
this.$ele.find('[data-notify="url"]').attr('href', commands[cmd]);
|
||||
break;
|
||||
case "target":
|
||||
this.$ele.find('[data-notify="url"]').attr('target', commands[cmd]);
|
||||
break;
|
||||
default:
|
||||
this.$ele.find('[data-notify="' + cmd + '"]').html(commands[cmd]);
|
||||
}
|
||||
}
|
||||
var posX = this.$ele.outerHeight() + parseInt(self.settings.spacing) + parseInt(self.settings.offset.y);
|
||||
self.reposition(posX);
|
||||
},
|
||||
close: function () {
|
||||
self.close();
|
||||
}
|
||||
};
|
||||
|
||||
},
|
||||
buildNotify: function () {
|
||||
var content = this.settings.content;
|
||||
this.$ele = $(String.format(this.settings.template, this.settings.type, content.title, content.message, content.url, content.target));
|
||||
this.$ele.attr('data-notify-position', this.settings.placement.from + '-' + this.settings.placement.align);
|
||||
if (!this.settings.allow_dismiss) {
|
||||
this.$ele.find('[data-notify="dismiss"]').css('display', 'none');
|
||||
}
|
||||
if ((this.settings.delay <= 0 && !this.settings.showProgressbar) || !this.settings.showProgressbar) {
|
||||
this.$ele.find('[data-notify="progressbar"]').remove();
|
||||
}
|
||||
},
|
||||
setIcon: function () {
|
||||
if (this.settings.icon_type.toLowerCase() === 'class') {
|
||||
this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon);
|
||||
} else {
|
||||
if (this.$ele.find('[data-notify="icon"]').is('img')) {
|
||||
this.$ele.find('[data-notify="icon"]').attr('src', this.settings.content.icon);
|
||||
} else {
|
||||
this.$ele.find('[data-notify="icon"]').append('<img src="' + this.settings.content.icon + '" alt="Notify Icon" />');
|
||||
}
|
||||
}
|
||||
},
|
||||
styleDismiss: function () {
|
||||
this.$ele.find('[data-notify="dismiss"]').css({
|
||||
position: 'absolute',
|
||||
right: '10px',
|
||||
top: '5px',
|
||||
zIndex: this.settings.z_index + 2
|
||||
});
|
||||
},
|
||||
styleURL: function () {
|
||||
this.$ele.find('[data-notify="url"]').css({
|
||||
backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)',
|
||||
height: '100%',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
zIndex: this.settings.z_index + 1
|
||||
});
|
||||
},
|
||||
placement: function () {
|
||||
var self = this,
|
||||
offsetAmt = this.settings.offset.y,
|
||||
css = {
|
||||
display: 'inline-block',
|
||||
margin: '0px auto',
|
||||
position: this.settings.position ? this.settings.position : (this.settings.element === 'body' ? 'fixed' : 'absolute'),
|
||||
transition: 'all .5s ease-in-out',
|
||||
zIndex: this.settings.z_index
|
||||
},
|
||||
hasAnimation = false,
|
||||
settings = this.settings;
|
||||
|
||||
$('[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])').each(function () {
|
||||
offsetAmt = Math.max(offsetAmt, parseInt($(this).css(settings.placement.from)) + parseInt($(this).outerHeight()) + parseInt(settings.spacing));
|
||||
});
|
||||
if (this.settings.newest_on_top === true) {
|
||||
offsetAmt = this.settings.offset.y;
|
||||
}
|
||||
css[this.settings.placement.from] = offsetAmt + 'px';
|
||||
|
||||
switch (this.settings.placement.align) {
|
||||
case "left":
|
||||
case "right":
|
||||
css[this.settings.placement.align] = this.settings.offset.x + 'px';
|
||||
break;
|
||||
case "center":
|
||||
css.left = 0;
|
||||
css.right = 0;
|
||||
break;
|
||||
}
|
||||
this.$ele.css(css).addClass(this.settings.animate.enter);
|
||||
$.each(Array('webkit-', 'moz-', 'o-', 'ms-', ''), function (index, prefix) {
|
||||
self.$ele[0].style[prefix + 'AnimationIterationCount'] = 1;
|
||||
});
|
||||
|
||||
$(this.settings.element).append(this.$ele);
|
||||
|
||||
if (this.settings.newest_on_top === true) {
|
||||
offsetAmt = (parseInt(offsetAmt) + parseInt(this.settings.spacing)) + this.$ele.outerHeight();
|
||||
this.reposition(offsetAmt);
|
||||
}
|
||||
|
||||
if ($.isFunction(self.settings.onShow)) {
|
||||
self.settings.onShow.call(this.$ele);
|
||||
}
|
||||
|
||||
this.$ele.one(this.animations.start, function () {
|
||||
hasAnimation = true;
|
||||
}).one(this.animations.end, function () {
|
||||
self.$ele.removeClass(self.settings.animate.enter);
|
||||
if ($.isFunction(self.settings.onShown)) {
|
||||
self.settings.onShown.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
if (!hasAnimation) {
|
||||
if ($.isFunction(self.settings.onShown)) {
|
||||
self.settings.onShown.call(this);
|
||||
}
|
||||
}
|
||||
}, 600);
|
||||
},
|
||||
bind: function () {
|
||||
var self = this;
|
||||
|
||||
this.$ele.find('[data-notify="dismiss"]').on('click', function () {
|
||||
self.close();
|
||||
});
|
||||
|
||||
if ($.isFunction(self.settings.onClick)) {
|
||||
this.$ele.on('click', function (event) {
|
||||
if (event.target != self.$ele.find('[data-notify="dismiss"]')[0]) {
|
||||
self.settings.onClick.call(this, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.$ele.mouseover(function () {
|
||||
$(this).data('data-hover', "true");
|
||||
}).mouseout(function () {
|
||||
$(this).data('data-hover', "false");
|
||||
});
|
||||
this.$ele.data('data-hover', "false");
|
||||
|
||||
if (this.settings.delay > 0) {
|
||||
self.$ele.data('notify-delay', self.settings.delay);
|
||||
var timer = setInterval(function () {
|
||||
var delay = parseInt(self.$ele.data('notify-delay')) - self.settings.timer;
|
||||
if ((self.$ele.data('data-hover') === 'false' && self.settings.mouse_over === "pause") || self.settings.mouse_over != "pause") {
|
||||
var percent = ((self.settings.delay - delay) / self.settings.delay) * 100;
|
||||
self.$ele.data('notify-delay', delay);
|
||||
self.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', percent).css('width', percent + '%');
|
||||
}
|
||||
if (delay <= -(self.settings.timer)) {
|
||||
clearInterval(timer);
|
||||
self.close();
|
||||
}
|
||||
}, self.settings.timer);
|
||||
}
|
||||
},
|
||||
close: function () {
|
||||
var self = this,
|
||||
posX = parseInt(this.$ele.css(this.settings.placement.from)),
|
||||
hasAnimation = false;
|
||||
|
||||
this.$ele.attr('data-closing', 'true').addClass(this.settings.animate.exit);
|
||||
self.reposition(posX);
|
||||
|
||||
if ($.isFunction(self.settings.onClose)) {
|
||||
self.settings.onClose.call(this.$ele);
|
||||
}
|
||||
|
||||
this.$ele.one(this.animations.start, function () {
|
||||
hasAnimation = true;
|
||||
}).one(this.animations.end, function () {
|
||||
$(this).remove();
|
||||
if ($.isFunction(self.settings.onClosed)) {
|
||||
self.settings.onClosed.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
if (!hasAnimation) {
|
||||
self.$ele.remove();
|
||||
if ($.isFunction(self.settings.onClosed)) {
|
||||
self.settings.onClosed.call(this);
|
||||
}
|
||||
}
|
||||
}, 600);
|
||||
},
|
||||
reposition: function (posX) {
|
||||
var self = this,
|
||||
notifies = '[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])',
|
||||
$elements = this.$ele.nextAll(notifies);
|
||||
if (this.settings.newest_on_top === true) {
|
||||
$elements = this.$ele.prevAll(notifies);
|
||||
}
|
||||
$elements.each(function () {
|
||||
$(this).css(self.settings.placement.from, posX);
|
||||
posX = (parseInt(posX) + parseInt(self.settings.spacing)) + $(this).outerHeight();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$.notify = function (content, options) {
|
||||
var plugin = new Notify(this, content, options);
|
||||
return plugin.notify;
|
||||
};
|
||||
$.notifyDefaults = function (options) {
|
||||
defaults = $.extend(true, {}, defaults, options);
|
||||
return defaults;
|
||||
};
|
||||
|
||||
$.notifyClose = function (selector) {
|
||||
|
||||
if (typeof selector === "undefined" || selector === "all") {
|
||||
$('[data-notify]').find('[data-notify="dismiss"]').trigger('click');
|
||||
}else if(selector === 'success' || selector === 'info' || selector === 'warning' || selector === 'danger'){
|
||||
$('.alert-' + selector + '[data-notify]').find('[data-notify="dismiss"]').trigger('click');
|
||||
} else if(selector){
|
||||
$(selector + '[data-notify]').find('[data-notify="dismiss"]').trigger('click');
|
||||
}
|
||||
else {
|
||||
$('[data-notify-position="' + selector + '"]').find('[data-notify="dismiss"]').trigger('click');
|
||||
}
|
||||
};
|
||||
|
||||
$.notifyCloseExcept = function (selector) {
|
||||
|
||||
if(selector === 'success' || selector === 'info' || selector === 'warning' || selector === 'danger'){
|
||||
$('[data-notify]').not('.alert-' + selector).find('[data-notify="dismiss"]').trigger('click');
|
||||
} else{
|
||||
$('[data-notify]').not(selector).find('[data-notify="dismiss"]').trigger('click');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}));
|
||||
|
||||
|
||||
Vendored
+7
File diff suppressed because one or more lines are too long
Vendored
-7
File diff suppressed because one or more lines are too long
Vendored
-6
File diff suppressed because one or more lines are too long
@@ -31,13 +31,16 @@ LMS.mediaplayer = function () {
|
||||
var _lastStartPlaying = null;
|
||||
|
||||
var _updateControls = function() {
|
||||
const pauseClass = "fa-pause";
|
||||
const playClass = "fa-play";
|
||||
|
||||
if (_elems.audio.paused) {
|
||||
_elems.playpause.classList.remove("fa-pause");
|
||||
_elems.playpause.classList.add("fa-play");
|
||||
_elems.playpause.firstElementChild.classList.remove(pauseClass);
|
||||
_elems.playpause.firstElementChild.classList.add(playClass);
|
||||
}
|
||||
else {
|
||||
_elems.playpause.classList.remove("fa-play");
|
||||
_elems.playpause.classList.add("fa-pause");
|
||||
_elems.playpause.firstElementChild.classList.remove(playClass);
|
||||
_elems.playpause.firstElementChild.classList.add(pauseClass);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,8 +181,6 @@ LMS.mediaplayer = function () {
|
||||
_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);
|
||||
_gainNode.connect(_audioCtx.destination);
|
||||
|
||||
@@ -241,7 +241,7 @@ AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
|
||||
std::optional<AudioFileFormat>
|
||||
guessMediaFileFormat(const std::filesystem::path& file)
|
||||
{
|
||||
AVOutputFormat* format {av_guess_format(NULL,file.string().c_str(),NULL)};
|
||||
const AVOutputFormat* format {av_guess_format(NULL,file.string().c_str(),NULL)};
|
||||
if (!format || !format->name)
|
||||
return {};
|
||||
|
||||
|
||||
@@ -226,7 +226,6 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
track.clusters[tag] = std::move(values);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
track.artists = getArtists(metadataMap);
|
||||
|
||||
@@ -148,7 +148,6 @@ getAlbum(const TagLib::PropertyMap& properties)
|
||||
void
|
||||
TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug)
|
||||
{
|
||||
|
||||
if (debug)
|
||||
{
|
||||
std::vector<std::string> strs;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <optional>
|
||||
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
@@ -204,8 +204,8 @@ Artist::find(Session& session, const FindParameters& params)
|
||||
return execQuery(query, params.range);
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Artist::getReleases(const std::vector<ClusterId>& clusterIds) const
|
||||
RangeResults<ReleaseId>
|
||||
Artist::getReleases(Range range, const std::vector<ClusterId>& clusterIds) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -213,7 +213,7 @@ Artist::getReleases(const std::vector<ClusterId>& clusterIds) const
|
||||
|
||||
std::ostringstream oss;
|
||||
|
||||
oss << "SELECT DISTINCT r FROM release r INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id INNER JOIN track t ON t.release_id = r.id";
|
||||
oss << "SELECT DISTINCT r.id FROM release r INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id INNER JOIN track t ON t.release_id = r.id";
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
@@ -236,13 +236,12 @@ Artist::getReleases(const std::vector<ClusterId>& clusterIds) const
|
||||
|
||||
oss << " ORDER BY t.date DESC, r.name COLLATE NOCASE";
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Release>>(oss.str())};
|
||||
auto query {session()->query<ReleaseId>(oss.str())};
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
auto res {query.resultList()};
|
||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "services/database/ArtistId.hpp"
|
||||
#include "services/database/ClusterId.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
@@ -88,7 +89,7 @@ class Artist : public Object<Artist, ArtistId>
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
|
||||
std::vector<ObjectPtr<Release>> getReleases(const std::vector<ClusterId>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
|
||||
RangeResults<ReleaseId> getReleases(Range range, const std::vector<ClusterId>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
|
||||
std::size_t getReleaseCount() const;
|
||||
std::vector<ObjectPtr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
|
||||
bool hasNonReleaseTracks(std::optional<TrackArtistLinkType> linkType = std::nullopt) const;
|
||||
|
||||
@@ -574,13 +574,13 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
auto releases {artist->getReleases()};
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
EXPECT_EQ(releases.front()->getId(), release.getId());
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = artist->getReleases({cluster.getId()});
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
EXPECT_EQ(releases.front()->getId(), release.getId());
|
||||
releases = artist->getReleases(Range {}, {cluster.getId()});
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,13 +605,13 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto releases {artist->getReleases()};
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
EXPECT_EQ(releases.front()->getId(), release.getId());
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = artist->getReleases({cluster1.getId(), cluster2.getId()});
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
EXPECT_EQ(releases.front()->getId(), release.getId());
|
||||
releases = artist->getReleases(Range {}, {cluster1.getId(), cluster2.getId()});
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,8 +50,9 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
EXPECT_EQ(artist->getReleaseCount(), 1);
|
||||
ASSERT_EQ(artist->getReleases().size(), 1);
|
||||
EXPECT_EQ(artist->getReleases().front()->getId(), release.getId());
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
EXPECT_EQ(release->getTracks().size(), nbTracks);
|
||||
}
|
||||
@@ -73,9 +74,9 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
auto releases {artist->getReleases()};
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
EXPECT_EQ(releases.front()->getId(), release.getId());
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
EXPECT_EQ(artist->getReleaseCount(), 1);
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace SOM
|
||||
class DataNormalizer
|
||||
{
|
||||
public:
|
||||
|
||||
struct MinMax
|
||||
{
|
||||
InputVector::value_type min;
|
||||
|
||||
@@ -163,7 +163,6 @@ class InputVector
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
friend class InputVector operator-(const InputVector& a, const InputVector& b)
|
||||
{
|
||||
if (!a.hasSameDimension(b.getNbDimensions()))
|
||||
|
||||
@@ -53,7 +53,6 @@ template <typename T>
|
||||
class Matrix
|
||||
{
|
||||
public:
|
||||
|
||||
Matrix() = default;
|
||||
|
||||
Matrix(Coordinate width, Coordinate height)
|
||||
@@ -109,7 +108,6 @@ class Matrix
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
Coordinate _width {};
|
||||
Coordinate _height {};
|
||||
std::vector<T> _values;
|
||||
|
||||
@@ -40,7 +40,6 @@ std::ostream& operator<<(std::ostream& os, const InputVector& a);
|
||||
class Network
|
||||
{
|
||||
public:
|
||||
|
||||
// Init a network with random values
|
||||
Network(Coordinate width, Coordinate height, std::size_t inputDimCount);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <atomic>
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <Wt/WLocalDateTime.h>
|
||||
@@ -885,9 +886,12 @@ handleGetArtistRequest(RequestContext& context)
|
||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||
Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)};
|
||||
|
||||
auto releases {artist->getReleases()};
|
||||
for (const Release::pointer& release : releases)
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
for (const ReleaseId releaseId : releases.results)
|
||||
{
|
||||
const Release::pointer release {Release::find(context.dbSession, releaseId)};
|
||||
artistNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, true /* id3 */));
|
||||
}
|
||||
|
||||
response.addNode("artist", std::move(artistNode));
|
||||
|
||||
@@ -997,9 +1001,12 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
|
||||
directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName()));
|
||||
|
||||
auto releases {artist->getReleases()};
|
||||
for (const Release::pointer& release : releases)
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
for (const ReleaseId releaseId : releases.results)
|
||||
{
|
||||
const Release::pointer release {Release::find(context.dbSession, releaseId)};
|
||||
directoryNode.addArrayChild("child", releaseToResponseNode(release, context.dbSession, user, false /* no id3 */));
|
||||
}
|
||||
}
|
||||
else if (releaseId)
|
||||
{
|
||||
@@ -1065,46 +1072,30 @@ handleGetArtistsRequestCommon(RequestContext& context, bool id3)
|
||||
break;
|
||||
}
|
||||
|
||||
Response::Node* currentIndexNode {};
|
||||
char currentIndex{};
|
||||
|
||||
Response::Node* unknownIndexNode {};
|
||||
auto getOrCreateUnknownIndexNode {[&]
|
||||
{
|
||||
if (!unknownIndexNode)
|
||||
{
|
||||
unknownIndexNode = &artistsNode.createArrayChild("index");
|
||||
unknownIndexNode->setAttribute("name", "?");
|
||||
}
|
||||
|
||||
return unknownIndexNode;
|
||||
}};
|
||||
|
||||
auto getOrCreateIndexNode {[&](char first)
|
||||
{
|
||||
if (!currentIndexNode || currentIndex != first)
|
||||
{
|
||||
currentIndexNode = &artistsNode.createArrayChild("index");
|
||||
currentIndexNode->setAttribute("name", std::string {first});
|
||||
currentIndex = first;
|
||||
}
|
||||
|
||||
return currentIndexNode;
|
||||
}};
|
||||
|
||||
std::map<char, std::vector<Artist::pointer>> artistsSortedByFirstChar;
|
||||
const RangeResults<ArtistId> artists {Artist::find(context.dbSession, parameters)};
|
||||
for (const ArtistId artistId : artists.results)
|
||||
{
|
||||
const Artist::pointer artist {Artist::find(context.dbSession, artistId)};
|
||||
const std::string& sortName {artist->getSortName()};
|
||||
|
||||
Response::Node* indexNode{};
|
||||
char sortChar;
|
||||
if (sortName.empty() || !std::isalpha(sortName[0]))
|
||||
indexNode = getOrCreateUnknownIndexNode();
|
||||
sortChar = '?';
|
||||
else
|
||||
indexNode = getOrCreateIndexNode(std::toupper(sortName[0]));
|
||||
sortChar = std::toupper(sortName[0]);
|
||||
|
||||
indexNode->addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||
artistsSortedByFirstChar[sortChar].push_back(artist);
|
||||
}
|
||||
|
||||
|
||||
for (const auto& [sortChar, artists] : artistsSortedByFirstChar)
|
||||
{
|
||||
Response::Node& indexNode {artistsNode.createArrayChild("index")};
|
||||
indexNode.setAttribute("name", std::string {sortChar});
|
||||
|
||||
for (const Artist::pointer& artist :artists)
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -203,7 +203,6 @@ class Response
|
||||
void addArrayChild(const std::string& key, Node node);
|
||||
|
||||
private:
|
||||
|
||||
void setVersionAttribute(ProtocolVersion version);
|
||||
|
||||
friend class Response;
|
||||
|
||||
@@ -28,7 +28,6 @@ class FileResourceHandler final : public IResourceHandler
|
||||
FileResourceHandler(const std::filesystem::path& filePath);
|
||||
|
||||
private:
|
||||
|
||||
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||
|
||||
static constexpr std::size_t _chunkSize {65536};
|
||||
|
||||
@@ -32,15 +32,13 @@ namespace Http
|
||||
{
|
||||
public:
|
||||
Client(boost::asio::io_context& ioContext, std::string_view baseUrl)
|
||||
: _ioContext {ioContext}
|
||||
, _sendQueue {ioContext, baseUrl}
|
||||
: _sendQueue {ioContext, baseUrl}
|
||||
{}
|
||||
|
||||
private:
|
||||
void sendGETRequest(ClientGETRequestParameters&& request) override;
|
||||
void sendPOSTRequest(ClientPOSTRequestParameters&& request) override;
|
||||
|
||||
boost::asio::io_context& _ioContext;
|
||||
SendQueue _sendQueue;
|
||||
};
|
||||
} // namespace Http
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
class IConfig
|
||||
{
|
||||
public:
|
||||
|
||||
virtual ~IConfig() = default;
|
||||
|
||||
// Default values are returned in case of setting not found
|
||||
|
||||
@@ -31,34 +31,43 @@
|
||||
|
||||
namespace StringUtils {
|
||||
|
||||
[[nodiscard]]
|
||||
std::vector<std::string>
|
||||
splitStringCopy(std::string_view string, std::string_view separators);
|
||||
|
||||
[[nodiscard]]
|
||||
std::vector<std::string_view>
|
||||
splitString(std::string_view string, std::string_view separators);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
stringToLower(std::string_view str);
|
||||
|
||||
void
|
||||
stringToLower(std::string& str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
stringToUpper(const std::string& str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
bufferToString(const std::vector<unsigned char>& data);
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]]
|
||||
std::optional<T> readAs(std::string_view str)
|
||||
{
|
||||
T res;
|
||||
@@ -72,21 +81,27 @@ std::optional<T> readAs(std::string_view str)
|
||||
}
|
||||
|
||||
template<>
|
||||
[[nodiscard]]
|
||||
std::optional<std::string>
|
||||
readAs(std::string_view str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
replaceInString(const std::string& str, const std::string& from, const std::string& to);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
jsEscape(const std::string& str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
|
||||
|
||||
[[nodiscard]]
|
||||
bool
|
||||
stringEndsWith(const std::string& str, const std::string& ending);
|
||||
|
||||
[[nodiscard]]
|
||||
std::optional<std::string>
|
||||
stringFromHex(const std::string& str);
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
class UUID
|
||||
{
|
||||
public:
|
||||
|
||||
static std::optional<UUID> fromString(std::string_view str);
|
||||
|
||||
std::string_view getAsString() const { return _value; }
|
||||
|
||||
@@ -40,7 +40,6 @@ namespace Zip
|
||||
class Zipper
|
||||
{
|
||||
public:
|
||||
|
||||
Zipper(const std::map<std::string, std::filesystem::path>& files, const Wt::WDateTime& lastModifiedTime = {});
|
||||
|
||||
static constexpr SizeType minOutputBufferSize {64};
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
TEST(StringUtils, splitString)
|
||||
{
|
||||
{
|
||||
const std::string test{"a"};
|
||||
const std::string test {"a"};
|
||||
|
||||
const std::vector<std::string_view> strings {StringUtils::splitString(test, "")};
|
||||
ASSERT_EQ(strings.size(), 1);
|
||||
@@ -32,7 +32,7 @@ TEST(StringUtils, splitString)
|
||||
}
|
||||
|
||||
{
|
||||
const std::string test{"a b"};
|
||||
const std::string test {"a b"};
|
||||
|
||||
const std::vector<std::string_view> strings {StringUtils::splitString(test, "|")};
|
||||
ASSERT_EQ(strings.size(), 1);
|
||||
@@ -40,7 +40,7 @@ TEST(StringUtils, splitString)
|
||||
}
|
||||
|
||||
{
|
||||
const std::string test{" a"};
|
||||
const std::string test {" a"};
|
||||
|
||||
const std::vector<std::string_view> strings {StringUtils::splitString(test, " ")};
|
||||
ASSERT_EQ(strings.size(), 1);
|
||||
@@ -48,7 +48,7 @@ TEST(StringUtils, splitString)
|
||||
}
|
||||
|
||||
{
|
||||
const std::string test{"a "};
|
||||
const std::string test {"a "};
|
||||
|
||||
const std::vector<std::string_view> strings {StringUtils::splitString(test, " ")};
|
||||
ASSERT_EQ(strings.size(), 1);
|
||||
@@ -56,7 +56,7 @@ TEST(StringUtils, splitString)
|
||||
}
|
||||
|
||||
{
|
||||
const std::string test{"a b"};
|
||||
const std::string test {"a b"};
|
||||
|
||||
const std::vector<std::string_view> strings {StringUtils::splitString(test, " ")};
|
||||
ASSERT_EQ(strings.size(), 2);
|
||||
@@ -65,7 +65,7 @@ TEST(StringUtils, splitString)
|
||||
}
|
||||
|
||||
{
|
||||
const std::string test{"a b,c|defgh "};
|
||||
const std::string test {"a b,c|defgh "};
|
||||
|
||||
const std::vector<std::string_view> strings {StringUtils::splitString(test, " ,|")};
|
||||
ASSERT_EQ(strings.size(), 4);
|
||||
@@ -76,6 +76,28 @@ TEST(StringUtils, splitString)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(StringUtils, splitStringCopy)
|
||||
{
|
||||
{
|
||||
const std::string test {"test=foo"};
|
||||
|
||||
const std::vector<std::string> strings {StringUtils::splitStringCopy(test, "=")};
|
||||
ASSERT_EQ(strings.size(), 2);
|
||||
EXPECT_EQ(strings[0], "test");
|
||||
EXPECT_EQ(strings[1], "foo");
|
||||
}
|
||||
|
||||
{
|
||||
const std::string test {"test=foo bar"};
|
||||
|
||||
const std::vector<std::string> strings {StringUtils::splitStringCopy(test, "=")};
|
||||
ASSERT_EQ(strings.size(), 2);
|
||||
EXPECT_EQ(strings[0], "test");
|
||||
EXPECT_EQ(strings[1], "foo bar");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(StringUtils, escapeString)
|
||||
{
|
||||
EXPECT_EQ(StringUtils::escapeString("", "*", ' '), "");
|
||||
|
||||
@@ -6,20 +6,24 @@ add_executable(lms
|
||||
ui/LmsApplicationManager.cpp
|
||||
ui/LmsTheme.cpp
|
||||
ui/MediaPlayer.cpp
|
||||
ui/ModalManager.cpp
|
||||
ui/NotificationContainer.cpp
|
||||
ui/PlayQueue.cpp
|
||||
ui/SettingsView.cpp
|
||||
ui/TrackStringUtils.cpp
|
||||
ui/Utils.cpp
|
||||
ui/admin/DatabaseSettingsView.cpp
|
||||
ui/admin/ScannerController.cpp
|
||||
ui/admin/InitWizardView.cpp
|
||||
ui/admin/UserView.cpp
|
||||
ui/admin/UsersView.cpp
|
||||
ui/common/DirectoryValidator.cpp
|
||||
ui/common/DoubleValidator.cpp
|
||||
ui/common/InfiniteScrollingContainer.cpp
|
||||
ui/common/LoadingIndicator.cpp
|
||||
ui/common/LoginNameValidator.cpp
|
||||
ui/common/MandatoryValidator.cpp
|
||||
ui/common/PasswordValidator.cpp
|
||||
ui/common/Template.cpp
|
||||
ui/common/UUIDValidator.cpp
|
||||
ui/explore/ArtistCollector.cpp
|
||||
ui/explore/ArtistListHelpers.cpp
|
||||
@@ -30,13 +34,11 @@ add_executable(lms
|
||||
ui/explore/Filters.cpp
|
||||
ui/explore/ReleaseCollector.cpp
|
||||
ui/explore/ReleaseListHelpers.cpp
|
||||
ui/explore/ReleasePopup.cpp
|
||||
ui/explore/ReleasesView.cpp
|
||||
ui/explore/ReleaseView.cpp
|
||||
ui/explore/SearchView.cpp
|
||||
ui/explore/TrackCollector.cpp
|
||||
ui/explore/TrackListHelpers.cpp
|
||||
ui/explore/TrackPopup.cpp
|
||||
ui/explore/TracksView.cpp
|
||||
ui/resource/AudioFileResource.cpp
|
||||
ui/resource/AudioTranscodeResource.cpp
|
||||
|
||||
@@ -86,7 +86,6 @@ processAuthToken(const Wt::WEnvironment& env)
|
||||
class AuthModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
|
||||
// Associate each field with a unique string literal.
|
||||
static const Field LoginNameField;
|
||||
static const Field PasswordField;
|
||||
@@ -160,7 +159,6 @@ class AuthModel : public Wt::WFormModel
|
||||
std::optional<Database::UserId> getUserId() const { return _userId; }
|
||||
|
||||
private:
|
||||
|
||||
std::optional<Database::UserId> _userId;
|
||||
};
|
||||
|
||||
@@ -168,7 +166,6 @@ const AuthModel::Field AuthModel::LoginNameField {"login-name"};
|
||||
const AuthModel::Field AuthModel::PasswordField {"password"};
|
||||
const AuthModel::Field AuthModel::RememberMeField {"remember-me"};
|
||||
|
||||
|
||||
Auth::Auth()
|
||||
: Wt::WTemplateFormView {Wt::WString::tr("Lms.Auth.template")}
|
||||
{
|
||||
@@ -217,7 +214,6 @@ Auth::Auth()
|
||||
loginBtn->clicked().connect(this, processAuth);
|
||||
|
||||
updateView(model.get());
|
||||
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
+38
-111
@@ -22,11 +22,9 @@
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WEnvironment.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WPopupMenu.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WServer.h>
|
||||
#include <Wt/WStackedWidget.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "services/auth/IEnvService.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
@@ -57,6 +55,8 @@
|
||||
#include "LmsApplicationManager.hpp"
|
||||
#include "LmsTheme.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "ModalManager.hpp"
|
||||
#include "NotificationContainer.hpp"
|
||||
#include "PlayQueue.hpp"
|
||||
#include "SettingsView.hpp"
|
||||
|
||||
@@ -162,7 +162,12 @@ LmsApplication::~LmsApplication() = default;
|
||||
void
|
||||
LmsApplication::init()
|
||||
{
|
||||
setTheme(std::make_shared<LmsTheme>());
|
||||
|
||||
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
||||
require("js/mediaplayer.js");
|
||||
|
||||
setTitle("LMS");
|
||||
|
||||
// Add a resource bundle
|
||||
messageResourceBundle().use(appRoot() + "admin-database");
|
||||
@@ -175,24 +180,18 @@ LmsApplication::init()
|
||||
messageResourceBundle().use(appRoot() + "error");
|
||||
messageResourceBundle().use(appRoot() + "explore");
|
||||
messageResourceBundle().use(appRoot() + "login");
|
||||
messageResourceBundle().use(appRoot() + "main");
|
||||
messageResourceBundle().use(appRoot() + "mediaplayer");
|
||||
messageResourceBundle().use(appRoot() + "messages");
|
||||
messageResourceBundle().use(appRoot() + "misc");
|
||||
messageResourceBundle().use(appRoot() + "notifications");
|
||||
messageResourceBundle().use(appRoot() + "playqueue");
|
||||
messageResourceBundle().use(appRoot() + "release");
|
||||
messageResourceBundle().use(appRoot() + "releases");
|
||||
messageResourceBundle().use(appRoot() + "search");
|
||||
messageResourceBundle().use(appRoot() + "settings");
|
||||
messageResourceBundle().use(appRoot() + "templates");
|
||||
messageResourceBundle().use(appRoot() + "tracks");
|
||||
|
||||
// Require js here to avoid async problems
|
||||
requireJQuery("js/jquery-1.10.2.min.js");
|
||||
require("js/bootstrap-notify.js");
|
||||
require("js/bootstrap.min.js");
|
||||
require("js/mediaplayer.js");
|
||||
|
||||
setTitle("LMS");
|
||||
|
||||
// Handle Media Scanner events and other session events
|
||||
enableUpdates(true);
|
||||
|
||||
@@ -218,8 +217,6 @@ LmsApplication::processPasswordAuth()
|
||||
}
|
||||
}
|
||||
|
||||
setTheme();
|
||||
|
||||
// If here is no account in the database, launch the first connection wizard
|
||||
bool firstConnection {};
|
||||
{
|
||||
@@ -244,19 +241,6 @@ LmsApplication::processPasswordAuth()
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::setTheme()
|
||||
{
|
||||
Database::UITheme theme {Database::User::defaultUITheme};
|
||||
{
|
||||
auto transaction {getDbSession().createSharedTransaction()};
|
||||
if (const auto user {getUser()})
|
||||
theme = user->getUITheme();
|
||||
}
|
||||
|
||||
WApplication::setTheme(std::make_unique<LmsTheme>(theme));
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::finalize()
|
||||
{
|
||||
@@ -306,7 +290,6 @@ LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool add
|
||||
|
||||
if (addText)
|
||||
{
|
||||
res->setWordWrap(false);
|
||||
res->setTextFormat(Wt::TextFormat::Plain);
|
||||
res->setText(Wt::WString::fromUTF8(release->getName()));
|
||||
res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
|
||||
@@ -315,40 +298,6 @@ LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool add
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WText>
|
||||
LmsApplication::createCluster(Database::Cluster::pointer cluster, bool canDelete)
|
||||
{
|
||||
auto getStyleClass = [](const Database::Cluster::pointer cluster)
|
||||
{
|
||||
switch (cluster->getType()->getId().getValue() % 6)
|
||||
{
|
||||
case 0: return "label-info";
|
||||
case 1: return "label-warning";
|
||||
case 2: return "label-primary";
|
||||
case 3: return "label-default";
|
||||
case 4: return "label-success";
|
||||
case 5: return "label-danger";
|
||||
}
|
||||
return "label-default";
|
||||
};
|
||||
|
||||
const std::string styleClass {getStyleClass(cluster)};
|
||||
auto res {std::make_unique<Wt::WText>(std::string {} + (canDelete ? "<i class=\"fa fa-times-circle\"></i> " : "") + Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::UnsafeXHTML)};
|
||||
|
||||
res->setStyleClass("Lms-cluster label " + styleClass);
|
||||
res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain);
|
||||
res->setInline(true);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Wt::WPopupMenu*
|
||||
LmsApplication::createPopupMenu()
|
||||
{
|
||||
_popupMenu = std::make_unique<Wt::WPopupMenu>();
|
||||
return _popupMenu.get();
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::handleException(LmsApplicationException& e)
|
||||
{
|
||||
@@ -407,8 +356,7 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
|
||||
|
||||
LmsApp->doJavaScript(R"($('.navbar-nav li.active').removeClass('active'); $('.navbar-nav a[href="' + location.pathname + '"]').closest('li').addClass('active');)");
|
||||
|
||||
LmsApp->doJavaScript(R"($('.navbar-nav a.active').removeClass('active'); $('.navbar-nav a[href="' + location.pathname + '"]').closest('a').addClass('active');)");
|
||||
for (const auto& view : views)
|
||||
{
|
||||
if (wApp->internalPathMatches(view.path))
|
||||
@@ -439,7 +387,6 @@ LmsApplication::logoutUser()
|
||||
void
|
||||
LmsApplication::onUserLoggedIn()
|
||||
{
|
||||
setTheme();
|
||||
root()->clear();
|
||||
|
||||
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent();
|
||||
@@ -466,45 +413,50 @@ LmsApplication::createHome()
|
||||
_coverResource = std::make_shared<CoverResource>();
|
||||
|
||||
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
|
||||
doJavaScript("$('body').tooltip({ selector: '[data-toggle=\"tooltip\"]'})");
|
||||
|
||||
Wt::WTemplate* main {root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.template")))};
|
||||
Wt::WTemplate* main {root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.main.template")))};
|
||||
|
||||
main->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
Wt::WTemplate* navbar {main->bindNew<Wt::WTemplate>("navbar", Wt::WString::tr("Lms.main.template.navbar"))};
|
||||
navbar->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_notificationContainer = main->bindNew<NotificationContainer>("notifications");
|
||||
_modalManager = main->bindNew<ModalManager>("modal");
|
||||
|
||||
// MediaPlayer
|
||||
_mediaPlayer = main->bindNew<MediaPlayer>("player");
|
||||
|
||||
main->bindNew<Wt::WAnchor>("title", Wt::WLink {Wt::LinkType::InternalPath, defaultPath}, "LMS");
|
||||
main->bindNew<Wt::WAnchor>("artists", Wt::WLink {Wt::LinkType::InternalPath, "/artists"}, Wt::WString::tr("Lms.Explore.artists"));
|
||||
main->bindNew<Wt::WAnchor>("releases", Wt::WLink {Wt::LinkType::InternalPath, "/releases"}, Wt::WString::tr("Lms.Explore.releases"));
|
||||
main->bindNew<Wt::WAnchor>("tracks", Wt::WLink {Wt::LinkType::InternalPath, "/tracks"}, Wt::WString::tr("Lms.Explore.tracks"));
|
||||
navbar->bindNew<Wt::WAnchor>("title", Wt::WLink {Wt::LinkType::InternalPath, defaultPath}, "LMS");
|
||||
navbar->bindNew<Wt::WAnchor>("artists", Wt::WLink {Wt::LinkType::InternalPath, "/artists"}, Wt::WString::tr("Lms.Explore.artists"));
|
||||
navbar->bindNew<Wt::WAnchor>("releases", Wt::WLink {Wt::LinkType::InternalPath, "/releases"}, Wt::WString::tr("Lms.Explore.releases"));
|
||||
navbar->bindNew<Wt::WAnchor>("tracks", Wt::WLink {Wt::LinkType::InternalPath, "/tracks"}, Wt::WString::tr("Lms.Explore.tracks"));
|
||||
|
||||
Filters* filters {main->bindNew<Filters>("filters")};
|
||||
main->bindNew<Wt::WAnchor>("playqueue", Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"}, Wt::WString::tr("Lms.PlayQueue.playqueue"));
|
||||
main->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
|
||||
main->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings"));
|
||||
Filters* filters {navbar->bindNew<Filters>("filters")};
|
||||
navbar->bindNew<Wt::WAnchor>("playqueue", Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"}, Wt::WString::tr("Lms.PlayQueue.playqueue"));
|
||||
navbar->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
|
||||
navbar->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings"));
|
||||
|
||||
{
|
||||
Wt::WAnchor* logout {main->bindNew<Wt::WAnchor>("logout")};
|
||||
Wt::WAnchor* logout {navbar->bindNew<Wt::WAnchor>("logout")};
|
||||
logout->setText(Wt::WString::tr("Lms.logout"));
|
||||
logout->clicked().connect(this, &LmsApplication::logoutUser);
|
||||
}
|
||||
|
||||
Wt::WLineEdit* searchEdit {main->bindNew<Wt::WLineEdit>("search")};
|
||||
Wt::WLineEdit* searchEdit {navbar->bindNew<Wt::WLineEdit>("search")};
|
||||
searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
|
||||
|
||||
if (LmsApp->getUserType() == Database::UserType::ADMIN)
|
||||
{
|
||||
main->setCondition("if-is-admin", true);
|
||||
main->bindNew<Wt::WAnchor>("database", Wt::WLink {Wt::LinkType::InternalPath, "/admin/database"}, Wt::WString::tr("Lms.Admin.Database.menu-database"));
|
||||
main->bindNew<Wt::WAnchor>("users", Wt::WLink {Wt::LinkType::InternalPath, "/admin/users"}, Wt::WString::tr("Lms.Admin.Users.menu-users"));
|
||||
navbar->setCondition("if-is-admin", true);
|
||||
navbar->bindNew<Wt::WAnchor>("database", Wt::WLink {Wt::LinkType::InternalPath, "/admin/database"}, Wt::WString::tr("Lms.Admin.Database.menu-database"));
|
||||
navbar->bindNew<Wt::WAnchor>("users", Wt::WLink {Wt::LinkType::InternalPath, "/admin/users"}, Wt::WString::tr("Lms.Admin.Users.menu-users"));
|
||||
}
|
||||
|
||||
// Contents
|
||||
// Order is important in mainStack, see IdxRoot!
|
||||
Wt::WStackedWidget* mainStack {main->bindNew<Wt::WStackedWidget>("contents")};
|
||||
mainStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
|
||||
mainStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
|
||||
|
||||
Explore* explore {mainStack->addNew<Explore>(filters)};
|
||||
_playQueue = mainStack->addNew<PlayQueue>();
|
||||
@@ -578,7 +530,9 @@ LmsApplication::createHome()
|
||||
{
|
||||
_scannerEvents.scanComplete.connect([=] (const Scanner::ScanStats& stats)
|
||||
{
|
||||
notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-complete")
|
||||
notifyMsg(Notification::Type::Info,
|
||||
Wt::WString::tr("Lms.Admin.Database.database"),
|
||||
Wt::WString::tr("Lms.Admin.Database.scan-complete")
|
||||
.arg(static_cast<unsigned>(stats.nbFiles()))
|
||||
.arg(static_cast<unsigned>(stats.additions))
|
||||
.arg(static_cast<unsigned>(stats.updates))
|
||||
@@ -615,46 +569,19 @@ LmsApplication::notify(const Wt::WEvent& event)
|
||||
}
|
||||
}
|
||||
|
||||
static std::string msgTypeToString(LmsApplication::MsgType type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case LmsApplication::MsgType::Success: return "success";
|
||||
case LmsApplication::MsgType::Info: return "info";
|
||||
case LmsApplication::MsgType::Warning: return "warning";
|
||||
case LmsApplication::MsgType::Danger: return "danger";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::post(std::function<void()> func)
|
||||
{
|
||||
Wt::WServer::instance()->post(LmsApp->sessionId(), std::move(func));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LmsApplication::notifyMsg(MsgType type, const Wt::WString& message, std::chrono::milliseconds duration)
|
||||
LmsApplication::notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
||||
{
|
||||
LMS_LOG(UI, INFO) << "Notifying message '" << message.toUTF8() << "' of type '" << msgTypeToString(type) << "'";
|
||||
|
||||
std::ostringstream oss;
|
||||
|
||||
oss << "$.notify({"
|
||||
"message: '" << StringUtils::jsEscape(message.toUTF8()) << "'"
|
||||
"},{"
|
||||
"type: '" << msgTypeToString(type) << "',"
|
||||
"placement: {from: 'bottom', align: 'right'},"
|
||||
"timer: 250,"
|
||||
"offset: {x: 20, y: 80},"
|
||||
"delay: " << duration.count() << ""
|
||||
"});";
|
||||
|
||||
LmsApp->doJavaScript(oss.str());
|
||||
LMS_LOG(UI, INFO) << "Notifying message '" << message.toUTF8() << "' for category '" << category.toUTF8() << "'";
|
||||
_notificationContainer->add(type, category, message, duration);
|
||||
}
|
||||
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "services/database/UserId.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "services/scanner/ScannerEvents.hpp"
|
||||
#include "Notification.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -37,10 +38,6 @@ namespace Database
|
||||
class Session;
|
||||
class User;
|
||||
}
|
||||
namespace Wt
|
||||
{
|
||||
class WPopupMenu;
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
@@ -49,11 +46,12 @@ class LmsApplicationException;
|
||||
class MediaPlayer;
|
||||
class PlayQueue;
|
||||
class LmsApplicationManager;
|
||||
class NotificationContainer;
|
||||
class ModalManager;
|
||||
|
||||
class LmsApplication : public Wt::WApplication
|
||||
{
|
||||
public:
|
||||
|
||||
LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager, std::optional<Database::UserId> userId = std::nullopt);
|
||||
~LmsApplication();
|
||||
|
||||
@@ -78,31 +76,22 @@ class LmsApplication : public Wt::WApplication
|
||||
void post(std::function<void()> func);
|
||||
|
||||
// Used to classify the message sent to the user
|
||||
enum class MsgType
|
||||
{
|
||||
Success,
|
||||
Info,
|
||||
Warning,
|
||||
Danger,
|
||||
};
|
||||
void notifyMsg(MsgType type, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {4000});
|
||||
void notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {5000});
|
||||
|
||||
static Wt::WLink createArtistLink(Database::ObjectPtr<Database::Artist> artist);
|
||||
static std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::ObjectPtr<Database::Artist> artist, bool addText = true);
|
||||
static Wt::WLink createReleaseLink(Database::ObjectPtr<Database::Release> release);
|
||||
static std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Database::ObjectPtr<Database::Release> release, bool addText = true);
|
||||
static std::unique_ptr<Wt::WText> createCluster(Database::ObjectPtr<Database::Cluster> cluster, bool canDelete = false);
|
||||
Wt::WPopupMenu* createPopupMenu();
|
||||
|
||||
MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; }
|
||||
PlayQueue& getPlayQueue() const { return *_playQueue; }
|
||||
ModalManager& getModalManager() const { return *_modalManager; }
|
||||
|
||||
// Signal emitted just before the session ends (user may already be logged out)
|
||||
Wt::Signal<>& preQuit() { return _preQuit; }
|
||||
|
||||
private:
|
||||
void init();
|
||||
void setTheme();
|
||||
void processPasswordAuth();
|
||||
void handleException(LmsApplicationException& e);
|
||||
void goHomeAndQuit();
|
||||
@@ -129,7 +118,8 @@ class LmsApplication : public Wt::WApplication
|
||||
std::shared_ptr<CoverResource> _coverResource;
|
||||
MediaPlayer* _mediaPlayer {};
|
||||
PlayQueue* _playQueue {};
|
||||
std::unique_ptr<Wt::WPopupMenu> _popupMenu;
|
||||
NotificationContainer* _notificationContainer {};
|
||||
ModalManager* _modalManager {};
|
||||
};
|
||||
|
||||
|
||||
|
||||
+50
-77
@@ -1,89 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
* Copyright (C) 2020 Emweb bv, Herent, Belgium.
|
||||
*
|
||||
* 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/>.
|
||||
* See the LICENSE file for terms of use.
|
||||
*/
|
||||
|
||||
#include "LmsTheme.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include <Wt/WApplication.h>
|
||||
#include <Wt/WLinkedCssStyleSheet.h>
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
LmsTheme::LmsTheme(Database::UITheme theme)
|
||||
: _theme {theme}
|
||||
namespace UserInterface
|
||||
{
|
||||
setVersion(Wt::BootstrapVersion::v3);
|
||||
setResponsive(true); // Should be not necessary since applied via head-matter
|
||||
}
|
||||
|
||||
void
|
||||
LmsTheme::setTheme(Database::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)
|
||||
LmsApp->removeStyleSheet(*it);
|
||||
|
||||
_theme = theme;
|
||||
for (const auto& styleSheet : getStyleSheets(theme))
|
||||
LmsApp->useStyleSheet(styleSheet);
|
||||
}
|
||||
|
||||
std::vector<Wt::WLinkedCssStyleSheet>
|
||||
LmsTheme::styleSheets() const
|
||||
{
|
||||
const std::vector<Wt::WLink> styleSheets {getStyleSheets(_theme)};
|
||||
std::vector<Wt::WLinkedCssStyleSheet> res;
|
||||
res.reserve(styleSheets.size());
|
||||
|
||||
std::transform(std::cbegin(styleSheets), std::cend(styleSheets), std::back_inserter(res), [](const Wt::WLink& styleSheet) { return Wt::WLinkedCssStyleSheet {styleSheet}; });
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::WLink>
|
||||
LmsTheme::getStyleSheets(Database::UITheme theme)
|
||||
{
|
||||
switch (theme)
|
||||
void
|
||||
LmsTheme::init(Wt::WApplication* app) const
|
||||
{
|
||||
case Database::UITheme::Dark:
|
||||
return
|
||||
{
|
||||
{"css/fonts.css"},
|
||||
{"css/bootstrap-darkly.min.css"},
|
||||
{"resources/themes/bootstrap/3/wt.css"},
|
||||
{"css/lms.css"},
|
||||
{"css/lms-darkly.css"},
|
||||
};
|
||||
|
||||
case Database::UITheme::Light:
|
||||
return
|
||||
{
|
||||
{"css/fonts.css"},
|
||||
{"css/bootstrap-flatly.min.css"},
|
||||
{"resources/themes/bootstrap/3/wt.css"},
|
||||
{"css/lms.css"},
|
||||
{"css/lms-flatly.css"},
|
||||
};
|
||||
app->require("js/bootstrap.bundle.min.js");
|
||||
Wt::WString v = app->metaHeader(Wt::MetaHeaderType::Meta, "viewport");
|
||||
if (v.empty())
|
||||
app->addMetaHeader("viewport", "width=device-width, initial-scale=1, user-scalable=no");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
std::string
|
||||
LmsTheme::name() const
|
||||
{
|
||||
return "bootstrap5";
|
||||
}
|
||||
|
||||
std::string
|
||||
LmsTheme::resourcesUrl() const
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<Wt::WLinkedCssStyleSheet>
|
||||
LmsTheme::styleSheets() const
|
||||
{
|
||||
static const std::vector<Wt::WLinkedCssStyleSheet> files
|
||||
{
|
||||
{"css/bootstrap.solar.min.css"}, // TODO parametrize this
|
||||
{"css/lms.css"},
|
||||
};
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
void LmsTheme::applyValidationStyle(Wt::WWidget* widget, const Wt::WValidator::Result& validation, Wt::WFlags<Wt::ValidationStyleFlag> styles) const
|
||||
{
|
||||
{
|
||||
const bool validStyle {(validation.state() == Wt::ValidationState::Valid) && styles.test(Wt::ValidationStyleFlag::ValidStyle)};
|
||||
widget->toggleStyleClass("is-valid", validStyle);
|
||||
}
|
||||
|
||||
{
|
||||
const bool invalidStyle {(validation.state() != Wt::ValidationState::Valid) && styles.test(Wt::ValidationStyleFlag::InvalidStyle)};
|
||||
widget->toggleStyleClass("is-invalid", invalidStyle);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
+17
-16
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
* Copyright (C) 2022 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,26 +19,27 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WBootstrapTheme.h>
|
||||
#include <Wt/WLinkedCssStyleSheet.h>
|
||||
|
||||
#include "services/database/Types.hpp"
|
||||
#include <Wt/WTheme.h>
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
class LmsTheme : public Wt::WBootstrapTheme
|
||||
class LmsTheme : public Wt::WTheme
|
||||
{
|
||||
public:
|
||||
LmsTheme(Database::UITheme theme);
|
||||
|
||||
void setTheme(Database::UITheme theme);
|
||||
|
||||
private:
|
||||
std::vector<Wt::WLinkedCssStyleSheet> styleSheets() const override;
|
||||
static std::vector<Wt::WLink> getStyleSheets(Database::UITheme theme);
|
||||
void init(Wt::WApplication *app) const override;
|
||||
|
||||
Database::UITheme _theme;
|
||||
std::string name() const override;
|
||||
std::string resourcesUrl() const override;
|
||||
std::vector<Wt::WLinkedCssStyleSheet> styleSheets() const override;
|
||||
void apply(Wt::WWidget*, Wt::WWidget*, int) const override {};
|
||||
void apply(Wt::WWidget*, Wt::DomElement&, int) const override {};
|
||||
std::string disabledClass() const override { return "disabled"; }
|
||||
std::string activeClass() const override { return "active"; };
|
||||
std::string utilityCssClass(int) const override { return ""; };
|
||||
bool canStyleAnchorAsButton() const override { return true; };
|
||||
void applyValidationStyle(Wt::WWidget* widget,
|
||||
const Wt::WValidator::Result& validation,
|
||||
Wt::WFlags<Wt::ValidationStyleFlag> flags) const override;
|
||||
bool canBorderBoxElement(const Wt::DomElement&) const override { return true; }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 "ModalManager.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
ModalManager::ModalManager()
|
||||
: _closed {this, "closed"}
|
||||
{
|
||||
_closed.connect([=](const std::string& id)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Received closed for id '" << id << "'";
|
||||
for (int i {}; i < count(); ++i)
|
||||
{
|
||||
Wt::WWidget* widget {this->widget(i)};
|
||||
LMS_LOG(UI, DEBUG) << "Widget " << i << ", id = '" << widget->id();
|
||||
if (widget->id() == id)
|
||||
{
|
||||
removeWidget(widget);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
ModalManager::show(std::unique_ptr<Wt::WWidget> modalWidget)
|
||||
{
|
||||
assert(count() == 0);
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Want to show, id = " << modalWidget->id();
|
||||
|
||||
std::ostringstream oss;
|
||||
oss
|
||||
<< R"({const modalElement = )" << jsRef() << R"(.getElementsByClassName('modal')[0];)"
|
||||
<< R"(const modal = bootstrap.Modal.getOrCreateInstance(modalElement);)"
|
||||
<< R"(modal.show();)"
|
||||
<< R"(modalElement.addEventListener('hidden.bs.modal', function () {)"
|
||||
<< _closed.createCall({"'" + modalWidget->id() + "'"})
|
||||
<< R"(modal.dispose();)"
|
||||
<< R"(});})";
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Running JS '" << oss.str() << "'";
|
||||
|
||||
doJavaScript(oss.str());
|
||||
|
||||
addWidget(move(modalWidget));
|
||||
}
|
||||
|
||||
void
|
||||
ModalManager::dispose(Wt::WWidget* modalWidget)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss
|
||||
<< R"({const modalElementParent = document.getElementById(')" << modalWidget->id() << R"(');)"
|
||||
<< R"(const modalElement = modalElementParent.getElementsByClassName('modal')[0];)"
|
||||
<< R"(const modal = bootstrap.Modal.getInstance(modalElement);)"
|
||||
<< R"(modal.hide();)"
|
||||
<< R"(})";
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Running JS '" << oss.str() << "'";
|
||||
|
||||
doJavaScript(oss.str());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
* Copyright (C) 2022 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -17,21 +17,25 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TrackStringUtils.hpp"
|
||||
#pragma once
|
||||
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WSignal.h>
|
||||
#include <Wt/WWidget.h>
|
||||
|
||||
std::string
|
||||
durationToString(std::chrono::milliseconds msDuration)
|
||||
namespace UserInterface
|
||||
{
|
||||
const std::chrono::seconds duration {std::chrono::duration_cast<std::chrono::seconds>(msDuration)};
|
||||
class ModalManager : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
ModalManager();
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << (duration.count() / 60)
|
||||
<< ":"
|
||||
<< std::setfill('0') << std::setw(2) << duration.count() % 60;
|
||||
// should handle only one modal at a time
|
||||
// Widget must contain a "modal" element
|
||||
void show(std::unique_ptr<Wt::WWidget> modalWidget);
|
||||
void dispose(Wt::WWidget* modalWidget);
|
||||
|
||||
return oss.str();
|
||||
private:
|
||||
Wt::JSignal<std::string> _closed;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
namespace UserInterface::Notification
|
||||
{
|
||||
enum class Type
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Danger,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 "NotificationContainer.hpp"
|
||||
|
||||
#include <sstream>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
class NotificationWidget : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
NotificationWidget(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration);
|
||||
Wt::JSignal<> closed {this, "closed"};
|
||||
};
|
||||
|
||||
NotificationWidget::NotificationWidget(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.notifications.template.entry")}
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case Notification::Type::Info:
|
||||
bindString("bg-color", "bg-primary");
|
||||
bindString("text-color", "white");
|
||||
break;
|
||||
case Notification::Type::Warning:
|
||||
bindString("bg-color", "bg-warning");
|
||||
bindString("text-color", "dark");
|
||||
break;
|
||||
case Notification::Type::Danger:
|
||||
bindString("bg-color", "bg-danger");
|
||||
bindString("text-color", "white");
|
||||
break;
|
||||
}
|
||||
|
||||
bindString("category", category);
|
||||
bindString("message", message);
|
||||
bindInt("duration", duration.count());
|
||||
|
||||
std::ostringstream oss;
|
||||
|
||||
oss
|
||||
<< R"({const toastElement = )" << jsRef() << R"(.getElementsByClassName('toast')[0];)"
|
||||
<< R"(const toast = bootstrap.Toast.getOrCreateInstance(toastElement);)"
|
||||
<< R"(toast.show();)"
|
||||
<< R"(toastElement.addEventListener('hidden.bs.toast', function () {)"
|
||||
<< closed.createCall({})
|
||||
<< R"(toast.dispose();)"
|
||||
<< R"(});})";
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Running JS '" << oss.str() << "'";
|
||||
|
||||
doJavaScript(oss.str());
|
||||
}
|
||||
|
||||
void
|
||||
NotificationContainer::add(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration)
|
||||
{
|
||||
NotificationWidget* notification {addNew<NotificationWidget>(type, category, message, duration)};
|
||||
|
||||
notification->closed.connect([=]
|
||||
{
|
||||
removeWidget(notification);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
* Copyright (C) 2022 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,16 +19,19 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WWidget.h>
|
||||
#include <Wt/WSignal.h>
|
||||
#include <chrono>
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WString.h>
|
||||
|
||||
#include "services/database/Types.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
#include "Notification.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
void displayTrackPopupMenu(Wt::WInteractWidget& target,
|
||||
Database::TrackId trackId,
|
||||
PlayQueueActionTrackSignal& tracksAction);
|
||||
} // namespace UserInterface
|
||||
class NotificationContainer : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
void add(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration);
|
||||
|
||||
private:
|
||||
};
|
||||
} // namespace UserInterface
|
||||
+62
-95
@@ -19,8 +19,8 @@
|
||||
|
||||
#include "PlayQueue.hpp"
|
||||
|
||||
#include <Wt/WPopupMenu.h>
|
||||
#include <Wt/WText.h>
|
||||
#include <Wt/WCheckBox.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
@@ -36,34 +36,20 @@
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "common/InfiniteScrollingContainer.hpp"
|
||||
#include "resource/CoverResource.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "TrackStringUtils.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
PlayQueue::PlayQueue()
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.PlayQueue.template"))
|
||||
: Template {Wt::WString::tr("Lms.PlayQueue.template")}
|
||||
{
|
||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
_repeatAll = LmsApp->getUser()->isRepeatAllSet();
|
||||
_radioMode = LmsApp->getUser()->isRadioSet();
|
||||
}
|
||||
|
||||
auto setToolTip = [](Wt::WWidget& widget, Wt::WString title)
|
||||
{
|
||||
widget.setAttributeValue("data-toggle", "tooltip");
|
||||
widget.setAttributeValue("data-placement", "bottom");
|
||||
widget.setAttributeValue("title", std::move(title));
|
||||
};
|
||||
|
||||
Wt::WText* clearBtn = bindNew<Wt::WText>("clear-btn", Wt::WString::tr("Lms.PlayQueue.template.clear-btn"), Wt::TextFormat::XHTML);
|
||||
setToolTip(*clearBtn, Wt::WString::tr("Lms.PlayQueue.clear"));
|
||||
clearBtn->clicked().connect([=]
|
||||
{
|
||||
clearTracks();
|
||||
@@ -77,7 +63,6 @@ PlayQueue::PlayQueue()
|
||||
});
|
||||
|
||||
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"));
|
||||
shuffleBtn->clicked().connect([=]
|
||||
{
|
||||
{
|
||||
@@ -95,33 +80,33 @@ PlayQueue::PlayQueue()
|
||||
addSome();
|
||||
});
|
||||
|
||||
_repeatBtn = bindNew<Wt::WText>("repeat-btn", Wt::WString::tr("Lms.PlayQueue.template.repeat-btn"), Wt::TextFormat::XHTML);
|
||||
setToolTip(*_repeatBtn, Wt::WString::tr("Lms.PlayQueue.repeat"));
|
||||
_repeatBtn = bindNew<Wt::WCheckBox>("repeat-btn");
|
||||
_repeatBtn->clicked().connect([=]
|
||||
{
|
||||
_repeatAll = !_repeatAll;
|
||||
updateRepeatBtn();
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
if (!LmsApp->getUser()->isDemo())
|
||||
LmsApp->getUser().modify()->setRepeatAll(_repeatAll);
|
||||
LmsApp->getUser().modify()->setRepeatAll(isRepeatAllSet());
|
||||
});
|
||||
updateRepeatBtn();
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
if (LmsApp->getUser()->isRepeatAllSet())
|
||||
_repeatBtn->setCheckState(Wt::CheckState::Checked);
|
||||
}
|
||||
|
||||
_radioBtn = bindNew<Wt::WText>("radio-btn", Wt::WString::tr("Lms.PlayQueue.template.radio-btn"));
|
||||
setToolTip(*_radioBtn, Wt::WString::tr("Lms.PlayQueue.radio-mode"));
|
||||
_radioBtn = bindNew<Wt::WCheckBox>("radio-btn");
|
||||
_radioBtn->clicked().connect([=]
|
||||
{
|
||||
_radioMode = !_radioMode;
|
||||
updateRadioBtn();
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
if (!LmsApp->getUser()->isDemo())
|
||||
LmsApp->getUser().modify()->setRadio(_radioMode);
|
||||
LmsApp->getUser().modify()->setRadio(isRadioModeSet());
|
||||
});
|
||||
updateRadioBtn();
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
if (LmsApp->getUser()->isRadioSet())
|
||||
_radioBtn->setCheckState(Wt::CheckState::Checked);
|
||||
}
|
||||
|
||||
_nbTracks = bindNew<Wt::WText>("nb-tracks");
|
||||
|
||||
@@ -180,18 +165,16 @@ PlayQueue::PlayQueue()
|
||||
addSome();
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::updateRepeatBtn()
|
||||
bool
|
||||
PlayQueue::isRepeatAllSet() const
|
||||
{
|
||||
_repeatBtn->toggleStyleClass("text-success", _repeatAll);
|
||||
_repeatBtn->toggleStyleClass("text-muted", !_repeatAll);
|
||||
return _repeatBtn->checkState() == Wt::CheckState::Checked;
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::updateRadioBtn()
|
||||
bool
|
||||
PlayQueue::isRadioModeSet() const
|
||||
{
|
||||
_radioBtn->toggleStyleClass("text-success",_radioMode);
|
||||
_radioBtn->toggleStyleClass("text-muted", !_radioMode);
|
||||
return _radioBtn->checkState() == Wt::CheckState::Checked;
|
||||
}
|
||||
|
||||
Database::TrackList::pointer
|
||||
@@ -243,7 +226,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
|
||||
// If out of range, stop playing
|
||||
if (pos >= tracklist->getCount())
|
||||
{
|
||||
if (!_repeatAll || tracklist->getCount() == 0)
|
||||
if (!isRepeatAllSet() || tracklist->getCount() == 0)
|
||||
{
|
||||
stop();
|
||||
return;
|
||||
@@ -253,7 +236,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
|
||||
}
|
||||
|
||||
// If last and radio mode, fill the next song
|
||||
if (_radioMode && pos == tracklist->getCount() - 1)
|
||||
if (isRadioModeSet() && pos == tracklist->getCount() - 1)
|
||||
addRadioTrack = true;
|
||||
|
||||
_trackPos = pos;
|
||||
@@ -313,9 +296,11 @@ PlayQueue::updateCurrentTrack(bool selected)
|
||||
if (!_trackPos || *_trackPos >= static_cast<std::size_t>(_entriesContainer->getCount()))
|
||||
return;
|
||||
|
||||
Wt::WTemplate* entry {static_cast<Wt::WTemplate*>(_entriesContainer->getWidget(*_trackPos))};
|
||||
if (entry)
|
||||
entry->bindString("is-selected", selected ? "Lms-playqueue-selected" : "");
|
||||
Template* entry {static_cast<Template*>(_entriesContainer->getWidget(*_trackPos))};
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
entry->toggleStyleClass("Lms-entry-playing", selected);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
@@ -383,12 +368,10 @@ PlayQueue::processTracks(PlayQueueAction action, const std::vector<Database::Tra
|
||||
}
|
||||
|
||||
if (nbAddedTracks > 0)
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Info, Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::milliseconds(2000));
|
||||
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.PlayQueue.playqueue"), Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::milliseconds(2000));
|
||||
|
||||
if (isFull())
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Warning, Wt::WString::tr("Lms.PlayQueue.playqueue-full"), std::chrono::milliseconds(2000));
|
||||
|
||||
|
||||
LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.PlayQueue.playqueue"), Wt::WString::tr("Lms.PlayQueue.playqueue-full"), std::chrono::milliseconds(2000));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -412,50 +395,30 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
|
||||
const auto track {tracklistEntry->getTrack()};
|
||||
const Database::TrackId trackId {track->getId()};
|
||||
|
||||
Wt::WTemplate* entry = _entriesContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.PlayQueue.template.entry"));
|
||||
Template* entry {_entriesContainer->addNew<Template>(Wt::WString::tr("Lms.PlayQueue.template.entry"))};
|
||||
entry->addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
|
||||
entry->bindString("is-selected", "");
|
||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
||||
|
||||
const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
|
||||
const auto release {track->getRelease()};
|
||||
|
||||
if (!artists.empty() || release)
|
||||
entry->setCondition("if-has-artists-or-release", true);
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
entry->setCondition("if-has-artists", true);
|
||||
|
||||
Wt::WContainerWidget* artistContainer {entry->bindNew<Wt::WContainerWidget>("artists")};
|
||||
for (const auto& artist : artists)
|
||||
{
|
||||
Wt::WTemplate* a {artistContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.PlayQueue.template.entry-artist"))};
|
||||
a->bindWidget("artist", LmsApplication::createArtistAnchor(artist));
|
||||
}
|
||||
}
|
||||
if (release)
|
||||
{
|
||||
entry->setCondition("if-has-release", true);
|
||||
entry->bindWidget("release", LmsApplication::createReleaseAnchor(release));
|
||||
{
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(release->getId(), CoverResource::Size::Large));
|
||||
cover->setStyleClass("Lms-cover");
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
|
||||
Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))};
|
||||
auto cover {Utils::createCover(release->getId(), CoverResource::Size::Small)};
|
||||
cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK
|
||||
anchor->setImage(std::move(cover));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||
cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(track->getId(), CoverResource::Size::Large));
|
||||
cover->setStyleClass("Lms-cover");
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
|
||||
auto cover {entry->bindWidget<Wt::WImage>("cover", Utils::createCover(track->getId(), CoverResource::Size::Small))};
|
||||
cover->addStyleClass("Lms-cover-track");
|
||||
}
|
||||
|
||||
entry->bindString("duration", durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||
|
||||
Wt::WText* playBtn {entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.PlayQueue.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||
playBtn->clicked().connect([=]
|
||||
@@ -488,25 +451,29 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
|
||||
updateInfo();
|
||||
});
|
||||
|
||||
Wt::WText* moreBtn {entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.PlayQueue.template.more-btn"), Wt::TextFormat::XHTML)};
|
||||
moreBtn->clicked().connect([=]
|
||||
entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.PlayQueue.template.more-btn"), Wt::TextFormat::XHTML);
|
||||
|
||||
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
|
||||
|
||||
Wt::WPushButton* starBtn {entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))};
|
||||
starBtn->clicked().connect([=]
|
||||
{
|
||||
Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
const bool isStarred {Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), trackId)};
|
||||
popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star"))
|
||||
->triggered().connect(moreBtn, [=]
|
||||
{
|
||||
if (isStarred)
|
||||
Service<Scrobbling::IScrobblingService>::get()->unstar(LmsApp->getUserId(), trackId);
|
||||
else
|
||||
Service<Scrobbling::IScrobblingService>::get()->star(LmsApp->getUserId(), trackId);
|
||||
});
|
||||
popup->addItem(Wt::WString::tr("Lms.Explore.download"))
|
||||
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
|
||||
|
||||
popup->popup(moreBtn);
|
||||
if (isStarred())
|
||||
{
|
||||
Service<Scrobbling::IScrobblingService>::get()->unstar(LmsApp->getUserId(), trackId);
|
||||
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
|
||||
}
|
||||
else
|
||||
{
|
||||
Service<Scrobbling::IScrobblingService>::get()->star(LmsApp->getUserId(), trackId);
|
||||
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
||||
}
|
||||
});
|
||||
|
||||
entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <Wt/WCheckBox.h>
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
#include <Wt/WText.h>
|
||||
@@ -29,6 +30,8 @@
|
||||
#include "services/database/TrackListId.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
|
||||
#include "common/Template.hpp"
|
||||
|
||||
namespace Similarity
|
||||
{
|
||||
class Finder;
|
||||
@@ -45,7 +48,7 @@ namespace UserInterface {
|
||||
|
||||
class InfiniteScrollingContainer;
|
||||
|
||||
class PlayQueue : public Wt::WTemplate
|
||||
class PlayQueue : public Template
|
||||
{
|
||||
public:
|
||||
PlayQueue();
|
||||
@@ -75,8 +78,8 @@ class PlayQueue : public Wt::WTemplate
|
||||
void enqueueRadioTracks();
|
||||
void updateInfo();
|
||||
void updateCurrentTrack(bool selected);
|
||||
void updateRepeatBtn();
|
||||
void updateRadioBtn();
|
||||
bool isRepeatAllSet() const;
|
||||
bool isRadioModeSet() const;
|
||||
|
||||
void loadTrack(std::size_t pos, bool play);
|
||||
void stop();
|
||||
@@ -88,14 +91,12 @@ class PlayQueue : public Wt::WTemplate
|
||||
static inline constexpr std::size_t _nbMaxEntries {1000};
|
||||
static inline constexpr std::size_t _batchSize {12};
|
||||
|
||||
bool _repeatAll {};
|
||||
bool _radioMode {};
|
||||
bool _mediaPlayerSettingsLoaded {};
|
||||
Database::TrackListId _tracklistId {};
|
||||
InfiniteScrollingContainer* _entriesContainer {};
|
||||
Wt::WText* _nbTracks {};
|
||||
Wt::WText* _repeatBtn {};
|
||||
Wt::WText* _radioBtn {};
|
||||
Wt::WCheckBox* _repeatBtn {};
|
||||
Wt::WCheckBox* _radioBtn {};
|
||||
std::optional<std::size_t> _trackPos; // current track position, if set
|
||||
};
|
||||
|
||||
|
||||
+10
-32
@@ -21,7 +21,6 @@
|
||||
|
||||
#include <Wt/WCheckBox.h>
|
||||
#include <Wt/WComboBox.h>
|
||||
#include <Wt/WDoubleValidator.h>
|
||||
#include <Wt/WDoubleSpinBox.h>
|
||||
#include <Wt/WFormModel.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
@@ -29,8 +28,9 @@
|
||||
#include <Wt/WString.h>
|
||||
#include <Wt/WTemplateFormView.h>
|
||||
|
||||
#include "common/PasswordValidator.hpp"
|
||||
#include "common/DoubleValidator.hpp"
|
||||
#include "common/MandatoryValidator.hpp"
|
||||
#include "common/PasswordValidator.hpp"
|
||||
#include "common/UUIDValidator.hpp"
|
||||
#include "common/ValueStringModel.hpp"
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "LmsTheme.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
@@ -53,7 +52,6 @@ class SettingsModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
// Associate each field with a unique string literal.
|
||||
static inline const Field DarkModeField {"dark-mode"};
|
||||
static inline const Field TranscodeModeField {"transcode-mode"};
|
||||
static inline const Field TranscodeFormatField {"transcode-format"};
|
||||
static inline const Field TranscodeBitrateField {"transcode-bitrate"};
|
||||
@@ -80,7 +78,6 @@ class SettingsModel : public Wt::WFormModel
|
||||
{
|
||||
initializeModels();
|
||||
|
||||
addField(DarkModeField);
|
||||
addField(TranscodeModeField);
|
||||
addField(TranscodeBitrateField);
|
||||
addField(TranscodeFormatField);
|
||||
@@ -111,12 +108,10 @@ class SettingsModel : public Wt::WFormModel
|
||||
setValidator(TranscodeBitrateField, createMandatoryValidator());
|
||||
setValidator(TranscodeFormatField, createMandatoryValidator());
|
||||
setValidator(ReplayGainModeField, createMandatoryValidator());
|
||||
auto createPreAmpValidator = []
|
||||
auto createPreAmpValidator {[]
|
||||
{
|
||||
auto preampGainValidator {std::make_unique<Wt::WDoubleValidator>()};
|
||||
preampGainValidator->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
|
||||
return preampGainValidator;
|
||||
};
|
||||
return createDoubleValidator(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
|
||||
}};
|
||||
|
||||
setValidator(ReplayGainPreAmpGainField, createPreAmpValidator());
|
||||
setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator());
|
||||
@@ -139,14 +134,6 @@ class SettingsModel : public Wt::WFormModel
|
||||
|
||||
User::pointer user {LmsApp->getUser()};
|
||||
|
||||
{
|
||||
const UITheme newTheme {Wt::asNumber(value(DarkModeField)) ? UITheme::Dark : UITheme::Light};
|
||||
LmsTheme* lmsTheme {static_cast<LmsTheme*>(LmsApp->theme().get())};
|
||||
lmsTheme->setTheme(newTheme);
|
||||
|
||||
user.modify()->setUITheme(newTheme);
|
||||
}
|
||||
|
||||
{
|
||||
MediaPlayer::Settings settings;
|
||||
|
||||
@@ -200,7 +187,6 @@ class SettingsModel : public Wt::WFormModel
|
||||
{
|
||||
_authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void loadData()
|
||||
@@ -209,8 +195,6 @@ class SettingsModel : public Wt::WFormModel
|
||||
|
||||
User::pointer user {LmsApp->getUser()};
|
||||
|
||||
setValue(DarkModeField, user->getUITheme() == UITheme::Dark);
|
||||
|
||||
{
|
||||
const auto settings {*LmsApp->getMediaPlayer().getSettings()};
|
||||
|
||||
@@ -408,12 +392,6 @@ SettingsView::refreshView()
|
||||
|
||||
auto model {std::make_shared<SettingsModel>(authPasswordService, !LmsApp->isUserAuthStrong())};
|
||||
|
||||
// Appearance
|
||||
{
|
||||
auto darkMode {std::make_unique<Wt::WCheckBox>()};
|
||||
t->setFormWidget(SettingsModel::DarkModeField, std::move(darkMode));
|
||||
}
|
||||
|
||||
if (authPasswordService)
|
||||
{
|
||||
t->setCondition("if-has-change-password", true);
|
||||
@@ -557,14 +535,14 @@ SettingsView::refreshView()
|
||||
Wt::WPushButton *saveBtn {t->bindWidget("apply-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.apply")))};
|
||||
Wt::WPushButton *discardBtn {t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard")))};
|
||||
|
||||
saveBtn->clicked().connect([=]()
|
||||
saveBtn->clicked().connect([=]
|
||||
{
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
if (LmsApp->getUser()->isDemo())
|
||||
{
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Warning, Wt::WString::tr("Lms.Settings.demo-cannot-save"));
|
||||
LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.Settings.settings"), Wt::WString::tr("Lms.Settings.demo-cannot-save"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -574,19 +552,19 @@ SettingsView::refreshView()
|
||||
if (model->validate())
|
||||
{
|
||||
model->saveData();
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Success, Wt::WString::tr("Lms.Settings.settings-saved"));
|
||||
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Settings.settings"), Wt::WString::tr("Lms.Settings.settings-saved"));
|
||||
}
|
||||
|
||||
// Udate the view: Delete any validation message in the view, etc.
|
||||
t->updateView(model.get());
|
||||
});
|
||||
|
||||
discardBtn->clicked().connect(std::bind([=] ()
|
||||
discardBtn->clicked().connect([=]
|
||||
{
|
||||
model->loadData();
|
||||
model->validate();
|
||||
t->updateView(model.get());
|
||||
}));
|
||||
});
|
||||
|
||||
t->updateView(model.get());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 "Utils.hpp"
|
||||
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface::Utils
|
||||
{
|
||||
|
||||
std::string
|
||||
durationToString(std::chrono::milliseconds msDuration)
|
||||
{
|
||||
const std::chrono::seconds duration {std::chrono::duration_cast<std::chrono::seconds>(msDuration)};
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << (duration.count() / 60)
|
||||
<< ":"
|
||||
<< std::setfill('0') << std::setw(2) << duration.count() % 60;
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<Wt::WImage>
|
||||
createCover(Database::ReleaseId releaseId, CoverResource::Size size)
|
||||
{
|
||||
auto cover {std::make_unique<Wt::WImage>()};
|
||||
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(releaseId, size));
|
||||
cover->setStyleClass("Lms-cover img-fluid"); // HACK
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
|
||||
return cover;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WImage>
|
||||
createCover(Database::TrackId trackId, CoverResource::Size size)
|
||||
{
|
||||
auto cover {std::make_unique<Wt::WImage>()};
|
||||
cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(trackId, size));
|
||||
cover->setStyleClass("Lms-cover img-fluid"); // HACK
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
|
||||
return cover;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WInteractWidget>
|
||||
createCluster(Database::ClusterId clusterId, bool canDelete)
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Cluster::pointer cluster {Database::Cluster::find(LmsApp->getDbSession(), clusterId)};
|
||||
if (!cluster)
|
||||
return {};
|
||||
|
||||
auto getStyleClass {[](const Database::Cluster::pointer& cluster) -> const char*
|
||||
{
|
||||
switch (cluster->getType()->getId().getValue() % 8)
|
||||
{
|
||||
case 0: return "bg-primary";
|
||||
case 1: return "bg-secondary";
|
||||
case 2: return "bg-success";
|
||||
case 3: return "bg-danger";
|
||||
case 4: return "bg-warning text-dark";
|
||||
case 5: return "bg-info text-dark";
|
||||
case 6: return "bg-light text-dark";
|
||||
case 7: return "bg-dark";
|
||||
}
|
||||
|
||||
return "bg-primary";
|
||||
}};
|
||||
|
||||
const std::string styleClass {getStyleClass(cluster)};
|
||||
auto res {std::make_unique<Wt::WText>(std::string {} + (canDelete ? "<i class=\"fa fa-times-circle\"></i> " : "") + Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::UnsafeXHTML)};
|
||||
|
||||
res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK
|
||||
res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain);
|
||||
res->setInline(true);
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 <chrono>
|
||||
#include <string>
|
||||
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WInteractWidget.h>
|
||||
|
||||
#include "services/database/ClusterId.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/TrackId.hpp"
|
||||
|
||||
#include "resource/CoverResource.hpp"
|
||||
|
||||
namespace UserInterface::Utils
|
||||
{
|
||||
std::string
|
||||
durationToString(std::chrono::milliseconds msDuration);
|
||||
|
||||
std::unique_ptr<Wt::WImage> createCover(Database::ReleaseId releaseId, CoverResource::Size size);
|
||||
std::unique_ptr<Wt::WImage> createCover(Database::TrackId trackId, CoverResource::Size size);
|
||||
|
||||
std::unique_ptr<Wt::WInteractWidget> createCluster(Database::ClusterId clusterId, bool canDelete = false);
|
||||
}
|
||||
@@ -69,12 +69,11 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
|
||||
auto dirValidator {createDirectoryValidator()};
|
||||
dirValidator->setMandatory(true);
|
||||
setValidator(MediaDirectoryField, dirValidator);
|
||||
setValidator(MediaDirectoryField, move(dirValidator));
|
||||
|
||||
setValidator(UpdatePeriodField, createMandatoryValidator());
|
||||
setValidator(UpdateStartTimeField, createMandatoryValidator());
|
||||
setValidator(RecommendationEngineTypeField, createMandatoryValidator());
|
||||
setValidator(TagsField, createTagsValidator());
|
||||
|
||||
// populate the model with initial data
|
||||
loadData();
|
||||
@@ -144,12 +143,6 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
}
|
||||
|
||||
private:
|
||||
static std::shared_ptr<Wt::WValidator> createTagsValidator()
|
||||
{
|
||||
auto v = std::make_shared<Wt::WValidator>();
|
||||
return v;
|
||||
}
|
||||
|
||||
void initializeModels()
|
||||
{
|
||||
_updatePeriodModel = std::make_shared<ValueStringModel<ScanSettings::UpdatePeriod>>();
|
||||
@@ -241,7 +234,7 @@ DatabaseSettingsView::refreshView()
|
||||
model->saveData();
|
||||
|
||||
Service<Scanner::IScannerService>::get()->requestImmediateScan(false);
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved"));
|
||||
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Admin.Database.database"), Wt::WString::tr("Lms.Admin.Database.settings-saved"));
|
||||
}
|
||||
|
||||
// Udate the view: Delete any validation message in the view, etc.
|
||||
|
||||
@@ -41,7 +41,6 @@ namespace UserInterface {
|
||||
class InitWizardModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
|
||||
// Associate each field with a unique string literal.
|
||||
static inline const Field AdminLoginField {"admin-login"};
|
||||
static inline const Field PasswordField {"password"};
|
||||
@@ -126,6 +125,11 @@ InitWizardView::InitWizardView()
|
||||
setFormWidget(InitWizardModel::PasswordConfirmField, std::move(passwordConfirmEdit));
|
||||
}
|
||||
|
||||
// Result notification
|
||||
Wt::WText* resultNotification {bindNew<Wt::WText>("info")};
|
||||
resultNotification->setHidden(true);
|
||||
|
||||
|
||||
Wt::WPushButton* saveButton = bindNew<Wt::WPushButton>("create-btn", Wt::WString::tr("Lms.create"));
|
||||
saveButton->clicked().connect([=]
|
||||
{
|
||||
@@ -134,7 +138,8 @@ InitWizardView::InitWizardView()
|
||||
if (model->validate())
|
||||
{
|
||||
model->saveData();
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Success, Wt::WString::tr("Lms.Admin.InitWizard.done"));
|
||||
resultNotification->setText(Wt::WString::tr("Lms.Admin.InitWizard.done"));
|
||||
resultNotification->setHidden(false);
|
||||
saveButton->setEnabled(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ class InitWizardView : public Wt::WTemplateFormView
|
||||
{
|
||||
public:
|
||||
InitWizardView();
|
||||
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -23,10 +23,8 @@
|
||||
|
||||
#include <Wt/Http/Response.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/WPopupMenu.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WResource.h>
|
||||
#include <Wt/WSplitButton.h>
|
||||
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
@@ -57,9 +55,7 @@ durationToString(const Wt::WDateTime& begin, const Wt::WDateTime& end)
|
||||
class ReportResource : public Wt::WResource
|
||||
{
|
||||
public:
|
||||
|
||||
ReportResource(const Scanner::ScanStats& stats)
|
||||
: _stats {stats}
|
||||
ReportResource()
|
||||
{
|
||||
suggestFileName("report.txt");
|
||||
}
|
||||
@@ -69,11 +65,22 @@ class ReportResource : public Wt::WResource
|
||||
beingDeleted();
|
||||
}
|
||||
|
||||
void setScanStats(const Scanner::ScanStats& stats)
|
||||
{
|
||||
if (!_stats)
|
||||
_stats = std::make_unique<Scanner::ScanStats>();
|
||||
|
||||
*_stats = stats;
|
||||
}
|
||||
|
||||
void handleRequest(const Wt::Http::Request&, Wt::Http::Response& response)
|
||||
{
|
||||
response.out() << Wt::WString::tr("Lms.Admin.ScannerController.errors-header").arg(_stats.errors.size()).toUTF8() << std::endl;
|
||||
if (!_stats)
|
||||
return;
|
||||
|
||||
for (const auto& error : _stats.errors)
|
||||
response.out() << Wt::WString::tr("Lms.Admin.ScannerController.errors-header").arg(_stats->errors.size()).toUTF8() << std::endl;
|
||||
|
||||
for (const auto& error : _stats->errors)
|
||||
{
|
||||
response.out() << error.file.string() << " - " << errorTypeToWString(error.error).toUTF8();
|
||||
if (!error.systemError.empty())
|
||||
@@ -83,12 +90,12 @@ class ReportResource : public Wt::WResource
|
||||
|
||||
response.out() << std::endl;
|
||||
|
||||
response.out() << Wt::WString::tr("Lms.Admin.ScannerController.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;
|
||||
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
for (const auto& duplicate : _stats.duplicates)
|
||||
for (const auto& duplicate : _stats->duplicates)
|
||||
{
|
||||
const auto& track {Database::Track::find(LmsApp->getDbSession(), duplicate.trackId)};
|
||||
if (!track)
|
||||
@@ -127,7 +134,7 @@ class ReportResource : public Wt::WResource
|
||||
return "?";
|
||||
}
|
||||
|
||||
Scanner::ScanStats _stats;
|
||||
std::unique_ptr<Scanner::ScanStats> _stats;
|
||||
};
|
||||
|
||||
|
||||
@@ -135,14 +142,48 @@ ScannerController::ScannerController()
|
||||
: WTemplate {Wt::WString::tr("Lms.Admin.ScannerController.template")}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
|
||||
using namespace Scanner;
|
||||
|
||||
auto onDbEvent = [&]() { refreshContents(); };
|
||||
{
|
||||
_reportBtn = bindNew<Wt::WPushButton>("report-btn", Wt::WString::tr("Lms.Admin.ScannerController.get-report"));
|
||||
|
||||
auto reportResource {std::make_shared<ReportResource>()};
|
||||
reportResource->setTakesUpdateLock(true);
|
||||
_reportResource = reportResource.get();
|
||||
|
||||
Wt::WLink link {reportResource};
|
||||
link.setTarget(Wt::LinkTarget::NewWindow);
|
||||
_reportBtn->setLink(link);
|
||||
}
|
||||
|
||||
Wt::WPushButton* scanBtn {bindNew<Wt::WPushButton>("scan-btn", Wt::WString::tr("Lms.Admin.ScannerController.scan-now"))};
|
||||
scanBtn->clicked().connect([]
|
||||
{
|
||||
Service<Scanner::IScannerService>::get()->requestImmediateScan(false);
|
||||
});
|
||||
|
||||
Wt::WPushButton* fullScanBtn {bindNew<Wt::WPushButton>("full-scan-btn", Wt::WString::tr("Lms.Admin.ScannerController.force-scan-now"))};
|
||||
fullScanBtn->clicked().connect([]
|
||||
{
|
||||
Service<Scanner::IScannerService>::get()->requestImmediateScan(true);
|
||||
});
|
||||
|
||||
_lastScanStatus = bindNew<Wt::WLineEdit>("last-scan");
|
||||
_lastScanStatus->setReadOnly(true);
|
||||
|
||||
_status = bindNew<Wt::WLineEdit>("status");
|
||||
_status->setReadOnly(true);
|
||||
|
||||
_stepStatus = bindNew<Wt::WLineEdit>("step-status");
|
||||
_stepStatus->setReadOnly(true);
|
||||
|
||||
auto onDbEvent {[&]() { refreshContents(); }};
|
||||
|
||||
LmsApp->getScannerEvents().scanStarted.connect(this, []
|
||||
{
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched"));
|
||||
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Admin.Database.database"), Wt::WString::tr("Lms.Admin.Database.scan-launched"));
|
||||
});
|
||||
LmsApp->getScannerEvents().scanComplete.connect(this, onDbEvent);
|
||||
LmsApp->getScannerEvents().scanInProgress.connect(this, onDbEvent);
|
||||
@@ -156,30 +197,10 @@ ScannerController::refreshContents()
|
||||
{
|
||||
using namespace Scanner;
|
||||
|
||||
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([]
|
||||
{
|
||||
Service<Scanner::IScannerService>::get()->requestImmediateScan(false);
|
||||
});
|
||||
|
||||
auto popup = std::make_unique<Wt::WPopupMenu>();
|
||||
popup->addItem(Wt::WString::tr("Lms.Admin.ScannerController.force-scan-now"));
|
||||
popup->itemSelected().connect([]
|
||||
{
|
||||
Service<Scanner::IScannerService>::get()->requestImmediateScan(true);
|
||||
});
|
||||
actionBtn->dropDownButton()->setMenu(std::move(popup));
|
||||
actionBtn->dropDownButton()->addStyleClass("btn-primary");
|
||||
|
||||
|
||||
const IScannerService::Status status {Service<IScannerService>::get()->getStatus()};
|
||||
if (status.lastCompleteScanStats)
|
||||
{
|
||||
bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-status")
|
||||
_lastScanStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.last-scan-status")
|
||||
.arg(status.lastCompleteScanStats->nbFiles())
|
||||
.arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime))
|
||||
.arg(status.lastCompleteScanStats->stopTime.toString())
|
||||
@@ -187,66 +208,65 @@ ScannerController::refreshContents()
|
||||
.arg(status.lastCompleteScanStats->duplicates.size())
|
||||
);
|
||||
|
||||
Wt::WLink link {std::make_shared<ReportResource>(*status.lastCompleteScanStats)};
|
||||
link.setTarget(Wt::LinkTarget::NewWindow);
|
||||
reportBtn->setLink(link);
|
||||
_reportResource->setScanStats(*status.lastCompleteScanStats);
|
||||
_reportBtn->setEnabled(true);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-not-available"));
|
||||
reportBtn->setEnabled(false);
|
||||
_lastScanStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.last-scan-not-available"));
|
||||
_reportBtn->setEnabled(false);
|
||||
}
|
||||
|
||||
|
||||
switch (status.currentState)
|
||||
{
|
||||
case IScannerService::State::NotScheduled:
|
||||
bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-not-scheduled"));
|
||||
bindEmpty("step-status");
|
||||
_status->setText(Wt::WString::tr("Lms.Admin.ScannerController.status-not-scheduled"));
|
||||
_stepStatus->setText("");
|
||||
break;
|
||||
case IScannerService::State::Scheduled:
|
||||
bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-scheduled")
|
||||
_status->setText(Wt::WString::tr("Lms.Admin.ScannerController.status-scheduled")
|
||||
.arg(status.nextScheduledScan.toString()));
|
||||
bindEmpty("step-status");
|
||||
_stepStatus->setText("");
|
||||
break;
|
||||
case IScannerService::State::InProgress:
|
||||
bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress")
|
||||
_status->setText(Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress")
|
||||
.arg(static_cast<int>(status.currentScanStepStats->currentStep) + 1)
|
||||
.arg(Scanner::ScanProgressStepCount));
|
||||
|
||||
switch (status.currentScanStepStats->currentStep)
|
||||
{
|
||||
case Scanner::ScanProgressStep::ChekingForMissingFiles:
|
||||
bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-missing-files")
|
||||
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-missing-files")
|
||||
.arg(status.currentScanStepStats->progress()));
|
||||
break;
|
||||
|
||||
case Scanner::ScanProgressStep::DiscoveringFiles:
|
||||
bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-discovering-files")
|
||||
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-discovering-files")
|
||||
.arg(status.currentScanStepStats->processedElems));
|
||||
break;
|
||||
|
||||
case Scanner::ScanProgressStep::ScanningFiles:
|
||||
bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-files")
|
||||
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-scanning-files")
|
||||
.arg(status.currentScanStepStats->processedElems)
|
||||
.arg(status.currentScanStepStats->totalElems)
|
||||
.arg(status.currentScanStepStats->progress()));
|
||||
break;
|
||||
|
||||
case Scanner::ScanProgressStep::FetchingTrackFeatures:
|
||||
bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-fetching-track-features")
|
||||
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-fetching-track-features")
|
||||
.arg(status.currentScanStepStats->processedElems)
|
||||
.arg(status.currentScanStepStats->totalElems)
|
||||
.arg(status.currentScanStepStats->progress()));
|
||||
break;
|
||||
case Scanner::ScanProgressStep::ReloadingSimilarityEngine:
|
||||
bindString("step-status", Wt::WString::tr("Lms.Admin.ScannerController.step-reloading-similarity-engine")
|
||||
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-reloading-similarity-engine")
|
||||
.arg(status.currentScanStepStats->progress()));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
@@ -31,6 +33,12 @@ namespace UserInterface
|
||||
|
||||
private:
|
||||
void refreshContents();
|
||||
|
||||
Wt::WPushButton* _reportBtn;
|
||||
Wt::WLineEdit* _lastScanStatus;
|
||||
Wt::WLineEdit* _status;
|
||||
Wt::WLineEdit* _stepStatus;
|
||||
class ReportResource* _reportResource;
|
||||
};
|
||||
|
||||
} // namespace DatabaseStatus
|
||||
|
||||
@@ -47,7 +47,6 @@ using namespace Database;
|
||||
|
||||
class UserModel : public Wt::WFormModel
|
||||
{
|
||||
|
||||
public:
|
||||
static inline const Field LoginField {"login"};
|
||||
static inline const Field PasswordField {"password"};
|
||||
@@ -252,7 +251,9 @@ UserView::refreshView()
|
||||
if (model->validate())
|
||||
{
|
||||
model->saveData();
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Success, Wt::WString::tr(userId ? "Lms.Admin.User.user-updated" : "Lms.Admin.User.user-created"));
|
||||
LmsApp->notifyMsg(Notification::Type::Info,
|
||||
Wt::WString::tr("Lms.Admin.Users.users"),
|
||||
Wt::WString::tr(userId ? "Lms.Admin.User.user-updated" : "Lms.Admin.User.user-created"));
|
||||
LmsApp->setInternalPath("/admin/users", true);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "ModalManager.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
@@ -101,30 +102,31 @@ UsersView::refreshView()
|
||||
Wt::WPushButton* delBtn = entry->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.Admin.Users.del"));
|
||||
delBtn->clicked().connect([=]
|
||||
{
|
||||
auto msgBox = delBtn->addChild(std::make_unique<Wt::WMessageBox>(Wt::WString::tr("Lms.Admin.Users.del-user"),
|
||||
Wt::WString::tr("Lms.Admin.Users.del-user-name"),
|
||||
Wt::Icon::Warning, Wt::StandardButton::Yes | Wt::StandardButton::No));
|
||||
auto modal {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.Users.template.delete-user"))};
|
||||
modal->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
Wt::WWidget* modalPtr {modal.get()};
|
||||
|
||||
msgBox->setModal(true);
|
||||
msgBox->buttonClicked().connect([=] (Wt::StandardButton btn)
|
||||
auto* delBtn {modal->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.Admin.Users.del"))};
|
||||
delBtn->clicked().connect([=]
|
||||
{
|
||||
if (btn == Wt::StandardButton::Yes)
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
User::pointer user {User::find(LmsApp->getDbSession(), userId)};
|
||||
if (user)
|
||||
user.remove();
|
||||
User::pointer user {User::find(LmsApp->getDbSession(), userId)};
|
||||
if (user)
|
||||
user.remove();
|
||||
|
||||
_container->removeWidget(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
delBtn->removeChild(msgBox);
|
||||
}
|
||||
_container->removeWidget(entry);
|
||||
|
||||
LmsApp->getModalManager().dispose(modalPtr);
|
||||
});
|
||||
|
||||
msgBox->show();
|
||||
auto* cancelBtn {modal->bindNew<Wt::WPushButton>("cancel-btn", Wt::WString::tr("Lms.cancel"))};
|
||||
cancelBtn->clicked().connect([=]
|
||||
{
|
||||
LmsApp->getModalManager().dispose(modalPtr);
|
||||
});
|
||||
|
||||
LmsApp->getModalManager().show(std::move(modal));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ namespace UserInterface
|
||||
{
|
||||
class DirectoryValidator : public Wt::WValidator
|
||||
{
|
||||
public:
|
||||
private:
|
||||
Wt::WValidator::Result validate(const Wt::WString& input) const override;
|
||||
std::string javaScriptValidate() const override { return {}; }
|
||||
};
|
||||
|
||||
Wt::WValidator::Result
|
||||
@@ -48,7 +49,7 @@ namespace UserInterface
|
||||
return Wt::WValidator::Result(Wt::ValidationState::Invalid, Wt::WString::tr("Lms.not-a-directory"));
|
||||
}
|
||||
|
||||
std::shared_ptr<Wt::WValidator>
|
||||
std::unique_ptr<Wt::WValidator>
|
||||
createDirectoryValidator()
|
||||
{
|
||||
return std::make_unique<DirectoryValidator>();
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
std::shared_ptr<Wt::WValidator> createDirectoryValidator();
|
||||
std::unique_ptr<Wt::WValidator> createDirectoryValidator();
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 "DoubleValidator.hpp"
|
||||
|
||||
#include <Wt/WDoubleValidator.h>
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
class DoubleValidator : public Wt::WDoubleValidator
|
||||
{
|
||||
public:
|
||||
using Wt::WDoubleValidator::WDoubleValidator;
|
||||
|
||||
private:
|
||||
std::string javaScriptValidate() const override { return {}; }
|
||||
};
|
||||
|
||||
std::unique_ptr<Wt::WValidator>
|
||||
createDoubleValidator(double min, double max)
|
||||
{
|
||||
auto validator {std::make_unique<DoubleValidator>(min, max)};
|
||||
return validator;
|
||||
}
|
||||
} // namespace UserInterface
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
* Copyright (C) 2022 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,9 +19,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <Wt/WValidator.h>
|
||||
|
||||
std::string
|
||||
durationToString(std::chrono::milliseconds msDuration);
|
||||
namespace UserInterface
|
||||
{
|
||||
std::unique_ptr<Wt::WValidator> createDoubleValidator(double min, double max);
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -24,10 +24,16 @@
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
std::shared_ptr<Wt::WValidator>
|
||||
class LengthValidator : public Wt::WLengthValidator
|
||||
{
|
||||
private:
|
||||
std::string javaScriptValidate() const override { return {}; }
|
||||
};
|
||||
|
||||
std::unique_ptr<Wt::WValidator>
|
||||
createLoginNameValidator()
|
||||
{
|
||||
auto v = std::make_unique<Wt::WLengthValidator>();
|
||||
auto v = std::make_unique<LengthValidator>();
|
||||
v->setMandatory(true);
|
||||
v->setMinimumLength(::Database::User::MinNameLength);
|
||||
v->setMaximumLength(::Database::User::MaxNameLength);
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
std::shared_ptr<Wt::WValidator> createLoginNameValidator();
|
||||
std::unique_ptr<Wt::WValidator> createLoginNameValidator();
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -21,10 +21,16 @@
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
std::shared_ptr<Wt::WValidator>
|
||||
class MandatoryValidator : public Wt::WValidator
|
||||
{
|
||||
private:
|
||||
std::string javaScriptValidate() const override { return {}; }
|
||||
};
|
||||
|
||||
std::unique_ptr<Wt::WValidator>
|
||||
createMandatoryValidator()
|
||||
{
|
||||
auto v {std::make_shared<Wt::WValidator>()};
|
||||
auto v {std::make_unique<MandatoryValidator>()};
|
||||
v->setMandatory(true);
|
||||
return v;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user