5 Commits
Author SHA1 Message Date
Andre 4a1124e9e4 chore: release 0.5.1 2026-07-18 14:02:54 +02:00
Andre 78d93555e2 fix: enable HLS cache by default
Disabling HLS caching makes live playback buffer and VOD seeking slower on Steam Deck. Keeping the performance cache enabled by default while bounding and clearing the cache preserves smooth playback without allowing cached media data to accumulate across sessions.
2026-06-07 19:40:19 +02:00
Andre 7e208b49b0 chore: remove dead mpv/libplacebo build steps from build-flatpak.sh
Both steps became dead code after the mpv → hls.js migration: libplacebo
was an mpv GPU dependency no longer in the manifest, and the SHA256
download step targeted FILL_IN placeholders that no longer exist.
Step numbering updated from 7 to 5 to reflect the actual sequence.
2026-05-17 11:55:41 +02:00
Andre 6ce0d1267c docs: add GitHub Pages landing page 2026-05-17 10:38:46 +02:00
Andre c6c7fcf01c docs: fix Steam Deck installation and add grid artwork hint
Running flatpak run from the terminal does not work in Gaming Mode —
the app must be added as a non-Steam game to be launchable from the
Steam UI. The grid artwork hint helps users avoid a bare, icon-less
library entry.
2026-05-17 09:58:04 +02:00
12 changed files with 1075 additions and 103 deletions
+51 -3
View File
@@ -68,13 +68,37 @@ Electron (Renderer)
## Installation (Steam Deck)
**1. Install the Flatpak**
```bash
flatpak install --user twitch4steamdeck.flatpak
flatpak run tv.twitch4steamdeck.App
```
Finished Flatpak bundles include everything (incl. Streamlink and Twitch integration) — no further setup required.
**2. Add to Steam as a Non-Steam Game**
The app does not appear automatically in Steam's "Add a Non-Steam Game" list. Add it manually:
1. In Steam (Desktop Mode), go to **Games → Add a Non-Steam Game to My Library**
2. Click **Browse** and select `/usr/bin/flatpak` as the executable
3. Set the **Launch Options** to: `run tv.twitch4steamdeck.App`
4. Click **Add Selected Programs**
The app can then be launched from Gaming Mode like any other game.
**3. Optional: Steam Grid Capsule Artwork**
For a polished look in your Steam library, you can set a custom animated capsule image via [SteamGridDB](https://www.steamgriddb.com/grid/99800):
<p align="center">
<a href="https://www.steamgriddb.com/grid/99800">
<img src="screenshots/twitch_artwork-preview.gif" width="300" alt="Twitch4SteamDeck Steam Grid Artwork" />
</a>
</p>
Download the artwork from SteamGridDB and set it as the grid image for the entry in your Steam library.
---
## Build yourself (developers)
@@ -248,12 +272,36 @@ Electron (Renderer)
## Installation (Steam Deck)
**1. Flatpak installieren**
```bash
flatpak install --user twitch4steamdeck.flatpak
flatpak run tv.twitch4steamdeck.App
```
Fertige Flatpak-Bundles enthalten alles (inkl. Streamlink und Twitch-Anbindung) -- keine weitere Einrichtung noetig.
Fertige Flatpak-Bundles enthalten alles (inkl. Streamlink und Twitch-Anbindung) — keine weitere Einrichtung noetig.
**2. In Steam als steamfremdes Spiel hinzufuegen**
Die App erscheint nach der Installation nicht automatisch in der Liste von Steam. Sie muss manuell hinzugefuegt werden:
1. In Steam (Desktop-Modus) auf **Spiele → Steamfremdes Spiel zu meiner Bibliothek hinzufuegen** gehen
2. Auf **Durchsuchen** klicken und `/usr/bin/flatpak` als Programm auswaehlen
3. Als **Startoptionen** eintragen: `run tv.twitch4steamdeck.App`
4. Auf **Ausgewaehlte Programme hinzufuegen** klicken
Die App kann anschliessend im Gaming-Modus wie ein normales Spiel gestartet werden.
**3. Optional: Steam-Grid-Capsule-Artwork**
Fuer eine saubere Darstellung in der Steam-Bibliothek kann ein animiertes Capsule-Bild ueber [SteamGridDB](https://www.steamgriddb.com/grid/99800) gesetzt werden:
<p align="center">
<a href="https://www.steamgriddb.com/grid/99800">
<img src="screenshots/twitch_artwork-preview.gif" width="300" alt="Twitch4SteamDeck Steam-Grid-Artwork" />
</a>
</p>
Das Artwork von SteamGridDB herunterladen und als Grid-Bild fuer den Eintrag in der Steam-Bibliothek setzen.
---
+16 -80
View File
@@ -42,7 +42,7 @@ fi
# Step 1: verify Flatpak dependencies
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo "=== [1/6] Checking Flatpak prerequisites ==="
echo "=== [1/5] Checking Flatpak prerequisites ==="
for cmd in flatpak flatpak-builder node npm python3 rsvg-convert; do
if ! command -v "$cmd" &>/dev/null; then
echo "ERROR: '$cmd' not found."
@@ -56,44 +56,11 @@ for cmd in flatpak flatpak-builder node npm python3 rsvg-convert; do
done
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 2: libplacebo-Tarball erzeugen (enthält glad2-Submodul, GitHub-Tarball reicht nicht)
# Step 2: create the libplacebo tarball (includes the glad2 submodule, GitHub tarball is not enough)
# ─────────────────────────────────────────────────────────────────────────────
LIBPLACEBO_VERSION="6.338.2"
LIBPLACEBO_TARBALL="flatpak/libplacebo-${LIBPLACEBO_VERSION}-full.tar.gz"
LIBPLACEBO_SHA="7f5da41b872302208f0e10c22d3e108ab9fdb1f253eb47c18651698c65d6b426"
echo ""
echo "=== [2/7] libplacebo-Tarball ==="
if [ -f "$LIBPLACEBO_TARBALL" ] && echo "$LIBPLACEBO_SHA $LIBPLACEBO_TARBALL" | sha256sum -c --quiet 2>/dev/null; then
echo " ✓ libplacebo-${LIBPLACEBO_VERSION}-full.tar.gz already exists and is correct"
else
echo " → Cloning libplacebo v${LIBPLACEBO_VERSION} with submodules ..."
TMPDIR_LP=$(mktemp -d)
git clone --recurse-submodules --depth 1 --branch "v${LIBPLACEBO_VERSION}" \
https://github.com/haasn/libplacebo.git "$TMPDIR_LP/libplacebo"
echo " → Creating tarball ..."
tar czf "$LIBPLACEBO_TARBALL" -C "$TMPDIR_LP" libplacebo/
rm -rf "$TMPDIR_LP"
ACTUAL_SHA=$(sha256sum "$LIBPLACEBO_TARBALL" | awk '{print $1}')
if [ "$ACTUAL_SHA" != "$LIBPLACEBO_SHA" ]; then
echo " WARNING: SHA256 differs!"
echo " Expected: $LIBPLACEBO_SHA"
echo " Received: $ACTUAL_SHA"
echo " → Manifest must be updated. Updating now ..."
sed -i "s|sha256: ${LIBPLACEBO_SHA}|sha256: ${ACTUAL_SHA}|" flatpak/tv.twitch4steamdeck.App.yml
echo " ✓ Manifest SHA updated"
else
echo " ✓ SHA256 is correct"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 3: Flatpak-Runtimes prüfen / installieren
# Step 3: verify / install Flatpak runtimes
# Schritt 2: Flatpak-Runtimes prüfen / installieren
# Step 2: verify / install Flatpak runtimes
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo "=== [3/7] Checking Flatpak runtimes ==="
echo "=== [2/5] Checking Flatpak runtimes ==="
RUNTIMES=(
"org.freedesktop.Platform//24.08"
"org.freedesktop.Sdk//24.08"
@@ -110,11 +77,11 @@ for rt in "${RUNTIMES[@]}"; do
done
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 4: .env prüfen
# Step 4: verify .env
# Schritt 3: .env prüfen
# Step 3: verify .env
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo "=== [4/7] Checking .env (Twitch client ID) ==="
echo "=== [3/5] Checking .env (Twitch client ID) ==="
if [ ! -f .env ]; then
echo "ERROR: .env not found."
echo " → Copy .env.example to .env and add MAIN_VITE_TWITCH_CLIENT_ID."
@@ -127,11 +94,11 @@ fi
echo " ✓ .env found"
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 5: npm install (Linux) + better-sqlite3 rebuild + Build
# Step 5: npm install (Linux) + better-sqlite3 rebuild + build
# Schritt 4: npm install (Linux) + better-sqlite3 rebuild + Build
# Step 4: npm install (Linux) + better-sqlite3 rebuild + build
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo "=== [5/7] npm install (Linux binaries) + rebuild native modules ==="
echo "=== [4/5] npm install (Linux binaries) + rebuild native modules ==="
npm install
echo " → Rebuilding better-sqlite3 for Linux x64 ..."
npx @electron/rebuild --module-dir node_modules/better-sqlite3
@@ -152,15 +119,15 @@ if [ -f "$NATIVE_NODE" ]; then
fi
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 5b: Python-Deps für streamlink via flatpak-pip-generator
# Step 5b: generate Python dependencies for streamlink via flatpak-pip-generator
# Schritt 4b: Python-Deps für streamlink via flatpak-pip-generator
# Step 4b: generate Python dependencies for streamlink via flatpak-pip-generator
# Erzeugt flatpak/python3-streamlink.json mit allen pip-Deps + sha256-Hashes.
# Generates flatpak/python3-streamlink.json with all pip dependencies + sha256 hashes.
# Die Datei wird von tv.twitch4steamdeck.App.yml per !include eingebunden.
# The file is included by tv.twitch4steamdeck.App.yml via !include.
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo "=== [5b] Generating Python dependencies for streamlink ==="
echo "=== [4b] Generating Python dependencies for streamlink ==="
if ! python3 -c "import flatpak_pip_generator" &>/dev/null; then
echo " → Installing flatpak-pip-generator ..."
# Ubuntu 24.04: externally-managed-environment → --break-system-packages nötig
@@ -173,42 +140,11 @@ python3 -m flatpak_pip_generator \
echo " ✓ Generated flatpak/python3-streamlink.json ($(wc -l < flatpak/python3-streamlink.json) lines)"
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 6: Manifest-SHA256-Werte füllen (interaktiv)
# Step 6: fill manifest SHA256 values (interactive)
# Schritt 5: flatpak-builder — Build + lokale Installation
# Step 5: flatpak-builder - build + local install
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo "=== [6/7] SHA256 checksums for manifest sources ==="
echo ""
MPV_URL="https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz"
SL_URL="https://github.com/streamlink/streamlink/archive/refs/tags/6.11.0.tar.gz"
MANIFEST="flatpak/tv.twitch4steamdeck.App.yml"
MPV_ARCHIVE="/tmp/mpv-0.41.0.tar.gz"
SL_ARCHIVE="/tmp/streamlink-6.11.0.tar.gz"
echo "Downloading mpv 0.41.0 ..."
curl -L --progress-bar -o "$MPV_ARCHIVE" "$MPV_URL"
MPV_SHA=$(sha256sum "$MPV_ARCHIVE" | awk '{print $1}')
echo " mpv sha256: $MPV_SHA"
echo "Downloading streamlink 6.11.0 ..."
curl -L --progress-bar -o "$SL_ARCHIVE" "$SL_URL"
SL_SHA=$(sha256sum "$SL_ARCHIVE" | awk '{print $1}')
echo " streamlink sha256: $SL_SHA"
# Platzhalter im Manifest ersetzen
# Replace placeholders in the manifest
sed -i "s/FILL_IN_sha256sum_of_mpv_0\.41\.0_tarball/$MPV_SHA/" "$MANIFEST"
sed -i "s/FILL_IN_sha256sum_of_streamlink_tarball/$SL_SHA/" "$MANIFEST"
echo " ✓ Manifest updated"
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 7: flatpak-builder — Build + lokale Installation
# Step 7: flatpak-builder - build + local install
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo "=== [7/7] flatpak-builder ==="
echo "=== [5/5] flatpak-builder ==="
flatpak-builder \
--user \
--install \
+945
View File
@@ -0,0 +1,945 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- SEO -->
<title>Twitch4SteamDeck — The Twitch Client for Steam Deck</title>
<meta name="description" content="The open-source Twitch client for Steam Deck. Watch live streams and VODs with full gamepad navigation in Gaming Mode. Free Flatpak — no mouse needed." />
<meta name="keywords" content="twitch client steam deck, twitch steamdeck, twitch client for steam deck, steam deck twitch app, flatpak twitch steamdeck" />
<link rel="canonical" href="https://Andre-Bzk.github.io/twitch4steamdeck/" />
<!-- Open Graph -->
<meta property="og:title" content="Twitch4SteamDeck — Twitch Client for Steam Deck" />
<meta property="og:description" content="Watch live streams and VODs on your Steam Deck with full gamepad navigation. Open-source Twitch client built for Gaming Mode." />
<meta property="og:image" content="screenshots/player_overlay.jpg" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://Andre-Bzk.github.io/twitch4steamdeck/" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Twitch4SteamDeck — Twitch Client for Steam Deck" />
<meta name="twitter:description" content="Open-source Twitch client for Steam Deck with full gamepad navigation. Free Flatpak." />
<meta name="twitter:image" content="screenshots/player_overlay.jpg" />
<!-- Fonts: Chakra Petch (display) + DM Sans (body) -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;600;700&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&display=swap" rel="stylesheet" />
<style>
/* ===== Reset ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* ===== Design tokens ===== */
:root {
--bg: #0e0e10;
--bg-2: #18181b;
--bg-3: #1f1f23;
--accent: #9147ff;
--accent-dim: rgba(145, 71, 255, 0.12);
--accent-glow: rgba(145, 71, 255, 0.35);
--text: #efeff1;
--text-muted: #adadb8;
--text-dim: #5c5c7a;
--border: rgba(145, 71, 255, 0.18);
--border-sub: rgba(255, 255, 255, 0.06);
--fd: 'Chakra Petch', sans-serif;
--fb: 'DM Sans', sans-serif;
--r: 12px;
--r-lg: 18px;
}
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--fb);
font-size: 16px;
line-height: 1.6;
overflow-x: hidden;
}
/* ===== Language system ===== */
.de { display: none; }
body.lang-de .de { display: revert; }
body.lang-de .en { display: none; }
/* ===== Subtle noise grain ===== */
body::before {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E");
background-size: 200px 200px;
pointer-events: none;
z-index: 999;
}
/* ===== Navigation ===== */
header {
position: fixed;
inset: 0 0 auto;
z-index: 90;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28px;
background: rgba(14, 14, 16, 0.8);
backdrop-filter: blur(24px) saturate(1.4);
border-bottom: 1px solid var(--border-sub);
}
.nav-logo {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--text);
}
.logo-icon {
width: 26px;
height: 26px;
fill: var(--accent);
flex-shrink: 0;
}
.logo-wordmark {
font-family: var(--fd);
font-weight: 700;
font-size: 1rem;
letter-spacing: 0.03em;
}
.logo-wordmark .sep { color: var(--accent); }
.nav-end {
display: flex;
align-items: center;
gap: 10px;
}
.pill-btn {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 14px;
border-radius: 8px;
border: 1px solid var(--border-sub);
background: transparent;
color: var(--text-muted);
font-family: var(--fd);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
text-decoration: none;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
}
.pill-btn:hover { border-color: var(--accent); color: var(--text); }
.pill-btn svg { width: 15px; height: 15px; fill: currentColor; }
/* ===== Hero ===== */
.hero {
min-height: 100svh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100px 24px 72px;
text-align: center;
position: relative;
overflow: hidden;
}
.hero > * { position: relative; z-index: 1; }
/* Large purple radial bloom */
.hero-bloom {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 1000px;
height: 600px;
background: radial-gradient(ellipse 70% 60% at 50% 20%, rgba(145,71,255,0.10) 0%, transparent 100%);
pointer-events: none;
}
/* Decorative horizontal line through bloom */
.hero-line {
position: absolute;
top: 25%;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent 5%, rgba(145,71,255,0.18) 30%, rgba(145,71,255,0.35) 50%, rgba(145,71,255,0.18) 70%, transparent 95%);
pointer-events: none;
}
.hero-eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid var(--border);
border-radius: 100px;
padding: 5px 16px 5px 10px;
font-family: var(--fd);
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
background: var(--accent-dim);
margin-bottom: 30px;
animation: rise 0.7s ease both;
}
.live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 8px var(--accent);
animation: blink 2.4s ease infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
@keyframes rise {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
h1 {
font-family: var(--fd);
font-size: clamp(2.6rem, 6.5vw, 5.2rem);
font-weight: 700;
line-height: 1.06;
letter-spacing: -0.025em;
max-width: 860px;
margin-bottom: 22px;
animation: rise 0.7s ease 0.08s both;
}
h1 em {
font-style: normal;
color: var(--accent);
position: relative;
}
/* Underline glow on accent word */
h1 em::after {
content: '';
position: absolute;
bottom: -4px;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
border-radius: 2px;
}
.hero-sub {
font-size: clamp(1rem, 2vw, 1.15rem);
color: var(--text-muted);
max-width: 580px;
margin-bottom: 42px;
font-weight: 300;
line-height: 1.75;
animation: rise 0.7s ease 0.16s both;
}
.cta-row {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 64px;
animation: rise 0.7s ease 0.24s both;
}
.btn-p {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 13px 28px;
border-radius: var(--r);
background: var(--accent);
color: #fff;
font-family: var(--fd);
font-weight: 600;
font-size: 0.88rem;
letter-spacing: 0.05em;
text-decoration: none;
box-shadow: 0 0 36px rgba(145,71,255,0.28);
transition: background 0.2s, box-shadow 0.2s, transform 0.15s;
}
.btn-p:hover {
background: #a259ff;
box-shadow: 0 0 56px rgba(145,71,255,0.45);
transform: translateY(-2px);
}
.btn-s {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 12px 28px;
border-radius: var(--r);
border: 1px solid var(--border-sub);
background: transparent;
color: var(--text-muted);
font-family: var(--fd);
font-weight: 600;
font-size: 0.88rem;
letter-spacing: 0.05em;
text-decoration: none;
transition: border-color 0.2s, color 0.2s, transform 0.15s;
}
.btn-s:hover {
border-color: var(--accent);
color: var(--text);
transform: translateY(-2px);
}
.btn-p svg, .btn-s svg { width: 17px; height: 17px; fill: currentColor; flex-shrink: 0; }
/* Hero screenshot */
.hero-shot {
width: 100%;
max-width: 1020px;
position: relative;
border-radius: var(--r-lg);
animation: rise 0.9s ease 0.32s both;
}
/* Gradient border ring */
.hero-shot::before {
content: '';
position: absolute;
inset: -1.5px;
border-radius: calc(var(--r-lg) + 1.5px);
background: linear-gradient(145deg, rgba(145,71,255,0.7) 0%, rgba(145,71,255,0.1) 40%, rgba(145,71,255,0.4) 100%);
z-index: -1;
}
.hero-shot img {
display: block;
width: 100%;
height: auto;
border-radius: var(--r-lg);
}
/* Scanlines */
.hero-shot .scanlines {
position: absolute;
inset: 0;
border-radius: var(--r-lg);
background: repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(0,0,0,0.025) 3px, rgba(0,0,0,0.025) 4px);
pointer-events: none;
}
/* Bottom reflection fade */
.hero-shot .fade-bot {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 35%;
border-radius: 0 0 var(--r-lg) var(--r-lg);
background: linear-gradient(to bottom, transparent, rgba(14,14,16,0.55));
pointer-events: none;
}
/* ===== Section wrapper ===== */
.wrap {
max-width: 1080px;
margin: 0 auto;
padding: 88px 28px;
}
.section-label {
font-family: var(--fd);
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 12px;
}
h2 {
font-family: var(--fd);
font-size: clamp(1.9rem, 3.5vw, 2.8rem);
font-weight: 700;
line-height: 1.12;
letter-spacing: -0.015em;
color: var(--text);
margin-bottom: 48px;
}
.rule {
width: 100%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--border-sub), transparent);
max-width: 1080px;
margin: 0 auto;
}
/* ===== Features ===== */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 14px;
}
.feat {
background: var(--bg-2);
border: 1px solid var(--border-sub);
border-radius: var(--r-lg);
padding: 30px 26px;
position: relative;
overflow: hidden;
transition: border-color 0.25s, transform 0.25s, box-shadow 0.25s;
}
.feat::after {
content: '';
position: absolute;
inset: 0;
border-radius: var(--r-lg);
background: radial-gradient(ellipse 80% 50% at 50% 0%, rgba(145,71,255,0.06), transparent);
opacity: 0;
transition: opacity 0.3s;
}
.feat:hover {
border-color: rgba(145,71,255,0.32);
transform: translateY(-4px);
box-shadow: 0 24px 64px rgba(0,0,0,0.45), 0 0 48px rgba(145,71,255,0.07);
}
.feat:hover::after { opacity: 1; }
.feat-icon {
width: 44px;
height: 44px;
background: var(--accent-dim);
border: 1px solid var(--border);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.feat-icon svg { width: 22px; height: 22px; fill: var(--accent); }
.feat h3 {
font-family: var(--fd);
font-size: 0.95rem;
font-weight: 600;
letter-spacing: 0.02em;
margin-bottom: 10px;
}
.feat p {
font-size: 0.88rem;
color: var(--text-muted);
line-height: 1.7;
}
/* ===== Screenshots ===== */
.shots-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
}
.shot {
border-radius: var(--r-lg);
border: 1px solid var(--border-sub);
overflow: hidden;
transition: border-color 0.25s, transform 0.25s, box-shadow 0.25s;
}
.shot:hover {
border-color: rgba(145,71,255,0.4);
transform: translateY(-5px) scale(1.01);
box-shadow: 0 0 48px rgba(145,71,255,0.12), 0 24px 64px rgba(0,0,0,0.5);
}
.shot img { display: block; width: 100%; height: auto; }
.shot-cap {
padding: 13px 16px;
background: var(--bg-2);
font-size: 0.8rem;
color: var(--text-muted);
border-top: 1px solid var(--border-sub);
}
/* ===== Install ===== */
.install-card {
background: var(--bg-2);
border: 1px solid var(--border-sub);
border-radius: var(--r-lg);
padding: 60px 52px;
position: relative;
overflow: hidden;
}
/* Top accent stripe */
.install-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent 5%, var(--accent) 30%, rgba(145,71,255,0.4) 70%, transparent 95%);
}
/* Background bloom */
.install-card::after {
content: '';
position: absolute;
bottom: -80px;
right: -80px;
width: 480px;
height: 480px;
background: radial-gradient(circle, rgba(145,71,255,0.07) 0%, transparent 65%);
pointer-events: none;
}
.install-inner {
position: relative;
z-index: 1;
max-width: 620px;
}
.install-card h2 { margin-bottom: 14px; }
.install-sub {
font-size: 1rem;
color: var(--text-muted);
font-weight: 300;
margin-bottom: 32px;
line-height: 1.7;
}
.install-sub code {
font-family: 'Courier New', monospace;
color: var(--text);
background: var(--bg-3);
padding: 1px 7px;
border-radius: 5px;
font-size: 0.9em;
}
.code-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
background: #090a0d;
border: 1px solid rgba(145,71,255,0.22);
border-radius: var(--r);
padding: 18px 22px;
margin-bottom: 18px;
}
.code-row code {
font-family: 'Courier New', monospace;
font-size: 0.88rem;
color: #bf99ff;
letter-spacing: 0.02em;
white-space: nowrap;
overflow: auto;
}
.copy-btn {
flex-shrink: 0;
padding: 6px 14px;
border-radius: 7px;
border: 1px solid var(--border-sub);
background: var(--bg-3);
color: var(--text-muted);
font-family: var(--fd);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
}
.copy-btn:hover { border-color: var(--accent); color: var(--accent); }
.install-note {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 0.82rem;
color: var(--text-dim);
line-height: 1.6;
}
.install-note svg { width: 14px; height: 14px; fill: currentColor; flex-shrink: 0; margin-top: 2px; }
.install-note a { color: var(--accent); text-decoration: none; }
.install-note a:hover { text-decoration: underline; }
/* ===== Footer ===== */
footer {
max-width: 1080px;
margin: 0 auto;
padding: 32px 28px;
border-top: 1px solid var(--border-sub);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
}
.footer-brand {
font-family: var(--fd);
font-size: 0.88rem;
font-weight: 700;
color: var(--text-dim);
}
.footer-brand .sep { color: var(--accent); }
.footer-meta {
font-size: 0.78rem;
color: var(--text-dim);
margin-left: 4px;
}
.footer-end {
display: flex;
align-items: center;
gap: 10px;
}
.footer-gh {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-dim);
text-decoration: none;
font-size: 0.82rem;
transition: color 0.2s;
}
.footer-gh:hover { color: var(--accent); }
.footer-gh svg { width: 15px; height: 15px; fill: currentColor; }
/* ===== Scroll reveal ===== */
.rv {
opacity: 0;
transform: translateY(22px);
transition: opacity 0.65s ease, transform 0.65s ease;
}
.rv.in { opacity: 1; transform: none; }
/* ===== Responsive ===== */
@media (max-width: 860px) {
.shots-grid { grid-template-columns: 1fr; }
.install-card { padding: 40px 28px; }
}
@media (max-width: 600px) {
header { padding: 0 16px; }
.logo-wordmark { font-size: 0.88rem; }
h1 { font-size: 2.4rem; }
.wrap { padding: 64px 18px; }
.features-grid { grid-template-columns: 1fr; }
footer { flex-direction: column; align-items: flex-start; }
}
</style>
</head>
<body>
<!-- ===== NAV ===== -->
<header>
<a href="#" class="nav-logo" aria-label="Twitch4SteamDeck Home">
<svg class="logo-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z"/>
</svg>
<span class="logo-wordmark">Twitch<span class="sep">4</span>SteamDeck</span>
</a>
<nav class="nav-end">
<button class="pill-btn" id="navLang" onclick="toggleLang()" aria-label="Switch language to German">DE</button>
<a href="https://github.com/Andre-Bzk/twitch4steamdeck" class="pill-btn" target="_blank" rel="noopener noreferrer" aria-label="View on GitHub">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
</nav>
</header>
<main>
<!-- ===== HERO ===== -->
<section class="hero" aria-label="Hero">
<div class="hero-bloom" aria-hidden="true"></div>
<div class="hero-line" aria-hidden="true"></div>
<div class="hero-eyebrow">
<span class="live-dot" aria-hidden="true"></span>
<span class="en">Open Source &nbsp;·&nbsp; Free &nbsp;·&nbsp; Steam Deck</span>
<span class="de">Open Source &nbsp;·&nbsp; Kostenlos &nbsp;·&nbsp; Steam Deck</span>
</div>
<h1>
<span class="en">The Twitch Client<br>for Your <em>Steam Deck</em></span>
<span class="de">Der Twitch-Client<br>für dein <em>Steam Deck</em></span>
</h1>
<p class="hero-sub en">Watch live streams and VODs with full gamepad navigation — built for Steam Deck Gaming Mode. The open-source Twitch client for Steam Deck that works without a mouse or keyboard.</p>
<p class="hero-sub de">Live-Streams und VODs mit vollständiger Gamepad-Navigation — optimiert für den Steam Deck Gaming Mode. Der Open-Source-Twitch-Client für den Steam Deck, der ohne Maus auskommt.</p>
<div class="cta-row">
<a href="https://github.com/Andre-Bzk/twitch4steamdeck/releases" class="btn-p" target="_blank" rel="noopener noreferrer">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7 7-7z"/></svg>
<span class="en">Install via Flatpak</span>
<span class="de">Als Flatpak installieren</span>
</a>
<a href="https://github.com/Andre-Bzk/twitch4steamdeck" class="btn-s" target="_blank" rel="noopener noreferrer">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
<span class="en">View on GitHub</span>
<span class="de">Auf GitHub ansehen</span>
</a>
</div>
<div class="hero-shot">
<img
src="screenshots/player_overlay.jpg"
alt="Twitch4SteamDeck — video player with gamepad control overlay running on Steam Deck in Gaming Mode"
width="1280"
height="800"
loading="eager"
decoding="async"
/>
<div class="scanlines" aria-hidden="true"></div>
<div class="fade-bot" aria-hidden="true"></div>
</div>
</section>
<div class="rule" aria-hidden="true"></div>
<!-- ===== FEATURES ===== -->
<section aria-labelledby="feat-title">
<div class="wrap">
<p class="section-label en">Features</p>
<p class="section-label de">Funktionen</p>
<h2 id="feat-title">
<span class="en">Everything you need.<br>Nothing you don't.</span>
<span class="de">Alles, was du brauchst.<br>Nichts, was du nicht brauchst.</span>
</h2>
<div class="features-grid">
<div class="feat rv">
<div class="feat-icon">
<!-- Gamepad / D-pad icon -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 12c0-2.76-2.24-5-5-5s-5 2.24-5 5 2.24 5 5 5 5-2.24 5-5zm-5 3l-2-2h1.5v-2h1v2H14l-2 2zm0-6l2 2h-1.5v2h-1v-2H9l2-2zm-5 3H5.5v-1H7v-1.5l2 2-2 2V13zm10 0v1.5l-2-2 2-2V13H18v1h-1.5z"/></svg>
</div>
<h3><span class="en">Gamepad Navigation</span><span class="de">Gamepad-Navigation</span></h3>
<p class="en">Full controller support with A/B/X/Y, D-Pad, and shoulder buttons. Every screen is navigable from the couch — no mouse, no keyboard.</p>
<p class="de">Vollständige Controller-Unterstützung mit A/B/X/Y, D-Pad und Schultertasten. Jede Ansicht vom Sofa bedienen — ohne Maus, ohne Tastatur.</p>
</div>
<div class="feat rv">
<div class="feat-icon">
<!-- Play icon -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
</div>
<h3><span class="en">Live Streams &amp; VODs</span><span class="de">Live-Streams &amp; VODs</span></h3>
<p class="en">Watch live streams and past broadcasts from channels you follow. Browse top streams and categories, filter by language.</p>
<p class="de">Live-Streams und vergangene Sendungen gefolgter Kanäle ansehen. Top-Streams und Kategorien durchsuchen, nach Sprache filtern.</p>
</div>
<div class="feat rv">
<div class="feat-icon">
<!-- Resume / clock icon -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm4.24 14.24L11 13V7h1.5v5.25l4.5 2.67-1.26 2.32z"/></svg>
</div>
<h3><span class="en">Resume &amp; Chapters</span><span class="de">Fortsetzen &amp; Kapitel</span></h3>
<p class="en">VODs resume exactly where you left off. Jump to any chapter with one button press. Watch history stored locally on the device.</p>
<p class="de">VODs werden genau dort fortgesetzt, wo du aufgehört hast. Mit einem Knopfdruck zu jedem Kapitel springen. Verlauf lokal gespeichert.</p>
</div>
<div class="feat rv">
<div class="feat-icon">
<!-- Display / screen icon -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9.5 11.5h1v1h-1zm0-3h1v1h-1zm3 3h1v1h-1zm0-3h1v1h-1zm3 3h1v1h-1zm0-3h1v1h-1zM8 14.5H7v-1h1v-1H6.5v-1H8v-1H6v5h2zm1-3.5H8v-1h1z"/></svg>
</div>
<h3><span class="en">Steam Deck Optimized</span><span class="de">Steam Deck Optimiert</span></h3>
<p class="en">Maximized window in Gaming Mode, dark 1280×800 UI, and quality selection up to 1080p60. Delivered as a Flatpak for seamless installation.</p>
<p class="de">Maximiertes Fenster im Gaming Mode, dunkle 1280×800-Oberfläche und Qualitätswahl bis 1080p60. Als Flatpak zur einfachen Installation.</p>
</div>
</div>
</div>
</section>
<div class="rule" aria-hidden="true"></div>
<!-- ===== SCREENSHOTS ===== -->
<section aria-labelledby="shots-title">
<div class="wrap">
<p class="section-label en">Screenshots</p>
<p class="section-label de">Screenshots</p>
<h2 id="shots-title">
<span class="en">See it in action</span>
<span class="de">In Aktion erleben</span>
</h2>
<div class="shots-grid">
<div class="shot rv">
<img src="screenshots/follow.jpg" alt="Following screen showing a grid of followed Twitch channels on the Steam Deck client" width="1280" height="800" loading="lazy" decoding="async" />
<div class="shot-cap">
<span class="en">Your followed channels at a glance</span>
<span class="de">Deine gefolgten Kanäle auf einen Blick</span>
</div>
</div>
<div class="shot rv">
<img src="screenshots/browse.jpg" alt="Browse screen showing top live streams and game categories in the Steam Deck Twitch client" width="1280" height="800" loading="lazy" decoding="async" />
<div class="shot-cap">
<span class="en">Browse top streams and categories</span>
<span class="de">Top-Streams und Kategorien durchsuchen</span>
</div>
</div>
<div class="shot rv">
<img src="screenshots/channelscreen.jpg" alt="Channel page displaying VOD history and channel info in Twitch4SteamDeck" width="1280" height="800" loading="lazy" decoding="async" />
<div class="shot-cap">
<span class="en">Channel page with VOD history</span>
<span class="de">Kanalseite mit VOD-Verlauf</span>
</div>
</div>
</div>
</div>
</section>
<div class="rule" aria-hidden="true"></div>
<!-- ===== INSTALLATION ===== -->
<section aria-labelledby="install-title">
<div class="wrap">
<div class="install-card rv">
<div class="install-inner">
<p class="section-label en">Installation</p>
<p class="section-label de">Installation</p>
<h2 id="install-title">
<span class="en">Install on your Steam Deck<br>in seconds</span>
<span class="de">In Sekunden auf deinem<br>Steam Deck installieren</span>
</h2>
<p class="install-sub en">Download the <code>.flatpak</code> file from the GitHub Releases page, transfer it to your Steam Deck, then run:</p>
<p class="install-sub de">Lade die <code>.flatpak</code>-Datei von der GitHub-Releases-Seite herunter, übertrage sie auf den Steam Deck und führe aus:</p>
<div class="code-row">
<code>flatpak install --user twitch4steamdeck.flatpak</code>
<button class="copy-btn" id="copyBtn" onclick="copyCmd(this)" type="button">
<span class="en">COPY</span>
<span class="de">KOPIEREN</span>
</button>
</div>
<p class="install-note en">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
Download the latest release from the <a href="https://github.com/Andre-Bzk/twitch4steamdeck/releases" target="_blank" rel="noopener noreferrer">GitHub Releases page</a>.
</p>
<p class="install-note de">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
Lade das neueste Release von der <a href="https://github.com/Andre-Bzk/twitch4steamdeck/releases" target="_blank" rel="noopener noreferrer">GitHub-Releases-Seite</a> herunter.
</p>
</div>
</div>
</div>
</section>
</main>
<!-- ===== FOOTER ===== -->
<footer>
<div>
<span class="footer-brand">Twitch<span class="sep">4</span>SteamDeck</span>
<span class="footer-meta en">· MIT License · Open Source</span>
<span class="footer-meta de">· MIT-Lizenz · Open Source</span>
</div>
<div class="footer-end">
<button class="pill-btn" onclick="toggleLang()" id="footerLang" aria-label="Switch language">DE</button>
<a href="https://github.com/Andre-Bzk/twitch4steamdeck" class="footer-gh" target="_blank" rel="noopener noreferrer" aria-label="GitHub repository">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
</div>
</footer>
<script>
// ── Language toggle ──────────────────────────────────────────────
var lang = 'en';
function toggleLang() {
lang = lang === 'en' ? 'de' : 'en';
document.documentElement.lang = lang;
document.body.classList.toggle('lang-de', lang === 'de');
var label = lang === 'en' ? 'DE' : 'EN';
var ariaLabel = lang === 'en' ? 'Switch language to German' : 'Switch language to English';
['navLang', 'footerLang'].forEach(function(id) {
var el = document.getElementById(id);
el.textContent = label;
el.setAttribute('aria-label', ariaLabel);
});
}
// ── Copy command ─────────────────────────────────────────────────
function copyCmd(btn) {
navigator.clipboard.writeText('flatpak install --user twitch4steamdeck.flatpak').then(function() {
var enSpan = btn.querySelector('.en');
var deSpan = btn.querySelector('.de');
var prevEn = enSpan.textContent;
var prevDe = deSpan.textContent;
enSpan.textContent = 'COPIED!';
deSpan.textContent = 'KOPIERT!';
btn.style.borderColor = 'var(--accent)';
btn.style.color = 'var(--accent)';
setTimeout(function() {
enSpan.textContent = prevEn;
deSpan.textContent = prevDe;
btn.style.borderColor = '';
btn.style.color = '';
}, 2200);
});
}
// ── Scroll reveal ─────────────────────────────────────────────────
var io = new IntersectionObserver(function(entries) {
entries.forEach(function(e) {
if (e.isIntersecting) {
e.target.classList.add('in');
io.unobserve(e.target);
}
});
}, { threshold: 0.12 });
document.querySelectorAll('.rv').forEach(function(el) { io.observe(el); });
</script>
</body>
</html>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twitch4steamdeck",
"version": "0.5.0",
"version": "0.5.1",
"description": "Twitch client for Steam Deck (live, VOD, big-screen UI)",
"main": "out/main/index.js",
"author": "twitch4steamdeck",
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

