Merge pull request #159 from TylerVigario/node-build-flow

Implement Node build flow
This commit is contained in:
Terry Geng 2020-06-21 22:22:15 +08:00 committed by GitHub
commit fa246553a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 9836 additions and 1762 deletions

2
.gitignore vendored
View File

@ -117,4 +117,4 @@ tmp/
*.db *.db
# Pycharm # Pycharm
.idea/ .idea/

2
static/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
css/
js/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,49 +1 @@
<!DOCTYPE html> <!doctype html><head><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta charset="UTF-8"><title>botamusique web interface</title><link rel="icon" href="static/image/favicon.ico"/><link rel="stylesheet" href="static/css/main.css"></head><body><div class="container" style="max-width: 800px"><div class="col-8" style="margin: auto; padding-top: 50px;"><div class="card"><div class="card-header">Token Required</div><div class="card-body"><h3>Token Required!</h3>You are accessing the web interface of {{ name }}. A token is needed to grant you access.<br/>Please send "{{ command }}" to the bot in mumble to acquire one.<form action="."><div class="form-group mt-3"><label for="token_input">Token</label><div class="input-group"><input type="password" class="form-control btn-space" id="token_input" name="token" placeholder="xxxxxxx"> <button type="submit" class="btn btn-primary">Submit</button></div></div></form></div></div></div></div><script src="static/js/main.js"></script></body>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta charset="UTF-8">
<title>botamusique web interface</title>
<link rel="icon" href="static/image/favicon.ico" />
<link id="pagestyle" rel="stylesheet" href="static/css/bootstrap.min.css">
<link rel="stylesheet" href="static/css/custom.css">
</head>
<body>
<div class="container" style="max-width: 800px">
<div class="col-8" style="margin: auto; padding-top: 50px;">
<div class="card">
<div class="card-header">
Token Required
</div>
<div class="card-body">
<h3>Token Required!</h3>
You are accessing the web interface of {{ name }}.
A token is needed to grant you access.<br />
Please send "{{ command }}" to the bot in mumble to acquire one.
<form action="." method="get">
<div class="form-group mt-3">
<label for="token_input">Token</label>
<div class="input-group">
<input type="password" class="form-control btn-space" id="token_input" name="token"
placeholder="xxxxxxx">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="static/js/jquery-3.5.1.min.js"></script>
<script src="static/js/popper.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
<script src="static/js/fontawesome.all.min.js"></script>
</body>
</html>

12
web/.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
quote_type = single
[*.json]
quote_type = double

38
web/.eslintrc.json Normal file
View File

@ -0,0 +1,38 @@
{
"env": {
"browser": true,
"es6": true,
"es2017": true,
"es2020": true,
"jquery": true
},
"plugins": [
"node",
"import",
"jsdoc"
],
"extends": [
"eslint:recommended",
"google",
"plugin:node/recommended-module",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:jsdoc/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "babel-eslint",
"rules": {
"require-jsdoc": "off",
"valid-jsdoc": "off",
"jsdoc/require-jsdoc": "off",
"max-len": ["warn", {
"code": 120
}],
"import/no-commonjs": "error",
"import/no-amd": "error",
"linebreak-style": "off"
}
}

1
web/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

7757
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

55
web/package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "botamusique",
"private": true,
"type": "module",
"scripts": {
"lint": "eslint --config .eslintrc.json src/js/ --ext .mjs",
"lint:fix": "npm run lint -- --fix",
"build": "webpack --config webpack.config.cjs --progress",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/azlux/botamusique.git"
},
"author": "azlux",
"license": "MIT",
"bugs": {
"url": "https://github.com/azlux/botamusique/issues"
},
"homepage": "https://github.com/azlux/botamusique#readme",
"devDependencies": {
"@babel/core": "^7.10.3",
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/preset-env": "^7.10.3",
"autoprefixer": "^9.8.1",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"core-js": "^3.6.5",
"css-loader": "^3.6.0",
"eslint": "^7.3.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.21.2",
"eslint-plugin-jsdoc": "^27.1.2",
"eslint-plugin-node": "^11.1.0",
"html-webpack-plugin": "^4.3.0",
"mini-css-extract-plugin": "^0.9.0",
"postcss-loader": "^3.0.0",
"regenerator-runtime": "^0.13.5",
"sass": "^1.26.8",
"sass-loader": "^8.0.2",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.29",
"@fortawesome/free-brands-svg-icons": "^5.13.1",
"@fortawesome/free-regular-svg-icons": "^5.13.1",
"@fortawesome/free-solid-svg-icons": "^5.13.1",
"bootstrap": "^4.5.0",
"bootswatch": "^4.5.0",
"jquery": "^3.5.1",
"jquery-migrate": "^3.3.0",
"popper.js": "^1.16.1"
}
}

