mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 15:43:08 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a1124e9e4 | ||
|
|
78d93555e2 | ||
|
|
7e208b49b0 | ||
|
|
6ce0d1267c | ||
|
|
c6c7fcf01c |
@@ -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
@@ -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
@@ -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 · Free · Steam Deck</span>
|
||||
<span class="de">Open Source · Kostenlos · 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 & VODs</span><span class="de">Live-Streams & 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 & Chapters</span><span class="de">Fortsetzen & 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
@@ -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
@@ -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,4 +1,4 @@
|
||||
let hlsCacheEnabled = false
|
||||
let hlsCacheEnabled = true
|
||||
|
||||
export function getHlsCacheEnabled(): boolean {
|
||||
return hlsCacheEnabled
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 ─────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user