+24 -2
View File
@@ -13,14 +13,23 @@ log.transports.file.level = 'error'
log.transports.console.level = 'debug'
const isDev = !app.isPackaged
const MAX_CACHE_BYTES = 512 * 1024 * 1024
// Allows video.play() without a prior user gesture — play() is called from hls.js callbacks
// that fire asynchronously long after the original click event.
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required')
// Explicit cache size prevents Chromium's dynamic sizing, which causes "Invalid cache (current)
// size" errors when available disk space fluctuates.
app.commandLine.appendSwitch('disk-cache-size', String(MAX_CACHE_BYTES))
app.commandLine.appendSwitch('disk-cache-size', String(500 * 1024 * 1024))
async function clearDefaultSessionCache(reason: 'startup' | 'quit'): Promise<void> {
try {
await session.defaultSession.clearCache()
log.info(`[twitch4steamdeck] Cleared Electron cache on ${reason}.`)
} catch (err) {
log.warn(`[twitch4steamdeck] Failed to clear Electron cache on ${reason}.`, err)
}
}
function createWindow(): BrowserWindow {
const isLinux = process.platform === 'linux'
@@ -105,6 +114,7 @@ function setupTwitchCors(): void {
}
app.whenReady().then(async () => {
await clearDefaultSessionCache('startup')
setupTwitchCors()
const clientId = import.meta.env.MAIN_VITE_TWITCH_CLIENT_ID ?? ''
const auth = new AuthService(clientId)
@@ -119,10 +129,22 @@ app.whenReady().then(async () => {
registerIpcHandlers(auth, helix, playback)
const stopGamepad = startGamepadReader(() => BrowserWindow.getAllWindows()[0] ?? null)
let servicesStopped = false
let quitAfterCacheClear = false
app.on('before-quit', () => {
app.on('before-quit', (event) => {
if (!servicesStopped) {
stopGamepad()
playback.stop()
servicesStopped = true
}
if (quitAfterCacheClear) return
event.preventDefault()
void clearDefaultSessionCache('quit').finally(() => {
quitAfterCacheClear = true
app.quit()
})
})
if (!clientId) {
+1 -1
View File
@@ -1,4 +1,4 @@
let hlsCacheEnabled = false
let hlsCacheEnabled = true
export function getHlsCacheEnabled(): boolean {
return hlsCacheEnabled
+19 -4
View File
@@ -13,6 +13,7 @@ export interface AppSettings {
}
const STORAGE_KEY = 't4sd:settings'
const HLS_CACHE_DEFAULT_MIGRATION_KEY = 't4sd:hls-cache-default-enabled-v1'
const SIDEBAR_MIN = 240
const SIDEBAR_MAX = 400
const SIDEBAR_DEFAULT = 270
@@ -23,23 +24,37 @@ const DEFAULTS: AppSettings = {
streamBadgeMode: 'both',
sidebarWidth: SIDEBAR_DEFAULT,
badgeGap: BADGE_GAP_DEFAULT,
hlsCacheEnabled: false,
hlsCacheEnabled: true,
fileLoggingEnabled: false,
language: 'en'
}
export { SIDEBAR_MIN, SIDEBAR_MAX, SIDEBAR_DEFAULT, BADGE_GAP_MIN, BADGE_GAP_MAX, BADGE_GAP_DEFAULT }
function applyHlsCacheDefaultMigration(settings: AppSettings): AppSettings {
try {
if (localStorage.getItem(HLS_CACHE_DEFAULT_MIGRATION_KEY) === '1') {
return settings
}
const next = { ...settings, hlsCacheEnabled: true }
saveSettings(next)
localStorage.setItem(HLS_CACHE_DEFAULT_MIGRATION_KEY, '1')
return next
} catch {
return { ...settings, hlsCacheEnabled: true }
}
}
function loadSettings(): AppSettings {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return DEFAULTS
if (!raw) return applyHlsCacheDefaultMigration(DEFAULTS)
const parsed = JSON.parse(raw) as Partial<AppSettings>
const modes: StreamBadgeMode[] = ['off', 'language', 'flag', 'both']
const languages: Language[] = ['de', 'en']
const sw = Number(parsed.sidebarWidth)
const bg = Number(parsed.badgeGap)
return {
return applyHlsCacheDefaultMigration({
streamBadgeMode: modes.includes(parsed.streamBadgeMode as StreamBadgeMode)
? (parsed.streamBadgeMode as StreamBadgeMode)
: DEFAULTS.streamBadgeMode,
@@ -50,7 +65,7 @@ function loadSettings(): AppSettings {
language: languages.includes(parsed.language as Language)
? (parsed.language as Language)
: DEFAULTS.language
}
})
} catch {
return DEFAULTS
}
+4 -4
View File
@@ -152,10 +152,10 @@ export const de = {
'settings.badgeGap.title': 'Flaggen-Badge Abstand',
'settings.badgeGap.hint': 'Abstand zwischen Menütext und Flaggen-Badge ({min}–{max} px).',
'settings.storage.title': 'Speicher',
'settings.storage.hint': 'HLS-Segmente (Live & VODs) sind Single-Use — sie werden nach dem Abspielen nie wieder abgerufen. Caching erzeugt auf dem Steam Deck unnötiges Disk-Wachstum und Akku-Belastung. Thumbnails und API-Antworten bleiben immer gecacht. Maximale Cache-Größe: 500 MB.',
'settings.hlsCache.label': 'HLS-Cache (Live & VODs)',
'settings.hlsCache.on': 'Aktiv — Segmente werden gecacht',
'settings.hlsCache.off': 'Inaktiv — kein Disk-Wachstum (empfohlen)',
'settings.storage.hint': 'Thumbnails und API-Antworten bleiben während der App-Sitzung gecacht. Maximale Cache-Größe: 512 MB.',
'settings.hlsCache.label': 'HLS-Cache',
'settings.hlsCache.on': 'Aktiv — empfohlen für störungsfreie Wiedergabe und schnelles Seeking',
'settings.hlsCache.off': 'Inaktiv — kann Puffern und langsameres Seeking verursachen',
'settings.cache.clearing': 'Wird geleert …',
'settings.cache.clear': 'Cache leeren',
'settings.fileLog.label': 'Datei-Logging',
+4 -4
View File
@@ -153,10 +153,10 @@ export const en: Messages = {
'settings.badgeGap.title': 'Flag badge spacing',
'settings.badgeGap.hint': 'Spacing between menu text and the flag badge ({min}–{max} px).',
'settings.storage.title': 'Storage',
'settings.storage.hint': 'HLS segments (live & VODs) are single-use — they are never fetched again after playback. Caching causes unnecessary disk growth and battery drain on the Steam Deck. Thumbnails and API responses are always cached. Maximum cache size: 500 MB.',
'settings.hlsCache.label': 'HLS cache (live & VODs)',
'settings.hlsCache.on': 'Active — segments will be cached',
'settings.hlsCache.off': 'Inactive — no disk growth (recommended)',
'settings.storage.hint': 'Thumbnails and API responses remain cached during the app session. Maximum cache size: 512 MB.',
'settings.hlsCache.label': 'HLS cache',
'settings.hlsCache.on': 'Active — recommended for smooth playback and fast seeking',
'settings.hlsCache.off': 'Inactive — may cause buffering and slower seeking',
'settings.cache.clearing': 'Clearing…',
'settings.cache.clear': 'Clear cache',
'settings.fileLog.label': 'File logging',
+8 -2
View File
@@ -1299,15 +1299,21 @@ body,
}
.settings-option__label {
flex: 1;
align-self: stretch;
display: flex;
align-items: center;
flex: 1 1 auto;
font-weight: 600;
min-width: 0;
}
.settings-option__preview {
flex: 0 1 58%;
font-size: 17px;
color: var(--color-fg-dim);
font-weight: 400;
white-space: nowrap;
line-height: 1.3;
text-align: right;
}
/* ── Settings Slider ─────────────────────────────── */