1 Commits
Author SHA1 Message Date
Andre f365170ded feat: Account-Screen mit Nutzerprofil, App-Version aus package.json
- AccountScreen zeigt jetzt Profilbild, Anzeigename und @Login des eingeloggten Twitch-Nutzers (via neuem IPC-Kanal twitch:get-own-user)
- CSS: account__avatar zeigt Profilbild, neue Klassen für identity, name, login und error
2026-04-25 16:14:59 +02:00
9 changed files with 154 additions and 5 deletions
+27
View File
@@ -1,9 +1,30 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path' import { resolve } from 'node:path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
interface PackageJson {
version?: string
}
function getAppVersion(): string {
try {
const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf8')) as PackageJson
if (pkg.version) return pkg.version
} catch {
// ignore malformed package.json and use final fallback below
}
return '0.0.0-dev'
}
const appVersion = getAppVersion()
export default defineConfig({ export default defineConfig({
main: { main: {
define: {
'process.env.APP_VERSION': JSON.stringify(appVersion)
},
plugins: [externalizeDepsPlugin()], plugins: [externalizeDepsPlugin()],
build: { build: {
rollupOptions: { rollupOptions: {
@@ -12,6 +33,9 @@ export default defineConfig({
} }
}, },
preload: { preload: {
define: {
'process.env.APP_VERSION': JSON.stringify(appVersion)
},
plugins: [externalizeDepsPlugin()], plugins: [externalizeDepsPlugin()],
build: { build: {
rollupOptions: { rollupOptions: {
@@ -21,6 +45,9 @@ export default defineConfig({
}, },
renderer: { renderer: {
root: resolve(__dirname, 'src/renderer'), root: resolve(__dirname, 'src/renderer'),
define: {
'process.env.APP_VERSION': JSON.stringify(appVersion)
},
plugins: [react()], plugins: [react()],
resolve: { resolve: {
alias: { alias: {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "twitch4steamdeck", "name": "twitch4steamdeck",
"version": "0.0.1", "version": "0.3.2",
"description": "Werbefreier Twitch-Client für das Steam Deck (Live & VOD, Big-Screen-UI)", "description": "Werbefreier Twitch-Client für das Steam Deck (Live & VOD, Big-Screen-UI)",
"main": "out/main/index.js", "main": "out/main/index.js",
"author": "twitch4steamdeck", "author": "twitch4steamdeck",
+2
View File
@@ -16,6 +16,7 @@ export const IPC = {
authEvent: 'auth:event', authEvent: 'auth:event',
twitchGetFollowed: 'twitch:get-followed', twitchGetFollowed: 'twitch:get-followed',
twitchGetOwnUser: 'twitch:get-own-user',
twitchGetVideos: 'twitch:get-videos', twitchGetVideos: 'twitch:get-videos',
twitchGetTopGames: 'twitch:get-top-games', twitchGetTopGames: 'twitch:get-top-games',
twitchGetTopStreams: 'twitch:get-top-streams', twitchGetTopStreams: 'twitch:get-top-streams',
@@ -61,6 +62,7 @@ export function registerIpcHandlers(
}) })
ipcMain.handle(IPC.twitchGetFollowed, () => helix.getFollowedWithLiveStatus()) ipcMain.handle(IPC.twitchGetFollowed, () => helix.getFollowedWithLiveStatus())
ipcMain.handle(IPC.twitchGetOwnUser, () => helix.getOwnUserInfo())
ipcMain.handle( ipcMain.handle(
IPC.twitchGetTopGames, IPC.twitchGetTopGames,
(_e, cursor?: string) => helix.getTopGames(40, cursor) (_e, cursor?: string) => helix.getTopGames(40, cursor)
+11
View File
@@ -5,6 +5,7 @@ import type {
HelixGame, HelixGame,
HelixPaginatedResponse, HelixPaginatedResponse,
HelixStream, HelixStream,
OwnUserInfo,
TopStreamsResult, TopStreamsResult,
HelixUser, HelixUser,
HelixVideo, HelixVideo,
@@ -62,6 +63,16 @@ export class HelixClient {
return user return user
} }
async getOwnUserInfo(): Promise<OwnUserInfo> {
const user = await this.getOwnUser()
return {
id: user.id,
login: user.login,
displayName: user.display_name,
profileImageUrl: user.profile_image_url
}
}
private async getOwnUserId(): Promise<string> { private async getOwnUserId(): Promise<string> {
if (this.cachedUserId) return this.cachedUserId if (this.cachedUserId) return this.cachedUserId
const user = await this.getOwnUser() const user = await this.getOwnUser()
+7
View File
@@ -5,6 +5,13 @@ export interface HelixUser {
profile_image_url: string profile_image_url: string
} }
export interface OwnUserInfo {
id: string
login: string
displayName: string
profileImageUrl: string
}
export interface HelixFollowedChannel { export interface HelixFollowedChannel {
broadcaster_id: string broadcaster_id: string
broadcaster_login: string broadcaster_login: string
+11 -1
View File
@@ -10,6 +10,7 @@ const IPC = {
authEvent: 'auth:event', authEvent: 'auth:event',
twitchGetFollowed: 'twitch:get-followed', twitchGetFollowed: 'twitch:get-followed',
twitchGetOwnUser: 'twitch:get-own-user',
twitchGetVideos: 'twitch:get-videos', twitchGetVideos: 'twitch:get-videos',
twitchGetTopGames: 'twitch:get-top-games', twitchGetTopGames: 'twitch:get-top-games',
twitchGetTopStreams: 'twitch:get-top-streams', twitchGetTopStreams: 'twitch:get-top-streams',
@@ -62,6 +63,13 @@ export interface FollowedChannelInfo {
language?: string language?: string
} }
export interface OwnUserInfo {
id: string
login: string
displayName: string
profileImageUrl: string
}
export interface PlaybackEvent { export interface PlaybackEvent {
kind: 'started' | 'stopped' | 'error' kind: 'started' | 'stopped' | 'error'
channelLogin?: string channelLogin?: string
@@ -103,7 +111,7 @@ export interface TopStreamsResult {
} }
const api = { const api = {
appVersion: process.env.npm_package_version ?? '0.0.0', appVersion: process.env.APP_VERSION ?? process.env.npm_package_version ?? '0.0.0-dev',
app: { app: {
quit: (): Promise<void> => ipcRenderer.invoke(IPC.appQuit) quit: (): Promise<void> => ipcRenderer.invoke(IPC.appQuit)
}, },
@@ -122,6 +130,8 @@ const api = {
twitch: { twitch: {
getFollowed: (): Promise<FollowedChannelInfo[]> => getFollowed: (): Promise<FollowedChannelInfo[]> =>
ipcRenderer.invoke(IPC.twitchGetFollowed), ipcRenderer.invoke(IPC.twitchGetFollowed),
getOwnUser: (): Promise<OwnUserInfo> =>
ipcRenderer.invoke(IPC.twitchGetOwnUser),
getVideos: (broadcasterId: string): Promise<VodInfo[]> => getVideos: (broadcasterId: string): Promise<VodInfo[]> =>
ipcRenderer.invoke(IPC.twitchGetVideos, broadcasterId), ipcRenderer.invoke(IPC.twitchGetVideos, broadcasterId),
getTopGames: (cursor?: string): Promise<{ games: GameInfo[]; cursor?: string }> => getTopGames: (cursor?: string): Promise<{ games: GameInfo[]; cursor?: string }> =>
+49 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef, useState } from 'react'
import { UserIcon } from '../components/Icons' import { UserIcon } from '../components/Icons'
import type { OwnUserInfo } from '../types/t4sd'
interface Props { interface Props {
hasFocus: boolean hasFocus: boolean
@@ -9,11 +10,40 @@ interface Props {
export default function AccountScreen({ hasFocus, onRequestSidebar, onLogout }: Props): JSX.Element { export default function AccountScreen({ hasFocus, onRequestSidebar, onLogout }: Props): JSX.Element {
const logoutBtnRef = useRef<HTMLButtonElement>(null) const logoutBtnRef = useRef<HTMLButtonElement>(null)
const [user, setUser] = useState<OwnUserInfo | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState('')
useEffect(() => { useEffect(() => {
if (hasFocus) logoutBtnRef.current?.focus({ preventScroll: true }) if (hasFocus) logoutBtnRef.current?.focus({ preventScroll: true })
}, [hasFocus]) }, [hasFocus])
useEffect(() => {
let cancelled = false
setIsLoading(true)
setError('')
void window.t4sd.twitch
.getOwnUser()
.then((nextUser) => {
if (!cancelled) setUser(nextUser)
})
.catch((err) => {
if (!cancelled) {
setUser(null)
setError(err instanceof Error ? err.message : 'Accountdaten konnten nicht geladen werden.')
}
})
.finally(() => {
if (!cancelled) setIsLoading(false)
})
return () => {
cancelled = true
}
}, [])
useEffect(() => { useEffect(() => {
if (!hasFocus) return if (!hasFocus) return
const onKey = (e: KeyboardEvent): void => { const onKey = (e: KeyboardEvent): void => {
@@ -31,6 +61,8 @@ export default function AccountScreen({ hasFocus, onRequestSidebar, onLogout }:
onLogout() onLogout()
} }
const showLogin = user && user.login.toLowerCase() !== user.displayName.toLowerCase()
return ( return (
<div className="screen"> <div className="screen">
<header className="screen__header"> <header className="screen__header">
@@ -38,9 +70,25 @@ export default function AccountScreen({ hasFocus, onRequestSidebar, onLogout }:
</header> </header>
<div className="account"> <div className="account">
<div className="account__avatar"> <div className="account__avatar">
{user?.profileImageUrl ? (
<img src={user.profileImageUrl} alt="" draggable={false} />
) : (
<UserIcon /> <UserIcon />
)}
</div> </div>
<div className="account__identity">
<p className="account__hint">Eingeloggt bei Twitch</p> <p className="account__hint">Eingeloggt bei Twitch</p>
{isLoading ? (
<p className="account__name">Lade Account…</p>
) : user ? (
<>
<p className="account__name">{user.displayName}</p>
{showLogin && <p className="account__login">@{user.login}</p>}
</>
) : (
<p className="account__error">{error || 'Accountdaten konnten nicht geladen werden.'}</p>
)}
</div>
<p className="account__version">App-Version {window.t4sd.appVersion}</p> <p className="account__version">App-Version {window.t4sd.appVersion}</p>
<button ref={logoutBtnRef} className="btn" onClick={handleLogout}> <button ref={logoutBtnRef} className="btn" onClick={handleLogout}>
Abmelden Abmelden
+36
View File
@@ -1323,6 +1323,13 @@ body,
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 8px; margin-bottom: 8px;
overflow: hidden;
}
.account__avatar img {
width: 100%;
height: 100%;
object-fit: cover;
} }
.account__avatar svg { .account__avatar svg {
@@ -1331,12 +1338,33 @@ body,
color: var(--color-fg-dim); color: var(--color-fg-dim);
} }
.account__identity {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.account__hint { .account__hint {
margin: 0; margin: 0;
font-size: 20px; font-size: 20px;
color: var(--color-fg); color: var(--color-fg);
} }
.account__name {
margin: 0;
font-size: 32px;
line-height: 1.2;
font-weight: 700;
color: var(--color-fg);
}
.account__login {
margin: 0;
font-size: 18px;
color: var(--color-fg-dim);
}
.account__version { .account__version {
margin: 0; margin: 0;
font-size: 16px; font-size: 16px;
@@ -1344,6 +1372,14 @@ body,
margin-bottom: 16px; margin-bottom: 16px;
} }
.account__error {
margin: 0;
max-width: 560px;
font-size: 18px;
color: #ff6868;
text-align: center;
}
/* ── Chapter Overlay (Modal) ──────────────────────────── */ /* ── Chapter Overlay (Modal) ──────────────────────────── */
.chapter-overlay { .chapter-overlay {
position: fixed; position: fixed;
+8
View File
@@ -30,6 +30,13 @@ export interface FollowedChannelInfo {
language?: string language?: string
} }
export interface OwnUserInfo {
id: string
login: string
displayName: string
profileImageUrl: string
}
export interface GameInfo { export interface GameInfo {
id: string id: string
name: string name: string
@@ -85,6 +92,7 @@ export interface T4sdApi {
} }
twitch: { twitch: {
getFollowed: () => Promise<FollowedChannelInfo[]> getFollowed: () => Promise<FollowedChannelInfo[]>
getOwnUser: () => Promise<OwnUserInfo>
getVideos: (broadcasterId: string) => Promise<VodInfo[]> getVideos: (broadcasterId: string) => Promise<VodInfo[]>
getTopGames: (cursor?: string) => Promise<{ games: GameInfo[]; cursor?: string }> getTopGames: (cursor?: string) => Promise<{ games: GameInfo[]; cursor?: string }>
getTopStreams: (options?: { gameId?: string; language?: string; cursor?: string; limit?: number }) => Promise<TopStreamsResult> getTopStreams: (options?: { gameId?: string; language?: string; cursor?: string; limit?: number }) => Promise<TopStreamsResult>