Merge branch 'master' into metadata
33
README.rst
@ -11,14 +11,14 @@ Requirements
|
|||||||
- youtube-dl
|
- youtube-dl
|
||||||
- espeak
|
- espeak
|
||||||
|
|
||||||
. code::
|
.. code::
|
||||||
|
|
||||||
apt install espeak omxplayer python3.4 python3-pip
|
apt install espeak omxplayer python3.4 python3-pip
|
||||||
pip3 install virtualenv youtube-dl
|
pip3 install virtualenv youtube-dl
|
||||||
|
|
||||||
It is highly recommended to install the soundboard and its dependencies into a virtual environment to keep the system clean. To do so first create a new virtual environment in the desired folder and install the remaining Python dependencies via the `pip` of the virtualenv:
|
It is highly recommended to install the soundboard and its dependencies into a virtual environment to keep the system clean. To do so first create a new virtual environment in the desired folder and install the remaining Python dependencies via the `pip` of the virtualenv:
|
||||||
|
|
||||||
. code::
|
.. code::
|
||||||
|
|
||||||
virtualenv py
|
virtualenv py
|
||||||
py/bin/pip install -r requirements.txt
|
py/bin/pip install -r requirements.txt
|
||||||
@ -28,7 +28,7 @@ Apache
|
|||||||
|
|
||||||
It is also recommended to use Apache instead of the Python Flask web server.
|
It is also recommended to use Apache instead of the Python Flask web server.
|
||||||
|
|
||||||
. code::
|
.. code::
|
||||||
|
|
||||||
apt install apache2 libapache2-mod-wsgi-py3
|
apt install apache2 libapache2-mod-wsgi-py3
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ Development/Debugging
|
|||||||
|
|
||||||
For development and debugging the internal web server of Flask can be used e.g. by running following command inside the project folder:
|
For development and debugging the internal web server of Flask can be used e.g. by running following command inside the project folder:
|
||||||
|
|
||||||
. code::
|
.. code::
|
||||||
|
|
||||||
FLASK_DEBUG=1 FLASK_APP=soundboard.py py/bin/flask run --host=0.0.0.0
|
FLASK_DEBUG=1 FLASK_APP=soundboard.py py/bin/flask run --host=0.0.0.0
|
||||||
|
|
||||||
@ -59,3 +59,28 @@ Production
|
|||||||
For production deployment Apache (or other webservers like nginx, lighttpd etc.) should be used.
|
For production deployment Apache (or other webservers like nginx, lighttpd etc.) should be used.
|
||||||
|
|
||||||
For Apache a sample configuration is provided in the folder `apache` which can be adapted.
|
For Apache a sample configuration is provided in the folder `apache` which can be adapted.
|
||||||
|
|
||||||
|
Please keep in mind that you may have to adjust the path to the sound files in your apache config in order to enable local playback on the clients.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
Users can browse the webinterface, search for sounds and play them on the remote server or their local browser.
|
||||||
|
|
||||||
|
Additionally to the normal point and click usage there are also several keyboard shortcuts available.
|
||||||
|
|
||||||
|
Keyboard shortcuts
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Navigation items, input fields and sound buttons can be selected by using tab and shift+tab
|
||||||
|
- esc clears the search field
|
||||||
|
- enter plays the selected sound
|
||||||
|
- ctrl+k and ctrl+c kill all sounds
|
||||||
|
- ctrl+x switches between local and remote playback
|
||||||
|
- ctrl+enter kills sounds before the selected gets played
|
||||||
|
|
||||||
|
|
||||||
|
Live version
|
||||||
|
============
|
||||||
|
|
||||||
|
A list of sound requests and already added sounds can be found here: https://pad.wiai.de/p/soundboardTodos
|
||||||
@ -1,10 +1,10 @@
|
|||||||
Listen 5000
|
Listen 5000
|
||||||
<VirtualHost *:5000>
|
<VirtualHost *:5000>
|
||||||
#ServerName example.com
|
|
||||||
|
|
||||||
WSGIDaemonProcess soundboard user=www-data group=www-data threads=5
|
WSGIDaemonProcess soundboard user=www-data group=www-data threads=5
|
||||||
WSGIScriptAlias / /var/www/soundboard/soundboard.wsgi
|
WSGIScriptAlias / /var/www/soundboard/soundboard.wsgi
|
||||||
|
|
||||||
|
Alias /sounds /home/pi/sounds
|
||||||
|
|
||||||
<Directory /var/www/soundboard>
|
<Directory /var/www/soundboard>
|
||||||
WSGIProcessGroup soundboard
|
WSGIProcessGroup soundboard
|
||||||
WSGIApplicationGroup %{GLOBAL}
|
WSGIApplicationGroup %{GLOBAL}
|
||||||
@ -12,4 +12,14 @@ Listen 5000
|
|||||||
Order deny,allow
|
Order deny,allow
|
||||||
Allow from all
|
Allow from all
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
|
<Directory /home/pi/sounds>
|
||||||
|
Options FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Order deny,allow
|
||||||
|
Allow from all
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
|
||||||
|
|||||||
12
devserver.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
basedir=$(dirname "$0")
|
||||||
|
|
||||||
|
if [ ! -d "$basedir/py" ]; then
|
||||||
|
echo "Directory \"py\" with a Python virtual environment"
|
||||||
|
echo "does not exist. See README.rst on how to set it up."
|
||||||
|
else
|
||||||
|
export FLASK_DEBUG=1
|
||||||
|
export FLASK_APP="$basedir/soundboard.py"
|
||||||
|
"$basedir"/py/bin/flask run
|
||||||
|
fi
|
||||||
@ -3,7 +3,7 @@ import sys
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
from flask import Flask, render_template, request, redirect, url_for, g
|
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, g
|
||||||
|
|
||||||
import config
|
import config
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ def index(sound=None, text=None, video=None):
|
|||||||
pitch = request.form.get("pitch", default="")
|
pitch = request.form.get("pitch", default="")
|
||||||
pitch = pitch if pitch.strip() != "" else "50"
|
pitch = pitch if pitch.strip() != "" else "50"
|
||||||
|
|
||||||
subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text.encode("utf-8")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
video = request.args.get("video")
|
video = request.args.get("video")
|
||||||
@ -140,3 +140,7 @@ ON tag.id = checked.id""", (sound,))
|
|||||||
return redirect("/edit")
|
return redirect("/edit")
|
||||||
|
|
||||||
return render_template("edit.html", sound=sound, tags=tags)
|
return render_template("edit.html", sound=sound, tags=tags)
|
||||||
|
|
||||||
|
@app.route("/sounds/<path:name>")
|
||||||
|
def sounds(name):
|
||||||
|
return send_from_directory(config.path, name)
|
||||||
|
|||||||
BIN
static/favicon/apple-touch-icon-114x114.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/favicon/apple-touch-icon-120x120.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
static/favicon/apple-touch-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
static/favicon/apple-touch-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
static/favicon/apple-touch-icon-57x57.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/favicon/apple-touch-icon-60x60.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
static/favicon/apple-touch-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
static/favicon/apple-touch-icon-76x76.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
static/favicon/favicon-128.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
static/favicon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 626 B |
BIN
static/favicon/favicon-196x196.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
static/favicon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/favicon/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
static/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
static/favicon/mstile-144x144.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
static/favicon/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
static/favicon/mstile-310x150.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
static/favicon/mstile-310x310.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
static/favicon/mstile-70x70.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
2801
static/howler.js
Normal file
@ -1,4 +1,4 @@
|
|||||||
htl, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ form {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input, button {
|
||||||
background-color: #666;
|
background-color: #666;
|
||||||
border: 1px solid #999;
|
border: 1px solid #999;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@ -33,6 +33,21 @@ input {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #999;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background-color: #999;
|
||||||
|
border: 1px solid #666;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
.editform {
|
.editform {
|
||||||
display: table;
|
display: table;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@ -90,7 +105,7 @@ input {
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.youtube label {
|
label {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +114,7 @@ nav {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav > a {
|
nav a {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@ -121,12 +136,14 @@ section {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset {
|
#reset {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
|
display: inline;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset:hover {
|
#reset:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +189,18 @@ nav .extra > a {
|
|||||||
.sound a:hover {
|
.sound a:hover {
|
||||||
background-color: #ff4136;
|
background-color: #ff4136;
|
||||||
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6);
|
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sound a:active {
|
||||||
|
background-color: #ff4136;
|
||||||
|
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sound-pressed {
|
||||||
|
background-color: #ff4136 !important;
|
||||||
|
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit {
|
.edit {
|
||||||
@ -184,10 +213,15 @@ nav .extra > a {
|
|||||||
box-shadow: inset -5px -5px 5px rgba(0, 0, 0, 0.6);
|
box-shadow: inset -5px -5px 5px rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-row {
|
.unselectable {
|
||||||
display: table-row;
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-cell {
|
#local-mode-button .local {
|
||||||
display: table-cell;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|||||||
170
static/main.js
@ -1,3 +1,10 @@
|
|||||||
|
/* jshint esversion: 6 */
|
||||||
|
|
||||||
|
var localModeEnabled = false;
|
||||||
|
|
||||||
|
// References to howler objects
|
||||||
|
var howlerSounds = [];
|
||||||
|
|
||||||
function ready(fn) {
|
function ready(fn) {
|
||||||
if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") {
|
if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") {
|
||||||
fn();
|
fn();
|
||||||
@ -6,6 +13,10 @@ function ready(fn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
ready(function() {
|
ready(function() {
|
||||||
hideSections();
|
hideSections();
|
||||||
|
|
||||||
@ -29,38 +40,18 @@ ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var searchfield = document.querySelector("#search");
|
var searchfield = document.querySelector("#search");
|
||||||
|
searchfield.focus();
|
||||||
|
|
||||||
searchFilter(searchfield, ".sound", "inline-block");
|
searchFilter(searchfield, ".sound", "inline-block");
|
||||||
searchFilter(searchfield, ".tag", "block");
|
searchFilter(searchfield, ".tag", "block");
|
||||||
|
|
||||||
var reset = document.querySelector("#sounds .reset");
|
var reset = document.querySelector("#reset");
|
||||||
|
var sounds_nav = document.querySelector("#sounds-nav");
|
||||||
|
|
||||||
if (reset !== null) {
|
reset.addEventListener("click", resetSearch, false);
|
||||||
reset.addEventListener("click", function() {
|
sounds_nav.addEventListener("click", resetSearch, false);
|
||||||
var buttons = document.querySelectorAll(".sound");
|
|
||||||
|
|
||||||
buttons.forEach(function(item) {
|
addKeyListeners();
|
||||||
item.style.display = "inline-block";
|
|
||||||
});
|
|
||||||
|
|
||||||
searchfield.value = "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var taginputs = document.querySelectorAll(".taginput");
|
|
||||||
|
|
||||||
if (taginputs.length > 0) {
|
|
||||||
taginputs.forEach(function(taginput) {
|
|
||||||
taginput.addEventListener("keydown", function(e) {
|
|
||||||
if (e.target == taginput && (e.which == 13 || e.keyCode == 13)) {
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (e.which == 32 || e.keyCode == 13) {
|
|
||||||
console.log("Space");
|
|
||||||
addTag(e.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function searchFilter(searchfield, itemSelector, unHideStyle) {
|
function searchFilter(searchfield, itemSelector, unHideStyle) {
|
||||||
@ -74,7 +65,6 @@ function searchFilter(searchfield, itemSelector, unHideStyle) {
|
|||||||
|
|
||||||
items.forEach(function(item) {
|
items.forEach(function(item) {
|
||||||
var name = item.firstChild.innerHTML;
|
var name = item.firstChild.innerHTML;
|
||||||
console.log(name);
|
|
||||||
|
|
||||||
if (name.toLowerCase().indexOf(searchfield.value.toLowerCase()) === -1) {
|
if (name.toLowerCase().indexOf(searchfield.value.toLowerCase()) === -1) {
|
||||||
item.style.display = "none";
|
item.style.display = "none";
|
||||||
@ -84,6 +74,68 @@ function searchFilter(searchfield, itemSelector, unHideStyle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Adds all necessary KeyListeners to document
|
||||||
|
*/
|
||||||
|
function addKeyListeners() {
|
||||||
|
// keyboard listener for global key strokes/inputs
|
||||||
|
document.onkeydown = function(evt) {
|
||||||
|
evt = evt || window.event;
|
||||||
|
if (evt.keyCode == 27) {
|
||||||
|
// esc key
|
||||||
|
resetSearch();
|
||||||
|
} else if ((evt.keyCode == 75 || evt.keyCode == 67) && evt.ctrlKey) {
|
||||||
|
// ctrl+k and ctrl+c key binding
|
||||||
|
killAllAudio();
|
||||||
|
} else if (evt.keyCode == 88 && evt.ctrlKey){
|
||||||
|
// ctrl+x key binding
|
||||||
|
toggleLocalMode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// keyboard listener for playing sounds using enter key
|
||||||
|
var buttons = document.querySelectorAll(".sound");
|
||||||
|
|
||||||
|
buttons.forEach(function(item) {
|
||||||
|
item.firstChild.addEventListener('keypress', async function (e) {
|
||||||
|
var key = e.keyCode;
|
||||||
|
var source = e.target;
|
||||||
|
// keylistener for enter or ctrl+enter (which is k10 on chrome)
|
||||||
|
if (key === 13 || (key === 10 && e.ctrlKey)) {
|
||||||
|
if (e.ctrlKey){
|
||||||
|
killAllAudio();
|
||||||
|
await sleep(100);
|
||||||
|
}
|
||||||
|
source.classList.add("sound-pressed");
|
||||||
|
source.onclick();
|
||||||
|
await sleep(300);
|
||||||
|
source.classList.remove("sound-pressed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function resetSearch() {
|
||||||
|
var searchfield = document.querySelector("#search");
|
||||||
|
var buttons = document.querySelectorAll(".sound");
|
||||||
|
|
||||||
|
buttons.forEach(function(item) {
|
||||||
|
item.style.display = "inline-block";
|
||||||
|
});
|
||||||
|
|
||||||
|
searchfield.value = "";
|
||||||
|
searchfield.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reads the stream url from input with id #streaming-url and forwards it to the server using AJAX.
|
||||||
|
*/
|
||||||
|
function playStream() {
|
||||||
|
var streamUrl = document.querySelector("#streaming-url").value;
|
||||||
|
ajaxRequest("/?video="+encodeURI(streamUrl));
|
||||||
|
}
|
||||||
|
|
||||||
function hideSections() {
|
function hideSections() {
|
||||||
var sections = document.querySelectorAll("section");
|
var sections = document.querySelectorAll("section");
|
||||||
|
|
||||||
@ -91,3 +143,67 @@ function hideSections() {
|
|||||||
item.style.display = "none";
|
item.style.display = "none";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ajaxRequest(url) {
|
||||||
|
var ajaxRequest;
|
||||||
|
try {
|
||||||
|
ajaxRequest = new XMLHttpRequest();
|
||||||
|
ajaxRequest.open("GET", url, true);
|
||||||
|
ajaxRequest.send(null);
|
||||||
|
} catch (e) {
|
||||||
|
alert("Unfortunately we were unable to handle your request. Please try again later and contact the server administrator if the problem persists.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Switches between local and remote playback mode.
|
||||||
|
* Additionally changes the button text and its color so that the user has a visible feedback.
|
||||||
|
*/
|
||||||
|
function toggleLocalMode() {
|
||||||
|
toggleButton = document.getElementById("local-mode-button").children[0];
|
||||||
|
if (localModeEnabled) {
|
||||||
|
localModeEnabled = false;
|
||||||
|
toggleButton.classList.remove("local");
|
||||||
|
toggleButton.innerHTML = "[S]";
|
||||||
|
toggleButton.title = "play sounds on server";
|
||||||
|
} else {
|
||||||
|
localModeEnabled = true;
|
||||||
|
toggleButton.classList.add("local");
|
||||||
|
toggleButton.innerHTML = "[L]";
|
||||||
|
toggleButton.title = "play sounds locally";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Either plays the given sound file locally by using howler.js or forwards the request to the remote server using AJAX.
|
||||||
|
*/
|
||||||
|
function playSound(filename) {
|
||||||
|
if (localModeEnabled){
|
||||||
|
// play local audio using howler.js
|
||||||
|
var sound = new Howl({
|
||||||
|
src: ['/sounds/' + filename]
|
||||||
|
});
|
||||||
|
sound.play();
|
||||||
|
howlerSounds.push(sound);
|
||||||
|
} else {
|
||||||
|
ajaxRequest("/play/" + filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function killAllHowlerAudio() {
|
||||||
|
for (var i = 0; i < howlerSounds.length; i++) {
|
||||||
|
howlerSounds[i].stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Kills all local or remote audio, depending on localModeEnabled
|
||||||
|
*/
|
||||||
|
function killAllAudio() {
|
||||||
|
if (localModeEnabled) {
|
||||||
|
killAllHowlerAudio();
|
||||||
|
} else {
|
||||||
|
ajaxRequest("/?killvideo=yes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,28 +1,71 @@
|
|||||||
{% extends "base.html" %}
|
<!DOCTYPE html>
|
||||||
{% block content %}
|
<html>
|
||||||
<section id="sounds">
|
<head>
|
||||||
<div><input type="text" id="search" /><span class="reset">⨯</span></div>
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>WIAI Soundboard</title>
|
||||||
|
<link rel="stylesheet" href="/static/main.css" />
|
||||||
|
|
||||||
|
<!-- favicon clipart source: https://openclipart.org/detail/190592/button -->
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="/static/favicon/apple-touch-icon-57x57.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/static/favicon/apple-touch-icon-114x114.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/static/favicon/apple-touch-icon-72x72.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/static/favicon/apple-touch-icon-144x144.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="/static/favicon/apple-touch-icon-60x60.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="/static/favicon/apple-touch-icon-120x120.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="/static/favicon/apple-touch-icon-76x76.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/static/favicon/apple-touch-icon-152x152.png" />
|
||||||
|
<link rel="icon" type="image/png" href="/static/favicon/favicon-196x196.png" sizes="196x196" />
|
||||||
|
<link rel="icon" type="image/png" href="/static/favicon/favicon-96x96.png" sizes="96x96" />
|
||||||
|
<link rel="icon" type="image/png" href="/static/favicon/favicon-32x32.png" sizes="32x32" />
|
||||||
|
<link rel="icon" type="image/png" href="/static/favicon/favicon-16x16.png" sizes="16x16" />
|
||||||
|
<link rel="icon" type="image/png" href="/static/favicon/favicon-128.png" sizes="128x128" />
|
||||||
|
<meta name="application-name" content="WIAI Soundboard"/>
|
||||||
|
<meta name="msapplication-TileColor" content="#FFFFFF" />
|
||||||
|
<meta name="msapplication-TileImage" content="/static/favicon/mstile-144x144.png" />
|
||||||
|
<meta name="msapplication-square70x70logo" content="/static/favicon/mstile-70x70.png" />
|
||||||
|
<meta name="msapplication-square150x150logo" content="/static/favicon/mstile-150x150.png" />
|
||||||
|
<meta name="msapplication-wide310x150logo" content="/static/favicon/mstile-310x150.png" />
|
||||||
|
<meta name="msapplication-square310x310logo" content="/static/favicon/mstile-310x310.png" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="#sounds" id="sounds-nav">Sounds</a>
|
||||||
|
<a href="#streams">Streams</a>
|
||||||
|
<a href="#voice">Voices</a>
|
||||||
|
{% if not edit %}
|
||||||
|
<div class="extra"><a href="/edit">Edit</a></div>
|
||||||
|
{% else %}
|
||||||
|
<div class="extra"><a href="/">Done</a></div>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
<content>
|
||||||
|
<section id="sounds" style="display:none;">
|
||||||
|
<div>
|
||||||
|
<a onclick="toggleLocalMode();" id="local-mode-button"><button title="play sounds on server">[S]</button></a>
|
||||||
|
<input type="text" id="search" autofocus/>
|
||||||
|
<div id="reset">✕</div>
|
||||||
<ul class="tags">
|
<ul class="tags">
|
||||||
{% for tag in tags %}
|
{% for tag in tags %}
|
||||||
<li><a href="#">{{ tag.name }}</a></li>
|
<li><a href="#">{{ tag.name }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% for sound in sounds %}<!--
|
</div><!--
|
||||||
--><div class="sound{{ ' edit' if edit }}"><a href="{{ '/' + ('edit' if edit else 'play') + '/' + sound | urlencode }}">{{ sound.split('.', 1)[0] }}</a></div><!--
|
{% for sound in sounds %}
|
||||||
-->{% endfor %}
|
--><div class="sound{{ ' edit' if edit }}"><a tabindex="0" class="unselectable" {{ 'href=/edit/' + sound | urlencode if edit else 'onclick=playSound(\'' + sound | urlencode + '\');' }}>{{ sound.split('.', 1)[0] }}</a></div><!--
|
||||||
|
{% endfor %}-->
|
||||||
</section>
|
</section>
|
||||||
<section id="youtube">
|
<section id="streams" style="display:none;">
|
||||||
<form action="/" method="GET">
|
<div>
|
||||||
<label for="video">YouTube URL</label>
|
<form>
|
||||||
<input type="text" name="video" />
|
<label for="video">Stream URL</label>
|
||||||
<input type="submit" value="Play" />
|
<input id="streaming-url" type="text" name="video" placeholder="paste stream url here..." />
|
||||||
</form>
|
|
||||||
<form action="/" method="GET">
|
|
||||||
<input type="hidden" name="killvideo" value="yes" />
|
|
||||||
<input type="submit" name="submit" value="Terminate videos" />
|
|
||||||
</form>
|
</form>
|
||||||
|
<button onclick="playStream();">Play!</button>
|
||||||
|
</div>
|
||||||
|
<button name="submit" onclick="killAllAudio();">Terminate videos</button>
|
||||||
</section>
|
</section>
|
||||||
<section id="voice">
|
<section id="voice" style="display:none;">
|
||||||
<form action="/say/" method="POST">
|
<form action="/say/" method="POST">
|
||||||
<input type="text" name="text" />
|
<input type="text" name="text" />
|
||||||
<input type="text" name="voice" placeholder="DE" />
|
<input type="text" name="voice" placeholder="DE" />
|
||||||
@ -33,4 +76,8 @@
|
|||||||
<input type="submit" value="Voice" />
|
<input type="submit" value="Voice" />
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
</content>
|
||||||
|
<script src="/static/howler.js"></script>
|
||||||
|
<script src="/static/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||