Fix Volumio startup and online updates
This commit is contained in:
@@ -117,6 +117,16 @@ Volumio-spezifisch:
|
|||||||
/home/volumio/phoenix.log
|
/home/volumio/phoenix.log
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Die Volumio-spezifischen Units, der eingeschränkte Update-Helfer und die
|
||||||
|
zugehörige sudo-Regel werden einmalig als root installiert:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash /home/volumio/Phoenix/scripts/install_volumio_services.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Installer verändert keine Debian-/Analyzer-Units und startet laufende
|
||||||
|
Dienste nicht automatisch neu.
|
||||||
|
|
||||||
Wenn `toggle-ext` genutzt werden soll, sollte es also unter diesem Pfad liegen:
|
Wenn `toggle-ext` genutzt werden soll, sollte es also unter diesem Pfad liegen:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
volumio ALL=(root) NOPASSWD: /usr/bin/systemctl --no-block start phoenix-update-volumio.service
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Apply staged Phoenix update on Volumio
|
||||||
|
After=local-fs.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
TimeoutStartSec=30min
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=idle
|
||||||
|
Environment=PHOENIX_INSTALL_PROFILE=volumio
|
||||||
|
Environment=PHOENIX_UPDATE_DETACHED=1
|
||||||
|
Environment=PHOENIX_ROOT_OVERRIDE=/home/volumio/Phoenix
|
||||||
|
ExecStart=/bin/bash /usr/local/libexec/phoenix/restart_phoenix_services.sh
|
||||||
Executable
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "run this installer as root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
|
PHOENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
SYSTEMD_SOURCE="$PHOENIX_ROOT/etc/systemd/system"
|
||||||
|
SUDOERS_SOURCE="$PHOENIX_ROOT/etc/sudoers.d/phoenix-update-volumio"
|
||||||
|
LIBEXEC_DIR="/usr/local/libexec/phoenix"
|
||||||
|
|
||||||
|
required_files=(
|
||||||
|
"$SYSTEMD_SOURCE/analyzer-kiosk-volumio.service"
|
||||||
|
"$SYSTEMD_SOURCE/analyzer-web-volumio.service"
|
||||||
|
"$SYSTEMD_SOURCE/phoenix-update-volumio.service"
|
||||||
|
"$SUDOERS_SOURCE"
|
||||||
|
"$PHOENIX_ROOT/scripts/restart_phoenix_services.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
for path in "${required_files[@]}"; do
|
||||||
|
if [ ! -f "$path" ]; then
|
||||||
|
echo "required Volumio integration file missing: $path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! command -v visudo >/dev/null 2>&1; then
|
||||||
|
echo "visudo not found; refusing to install sudo policy" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
visudo -cf "$SUDOERS_SOURCE"
|
||||||
|
|
||||||
|
install -d -o root -g root -m 0755 "$LIBEXEC_DIR"
|
||||||
|
install -o root -g root -m 0755 \
|
||||||
|
"$PHOENIX_ROOT/scripts/restart_phoenix_services.sh" \
|
||||||
|
"$LIBEXEC_DIR/restart_phoenix_services.sh"
|
||||||
|
install -o root -g root -m 0644 \
|
||||||
|
"$SYSTEMD_SOURCE/analyzer-kiosk-volumio.service" \
|
||||||
|
/etc/systemd/system/analyzer-kiosk-volumio.service
|
||||||
|
install -o root -g root -m 0644 \
|
||||||
|
"$SYSTEMD_SOURCE/analyzer-web-volumio.service" \
|
||||||
|
/etc/systemd/system/analyzer-web-volumio.service
|
||||||
|
install -o root -g root -m 0644 \
|
||||||
|
"$SYSTEMD_SOURCE/phoenix-update-volumio.service" \
|
||||||
|
/etc/systemd/system/phoenix-update-volumio.service
|
||||||
|
install -o root -g root -m 0440 \
|
||||||
|
"$SUDOERS_SOURCE" \
|
||||||
|
/etc/sudoers.d/phoenix-update-volumio
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable analyzer-web-volumio.service analyzer-kiosk-volumio.service
|
||||||
|
|
||||||
|
echo "Volumio integration installed"
|
||||||
|
echo "Existing services were not restarted"
|
||||||
@@ -3,7 +3,12 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
PHOENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
PHOENIX_ROOT="${PHOENIX_ROOT_OVERRIDE:-}"
|
||||||
|
if [ -z "$PHOENIX_ROOT" ]; then
|
||||||
|
PHOENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
else
|
||||||
|
PHOENIX_ROOT="$(cd "$PHOENIX_ROOT" && pwd -P)"
|
||||||
|
fi
|
||||||
PHOENIX_PARENT="$(cd "$PHOENIX_ROOT/.." && pwd -P)"
|
PHOENIX_PARENT="$(cd "$PHOENIX_ROOT/.." && pwd -P)"
|
||||||
PHOENIX_NAME="$(basename "$PHOENIX_ROOT")"
|
PHOENIX_NAME="$(basename "$PHOENIX_ROOT")"
|
||||||
STAGE_DIR="$PHOENIX_PARENT/${PHOENIX_NAME}.update"
|
STAGE_DIR="$PHOENIX_PARENT/${PHOENIX_NAME}.update"
|
||||||
@@ -14,6 +19,10 @@ LOG_FILE="$PHOENIX_PARENT/${PHOENIX_NAME}.update.log"
|
|||||||
exec >>"$LOG_FILE" 2>&1
|
exec >>"$LOG_FILE" 2>&1
|
||||||
echo "=== $(date): restart_phoenix_services.sh START ==="
|
echo "=== $(date): restart_phoenix_services.sh START ==="
|
||||||
|
|
||||||
|
is_volumio_install() {
|
||||||
|
[ "${PHOENIX_INSTALL_PROFILE:-}" = "volumio" ] || [ "$PHOENIX_ROOT" = "/home/volumio/Phoenix" ]
|
||||||
|
}
|
||||||
|
|
||||||
restart_if_present() {
|
restart_if_present() {
|
||||||
local unit="$1"
|
local unit="$1"
|
||||||
if ! command -v systemctl >/dev/null 2>&1; then
|
if ! command -v systemctl >/dev/null 2>&1; then
|
||||||
@@ -61,6 +70,19 @@ run_systemd_run() {
|
|||||||
/usr/bin/env PHOENIX_UPDATE_DETACHED=1 /bin/bash "$0"
|
/usr/bin/env PHOENIX_UPDATE_DETACHED=1 /bin/bash "$0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start_volumio_update_service() {
|
||||||
|
local unit="phoenix-update-volumio.service"
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
/usr/bin/systemctl --no-block start "$unit"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
if ! command -v sudo >/dev/null 2>&1; then
|
||||||
|
echo "sudo not found; cannot start $unit as non-root" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sudo -n /usr/bin/systemctl --no-block start "$unit"
|
||||||
|
}
|
||||||
|
|
||||||
health_ok() {
|
health_ok() {
|
||||||
if ! command -v curl >/dev/null 2>&1; then
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
return 0
|
return 0
|
||||||
@@ -144,7 +166,56 @@ wait_for_candidate_health() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
build_volumio_candidate() {
|
||||||
|
local phoenix_dir="${1:-$STAGE_DIR}"
|
||||||
|
local cargo_bin=""
|
||||||
|
local cargo_dir=""
|
||||||
|
local candidate
|
||||||
|
|
||||||
|
if ! needs_phoenix_build "$phoenix_dir"; then
|
||||||
|
echo "Volumio candidate already contains a runnable Phoenix binary"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for candidate in /root/.cargo/bin/cargo /home/volumio/.cargo/bin/cargo /usr/local/bin/cargo /usr/bin/cargo; do
|
||||||
|
if [ -x "$candidate" ]; then
|
||||||
|
cargo_bin="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -z "$cargo_bin" ]; then
|
||||||
|
echo "cargo not found; cannot build staged Volumio update" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
cargo_dir="$(dirname "$cargo_bin")"
|
||||||
|
|
||||||
|
echo "building staged Volumio update before stopping the running kiosk"
|
||||||
|
if ! (
|
||||||
|
cd "$phoenix_dir"
|
||||||
|
PATH="$cargo_dir:/root/.cargo/bin:/home/volumio/.cargo/bin:/usr/local/bin:/usr/bin:/bin" \
|
||||||
|
CARGO_TERM_COLOR=never "$cargo_bin" build --release --locked
|
||||||
|
); then
|
||||||
|
if [ -d "$phoenix_dir/target" ]; then
|
||||||
|
chown -R volumio:volumio "$phoenix_dir/target" || true
|
||||||
|
fi
|
||||||
|
echo "staged Volumio build failed; running installation was left untouched" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
chown -R volumio:volumio "$phoenix_dir/target"
|
||||||
|
if [ ! -x "$phoenix_dir/target/release/phoenix" ] || [ ! -s "$phoenix_dir/target/release/phoenix" ]; then
|
||||||
|
echo "staged Volumio build did not produce a runnable Phoenix binary" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "staged Volumio build completed"
|
||||||
|
}
|
||||||
|
|
||||||
start_all_services() {
|
start_all_services() {
|
||||||
|
if is_volumio_install; then
|
||||||
|
restart_if_present analyzer-web-volumio.service
|
||||||
|
clear_browser_asset_caches
|
||||||
|
restart_if_present analyzer-kiosk-volumio.service
|
||||||
|
return
|
||||||
|
fi
|
||||||
restart_if_present phoenix.service
|
restart_if_present phoenix.service
|
||||||
restart_if_present analyzer-web.service
|
restart_if_present analyzer-web.service
|
||||||
restart_if_present analyzer-web-volumio.service
|
restart_if_present analyzer-web-volumio.service
|
||||||
@@ -154,6 +225,11 @@ start_all_services() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop_all_services() {
|
stop_all_services() {
|
||||||
|
if is_volumio_install; then
|
||||||
|
stop_if_present analyzer-kiosk-volumio.service
|
||||||
|
stop_if_present analyzer-web-volumio.service
|
||||||
|
return
|
||||||
|
fi
|
||||||
stop_if_present analyzer-kiosk.service
|
stop_if_present analyzer-kiosk.service
|
||||||
stop_if_present analyzer-kiosk-volumio.service
|
stop_if_present analyzer-kiosk-volumio.service
|
||||||
stop_if_present analyzer-web.service
|
stop_if_present analyzer-web.service
|
||||||
@@ -180,6 +256,16 @@ rollback_to_backup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if [ "${PHOENIX_UPDATE_DETACHED:-0}" != "1" ]; then
|
if [ "${PHOENIX_UPDATE_DETACHED:-0}" != "1" ]; then
|
||||||
|
if is_volumio_install; then
|
||||||
|
echo "handing Volumio update to phoenix-update-volumio.service"
|
||||||
|
if start_volumio_update_service; then
|
||||||
|
echo "Volumio update service accepted"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "failed to start phoenix-update-volumio.service" >&2
|
||||||
|
echo "run scripts/install_volumio_services.sh once as root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
if ! command -v systemd-run >/dev/null 2>&1; then
|
if ! command -v systemd-run >/dev/null 2>&1; then
|
||||||
echo "systemd-run not found; cannot safely detach updater" >&2
|
echo "systemd-run not found; cannot safely detach updater" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -202,6 +288,10 @@ if [ ! -f "$STAGE_DIR/Cargo.toml" ] || [ ! -d "$STAGE_DIR/src" ] || [ ! -d "$STA
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if is_volumio_install; then
|
||||||
|
build_volumio_candidate "$STAGE_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
stop_all_services
|
stop_all_services
|
||||||
|
|
||||||
rm -rf "$BACKUP_DIR"
|
rm -rf "$BACKUP_DIR"
|
||||||
@@ -222,4 +312,11 @@ if ! wait_for_candidate_health "$PHOENIX_ROOT" "$health_attempts" "$health_delay
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if is_volumio_install && [ "$(id -u)" -eq 0 ] && [ -f "$PHOENIX_ROOT/scripts/restart_phoenix_services.sh" ]; then
|
||||||
|
install -o root -g root -m 0755 \
|
||||||
|
"$PHOENIX_ROOT/scripts/restart_phoenix_services.sh" \
|
||||||
|
/usr/local/libexec/phoenix/restart_phoenix_services.sh
|
||||||
|
echo "Volumio privileged update helper refreshed"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "=== $(date): restart_phoenix_services.sh OK ==="
|
echo "=== $(date): restart_phoenix_services.sh OK ==="
|
||||||
|
|||||||
+4
-11
@@ -165,10 +165,6 @@ else
|
|||||||
chmod 700 "$CHROME_USER_DATA_DIR"
|
chmod 700 "$CHROME_USER_DATA_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pkill -9 -x chromium || true
|
|
||||||
pkill -9 -f "chromium.*--kiosk" || true
|
|
||||||
sleep 0.3
|
|
||||||
|
|
||||||
EXTENSIONS=()
|
EXTENSIONS=()
|
||||||
EXT_VK_RESOLVED="$(resolve_extension_dir "$EXT_VK" \
|
EXT_VK_RESOLVED="$(resolve_extension_dir "$EXT_VK" \
|
||||||
"$PHOENIX_DIR/VirtualKeyboard" \
|
"$PHOENIX_DIR/VirtualKeyboard" \
|
||||||
@@ -216,21 +212,18 @@ CHROME_CMD=(
|
|||||||
--disable-features=Translate,TranslateUI
|
--disable-features=Translate,TranslateUI
|
||||||
--memory-pressure-off
|
--memory-pressure-off
|
||||||
--max-active-webgl-contexts=1
|
--max-active-webgl-contexts=1
|
||||||
"$ANALYZER_URL"
|
|
||||||
"$VOLUMIO_URL"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if [ -n "$EXTENSION_ARG" ]; then
|
if [ -n "$EXTENSION_ARG" ]; then
|
||||||
CHROME_CMD+=(--load-extension="$EXTENSION_ARG")
|
CHROME_CMD+=(--load-extension="$EXTENSION_ARG")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Start with the lightweight Analyzer page. The tab-toggle extension creates
|
||||||
|
# exactly one inactive Volumio tab after Chromium is ready.
|
||||||
|
CHROME_CMD+=("$ANALYZER_URL")
|
||||||
|
|
||||||
sudo -u "$KIOSK_USER" "${CHROME_CMD[@]}" >>"$LOG" 2>&1 &
|
sudo -u "$KIOSK_USER" "${CHROME_CMD[@]}" >>"$LOG" 2>&1 &
|
||||||
CHROME_PID=$!
|
CHROME_PID=$!
|
||||||
|
|
||||||
sleep 2
|
|
||||||
if command -v xdotool >/dev/null 2>&1; then
|
|
||||||
xdotool key Ctrl+t type "$VOLUMIO_URL" key Return || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
wait "$CHROME_PID"
|
wait "$CHROME_PID"
|
||||||
echo "=== $(date): session_volumio.sh END ==="
|
echo "=== $(date): session_volumio.sh END ==="
|
||||||
|
|||||||
+38
-9
@@ -1,16 +1,45 @@
|
|||||||
// Aktiviert den bestehenden Tab mit Ziel-URL (ohne Reload). Falls nicht offen, wird er geöffnet.
|
const ANALYZER_URL = 'http://localhost:8088/';
|
||||||
|
const VOLUMIO_URL = 'http://localhost:3000/';
|
||||||
|
|
||||||
|
function matchingTabs(tabs, url) {
|
||||||
|
return tabs.filter(tab => tab.url && tab.url.startsWith(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep one Analyzer and one Volumio tab. Chromium starts on Analyzer, so
|
||||||
|
// Volumio can load in the background without delaying the first visible UI.
|
||||||
|
function ensureDualTabs() {
|
||||||
|
chrome.tabs.query({ currentWindow: true }, tabs => {
|
||||||
|
const analyzerTabs = matchingTabs(tabs, ANALYZER_URL);
|
||||||
|
const volumioTabs = matchingTabs(tabs, VOLUMIO_URL);
|
||||||
|
const duplicates = [...analyzerTabs.slice(1), ...volumioTabs.slice(1)]
|
||||||
|
.map(tab => tab.id)
|
||||||
|
.filter(id => Number.isInteger(id));
|
||||||
|
|
||||||
|
if (duplicates.length > 0) chrome.tabs.remove(duplicates);
|
||||||
|
if (analyzerTabs.length === 0) chrome.tabs.create({ url: ANALYZER_URL, active: true });
|
||||||
|
if (volumioTabs.length === 0) chrome.tabs.create({ url: VOLUMIO_URL, active: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate an existing application tab without reloading it.
|
||||||
function activateOrCreate(url) {
|
function activateOrCreate(url) {
|
||||||
chrome.tabs.query({}, tabs => {
|
chrome.tabs.query({ currentWindow: true }, tabs => {
|
||||||
const t = tabs.find(tt => tt.url && tt.url.startsWith(url));
|
const tab = matchingTabs(tabs, url)[0];
|
||||||
if (t) { chrome.tabs.update(t.id, {active: true}); }
|
if (tab) chrome.tabs.update(tab.id, { active: true });
|
||||||
else { chrome.tabs.create({url}); }
|
else chrome.tabs.create({ url, active: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||||
if (msg && msg.cmd === 'toggle') {
|
if (!msg) return;
|
||||||
if (msg.from === 'volumio') activateOrCreate('http://localhost:8088/');
|
if (msg.cmd === 'bootstrap') {
|
||||||
else activateOrCreate('http://localhost:3000/');
|
ensureDualTabs();
|
||||||
sendResponse({ok:true});
|
sendResponse({ ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.cmd === 'toggle') {
|
||||||
|
if (msg.from === 'volumio') activateOrCreate(ANALYZER_URL);
|
||||||
|
else activateOrCreate(VOLUMIO_URL);
|
||||||
|
sendResponse({ ok: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+40
-1
@@ -1 +1,40 @@
|
|||||||
(function(){try{const e="3000"===location.port,t=document.createElement("button");function o(){chrome.runtime.sendMessage({cmd:"toggle",from:e?"volumio":"analyzer"},(()=>{}))}t.textContent=e?"Analyzer":"Volumio",Object.assign(t.style,{position:"fixed",right:"10px",top:"10px",zIndex:999999,font:"bold 14px ui-monospace,monospace",padding:"6px 10px",background:"#0b0b12",color:"#ddd",border:"1px solid #333",borderRadius:"8px",cursor:"pointer",opacity:"0.85"}),t.onmouseenter=()=>t.style.opacity="1",t.onmouseleave=()=>t.style.opacity="0.85",t.onclick=o,document.body.appendChild(t),window.addEventListener("keydown",(t=>{"t"===t.key.toLowerCase()&&(t.preventDefault(),o())}),{capture:!0})}catch(e){}})();
|
(function () {
|
||||||
|
try {
|
||||||
|
const isVolumio = location.port === '3000';
|
||||||
|
const button = document.createElement('button');
|
||||||
|
const toggle = () => {
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ cmd: 'toggle', from: isVolumio ? 'volumio' : 'analyzer' },
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({ cmd: 'bootstrap' }, () => {});
|
||||||
|
|
||||||
|
button.textContent = isVolumio ? 'Analyzer' : 'Volumio';
|
||||||
|
Object.assign(button.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
right: '10px',
|
||||||
|
top: '10px',
|
||||||
|
zIndex: 999999,
|
||||||
|
font: 'bold 14px ui-monospace,monospace',
|
||||||
|
padding: '6px 10px',
|
||||||
|
background: '#0b0b12',
|
||||||
|
color: '#ddd',
|
||||||
|
border: '1px solid #333',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
opacity: '0.85',
|
||||||
|
});
|
||||||
|
button.onmouseenter = () => { button.style.opacity = '1'; };
|
||||||
|
button.onmouseleave = () => { button.style.opacity = '0.85'; };
|
||||||
|
button.onclick = toggle;
|
||||||
|
document.body.appendChild(button);
|
||||||
|
window.addEventListener('keydown', event => {
|
||||||
|
if (event.key.toLowerCase() === 't') {
|
||||||
|
event.preventDefault();
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
}, { capture: true });
|
||||||
|
} catch (_) {}
|
||||||
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user