mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 09:33:09 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e6c61b2c7 | ||
|
|
f365170ded |
@@ -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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch4steamdeck",
|
"name": "twitch4steamdeck",
|
||||||
"version": "0.0.1",
|
"version": "0.3.3",
|
||||||
"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",
|
||||||
|
|||||||
+12
-4
@@ -1,4 +1,4 @@
|
|||||||
import { app, BrowserWindow, shell } from 'electron'
|
import { app, BrowserWindow, shell, screen } from 'electron'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { AuthService } from './auth/authService'
|
import { AuthService } from './auth/authService'
|
||||||
import { HelixClient } from './twitch/helixClient'
|
import { HelixClient } from './twitch/helixClient'
|
||||||
@@ -9,9 +9,14 @@ import { startGamepadReader } from './input/gamepadReader'
|
|||||||
const isDev = !app.isPackaged
|
const isDev = !app.isPackaged
|
||||||
|
|
||||||
function createWindow(): BrowserWindow {
|
function createWindow(): BrowserWindow {
|
||||||
|
const isLinux = process.platform === 'linux'
|
||||||
|
const { width, height } = isLinux
|
||||||
|
? screen.getPrimaryDisplay().workAreaSize
|
||||||
|
: { width: 1280, height: 800 }
|
||||||
|
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
width: 1280,
|
width,
|
||||||
height: 800,
|
height,
|
||||||
minWidth: 1024,
|
minWidth: 1024,
|
||||||
minHeight: 640,
|
minHeight: 640,
|
||||||
show: false,
|
show: false,
|
||||||
@@ -25,7 +30,10 @@ function createWindow(): BrowserWindow {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
win.once('ready-to-show', () => win.show())
|
win.once('ready-to-show', () => {
|
||||||
|
if (isLinux) win.maximize()
|
||||||
|
win.show()
|
||||||
|
})
|
||||||
|
|
||||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
shell.openExternal(url)
|
shell.openExternal(url)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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 }> =>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Vendored
+8
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user