'use strict' const qs = s => document.querySelector(s) const qsa = s => [...document.querySelectorAll(s)] const runningCards = new Map() const state = { lang: 'en-us', texts: {}, langs: [], settings: null, wines: [], prefixes: { default: '', prefixes: [] }, games: [], currentGame: null, currentView: 'library' } function t(key, ...args) { let text = state.texts[key] ?? key args.forEach((v, i) => { text = String(text).split(`{${i}}`).join(String(v)) }) return text } const ICONS = { library: () => '', prefixes: () => '', wines: () => '', settings: () => '', plus: () => '', config: () => '', folder: () => '', trash: () => '', terminal: () => '', logs: () => '', star: () => '', download: () => '', chevronDown: () => '' } function applyI18n() { qsa('[data-t]').forEach(el => { el.textContent = t(el.dataset.t) }) qsa('[data-ph]').forEach(el => { el.placeholder = t(el.dataset.ph) }) qsa('custom-select').forEach(el => { if (el.refresh) el.refresh() }) document.title = t('app.title') qs('#loadingstatus').textContent = t('common.loading') } function setButtonIcons() { const map = { 'nav-library': ICONS.library(), 'nav-prefixes': ICONS.prefixes(), 'nav-wines': ICONS.wines(), 'nav-settings': ICONS.settings(), 'btn-import-side': ICONS.plus() } for (const [id, svg] of Object.entries(map)) { const el = document.getElementById(id) const btn = el && el.querySelector('button') if (btn) btn.innerHTML = svg } } function toast(message, kind) { const el = document.createElement('div') el.className = 'toast' + (kind ? ' ' + kind : '') el.textContent = message qs('#toasts').appendChild(el) requestAnimationFrame(() => requestAnimationFrame(() => el.classList.add('show'))) setTimeout(() => { el.classList.add('hide') setTimeout(() => el.remove(), 300) }, 4000) } function showModal(html) { const overlay = qs('#modal-overlay') qs('#modal').innerHTML = html overlay.style.display = 'flex' requestAnimationFrame(() => requestAnimationFrame(() => overlay.classList.add('show'))) return qs('#modal') } function closeModal() { const overlay = qs('#modal-overlay') const modal = qs('#modal') if (overlay.style.display === 'none') return modal.classList.add('closing') overlay.classList.remove('show') setTimeout(() => { overlay.style.display = 'none' modal.classList.remove('closing') modal.innerHTML = '' }, 160) } function confirmModal(message, okLabel) { return new Promise(resolve => { const modal = showModal(`

${t('dialog.confirm')}

${message}

`) modal.querySelector('#modal-cancel').onclick = () => { closeModal() resolve(false) } modal.querySelector('#modal-ok').onclick = () => { closeModal() resolve(true) } }) } class CustomSelect { constructor(host, options, value, onChange) { this.host = host this.options = options this.value = value this.onChange = onChange this.render() if (!this.host.dataset.wired) { this.wire() this.host.dataset.wired = '1' } } render() { const selected = this.options.find(o => o.value === this.value) this.host.classList.add('custom-select') this.host.dataset.value = this.value ?? '' this.host.innerHTML = `
${ICONS.chevronDown()}
` this.host.querySelector('.custom-select-value span').textContent = selected ? selected.label : '' const box = this.host.querySelector('.custom-select-options') for (const opt of this.options) { const btn = document.createElement('button') btn.type = 'button' btn.className = 'custom-select-option' + (opt.value === this.value ? ' selected' : '') btn.textContent = opt.label btn.onclick = e => { e.stopPropagation() this.setValue(opt.value) this.close() } box.appendChild(btn) } this.host.refresh = () => { const sel = this.options.find(o => o.value === this.value) this.host.querySelector('.custom-select-value span').textContent = sel ? sel.label : '' const btns = box.querySelectorAll('.custom-select-option') this.options.forEach((opt, i) => { btns[i].textContent = opt.label btns[i].classList.toggle('selected', opt.value === this.value) }) } } wire() { this.host.onclick = e => { if (e.target.closest('.custom-select-option')) return e.stopPropagation() this.toggle() } document.addEventListener('click', () => this.close()) } toggle() { const wasOpen = this.host.classList.contains('open') CustomSelect.closeAll() if (!wasOpen) this.host.classList.add('open') } close() { this.host.classList.remove('open') } setValue(v) { this.value = v this.host.dataset.value = v ?? '' this.render() if (this.onChange) this.onChange(v) } update(options, value) { this.options = options if (value !== undefined) this.value = value this.host.dataset.value = this.value ?? '' this.render() } static closeAll() { qsa('.custom-select.open').forEach(el => el.classList.remove('open')) } } const selects = {} const NAV_PARENT = { gameconfig: 'library' } function showView(name) { state.currentView = name qsa('.view').forEach(v => v.classList.remove('active')) const view = qs('#view-' + name) if (view) view.classList.add('active') const navName = NAV_PARENT[name] || name qsa('sidebarelement').forEach(el => el.classList.remove('active')) const nav = qs('#nav-' + navName) if (nav) nav.classList.add('active') if (name === 'library') renderLibrary() if (name === 'prefixes') renderPrefixes() if (name === 'wines') renderWines() if (name === 'settings') renderSettings() } async function loadAll() { const [langs, settings, wines, prefixes, games] = await Promise.all([ window.plgl.i18nList(), window.plgl.settingsGet(), window.plgl.winesList(), window.plgl.prefixesList(), window.plgl.gamesList() ]) state.langs = langs state.settings = settings state.wines = wines state.prefixes = prefixes state.games = games state.lang = settings.language || 'en-us' await loadLangTexts(state.lang) } async function loadLangTexts(code) { const lang = await window.plgl.i18nGet(code) if (lang && lang.texts) { state.texts = lang.texts state.lang = code } } function wineLabel(id) { const w = state.wines.find(w => w.id === id) return w ? w.label : (id === 'system' ? t('wines.system') : id) } function iconUrl(id) { return `plgl-icon://${encodeURIComponent(id)}/icon` } function fmtPlaytime(seconds) { const s = Math.round(seconds || 0) if (s < 60) return t('library.playtimeSeconds', s) const h = Math.floor(s / 3600) const m = Math.floor((s % 3600) / 60) if (h > 0) return t('library.playtimeHM', h, m) return t('library.playtimeMin', m) } async function attachCardIcon(card, game) { const p = await window.plgl.gamesIconPath(game.id) if (!p) return const img = document.createElement('img') img.className = 'game-icon' img.alt = '' img.onerror = () => img.remove() img.onload = () => card.prepend(img) img.src = iconUrl(game.id) if (img.complete && img.naturalWidth > 0) card.prepend(img) } function renderLibrary() { const list = qs('#game-list') list.innerHTML = '' runningCards.clear() qs('#library-empty').style.display = state.games.length ? 'none' : 'flex' state.games.forEach((game, idx) => { const card = document.createElement('div') card.className = 'game-card' card.style.animationDelay = `${idx * 0.04}s` const portable = game.mode === 'copy' || game.mode === 'move' const last = game.lastPlayed ? new Date(game.lastPlayed).toLocaleDateString() : t('library.never') card.innerHTML = `

${portable ? t('library.portable') : t('library.external')} ${wineLabel(game.wine)}
${t('library.lastPlayed', last)}
${t('library.playtime', fmtPlaytime(game.playSeconds || 0))}
` card.querySelector('h3').textContent = game.name const playBtn = card.querySelector('[data-act=play]') const killBtn = card.querySelector('[data-act=kill]') const lastMeta = card.querySelectorAll('.meta')[1] const applyRunning = isRunning => { playBtn.style.display = isRunning ? 'none' : '' killBtn.style.display = isRunning ? '' : 'none' } runningCards.set(game.id, { applyRunning, lastMeta, game }) window.plgl.gamesIsRunning(game.id).then(applyRunning) playBtn.onclick = () => playGame(game) killBtn.onclick = async () => { await window.plgl.gamesKill(game.id) applyRunning(false) } card.querySelector('[data-act=config]').onclick = () => openGameConfig(game.id) card.querySelector('[data-act=folder]').onclick = () => window.plgl.shellOpenPath(game.workdir) card.querySelector('[data-act=logs]').onclick = async () => { const logs = await window.plgl.gamesGetLogs(game.id) if (logs.length) window.plgl.shellShowItemInFolder(logs[logs.length - 1]) else toast(t('library.viewLogs')) } card.querySelector('[data-act=remove]').onclick = async () => { const note = game.mode === 'copy' || game.mode === 'move' ? t('library.removeFilesNote') : '' const ok = await confirmModal(t('library.removeConfirm', game.name, note), t('library.remove')) if (!ok) return await window.plgl.gamesRemove(game.id) toast(t('toast.gameRemoved', game.name), 'success') state.games = await window.plgl.gamesList() renderLibrary() } list.appendChild(card) attachCardIcon(card, game) }) } async function playGame(game) { const res = await window.plgl.gamesRun(game.id) if (res.ok) { const entry = runningCards.get(game.id) if (entry) entry.applyRunning(true) toast(t('toast.started', game.name), 'success') } else if (res.error === 'already-running') { const entry = runningCards.get(game.id) if (entry) entry.applyRunning(true) toast(t('library.running'), 'error') } else { toast(t('toast.startFailed', game.name, res.error || ''), 'error') } } async function refreshLibraryData() { state.games = await window.plgl.gamesList() const runningList = await window.plgl.gamesListRunning() const runningSet = new Set(runningList) for (const [id, entry] of runningCards) { entry.applyRunning(runningSet.has(id)) const fresh = state.games.find(g => g.id === id) if (fresh && entry.lastMeta) { const last = fresh.lastPlayed ? new Date(fresh.lastPlayed).toLocaleDateString() : t('library.never') entry.lastMeta.textContent = t('library.lastPlayed', last) } } } function wireRealtime() { window.plgl.onGamesChanged(() => { if (state.currentView === 'library') refreshLibraryData() }) } async function startImportFlow() { const modal = showModal(`

${t('library.import')}

${t('import.question')}

`) modal.querySelector('#modal-cancel').onclick = closeModal modal.querySelectorAll('[data-mode]').forEach(btn => { btn.onclick = async () => { const mode = btn.dataset.mode closeModal() const srcDir = await window.plgl.gamesPickFolder() if (!srcDir) return await doImport(srcDir, mode) } }) } async function doImport(srcDir, mode) { const name = srcDir.split('/').filter(Boolean).pop() const modal = showModal(`

${t('import.progress', name)}

`) let count = 0 window.plgl.onImportProgress(() => { count++ const status = modal.querySelector('#import-status') if (status) status.textContent = t('import.fileDone', count) }) try { const game = await window.plgl.gamesImport(srcDir, mode) closeModal() toast(t('toast.importDone', game.name), 'success') state.games = await window.plgl.gamesList() renderLibrary() await openGameConfig(game.id) } catch (err) { closeModal() toast(t('toast.importFailed') + ': ' + (err.message || err), 'error') } } function renderPrefixes() { const list = qs('#prefix-list') list.innerHTML = '' const defs = state.prefixes if (!defs.prefixes.length) { const empty = document.createElement('p') empty.className = 'subtitle' empty.textContent = t('prefixes.empty') list.appendChild(empty) return } for (const p of defs.prefixes) { const locked = !!p.locked || p.name === 'Main' const row = document.createElement('div') row.className = 'list-row' row.innerHTML = `
${p.name === defs.default ? `${t('prefixes.default')}` : ''}
` row.querySelector('.title').textContent = p.name const defBtn = row.querySelector('[data-act=default]') defBtn.textContent = t('prefixes.makeDefault') defBtn.onclick = async () => { const res = await window.plgl.prefixesSetDefault(p.name) if (res && res.ok !== false) toast(t('toast.prefixDefault', p.name), 'success') state.prefixes = await window.plgl.prefixesList() renderPrefixes() } const delBtn = row.querySelector('[data-act=delete]') delBtn.innerHTML = `${ICONS.trash()}${t('prefixes.delete')}` delBtn.onclick = async () => { const ok = await confirmModal(t('prefixes.deleteConfirm', p.name), t('prefixes.delete')) if (!ok) return const res = await window.plgl.prefixesDelete(p.name) if (res.ok) toast(t('toast.prefixDeleted', p.name), 'success') else if (res.error === 'isMain') toast(t('prefixes.mainLocked'), 'error') else toast(t('toast.prefixDeleteFailed', p.name), 'error') state.prefixes = await window.plgl.prefixesList() renderPrefixes() } list.appendChild(row) } } function renderWines() { const list = qs('#wine-list') list.innerHTML = '' const defaultWine = (state.settings && state.settings.defaultWine) || 'system' if (!state.wines.length) { const empty = document.createElement('p') empty.className = 'subtitle' empty.textContent = t('wines.empty') list.appendChild(empty) } for (const w of state.wines) { const isDefault = w.id === defaultWine const row = document.createElement('div') row.className = 'list-row' const typeBadge = w.type === 'proton' ? t('wines.type.proton') : (w.type === 'system' ? t('wines.type.system') : t('wines.type.wine')) row.innerHTML = `
${isDefault ? `${t('wines.default')}` : ''} ${typeBadge} ${w.version ? `${w.version}` : ''}
${w.id !== 'system' ? `` : ''}
` row.querySelector('.title').textContent = w.label row.querySelector('.sub').textContent = w.path || t('wines.systemHint') row.querySelector('.sub').title = row.querySelector('.sub').textContent const defBtn = row.querySelector('[data-act=default]') defBtn.textContent = t('wines.makeDefault') defBtn.onclick = async () => { const res = await window.plgl.winesSetDefault(w.id) if (res && res.ok) { state.settings = await window.plgl.settingsGet() toast(t('wines.defaultSet', w.label), 'success') } renderWines() } const delBtn = row.querySelector('[data-act=delete]') if (delBtn) { delBtn.innerHTML = `${ICONS.trash()}${t('wines.delete')}` delBtn.onclick = async () => { const ok = await confirmModal(t('wines.deleteConfirm', w.label), t('wines.delete')) if (!ok) return const res = await window.plgl.winesDelete(w.id) if (res.ok) { toast(t('wines.deleted', w.label), 'success') state.wines = await window.plgl.winesList() state.settings = await window.plgl.settingsGet() } else if (res.error === 'isSystem') { toast(t('wines.systemLocked'), 'error') } renderWines() } } list.appendChild(row) } } function renderSettings() { if (!selects['set-language']) { selects['set-language'] = new CustomSelect(qs('#set-language'), [], state.lang) selects['set-wine'] = new CustomSelect(qs('#set-wine'), [], 'system') selects['set-prefix'] = new CustomSelect(qs('#set-prefix'), [], '') } selects['set-language'].update(state.langs.map(l => ({ value: l.code, label: l.nameSelf })), state.lang) selects['set-wine'].update(state.wines.map(w => ({ value: w.id, label: w.label })), state.settings.defaultWine || 'system') selects['set-prefix'].update(state.prefixes.prefixes.map(p => ({ value: p.name, label: p.name })), state.prefixes.default) } async function saveSettings() { state.settings = await window.plgl.settingsSet({ language: selects['set-language'].value, defaultWine: selects['set-wine'].value }) await window.plgl.prefixesSetDefault(selects['set-prefix'].value || state.prefixes.default) state.prefixes = await window.plgl.prefixesList() const lang = selects['set-language'].value if (lang !== state.lang) { await loadLangTexts(lang) applyI18n() } toast(t('settings.saved'), 'success') if (state.currentView === 'settings') renderSettings() } function renderWineOptions(selectId, selected) { const defaultWine = (state.settings && state.settings.defaultWine) || 'system' const defLabel = wineLabel(defaultWine) const opts = [ { value: 'default', label: t('config.wineDefault', defLabel) }, ...state.wines.map(w => ({ value: w.id, label: w.label })) ] const value = !selected || selected === 'default' ? 'default' : selected if (!selects[selectId]) selects[selectId] = new CustomSelect(qs('#' + selectId), opts, value) else selects[selectId].update(opts, value) } function renderPrefixOptions(selectId, selected) { const defs = state.prefixes const opts = [ { value: '', label: t('config.prefixDefault', defs.default) }, ...defs.prefixes.filter(p => p.name !== defs.default).map(p => ({ value: p.name, label: p.name })) ] if (!selects[selectId]) selects[selectId] = new CustomSelect(qs('#' + selectId), opts, selected || '') else selects[selectId].update(opts, selected || '') } async function refreshConfigIcon() { const game = state.currentGame if (!game) return const p = await window.plgl.gamesIconPath(game.id) const img = qs('#gc-icon') const ph = qs('#gc-icon-placeholder') img.onerror = () => { img.style.display = 'none' img.removeAttribute('src') ph.style.display = 'flex' } if (p) { img.onload = () => { img.style.display = '' ph.style.display = 'none' } img.src = iconUrl(game.id) + '?t=' + Date.now() if (img.complete && img.naturalWidth > 0) img.onload() } else { img.style.display = 'none' img.removeAttribute('src') ph.style.display = 'flex' } } async function openGameConfig(id) { const game = state.games.find(g => g.id === id) if (!game) return state.currentGame = JSON.parse(JSON.stringify(game)) showView('gameconfig') qs('#gc-title').textContent = game.name qs('#gc-name').value = game.name refreshConfigIcon() qs('#gc-exe').value = game.exePath qs('#gc-workdir').value = game.workdir qs('#gc-args').value = game.args || '' qs('#gc-env').value = Object.entries(game.env || {}).map(([k, v]) => `${k}=${v}`).join('\n') renderWineOptions('gc-wine', game.wine) renderPrefixOptions('gc-prefix', game.prefix) renderOverrides(game.dllOverrides || []) renderTools() } function renderOverrides(overrides) { const box = qs('#gc-overrides') box.innerHTML = '' for (const o of overrides) { box.appendChild(buildOverrideRow(o)) } } const OVERRIDE_OPTIONS = [ { value: 'native', label: 'native' }, { value: 'builtin', label: 'builtin' }, { value: 'native,builtin', label: 'native,builtin' }, { value: 'builtin,native', label: 'builtin,native' }, { value: '', label: 'disabled' } ] function buildOverrideRow(o) { const row = document.createElement('div') row.className = 'override-row' row.innerHTML = ` ` row.querySelector('.dll').value = o.dll || '' const selectHost = document.createElement('span') new CustomSelect(selectHost, OVERRIDE_OPTIONS, o.setting || 'native') row.insertBefore(selectHost, row.querySelector('[data-act=del]')) row.querySelector('[data-act=del]').onclick = () => row.remove() return row } function addOverrideRow() { qs('#gc-overrides').appendChild(buildOverrideRow({ dll: '', setting: 'native' })) const rows = qsa('#gc-overrides .override-row') rows[rows.length - 1].querySelector('.dll').focus() } function readOverrides() { return qsa('#gc-overrides .override-row').map(row => ({ dll: row.querySelector('.dll').value.trim(), setting: row.querySelector('.custom-select').dataset.value ?? 'native' })).filter(o => o.dll) } async function saveGameConfig() { const game = state.currentGame if (!game) return const env = {} qs('#gc-env').value.split('\n').forEach(line => { const idx = line.indexOf('=') if (idx > 0) env[line.slice(0, idx).trim()] = line.slice(idx + 1).trim() }) const patch = { name: qs('#gc-name').value.trim() || game.name, exePath: qs('#gc-exe').value.trim(), workdir: qs('#gc-workdir').value.trim(), args: qs('#gc-args').value, wine: selects['gc-wine'].value, prefix: selects['gc-prefix'].value, env, dllOverrides: readOverrides() } await window.plgl.gamesUpdate(game.id, patch) state.games = await window.plgl.gamesList() toast(t('config.saved'), 'success') if (state.currentView === 'gameconfig') { state.currentGame = { ...state.currentGame, ...patch } qs('#gc-title').textContent = patch.name } } function renderTools() { const grid = qs('#gc-tools') grid.innerHTML = '' const tools = ['winecfg', 'regedit', 'taskmgr', 'cmd', 'control', 'eject'] for (const tool of tools) { const btn = document.createElement('button') btn.className = 'button small' btn.innerHTML = `${ICONS.terminal()}${tool}` btn.onclick = async () => { const game = state.currentGame const res = await window.plgl.gamesRunTool(game.id, tool, '') if (res.ok) toast(t('toast.started', tool)) else toast(t('toast.startFailed', tool, res.error || ''), 'error') } grid.appendChild(btn) } } async function runCustomTool() { const input = qs('#gc-custom-tool').value.trim() if (!input) return const [tool, ...rest] = input.split(/\s+/) const game = state.currentGame const res = await window.plgl.gamesRunTool(game.id, tool, rest.join(' ')) if (res.ok) toast(t('toast.started', tool)) else toast(t('toast.startFailed', tool, res.error || ''), 'error') } function wireEvents() { qsa('[data-nav]').forEach(btn => { btn.addEventListener('click', () => showView(btn.dataset.nav)) }) qs('#btn-import-side').addEventListener('click', startImportFlow) qs('#btn-import-top').addEventListener('click', startImportFlow) qs('#btn-import-empty').addEventListener('click', startImportFlow) qs('#btn-prefix-create').addEventListener('click', async () => { const input = qs('#prefix-name-input') const name = input.value.trim() if (!name) return const res = await window.plgl.prefixesCreate(name) if (!res.ok) { toast(t(res.error === 'alreadyExists' ? 'prefixes.exists' : 'prefixes.invalidName'), 'error') return } input.value = '' toast(t('toast.prefixCreated', res.name), 'success') state.prefixes = await window.plgl.prefixesList() renderPrefixes() }) qs('#btn-wines-rescan').addEventListener('click', async () => { await window.plgl.winesRefresh() state.wines = await window.plgl.winesList() renderWines() }) qs('#btn-wines-import').addEventListener('click', async () => { const srcDir = await window.plgl.dialogPickFile({ properties: ['openDirectory'], title: t('wines.importPick') }) if (!srcDir) return const modal = showModal(`

${t('wines.importing')}

`) const res = await window.plgl.winesImport(srcDir) closeModal() if (res.ok) { toast(t('wines.imported', res.path), 'success') state.wines = await window.plgl.winesList() } else { toast(t('toast.importFailed') + ': ' + (res.error || ''), 'error') } renderWines() }) qs('#btn-settings-save').addEventListener('click', saveSettings) qs('#btn-open-files').addEventListener('click', () => window.plgl.shellOpenPath(window.plgl.FILES_DIR)) qs('#btn-gc-back').addEventListener('click', () => showView('library')) qs('#btn-gc-save').addEventListener('click', saveGameConfig) qs('#gc-override-add').addEventListener('click', addOverrideRow) qs('#gc-exe-browse').addEventListener('click', async () => { const p = await window.plgl.dialogPickFile({ properties: ['openFile'], defaultPath: qs('#gc-workdir').value || window.plgl.FILES_DIR }) if (p) qs('#gc-exe').value = p }) qs('#gc-workdir-browse').addEventListener('click', async () => { const p = await window.plgl.dialogPickFile({ properties: ['openDirectory'], defaultPath: qs('#gc-exe').value || window.plgl.FILES_DIR }) if (p) qs('#gc-workdir').value = p }) qs('#gc-tool-run').addEventListener('click', runCustomTool) qs('#gc-icon-auto').addEventListener('click', async () => { const res = await window.plgl.gamesAutoIcon(state.currentGame.id) if (res.ok) { toast(t('config.icon.set'), 'success') refreshConfigIcon() } else { toast(t('config.icon.noIcon'), 'error') } }) qs('#gc-icon-browse').addEventListener('click', async () => { const p = await window.plgl.dialogPickFile({ properties: ['openFile'], title: t('config.icon.pick'), filters: [{ name: t('config.icon.pick'), extensions: ['png', 'jpg', 'jpeg', 'svg', 'ico', 'webp', 'gif', 'bmp', 'exe'] }] }) if (!p) return let res if (p.toLowerCase().endsWith('.exe')) { res = await window.plgl.gamesSetIconFile(state.currentGame.id, p) if (res.ok) toast(t('config.icon.set'), 'success') else toast(t('config.icon.noIcon'), 'error') } else { res = await window.plgl.gamesSetIconFile(state.currentGame.id, p) if (res.ok) toast(t('config.icon.set'), 'success') else toast(t('config.icon.noIcon'), 'error') } refreshConfigIcon() }) qs('#gc-icon-reset').addEventListener('click', async () => { await window.plgl.gamesResetIcon(state.currentGame.id) refreshConfigIcon() }) qs('#btn-gc-clear-playtime').addEventListener('click', async () => { const ok = await confirmModal(t('config.clearPlaytimeConfirm'), t('config.clearPlaytime')) if (!ok) return await window.plgl.gamesClearPlaytime(state.currentGame.id) state.games = await window.plgl.gamesList() state.currentGame = state.games.find(g => g.id === state.currentGame.id) toast(t('config.playtimeCleared'), 'success') }) qs('#modal-overlay').addEventListener('click', e => { if (e.target === qs('#modal-overlay')) closeModal() }) document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal() }) } async function boot() { setButtonIcons() await loadAll() applyI18n() wireEvents() wireRealtime() qs('#loading').style.display = 'none' qs('#main').style.display = 'flex' showView('library') } boot()