mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 15:43:08 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f365170ded |
@@ -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
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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 }> =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Vendored
+8
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user