<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<link rel="manifest" href="ir.webmanifest">
<link rel="icon" href="ir.png">
<title>IR</title>
<script>
"use strict"
function fillTemplate(template,values) {
const newElement=template.content.cloneNode(true);
newElement.querySelectorAll('*').forEach(
(e) => {
if (e.tagName == 'SLOT') {
const valueKey=e.textContent.trim();
e.replaceWith(e.ownerDocument.createTextNode(values[valueKey]));
return;
}
const attrs = e.attributes;
for (var i = attrs.length - 1; i >= 0; i--) {
if (attrs[i].name.startsWith('slot:')) {
const name=attrs[i].name.replace('slot:','');
const valueKey=attrs[i].value;
e.removeAttribute(attrs[i].name);
if (values.hasOwnProperty(valueKey)) {
e.setAttribute(name,values[valueKey]);
}
}
}
},
);
return newElement;
}
function call(method,path,args) {
args ||= {};
let url=new URL(path,location.href);
for (let a in args) {
url.searchParams.set(a,args[a]);
}
return fetch(url, {
method: method,
mode: 'no-cors',
});
}
async function vlcCommand(command,args) {
await call('post',`vlc/${command}`,args);
}
async function irCommand(thing,arg) {
await call('post',`ir/${thing}/${arg}`);
}
async function updateView() {
const statusRes = (await (await call('get','vlc/status')).json());
const status = statusRes.status;
const playlist = statusRes.playlist;
const current = playlist.find(i => { return i.current });
if (current) {
document.querySelector('#playing-name span').textContent = current.name;
}
const currentPos = document.querySelector('#current-pos');
currentPos.disabled = status.state != 'playing';
const length = parseInt(status.length);
if (!length) { return }
const time = parseInt(status.time);
currentPos.max=length;
currentPos.value=time;
}
async function play(id) {
await vlcCommand(`play/${id}`);
const controls = document.querySelector('#vlc-controls');
controls.open=true;
controls.scrollIntoView(true);
}
const filesOpenAbort = new AbortController();
const ageToClass = [
[ 86400, 'ageDay' ],
[ 86400*7, 'ageWeek' ],
[ 86400*30, 'ageMonth' ],
[ 86400*180, 'ageSixMonth' ],
[ 86400*365, 'ageYear' ],
[ 1e10, 'ageNever' ],
];
function fillFileTemplates(list) {
const itemTemplate = document.querySelector('#file-item');
return list.map(f => {
const age = Math.floor(Date.now()/1000) - f.watched_time;
const ageClass = (ageToClass.find(e => e[0] > age) || [0,'ageNever'])[1];
return fillTemplate(itemTemplate, {
'url': f.is_dir ? `javascript:browseTo(${f.id})` : `javascript:play('${f.id}')`,
'name': f.name,
'dirclass': f.is_dir ? 'dir' : 'file',
'ageclass': ageClass,
});
});
}
async function browseTo(id) {
filesOpenAbort.abort();
const filesElem = document.querySelector('#files');
filesElem.open=true;
filesElem.scrollIntoView(true);
document.querySelector('#file-search').value='';
const fileList = document.querySelector('ul#file-list');
const pathList = document.querySelector('ul#path-list');
const itemTemplate = document.querySelector('#file-item');
const browseData = await (await call('get',`media${ id ? '/' + id : ''}`)).json();
const fileItems = fillFileTemplates(browseData.children);
fileList.replaceChildren(...fileItems);
const parents = browseData.parents ? [{id:null,name:'(root)'},...browseData.parents] : [];
const pathItems = parents.map(f => {
return fillTemplate(itemTemplate, {
'url': `javascript:browseTo(${f.id})`,
'name': f.name,
});
});
pathList.replaceChildren(...pathItems);
}
async function loadRecent() {
const recentList = document.querySelector('ul#recent-list');
const recentData = await (await call('get','media/recent')).json();
const recentItems = fillFileTemplates(recentData)
recentList.replaceChildren(...recentItems);
}
document.addEventListener('readystatechange', (event) => {
if (document.readyState == 'complete') {
document.querySelector('#files').
addEventListener(
'toggle',
async (event) => {
if (event.target.open) {
await browseTo(null);
}
},
{
once:true,
passive:true,
signal:filesOpenAbort.signal,
},
);
document.querySelector('#file-search').
addEventListener(
'input',
(event) => {
const textRe = new RegExp(event.target.value,'i');
document.querySelector('ul#file-list').childNodes.forEach(
e => {
if (e.tagName != "LI") { return; }
const match = e.textContent.search(textRe) >= 0;
if (match) {
e.classList.remove('hidden');
} else {
e.classList.add('hidden');
}
},
);
},
);
document.querySelector('#recent').
addEventListener(
'toggle',
async (event) => {
if (event.target.open) {
await loadRecent();
}
},
{ passive:true },
);
let vlcUpdateIntervalId;
document.querySelector('#vlc-controls').
addEventListener(
'toggle',
(event) => {
if (vlcUpdateIntervalId) {
clearInterval(vlcUpdateIntervalId);
vlcUpdateIntervalId=null;
}
if (event.target.open) {
updateView();
vlcUpdateIntervalId = setInterval(updateView, 1000);
}
},
{ passive:true },
);
document.querySelector('#current-pos').
addEventListener(
'change',
(event) => {
vlcCommand('seek',{val:event.target.value});
},
{ passive: true },
);
}
});
</script>
<style>
body { font-size: 12vw }
* { font-size: inherit }
.hidden {
display: none;
}
div.thing {
padding: 0.2em 0.1em 0.2em 0.1em;
margin-top: 0.5em;
margin-bottom: 1em;
border: solid 0.05em black;
border-radius: 0.5em;
position: relative;
}
div.thing label {
font-size: 0.8em;
line-height: 0.5em;
position: absolute;
top: -0.5em;
left: 1em;
background-color: white;
}
div.thing table {
width: 100%;
table-layout: fixed;
}
td button {
border-radius: 0.5em;
width: calc(100% - 0.2em);
margin: 0.1em;
}
button.small {
border-radius: 0.2em;
font-size: 0.4em;
}
.power { color: white }
.power.on { background-color: green }
.power.off { background-color: red }
.volume { background-color: lightblue }
.input { background-color: darkorange }
.control { background-color: black; color: white; filter: grayscale(100%) }
.bd-red { background-color: red }
.bd-yellow { background-color: yellow }
.bd-blue { background-color: blue; color: white }
input[type=range] {
display: block;
width: 100%;
height: 1em;
position: relative;
background: transparent;
}
[type=range]::-moz-range-track {
width: 100%;
height: 0.2em;
background: #ccc;
}
[type=range]::-moz-range-progress {
height: 0.4em;
background: #48a;
}
[type=range]::-moz-range-thumb {
border: solid 0.1em #48a;
padding: 0;
width: 0.4em;
height: 0.8em;
border-radius: 0.5em;
background: #4bf;
}
.file-list {
width: 100%;
overflow-x: scroll;
}
.file-list summary {
position: sticky;
left: 0;
}
.file-list a {
text-decoration: none;
color: inherit;
}
.file-list ul {
width: min-content;
margin-top: 0;
padding-left: 0;
font-size: 50%;
list-style-type: none;
list-style-position: inside;
}
.file-list li {
white-space: pre;
}
#file-search-p {
margin: 0.2em;
border: dotted thick blue;
}
#file-search {
border: none;
}
#path-list {
display: flex;
}
#path-list li::before {
content: '/';
}
#path-list li:first-child::before {
content: '';
}
.ageDay { background-color: hsl(180,50%,50%) }
.ageWeek { background-color: hsl(180,50%,60%) }
.ageMonth { background-color: hsl(180,50%,70%) }
.ageSixMonth { background-color: hsl(180,50%,80%) }
.ageYear { background-color: hsl(180,50%,90%) }
.ageNever { background-color: hsl(180,50%,100%) }
li.dir::marker { content: '📁' }
li.dir::after { content: '/' }
li.file::marker { content: '🎞️' }
#playing-name p {
white-space: pre;
font-size: 50%;
width: 100%;
overflow-x: scroll;
}
</style>
</head>
<body>
<div class="thing">
<label>Vlc</label>
<table>
<tr>
<td><button onclick="irCommand('start','hdmi3')" class="power on">start</button></td>
<td><button onclick="irCommand('stop','hdmi')" class="power off">stop</button></td>
</tr>
</table>
<details id="vlc-controls">
<summary>controls</summary>
<table>
<tr>
<td colspan="2"><button onclick="vlcCommand('play')" class="control">▶︎</button></td>
<td colspan="2"><button onclick="vlcCommand('pause')" class="control">⏸︎</button></td>
<td colspan="2"><button onclick="vlcCommand('stop')" class="control">⏹︎</button></td>
</tr>
<tr>
<td colspan="3"><button onclick="vlcCommand('subs')" class="control">💬︎</button></td>
<td colspan="3"><button onclick="vlcCommand('audio')" class="control">🗣︎</button></td>
</tr>
<tr>
<td colspan="6" id="playing-name"><p><span></span></p></td>
</tr>
<tr>
<td colspan="6"><input type="range" id="current-pos"></td>
</tr>
<tr>
<td><button onclick="vlcCommand('seek',{val:'-60'})" class="control small">-1'</button></td>
<td><button onclick="vlcCommand('seek',{val:'-30'})" class="control small">-30"</button></td>
<td><button onclick="vlcCommand('seek',{val:'-15'})" class="control small">-15"</button></td>
<td><button onclick="vlcCommand('seek',{val:'+14'})" class="control small">+15"</button></td>
<td><button onclick="vlcCommand('seek',{val:'+28'})" class="control small">+30"</button></td>
<td><button onclick="vlcCommand('seek',{val:'+58'})" class="control small">+1'</button></td>
</tr>
<tr>
<td colspan="2"><button onclick="vlcCommand('snapshot')" class="control">📷</button></td>
<td colspan="4"> </td>
</tr>
</table>
</details>
<details id="files" class="file-list">
<summary>files</summary>
<p id="file-search-p"><input type="text" id="file-search"></p>
<ul id="path-list">
</ul>
<ul id="file-list">
</ul>
</details>
<details id="recent" class="file-list">
<summary>recent</summary>
<ul id="recent-list">
</ul>
</details>
</div>
<div class="thing">
<label>Laptop</label>
<table>
<tr>
<td><button onclick="irCommand('start','hdmi1')" class="power on">start</button></td>
<td><button onclick="irCommand('stop','hdmi')" class="power off">stop</button></td>
</tr>
</table>
</div>
<div class="thing">
<label>Bluray</label>
<table>
<tr>
<td><button onclick="irCommand('start','bluray')" class="power on">start</button></td>
<td><button onclick="irCommand('stop','bluray')" class="power off">stop</button></td>
</tr>
</table>
<details>
<summary>controls</summary>
<table>
<tr>
<td><button onclick="irCommand('bluray','ejectcd')" class="input">⏏</button></td>
<td> </td>
<td><button onclick="irCommand('bluray','power')" class="power on">ON</button></td>
</tr>
<tr>
<td><button onclick="irCommand('bluray','yellow')" class="bd-yellow">a</button></td>
<td><button onclick="irCommand('bluray','blue')" class="bd-blue">b</button></td>
<td><button onclick="irCommand('bluray','red')" class="bd-red">c</button></td>
</tr>
<tr>
<td><button onclick="irCommand('bluray','menu')" class="control">🔝︎</button></td>
<td><button onclick="irCommand('bluray','up')" class="control">↑</button></td>
<td><button onclick="irCommand('bluray','context_menu')" class="control">⎙</button></td>
</tr>
<tr>
<td><button onclick="irCommand('bluray','left')" class="control">←</button></td>
<td><button onclick="irCommand('bluray','ok')" class="control">ok</button></td>
<td><button onclick="irCommand('bluray','right')" class="control">→</button></td>
</tr>
<tr>
<td><button onclick="irCommand('bluray','back')" class="control">🔙︎</button></td>
<td><button onclick="irCommand('bluray','down')" class="control">↓</button></td>
<td><button onclick="irCommand('bluray','option')" class="control">opt</button></td>
</tr>
<tr>
<td colspan="3"><button onclick="irCommand('bluray','home')" class="control">home</button></td>
</tr>
<tr>
<td><button onclick="irCommand('bluray','previous')" class="control">⏪︎</button></td>
<td><button onclick="irCommand('bluray','play')" class="control">▶</button></td>
<td><button onclick="irCommand('bluray','next')" class="control">⏩︎</button></td>
</tr>
<tr>
<td><button onclick="irCommand('bluray','review')" class="control">⏮</button></td>
<td><button onclick="irCommand('bluray','pause')" class="control">⏸</button></td>
<td><button onclick="irCommand('bluray','fastforward')" class="control">⏭</button></td>
</tr>
<tr>
<td><button onclick="irCommand('bluray','subtitle')" class="control">sub</button></td>
<td><button onclick="irCommand('bluray','stop')" class="control">⏹</button></td>
<td><button onclick="irCommand('bluray','audio')" class="control">aud</button></td>
</tr>
<tr>
<td> </td>
<td> </td>
<td><button onclick="irCommand('bluray','displaytoggle')" class="control">disp</button></td>
</tr>
</table>
</details>
</div>
<div class="thing">
<label>Amp</label>
<table>
<tr>
<td><button onclick="irCommand('amplifier','power')" class="power on">ON</button></td>
<td><button onclick="irCommand('amplifier','power')" class="power off">OFF</button></td>
<td><button onclick="irCommand('amplifier','volumeup')" class="volume">↑</button></td>
</tr>
<tr>
<td> </td>
<td> </td>
<td><button onclick="irCommand('amplifier','volumedown')" class="volume">↓</button></td>
</tr>
<tr>
<td><button onclick="irCommand('amplifier','hdmi1')" class="input">Lap</button></td>
<td><button onclick="irCommand('amplifier','hdmi2')" class="input">BD</button></td>
<td><button onclick="irCommand('amplifier','hdmi3')" class="input">NAS</button></td>
</tr>
</table>
</div>
<div class="thing">
<label>Projector</label>
<table>
<tr>
<td><button onclick="irCommand('projector','power')" class="power on">ON</button></td>
<td><button onclick="irCommand('projector','suspend')" class="power off">OFF</button></td>
</tr>
</table>
</div>
<template id="file-item">
<li slot:class="dirclass"><a slot:href="url"><span slot:class="ageclass"><slot>name</slot></span></a></li>
</template>
</body>
</html>