commit f9f8e796951cb44b01d2ae99ca21078767e52ab7
Author: Jens Kristoffersson <jens.se@icloud.com>
AuthorDate: Sun Apr 5 14:54:43 2026 +0200
Commit: Jens Kristoffersson <jens.se@icloud.com>
CommitDate: Sun Apr 5 14:54:43 2026 +0200
update
---
kiosk/addons/emulator/__init__.py | 16 +++
kiosk/addons/emulator/routes.py | 181 +++++++++++++++++++++++++
kiosk/app.py | 21 ++-
kiosk/static/addons/emulator/panel.js | 203 +++++++++++++++++++++++++++++
kiosk/static/admin.js | 31 ++++-
kiosk/static/display.js | 7 +-
kiosk/templates/addons/emulator/panel.html | 75 +++++++++++
kiosk/templates/admin.html | 16 ++-
kiosk/templates/emulator.html | 50 +++++++
9 files changed, 587 insertions(+), 13 deletions(-)
diff --git a/kiosk/addons/emulator/__init__.py b/kiosk/addons/emulator/__init__.py
new file mode 100644
index 0000000..8c92b76
--- /dev/null
+++ b/kiosk/addons/emulator/__init__.py
@@ -0,0 +1,16 @@
+ADDON = {
+ "name": "emulator",
+ "label": "Emulator",
+ "version": "1.0",
+ "order": 7,
+ "has_panel": True,
+ "has_overlay": False,
+ "has_socketio": False,
+ "state_prefix": "emulator_",
+ "default_enabled": True,
+}
+
+
+def create_blueprint():
+ from .routes import bp
+ return bp
diff --git a/kiosk/addons/emulator/routes.py b/kiosk/addons/emulator/routes.py
new file mode 100644
index 0000000..30a1d01
--- /dev/null
+++ b/kiosk/addons/emulator/routes.py
@@ -0,0 +1,181 @@
+from pathlib import Path
+from flask import Blueprint, jsonify, request, abort, render_template, send_file
+import db
+import extensions
+from auth import login_required
+from config import ALLOWED_ROOTS
+
+bp = Blueprint('emulator', __name__)
+
+# Filändelse → EJS_core-namn
+_EXT_TO_CORE = {
+ '.sfc': 'snes', '.smc': 'snes',
+ '.nes': 'nes',
+ '.gba': 'gba',
+ '.gbc': 'gbc', '.gb': 'gb',
+ '.n64': 'n64', '.z64': 'n64', '.v64': 'n64',
+ '.nds': 'nds',
+ '.md': 'segaMD', '.gen': 'segaMD', '.smd': 'segaMD',
+ '.bin': 'psx', '.iso': 'psx',
+ '.psp': 'psp',
+ '.a26': 'atari2600',
+ '.gba': 'gba',
+}
+
+_CORE_LABELS = {
+ 'snes': 'SNES',
+ 'nes': 'NES / Famicom',
+ 'gba': 'Game Boy Advance',
+ 'gbc': 'Game Boy Color',
+ 'gb': 'Game Boy',
+ 'n64': 'Nintendo 64',
+ 'nds': 'Nintendo DS',
+ 'segaMD': 'Sega Mega Drive',
+ 'psx': 'PlayStation 1',
+ 'psp': 'PSP',
+ 'atari2600': 'Atari 2600',
+}
+
+
+def _safe_path(path_str):
+ """Returnerar ett validerat Path-objekt eller None om utanför ALLOWED_ROOTS."""
+ try:
+ p = Path(path_str).resolve()
+ if any(str(p).startswith(str(r)) for r in ALLOWED_ROOTS):
+ return p
+ except Exception:
+ pass
+ return None
+
+
+# --- Spelsida (laddas i display-iframe) ---
+
+@bp.route('/emulator')
+def emulator_page():
+ rom_path = request.args.get('rom', '')
+ core = request.args.get('core', '')
+ # Validera path
+ if not rom_path or not _safe_path(rom_path):
+ abort(403)
+ if core not in _CORE_LABELS:
+ core = _EXT_TO_CORE.get(Path(rom_path).suffix.lower(), 'snes')
+ ejs_path = db.get_state().get('emulator_ejs_path',
+ 'https://cdn.emulatorjs.org/stable/data/')
+ return render_template('emulator.html',
+ rom_path=rom_path,
+ rom_name=Path(rom_path).name,
+ core=core,
+ core_label=_CORE_LABELS.get(core, core),
+ ejs_path=ejs_path)
+
+
+# --- ROM-fil (serveras till webbläsaren) ---
+
+@bp.route('/api/emulator/rom')
+def serve_rom():
+ path_str = request.args.get('path', '')
+ p = _safe_path(path_str)
+ if not p or not p.is_file():
+ abort(404)
+ if p.suffix.lower() not in _EXT_TO_CORE:
+ abort(403)
+ return send_file(p)
+
+
+# --- ROM-lista ---
+
+@bp.route('/api/emulator/roms')
+@login_required
+def list_roms():
+ folder_str = request.args.get('path', db.get_state().get('emulator_rom_dir', ''))
+ if not folder_str:
+ return jsonify([])
+ folder = _safe_path(folder_str)
+ if not folder or not folder.is_dir():
+ return jsonify([])
+ roms = []
+ for f in sorted(folder.rglob('*')):
+ if f.is_file() and f.suffix.lower() in _EXT_TO_CORE:
+ core = _EXT_TO_CORE[f.suffix.lower()]
+ roms.append({
+ 'name': f.name,
+ 'path': str(f),
+ 'core': core,
+ 'core_label': _CORE_LABELS.get(core, core),
+ 'size_mb': round(f.stat().st_size / 1_048_576, 1),
+ })
+ return jsonify(roms)
+
+
+# --- Starta emulator på display ---
+
+@bp.route('/api/emulator/launch', methods=['POST'])
+@login_required
+def launch():
+ data = request.get_json() or {}
+ rom_path = str(data.get('rom', '')).strip()
+ core = str(data.get('core', '')).strip()
+
+ if not rom_path or not _safe_path(rom_path):
+ return jsonify({'error': 'Ogiltig ROM-sökväg'}), 400
+ if core not in _CORE_LABELS:
+ core = _EXT_TO_CORE.get(Path(rom_path).suffix.lower(), 'snes')
+
+ db.set_state('emulator_active', '1')
+ db.set_state('emulator_rom', Path(rom_path).name)
+ db.set_state('emulator_rom_path', rom_path)
+ db.set_state('emulator_core', core)
+
+ url = f'/emulator?rom={rom_path}&core={core}'
+ extensions.socketio.emit('switch', {
+ 'type': 'url',
+ 'source': url,
+ 'scheduled': False,
+ })
+ return '', 204
+
+
+# --- Stoppa emulator ---
+
+@bp.route('/api/emulator/stop', methods=['POST'])
+@login_required
+def stop():
+ db.set_state('emulator_active', '0')
+ extensions.socketio.emit('switch', {
+ 'type': 'idle',
+ 'source': '',
+ 'scheduled': False,
+ })
+ return '', 204
+
+
+# --- Status ---
+
+@bp.route('/api/emulator/status')
+@login_required
+def status():
+ state = db.get_state()
+ return jsonify({
+ 'active': state.get('emulator_active', '0') == '1',
+ 'rom': state.get('emulator_rom', ''),
+ 'core': state.get('emulator_core', ''),
+ 'core_label': _CORE_LABELS.get(state.get('emulator_core', ''), ''),
+ 'rom_dir': state.get('emulator_rom_dir', ''),
+ 'ejs_path': state.get('emulator_ejs_path',
+ 'https://cdn.emulatorjs.org/stable/data/'),
+ })
+
+
+# --- Spara inställningar ---
+
+@bp.route('/api/emulator/settings', methods=['POST'])
+@login_required
+def save_settings():
+ data = request.get_json() or {}
+ rom_dir = str(data.get('rom_dir', '')).strip()
+ ejs_path = str(data.get('ejs_path', '')).strip()
+ if rom_dir:
+ db.set_state('emulator_rom_dir', rom_dir)
+ if ejs_path:
+ db.set_state('emulator_ejs_path', ejs_path)
+ return '', 204
diff --git a/kiosk/app.py b/kiosk/app.py
index ef547a8..e7eae5e 100644
--- a/kiosk/app.py
+++ b/kiosk/app.py
@@ -51,15 +51,31 @@ def _load_kiosk_conf():
_load_kiosk_conf()
app = Flask(__name__)
-app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', os.urandom(24))
+
+def _get_secret_key():
+ """Hämta en persistent SECRET_KEY från DB eller miljövariabel."""
+ env_key = os.environ.get('SECRET_KEY')
+ if env_key:
+ return env_key
+ # Spara i DB så att sessioner överlever omstart
+ import db as _db
+ _db.init_db()
+ key = _db.get_state().get('_secret_key')
+ if not key:
+ key = os.urandom(32).hex()
+ _db.set_state('_secret_key', key)
+ return key
+
+app.config['SECRET_KEY'] = _get_secret_key()
socketio = SocketIO(app, async_mode='eventlet', cors_allowed_origins='*')
import extensions as _ext
_ext.socketio = socketio
-# Ladda addons direkt vid import så Blueprints registreras oavsett startmetod
+# Ladda addons och starta scheduler direkt vid import, oavsett startmetod
db.init_db()
discover_and_load(app, socketio)
+sched.init_scheduler(socketio)
KIOSK_USER = os.environ.get('KIOSK_USER', 'admin')
KIOSK_PASS = os.environ.get('KIOSK_PASS', 'changeme')
@@ -314,5 +330,4 @@ if __name__ == '__main__':
_apply_saved_audio()
# Apply display settings in background after sway/Wayland is up
eventlet.spawn(_apply_settings_after_cage)
- sched.init_scheduler(socketio)
socketio.run(app, host='0.0.0.0', port=5000)
diff --git a/kiosk/static/addons/emulator/panel.js b/kiosk/static/addons/emulator/panel.js
new file mode 100644
index 0000000..6cdc398
--- /dev/null
+++ b/kiosk/static/addons/emulator/panel.js
@@ -0,0 +1,203 @@
+/* Emulator admin-panel */
+(function () {
+
+ let _roms = []; // alla hittade ROMs
+ let _selectedRom = null; // { name, path, core, core_label }
+
+ // --- Init ---
+
+ async function emuInit() {
+ const s = await fetch('/api/emulator/status').then(r => r.json());
+ document.getElementById('emu-rom-dir').value = s.rom_dir || '';
+ document.getElementById('emu-ejs-path').value = s.ejs_path || '';
+ if (s.active) {
+ _showNowPlaying(s.rom, s.core_label);
+ }
+ }
+
+ // --- Bläddra efter ROM-mapp ---
+
+ function emuBrowseFolder() {
+ openBrowser(function (path) {
+ // openBrowser väljer fil; ta dess mapp
+ const dir = path.includes('/') ? path.substring(0, path.lastIndexOf('/')) : path;
+ document.getElementById('emu-rom-dir').value = dir;
+ emuScanRoms();
+ });
+ }
+
+ // --- Skanna ROM-mapp ---
+
+ async function emuScanRoms() {
+ const dir = document.getElementById('emu-rom-dir').value.trim();
+ if (!dir) return;
+
+ _setStatus('Skannar...');
+ await fetch('/api/emulator/settings', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ rom_dir: dir }),
+ });
+
+ const roms = await fetch(`/api/emulator/roms?path=${encodeURIComponent(dir)}`).then(r => r.json());
+ _roms = roms;
+ _renderRomList(roms);
+ _setStatus('');
+ }
+
+ // --- Filtrera ROM-lista ---
+
+ function emuFilterRoms(query) {
+ const q = query.toLowerCase();
+ _renderRomList(q ? _roms.filter(r => r.name.toLowerCase().includes(q)) : _roms);
+ }
+
+ // --- Rendera ROM-lista ---
+
+ function _renderRomList(roms) {
+ const wrap = document.getElementById('emu-rom-wrap');
+ const list = document.getElementById('emu-rom-list');
+ const none = document.getElementById('emu-no-roms');
+
+ if (!roms.length) {
+ wrap.style.display = 'none';
+ none.style.display = 'block';
+ return;
+ }
+
+ none.style.display = 'none';
+ wrap.style.display = 'block';
+
+ // Gruppera per system
+ const byCore = {};
+ roms.forEach(r => {
+ (byCore[r.core_label] = byCore[r.core_label] || []).push(r);
+ });
+
+ list.innerHTML = Object.entries(byCore).map(([label, group]) => `
+ <div style="padding:.35rem .6rem;font-size:.72rem;font-weight:700;color:#555;
+ text-transform:uppercase;letter-spacing:.06em;background:#0a0a0a;
+ border-bottom:1px solid #1a1a1a">${label}</div>
+ ${group.map(r => `
+ <div class="emu-rom-item" data-path="${_esc(r.path)}" data-core="${_esc(r.core)}"
+ data-name="${_esc(r.name)}" data-core-label="${_esc(r.core_label)}"
+ onclick="emuSelectRom(this)"
+ style="padding:.45rem .75rem;cursor:pointer;font-size:.88rem;
+ display:flex;align-items:center;justify-content:space-between;
+ border-bottom:1px solid #1a1a1a;transition:background .1s">
+ <span style="font-family:monospace;white-space:nowrap;overflow:hidden;
+ text-overflow:ellipsis;max-width:80%">${_esc(r.name)}</span>
+ <span style="color:#444;font-size:.75rem;flex-shrink:0">${r.size_mb} MB</span>
+ </div>
+ `).join('')}
+ `).join('');
+
+ // Hover-effekt
+ list.querySelectorAll('.emu-rom-item').forEach(el => {
+ el.addEventListener('mouseenter', () => el.style.background = '#1a1a1a');
+ el.addEventListener('mouseleave', () => {
+ el.style.background = _selectedRom && el.dataset.path === _selectedRom.path
+ ? '#1e1b4b' : '';
+ });
+ });
+ }
+
+ // --- Välj ROM ---
+
+ function emuSelectRom(el) {
+ _selectedRom = {
+ path: el.dataset.path,
+ core: el.dataset.core,
+ name: el.dataset.name,
+ core_label: el.dataset.coreLabel,
+ };
+
+ // Markera vald rad
+ document.querySelectorAll('.emu-rom-item').forEach(e => {
+ e.style.background = e === el ? '#1e1b4b' : '';
+ });
+
+ document.getElementById('emu-selected-name').textContent = _selectedRom.name;
+ document.getElementById('emu-selected-core-label').textContent = _selectedRom.core_label;
+ document.getElementById('emu-selected-wrap').style.display = 'block';
+ document.getElementById('emu-launch-btn').disabled = false;
+ _setStatus('');
+ }
+
+ // --- Starta spel ---
+
+ async function emuLaunch() {
+ if (!_selectedRom) return;
+ _setStatus('Startar...');
+ const r = await fetch('/api/emulator/launch', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ rom: _selectedRom.path, core: _selectedRom.core }),
+ });
+ if (r.ok) {
+ _showNowPlaying(_selectedRom.name, _selectedRom.core_label);
+ _setStatus('');
+ } else {
+ _setStatus('Kunde inte starta spelet');
+ }
+ }
+
+ // --- Stoppa ---
+
+ async function emuStop() {
+ await fetch('/api/emulator/stop', { method: 'POST' });
+ document.getElementById('emu-now-playing').style.display = 'none';
+ _setStatus('');
+ }
+
+ // --- Spara avancerade inställningar ---
+
+ async function emuSaveSettings() {
+ const ejs_path = document.getElementById('emu-ejs-path').value.trim();
+ await fetch('/api/emulator/settings', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ejs_path }),
+ });
+ _setStatus('Sparat');
+ setTimeout(() => _setStatus(''), 2000);
+ }
+
+ // --- Helpers ---
+
+ function _showNowPlaying(rom, coreLabel) {
+ document.getElementById('emu-now-playing').style.display = 'flex';
+ document.getElementById('emu-now-playing-text').textContent =
+ `${coreLabel ? coreLabel + ' — ' : ''}${rom}`;
+ }
+
+ function _setStatus(msg) {
+ document.getElementById('emu-status').textContent = msg;
+ }
+
+ function _esc(s) {
+ return String(s)
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(/</g, '<')
+ .replace(/>/g, '>');
+ }
+
+ // --- Exponera till global scope (anropas från panel.html) ---
+
+ window.emuScanRoms = emuScanRoms;
+ window.emuBrowseFolder = emuBrowseFolder;
+ window.emuFilterRoms = emuFilterRoms;
+ window.emuSelectRom = emuSelectRom;
+ window.emuLaunch = emuLaunch;
+ window.emuStop = emuStop;
+ window.emuSaveSettings = emuSaveSettings;
+
+ // Init när DOM är redo
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', emuInit);
+ } else {
+ emuInit();
+ }
+
+})();
diff --git a/kiosk/static/admin.js b/kiosk/static/admin.js
index de0920e..5f54a4f 100644
--- a/kiosk/static/admin.js
+++ b/kiosk/static/admin.js
@@ -236,18 +236,37 @@ function sendReorder(tbody) {
}).then(r => { if (r.ok) laddaJobb(); });
}
-function _jobTypeChanged(formId, wrapId, statusId) {
- const form = document.getElementById(formId);
- const wrap = document.getElementById(wrapId);
- if (!form || !wrap) return;
- const isPlaylist = form.type.value === 'youtube_playlist';
- wrap.style.display = isPlaylist ? 'flex' : 'none';
+function _jobTypeChanged(formId, wrapId, statusId, emuWrapId) {
+ const form = document.getElementById(formId);
+ const wrap = document.getElementById(wrapId);
+ const emuWrap = emuWrapId ? document.getElementById(emuWrapId) : null;
+ if (!form) return;
+
+ const type = form.type.value;
+ const isPlaylist = type === 'youtube_playlist';
+ const isEmulator = type === 'emulator';
+
+ if (wrap) wrap.style.display = isPlaylist ? 'flex' : 'none';
+ if (emuWrap) emuWrap.style.display = isEmulator ? 'flex' : 'none';
+
if (!isPlaylist && statusId) {
const s = document.getElementById(statusId);
if (s) s.textContent = '';
}
}
+// Öppnar ROM-bläddraren och fyller source-fältet i det angivna formuläret
+function openEmuBrowser(formId) {
+ const form = document.getElementById(formId);
+ const nameEl = document.getElementById(
+ formId === 'job-form' ? 'job-emu-name' : 'edit-emu-name'
+ );
+ openBrowser(function (path, _type) {
+ form.source.value = path;
+ if (nameEl) nameEl.textContent = path.split('/').pop();
+ });
+}
+
async function extractJobPlaylist(formId) {
const form = document.getElementById(formId);
const isAdd = formId === 'job-form';
diff --git a/kiosk/static/display.js b/kiosk/static/display.js
index 9ea47de..f282e0b 100644
--- a/kiosk/static/display.js
+++ b/kiosk/static/display.js
@@ -599,7 +599,7 @@ socket.on('switch', (data) => {
document.getElementById('frame').src = window.location.origin + '/chat-display';
showLayer('iframe');
- } else if (data.type === 'url') {
+ } else if (data.type === 'url' || data.type === 'youtube') {
document.getElementById('frame').src = data.source;
showLayer('iframe');
@@ -617,6 +617,11 @@ socket.on('switch', (data) => {
} else if (data.type === 'youtube_playlist') {
_startVideoPlaylist(data.source);
+ } else if (data.type === 'emulator') {
+ document.getElementById('frame').src =
+ '/emulator?rom=' + encodeURIComponent(data.source);
+ showLayer('iframe');
+
} else if (data.type === 'pdf' || data.type === 'game') {
document.getElementById('frame').src = data.source;
showLayer('iframe');
diff --git a/kiosk/templates/addons/emulator/panel.html b/kiosk/templates/addons/emulator/panel.html
new file mode 100644
index 0000000..8204e26
--- /dev/null
+++ b/kiosk/templates/addons/emulator/panel.html
@@ -0,0 +1,75 @@
+ <script src="/static/addons/emulator/panel.js"></script>
+
+ <!-- Sektion: Emulator -->
+ <section>
+ <h2>Emulator</h2>
+ <p style="font-size:.85rem;color:#555;margin-bottom:.75rem">
+ Kör retro-spel direkt i display-fönstret via EmulatorJS (WebAssembly). Stöder SNES, NES, GBA, N64, PS1, Sega m.fl.
+ </p>
+
+ <!-- Spelar nu-banner -->
+ <div id="emu-now-playing" style="display:none;margin-bottom:.75rem;padding:.45rem .85rem;background:rgba(139,92,246,0.12);border:1px solid rgba(139,92,246,0.3);border-radius:7px;align-items:center;gap:.6rem;font-size:.88rem;color:#c4b5fd;">
+ <span>🎮</span>
+ <span id="emu-now-playing-text" style="flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">Spelar nu...</span>
+ <button class="secondary" style="padding:.2rem .6rem;font-size:.8rem" onclick="emuStop()">Avsluta</button>
+ </div>
+
+ <h3>ROM-mapp</h3>
+ <div class="prop-grid">
+ <label>Mapp</label>
+ <div class="row">
+ <input id="emu-rom-dir" type="text" placeholder="/home/mrfox/roms" style="flex:1"
+ onkeydown="if(event.key==='Enter')emuScanRoms()">
+ <button type="button" class="secondary" onclick="emuBrowseFolder()">Bläddra</button>
+ <button type="button" onclick="emuScanRoms()">Skanna</button>
+ </div>
+ </div>
+
+ <!-- ROM-lista -->
+ <div id="emu-rom-wrap" style="display:none;margin:.75rem 0">
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.4rem">
+ <h3 style="margin:0">Hittade spel</h3>
+ <input id="emu-filter" type="text" placeholder="Filtrera..." style="max-width:12rem;font-size:.85rem"
+ oninput="emuFilterRoms(this.value)">
+ </div>
+ <div id="emu-rom-list" style="max-height:280px;overflow-y:auto;background:#111;border-radius:6px;border:1px solid #222;"></div>
+ </div>
+
+ <div id="emu-no-roms" style="display:none;color:#555;font-size:.85rem;font-style:italic;margin:.5rem 0">
+ Inga ROM-filer hittades i mappen.
+ </div>
+
+ <!-- Valt spel -->
+ <div id="emu-selected-wrap" style="display:none;margin:.75rem 0">
+ <h3>Valt spel</h3>
+ <div class="prop-grid">
+ <label>Spel</label>
+ <span id="emu-selected-name" style="font-size:.9rem;color:#e2e8f0;font-family:monospace"></span>
+ <label>System</label>
+ <span id="emu-selected-core-label" style="font-size:.9rem;color:#94a3b8"></span>
+ </div>
+ </div>
+
+ <div class="row" style="margin-top:.75rem">
+ <button id="emu-launch-btn" onclick="emuLaunch()" disabled>Starta spel</button>
+ <button class="secondary" onclick="emuStop()">Avsluta</button>
+ <span id="emu-status" style="color:#aaa;font-size:.85rem;align-self:center"></span>
+ </div>
+
+ <!-- Avancerat -->
+ <details style="margin-top:1.25rem">
+ <summary style="cursor:pointer;color:#555;font-size:.85rem;user-select:none">Avancerat</summary>
+ <div class="prop-grid" style="margin-top:.75rem">
+ <label>EmulatorJS-källa</label>
+ <div>
+ <input id="emu-ejs-path" type="text" style="width:100%;font-size:.85rem;font-family:monospace"
+ placeholder="https://cdn.emulatorjs.org/stable/data/">
+ <div style="font-size:.75rem;color:#444;margin-top:.3rem">
+ CDN (standard) eller lokal sökväg om du hostar EmulatorJS själv, t.ex. <code>/static/emulator/data/</code>
+ </div>
+ </div>
+ <label></label>
+ <button class="secondary" onclick="emuSaveSettings()" style="width:fit-content">Spara</button>
+ </div>
+ </details>
+ </section>
diff --git a/kiosk/templates/admin.html b/kiosk/templates/admin.html
index 7eb40c4..e7c4d9d 100644
--- a/kiosk/templates/admin.html
+++ b/kiosk/templates/admin.html
@@ -71,7 +71,7 @@
<div class="form-grid">
<input name="label" type="text" placeholder="Namn" required>
<input name="duration" type="number" min="0.01" step="any" placeholder="Minuter (ex. 0.5)" required>
- <select name="type" onchange="_jobTypeChanged('job-form','job-extract-wrap','job-extract-status')">
+ <select name="type" onchange="_jobTypeChanged('job-form','job-extract-wrap','job-extract-status','job-emu-wrap')">
<option value="url">URL / HTML</option>
<option value="youtube">YouTube (video)</option>
<option value="youtube_playlist">YouTube-spellista (video)</option>
@@ -79,15 +79,20 @@
<option value="image">Bild</option>
<option value="pdf">PDF / Presentation</option>
<option value="kiosk_chat">Kiosk Livechat</option>
+ <option value="emulator">Emulator (spel)</option>
</select>
<div class="source-with-browse">
<input name="source" type="text" placeholder="URL eller välj fil" required>
<button type="button" class="secondary" onclick="openBrowser(setSchemaCalla)">Bläddra</button>
</div>
- <div id="job-extract-wrap" style="display:none;grid-column:1/-1;display:none;flex-direction:column;gap:.25rem">
+ <div id="job-extract-wrap" style="display:none;grid-column:1/-1;flex-direction:column;gap:.25rem">
<button type="button" class="secondary" onclick="extractJobPlaylist('job-form')">Extrahera spellista</button>
<span id="job-extract-status" style="font-size:.75rem;color:#444"></span>
</div>
+ <div id="job-emu-wrap" style="display:none;grid-column:1/-1;flex-direction:column;gap:.25rem">
+ <button type="button" class="secondary" onclick="openEmuBrowser('job-form')">Välj ROM-fil</button>
+ <span id="job-emu-name" style="font-size:.8rem;color:#94a3b8;font-family:monospace"></span>
+ </div>
<label style="display:flex;align-items:center;gap:.5rem;color:#aaa;font-size:.9rem">
<input type="checkbox" name="show_chat"> Visa livechat (YouTube)
</label>
@@ -108,7 +113,7 @@
<div class="form-grid">
<input name="label" type="text" placeholder="Namn" required>
<input name="duration" type="number" min="0.01" step="any" placeholder="Minuter" required>
- <select name="type" onchange="_jobTypeChanged('edit-form','edit-extract-wrap','edit-extract-status')">
+ <select name="type" onchange="_jobTypeChanged('edit-form','edit-extract-wrap','edit-extract-status','edit-emu-wrap')">
<option value="url">URL / HTML</option>
<option value="youtube">YouTube (video)</option>
<option value="youtube_playlist">YouTube-spellista (video)</option>
@@ -116,12 +121,17 @@
<option value="image">Bild</option>
<option value="pdf">PDF / Presentation</option>
<option value="kiosk_chat">Kiosk Livechat</option>
+ <option value="emulator">Emulator (spel)</option>
</select>
<input name="source" type="text" placeholder="URL eller filväg" required>
<div id="edit-extract-wrap" style="display:none;grid-column:1/-1;flex-direction:column;gap:.25rem">
<button type="button" class="secondary" onclick="extractJobPlaylist('edit-form')">Extrahera spellista</button>
<span id="edit-extract-status" style="font-size:.75rem;color:#444"></span>
</div>
+ <div id="edit-emu-wrap" style="display:none;grid-column:1/-1;flex-direction:column;gap:.25rem">
+ <button type="button" class="secondary" onclick="openEmuBrowser('edit-form')">Välj ROM-fil</button>
+ <span id="edit-emu-name" style="font-size:.8rem;color:#94a3b8;font-family:monospace"></span>
+ </div>
</div>
<div style="margin-top:.6rem"><button type="submit">Spara</button></div>
</form>
diff --git a/kiosk/templates/emulator.html b/kiosk/templates/emulator.html
new file mode 100644
index 0000000..6f5f72e
--- /dev/null
+++ b/kiosk/templates/emulator.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html lang="sv">
+<head>
+ <meta charset="UTF-8">
+ <title>{{ core_label }} — {{ rom_name }}</title>
+ <style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ html, body {
+ width: 100%; height: 100%;
+ background: #000;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ #game {
+ width: 100%;
+ height: 100%;
+ }
+ </style>
+</head>
+<body>
+ <div id="game"></div>
+
+ <script>
+ EJS_player = '#game';
+ EJS_core = '{{ core }}';
+ EJS_gameUrl = '/api/emulator/rom?path={{ rom_path | urlencode }}';
+ EJS_pathtodata = '{{ ejs_path }}';
+ EJS_language = 'sv-SE';
+ EJS_color = '#8b5cf6';
+
+ EJS_Buttons = {
+ playPause: true,
+ restart: true,
+ mute: true,
+ settings: true,
+ fullscreen: true,
+ saveState: true,
+ loadState: true,
+ screenRecord: false,
+ gamepad: true,
+ cheat: false,
+ volume: true,
+ saveSavFiles: true,
+ };
+ </script>
+ <script src="{{ ejs_path }}loader.js"></script>
+</body>
+</html>