French l10n of app
This commit is contained in:
parent
713a1eef79
commit
0ccf5eb40c
386
app.py
386
app.py
|
@ -1,386 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import sys, os, base64, toml
|
||||
import http.client, ssl
|
||||
import xml.etree.ElementTree as ET
|
||||
from flask import Flask, render_template, request, make_response, jsonify
|
||||
from waitress import serve
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Load config defaults, then look for other config files
|
||||
app.config.from_file("defaults.toml", load=toml.load)
|
||||
config_locations = ["./", "~/.", "~/.config/"]
|
||||
for location in config_locations:
|
||||
# Optional config files, ~ is expanded to $HOME on *nix, %USERPROFILE% on windows
|
||||
# ~ app.config.from_file("videopi.toml", load=toml.load, silent=True)
|
||||
if app.config.from_file(os.path.expanduser( location + "pilpil-server.toml"), load=toml.load, silent=True):
|
||||
print("Found configuration file in " + os.path.expanduser( location ))
|
||||
# ~ app.config.from_file(os.path.expanduser("~/.config/videopi.toml"), load=toml.load, silent=True)
|
||||
|
||||
###
|
||||
|
||||
hosts_available, hosts_unavailable = [],[]
|
||||
|
||||
# Map vlc cmds
|
||||
# See https://github.com/videolan/vlc/blob/1336447566c0190c42a1926464fa1ad2e59adc4f/share/lua/http/requests/README.txt
|
||||
cmd_player = {
|
||||
"play" : "pl_play",
|
||||
"resume" : "pl_forceresume",
|
||||
"pause" : "pl_forcepause",
|
||||
"tpause" : "pl_pause",
|
||||
"previous" : "pl_previous",
|
||||
"next" : "pl_next",
|
||||
"stop" : "pl_stop",
|
||||
"enqueue" : "in_enqueue",
|
||||
"add" : "in_play",
|
||||
"clear" : "pl_empty",
|
||||
"delete" : "pl_delete",
|
||||
"loop" : "pl_loop",
|
||||
"repeat" : "pl_repeat",
|
||||
"random" : "pl_random",
|
||||
"move" : "pl_move",
|
||||
"sort" : "pl_sort",
|
||||
"seek" : "seek",
|
||||
"sync" : "sync",
|
||||
"status" : "status.xml",
|
||||
"list" : "playlist.xml",
|
||||
#"volume" : "volume",
|
||||
#"ratio" : "aspectratio",
|
||||
#"dir" : "?dir=<uri>",
|
||||
#"command" : "?command=<cmd>",
|
||||
#"key" : "key=",
|
||||
#"browse" : "browse.xml?uri=file://~"
|
||||
# System commands :
|
||||
# ~ "rssi" : "rssi",
|
||||
# ~ "blink" : "blink",
|
||||
# ~ "poweroff" : "poweroff",
|
||||
# ~ "reboot" : "reboot",
|
||||
|
||||
}
|
||||
|
||||
cmd_server = ["blink", "reboot", "poweroff", "rssi"]
|
||||
|
||||
# Set configuration
|
||||
|
||||
DEBUG = app.config['DEFAULT']['DEBUG']
|
||||
media_folder_remote = app.config['DEFAULT']['media_folder_remote']
|
||||
media_folder_local = os.path.expanduser(app.config['DEFAULT']['media_folder_local'])
|
||||
media_exts = app.config['DEFAULT']['media_exts']
|
||||
auth = str(base64.b64encode(str(":" + app.config['DEFAULT']['auth']).encode('utf-8')), 'utf-8')
|
||||
cmd_auth = str(base64.b64encode(str(":" + app.config['DEFAULT']['cmd_auth']).encode('utf-8')), 'utf-8')
|
||||
hosts = app.config['DEFAULT']['hosts']
|
||||
port = app.config['DEFAULT']['port']
|
||||
cmd_port = app.config['DEFAULT']['cmd_port']
|
||||
useSSL = app.config['DEFAULT']['useSSL']
|
||||
CAfile = app.config['DEFAULT']['CAfile']
|
||||
sync_facility = app.config['DEFAULT']['sync_facility']
|
||||
|
||||
headers = {"Authorization":"Basic " + auth}
|
||||
|
||||
# Network/link utilities
|
||||
# https://www.metageek.com/training/resources/understanding-rssi/
|
||||
|
||||
def isup(host_l, port):
|
||||
global DEBUG
|
||||
import socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
if useSSL:
|
||||
sslcontext = ssl.create_default_context()
|
||||
# ~ if os.path.exists(CAfile):
|
||||
# ~ sslcontext.load_verify_locations(cafile=CAfile)
|
||||
# ~ else:
|
||||
# Dont validate cert, we juts want to see if host is live
|
||||
sslcontext.check_hostname = False
|
||||
sslcontext.verify_mode = ssl.CERT_NONE
|
||||
s = sslcontext.wrap_socket(s, server_hostname=host_l)
|
||||
try:
|
||||
s.settimeout(3.0)
|
||||
s.connect((host_l, port))
|
||||
if DEBUG:
|
||||
print( "Port " + str(port) + " reachable")
|
||||
return 1
|
||||
except (socket.error, socket.timeout) as e:
|
||||
if DEBUG:
|
||||
print("Error on connection to " + host_l + ":" + str(port) + ": %s" % e)
|
||||
return 0
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
def checkHosts(host_l):
|
||||
hostdown, hostup = [], []
|
||||
hosts_number = str(len(host_l))
|
||||
for lhost in host_l:
|
||||
if not isup(lhost, port):
|
||||
hostdown.append(lhost)
|
||||
else:
|
||||
hostup.append(lhost)
|
||||
if DEBUG:
|
||||
print( str(len(hostup)) + " of " + hosts_number + " hosts found.")
|
||||
return hostup, hostdown
|
||||
|
||||
# File utilities
|
||||
|
||||
def listMediaFiles(folder):
|
||||
if os.path.exists(folder):
|
||||
files = os.listdir(folder);
|
||||
medias = []
|
||||
for fd in files:
|
||||
if len(fd.split('.')) > 1:
|
||||
if fd.split('.')[1] in media_exts:
|
||||
medias.append(fd)
|
||||
return medias
|
||||
else:
|
||||
return []
|
||||
|
||||
def httpUpload(filename, hostl, trailing_slash=1):
|
||||
import requests
|
||||
url = "https://" + hostl + ":" + str(cmd_port) + "/upload"
|
||||
if not trailing_slash:
|
||||
filename = "/" + filename
|
||||
files = { "file":( filename, open( media_folder_local + filename, "rb"), "multipart/form-data") }
|
||||
if DEBUG:
|
||||
print(files)
|
||||
resp = requests.post(url, files=files, headers=headers, verify=CAfile)
|
||||
if DEBUG:
|
||||
print(resp.text)
|
||||
if resp.ok:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def syncMediaFolder(media_folder_local, media_folder_remote, hostl, sync_facility=sync_facility):
|
||||
trailing_slash = 1
|
||||
# Check for trailing / and add it if missing
|
||||
if media_folder_local[-1:] != "/":
|
||||
media_folder_local += "/"
|
||||
trailing_slash = 0
|
||||
if media_folder_remote[-1:] != "/":
|
||||
media_folder_remote += "/"
|
||||
if sync_facility == "http":
|
||||
media_list = listMediaFiles(media_folder_local)
|
||||
transfer_ok = 0
|
||||
for media in media_list:
|
||||
transfer_ok += httpUpload(media, hostl, trailing_slash)
|
||||
return str(transfer_ok) + " files uploaded."
|
||||
|
||||
# Check sync utility exists
|
||||
elif which(sync_facility):
|
||||
from shutil import whichdb
|
||||
# Build subprocess arg list accroding to facility
|
||||
if sync_facility == "rsync":
|
||||
scrape_str = "total size is "
|
||||
sync_args = [sync_facility, "-zharm", "--include='*/'"]
|
||||
for media_type in media_exts:
|
||||
sync_args.append( "--include='*." + media_type + "'" )
|
||||
sync_args.extend(["--exclude='*'", media_folder_local, hostl + ":" + media_folder_remote])
|
||||
if sync_facility == "scp":
|
||||
media_list = listMediaFiles(media_folder_local)
|
||||
sync_args = [sync_facility, "-Crp", "-o IdentitiesOnly=yes"]
|
||||
for media in media_list:
|
||||
sync_args.append( media_folder_local + media )
|
||||
sync_args.append( hostl + ":" + media_folder_remote )
|
||||
|
||||
sync_proc = subprocess.run( sync_args , capture_output=True)
|
||||
if len(sync_proc.stdout):
|
||||
scrape_index = str(sync_proc.stdout).index(scrape_str)+len(scrape_str)
|
||||
total_size = str(sync_proc.stdout)[ scrape_index: ].split(" ")[0][:-1]
|
||||
else:
|
||||
total_size = "N/A";
|
||||
return total_size
|
||||
|
||||
# /requests/status.xml?command=in_enqueue&input=file:///home/pi/tst1.mp4
|
||||
def writeM3U(m3u_content : str, host : str):
|
||||
filename = host.replace(".", "_") + ".m3u"
|
||||
fd = open(filename, "w")
|
||||
fd.write(m3u_content)
|
||||
fd.close()
|
||||
return 1
|
||||
|
||||
# String utilities
|
||||
|
||||
def escapeStr(uri):
|
||||
return uri.replace(" ", "%20")
|
||||
|
||||
def sec2min(duration):
|
||||
return('%02d:%02d' % (duration / 60, duration % 60))
|
||||
|
||||
# VLC lua utilities
|
||||
|
||||
def sendCommand(host, arg0, arg1, arg2):
|
||||
portl = port
|
||||
# Build request
|
||||
req = "/requests/status.xml"
|
||||
if arg0 == "list" :
|
||||
req = "/requests/playlist.xml"
|
||||
elif arg0 in cmd_server:
|
||||
req = "/" + str(arg0)
|
||||
portl = cmd_port
|
||||
elif arg0 != "status" :
|
||||
req = req + "?command=" + cmd_player[arg0]
|
||||
if arg1 != "null" :
|
||||
if (arg0 == "play") or (arg0 == "delete") or (arg0 == "sort") or (arg0 == "move"):
|
||||
req = req + "&id=" + arg1
|
||||
if (arg0 == "sort") or (arg0 == "move") :
|
||||
# val possible values : id, title, title nodes first, artist, genre, random, duration, title numeric, album
|
||||
# https://github.com/videolan/vlc/blob/3.0.17.4/modules/lua/libs/playlist.c#L353-L362
|
||||
req = req + "&val=" + escapeStr(arg2)
|
||||
elif arg0 == "seek" :
|
||||
req = req + "&val=" + arg1
|
||||
elif (arg0 == "enqueue") or (arg0 == "add") :
|
||||
req = req + "&input=file://" + media_folder_remote + "/" + arg1
|
||||
# Send request
|
||||
if useSSL:
|
||||
sslcontext = ssl.create_default_context()
|
||||
if os.path.exists(CAfile):
|
||||
sslcontext.load_verify_locations(cafile=CAfile)
|
||||
else:
|
||||
sslcontext.check_hostname = False
|
||||
sslcontext.verify_mode = ssl.CERT_NONE
|
||||
conn = http.client.HTTPSConnection( host + ":" + str(portl), timeout=3, context = sslcontext )
|
||||
else:
|
||||
conn = http.client.HTTPConnection( host + ":" + str(portl), timeout=3 )
|
||||
try:
|
||||
conn.request( "GET", req, headers = headers )
|
||||
resp = conn.getresponse()
|
||||
data = resp.read()
|
||||
except http.client.HTTPException:
|
||||
print("Connection to " + host + " timed out")
|
||||
return "Connection to " + host + " timed out"
|
||||
except:
|
||||
return "Error while connecting to " + host + ":" + str(portl)
|
||||
finally:
|
||||
conn.close()
|
||||
# Parse response
|
||||
# ~ data = resp.read()
|
||||
|
||||
if arg0 == "rssi":
|
||||
if DEBUG:
|
||||
print(data)
|
||||
response_dict = {
|
||||
'host': host,
|
||||
'rssi': str(data, 'UTF-8')
|
||||
}
|
||||
return response_dict
|
||||
|
||||
xml = ET.fromstring(data)
|
||||
|
||||
if arg0 != "list" :
|
||||
meta = xml.findall("./information/category/")
|
||||
if meta:
|
||||
for leaf in meta:
|
||||
if leaf.get("name") == "filename":
|
||||
filename = leaf.text
|
||||
else:
|
||||
filename = "N/A"
|
||||
cur_length = int(xml.find('length').text)
|
||||
cur_time = int(xml.find('time').text)
|
||||
cur_length_fmtd = sec2min(cur_length)
|
||||
cur_time_fmtd = sec2min(cur_time)
|
||||
cur_id = int(xml.find('currentplid').text)
|
||||
cur_pos = xml.find('position').text
|
||||
cur_loop = xml.find('loop').text
|
||||
cur_repeat = xml.find('repeat').text
|
||||
response_dict = {
|
||||
'host': host,
|
||||
'file': filename,
|
||||
'time': cur_time_fmtd,
|
||||
'leng': cur_length_fmtd,
|
||||
'pos': cur_pos,
|
||||
'loop': cur_loop,
|
||||
'repeat': cur_repeat,
|
||||
# ~ 'pos': xml.find('position').text,
|
||||
# ~ 'loop': xml.find('loop').text,
|
||||
# ~ 'repeat': xml.find('repeat').text,
|
||||
'id': cur_id,
|
||||
}
|
||||
return response_dict
|
||||
|
||||
else:
|
||||
# Build M3U file from playlist
|
||||
playlist = []
|
||||
playlist_duration = 0
|
||||
m3u_hdr = "#EXTM3U\n"
|
||||
m3u_prefix = "#EXTINF:"
|
||||
item_list = []
|
||||
if xml.find("./node") and xml.find("./node").get('name') == "Playlist":
|
||||
m3u_content = m3u_hdr
|
||||
playlist = xml.findall("./node/leaf")
|
||||
#item_list = []
|
||||
for item in playlist:
|
||||
m3u_content += m3u_prefix + item.get("duration") + ", " + item.get("name") + "\n" + item.get("uri") + "\n"
|
||||
playlist_duration += int(item.get("duration"))
|
||||
# ~ item_info = item.get("id") + " : " + item.get("name") + " - " + sec2min( int( item.get("duration") ) )
|
||||
item_info = item.get("id") + ";" + item.get("name") + ";" + sec2min( int( item.get("duration") ) ) + ";"
|
||||
if "current" in item.keys():
|
||||
item_info += item.get("current")
|
||||
item_list.append(item_info)
|
||||
|
||||
playlist_overview = {
|
||||
"host" : host,
|
||||
"leng" : str(len(playlist)),
|
||||
"duration" : sec2min(playlist_duration),
|
||||
"items" : item_list
|
||||
}
|
||||
return playlist_overview
|
||||
|
||||
status_message = "Idle"
|
||||
|
||||
@app.route("/")
|
||||
def main():
|
||||
global hosts
|
||||
status_message = "Searching network for live hosts..."
|
||||
# ~ hosts_available, hosts_unavailable = checkHosts(hosts)
|
||||
templateData = {
|
||||
'hosts' : hosts,
|
||||
'status_message' : status_message
|
||||
}
|
||||
return render_template('main.html', **templateData)
|
||||
|
||||
@app.route("/scan")
|
||||
def scan():
|
||||
global hosts_available, hosts_unavailable
|
||||
hosts_available, hosts_unavailable = checkHosts(hosts)
|
||||
hosts_status = [hosts_available, hosts_unavailable]
|
||||
return hosts_status
|
||||
|
||||
@app.route("/browse")
|
||||
def browse():
|
||||
files = listMediaFiles(media_folder_local)
|
||||
return files;
|
||||
|
||||
|
||||
@app.route("/sync/<host>")
|
||||
def sync(host):
|
||||
if host == "all":
|
||||
for hostl in hosts_available:
|
||||
size = syncMediaFolder(media_folder_local, media_folder_remote, hostl)
|
||||
else:
|
||||
size = syncMediaFolder(media_folder_local, media_folder_remote, host)
|
||||
return size;
|
||||
|
||||
|
||||
@app.route("/<host>/<arg0>/", defaults = { "arg1": "null", "arg2": "null" })
|
||||
@app.route("/<host>/<arg0>/<arg1>/", defaults = { "arg2": "null" })
|
||||
@app.route("/<host>/<arg0>/<arg1>/<arg2>")
|
||||
def action(host, arg0, arg1, arg2):
|
||||
status_message = "Idle"
|
||||
if (arg0 not in cmd_player) and (arg0 not in cmd_server):
|
||||
status_message = "<p>Wrong command</p>"
|
||||
elif host == "all":
|
||||
resp = []
|
||||
for hostl in hosts_available:
|
||||
resp.append( sendCommand(hostl, arg0, arg1, arg2) )
|
||||
status_message = resp
|
||||
elif host not in hosts_available:
|
||||
status_message = "<p>Host is not reachable</p>"
|
||||
else:
|
||||
status_message = sendCommand(host, arg0, arg1, arg2)
|
||||
if DEBUG:
|
||||
print(status_message)
|
||||
return status_message
|
||||
|
||||
if __name__ == '__main__':
|
||||
# ~ app.run()
|
||||
serve(app, host='127.0.0.1', port=8080)
|
|
@ -1,79 +0,0 @@
|
|||
## 0.1 : 2022-07-19-videopi.img.xz
|
||||
md5sum : 7e80ede8ac4eed8b8088a3b075bdc1f2
|
||||
sha256 : 03de0272c71bd4614678b05c076d0e77df3f49039ad357ef9152374c748e7f1c
|
||||
|
||||
* VLC installed, H264 1080p playback ok, wifi ok, remote control via telnet script
|
||||
* Run `ssh-keygen -A` at first boot for the ssh server to work
|
||||
|
||||
## 0.2 : 2022-09-24-videopi.img.xz
|
||||
md5sum : f859f269c44f614e22e4fe601c3bb134
|
||||
sha256 : b6fd8ef4eb726d4ce7d196b9aebf910f32327ecd43f0d78140b8647d328ded22
|
||||
|
||||
* Switch VLC to use http lua control
|
||||
* Add systemd unit for running VLC on startup
|
||||
* Add RTL8821CU driver for rpi 1/3
|
||||
* Boot is now totally silent (blank screen)
|
||||
* Disable Bluetooth
|
||||
|
||||
## 0.3 : 2022-10-09-videopi.img.xz
|
||||
md5 : 8e5e5b474af47519785d5a4696db04e2
|
||||
sha256 : 0fe3fe76d0e56e445124fa20646fa8b3d8c59568786b3ebc8a96d83d92f203e3
|
||||
|
||||
* Add rtl8192eu driver for rpi1/3
|
||||
* Add http server for custom commands : reboot, shutdown, wifi signal, led blinking
|
||||
* Add vlc.playlist.move method to VLC http lua (httprequests.luac)
|
||||
* Add playlist to Webgui
|
||||
* config file parsing ( look for 'videopi.toml' in ./, ~/., ~/.config/)
|
||||
* Add VLC/waitress systemd units for automatic startup
|
||||
* Use nginx reverse proxy + SSL between server and clients ( https://medium.com/@antelle/how-to-generate-a-self-signed-ssl-certificate-for-an-ip-address-f0dd8dddf754 )
|
||||
* Webgui beautifying
|
||||
|
||||
## 0.4 : 2022-10-21-videopi.img.xz
|
||||
md5 : dee7af70135994169cab4f073ee51905
|
||||
sha256 : ec3e17fc9b41f8c5181484e9866be2d1d92cab8403210e3d22f4f689edd4cfde
|
||||
|
||||
* Switch to rpi os Bullseye
|
||||
* Switch to user 'pil', pw 'pilpoil'
|
||||
* client config file parsing ( look for 'pilpil-client.toml' in ./, ~/., ~/.config/)
|
||||
* Add media folder sync (scp, rsync, http upload)
|
||||
* Install script ; Wifi setup, generate/install SSH keys/ nginx SSL cert/key fore each host, change hostname, static IPs
|
||||
|
||||
## 0.5 : 2022-10-XX-videopi.img.xz
|
||||
md5 :
|
||||
sha256 :
|
||||
|
||||
* Add rt8821cu driver back
|
||||
* Use safe overclocking settings for rpi1, 3, 4, better memory split
|
||||
* Add blink function to pilpil
|
||||
|
||||
# FS checklist
|
||||
* /etc/dhcpcd.conf
|
||||
* /etc/ssh/sshd_config
|
||||
* ~/.ssh/authorized_keys
|
||||
* ~/Videos/*
|
||||
* /etc/hostname, /etc/hosts
|
||||
* /etc/ssl/private/nginx-selfsigned.key
|
||||
* /etc/ssl/certs/nginx-selfsigned.crt
|
||||
* /etc/wpa_supplicant/wpa_supplicant.conf
|
||||
* dd if=/dev/zero of=/home/pil/Videos/remove_me bs=4MB count=100
|
||||
|
||||
|
||||
# DOING NEXT :
|
||||
* ~ Test with several rpis
|
||||
* Define http auth secret at setup
|
||||
|
||||
|
||||
# DONE :
|
||||
* Increase live host scan when host first found
|
||||
|
||||
# OTHER:
|
||||
* get_client_rssi.sh on server
|
||||
|
||||
# TODO :
|
||||
* FR localisation
|
||||
* GUI : Btn hover/press ?
|
||||
|
||||
* ? Scripts hotspot linux/win/mac
|
||||
* ? Config sync
|
||||
* ! Remove git personal details/resolv.conf, remove authorized_keys, ssh config, clean home, re-enable ssh pw login
|
||||
* ~ Doc
|
|
@ -1,16 +0,0 @@
|
|||
[DEFAULT]
|
||||
DEBUG = 0
|
||||
useSSL = false
|
||||
CAfile = "selfCA.crt"
|
||||
# scp, rsync, http
|
||||
sync_facility = "http"
|
||||
media_folder_local = "~/Videos"
|
||||
media_folder_remote = "~/Videos"
|
||||
media_exts = []
|
||||
auth = "secret"
|
||||
cmd_auth = "secret"
|
||||
hosts = []
|
||||
# VLC http LUA port
|
||||
port = 0
|
||||
# Clients cmd port
|
||||
cmd_port = 0
|
|
@ -1,17 +0,0 @@
|
|||
[DEFAULT]
|
||||
DEBUG = 0
|
||||
useSSL = true
|
||||
CAfile = "selfCA.crt"
|
||||
# scp, rsync, http
|
||||
sync_facility = "http"
|
||||
media_folder_local = "../medias"
|
||||
media_folder_remote = "/home/pil/Videos"
|
||||
media_exts = ["mp4", "avi", "mkv"]
|
||||
auth = "secret"
|
||||
# OnNlY3JldA==
|
||||
cmd_auth = "secret"
|
||||
hosts = ["10.42.0.10", "10.42.0.11"]
|
||||
# VLC http LUA port
|
||||
port = 8887
|
||||
# Clients cmd port
|
||||
cmd_port = 8888
|
|
@ -1,3 +0,0 @@
|
|||
flask
|
||||
waitress
|
||||
toml
|
326
static/script.js
326
static/script.js
|
@ -1,326 +0,0 @@
|
|||
// Timeline drag and drop
|
||||
const tl_cont_attr = {id:"tl_cont", ondrop: "drop(event, this)", ondragover:"allowDrop(event)"};
|
||||
const tl_drag_attr = {id:"tl_drag", draggable:"true", ondragstart:"drag(event, this)"};
|
||||
|
||||
// Config
|
||||
var timeline_color_cursor = "#FF8839";
|
||||
var timeline_color_bg = "#2EB8E6";
|
||||
|
||||
var src_id = "";
|
||||
var medias_status = {};
|
||||
|
||||
function updatePlaylist(){
|
||||
var new_list = [];
|
||||
var media_count = document.getElementById("timeline").children.length;
|
||||
for (i=media_count,l=0;i>l;i--) {
|
||||
//~ new_list.push(document.getElementById("timeline").children[i].children[0].getAttribute("media_id"));
|
||||
toMove = document.getElementById("timeline").children[i-1].children[0].getAttribute("media_id");
|
||||
console.log(toMove);
|
||||
sendCmd("/all/move/" + toMove + "/1");
|
||||
}
|
||||
}
|
||||
|
||||
function findTargetIndex(element, index, array, thisArg){
|
||||
if (element == this) {
|
||||
return index+1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
function moveElements(target) {
|
||||
var elem_list = Array.from(src_id.parentElement.children);
|
||||
var elem_list_slice = elem_list;
|
||||
var source_idx = elem_list.findIndex(findTargetIndex, src_id);
|
||||
var target_idx = elem_list.findIndex(findTargetIndex, target);
|
||||
var idx;
|
||||
if (source_idx < target_idx) {
|
||||
elem_list_slice = elem_list.slice(source_idx+1, target_idx+1);
|
||||
idx = source_idx;
|
||||
} else {
|
||||
elem_list_slice = elem_list.slice(target_idx, source_idx );
|
||||
idx = target_idx+1;
|
||||
}
|
||||
for (i=0, l=elem_list_slice.length; i<l;i++) {
|
||||
elem_list[idx+i].appendChild(elem_list_slice[i].children[0]);
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
function allowDrop(ev) {
|
||||
ev.preventDefault();
|
||||
drop_id = ev.dataTransfer.getData("text");
|
||||
};
|
||||
|
||||
function drag(ev, source) {
|
||||
src_id = ev.target.parentElement;
|
||||
ev.dataTransfer.setData("text", ev.target.id);
|
||||
};
|
||||
|
||||
function drop(ev, target) {
|
||||
ev.preventDefault();
|
||||
var data = ev.dataTransfer.getData("text");
|
||||
if (src_id.id != target.id) {
|
||||
dropTarget = moveElements(target);
|
||||
if (dropTarget) {
|
||||
dropTarget.appendChild(document.getElementById(data));
|
||||
updatePlaylist();
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function adjustTl() {
|
||||
var child = document.getElementById('timeline').children;
|
||||
var divWidth = 100 / child.length;
|
||||
for (i=0, l=child.length;i<l;i++) {
|
||||
child[i].style.width= divWidth + "%";
|
||||
}
|
||||
};
|
||||
|
||||
function addAttr(id, attr, val , child=-1) {
|
||||
var elem = document.getElementById(id);
|
||||
if (child>-1){
|
||||
elem = elem.children[child];
|
||||
}
|
||||
var att = document.createAttribute(attr);
|
||||
att.value = val;
|
||||
elem.setAttributeNode(att);
|
||||
};
|
||||
|
||||
function addElement(type, attr, meta = 0, j = 0){
|
||||
var elem = document.createElement(type);
|
||||
var keys_array = Object.keys(attr);
|
||||
for (i=0, l=keys_array.length;i<l;i++) {
|
||||
var att = document.createAttribute(keys_array[i]);
|
||||
if(!i){
|
||||
att.value = Object.values(attr)[i]+j;
|
||||
} else {
|
||||
att.value = Object.values(attr)[i];
|
||||
}
|
||||
elem.setAttributeNode(att);
|
||||
}
|
||||
// Set playlist id attribute
|
||||
if (meta) {
|
||||
att = document.createAttribute("media_id");
|
||||
att.value = meta[0];
|
||||
elem.setAttributeNode(att);
|
||||
}
|
||||
// Get filename
|
||||
elem.innerText = meta[1];
|
||||
return elem;
|
||||
};
|
||||
|
||||
var scanInterval = 3000;
|
||||
// Bouttons de commande
|
||||
addEventListener("DOMContentLoaded", function() {
|
||||
sendCmd("/scan");
|
||||
sendCmd("/browse");
|
||||
sendCmd("/all/rssi");
|
||||
sendCmd("/all/list");
|
||||
adjustTl();
|
||||
// Tous les elements avec la classe ".command"
|
||||
var commandButtons = document.querySelectorAll(".command");
|
||||
for (var i=0, l=commandButtons.length; i<l; i++) {
|
||||
var button = commandButtons[i];
|
||||
// Sur un click
|
||||
button.addEventListener("click", function(event) {
|
||||
// On intercepte le signal
|
||||
event.preventDefault();
|
||||
// On recupere la valeur de value="" sur le bouton
|
||||
var clickedButton = event.currentTarget;
|
||||
var command = clickedButton.value;
|
||||
if ( command.indexOf("/reboot" ) > -1 || command.indexOf("/poweroff") > -1 ) {
|
||||
if ( !confirm("Êtes vous certain de vouloir effectuer cette action ?") ) {
|
||||
return 0;
|
||||
}
|
||||
} else if ( command == "/scan" ) {
|
||||
document.getElementById("status_all").innerHTML = "Searching network for live hosts...";
|
||||
} else if ( command.indexOf("/sort") > -1 ){
|
||||
if (command.indexOf('/1/') > -1 ) {
|
||||
clickedButton.value = clickedButton.value.replace('/1/','/0/')
|
||||
} else {
|
||||
clickedButton.value = clickedButton.value.replace('/0/','/1/')
|
||||
}
|
||||
} else if ( command.indexOf("/move") > -1 ) {
|
||||
const test_array = [21,19,20];
|
||||
for (i=test_array.length, l=0;i>l;i--){
|
||||
console.log(test_array[i-1]);
|
||||
sendCmd("/all/move/" + test_array[i-1] + "/1");
|
||||
};
|
||||
sendCmd("/all/list");
|
||||
//setInterval( sendCmd, scanInterval, "/all/move/16/1");
|
||||
};
|
||||
|
||||
// On envoie la commande en AJAX
|
||||
var request = new XMLHttpRequest();
|
||||
if ( command == "/scan" ) {
|
||||
request.onload = sendCmd(command);
|
||||
}
|
||||
// On construit la commande
|
||||
request.open("GET", command, true);
|
||||
// et on l'envoie
|
||||
request.send();
|
||||
});
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Affichage des infos
|
||||
function parseResult(command, infos_array) {
|
||||
switch (command) {
|
||||
case "/all/status":
|
||||
// Iterate over array
|
||||
for (var i = 0, l=infos_array.length; i<l; i++) {
|
||||
// Get filename, time/length
|
||||
document.getElementById("status_"+infos_array[i].host).innerHTML = infos_array[i].file + " <br/> " + infos_array[i].time + " / " + infos_array[i].leng;
|
||||
medias_status[infos_array[i].id] = infos_array[i].pos;
|
||||
// Find currently playing element
|
||||
//~ var pl_length = document.getElementById("timeline").getAttribute("length");
|
||||
//~ for (j=0,k=pl_length;j<k;j++){
|
||||
//~ if (document.getElementById("timeline").children[j].children[0].getAttribute('media_id') == infos_array[i].id ) {
|
||||
//~ addAttr(document.getElementById("timeline").children[j].children[0].id, "pos", infos_array[i].pos);
|
||||
//~ }
|
||||
//~ };
|
||||
//~ document.getElementById("timeline").children[0].children[0].hasAttribute('media_id')
|
||||
// Toggle loop indicator
|
||||
if (infos_array[i].loop == "true") {
|
||||
document.getElementById("loop_ind_" + infos_array[i].host).style.backgroundColor = "#78E738"
|
||||
} else {
|
||||
document.getElementById("loop_ind_" + infos_array[i].host).style.backgroundColor = "#A42000"
|
||||
};
|
||||
// Toggle repeat indicator
|
||||
if (infos_array[i].repeat == "true") {
|
||||
document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#78E738"
|
||||
} else {
|
||||
document.getElementById("repeat_ind_" + infos_array[i].host).style.backgroundColor = "#A42000"
|
||||
};
|
||||
};
|
||||
break;
|
||||
case "/all/list":
|
||||
|
||||
for (var i = 0, l=infos_array.length; i<l; i++) {
|
||||
// Fill Playlist infos
|
||||
document.getElementById("playlist_"+infos_array[i].host).innerHTML = infos_array[i].leng + " item(s) in playlist - " + infos_array[i].duration;
|
||||
// Build html table and timeline
|
||||
var items_array = Array.from(infos_array[i].items);
|
||||
//console.log(items_array.length);
|
||||
if (items_array.length == 0){
|
||||
var child_list = Array.from(document.getElementById("timeline").children);
|
||||
for(i=0,l=child_list.length;i<l;i++){
|
||||
document.getElementById("timeline").removeChild(child_list[i]);
|
||||
};
|
||||
break;
|
||||
}
|
||||
var html_table = "<table>" +
|
||||
"<tr>" +
|
||||
"<th>Id</th>" +
|
||||
"<th>Filename</th>" +
|
||||
"<th>Duration</th>" +
|
||||
"</tr>";
|
||||
for (var j = 0, k=items_array.length; j<k; j++) {
|
||||
// Table
|
||||
item_meta = items_array[j].split(';');
|
||||
html_table += "<tr>" +
|
||||
"<td>" + item_meta[0] + "</td>" +
|
||||
"<td>" + item_meta[1] + "</td>" +
|
||||
"<td>" + item_meta[2] + "</td>" +
|
||||
"</tr>" ;
|
||||
// Timeline
|
||||
var child_node = addElement("div", tl_drag_attr, item_meta, j);
|
||||
var len = document.getElementById("timeline").children.length;
|
||||
addAttr("timeline", "length", len);
|
||||
if ( len < items_array.length ) {
|
||||
document.getElementById("timeline").appendChild( addElement("div", tl_cont_attr, 0, len) );
|
||||
}
|
||||
document.getElementById(tl_cont_attr.id + j).replaceChildren(child_node);
|
||||
// Adjust elements width
|
||||
adjustTl();
|
||||
// Highlight currently playing element
|
||||
if (item_meta[3] != ""){
|
||||
document.getElementById(tl_cont_attr.id + j).children[0].style.borderBottom = "4px solid " + timeline_color_cursor;
|
||||
document.getElementById(tl_cont_attr.id + j).children[0].style.fontWeight = "bold";
|
||||
var pos = medias_status[item_meta[0]] * 100;
|
||||
//~ pos = pos.toPrecision(2);
|
||||
var pos1 = pos-1 + "%";
|
||||
pos = pos + "%";
|
||||
//console.log( "linear-gradient(90deg," + timeline_color2 + " " + pos1 + ", " + timeline_color1 + " " + pos + ", " + timeline_color2 + " " + pos + ")" );
|
||||
document.getElementById(tl_cont_attr.id + j).children[0].style.background = "linear-gradient(90deg," + timeline_color_bg + " " + pos1 + ", " + timeline_color_cursor + " " + pos + ", " + timeline_color_bg + " " + pos + ")";
|
||||
}
|
||||
}
|
||||
html_table += "</table>";
|
||||
document.getElementById("playlist_"+infos_array[i].host).innerHTML += html_table;
|
||||
};
|
||||
break;
|
||||
case "/scan":
|
||||
var host_up = infos_array[0];
|
||||
var host_down = infos_array[1];
|
||||
for ( var i=0, l=host_up.length; i<l; i++){
|
||||
document.getElementById(host_up[i]).style.display = 'block';
|
||||
}
|
||||
for ( var i=0, l=host_down.length; i<l; i++){
|
||||
document.getElementById(host_down[i]).style.display = 'none';
|
||||
}
|
||||
if (host_up.length) {
|
||||
scanInterval = 10000;
|
||||
document.getElementById("status_all").innerHTML = "Scan intarvel set to " + scanInterval;
|
||||
}
|
||||
document.getElementById("status_all").innerHTML = host_up.length + " client(s) found.";
|
||||
break;
|
||||
case "/browse":
|
||||
var html_table = "<table>" +
|
||||
"<tr>" +
|
||||
"<th>Filename</th>" +
|
||||
"<th>Duration</th>" +
|
||||
"</tr>";
|
||||
for (var j = 0, k=infos_array.length; j<k; j++) {
|
||||
html_table += "<tr>" +
|
||||
"<td>" + infos_array[j] + "</td>" +
|
||||
"<td>" + "00:00" + "</td>" +
|
||||
"</tr>" ;
|
||||
}
|
||||
html_table += "</table>";
|
||||
document.getElementById("filelist").innerHTML += html_table;
|
||||
break;
|
||||
case "/all/rssi":
|
||||
var signal_color = 40;
|
||||
var best_rssi = 30;
|
||||
var worst_rssi = 70;
|
||||
for (var j = 0, k=infos_array.length; j<k; j++) {
|
||||
var rssi_norm = Math.ceil( (worst_rssi - parseInt(infos_array[j].rssi) ) / ( worst_rssi - best_rssi ) * 4 );
|
||||
signal_color = (rssi_norm-1) * signal_color;
|
||||
// Reset to grey
|
||||
for (i=0, l=4; i<l;i++) {
|
||||
document.getElementById("wl_"+i).style.backgroundColor = "hsl(0, 0%, 65%)";
|
||||
};
|
||||
// Color it
|
||||
for (i=0, l=rssi_norm; i<l;i++) {
|
||||
document.getElementById("wl_"+i).style.backgroundColor = "hsl(" + signal_color + ", 100%, 50%)";
|
||||
};
|
||||
};
|
||||
break;
|
||||
}; // End switch case
|
||||
};
|
||||
|
||||
function sendCmd(command) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.onload = function() {
|
||||
if (request.readyState === request.DONE) {
|
||||
if (request.status === 200) {
|
||||
// responseText is a string, use parse to get an array.
|
||||
var infos_array = JSON.parse(request.responseText);
|
||||
//console.log(infos_array);
|
||||
parseResult(command, infos_array);
|
||||
//return infos_array;
|
||||
};
|
||||
};
|
||||
};
|
||||
// On construit la commande
|
||||
request.open("GET", command, true);
|
||||
// et on l'envoie
|
||||
request.send();
|
||||
};
|
||||
|
||||
setInterval( sendCmd, 500, "/all/status");
|
||||
setInterval( sendCmd, 1000, "/all/list");
|
||||
setInterval( sendCmd, scanInterval, "/scan");
|
||||
setInterval( sendCmd, 20000, "/all/rssi");
|
||||
|
155
static/style.css
155
static/style.css
|
@ -1,155 +0,0 @@
|
|||
body {color:#fff;background-color:#666;margin:0;}
|
||||
div {box-sizing: border-box;}
|
||||
h2 {margin:0;}
|
||||
table {background-color: #555;margin:.5em;max-width:100%;font-size:.9em}
|
||||
td {padding: 0;}
|
||||
th {background-color: #aaa;}
|
||||
tr:nth-child(2n+1) {background-color: #888;}
|
||||
* {box-sizing: border-box;}
|
||||
.btn-group-lg > .btn, .btn-lg {
|
||||
padding: 10px 16px;
|
||||
font-size: 18px;
|
||||
line-height: 1.3333333;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#master_remote {
|
||||
background: linear-gradient(0deg, #222 10%, #444 80%);
|
||||
min-height: 20em;
|
||||
}
|
||||
#master_remote .right_col {background-color:transparent;}
|
||||
.client_container {
|
||||
border-bottom: #222 solid 1px;
|
||||
display:none;
|
||||
background: linear-gradient(0deg, #999 10%, #666 80%);
|
||||
}
|
||||
.client_container .button{}
|
||||
/*
|
||||
.timeline {height: 3em;background-color: #0f0;margin: 2em 0;}
|
||||
*/
|
||||
#timeline {
|
||||
height: 75px;
|
||||
width:100%;
|
||||
margin-top:1em;
|
||||
}
|
||||
[id^="tl_cont"] {
|
||||
float: left;
|
||||
width: 10%;
|
||||
height: 75px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid #aaaaaa;
|
||||
}
|
||||
[id^="tl_drag"] {
|
||||
cursor: grab;
|
||||
text-align:center;
|
||||
height:75px;
|
||||
line-height: 75px;
|
||||
width:100%;
|
||||
background-color:#1F7B99;
|
||||
}
|
||||
[id^="tl_cont"]:nth-child(2n+1) [id^="tl_drag"] {
|
||||
background-color:#255E70;
|
||||
}
|
||||
|
||||
.client_container:nth-child(2n+1) {background-color:#444;}
|
||||
.command {margin: 0 !important;}
|
||||
.buttons {width:75%;margin:auto;text-align: center;padding: 2em;}
|
||||
.btn {
|
||||
margin:auto;
|
||||
width:3em;
|
||||
height: 4em;
|
||||
display:inline-block;
|
||||
padding: 0;
|
||||
font-weight: 400;
|
||||
line-height: 1.42857143;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
-ms-touch-action: manipulation;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(0deg, #b9b9b9 10%, #f9f9f9 80%);
|
||||
}
|
||||
.btn:hover {
|
||||
background:#C8FFBD;
|
||||
box-shadow: 0 0 10px #A5FF9F;
|
||||
}
|
||||
.btn:active {
|
||||
background: #91FF7C;
|
||||
box-shadow: 0 0 18px #91FF7C;
|
||||
}
|
||||
.btn_txt {display: block;font-size: small;}
|
||||
/*Right column*/
|
||||
.right_col {width: 79.9%;display: inline-block;}
|
||||
/*Left column*/
|
||||
.left_col {width: 20%;display: inline-block;float: left;clear: left;}
|
||||
.left_col button {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
padding: 0;
|
||||
line-height: 2em;
|
||||
}
|
||||
.left_col button .btn_txt {
|
||||
display:none;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: medium;
|
||||
background-color: #ff3030;
|
||||
border: 1px solid #fff;
|
||||
padding: 0 .5%;
|
||||
width: 100px;
|
||||
position: absolute;
|
||||
}
|
||||
.left_col button:hover .btn_txt {display:block;}
|
||||
.col_1 {
|
||||
width: 40%;
|
||||
float: left;
|
||||
padding: 3% 0 0 5%;
|
||||
}
|
||||
}
|
||||
.col_2 {
|
||||
width: 60%;
|
||||
display: inline-block;
|
||||
clear: right;
|
||||
}
|
||||
.indicator {
|
||||
display:inline-block;
|
||||
background-color: #C32600;
|
||||
margin: 0 0 0 5%;
|
||||
padding: 0.3em;
|
||||
}
|
||||
.wl_indicator {
|
||||
display: inline-block;
|
||||
background-color: #bbb;
|
||||
vertical-align: bottom;
|
||||
margin: 0 1px;
|
||||
padding: 0;
|
||||
height: .5em;
|
||||
width: 5%;
|
||||
}
|
||||
#wl_0 {height:.5em;}
|
||||
#wl_1 {height:.65em;}
|
||||
#wl_2 {height:.80em;}
|
||||
#wl_3 {height:.95em;}
|
||||
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
table {margin:0}
|
||||
td {padding:0;}
|
||||
.btn-group-lg > .btn, .btn-lg {
|
||||
font-size:14px;
|
||||
}
|
||||
.left_col {width: 30%;}
|
||||
.right_col {width: 69.9%;}
|
||||
.col_2 {
|
||||
overflow: scroll;
|
||||
font-size: .9em;
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<title>RPi Web Server</title>
|
||||
<!--
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
||||
-->
|
||||
<!--
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
|
||||
-->
|
||||
<link rel="stylesheet" type="text/css" href="{{url_for('static', filename='style.css')}}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="master_remote">
|
||||
<div class="left_col">
|
||||
<h2>Videopi commander</h2>
|
||||
<p id="status_all">{{status_message}}</p>
|
||||
<p id="filelist">No files.</p>
|
||||
</div>
|
||||
<div class="right_col">
|
||||
<p class="buttons">
|
||||
<button value="/scan" class="command btn btn-block btn-lg btn-default" role="button">🔍<span class="btn_txt">Scan<br/>réseau</span></button>
|
||||
<button value="/all/previous" class="command btn btn-block btn-lg btn-default" role="button">⏮<span class="btn_txt">Préc.</span></button>
|
||||
<button value="/all/play" class="command btn btn-block btn-lg btn-default" role="button">⏵<span class="btn_txt">Lecture</span></button>
|
||||
<button value="/all/pause" class="command btn btn-block btn-lg btn-default" role="button">⏸<span class="btn_txt">Pause</span></button>
|
||||
<button value="/all/stop" class="command btn btn-block btn-lg btn-default" role="button">⏹<span class="btn_txt">Stop</span></button>
|
||||
<button value="/all/next" class="command btn btn-block btn-lg btn-default" role="button">⏭<span class="btn_txt">Suivant</span></button>
|
||||
<button value="/all/repeat" class="command btn btn-block btn-lg btn-default" role="button">🔂<span class="btn_txt">Répéter<br/>élem.</span></button>
|
||||
<button value="/all/loop" class="command btn btn-block btn-lg btn-default" role="button">🔁<span class="btn_txt">Boucler<br/>liste</span></button>
|
||||
<button value="/all/clear" class="command btn btn-block btn-lg btn-default" role="button">X<span class="btn_txt">Vider<br/>listes</span></button>
|
||||
<button value="/all/enqueue/tst.mp4" class="command btn btn-block btn-lg btn-default" role="button">β<span class="btn_txt">tst.mp4</span></button>
|
||||
<button value="/all/enqueue/tst1.mp4" class="command btn btn-block btn-lg btn-default" role="button">β<span class="btn_txt">tst1.mp4</span></button>
|
||||
<button value="/all/enqueue/sangliers_1080.mp4" class="command btn btn-block btn-lg btn-default" role="button">β<span class="btn_txt">sangli.mp4</span></button>
|
||||
<button value="/all/move/0/1" class="command btn btn-block btn-lg btn-default" role="button">β<span class="btn_txt">movePl</span></button>
|
||||
<button value="/sync/all" class="command btn btn-block btn-lg btn-default" role="button">↭<span class="btn_txt">Sync</span></button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% for host in hosts %}
|
||||
<div class="client_container" id="{{ host }}">
|
||||
<div class="left_col">
|
||||
<h2>{{ host }}</h2>
|
||||
<div class="col_1">
|
||||
<button value="/{{host}}/poweroff" class="command btn btn-block btn-lg btn-default" role="button">⏻<span class="btn_txt">Éteindre</span></button>
|
||||
<button value="/{{host}}/reboot" class="command btn btn-block btn-lg btn-default" role="button">↺<span class="btn_txt">Redémarrer</span></button>
|
||||
<button value="/{{host}}/blink" class="command btn btn-block btn-lg btn-default" role="button">💡<span class="btn_txt">Blink</span></button>
|
||||
<p id="status_{{ host }}">{{status_message}}</p>
|
||||
<p id="signal_{{ host }}">
|
||||
<span style="">Link:</span>
|
||||
<span class="wl_indicator" id="wl_0"></span>
|
||||
<span class="wl_indicator" id="wl_1"></span>
|
||||
<span class="wl_indicator" id="wl_2"></span>
|
||||
<span class="wl_indicator" id="wl_3"></span>
|
||||
</p>
|
||||
<p id="loop_ind_{{ host }}" class="indicator">Loop</p>
|
||||
<p id="repeat_ind_{{ host }}" class="indicator">Repeat</p>
|
||||
</div>
|
||||
<div class="col_2">
|
||||
<p id="playlist_{{ host }}">No files queued.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right_col">
|
||||
<div id="timeline">
|
||||
<!--
|
||||
<div id="tl_contX"></div>
|
||||
-->
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button value="/{{host}}/previous" class="command btn btn-block btn-lg btn-default" role="button">⏮<span class="btn_txt">Préc.</span></button>
|
||||
<button value="/{{host}}/play" class="command btn btn-block btn-lg btn-default" role="button">⏵<span class="btn_txt">Lecture</span></button>
|
||||
<button value="/{{host}}/pause" class="command btn btn-block btn-lg btn-default" role="button">⏸<span class="btn_txt">Pause</span></button>
|
||||
<button value="/{{host}}/stop" class="command btn btn-block btn-lg btn-default" role="button">⏹<span class="btn_txt">Stop</span></button>
|
||||
<button value="/{{host}}/next" class="command btn btn-block btn-lg btn-default" role="button">⏭<span class="btn_txt">Suivant</span></button>
|
||||
<button value="/{{host}}/repeat" class="command btn btn-block btn-lg btn-default" role="button">🔂<span class="btn_txt">Répéter<br/>élem.</span></button>
|
||||
<button value="/{{host}}/loop" class="command btn btn-block btn-lg btn-default" role="button">🔁<span class="btn_txt">Boucler<br/>liste</span></button>
|
||||
<button value="/{{host}}/clear" class="command btn btn-block btn-lg btn-default" role="button">X<span class="btn_txt">Vider<br/>liste</span></button>
|
||||
<button id="toggle_val_{{host}}}" value="/{{host}}/sort/1/id" class="command btn btn-block btn-lg btn-default" role="button">🔀<span class="btn_txt">Trier<br/>liste</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</body>
|
||||
<script type="text/javascript" src="{{url_for('static', filename='script.js')}}"></script>
|
||||
</html>
|
Loading…
Reference in New Issue