21
web/scripts/build-css.js Normal file
View File

@ -0,0 +1,21 @@
const sass = require('node-sass');
const fs = require('fs');
sass.render({
file: './src/sass/main.scss',
outFile: './build/assets/css/main.min.css',
outputStyle: 'compressed',
}, function (error, result) {
if (error) {
console.error(error);
}
// No errors during compilation, write the result to disk
fs.writeFile('./build/assets/css/main.min.css', result.css, function (err) {
if (err) {
console.error(err);
}
console.info('CSS compiled.');
});
});

23
web/src/js/app.mjs Normal file
View File

@ -0,0 +1,23 @@
import {library, dom} from '@fortawesome/fontawesome-svg-core/index.es';
import {fas} from '@fortawesome/free-solid-svg-icons/index.es';
import {far} from '@fortawesome/free-regular-svg-icons/index.es';
library.add(fas, far);
// Old application code
import './main.mjs';
// New application code
import Theme from './theme.mjs';
document.addEventListener('DOMContentLoaded', () => {
Theme.init();
// Replace any existing <i> tags with <svg> and set up a MutationObserver to
// continue doing this as the DOM changes.
dom.watch();
document.getElementById('theme-switch-btn').addEventListener('click', () => {
Theme.swap();
});
});

1210
web/src/js/main.mjs Normal file

File diff suppressed because it is too large Load Diff

42
web/src/js/theme.mjs Normal file
View File

