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
This commit is contained in:
Andre
2026-04-25 16:14:59 +02:00
parent eeddcb9dff
commit f365170ded
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 { defineConfig, externalizeDepsPlugin } from 'electron-vite'
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({
main: {
define: {
'process.env.APP_VERSION': JSON.stringify(appVersion)
},
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
@@ -12,6 +33,9 @@ export default defineConfig({
}
},
preload: {
define: {
'process.env.APP_VERSION': JSON.stringify(appVersion)
},
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
@@ -21,6 +45,9 @@ export default defineConfig({
},
renderer: {
root: resolve(__dirname, 'src/renderer'),
define: {
'process.env.APP_VERSION': JSON.stringify(appVersion)
},
plugins: [react()],
resolve: {
alias: {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twitch4steamdeck",
"version": "0.0.1",
"version": "0.3.2",
"description": "Werbefreier Twitch-Client für das Steam Deck (Live & VOD, Big-Screen-UI)",
"main": "out/main/index.js",
"author": "twitch4steamdeck",
+2
View File
@@ -16,6 +16,7 @@ export const IPC = {
authEvent: 'auth:event',
twitchGetFollowed: 'twitch:get-followed',
twitchGetOwnUser: 'twitch:get-own-user',
twitchGetVideos: 'twitch:get-videos',
twitchGetTopGames: 'twitch:get-top-games',
twitchGetTopStreams: 'twitch:get-top-streams',
@@ -61,6 +62,7 @@ export function registerIpcHandlers(
})
ipcMain.handle(IPC.twitchGetFollowed, () => helix.getFollowedWithLiveStatus())
ipcMain.handle(IPC.twitchGetOwnUser, () => helix.getOwnUserInfo())
ipcMain.handle(
IPC.twitchGetTopGames,
(_e, cursor?: string) => helix.getTopGames(40, cursor)
+11
View File
@@ -5,6 +5,7 @@ import type {
HelixGame,
HelixPaginatedResponse,
HelixStream,
OwnUserInfo,
TopStreamsResult,
HelixUser,
HelixVideo,
@@ -62,6 +63,16 @@ export class HelixClient {
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> {
if (this.cachedUserId) return this.cachedUserId
const user = await this.getOwnUser()
+7
View File
@@ -5,6 +5,13 @@ export interface HelixUser {
profile_image_url: string
}
export interface OwnUserInfo {
id: string
login: string
displayName: string
profileImageUrl: string
}
export interface HelixFollowedChannel {
broadcaster_id: string
broadcaster_login: string
+11 -1
View File
@@ -10,6 +10,7 @@ const IPC = {
authEvent: 'auth:event',
twitchGetFollowed: 'twitch:get-followed',
twitchGetOwnUser: 'twitch:get-own-user',
twitchGetVideos: 'twitch:get-videos',
twitchGetTopGames: 'twitch:get-top-games',
twitchGetTopStreams: 'twitch:get-top-streams',
@@ -62,6 +63,13 @@ export interface FollowedChannelInfo {
language?: string
}
export interface OwnUserInfo {
id: string
login: string
displayName: string
profileImageUrl: string
}
export interface PlaybackEvent {
kind: 'started' | 'stopped' | 'error'
channelLogin?: string
@@ -103,7 +111,7 @@ export interface TopStreamsResult {
}
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: {
quit: (): Promise<void> => ipcRenderer.invoke(IPC.appQuit)
},
@@ -122,6 +130,8 @@ const api = {
twitch: {
getFollowed: (): Promise<FollowedChannelInfo[]> =>
ipcRenderer.invoke(IPC.twitchGetFollowed),
getOwnUser: (): Promise<OwnUserInfo> =>
ipcRenderer.invoke(IPC.twitchGetOwnUser),
getVideos: (broadcasterId: string): Promise<VodInfo[]> =>
ipcRenderer.invoke(IPC.twitchGetVideos, broadcasterId),
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 type { OwnUserInfo } from '../types/t4sd'
interface Props {
hasFocus: boolean
@@ -9,11 +10,40 @@ interface Props {
export default function AccountScreen({ hasFocus, onRequestSidebar, onLogout }: Props): JSX.Element {
const logoutBtnRef = useRef<HTMLButtonElement>(null)
const [user, setUser] = useState<OwnUserInfo | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState('')
useEffect(() => {
if (hasFocus) logoutBtnRef.current?.focus({ preventScroll: true })
}, [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(() => {
if (!hasFocus) return
const onKey = (e: KeyboardEvent): void => {
@@ -31,6 +61,8 @@ export default function AccountScreen({ hasFocus, onRequestSidebar, onLogout }:
onLogout()
}
const showLogin = user && user.login.toLowerCase() !== user.displayName.toLowerCase()
return (
<div className="screen">
<header className="screen__header">
@@ -38,9 +70,25 @@ export default function AccountScreen({ hasFocus, onRequestSidebar, onLogout }:
</header>
<div className="account">
<div className="account__avatar">
{user?.profileImageUrl ? (
<img src={user.profileImageUrl} alt="" draggable={false} />
) : (
<UserIcon />
)}
</div>
<div className="account__identity">
<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>
<button ref={logoutBtnRef} className="btn" onClick={handleLogout}>
Abmelden
+36
View File
@@ -1323,6 +1323,13 @@ body,
align-items: center;
justify-content: center;
margin-bottom: 8px;
overflow: hidden;
}
.account__avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.account__avatar svg {
@@ -1331,12 +1338,33 @@ body,
color: var(--color-fg-dim);
}
.account__identity {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.account__hint {
margin: 0;
font-size: 20px;
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 {
margin: 0;
font-size: 16px;
@@ -1344,6 +1372,14 @@ body,
margin-bottom: 16px;
}
.account__error {
margin: 0;
max-width: 560px;
font-size: 18px;
color: #ff6868;
text-align: center;
}
/* ── Chapter Overlay (Modal) ──────────────────────────── */
.chapter-overlay {
position: fixed;
+8
View File
@@ -30,6 +30,13 @@ export interface FollowedChannelInfo {
language?: string
}
export interface OwnUserInfo {
id: string
login: string
displayName: string
profileImageUrl: string
}
export interface GameInfo {
id: string
name: string
@@ -85,6 +92,7 @@ export interface T4sdApi {
}
twitch: {
getFollowed: () => Promise<FollowedChannelInfo[]>
getOwnUser: () => Promise<OwnUserInfo>
getVideos: (broadcasterId: string) => Promise<VodInfo[]>
getTopGames: (cursor?: string) => Promise<{ games: GameInfo[]; cursor?: string }>
getTopStreams: (options?: { gameId?: string; language?: string; cursor?: string; limit?: number }) => Promise<TopStreamsResult>