Merge branch 'develop' for release v3.29.0

This commit is contained in:
emeric
2022-05-26 14:53:51 +02:00
137 changed files with 2327 additions and 3030 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ extraction:
- libtag1-dev - libtag1-dev
- libpam0g-dev - libpam0g-dev
after_prepare: after_prepare:
- export WT_VERSION=4.5.0 - export WT_VERSION=4.7.2
- export WT_INSTALL_PREFIX=${LGTM_WORKSPACE}/wt-${WT_VERSION} - export WT_INSTALL_PREFIX=${LGTM_WORKSPACE}/wt-${WT_VERSION}
- pushd ${LGTM_WORKSPACE} - pushd ${LGTM_WORKSPACE}
- git clone https://github.com/emweb/wt.git ${LGTM_WORKSPACE}/wt - git clone https://github.com/emweb/wt.git ${LGTM_WORKSPACE}/wt
@@ -28,7 +28,7 @@ extraction:
- popd - popd
configure: configure:
command: command:
- export WT_VERSION=4.5.0 - export WT_VERSION=4.7.2
- export WT_INSTALL_PREFIX=${LGTM_WORKSPACE}/wt-${WT_VERSION} - export WT_INSTALL_PREFIX=${LGTM_WORKSPACE}/wt-${WT_VERSION}
- cmake -DCMAKE_PREFIX_PATH=${WT_INSTALL_PREFIX} -DCMAKE_BUILD_TYPE=Release - cmake -DCMAKE_PREFIX_PATH=${WT_INSTALL_PREFIX} -DCMAKE_BUILD_TYPE=Release
-55
View File
@@ -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
View File
@@ -5,7 +5,7 @@ project(lms)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True) set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
if (UNIX) if (UNIX)
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--no-undefined") set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--no-undefined")
endif () endif ()
+3 -3
View File
@@ -1,5 +1,5 @@
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
FROM --platform=$BUILDPLATFORM alpine:3.14 FROM --platform=$BUILDPLATFORM alpine:3.16
ARG BUILD_PACKAGES="\ ARG BUILD_PACKAGES="\
clang \ clang \
@@ -30,7 +30,7 @@ RUN xx-apk add --no-scripts --no-cache ${LMS_BUILD_PACKAGES}
ARG PREFIX="/tmp/install" ARG PREFIX="/tmp/install"
# STB # STB
ARG STB_VERSION=b42009b3b9d4ca35bc703f5310eedc74f584be58 ARG STB_VERSION=af1a5bc352164740c1cc1354942b1c6b72eacb8a
RUN \ RUN \
DIR=/tmp/stb && mkdir -p ${DIR} && cd ${DIR} && \ DIR=/tmp/stb && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://github.com/nothings/stb/archive/${STB_VERSION}.tar.gz && \ 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} && \ 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) && \ VERBOSE=1 make -j$(nproc) && \
xx-verify src/lms/lms && \ xx-verify src/lms/lms && \
xx-info is-cross || make test (xx-info is-cross || make test)
+8 -3
View File
@@ -1,4 +1,4 @@
FROM alpine:3.14 AS build FROM alpine:3.16 AS build
WORKDIR /tmp/workdir WORKDIR /tmp/workdir
@@ -77,7 +77,7 @@ RUN \
make distclean make distclean
# WT # WT
ARG WT_VERSION=4.5.0 ARG WT_VERSION=4.7.2
RUN \ RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://github.com/emweb/wt/archive/${WT_VERSION}.tar.gz && \ 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/doc && \
rm -rf /tmp/fakeroot/share/man 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 ## Release Stage
FROM alpine:3.14 AS release FROM alpine:3.16 AS release
LABEL maintainer="Emeric Poupon <itmfr@yahoo.fr>" LABEL maintainer="Emeric Poupon <itmfr@yahoo.fr>"
ARG RUNTIME_PACKAGES=" \ ARG RUNTIME_PACKAGES=" \
+16 -45
View File
@@ -1,6 +1,6 @@
- [Installation](#installation) - [Installation](#installation)
* [Docker](#docker) * [Docker](#docker)
* [Debian Buster packages](#debian-buster-packages) * [Debian Bullseye packages](#debian-bullseye-packages)
* [From source](#from-source) * [From source](#from-source)
+ [Build dependencies](#build-dependencies) + [Build dependencies](#build-dependencies)
+ [Build](#build) + [Build](#build)
@@ -12,33 +12,25 @@
* [Deploy on non root path](#deploy-on-non-root-path) * [Deploy on non root path](#deploy-on-non-root-path)
* [Reverse proxy settings](#reverse-proxy-settings) * [Reverse proxy settings](#reverse-proxy-settings)
- [Run](#run) - [Run](#run)
# Installation # Installation
## Docker ## Docker
_Docker_ images are available, please see detailed instructions on https://hub.docker.com/r/epoupon/lms. _Docker_ images are available, please see detailed instructions on https://hub.docker.com/r/epoupon/lms.
## Debian Bullseye packages
## Debian Buster packages _Bullseye_ packages are provided for _amd64_ and _armhf_ architectures.
_Buster_ packages are provided for _amd64_ and _armhf_ architectures.
As root, trust the following debian package provider and add it in your list of repositories: As root, trust the following debian package provider and add it in your list of repositories:
```sh ```sh
wget -O - https://debian.poupon.dev/apt/debian/epoupon.gpg.key | apt-key add - 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_: To install or upgrade _LMS_:
```sh ```sh
apt update apt update
apt install lms apt install lms
``` ```
The _lms_ service is started just after the package installation, run by a dedicated _lms_ system user.</br> 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. Please refer to [Deployment](#deployment) for further configuration options.
## From source ## 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 ### Build dependencies
__Notes__: __Notes__:
* a C++17 compiler is needed * a C++17 compiler is needed
@@ -49,12 +41,9 @@ apt-get install g++ cmake libboost-program-options-dev libboost-system-dev libav
__Notes__: __Notes__:
* libpam0g-dev is optional (only for using PAM authentication) * 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) * 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> 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). No optional requirement is needed, except openSSL if you plan not to deploy behind a reverse proxy (which is not recommended).
### Build ### Build
Get the latest stable release and build it: Get the latest stable release and build it:
```sh ```sh
git clone https://github.com/epoupon/lms.git lms git clone https://github.com/epoupon/lms.git lms
@@ -66,80 +55,69 @@ cmake .. -DCMAKE_BUILD_TYPE=Release
__Notes__: __Notes__:
* you can customize the installation directory using `-DCMAKE_INSTALL_PREFIX=path` (defaults to `/usr/local`). * 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++>` * you can customize the image library using `-DIMAGE_LIBRARY=<STB|GraphicsMagick++>`
```sh ```sh
make make
``` ```
__Note__: you can use `make -jN` to speed up compilation time (N is the number of compilation workers to spawn). __Note__: you can use `make -jN` to speed up compilation time (N is the number of compilation workers to spawn).
### Installation ### Installation
__Note__: the commands of this section require root privileges. __Note__: the commands of this section require root privileges.
```sh ```sh
make install make install
``` ```
Create a dedicated system user: Create a dedicated system user:
```sh ```sh
useradd --system --group lms useradd --system --group lms
``` ```
Copy the configuration files: Copy the configuration files:
```sh ```sh
cp /usr/share/lms/lms.conf /etc/lms.conf cp /usr/share/lms/lms.conf /etc/lms.conf
cp /usr/share/lms/lms.service /lib/systemd/system/lms.service cp /usr/share/lms/lms.service /lib/systemd/system/lms.service
``` ```
Create the working directory and give it access to the _lms_ user: Create the working directory and give it access to the _lms_ user:
```sh ```sh
mkdir /var/lms mkdir /var/lms
chown lms:lms /var/lms chown lms:lms /var/lms
``` ```
To make _LMS_ run automatically during startup: To make _LMS_ run automatically during startup:
```sh ```sh
systemctl enable lms systemctl enable lms
``` ```
### Upgrade ### Upgrade
To upgrade _LMS_ from sources, you need to update the master branch and rebuild/install it: To upgrade _LMS_ from sources, you need to update the master branch and rebuild/install it:
```sh ```sh
cd build cd build
git pull git pull
make make
``` ```
Then using root privileges: Then using root privileges:
```sh ```sh
make install make install
systemctl restart lms systemctl restart lms
``` ```
# Deployment # Deployment
__Note__: don't forget to give the _lms_ user read access to the music directory you want to scan. __Note__: don't forget to give the _lms_ user read access to the music directory you want to scan.
## Configuration ## 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, ...). _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, ...). 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)) 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 ## Authentication backend
You can define which authentication backend to be used thanks to the `authentication-backend` option: 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. * `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). * `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. * `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 __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 ## 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`. 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: 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. * 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`. * 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";` docroot = "/usr/share/lms/docroot/;/newroot/resources,/newroot/css,/newroot/images,/newroot/js,/newroot/favicon.ico";`
deploy-path = "/newroot/"; # ending slash is important 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. If you use nginx as a reverse proxy, you can simply replace `location /` with `location /newroot/` to achieve the same result.
## Reverse proxy settings ## 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. _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_: Here is an example to make _LMS_ properly work on _myserver.org_ using _nginx_:
``` ```
server { server {
@@ -183,16 +158,12 @@ server {
} }
} }
``` ```
# Run # Run
```sh ```sh
systemctl start lms systemctl start lms
``` ```
Log traces can be accessed using journactl: Log traces can be accessed using journactl:
```sh ```sh
journalctl -u lms.service 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
View File
@@ -4,77 +4,60 @@
<!--FORMS message blocks--> <!--FORMS message blocks-->
<message id="Lms.Admin.Database.template"> <message id="Lms.Admin.Database.template">
<div class ="row"> <form class="row g-3">
<div class="col-lg-8"> <legend>${tr:Lms.Admin.Database.database}</legend>
<form> <div class="col-12">
<legend>${tr:Lms.Admin.Database.database}</legend> <label class="form-label" for="${id:media-directory}">
<div class="form-horizontal"> ${tr:Lms.Admin.Database.path}
<div class="form-group"> </label>
<label class="col-lg-3 control-label" for="${id:media-directory}"> ${media-directory class="form-control"}
${tr:Lms.Admin.Database.path} <div class="invalid-feedback">
</label> ${media-directory-info}
<div class="col-lg-9"> </div>
${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>
</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/> <br/>
<div class="row"> ${scanner-controller}
<div class="col-lg-8">
${scanner-controller}
</div>
</div>
</message> </message>
</messages> </messages>
+34 -39
View File
@@ -4,49 +4,44 @@
<message id="Lms.Admin.InitWizard.template"> <message id="Lms.Admin.InitWizard.template">
<div class="container"> <div class="container">
<div class="row"> <form class="row mt-3 justify-content-md-center g-3">
<div class="col-lg-8"> <div class="col-lg-6">
<div class="page-header"> <div class="row g-3">
<h2>${tr:Lms.Admin.InitWizard.header}</h2> <h2>${tr:Lms.Admin.InitWizard.header}</h2>
</div> <div class="col-12">
<form> <label class="form-label" for="${id:admin-login}">
<div class="form-horizontal"> ${tr:Lms.login}
<div class="form-group"> </label>
<label class="col-lg-3 control-label" for="${id:admin-login}"> ${admin-login class="form-control"}
${tr:Lms.login} <div class="invalid-feedback">
</label> ${admin-login-info}
<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> </div>
</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>
</div> </form>
</div> </div>
</message> </message>
+35 -33
View File
@@ -2,42 +2,44 @@
<messages xmlns:if="Wt.WTemplate.conditions"> <messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Admin.ScannerController.template"> <message id="Lms.Admin.ScannerController.template">
<div class="panel panel-info"> <div class="card">
<div class="panel-heading">${tr:Lms.Admin.ScannerController.scanner}</div> <h5 class="card-header">${tr:Lms.Admin.ScannerController.scanner}</h5>
<div class="panel-body"> <div class="card-body">
<div class="form-horizontal"> <form>
<div class="form-group"> <div class="row g-3">
<label class="col-lg-3 control-label"> <div class="col-lg-6">
${tr:Lms.Admin.ScannerController.status} <label class="form-label" for="${id:status}">
</label> ${tr:Lms.Admin.ScannerController.status}
<div class="col-lg-9"> </label>
<div class="well well-sm"> ${status class="form-control"}
${status} </div>
<div>${step-status}</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>
</div> </div>
<div class="form-group"> </form>
<label class="col-lg-3 control-label">
${tr:Lms.Admin.ScannerController.last-scan}
</label>
<div class="col-lg-9">
<div class="well well-sm">
${last-scan}
</div>
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-3 col-lg-9">
${btn-report class="btn btn-xs"}
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-3 col-lg-9">
${btn-action}
</div>
</div>
</div>
</div> </div>
</div> </div>
</message> </message>
+46 -55
View File
@@ -2,64 +2,55 @@
<messages xmlns:if="Wt.WTemplate.conditions"> <messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Admin.User.template"> <message id="Lms.Admin.User.template">
<div class="row"> <form class="row g-3">
<div class="col-lg-8"> <legend>${title}</legend>
<legend>${title}</legend> ${<if-has-last-login>}
<form> <div class="col-lg-6">
<div class="form-horizontal"> <label class="form-label" for="{id:last-login}">
${<if-has-last-login>} ${tr:Lms.Admin.User.last-login}
<div class="form-group"> </label>
<label class="control-label col-lg-3" for="{id:last-login}"> <input type="text" readonly="readonly" class="form-control" id="status" value="${last-login}"/>
${tr:Lms.Admin.User.last-login} </div>
</label> ${</if-has-last-login>}
<div class="col-lg-9"> ${<if-has-login>}
<div class="well well-sm"> <div class="col-lg-6">
${last-login} <label class="form-label" for="${id:login}">
</div> ${tr:Lms.login}
</div> </label>
</div> ${login class="form-control"}
${</if-has-last-login>} <div class="invalid-feedback">
${<if-has-login>} ${login-info}
<div class="form-group"> </div>
<label class="control-label col-lg-3" for="${id:login}"> </div>
${tr:Lms.login} ${</if-has-login>}
</label> ${<if-has-password>}
<div class="col-lg-9"> <div class="col-lg-6">
${login} <label class="form-label" for="${id:password}">
${login-info class="help-block"} ${tr:Lms.password-new}
</div> </label>
</div> ${password class="form-control"}
${</if-has-login>} <div class="invalid-feedback">
${<if-has-password>} ${password-info}
<div class="form-group"> </div>
<label class="control-label col-lg-3" for="${id:password}"> </div>
${tr:Lms.password-new} ${</if-has-password>}
</label> ${<if-demo>}
<div class="col-lg-9"> <div class="col-12">
${password} <div class="form-check">
${password-info class="help-block"} ${demo class="form-check-input"}
</div> <label class="form-check-label" for="${id:demo}">
</div> ${tr:Lms.Admin.User.demo-account}
${</if-has-password>} </label>
${<if-demo>} <div class="invalid-feedback">
<div class="form-group"> ${demo-info}
<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>
</div> </div>
</div> </div>
</form> </div>
${</if-demo>}
<div class="col-12">
${save-btn class="btn btn-primary"}
</div> </div>
</div> </form>
</message> </message>
</messages> </messages>
+27 -10
View File
@@ -2,22 +2,39 @@
<messages xmlns:if="Wt.WTemplate.conditions"> <messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Admin.Users.template"> <message id="Lms.Admin.Users.template">
<div class="Lms-header"> <h3>${tr:Lms.Admin.Users.users}</h3>
<h2>${tr:Lms.Admin.Users.users}</h2> ${users class="d-grid gap-1 mb-3"}
</div>
${users}
${<if-can-create-user>} ${<if-can-create-user>}
${add-btn class="btn-primary Lms-admin-users-add-btn"} ${add-btn class="btn btn-primary"}
${</if-can-create-user>} ${</if-can-create-user>}
</message> </message>
<message id="Lms.Admin.Users.template.entry"> <message id="Lms.Admin.Users.template.entry">
<div class="row Lms-admin-users-entry Lms-vertical-align"> <div class="d-flex align-items-center Lms-bg-dark-hover rounded">
<div class="col-md-5 col-xs-8 Lms-vertical-align"> <div class="p-2 flex-fill">
${name}${<if-tag>}<span class="label label-info Lms-admin-users-entry-role">${tag}</span>${</if-tag>} ${name}${<if-tag>}<span class="badge bg-info ms-2">${tag}</span>${</if-tag>}
</div> </div>
<div class="col-md-3 col-xs-4 Lms-horizontal-center"> <div class="p-2">
${<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>} ${<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>
</div> </div>
</message> </message>
+23 -44
View File
@@ -3,56 +3,35 @@
<!--FORMS message blocks--> <!--FORMS message blocks-->
<message id="Lms.Explore.Artist.template"> <message id="Lms.Explore.Artist.template">
<div class="Lms-header"> <div class="mb-3">
<h2><div class="Lms-artistname text-primary">${name}</div></h2> <h3>${name}</h3>
${clusters} ${clusters class="mb-2"}
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"} <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> </div>
${<if-has-release>} ${<if-has-release>}
<div class="row"> ${releases class="mb-3"}
${releases class="Lms-explore-artist-entries-container"}
</div>
${</if-has-release>} ${</if-has-release>}
${<if-has-non-release-track>} ${<if-has-non-release-track>}
<div class="Lms-explore-artist-tracks-container"> ${tracks class="mb-3"}
${tracks}
</div>
${</if-has-non-release-track>} ${</if-has-non-release-track>}
<div class="row"> ${<if-has-similar-artists>}
${<if-has-similar-artists>} <div>
<div class="col-md-4"> <h3>${tr:Lms.Explore.Artist.similar-artists}</h3>
<div class="well well-sm"> ${similar-artists class="d-grid gap-3"}
<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>
</div> </div>
${</if-has-similar-artists>}
</message> </message>
</messages> </messages>
+27 -11
View File
@@ -3,11 +3,30 @@
<!--FORMS message blocks--> <!--FORMS message blocks-->
<message id="Lms.Explore.Artists.template"> <message id="Lms.Explore.Artists.template">
<div class="Lms-header"> <div class="mb-3">
${mode class="nav nav-pills Lms-explore-mode-container"} <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="row">
<div class="col-sm-2"> <div class="col-lg-3">
${link-type} ${link-type class="form-select"}
</div> </div>
</div> </div>
</div> </div>
@@ -15,15 +34,12 @@
</message> </message>
<message id="Lms.Explore.Artists.template.entry"> <message id="Lms.Explore.Artists.template.entry">
<div class="Lms-explore-artists-entry Lms-entry-hoverable Lms-vertical-align"> ${name class="text-decoration-none link-secondary"}
<h4>${name class="Lms-btn Lms-artistname text-primary"}</h4>
</div>
</message> </message>
<message id="Lms.Explore.Artists.template.entry-small"> <message id="Lms.Explore.Artists.template.container">
<div class="Lms-explore-artists-entry-small Lms-entry-hoverable Lms-vertical-align"> ${elements class="d-grid gap-3"}
${name class="Lms-btn Lms-artistname text-primary"} ${loading-indicator}
</div>
</message> </message>
</messages> </messages>
+30 -24
View File
@@ -2,37 +2,43 @@
<messages xmlns:if="Wt.WTemplate.conditions"> <messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Explore.template"> <message id="Lms.Explore.template">
${contents class="Lms-explore-contents"} ${contents}
</message> </message>
<message id="Lms.Explore.template.more-btn"><i class="fa fa-fw fa-plus-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-circle"></i></message> <message id="Lms.Explore.template.play-btn"><i class="fa fa-fw fa-play"></i></message>
<message id="Lms.Explore.template.filters"> <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>
<message id="Lms.Explore.template.add-filter"> <message id="Lms.Explore.template.add-filter">
<div class="form-horizontal"> <div class="modal fade" tabindex="-1">
<div class="form-group"> <div class="modal-dialog">
<label class="control-label col-sm-2" for="${id:type}"> <div class="modal-content">
${tr:Lms.Explore.type} <div class="modal-header">
</label> <h5 class="modal-title">${tr:Lms.Explore.add-filter}</h5>
<div class="col-sm-10"> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
${type} </div>
</div> <div class="modal-body">
</div> <form class="row g-3">
<div class="form-group"> <div class="col-12">
<label class="control-label col-sm-2" for="${id:value}"> <label class="form-label" for="${id:type}">
${tr:Lms.Explore.value} ${tr:Lms.Explore.type}
</label> </label>
<div class="col-sm-10"> ${type class="form-control"}
${value} </div>
</div> <div class="col-12">
</div> <label class="form-label" for="${id:value}">
<div class="form-group"> ${tr:Lms.Explore.value}
<div class="col-sm-offset-2 col-sm-10"> </label>
${add-btn class="btn-primary"} ${cancel-btn} ${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> </div>
</div> </div>
+29 -37
View File
@@ -4,46 +4,38 @@
<message id="Lms.Auth.template"> <message id="Lms.Auth.template">
<div class="container"> <div class="container">
<div class="page-header"> <form class="row mt-3 justify-content-md-center">
<h2>${tr:Lms.Auth.welcome}</h2> <div class="col-lg-6">
</div> <div class="row g-3">
<form> <h2>${tr:Lms.Auth.welcome}</h2>
<div class="form-horizontal"> <div class="col-12">
<div class="form-group"> <label class="form-label" for="${id:login-name}">
<label class="control-label col-sm-2" for="${id:login-name}"> ${tr:Lms.login}
${tr:Lms.login} </label>
</label> ${login-name class="form-control"}
<div class="col-sm-5"> <div class="invalid-feedback">
${login-name} ${login-name-info}
</div>
</div> </div>
<div class="help-block col-sm-5"> <div class="col-12">
${login-name-info} <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> <div class="col-12">
<div class="form-check">
<div class="form-group"> ${remember-me class="form-check-input"}
<label class="control-label col-sm-2" for="${id:password}"> <label class="form-check-label" for="${id:remember-me}">
${tr:Lms.password} ${tr:Lms.Auth.remember-me}
</label> </label>
<div class="col-sm-5"> </div>
${password}
</div> </div>
<div class="help-block col-sm-5"> <div class="col-12">
${password-info} ${login-btn class="btn btn-primary"}
</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> </div>
</div> </div>
</div> </div>
+70
View File
@@ -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
View File
@@ -6,36 +6,35 @@
<audio id="lms-mp-audio"> <audio id="lms-mp-audio">
</audio> </audio>
<div class="container"> <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"/> <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 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 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>
</div> </div>
<div class="row"> <div class="d-flex align-items-center">
<div class="col-xs-5 col-sm-3 col-md-2 Lms-player-controls"> <div class="d-inline-flex me-2">
<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> <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>
<div class="hidden-xs hidden-sm col-md-2"> <div class="flex-fill text-center text-truncate">
<div class="Lms-player-volume"> ${title class="d-block text-truncate"}
<i id="lms-mp-volume" class="fa fa-fw fa-volume-up Lms-player-btn Lms-btn Lms-player-btn-volume"></i> ${release class="text-decoration-none text-truncate link-success"}
<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> </div>
<div class="col-xs-7 col-sm-7 col-md-6"> <div class="d-none d-md-flex align-items-center Lms-player-volume-container">
<div class="Lms-player-text-center"> <i id="lms-mp-volume" class="fa fa-fw fa-volume-up link-primary me-1"></i>
${title class="Lms-player-text Lms-trackname text-primary"} <input id ="lms-mp-volume-slider" class="form-range" type="range" min="0" max="1" step="0.01" value="0.8"/>
</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> </div>
<div class="hidden-xs col-sm-2 col-md-2 Lms-player-duration text-muted"> <div class="d-none d-sm-block text-muted text-nowrap">
<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> <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> <span id="lms-mp-curtime">--</span> / <span id="lms-mp-duration">--</span>
</div> </div>
</div> </div>
+3 -4
View File
@@ -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-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-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-scanning-files">Scanning files: {1}/{2} files ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Step status</message>
<!--Users--> <!--Users-->
<message id="Lms.Admin.Users.add">New user</message> <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-already-exists">User already exists!</message>
<message id="Lms.Admin.User.user-create">New user</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-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> <message id="Lms.Admin.User.user-updated">User updated!</message>
<!--Wizard--> <!--Wizard-->
@@ -117,10 +118,10 @@
<message id="Lms.Explore.download">Download</message> <message id="Lms.Explore.download">Download</message>
<message id="Lms.Explore.filter-added">Filter added</message> <message id="Lms.Explore.filter-added">Filter added</message>
<message id="Lms.Explore.filters">Filters</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.most-played">Most played</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message> <message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
<message id="Lms.Explore.play">Play</message>
<message id="Lms.Explore.play-last">Play last</message> <message id="Lms.Explore.play-last">Play last</message>
<message id="Lms.Explore.play-shuffled">Play shuffled</message> <message id="Lms.Explore.play-shuffled">Play shuffled</message>
<message id="Lms.Explore.random">Random</message> <message id="Lms.Explore.random">Random</message>
@@ -177,11 +178,9 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.appearance">Appearance</message>
<message id="Lms.Settings.audio">Audio</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.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.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.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.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
<message id="Lms.Settings.password-bad">Bad password</message> <message id="Lms.Settings.password-bad">Bad password</message>
+3 -4
View File
@@ -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-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-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-scanning-files">Scan des fichiers : {1}/{2} fichiers ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message>
<!--Users--> <!--Users-->
<message id="Lms.Admin.Users.add">Ajouter</message> <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-already-exists">L'utilisateur existe déjà !</message>
<message id="Lms.Admin.User.user-create">Nouvel utilisateur</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-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> <message id="Lms.Admin.User.user-updated">L'utilisateur a été mis à jour !</message>
<!--Wizard--> <!--Wizard-->
@@ -117,10 +118,10 @@
<message id="Lms.Explore.download">Télécharger</message> <message id="Lms.Explore.download">Télécharger</message>
<message id="Lms.Explore.filter-added">Filtre ajouté</message> <message id="Lms.Explore.filter-added">Filtre ajouté</message>
<message id="Lms.Explore.filters">Filtres</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.most-played">Plus joués</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message> <message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
<message id="Lms.Explore.play">Jouer</message>
<message id="Lms.Explore.play-last">Jouer en dernier</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.play-shuffled">Jouer aléatoirement</message>
<message id="Lms.Explore.random">Aléatoire</message> <message id="Lms.Explore.random">Aléatoire</message>
@@ -177,11 +178,9 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.appearance">Apparence</message>
<message id="Lms.Settings.audio">Audio</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.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.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.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.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> <message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
+2 -3
View File
@@ -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-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> <message id="Lms.Admin.ScannerController.step-scanning-files">Scansione files: {1}/{2} files ({3}%)...</message>
<!--Users--> <!--Users-->
<message id="Lms.Admin.Users.add">Nuovo utente</message> <message id="Lms.Admin.Users.add">Nuovo utente</message>
<message id="Lms.Admin.Users.admin">Amministratore</message> <message id="Lms.Admin.Users.admin">Amministratore</message>
@@ -116,10 +117,10 @@
<message id="Lms.Explore.download">Download</message> <message id="Lms.Explore.download">Download</message>
<message id="Lms.Explore.filter-added">Filtro aggiunto</message> <message id="Lms.Explore.filter-added">Filtro aggiunto</message>
<message id="Lms.Explore.filters">Filtri</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.most-played">Più ascoltati</message>
<message id="Lms.Explore.musicbrainz-artist">Artista MusicBrainz</message> <message id="Lms.Explore.musicbrainz-artist">Artista MusicBrainz</message>
<message id="Lms.Explore.musicbrainz-release">Album 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-last">Aggiungi alla coda di riproduzione</message>
<message id="Lms.Explore.play-shuffled">Aggiungi alla coda di riproduzione mischiando</message> <message id="Lms.Explore.play-shuffled">Aggiungi alla coda di riproduzione mischiando</message>
<message id="Lms.Explore.random">Casuali</message> <message id="Lms.Explore.random">Casuali</message>
@@ -176,11 +177,9 @@
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.appearance">Aspetto</message>
<message id="Lms.Settings.audio">Audio</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.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.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.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.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
<message id="Lms.Settings.password-bad">Password errata</message> <message id="Lms.Settings.password-bad">Password errata</message>
+2 -3
View File
@@ -81,6 +81,7 @@
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 文件 ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 文件 ({3}%)...</message>
<!--Users--> <!--Users-->
<message id="Lms.Admin.Users.add">新建用户</message> <message id="Lms.Admin.Users.add">新建用户</message>
<message id="Lms.Admin.Users.admin">管理员</message> <message id="Lms.Admin.Users.admin">管理员</message>
@@ -117,10 +118,10 @@
<message id="Lms.Explore.download">下载</message> <message id="Lms.Explore.download">下载</message>
<message id="Lms.Explore.filter-added">筛选已添加</message> <message id="Lms.Explore.filter-added">筛选已添加</message>
<message id="Lms.Explore.filters">筛选</message> <message id="Lms.Explore.filters">筛选</message>
<message id="Lms.Explore.links">链接</message>
<message id="Lms.Explore.most-played">播放最多</message> <message id="Lms.Explore.most-played">播放最多</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz 专辑</message> <message id="Lms.Explore.musicbrainz-release">MusicBrainz 专辑</message>
<message id="Lms.Explore.play-last">播放列表</message> <message id="Lms.Explore.play-last">播放列表</message>
<message id="Lms.Explore.play-shuffled">随机播放</message> <message id="Lms.Explore.play-shuffled">随机播放</message>
<message id="Lms.Explore.random">随机</message> <message id="Lms.Explore.random">随机</message>
@@ -176,11 +177,9 @@
<message id="Lms.PlayHistory.playhistory">播放历史</message> <message id="Lms.PlayHistory.playhistory">播放历史</message>
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.appearance">外观</message>
<message id="Lms.Settings.audio">音频</message> <message id="Lms.Settings.audio">音频</message>
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message> <message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
<message id="Lms.Settings.change-password">更改密码</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.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.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
<message id="Lms.Settings.password-bad">无效密码</message> <message id="Lms.Settings.password-bad">无效密码</message>
+17
View File
@@ -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>
+14
View File
@@ -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
View File
@@ -2,54 +2,62 @@
<messages xmlns:if="Wt.WTemplate.conditions"> <messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks--> <!--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.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.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.radio-btn"><i class="fa fa-fw fa-rss"></i></message>
<message id="Lms.PlayQueue.template"> <message id="Lms.PlayQueue.template">
<div class="Lms-header"> <div class="mb-3">
${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"} <div class="d-inline-block me-2" title="${tr:Lms.PlayQueue.clear}">
<h4><small>${nb-tracks}</small></h4> ${clear-btn class="btn btn-secondary"}
</div>
<div class="row">
<div class="col-lg-8">
${entries}
</div> </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> </div>
${entries}
</message> </message>
<message id="Lms.PlayQueue.template.play-btn"><i class="fa fa-fw fa-play-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-circle"></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-plus-circle"></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"> <message id="Lms.PlayQueue.template.entry">
<div class="row Lms-vertical-align Lms-explore-entry-list Lms-playqueue-entry ${is-selected}"> <div class="d-flex align-items-center rounded Lms-bg-dark-hover mb-2">
<div class="col-xs-3 col-sm-2 col-md-2"> <div class="p-1 Lms-cover-small">
<div class="Lms-responsive-square Lms-cover-container"> ${cover}
${cover}
</div>
</div> </div>
<div class="col-xs-7 col-sm-8 col-md-8 Lms-explore-entry-list-text-container"> <div class="p-2 flex-fill text-truncate">
<div class="Lms-trackname text-primary"><h5>${name}</h5></div> <div class="d-block text-truncate">${name}</div>
${<if-has-artists-or-release>} ${<if-has-release>}${release class="text-decoration-none link-success text-truncate"}${</if-has-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>
<div class="col-xs-2 col-sm-2 col-md-2"> <div class="p-2 d-none d-sm-block text-muted">
<div class="Lms-playqueue-entry-controls Lms-vertical-align"> ${duration}
${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> <div class="p-2 d-flex">
<div class="Lms-playqueue-entry-duration text-muted"> ${play-btn class="btn btn-sm btn-outline-secondary border-0"}
${duration} ${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> </div>
</div> </div>
</message> </message>
<message id="Lms.PlayQueue.template.entry-artist"> <message id="Lms.PlayQueue.template.entry-artist">
<small>${artist class="Lms-btn Lms-artistname text-muted"}</small> ${artist}
</message> </message>
</messages> </messages>
+53 -56
View File
@@ -7,83 +7,80 @@
</message> </message>
<message id="Lms.Explore.Release.template"> <message id="Lms.Explore.Release.template">
<div class="row Lms-explore-release"> <div class="row mb-3 gy-3">
<div class="col-md-4"> <div class="col-lg-3 col-md-4">
${cover class="Lms-explore-release-cover"} ${cover}
</div> </div>
<div class="col-md-8"> <div class="col">
<div class="Lms-header"> <h3>${name}</h3>
<h3><div class="Lms-releasename text-primary">${name}</div></h3> ${<if-has-release-artists>}${artists}${</if-has-release-artists>}
${<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>}
${<if-has-various-release-artists>}<div class="Lms-artistname text-muted">${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>
<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 class="mb-2"}
${clusters} <div class="btn-group">
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"} ${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> </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> </div>
<div class="row"> ${<if-has-similar-releases>}
${<if-has-similar-releases>} <h3>${tr:Lms.Explore.Release.similar-releases}</h3>
<div class="col-xs-12 col-md-8"> ${similar-releases class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 gx-2 gy-4"}
<div class="well well-sm"> ${</if-has-similar-releases>}
<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>
</message> </message>
<message id="Lms.Explore.Release.template.entry-release-artist"> <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>
<message id="Lms.Explore.Release.template.entry-disc"> <message id="Lms.Explore.Release.template.entry-disc">
<h4>${disc-title}</h4> <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>
<message id="Lms.Explore.Release.template.entry"> <message id="Lms.Explore.Release.template.entry">
<div class="row Lms-explore-release-entry Lms-entry-hoverable Lms-vertical-align ${is-playing}"> <div class="d-flex align-items-center rounded Lms-bg-dark-hover">
<div class="col-sm-1 col-xs-1 Lms-explore-release-entry-tracknumber text-muted"> <div class="p-2 text-muted text-end" style="min-width: 2.1rem;">
${<if-has-track-number>}${track-number}${</if-has-track-number>} ${<if-has-track-number>}${track-number}${</if-has-track-number>}
</div> </div>
<div class="col-sm-9 col-xs-7 Lms-explore-release-entry-name"> <div class="p-2 flex-fill">
<div class="Lms-trackname text-primary">${name}</div> ${name}
${<if-has-artists>}${artists}${</if-has-artists>} ${<if-has-artists>}${artists}${</if-has-artists>}
</div> </div>
<div class="col-sm-2 col-xs-4"> <div class="p-2 d-none d-sm-block text-muted">
<div class="Lms-explore-release-entry-controls"> ${duration}
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"} </div>
</div> <div class="p-2 d-flex">
<div class="Lms-explore-release-entry-track-duration text-muted"> ${play-btn class="btn btn-sm btn-outline-secondary border-0"}
${duration} <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> </div>
</div> </div>
</message> </message>
<message id="Lms.Explore.Release.template.entry-artist">
<small>${artist class="Lms-btn Lms-artistname text-muted"}</small>
</message>
</messages> </messages>
+38 -15
View File
@@ -3,33 +3,56 @@
<messages xmlns:if="Wt.WTemplate.conditions"> <messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Explore.Releases.template"> <message id="Lms.Explore.Releases.template">
<div class="Lms-header"> <div class="mb-3">
${mode class="nav nav-pills Lms-explore-mode-container"} <ul class="nav nav-pills mb-2">
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"} <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> </div>
${releases} ${releases}
</message> </message>
<message id="Lms.Explore.Releases.template.entry-grid"> <message id="Lms.Explore.Releases.template.entry-grid">
<div class="col-xs-6 col-sm-3 col-md-2 col-lg-2"> <div class="col h-100 Lms-bg-dark-hover rounded">
<div class="Lms-explore-entry-grid"> <div class="p-2">
<div class="Lms-responsive-square Lms-cover-container"> <div class="Lms-responsive-square Lms-cover-container">
${cover} ${cover class="shadow-sm"}
</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>
</div> </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>
</div> </div>
</message> </message>
<message id="Lms.Explore.Releases.template.container"> <message id="Lms.Explore.Releases.template.container">
<div class="row"> ${elements class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 gx-2 gy-4"}
${elements} ${loading-indicator}
</div>
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
</message> </message>
</messages> </messages>
+15 -3
View File
@@ -3,10 +3,22 @@
<!--FORMS message blocks--> <!--FORMS message blocks-->
<message id="Lms.Explore.Search.template"> <message id="Lms.Explore.Search.template">
<div class="Lms-header"> <ul class="nav nav-pills mb-3" id="myTab" role="tablist">
${mode class="nav nav-pills Lms-explore-mode-container"} <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> </div>
${stack}
</message> </message>
</messages> </messages>
+166 -191
View File
@@ -4,198 +4,173 @@
<!--FORMS message blocks--> <!--FORMS message blocks-->
<message id="Lms.Settings.template"> <message id="Lms.Settings.template">
<div class="row"> <form class="row g-3">
<div class="col-lg-8"> <legend>${tr:Lms.Settings.audio}</legend>
<form> <div class="alert alert-info">
<legend>${tr:Lms.Settings.appearance}</legend> ${tr:Lms.Settings.audio-settings-are-local}
<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>
</div> </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> </message>
</messages> </messages>
-69
View File
@@ -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
View File
@@ -4,49 +4,67 @@
<!--FORMS message blocks--> <!--FORMS message blocks-->
<message id="Lms.Explore.Tracks.template"> <message id="Lms.Explore.Tracks.template">
<div class="Lms-header"> <div class="mb-3">
${mode class="nav nav-pills Lms-explore-mode-container"} <ul class="nav nav-pills mb-2">
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"} <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> </div>
${tracks} ${tracks}
</message> </message>
<message id="Lms.Explore.Tracks.template.entry"> <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="d-flex align-items-center rounded Lms-bg-dark-hover mb-2">
<div class="col-xs-3 col-sm-2 col-md-2"> <div class="p-1 Lms-cover-small">
<div class="Lms-responsive-square Lms-cover-container"> ${cover}
${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> </div>
</message> <div class="p-2 flex-fill text-truncate">
<div class="d-block text-truncate">${name}</div>
<message id="Lms.Explore.Tracks.template.entry-artist"> ${<if-has-release>}${release class="text-decoration-none link-success text-truncate"}${</if-has-release>}
<small>${artist class="Lms-btn Lms-artistname text-muted"}</small> </div>
</message> <div class="p-2 d-none d-sm-block text-muted">
${duration}
<message id="Lms.Explore.Tracks.template.container"> </div>
<div class="row"> <div class="p-2 d-flex">
<div class="col-lg-8"> ${play-btn class="btn btn-sm btn-outline-secondary border-0"}
${elements} <div class="dropdown d-inline-block">
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"} ${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>
</div> </div>
</message> </message>
<message id="Lms.Explore.Tracks.template.entry-artist">
${artist class="text-decoration-none link-secondary"}
</message>
</messages> </messages>
+1 -1
View File
@@ -1,4 +1,4 @@
find_path(STB_INCLUDE_DIR stb/stb.h) find_path(STB_INCLUDE_DIR stb/stb_image.h)
include(FindPackageHandleStandardArgs) 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.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12
View File
File diff suppressed because one or more lines are too long
-54
View File
@@ -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;
}
-37
View File
@@ -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;
}
-17
View File
@@ -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
View File
@@ -1,73 +1,58 @@
/* Stylized for flaty/darkly themes */ /* Generic rules */
a, a:link, a:active, a:visited {
text-decoration: none; .link-success {
cursor: pointer; 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 { body {
padding-bottom: 78px; padding-bottom: 78px;
} }
.Lms-vertical-align { .Lms-badge-cluster {
display: flex; transition: filter 0.15s ease;
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 {
cursor: pointer; cursor: pointer;
} }
.Lms-btn:hover { .Lms-badge-cluster:hover {
color: var(--hover-color); filter: brightness(85%);
transition: color .20s cubic-bezier(.43,-.1,.16,1.1);
}
.Lms-cluster {
display: inline-block;
cursor: pointer; cursor: pointer;
line-height: 1.3;
margin-right: 4px;
margin-bottom: 4px;
} }
.Lms-cluster:hover { .Lms-entry-playing {
filter: brightness(130%); background-color: var(--bs-dark);
transition: filter .20s cubic-bezier(.43,-.1,.16,1.1);
}
.Lms-clusters-control {
margin-top: 4px;
} }
.Lms-cover { .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-height: 100%;
max-width: 100%; max-width: 100%;
width: auto; width: auto;
@@ -78,299 +63,53 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
left: 0; left: 0;
right: 0; right: 0;
margin: auto; 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 { .Lms-cover-track {
filter: brightness(130%); width: 64px;
transition: filter .20s cubic-bezier(.43,-.1,.16,1.1); min-width: 64px;
} }
.Lms-cover-container { #lms-mp-progress {
position: relative; transition: none;
} }
.Lms-cover-large { #lms-mp-volume {
border-radius: 3px; cursor: pointer;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
} }
.Lms-cover-loaded { #lms-mp-volume-slider::-webkit-slider-runnable-track {
opacity: 1; background-color: var(--bs-gray-900);
} }
.Lms-entry-playing { #lms-mp-volume-slider::-moz-range-track {
font-weight: bold; background-color: var(--bs-gray-900);
border-radius: 3px;
background-color: var(--playing-background-color);
} }
.Lms-explore-btn { .Lms-notification-container {
font-size: 24px; position: fixed;
} bottom: 78px;
right: 0;
.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-player { .Lms-player {
z-index: 3;
position: fixed;
bottom: 0;
width: 100%;
height: 70px; height: 70px;
padding: 0;
margin: 0;
background-color: var(--player-background-color);
} }
.Lms-player-btn-volume { .fixed-bottom.Lms-player {
font-size: 18px !important; z-index: 999;
margin: auto;
} }
.Lms-player-btn-controls { .Lms-player-volume-container {
font-size: 40px !important; width: 100px;
} }
.Lms-player-controls { .Lms-player-seek-common {
text-align: center; position: absolute;
white-space: nowrap; width:100%;
} top: 0;
left: 0;
.Lms-player-duration { height: 6px;
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 { .Lms-player-seek {
@@ -380,8 +119,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
background: transparent; background: transparent;
border-style: solid;
border-width: thin;
} }
.Lms-player-seek::-webkit-slider-thumb { .Lms-player-seek::-webkit-slider-thumb {
@@ -391,23 +128,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
width: 0; 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 { .Lms-player-seek::-moz-range-thumb {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
@@ -415,75 +135,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
width: 0; 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 { .Lms-responsive-square {
width: 100%; width: 100%;
} }
@@ -493,5 +144,3 @@ a.Lms-releasename:hover, a.Lms-releasename:focus {
display: block; display: block;
padding-bottom: 100%; padding-bottom: 100%;
} }
-417
View File
@@ -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">&times;</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');
}
};
}));
File diff suppressed because one or more lines are too long
-7
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7 -6
View File
@@ -31,13 +31,16 @@ LMS.mediaplayer = function () {
var _lastStartPlaying = null; var _lastStartPlaying = null;
var _updateControls = function() { var _updateControls = function() {
const pauseClass = "fa-pause";
const playClass = "fa-play";
if (_elems.audio.paused) { if (_elems.audio.paused) {
_elems.playpause.classList.remove("fa-pause"); _elems.playpause.firstElementChild.classList.remove(pauseClass);
_elems.playpause.classList.add("fa-play"); _elems.playpause.firstElementChild.classList.add(playClass);
} }
else { else {
_elems.playpause.classList.remove("fa-play"); _elems.playpause.firstElementChild.classList.remove(playClass);
_elems.playpause.classList.add("fa-pause"); _elems.playpause.firstElementChild.classList.add(pauseClass);
} }
} }
@@ -178,8 +181,6 @@ LMS.mediaplayer = function () {
_elems.volumeslider = document.getElementById("lms-mp-volume-slider"); _elems.volumeslider = document.getElementById("lms-mp-volume-slider");
_elems.transcodingActive = document.getElementById("lms-transcoding-active"); _elems.transcodingActive = document.getElementById("lms-transcoding-active");
$(_elems.transcodingActive).tooltip();
var source = _audioCtx.createMediaElementSource(_elems.audio); var source = _audioCtx.createMediaElementSource(_elems.audio);
source.connect(_gainNode); source.connect(_gainNode);
_gainNode.connect(_audioCtx.destination); _gainNode.connect(_audioCtx.destination);
+1 -1
View File
@@ -241,7 +241,7 @@ AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
std::optional<AudioFileFormat> std::optional<AudioFileFormat>
guessMediaFileFormat(const std::filesystem::path& file) 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) if (!format || !format->name)
return {}; return {};
@@ -226,7 +226,6 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
track.clusters[tag] = std::move(values); track.clusters[tag] = std::move(values);
} }
} }
} }
track.artists = getArtists(metadataMap); track.artists = getArtists(metadataMap);
-1
View File
@@ -148,7 +148,6 @@ getAlbum(const TagLib::PropertyMap& properties)
void void
TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug) TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug)
{ {
if (debug) if (debug)
{ {
std::vector<std::string> strs; std::vector<std::string> strs;
@@ -20,6 +20,7 @@
#pragma once #pragma once
#include <string_view> #include <string_view>
#include <optional>
#include <boost/asio/ip/address.hpp> #include <boost/asio/ip/address.hpp>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
+5 -6
View File
@@ -204,8 +204,8 @@ Artist::find(Session& session, const FindParameters& params)
return execQuery(query, params.range); return execQuery(query, params.range);
} }
std::vector<Release::pointer> RangeResults<ReleaseId>
Artist::getReleases(const std::vector<ClusterId>& clusterIds) const Artist::getReleases(Range range, const std::vector<ClusterId>& clusterIds) const
{ {
assert(session()); assert(session());
@@ -213,7 +213,7 @@ Artist::getReleases(const std::vector<ClusterId>& clusterIds) const
std::ostringstream oss; 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()) 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"; 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()) for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg); query.bind(bindArg);
auto res {query.resultList()}; return execQuery(query, range);
return std::vector<Release::pointer>(res.begin(), res.end());
} }
std::size_t std::size_t
@@ -30,6 +30,7 @@
#include "services/database/ArtistId.hpp" #include "services/database/ArtistId.hpp"
#include "services/database/ClusterId.hpp" #include "services/database/ClusterId.hpp"
#include "services/database/Object.hpp" #include "services/database/Object.hpp"
#include "services/database/ReleaseId.hpp"
#include "services/database/Types.hpp" #include "services/database/Types.hpp"
#include "services/database/UserId.hpp" #include "services/database/UserId.hpp"
#include "utils/EnumSet.hpp" #include "utils/EnumSet.hpp"
@@ -88,7 +89,7 @@ class Artist : public Object<Artist, ArtistId>
const std::string& getSortName() const { return _sortName; } const std::string& getSortName() const { return _sortName; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); } 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::size_t getReleaseCount() const;
std::vector<ObjectPtr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const; std::vector<ObjectPtr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
bool hasNonReleaseTracks(std::optional<TrackArtistLinkType> linkType = std::nullopt) const; bool hasNonReleaseTracks(std::optional<TrackArtistLinkType> linkType = std::nullopt) const;
+12 -12
View File
@@ -574,13 +574,13 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
auto releases {artist->getReleases()}; auto releases {artist->getReleases(Range {})};
ASSERT_EQ(releases.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.front()->getId(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
releases = artist->getReleases({cluster.getId()}); releases = artist->getReleases(Range {}, {cluster.getId()});
ASSERT_EQ(releases.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.front()->getId(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
} }
} }
@@ -605,13 +605,13 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto releases {artist->getReleases()}; auto releases {artist->getReleases(Range {})};
ASSERT_EQ(releases.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.front()->getId(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
releases = artist->getReleases({cluster1.getId(), cluster2.getId()}); releases = artist->getReleases(Range {}, {cluster1.getId(), cluster2.getId()});
ASSERT_EQ(releases.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.front()->getId(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
} }
} }
@@ -50,8 +50,9 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
EXPECT_EQ(artist->getReleaseCount(), 1); EXPECT_EQ(artist->getReleaseCount(), 1);
ASSERT_EQ(artist->getReleases().size(), 1); auto releases {artist->getReleases(Range {})};
EXPECT_EQ(artist->getReleases().front()->getId(), release.getId()); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
EXPECT_EQ(release->getTracks().size(), nbTracks); EXPECT_EQ(release->getTracks().size(), nbTracks);
} }
@@ -73,9 +74,9 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
auto releases {artist->getReleases()}; auto releases {artist->getReleases(Range {})};
ASSERT_EQ(releases.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.front()->getId(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
EXPECT_EQ(artist->getReleaseCount(), 1); EXPECT_EQ(artist->getReleaseCount(), 1);
@@ -30,7 +30,6 @@ namespace SOM
class DataNormalizer class DataNormalizer
{ {
public: public:
struct MinMax struct MinMax
{ {
InputVector::value_type min; InputVector::value_type min;
-1
View File
@@ -163,7 +163,6 @@ class InputVector
} }
private: private:
friend class InputVector operator-(const InputVector& a, const InputVector& b) friend class InputVector operator-(const InputVector& a, const InputVector& b)
{ {
if (!a.hasSameDimension(b.getNbDimensions())) if (!a.hasSameDimension(b.getNbDimensions()))
-2
View File
@@ -53,7 +53,6 @@ template <typename T>
class Matrix class Matrix
{ {
public: public:
Matrix() = default; Matrix() = default;
Matrix(Coordinate width, Coordinate height) Matrix(Coordinate width, Coordinate height)
@@ -109,7 +108,6 @@ class Matrix
} }
private: private:
Coordinate _width {}; Coordinate _width {};
Coordinate _height {}; Coordinate _height {};
std::vector<T> _values; std::vector<T> _values;
-1
View File
@@ -40,7 +40,6 @@ std::ostream& operator<<(std::ostream& os, const InputVector& a);
class Network class Network
{ {
public: public:
// Init a network with random values // Init a network with random values
Network(Coordinate width, Coordinate height, std::size_t inputDimCount); Network(Coordinate width, Coordinate height, std::size_t inputDimCount);
+26 -35
View File
@@ -22,6 +22,7 @@
#include <atomic> #include <atomic>
#include <ctime> #include <ctime>
#include <iomanip> #include <iomanip>
#include <map>
#include <unordered_map> #include <unordered_map>
#include <Wt/WLocalDateTime.h> #include <Wt/WLocalDateTime.h>
@@ -885,9 +886,12 @@ handleGetArtistRequest(RequestContext& context)
Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)}; Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)};
auto releases {artist->getReleases()}; auto releases {artist->getReleases(Range {})};
for (const Release::pointer& release : releases) 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 */)); artistNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, true /* id3 */));
}
response.addNode("artist", std::move(artistNode)); response.addNode("artist", std::move(artistNode));
@@ -997,9 +1001,12 @@ handleGetMusicDirectoryRequest(RequestContext& context)
directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName())); directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName()));
auto releases {artist->getReleases()}; auto releases {artist->getReleases(Range {})};
for (const Release::pointer& release : releases) 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 */)); directoryNode.addArrayChild("child", releaseToResponseNode(release, context.dbSession, user, false /* no id3 */));
}
} }
else if (releaseId) else if (releaseId)
{ {
@@ -1065,46 +1072,30 @@ handleGetArtistsRequestCommon(RequestContext& context, bool id3)
break; break;
} }
Response::Node* currentIndexNode {}; std::map<char, std::vector<Artist::pointer>> artistsSortedByFirstChar;
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;
}};
const RangeResults<ArtistId> artists {Artist::find(context.dbSession, parameters)}; const RangeResults<ArtistId> artists {Artist::find(context.dbSession, parameters)};
for (const ArtistId artistId : artists.results) for (const ArtistId artistId : artists.results)
{ {
const Artist::pointer artist {Artist::find(context.dbSession, artistId)}; const Artist::pointer artist {Artist::find(context.dbSession, artistId)};
const std::string& sortName {artist->getSortName()}; const std::string& sortName {artist->getSortName()};
Response::Node* indexNode{}; char sortChar;
if (sortName.empty() || !std::isalpha(sortName[0])) if (sortName.empty() || !std::isalpha(sortName[0]))
indexNode = getOrCreateUnknownIndexNode(); sortChar = '?';
else 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; return response;
@@ -203,7 +203,6 @@ class Response
void addArrayChild(const std::string& key, Node node); void addArrayChild(const std::string& key, Node node);
private: private:
void setVersionAttribute(ProtocolVersion version); void setVersionAttribute(ProtocolVersion version);
friend class Response; friend class Response;
@@ -28,7 +28,6 @@ class FileResourceHandler final : public IResourceHandler
FileResourceHandler(const std::filesystem::path& filePath); FileResourceHandler(const std::filesystem::path& filePath);
private: private:
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
static constexpr std::size_t _chunkSize {65536}; static constexpr std::size_t _chunkSize {65536};
+1 -3
View File
@@ -32,15 +32,13 @@ namespace Http
{ {
public: public:
Client(boost::asio::io_context& ioContext, std::string_view baseUrl) Client(boost::asio::io_context& ioContext, std::string_view baseUrl)
: _ioContext {ioContext} : _sendQueue {ioContext, baseUrl}
, _sendQueue {ioContext, baseUrl}
{} {}
private: private:
void sendGETRequest(ClientGETRequestParameters&& request) override; void sendGETRequest(ClientGETRequestParameters&& request) override;
void sendPOSTRequest(ClientPOSTRequestParameters&& request) override; void sendPOSTRequest(ClientPOSTRequestParameters&& request) override;
boost::asio::io_context& _ioContext;
SendQueue _sendQueue; SendQueue _sendQueue;
}; };
} // namespace Http } // namespace Http
-1
View File
@@ -26,7 +26,6 @@
class IConfig class IConfig
{ {
public: public:
virtual ~IConfig() = default; virtual ~IConfig() = default;
// Default values are returned in case of setting not found // Default values are returned in case of setting not found
+15
View File
@@ -31,34 +31,43 @@
namespace StringUtils { namespace StringUtils {
[[nodiscard]]
std::vector<std::string> std::vector<std::string>
splitStringCopy(std::string_view string, std::string_view separators); splitStringCopy(std::string_view string, std::string_view separators);
[[nodiscard]]
std::vector<std::string_view> std::vector<std::string_view>
splitString(std::string_view string, std::string_view separators); splitString(std::string_view string, std::string_view separators);
[[nodiscard]]
std::string std::string
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter); joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
[[nodiscard]]
std::string std::string
stringTrim(std::string_view str, std::string_view whitespaces = " \t"); stringTrim(std::string_view str, std::string_view whitespaces = " \t");
[[nodiscard]]
std::string std::string
stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t"); stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
[[nodiscard]]
std::string std::string
stringToLower(std::string_view str); stringToLower(std::string_view str);
void void
stringToLower(std::string& str); stringToLower(std::string& str);
[[nodiscard]]
std::string std::string
stringToUpper(const std::string& str); stringToUpper(const std::string& str);
[[nodiscard]]
std::string std::string
bufferToString(const std::vector<unsigned char>& data); bufferToString(const std::vector<unsigned char>& data);
template<typename T> template<typename T>
[[nodiscard]]
std::optional<T> readAs(std::string_view str) std::optional<T> readAs(std::string_view str)
{ {
T res; T res;
@@ -72,21 +81,27 @@ std::optional<T> readAs(std::string_view str)
} }
template<> template<>
[[nodiscard]]
std::optional<std::string> std::optional<std::string>
readAs(std::string_view str); readAs(std::string_view str);
[[nodiscard]]
std::string std::string
replaceInString(const std::string& str, const std::string& from, const std::string& to); replaceInString(const std::string& str, const std::string& from, const std::string& to);
[[nodiscard]]
std::string std::string
jsEscape(const std::string& str); jsEscape(const std::string& str);
[[nodiscard]]
std::string std::string
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar); escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
[[nodiscard]]
bool bool
stringEndsWith(const std::string& str, const std::string& ending); stringEndsWith(const std::string& str, const std::string& ending);
[[nodiscard]]
std::optional<std::string> std::optional<std::string>
stringFromHex(const std::string& str); stringFromHex(const std::string& str);
-1
View File
@@ -28,7 +28,6 @@
class UUID class UUID
{ {
public: public:
static std::optional<UUID> fromString(std::string_view str); static std::optional<UUID> fromString(std::string_view str);
std::string_view getAsString() const { return _value; } std::string_view getAsString() const { return _value; }
-1
View File
@@ -40,7 +40,6 @@ namespace Zip
class Zipper class Zipper
{ {
public: public:
Zipper(const std::map<std::string, std::filesystem::path>& files, const Wt::WDateTime& lastModifiedTime = {}); Zipper(const std::map<std::string, std::filesystem::path>& files, const Wt::WDateTime& lastModifiedTime = {});
static constexpr SizeType minOutputBufferSize {64}; static constexpr SizeType minOutputBufferSize {64};
+28 -6
View File
@@ -24,7 +24,7 @@
TEST(StringUtils, splitString) TEST(StringUtils, splitString)
{ {
{ {
const std::string test{"a"}; const std::string test {"a"};
const std::vector<std::string_view> strings {StringUtils::splitString(test, "")}; const std::vector<std::string_view> strings {StringUtils::splitString(test, "")};
ASSERT_EQ(strings.size(), 1); 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, "|")}; const std::vector<std::string_view> strings {StringUtils::splitString(test, "|")};
ASSERT_EQ(strings.size(), 1); 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, " ")}; const std::vector<std::string_view> strings {StringUtils::splitString(test, " ")};
ASSERT_EQ(strings.size(), 1); 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, " ")}; const std::vector<std::string_view> strings {StringUtils::splitString(test, " ")};
ASSERT_EQ(strings.size(), 1); 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, " ")}; const std::vector<std::string_view> strings {StringUtils::splitString(test, " ")};
ASSERT_EQ(strings.size(), 2); 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, " ,|")}; const std::vector<std::string_view> strings {StringUtils::splitString(test, " ,|")};
ASSERT_EQ(strings.size(), 4); 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) TEST(StringUtils, escapeString)
{ {
EXPECT_EQ(StringUtils::escapeString("", "*", ' '), ""); EXPECT_EQ(StringUtils::escapeString("", "*", ' '), "");
+5 -3
View File
@@ -6,20 +6,24 @@ add_executable(lms
ui/LmsApplicationManager.cpp ui/LmsApplicationManager.cpp
ui/LmsTheme.cpp ui/LmsTheme.cpp
ui/MediaPlayer.cpp ui/MediaPlayer.cpp
ui/ModalManager.cpp
ui/NotificationContainer.cpp
ui/PlayQueue.cpp ui/PlayQueue.cpp
ui/SettingsView.cpp ui/SettingsView.cpp
ui/TrackStringUtils.cpp ui/Utils.cpp
ui/admin/DatabaseSettingsView.cpp ui/admin/DatabaseSettingsView.cpp
ui/admin/ScannerController.cpp ui/admin/ScannerController.cpp
ui/admin/InitWizardView.cpp ui/admin/InitWizardView.cpp
ui/admin/UserView.cpp ui/admin/UserView.cpp
ui/admin/UsersView.cpp ui/admin/UsersView.cpp
ui/common/DirectoryValidator.cpp ui/common/DirectoryValidator.cpp
ui/common/DoubleValidator.cpp
ui/common/InfiniteScrollingContainer.cpp ui/common/InfiniteScrollingContainer.cpp
ui/common/LoadingIndicator.cpp ui/common/LoadingIndicator.cpp
ui/common/LoginNameValidator.cpp ui/common/LoginNameValidator.cpp
ui/common/MandatoryValidator.cpp ui/common/MandatoryValidator.cpp
ui/common/PasswordValidator.cpp ui/common/PasswordValidator.cpp
ui/common/Template.cpp
ui/common/UUIDValidator.cpp ui/common/UUIDValidator.cpp
ui/explore/ArtistCollector.cpp ui/explore/ArtistCollector.cpp
ui/explore/ArtistListHelpers.cpp ui/explore/ArtistListHelpers.cpp
@@ -30,13 +34,11 @@ add_executable(lms
ui/explore/Filters.cpp ui/explore/Filters.cpp
ui/explore/ReleaseCollector.cpp ui/explore/ReleaseCollector.cpp
ui/explore/ReleaseListHelpers.cpp ui/explore/ReleaseListHelpers.cpp
ui/explore/ReleasePopup.cpp
ui/explore/ReleasesView.cpp ui/explore/ReleasesView.cpp
ui/explore/ReleaseView.cpp ui/explore/ReleaseView.cpp
ui/explore/SearchView.cpp ui/explore/SearchView.cpp
ui/explore/TrackCollector.cpp ui/explore/TrackCollector.cpp
ui/explore/TrackListHelpers.cpp ui/explore/TrackListHelpers.cpp
ui/explore/TrackPopup.cpp
ui/explore/TracksView.cpp ui/explore/TracksView.cpp
ui/resource/AudioFileResource.cpp ui/resource/AudioFileResource.cpp
ui/resource/AudioTranscodeResource.cpp ui/resource/AudioTranscodeResource.cpp
-4
View File
@@ -86,7 +86,6 @@ processAuthToken(const Wt::WEnvironment& env)
class AuthModel : public Wt::WFormModel class AuthModel : public Wt::WFormModel
{ {
public: public:
// Associate each field with a unique string literal. // Associate each field with a unique string literal.
static const Field LoginNameField; static const Field LoginNameField;
static const Field PasswordField; static const Field PasswordField;
@@ -160,7 +159,6 @@ class AuthModel : public Wt::WFormModel
std::optional<Database::UserId> getUserId() const { return _userId; } std::optional<Database::UserId> getUserId() const { return _userId; }
private: private:
std::optional<Database::UserId> _userId; 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::PasswordField {"password"};
const AuthModel::Field AuthModel::RememberMeField {"remember-me"}; const AuthModel::Field AuthModel::RememberMeField {"remember-me"};
Auth::Auth() Auth::Auth()
: Wt::WTemplateFormView {Wt::WString::tr("Lms.Auth.template")} : Wt::WTemplateFormView {Wt::WString::tr("Lms.Auth.template")}
{ {
@@ -217,7 +214,6 @@ Auth::Auth()
loginBtn->clicked().connect(this, processAuth); loginBtn->clicked().connect(this, processAuth);
updateView(model.get()); updateView(model.get());
} }
} // namespace UserInterface } // namespace UserInterface
+38 -111
View File
@@ -22,11 +22,9 @@
#include <Wt/WAnchor.h> #include <Wt/WAnchor.h>
#include <Wt/WEnvironment.h> #include <Wt/WEnvironment.h>
#include <Wt/WLineEdit.h> #include <Wt/WLineEdit.h>
#include <Wt/WPopupMenu.h>
#include <Wt/WPushButton.h> #include <Wt/WPushButton.h>
#include <Wt/WServer.h> #include <Wt/WServer.h>
#include <Wt/WStackedWidget.h> #include <Wt/WStackedWidget.h>
#include <Wt/WText.h>
#include "services/auth/IEnvService.hpp" #include "services/auth/IEnvService.hpp"
#include "services/auth/IPasswordService.hpp" #include "services/auth/IPasswordService.hpp"
@@ -57,6 +55,8 @@
#include "LmsApplicationManager.hpp" #include "LmsApplicationManager.hpp"
#include "LmsTheme.hpp" #include "LmsTheme.hpp"
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
#include "ModalManager.hpp"
#include "NotificationContainer.hpp"
#include "PlayQueue.hpp" #include "PlayQueue.hpp"
#include "SettingsView.hpp" #include "SettingsView.hpp"
@@ -162,7 +162,12 @@ LmsApplication::~LmsApplication() = default;
void void
LmsApplication::init() LmsApplication::init()
{ {
setTheme(std::make_shared<LmsTheme>());
useStyleSheet("resources/font-awesome/css/font-awesome.min.css"); useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
require("js/mediaplayer.js");
setTitle("LMS");
// Add a resource bundle // Add a resource bundle
messageResourceBundle().use(appRoot() + "admin-database"); messageResourceBundle().use(appRoot() + "admin-database");
@@ -175,24 +180,18 @@ LmsApplication::init()
messageResourceBundle().use(appRoot() + "error"); messageResourceBundle().use(appRoot() + "error");
messageResourceBundle().use(appRoot() + "explore"); messageResourceBundle().use(appRoot() + "explore");
messageResourceBundle().use(appRoot() + "login"); messageResourceBundle().use(appRoot() + "login");
messageResourceBundle().use(appRoot() + "main");
messageResourceBundle().use(appRoot() + "mediaplayer"); messageResourceBundle().use(appRoot() + "mediaplayer");
messageResourceBundle().use(appRoot() + "messages"); messageResourceBundle().use(appRoot() + "messages");
messageResourceBundle().use(appRoot() + "misc");
messageResourceBundle().use(appRoot() + "notifications");
messageResourceBundle().use(appRoot() + "playqueue"); messageResourceBundle().use(appRoot() + "playqueue");
messageResourceBundle().use(appRoot() + "release"); messageResourceBundle().use(appRoot() + "release");
messageResourceBundle().use(appRoot() + "releases"); messageResourceBundle().use(appRoot() + "releases");
messageResourceBundle().use(appRoot() + "search"); messageResourceBundle().use(appRoot() + "search");
messageResourceBundle().use(appRoot() + "settings"); messageResourceBundle().use(appRoot() + "settings");
messageResourceBundle().use(appRoot() + "templates");
messageResourceBundle().use(appRoot() + "tracks"); 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 // Handle Media Scanner events and other session events
enableUpdates(true); enableUpdates(true);
@@ -218,8 +217,6 @@ LmsApplication::processPasswordAuth()
} }
} }
setTheme();
// If here is no account in the database, launch the first connection wizard // If here is no account in the database, launch the first connection wizard
bool firstConnection {}; 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 void
LmsApplication::finalize() LmsApplication::finalize()
{ {
@@ -306,7 +290,6 @@ LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool add
if (addText) if (addText)
{ {
res->setWordWrap(false);
res->setTextFormat(Wt::TextFormat::Plain); res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(release->getName())); res->setText(Wt::WString::fromUTF8(release->getName()));
res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain); res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
@@ -315,40 +298,6 @@ LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool add
return res; 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 void
LmsApplication::handleException(LmsApplicationException& e) LmsApplication::handleException(LmsApplicationException& e)
{ {
@@ -407,8 +356,7 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'"; 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) for (const auto& view : views)
{ {
if (wApp->internalPathMatches(view.path)) if (wApp->internalPathMatches(view.path))
@@ -439,7 +387,6 @@ LmsApplication::logoutUser()
void void
LmsApplication::onUserLoggedIn() LmsApplication::onUserLoggedIn()
{ {
setTheme();
root()->clear(); root()->clear();
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent(); 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>(); _coverResource = std::make_shared<CoverResource>();
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}"); 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); 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
_mediaPlayer = main->bindNew<MediaPlayer>("player"); _mediaPlayer = main->bindNew<MediaPlayer>("player");
main->bindNew<Wt::WAnchor>("title", Wt::WLink {Wt::LinkType::InternalPath, defaultPath}, "LMS"); navbar->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")); navbar->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")); navbar->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>("tracks", Wt::WLink {Wt::LinkType::InternalPath, "/tracks"}, Wt::WString::tr("Lms.Explore.tracks"));
Filters* filters {main->bindNew<Filters>("filters")}; Filters* filters {navbar->bindNew<Filters>("filters")};
main->bindNew<Wt::WAnchor>("playqueue", Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"}, Wt::WString::tr("Lms.PlayQueue.playqueue")); navbar->bindNew<Wt::WAnchor>("playqueue", Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"}, Wt::WString::tr("Lms.PlayQueue.playqueue"));
main->bindString("username", getUserLoginName(), Wt::TextFormat::Plain); navbar->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
main->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings")); 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->setText(Wt::WString::tr("Lms.logout"));
logout->clicked().connect(this, &LmsApplication::logoutUser); 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")); searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
if (LmsApp->getUserType() == Database::UserType::ADMIN) if (LmsApp->getUserType() == Database::UserType::ADMIN)
{ {
main->setCondition("if-is-admin", true); navbar->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")); navbar->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->bindNew<Wt::WAnchor>("users", Wt::WLink {Wt::LinkType::InternalPath, "/admin/users"}, Wt::WString::tr("Lms.Admin.Users.menu-users"));
} }
// Contents // Contents
// Order is important in mainStack, see IdxRoot! // Order is important in mainStack, see IdxRoot!
Wt::WStackedWidget* mainStack {main->bindNew<Wt::WStackedWidget>("contents")}; 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)}; Explore* explore {mainStack->addNew<Explore>(filters)};
_playQueue = mainStack->addNew<PlayQueue>(); _playQueue = mainStack->addNew<PlayQueue>();
@@ -578,7 +530,9 @@ LmsApplication::createHome()
{ {
_scannerEvents.scanComplete.connect([=] (const Scanner::ScanStats& stats) _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.nbFiles()))
.arg(static_cast<unsigned>(stats.additions)) .arg(static_cast<unsigned>(stats.additions))
.arg(static_cast<unsigned>(stats.updates)) .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 void
LmsApplication::post(std::function<void()> func) LmsApplication::post(std::function<void()> func)
{ {
Wt::WServer::instance()->post(LmsApp->sessionId(), std::move(func)); Wt::WServer::instance()->post(LmsApp->sessionId(), std::move(func));
} }
void 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) << "'"; LMS_LOG(UI, INFO) << "Notifying message '" << message.toUTF8() << "' for category '" << category.toUTF8() << "'";
_notificationContainer->add(type, category, message, duration);
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());
} }
} // namespace UserInterface } // namespace UserInterface
+7 -17
View File
@@ -27,6 +27,7 @@
#include "services/database/UserId.hpp" #include "services/database/UserId.hpp"
#include "services/database/Types.hpp" #include "services/database/Types.hpp"
#include "services/scanner/ScannerEvents.hpp" #include "services/scanner/ScannerEvents.hpp"
#include "Notification.hpp"
namespace Database namespace Database
{ {
@@ -37,10 +38,6 @@ namespace Database
class Session; class Session;
class User; class User;
} }
namespace Wt
{
class WPopupMenu;
}
namespace UserInterface { namespace UserInterface {
@@ -49,11 +46,12 @@ class LmsApplicationException;
class MediaPlayer; class MediaPlayer;
class PlayQueue; class PlayQueue;
class LmsApplicationManager; class LmsApplicationManager;
class NotificationContainer;
class ModalManager;
class LmsApplication : public Wt::WApplication class LmsApplication : public Wt::WApplication
{ {
public: public:
LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager, std::optional<Database::UserId> userId = std::nullopt); LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationManager& appManager, std::optional<Database::UserId> userId = std::nullopt);
~LmsApplication(); ~LmsApplication();
@@ -78,31 +76,22 @@ class LmsApplication : public Wt::WApplication
void post(std::function<void()> func); void post(std::function<void()> func);
// Used to classify the message sent to the user // Used to classify the message sent to the user
enum class MsgType void notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {5000});
{
Success,
Info,
Warning,
Danger,
};
void notifyMsg(MsgType type, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {4000});
static Wt::WLink createArtistLink(Database::ObjectPtr<Database::Artist> artist); 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 std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::ObjectPtr<Database::Artist> artist, bool addText = true);
static Wt::WLink createReleaseLink(Database::ObjectPtr<Database::Release> release); 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::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; } MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; }
PlayQueue& getPlayQueue() const { return *_playQueue; } PlayQueue& getPlayQueue() const { return *_playQueue; }
ModalManager& getModalManager() const { return *_modalManager; }
// Signal emitted just before the session ends (user may already be logged out) // Signal emitted just before the session ends (user may already be logged out)
Wt::Signal<>& preQuit() { return _preQuit; } Wt::Signal<>& preQuit() { return _preQuit; }
private: private:
void init(); void init();
void setTheme();
void processPasswordAuth(); void processPasswordAuth();
void handleException(LmsApplicationException& e); void handleException(LmsApplicationException& e);
void goHomeAndQuit(); void goHomeAndQuit();
@@ -129,7 +118,8 @@ class LmsApplication : public Wt::WApplication
std::shared_ptr<CoverResource> _coverResource; std::shared_ptr<CoverResource> _coverResource;
MediaPlayer* _mediaPlayer {}; MediaPlayer* _mediaPlayer {};
PlayQueue* _playQueue {}; PlayQueue* _playQueue {};
std::unique_ptr<Wt::WPopupMenu> _popupMenu; NotificationContainer* _notificationContainer {};
ModalManager* _modalManager {};
}; };
+50 -77
View File
@@ -1,89 +1,62 @@
/* /*
* Copyright (C) 2020 Emeric Poupon * Copyright (C) 2020 Emweb bv, Herent, Belgium.
* *
* This file is part of LMS. * See the LICENSE file for terms of use.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "LmsTheme.hpp" #include "LmsTheme.hpp"
#include "utils/Logger.hpp" #include <Wt/WApplication.h>
#include "LmsApplication.hpp" #include <Wt/WLinkedCssStyleSheet.h>
namespace UserInterface { namespace UserInterface
LmsTheme::LmsTheme(Database::UITheme theme)
: _theme {theme}
{ {
setVersion(Wt::BootstrapVersion::v3);
setResponsive(true); // Should be not necessary since applied via head-matter
}
void void
LmsTheme::setTheme(Database::UITheme theme) LmsTheme::init(Wt::WApplication* app) const
{
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)
{ {
case Database::UITheme::Dark: app->require("js/bootstrap.bundle.min.js");
return Wt::WString v = app->metaHeader(Wt::MetaHeaderType::Meta, "viewport");
{ if (v.empty())
{"css/fonts.css"}, app->addMetaHeader("viewport", "width=device-width, initial-scale=1, user-scalable=no");
{"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"},
};
} }
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
View File
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2020 Emeric Poupon * Copyright (C) 2022 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -19,26 +19,27 @@
#pragma once #pragma once
#include <vector> #include <Wt/WTheme.h>
#include <Wt/WBootstrapTheme.h>
#include <Wt/WLinkedCssStyleSheet.h>
#include "services/database/Types.hpp"
namespace UserInterface namespace UserInterface
{ {
class LmsTheme : public Wt::WBootstrapTheme class LmsTheme : public Wt::WTheme
{ {
public:
LmsTheme(Database::UITheme theme);
void setTheme(Database::UITheme theme);
private: private:
std::vector<Wt::WLinkedCssStyleSheet> styleSheets() const override; void init(Wt::WApplication *app) const override;
static std::vector<Wt::WLink> getStyleSheets(Database::UITheme theme);
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; }
}; };
} }
+83
View File
@@ -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. * This file is part of LMS.
* *
@@ -17,21 +17,25 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "TrackStringUtils.hpp" #pragma once
#include <iomanip> #include <Wt/WContainerWidget.h>
#include <sstream> #include <Wt/WSignal.h>
#include <Wt/WWidget.h>
std::string namespace UserInterface
durationToString(std::chrono::milliseconds msDuration)
{ {
const std::chrono::seconds duration {std::chrono::duration_cast<std::chrono::seconds>(msDuration)}; class ModalManager : public Wt::WContainerWidget
{
public:
ModalManager();
std::ostringstream oss; // should handle only one modal at a time
oss << (duration.count() / 60) // Widget must contain a "modal" element
<< ":" void show(std::unique_ptr<Wt::WWidget> modalWidget);
<< std::setfill('0') << std::setw(2) << duration.count() % 60; void dispose(Wt::WWidget* modalWidget);
return oss.str(); private:
Wt::JSignal<std::string> _closed;
};
} }
+11
View File
@@ -0,0 +1,11 @@
#pragma once
namespace UserInterface::Notification
{
enum class Type
{
Info,
Warning,
Danger,
};
}
+86
View File
@@ -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. * This file is part of LMS.
* *
@@ -19,16 +19,19 @@
#pragma once #pragma once
#include <Wt/WWidget.h> #include <chrono>
#include <Wt/WSignal.h> #include <Wt/WContainerWidget.h>
#include <Wt/WString.h>
#include "services/database/Types.hpp" #include "Notification.hpp"
#include "PlayQueueAction.hpp"
namespace UserInterface namespace UserInterface
{ {
void displayTrackPopupMenu(Wt::WInteractWidget& target, class NotificationContainer : public Wt::WContainerWidget
Database::TrackId trackId, {
PlayQueueActionTrackSignal& tracksAction); public:
} // namespace UserInterface void add(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration);
private:
};
} // namespace UserInterface
+62 -95
View File
@@ -19,8 +19,8 @@
#include "PlayQueue.hpp" #include "PlayQueue.hpp"
#include <Wt/WPopupMenu.h> #include <Wt/WCheckBox.h>
#include <Wt/WText.h> #include <Wt/WPushButton.h>
#include "services/database/Cluster.hpp" #include "services/database/Cluster.hpp"
#include "services/database/Release.hpp" #include "services/database/Release.hpp"
@@ -36,34 +36,20 @@
#include "utils/String.hpp" #include "utils/String.hpp"
#include "common/InfiniteScrollingContainer.hpp" #include "common/InfiniteScrollingContainer.hpp"
#include "resource/CoverResource.hpp"
#include "resource/DownloadResource.hpp" #include "resource/DownloadResource.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
#include "TrackStringUtils.hpp" #include "Utils.hpp"
namespace UserInterface { namespace UserInterface {
PlayQueue::PlayQueue() 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); 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); 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([=] clearBtn->clicked().connect([=]
{ {
clearTracks(); 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); 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([=] shuffleBtn->clicked().connect([=]
{ {
{ {
@@ -95,33 +80,33 @@ PlayQueue::PlayQueue()
addSome(); addSome();
}); });
_repeatBtn = bindNew<Wt::WText>("repeat-btn", Wt::WString::tr("Lms.PlayQueue.template.repeat-btn"), Wt::TextFormat::XHTML); _repeatBtn = bindNew<Wt::WCheckBox>("repeat-btn");
setToolTip(*_repeatBtn, Wt::WString::tr("Lms.PlayQueue.repeat"));
_repeatBtn->clicked().connect([=] _repeatBtn->clicked().connect([=]
{ {
_repeatAll = !_repeatAll;
updateRepeatBtn();
auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
if (!LmsApp->getUser()->isDemo()) 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")); _radioBtn = bindNew<Wt::WCheckBox>("radio-btn");
setToolTip(*_radioBtn, Wt::WString::tr("Lms.PlayQueue.radio-mode"));
_radioBtn->clicked().connect([=] _radioBtn->clicked().connect([=]
{ {
_radioMode = !_radioMode;
updateRadioBtn();
auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
if (!LmsApp->getUser()->isDemo()) 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"); _nbTracks = bindNew<Wt::WText>("nb-tracks");
@@ -180,18 +165,16 @@ PlayQueue::PlayQueue()
addSome(); addSome();
} }
void bool
PlayQueue::updateRepeatBtn() PlayQueue::isRepeatAllSet() const
{ {
_repeatBtn->toggleStyleClass("text-success", _repeatAll); return _repeatBtn->checkState() == Wt::CheckState::Checked;
_repeatBtn->toggleStyleClass("text-muted", !_repeatAll);
} }
void bool
PlayQueue::updateRadioBtn() PlayQueue::isRadioModeSet() const
{ {
_radioBtn->toggleStyleClass("text-success",_radioMode); return _radioBtn->checkState() == Wt::CheckState::Checked;
_radioBtn->toggleStyleClass("text-muted", !_radioMode);
} }
Database::TrackList::pointer Database::TrackList::pointer
@@ -243,7 +226,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
// If out of range, stop playing // If out of range, stop playing
if (pos >= tracklist->getCount()) if (pos >= tracklist->getCount())
{ {
if (!_repeatAll || tracklist->getCount() == 0) if (!isRepeatAllSet() || tracklist->getCount() == 0)
{ {
stop(); stop();
return; return;
@@ -253,7 +236,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
} }
// If last and radio mode, fill the next song // If last and radio mode, fill the next song
if (_radioMode && pos == tracklist->getCount() - 1) if (isRadioModeSet() && pos == tracklist->getCount() - 1)
addRadioTrack = true; addRadioTrack = true;
_trackPos = pos; _trackPos = pos;
@@ -313,9 +296,11 @@ PlayQueue::updateCurrentTrack(bool selected)
if (!_trackPos || *_trackPos >= static_cast<std::size_t>(_entriesContainer->getCount())) if (!_trackPos || *_trackPos >= static_cast<std::size_t>(_entriesContainer->getCount()))
return; return;
Wt::WTemplate* entry {static_cast<Wt::WTemplate*>(_entriesContainer->getWidget(*_trackPos))}; Template* entry {static_cast<Template*>(_entriesContainer->getWidget(*_trackPos))};
if (entry) if (!entry)
entry->bindString("is-selected", selected ? "Lms-playqueue-selected" : ""); return;
entry->toggleStyleClass("Lms-entry-playing", selected);
} }
std::size_t std::size_t
@@ -383,12 +368,10 @@ PlayQueue::processTracks(PlayQueueAction action, const std::vector<Database::Tra
} }
if (nbAddedTracks > 0) 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()) 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 void
@@ -412,50 +395,30 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
const auto track {tracklistEntry->getTrack()}; const auto track {tracklistEntry->getTrack()};
const Database::TrackId trackId {track->getId()}; 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); entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
const auto release {track->getRelease()}; 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) if (release)
{ {
entry->setCondition("if-has-release", true); entry->setCondition("if-has-release", true);
entry->bindWidget("release", LmsApplication::createReleaseAnchor(release)); entry->bindWidget("release", LmsApplication::createReleaseAnchor(release));
{ {
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false)); Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))};
auto cover = std::make_unique<Wt::WImage>(); auto cover {Utils::createCover(release->getId(), CoverResource::Size::Small)};
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(release->getId(), CoverResource::Size::Large)); cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK
cover->setStyleClass("Lms-cover");
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
anchor->setImage(std::move(cover)); anchor->setImage(std::move(cover));
} }
} }
else else
{ {
auto cover = entry->bindNew<Wt::WImage>("cover"); auto cover {entry->bindWidget<Wt::WImage>("cover", Utils::createCover(track->getId(), CoverResource::Size::Small))};
cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(track->getId(), CoverResource::Size::Large)); cover->addStyleClass("Lms-cover-track");
cover->setStyleClass("Lms-cover");
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
} }
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)}; Wt::WText* playBtn {entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.PlayQueue.template.play-btn"), Wt::TextFormat::XHTML)};
playBtn->clicked().connect([=] playBtn->clicked().connect([=]
@@ -488,25 +451,29 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
updateInfo(); updateInfo();
}); });
Wt::WText* moreBtn {entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.PlayQueue.template.more-btn"), Wt::TextFormat::XHTML)}; entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.PlayQueue.template.more-btn"), Wt::TextFormat::XHTML);
moreBtn->clicked().connect([=]
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)}; if (isStarred())
popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star")) {
->triggered().connect(moreBtn, [=] Service<Scrobbling::IScrobblingService>::get()->unstar(LmsApp->getUserId(), trackId);
{ starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
if (isStarred) }
Service<Scrobbling::IScrobblingService>::get()->unstar(LmsApp->getUserId(), trackId); else
else {
Service<Scrobbling::IScrobblingService>::get()->star(LmsApp->getUserId(), trackId); Service<Scrobbling::IScrobblingService>::get()->star(LmsApp->getUserId(), trackId);
}); starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
popup->addItem(Wt::WString::tr("Lms.Explore.download")) }
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
popup->popup(moreBtn);
}); });
entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
} }
void void
+8 -7
View File
@@ -21,6 +21,7 @@
#include <optional> #include <optional>
#include <Wt/WCheckBox.h>
#include <Wt/WContainerWidget.h> #include <Wt/WContainerWidget.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include <Wt/WText.h> #include <Wt/WText.h>
@@ -29,6 +30,8 @@
#include "services/database/TrackListId.hpp" #include "services/database/TrackListId.hpp"
#include "PlayQueueAction.hpp" #include "PlayQueueAction.hpp"
#include "common/Template.hpp"
namespace Similarity namespace Similarity
{ {
class Finder; class Finder;
@@ -45,7 +48,7 @@ namespace UserInterface {
class InfiniteScrollingContainer; class InfiniteScrollingContainer;
class PlayQueue : public Wt::WTemplate class PlayQueue : public Template
{ {
public: public:
PlayQueue(); PlayQueue();
@@ -75,8 +78,8 @@ class PlayQueue : public Wt::WTemplate
void enqueueRadioTracks(); void enqueueRadioTracks();
void updateInfo(); void updateInfo();
void updateCurrentTrack(bool selected); void updateCurrentTrack(bool selected);
void updateRepeatBtn(); bool isRepeatAllSet() const;
void updateRadioBtn(); bool isRadioModeSet() const;
void loadTrack(std::size_t pos, bool play); void loadTrack(std::size_t pos, bool play);
void stop(); 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 _nbMaxEntries {1000};
static inline constexpr std::size_t _batchSize {12}; static inline constexpr std::size_t _batchSize {12};
bool _repeatAll {};
bool _radioMode {};
bool _mediaPlayerSettingsLoaded {}; bool _mediaPlayerSettingsLoaded {};
Database::TrackListId _tracklistId {}; Database::TrackListId _tracklistId {};
InfiniteScrollingContainer* _entriesContainer {}; InfiniteScrollingContainer* _entriesContainer {};
Wt::WText* _nbTracks {}; Wt::WText* _nbTracks {};
Wt::WText* _repeatBtn {}; Wt::WCheckBox* _repeatBtn {};
Wt::WText* _radioBtn {}; Wt::WCheckBox* _radioBtn {};
std::optional<std::size_t> _trackPos; // current track position, if set std::optional<std::size_t> _trackPos; // current track position, if set
}; };
+10 -32
View File
@@ -21,7 +21,6 @@
#include <Wt/WCheckBox.h> #include <Wt/WCheckBox.h>
#include <Wt/WComboBox.h> #include <Wt/WComboBox.h>
#include <Wt/WDoubleValidator.h>
#include <Wt/WDoubleSpinBox.h> #include <Wt/WDoubleSpinBox.h>
#include <Wt/WFormModel.h> #include <Wt/WFormModel.h>
#include <Wt/WLineEdit.h> #include <Wt/WLineEdit.h>
@@ -29,8 +28,9 @@
#include <Wt/WString.h> #include <Wt/WString.h>
#include <Wt/WTemplateFormView.h> #include <Wt/WTemplateFormView.h>
#include "common/PasswordValidator.hpp" #include "common/DoubleValidator.hpp"
#include "common/MandatoryValidator.hpp" #include "common/MandatoryValidator.hpp"
#include "common/PasswordValidator.hpp"
#include "common/UUIDValidator.hpp" #include "common/UUIDValidator.hpp"
#include "common/ValueStringModel.hpp" #include "common/ValueStringModel.hpp"
@@ -42,7 +42,6 @@
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "LmsTheme.hpp"
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
namespace UserInterface { namespace UserInterface {
@@ -53,7 +52,6 @@ class SettingsModel : public Wt::WFormModel
{ {
public: public:
// Associate each field with a unique string literal. // 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 TranscodeModeField {"transcode-mode"};
static inline const Field TranscodeFormatField {"transcode-format"}; static inline const Field TranscodeFormatField {"transcode-format"};
static inline const Field TranscodeBitrateField {"transcode-bitrate"}; static inline const Field TranscodeBitrateField {"transcode-bitrate"};
@@ -80,7 +78,6 @@ class SettingsModel : public Wt::WFormModel
{ {
initializeModels(); initializeModels();
addField(DarkModeField);
addField(TranscodeModeField); addField(TranscodeModeField);
addField(TranscodeBitrateField); addField(TranscodeBitrateField);
addField(TranscodeFormatField); addField(TranscodeFormatField);
@@ -111,12 +108,10 @@ class SettingsModel : public Wt::WFormModel
setValidator(TranscodeBitrateField, createMandatoryValidator()); setValidator(TranscodeBitrateField, createMandatoryValidator());
setValidator(TranscodeFormatField, createMandatoryValidator()); setValidator(TranscodeFormatField, createMandatoryValidator());
setValidator(ReplayGainModeField, createMandatoryValidator()); setValidator(ReplayGainModeField, createMandatoryValidator());
auto createPreAmpValidator = [] auto createPreAmpValidator {[]
{ {
auto preampGainValidator {std::make_unique<Wt::WDoubleValidator>()}; return createDoubleValidator(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
preampGainValidator->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); }};
return preampGainValidator;
};
setValidator(ReplayGainPreAmpGainField, createPreAmpValidator()); setValidator(ReplayGainPreAmpGainField, createPreAmpValidator());
setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator()); setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator());
@@ -139,14 +134,6 @@ class SettingsModel : public Wt::WFormModel
User::pointer user {LmsApp->getUser()}; 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; MediaPlayer::Settings settings;
@@ -200,7 +187,6 @@ class SettingsModel : public Wt::WFormModel
{ {
_authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8()); _authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8());
} }
} }
void loadData() void loadData()
@@ -209,8 +195,6 @@ class SettingsModel : public Wt::WFormModel
User::pointer user {LmsApp->getUser()}; User::pointer user {LmsApp->getUser()};
setValue(DarkModeField, user->getUITheme() == UITheme::Dark);
{ {
const auto settings {*LmsApp->getMediaPlayer().getSettings()}; const auto settings {*LmsApp->getMediaPlayer().getSettings()};
@@ -408,12 +392,6 @@ SettingsView::refreshView()
auto model {std::make_shared<SettingsModel>(authPasswordService, !LmsApp->isUserAuthStrong())}; 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) if (authPasswordService)
{ {
t->setCondition("if-has-change-password", true); 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 *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")))}; 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()}; auto transaction {LmsApp->getDbSession().createSharedTransaction()};
if (LmsApp->getUser()->isDemo()) 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; return;
} }
} }
@@ -574,19 +552,19 @@ SettingsView::refreshView()
if (model->validate()) if (model->validate())
{ {
model->saveData(); 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. // Udate the view: Delete any validation message in the view, etc.
t->updateView(model.get()); t->updateView(model.get());
}); });
discardBtn->clicked().connect(std::bind([=] () discardBtn->clicked().connect([=]
{ {
model->loadData(); model->loadData();
model->validate(); model->validate();
t->updateView(model.get()); t->updateView(model.get());
})); });
t->updateView(model.get()); t->updateView(model.get());
} }
+103
View File
@@ -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;
}
}
+43
View File
@@ -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);
}
+2 -9
View File
@@ -69,12 +69,11 @@ class DatabaseSettingsModel : public Wt::WFormModel
auto dirValidator {createDirectoryValidator()}; auto dirValidator {createDirectoryValidator()};
dirValidator->setMandatory(true); dirValidator->setMandatory(true);
setValidator(MediaDirectoryField, dirValidator); setValidator(MediaDirectoryField, move(dirValidator));
setValidator(UpdatePeriodField, createMandatoryValidator()); setValidator(UpdatePeriodField, createMandatoryValidator());
setValidator(UpdateStartTimeField, createMandatoryValidator()); setValidator(UpdateStartTimeField, createMandatoryValidator());
setValidator(RecommendationEngineTypeField, createMandatoryValidator()); setValidator(RecommendationEngineTypeField, createMandatoryValidator());
setValidator(TagsField, createTagsValidator());
// populate the model with initial data // populate the model with initial data
loadData(); loadData();
@@ -144,12 +143,6 @@ class DatabaseSettingsModel : public Wt::WFormModel
} }
private: private:
static std::shared_ptr<Wt::WValidator> createTagsValidator()
{
auto v = std::make_shared<Wt::WValidator>();
return v;
}
void initializeModels() void initializeModels()
{ {
_updatePeriodModel = std::make_shared<ValueStringModel<ScanSettings::UpdatePeriod>>(); _updatePeriodModel = std::make_shared<ValueStringModel<ScanSettings::UpdatePeriod>>();
@@ -241,7 +234,7 @@ DatabaseSettingsView::refreshView()
model->saveData(); model->saveData();
Service<Scanner::IScannerService>::get()->requestImmediateScan(false); 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. // Udate the view: Delete any validation message in the view, etc.
+7 -2
View File
@@ -41,7 +41,6 @@ namespace UserInterface {
class InitWizardModel : public Wt::WFormModel class InitWizardModel : public Wt::WFormModel
{ {
public: public:
// Associate each field with a unique string literal. // Associate each field with a unique string literal.
static inline const Field AdminLoginField {"admin-login"}; static inline const Field AdminLoginField {"admin-login"};
static inline const Field PasswordField {"password"}; static inline const Field PasswordField {"password"};
@@ -126,6 +125,11 @@ InitWizardView::InitWizardView()
setFormWidget(InitWizardModel::PasswordConfirmField, std::move(passwordConfirmEdit)); 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")); Wt::WPushButton* saveButton = bindNew<Wt::WPushButton>("create-btn", Wt::WString::tr("Lms.create"));
saveButton->clicked().connect([=] saveButton->clicked().connect([=]
{ {
@@ -134,7 +138,8 @@ InitWizardView::InitWizardView()
if (model->validate()) if (model->validate())
{ {
model->saveData(); 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); saveButton->setEnabled(false);
} }
-1
View File
@@ -27,7 +27,6 @@ class InitWizardView : public Wt::WTemplateFormView
{ {
public: public:
InitWizardView(); InitWizardView();
}; };
} // namespace UserInterface } // namespace UserInterface
+69 -49
View File
@@ -23,10 +23,8 @@
#include <Wt/Http/Response.h> #include <Wt/Http/Response.h>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include <Wt/WPopupMenu.h>
#include <Wt/WPushButton.h> #include <Wt/WPushButton.h>
#include <Wt/WResource.h> #include <Wt/WResource.h>
#include <Wt/WSplitButton.h>
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
#include "services/database/Track.hpp" #include "services/database/Track.hpp"
@@ -57,9 +55,7 @@ durationToString(const Wt::WDateTime& begin, const Wt::WDateTime& end)
class ReportResource : public Wt::WResource class ReportResource : public Wt::WResource
{ {
public: public:
ReportResource()
ReportResource(const Scanner::ScanStats& stats)
: _stats {stats}
{ {
suggestFileName("report.txt"); suggestFileName("report.txt");
} }
@@ -69,11 +65,22 @@ class ReportResource : public Wt::WResource
beingDeleted(); 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) 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(); response.out() << error.file.string() << " - " << errorTypeToWString(error.error).toUTF8();
if (!error.systemError.empty()) if (!error.systemError.empty())
@@ -83,12 +90,12 @@ class ReportResource : public Wt::WResource
response.out() << std::endl; 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()}; 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)}; const auto& track {Database::Track::find(LmsApp->getDbSession(), duplicate.trackId)};
if (!track) if (!track)
@@ -127,7 +134,7 @@ class ReportResource : public Wt::WResource
return "?"; return "?";
} }
Scanner::ScanStats _stats; std::unique_ptr<Scanner::ScanStats> _stats;
}; };
@@ -135,14 +142,48 @@ ScannerController::ScannerController()
: WTemplate {Wt::WString::tr("Lms.Admin.ScannerController.template")} : WTemplate {Wt::WString::tr("Lms.Admin.ScannerController.template")}
{ {
addFunction("tr", &Wt::WTemplate::Functions::tr); addFunction("tr", &Wt::WTemplate::Functions::tr);
addFunction("id", &Wt::WTemplate::Functions::id);
using namespace Scanner; 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->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().scanComplete.connect(this, onDbEvent);
LmsApp->getScannerEvents().scanInProgress.connect(this, onDbEvent); LmsApp->getScannerEvents().scanInProgress.connect(this, onDbEvent);
@@ -156,30 +197,10 @@ ScannerController::refreshContents()
{ {
using namespace Scanner; 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()}; const IScannerService::Status status {Service<IScannerService>::get()->getStatus()};
if (status.lastCompleteScanStats) 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(status.lastCompleteScanStats->nbFiles())
.arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime)) .arg(durationToString(status.lastCompleteScanStats->startTime, status.lastCompleteScanStats->stopTime))
.arg(status.lastCompleteScanStats->stopTime.toString()) .arg(status.lastCompleteScanStats->stopTime.toString())
@@ -187,66 +208,65 @@ ScannerController::refreshContents()
.arg(status.lastCompleteScanStats->duplicates.size()) .arg(status.lastCompleteScanStats->duplicates.size())
); );
Wt::WLink link {std::make_shared<ReportResource>(*status.lastCompleteScanStats)}; _reportResource->setScanStats(*status.lastCompleteScanStats);
link.setTarget(Wt::LinkTarget::NewWindow); _reportBtn->setEnabled(true);
reportBtn->setLink(link);
} }
else else
{ {
bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-not-available")); _lastScanStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.last-scan-not-available"));
reportBtn->setEnabled(false); _reportBtn->setEnabled(false);
} }
switch (status.currentState) switch (status.currentState)
{ {
case IScannerService::State::NotScheduled: case IScannerService::State::NotScheduled:
bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-not-scheduled")); _status->setText(Wt::WString::tr("Lms.Admin.ScannerController.status-not-scheduled"));
bindEmpty("step-status"); _stepStatus->setText("");
break; break;
case IScannerService::State::Scheduled: 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())); .arg(status.nextScheduledScan.toString()));
bindEmpty("step-status"); _stepStatus->setText("");
break; break;
case IScannerService::State::InProgress: 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(static_cast<int>(status.currentScanStepStats->currentStep) + 1)
.arg(Scanner::ScanProgressStepCount)); .arg(Scanner::ScanProgressStepCount));
switch (status.currentScanStepStats->currentStep) switch (status.currentScanStepStats->currentStep)
{ {
case Scanner::ScanProgressStep::ChekingForMissingFiles: 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())); .arg(status.currentScanStepStats->progress()));
break; break;
case Scanner::ScanProgressStep::DiscoveringFiles: 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)); .arg(status.currentScanStepStats->processedElems));
break; break;
case Scanner::ScanProgressStep::ScanningFiles: 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->processedElems)
.arg(status.currentScanStepStats->totalElems) .arg(status.currentScanStepStats->totalElems)
.arg(status.currentScanStepStats->progress())); .arg(status.currentScanStepStats->progress()));
break; break;
case Scanner::ScanProgressStep::FetchingTrackFeatures: 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->processedElems)
.arg(status.currentScanStepStats->totalElems) .arg(status.currentScanStepStats->totalElems)
.arg(status.currentScanStepStats->progress())); .arg(status.currentScanStepStats->progress()));
break; break;
case Scanner::ScanProgressStep::ReloadingSimilarityEngine: 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())); .arg(status.currentScanStepStats->progress()));
break; break;
} }
break; break;
} }
} }
} // namespace UserInterface } // namespace UserInterface
+8
View File
@@ -19,7 +19,9 @@
#pragma once #pragma once
#include <Wt/WPushButton.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include <Wt/WLineEdit.h>
namespace UserInterface namespace UserInterface
{ {
@@ -31,6 +33,12 @@ namespace UserInterface
private: private:
void refreshContents(); void refreshContents();
Wt::WPushButton* _reportBtn;
Wt::WLineEdit* _lastScanStatus;
Wt::WLineEdit* _status;
Wt::WLineEdit* _stepStatus;
class ReportResource* _reportResource;
}; };
} // namespace DatabaseStatus } // namespace DatabaseStatus
+3 -2
View File
@@ -47,7 +47,6 @@ using namespace Database;
class UserModel : public Wt::WFormModel class UserModel : public Wt::WFormModel
{ {
public: public:
static inline const Field LoginField {"login"}; static inline const Field LoginField {"login"};
static inline const Field PasswordField {"password"}; static inline const Field PasswordField {"password"};
@@ -252,7 +251,9 @@ UserView::refreshView()
if (model->validate()) if (model->validate())
{ {
model->saveData(); 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); LmsApp->setInternalPath("/admin/users", true);
} }
else else
+20 -18
View File
@@ -30,6 +30,7 @@
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "ModalManager.hpp"
namespace UserInterface { namespace UserInterface {
@@ -101,30 +102,31 @@ UsersView::refreshView()
Wt::WPushButton* delBtn = entry->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.Admin.Users.del")); Wt::WPushButton* delBtn = entry->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.Admin.Users.del"));
delBtn->clicked().connect([=] delBtn->clicked().connect([=]
{ {
auto msgBox = delBtn->addChild(std::make_unique<Wt::WMessageBox>(Wt::WString::tr("Lms.Admin.Users.del-user"), auto modal {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.Users.template.delete-user"))};
Wt::WString::tr("Lms.Admin.Users.del-user-name"), modal->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::Icon::Warning, Wt::StandardButton::Yes | Wt::StandardButton::No)); Wt::WWidget* modalPtr {modal.get()};
msgBox->setModal(true); auto* delBtn {modal->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.Admin.Users.del"))};
msgBox->buttonClicked().connect([=] (Wt::StandardButton btn) 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)}; User::pointer user {User::find(LmsApp->getDbSession(), userId)};
if (user) if (user)
user.remove(); user.remove();
_container->removeWidget(entry); _container->removeWidget(entry);
}
else LmsApp->getModalManager().dispose(modalPtr);
{
delBtn->removeChild(msgBox);
}
}); });
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));
}); });
} }
} }
+3 -2
View File
@@ -25,8 +25,9 @@ namespace UserInterface
{ {
class DirectoryValidator : public Wt::WValidator class DirectoryValidator : public Wt::WValidator
{ {
public: private:
Wt::WValidator::Result validate(const Wt::WString& input) const override; Wt::WValidator::Result validate(const Wt::WString& input) const override;
std::string javaScriptValidate() const override { return {}; }
}; };
Wt::WValidator::Result Wt::WValidator::Result
@@ -48,7 +49,7 @@ namespace UserInterface
return Wt::WValidator::Result(Wt::ValidationState::Invalid, Wt::WString::tr("Lms.not-a-directory")); 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() createDirectoryValidator()
{ {
return std::make_unique<DirectoryValidator>(); return std::make_unique<DirectoryValidator>();
+1 -1
View File
@@ -23,6 +23,6 @@
namespace UserInterface namespace UserInterface
{ {
std::shared_ptr<Wt::WValidator> createDirectoryValidator(); std::unique_ptr<Wt::WValidator> createDirectoryValidator();
} // namespace UserInterface } // namespace UserInterface
+41
View File
@@ -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. * This file is part of LMS.
* *
@@ -19,9 +19,10 @@
#pragma once #pragma once
#include <chrono> #include <Wt/WValidator.h>
#include <string>
std::string namespace UserInterface
durationToString(std::chrono::milliseconds msDuration); {
std::unique_ptr<Wt::WValidator> createDoubleValidator(double min, double max);
} // namespace UserInterface
+8 -2
View File
@@ -24,10 +24,16 @@
namespace UserInterface 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() createLoginNameValidator()
{ {
auto v = std::make_unique<Wt::WLengthValidator>(); auto v = std::make_unique<LengthValidator>();
v->setMandatory(true); v->setMandatory(true);
v->setMinimumLength(::Database::User::MinNameLength); v->setMinimumLength(::Database::User::MinNameLength);
v->setMaximumLength(::Database::User::MaxNameLength); v->setMaximumLength(::Database::User::MaxNameLength);
+1 -1
View File
@@ -23,6 +23,6 @@
namespace UserInterface namespace UserInterface
{ {
std::shared_ptr<Wt::WValidator> createLoginNameValidator(); std::unique_ptr<Wt::WValidator> createLoginNameValidator();
} // namespace UserInterface } // namespace UserInterface
+8 -2
View File
@@ -21,10 +21,16 @@
namespace UserInterface 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() createMandatoryValidator()
{ {
auto v {std::make_shared<Wt::WValidator>()}; auto v {std::make_unique<MandatoryValidator>()};
v->setMandatory(true); v->setMandatory(true);
return v; return v;
} }

Some files were not shown because too many files have changed in this diff Show More