@ -0,0 +1,42 @@
export default class {
/**
* @property {boolean} #dark Interal state for dark theme activation.
* @private
*/
static #dark = false;
/**
* Inialize the theme class.
*/
static init() {
// Check LocalStorage for dark theme selection
if (localStorage.getItem('darkTheme') === 'true') {
// Update page theme
this.set(true);
}
}
/**
* Set page theme and update local storage variable.
*
* @param {boolean} dark Whether to activate dark theme.
*/
static set(dark = false) {
// Swap CSS to selected theme
document.getElementById('pagestyle')
.setAttribute('href', 'static/css/' + (dark ? 'dark' : 'main') + '.css');
// Update local storage
localStorage.setItem('darkTheme', dark);
// Update internal state
this.#dark = dark;
}
/**
* Swap page theme.
*/
static swap() {
this.set(!this.#dark);
}
}

18
web/src/js/util.js Normal file
View File

@ -0,0 +1,18 @@
export function isOverflown(element) {
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
}
export function setProgressBar(bar, progress, text = '') {
const progPos = (-1 * (1 - progress) * bar.scrollWidth).toString();
const progStr = (progress * 100).toString();
bar.setAttribute('aria-valuenow', progStr);
bar.style.transform = "translateX(" + progPos + "px)";
bar.textContent = text;
}
export function secondsToStr(seconds) {
seconds = Math.floor(seconds);
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return ('00' + mins).slice(-2) + ':' + ('00' + secs).slice(-2);
}

View File

@ -0,0 +1,5 @@
@import '~bootswatch/dist/darkly/variables';
@import '~bootstrap/scss/bootstrap';
@import '~bootswatch/dist/darkly/bootswatch';
@import './main';

3
web/src/sass/app.scss Normal file
View File

@ -0,0 +1,3 @@
@import '~bootstrap/scss/bootstrap';
@import './main';

View File

@ -0,0 +1,516 @@
<!DOCTYPE html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta charset="UTF-8">
<title>botamusique web interface</title>
<link rel="icon" href="static/image/favicon.ico" />
<link id="pagestyle" rel="stylesheet" href="static/css/main.css">
</head>
<body>
<div class="container page-header mb-5" id="banner">
<div class="row">
<div class="col-auto">
<img src="static/image/logo.png" height="200px">
</div>
<div class="col my-auto">
<h1>botamusique Web Interface</h1>
</div>
</div>
</div>
<div id="playlist" class="container mb-5">
<div class="btn-toolbar mb-2" role="toolbar" aria-label="Playlist controls">
<button type="button" id="play-pause-btn" class="btn btn-info mb-2 btn-space">
<i class="fas fa-play"></i>
</button>
<button type="button" id="fast-forward-btn" class="btn btn-info mb-2">
<i class="fas fa-fast-forward"></i>
</button>
<div class="ml-auto">
<div class="dropdown mr-2">
<button class="btn btn-secondary dropdown-toggle" type="button" id="play-mode" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-tasks mr-2" aria-hidden="true" id="modeIndicator"></i>
</button>
<div class="dropdown-menu" aria-labelledby="play-mode">
<a class="dropdown-item" href="#" id="one-shot-mode-btn">
<i class="fas fa-tasks mr-2" aria-hidden="true"></i>One-shot
</a>
<a class="dropdown-item" href="#" id="random-mode-btn">
<i class="fas fa-random mr-2" aria-hidden="true"></i>Random
</a>
<a class="dropdown-item" href="#" id="repeat-mode-btn">
<i class="fas fa-redo mr-2" aria-hidden="true"></i>Repeat
</a>
<a class="dropdown-item" href="#" id="autoplay-mode-btn">
<i class="fas fa-robot mr-2" aria-hidden="true"></i>Autoplay
</a>
</div>
</div>
<button type="button" id="volume-popover-btn" class="btn btn-warning ml-1">
<i class="fa fa-volume-up" aria-hidden="true"></i>
</button>
<div id="volume-popover">
<a id="volume-down-btn">
<i class="fa fa-volume-down" aria-hidden="true"></i>
</a>
<input type="range" class="custom-range ml-1" id="volume-slider" min="0" max="1" step="0.01" value="0.5" />
<a id="volume-up-btn">
<i class="fa fa-volume-up" aria-hidden="true"></i>
</a>
<div id="volume-popover-arrow"></div>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col" class="d-none d-md-table-cell">#</th>
<th scope="col" class="w-50">Title</th>
<th scope="col" class="d-none d-md-table-cell">Url/Path</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody id="playlist-table" class="playlist-table">
<tr id="playlist-loading">
<td colspan="4" class="text-center">
<img style="margin: auto; width: 35px;" src="static/image/loading.svg" />
</td>
</tr>
<tr id="playlist-empty" class="d-none">
<td colspan="4" class="text-center">
<img style="margin: auto; width: 35px;" src="static/image/empty_box.svg" />
</td>
</tr>
<tr class="playlist-expand table-dark d-none">
<td colspan="4" class="text-center">
<a class="text-muted" href="javascript:">See item <span class="playlist-expand-item-range"></span> on the playlist.</a>
</td>
</tr>
<tr class="playlist-item-template d-none">
<th scope="row" class="playlist-item-index d-none d-md-table-cell"></th>
<td>
<input hidden type="text" class="playlist-item-id" value="" />
<div class="float-left">
<img width="80" class="playlist-item-thumbnail" src="static/image/unknown-album.png" />
</div>
<div class="playlist-artwork">
<b class="playlist-item-title"></b>
<span class="playlist-item-type badge badge-secondary"></span>
<br />
<span class="playlist-item-artist"></span>
<br />
<div class="playlist-item-tags">
<a class="playlist-item-edit tag-space tag-click">
<i class="fas fa-edit" style="color: #AAAAAA"></i>
</a>
</div>
</div>
</td>
<td class="d-none d-md-table-cell">
<small class="playlist-item-path"></small>
</td>
<td>
<div class="btn-group">
<button type="button" class="playlist-item-play btn btn-info btn-sm">
<i class="fas fa-play" aria-hidden="true"></i>
</button>
<button type="button" class="playlist-item-trash btn btn-danger btn-sm ml-1">
<i class="fas fa-trash-alt" aria-hidden="true"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="btn-group">
<button type="button" id="clear-playlist-btn" class="btn btn-danger mr-1">
<i class="fas fa-trash-alt" aria-hidden="true"></i> Clear Playlist
</button>
</div>
</div>
<div class="container mb-3">
<h2 id="forms">Music Library</h2>
<div class="card mb-3">
<div class="card-header">
<h5 class="card-title">Filters</h5>
<hr>
<div class="row">
<div class="col">
<label>Type</label>
<div id="filter-type" class="input-group mb-2">
<div class="btn-group btn-group-sm btn-group-toggle">
<label id="filter-type-file" class="btn btn-secondary">
<input type="checkbox" name="options"> File
</label>
<label id="filter-type-url" class="btn btn-secondary">
<input type="checkbox" name="options"> URL
</label>
<label id="filter-type-radio" class="btn btn-secondary">
<input type="checkbox" name="options"> Radio
</label>
</div>
</div>
<label for="filter-dir">Directory</label>
<div id="filter-path" class="input-group mb-2">
<select class="form-control form-control-sm" id="filter-dir" disabled>
<option value="">.</option>
{% for dir in dirs %}
<option value="{{ dir }}">{{ dir }}</option>
{% endfor %}
</select>
</div>
<label for="filter-keywords">Keywords</label>
<div id="filter-path" class="input-group mb-2">
<input class="form-control form-control-sm" id="filter-keywords" name="keywords" placeholder="Keywords..." style="margin-top:5px;" />
</div>
</div>
<div class="col">
<label for="filter-tag">Tags</label>
<div id="filter-type mb-2">
{% for tag in tags_color_lookup.keys() %}
<span id="filter-tag" class="filter-tag tag-unclicked tag-click badge badge-{{ tags_color_lookup[tag] }}">{{ tag }}</span>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="card-body">
<div id="library-group" class="list-group library-group" style="overflow: auto;">
<div id="library-item-loading" class="list-group-item library-item">
<img style="margin: auto; width: 35px;" src="static/image/loading.svg" />
</div>
<div id="library-item-empty" style="display: none" class="list-group-item library-item">
<img style="margin: auto; width: 35px;" src="static/image/empty_box.svg" />
</div>
<div id="library-item" style="display: none;" class="list-group-item library-item">
<input hidden type="text" class="library-item-id" value="" />
<div class="library-thumb-col">
<div class="library-thumb-img">
<img class="library-item-thumb library-thumb-img" src="static/image/unknown-album.png" />
</div>
<div class="btn-group-vertical library-thumb-grp">
<div class="library-item-play btn btn-secondary library-thumb-btn-up" title="Play">
<i class="fas fa-play" aria-hidden="true"></i>
</div>
</div>
</div>
<div class="library-info-col library-info-title col-5" style="padding: 12px 0;">
<div>
<span class="library-item-type lead text-muted btn-space">[File]</span>
<span class="library-item-title lead btn-space">This is my title</span>
<span class="library-item-artist text-muted"> - Artist</span>
</div>
</div>
<div class="library-info-col col-4 d-none d-md-flex" style="padding: 3px;">
<span class="library-item-path text-muted path">Path/to/the/file</span>
<div class="library-item-tags">
<a class="tag-space tag-click library-item-edit"><i class="fas fa-edit" style="color: #AAAAAA"></i></a>
<span class="library-item-notag badge badge-light text-muted font-italic">No tag</span>
<span class="library-item-tag tag-space badge">Tag</span>
</div>
</div>
<div class="btn-group library-action">
<button class="library-item-add-next btn btn-info btn-sm btn-space" type="button" title="Next to play">
<svg class="library-btn-svg" style="width: 1rem; fill: currentColor;" viewBox="5 5 17 17">
<path d="m5.700245,3.92964l0,14.150376l11.451127,-7.075188l-11.451127,-7.075188z"></path>
<path d="m20.942859,18.221072l-3.323292,0l0,3.323292l-1.107764,0l0,-3.323292l-3.323292,0l0,-1.107764l3.323292,0l0,-3.323292l1.107764,0l0,3.323292l3.323292,0l0,1.107764z"></path>
</svg>
</button>
<button class="library-item-add-bottom library-btn btn btn-info btn-sm btn-space" type="button" title="Add to bottom">
<svg class="library-btn-svg" style="width: 1rem; fill: currentColor;" viewBox="2 2 20 20">
<path d="M2,16H10V14H2M18,14V10H16V14H12V16H16V20H18V16H22V14M14,6H2V8H14M14,10H2V12H14V10Z"></path>
</svg>
</button>
<button class="library-item-download btn btn-primary btn-sm btn-space" type="button">
<i class="fas fa-download" aria-hidden="true"></i>
</button>
<button class="library-item-trash btn btn-danger btn-sm btn-space" type="button">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
</div>
<div class="list-group">
<div id="library-pagination" style="margin-left: auto; margin-top: 10px;">
<ul id="library-page-ul" class="pagination pagination">
<li class="library-page-li page-item active">
<a class="library-page-no page-link">1</a>
</li>
</ul>
</div>
</div>
<div class="btn-group mb-2" role="group">
<button id="add-to-playlist-btn" type="button" class="btn btn-secondary mr-1">
<i class="fas fa-plus" aria-hidden="true"></i> Add All
</button>
<button id="library-rescan-btn" type="button" class="btn btn-secondary mr-1">
<i class="fas fa-sync-alt" aria-hidden="true"></i> Rescan Files
</button>
<button id="library-download-btn" type="button" class="btn btn-secondary mr-1">
<i class="fas fa-download" aria-hidden="true"></i> Download All
</button>
<button type="button" class="btn btn-danger mr-1" data-toggle="modal" data-target="#deleteWarningModal">
<i class="fas fa-trash-alt" aria-hidden="true"></i> Delete All
</button>
</div>
<div class="modal fade" id="deleteWarningModal" tabindex="-1" role="dialog" aria-labelledby="Warning-Delete-File" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteWarningModalLabel">Are you really sure?</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
All files listed here, include files on other pages, will be deleted from your
hard-drive.
Is that what you want?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button id="library-delete-btn" type="button" class="btn btn-danger" data-dismiss="modal">Delete All Listed Files</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- beautify ignore:start -->
{% if upload_enabled %}
<div id="upload" class="container mb-3">
{% else %}
<div id="upload" class="container mb-3" style="display: none;">
<input type="hidden" id="uploadDisabled" value="true" />
{% endif %}
<div class="card">
<div class="card-header">
<h5 class="card-title">Upload File</h5>
</div>
<div class="card-body">
<form action="./upload" method="post" enctype="multipart/form-data">
<div class="row">
<div id="uploadBox" class="col-lg-7">
<div class="input-group mb-3">
<div id="uploadField" style="display: flex; width: 100%">
<div class="custom-file">
<input type="file" name="file[]" class="custom-file-input" id="uploadSelectFile" aria-describedby="uploadSubmit" value="Browse Music file" multiple />
<label class="custom-file-label" for="uploadSelectFile">Choose file</label>
</div>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="row">
<div class="col">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">Upload To</span>
</div>
<input class="form-control" list="targetdirs" id="uploadTargetDir" name="targetdir" placeholder="uploads" />
<datalist id="targetdirs">
{% for dir in dirs %}
<option value="{{ dir }}">
{% endfor %}
</datalist>
</div>
</div>
<div class="col-auto">
<button class="btn btn-primary" type="button" id="uploadSubmit"><i class="fas fa-upload mr-1"></i>Upload!</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- beautify ignore:end -->
<div class="container mb-5">
<div class="card-deck">
<div id="add-music-url" class="card">
<div class="card-header">
<h5 class="card-title">Add URL</h5>
</div>
<div class="card-body">
<label for="music-url-input">Add Youtube or Soundcloud URL</label>
<div class="input-group mb-2">
<input class="form-control" type="text" id="music-url-input" placeholder="URL...">
</div>
<button type="submit" class="btn btn-primary">
Add URL
</button>
</div>
</div>
<div id="add-radio-url" class="card">
<div class="card-header">
<h5 class="card-title">Add Radio</h5>
</div>
<div class="card-body">
<label for="radio-url-input">Add Radio URL</label>
<div class="input-group mb-2">
<input id="radio-url-input" class="form-control" type="text" placeholder="Radio Address...">
</div>
<button type="submit" class="btn btn-primary">
Add Radio
</button>
</div>
</div>
</div>
</div>
<div id="player-toast" class="floating-button" style="bottom: 120px;">
<i class="fas fa-play" aria-hidden="true"></i>
</div>
<div id="theme-switch-btn" class="floating-button" style="bottom: 50px;">
<i class="fas fa-lightbulb" aria-hidden="true"></i>
</div>
<div id="playerToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<i class="fas fa-play-circle mr-2 text-primary"></i>
<strong class="mr-auto">Now Playing...</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body" id="playerContainer">
<img id="playerArtworkIdle" src="static/image/empty_box.svg" />
<img id="playerArtwork" src="static/image/unknown-album.png" style="display: none;" />
<div id="playerInfo">
<div id="playerActionBox">
<button id="playerPlayBtn" class="btn btn-primary btn-sm btn-space" style="display: none">
<i class="fas fa-play"></i>
</button>
<button id="playerPauseBtn" class="btn btn-primary btn-sm btn-space" style="display: none">
<i class="fas fa-pause"></i>
</button>
<button id="playerSkipBtn" class="btn btn-primary btn-sm">
<i class="fas fa-fast-forward"></i>
</button>
</div>
<div style="overflow: hidden; max-width: 320px;">
<strong id="playerTitle">Song Title</strong>
</div>
<span id="playerArtist">Artist</span>
<div id="playerBarBox" class="progress">
<div id="playerBar" class="progress-bar pr-2" role="progressbar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 100%; text-align: right; transform: translateX(-100%);"></div>
</div>
</div>
</div>
</div>
<div id="footer" style="height:50px; width: 100%; margin-top: 100px;"></div>
<form id="download-form" action="download" method="GET" target="_blank">
<input hidden type="text" name="id" value="">
<input hidden type="text" name="type" value="">
<input hidden type="text" name="dir" value="">
<input hidden type="text" name="tags" value="">
<input hidden type="text" name="keywords" value="">
</form>
<!-- Add tags modal -->
<div class="modal fade" id="addTagModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addTagModalTitle">Edit tags for ?</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div id="addTagModalBody" class="modal-body">
<input hidden type="text" id="addTagModalItemId" name="id" value="">
<div class="modal-tag" style="display: none; width: 100%;">
<span class="modal-tag-text tag-space badge badge-pill badge-dark">Tag</span>
<a class="modal-tag-remove tag-click small"><i class="fas fa-times-circle btn-outline-danger"></i></a>
</div>
<div id="addTagModalTags" style="margin-left: 5px; margin-bottom: 10px;">
</div>
<div class="input-group">
<input class="form-control form-control-sm btn-space" type="text" id="addTagModalInput" placeholder="tag1,tag2,...">
<button id="addTagModalAddBtn" type="button" class="btn btn-primary btn-sm">
<i class="fas fa-plus" aria-hidden="true"></i>
Add
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button id="addTagModalSubmit" type="button" class="btn btn-success" data-dismiss="modal">Edit!</button>
</div>
</div>
</div>
</div>
<!-- Upload files modal -->
<div class="modal fade" id="uploadModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="uploadTitle"><i class="fas fa-upload mr-1"></i>Uploading files...</h5>
</div>
<div id="uploadModalBody" class="modal-body">
<div id="uploadSuccessAlert" class="alert alert-success" role="alert" style="display: none">
<i class="fas fa-check mr-1"></i>
Uploaded finished!
</div>
<div id="uploadModalList" style="margin-left: 5px; margin-bottom: 10px;">
<div class="uploadItem" style="display: none; width: 100%; padding-bottom: 8px;">
<i class="far fa-file-alt mr-1"></i>
<span class="uploadItemTitle mr-3"></span>
<span class="uploadItemError text-danger"></span>
<div class="progress" style="margin-top: 5px; height: 10px;">
<div class="uploadProgress progress-bar pr-2" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 100%; text-align: right; transform: translateX(-100%);"></div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" id="uploadClose" class="btn btn-success" data-dismiss="modal">
<i class="fas fa-times mr-1"></i> Close</button>
<button type="button" id="uploadCancel" class="btn btn-danger" data-toggle="tooltip" data-html="true" title="<strong>Are you really sure?</strong> <br /> Click again to abort uploading.">
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> Cancel</button>
</div>
</div>
</div>
</div>
<input type="hidden" id="maxUploadFileSize" value="{{ max_upload_file_size }}" />
<script src="static/js/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta charset="UTF-8">
<title>botamusique web interface</title>
<link rel="icon" href="static/image/favicon.ico" />
<link rel="stylesheet" href="static/css/main.css">
</head>
<body>
<div class="container" style="max-width: 800px">
<div class="col-8" style="margin: auto; padding-top: 50px;">
<div class="card">
<div class="card-header">
Token Required
</div>
<div class="card-body">
<h3>Token Required!</h3>
You are accessing the web interface of {{ name }}.
A token is needed to grant you access.<br />
Please send "{{ command }}" to the bot in mumble to acquire one.
<form action="." method="get">
<div class="form-group mt-3">
<label for="token_input">Token</label>
<div class="input-group">
<input type="password" class="form-control btn-space" id="token_input" name="token" placeholder="xxxxxxx">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="static/js/main.js"></script>
</body>
</html>

87
web/webpack.config.cjs Normal file
View File

@ -0,0 +1,87 @@
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
main: [
'./src/js/app.mjs',
'./src/sass/app.scss',
],
dark: [
'./src/sass/app-dark.scss',
],
},
devtool: 'source-map',
/*optimization: {
splitChunks: {
chunks: 'all',
},
},*/
output: {
filename: 'static/js/[name].js',
path: path.resolve(__dirname, '../'),
// ecmaVersion: 5,
},
plugins: [
new MiniCssExtractPlugin({
filename: 'static/css/[name].css',
}),
new HtmlWebpackPlugin({
filename: 'templates/index.html',
template: './src/templates/index.html',
inject: false,
}),
new HtmlWebpackPlugin({
filename: 'templates/need_token.html',
template: './src/templates/need_token.html',
inject: false,
}),
],
module: {
rules: [{
test: /\.s[ac]ss$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader', // translates CSS into CommonJS modules
{
loader: 'postcss-loader',
options: {
plugins: function() {
return [
require('autoprefixer'),
];
},
},
},
'sass-loader', // compiles Sass to CSS
],
},
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
'corejs': '3.6',
'useBuiltIns': 'usage',
},
],
],
plugins: [
'@babel/plugin-proposal-class-properties',
],
},
},
},
],
},
/* experiments: {
mjs: true,
},*/
};