feat: Quit-Dialog und Xbox-Button-Prompts ergänzen

This commit is contained in:
Andre
2026-04-25 15:12:45 +02:00
parent e035ec9ac9
commit eeddcb9dff
12 changed files with 433 additions and 19 deletions
+6 -1
View File
@@ -1,4 +1,4 @@
import { BrowserWindow, ipcMain } from 'electron'
import { app, BrowserWindow, ipcMain } from 'electron'
import type { AuthEvent, AuthService } from '../auth/authService'
import type { HelixClient } from '../twitch/helixClient'
import type { PlaybackService } from '../playback/playbackService'
@@ -6,6 +6,7 @@ import type { PlaybackEvent } from '../playback/types'
import { getProgressMap } from '../store/historyRepo'
export const IPC = {
appQuit: 'app:quit',
authStatus: 'auth:get-status',
authStart: 'auth:start-device-flow',
authCancel: 'auth:cancel',
@@ -41,6 +42,10 @@ export function registerIpcHandlers(
helix: HelixClient,
playback: PlaybackService
): void {
ipcMain.handle(IPC.appQuit, () => {
app.quit()
})
ipcMain.handle(IPC.authStatus, () => auth.getStatus())
ipcMain.handle(IPC.authConfigured, () => auth.isConfigured())
ipcMain.handle(IPC.authStart, () => auth.startDeviceFlow())
+4
View File
@@ -1,6 +1,7 @@
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron'
const IPC = {
appQuit: 'app:quit',
authStatus: 'auth:get-status',
authStart: 'auth:start-device-flow',
authCancel: 'auth:cancel',
@@ -103,6 +104,9 @@ export interface TopStreamsResult {
const api = {
appVersion: process.env.npm_package_version ?? '0.0.0',
app: {
quit: (): Promise<void> => ipcRenderer.invoke(IPC.appQuit)
},
auth: {
isConfigured: (): Promise<boolean> => ipcRenderer.invoke(IPC.authConfigured),
getStatus: (): Promise<AuthStatus> => ipcRenderer.invoke(IPC.authStatus),
@@ -0,0 +1,77 @@
import type { ReactNode } from 'react'
export type GamepadPromptKey =
| 'a'
| 'b'
| 'x'
| 'y'
| 'lb'
| 'rb'
| 'lt'
| 'rt'
| 'dpad-up'
| 'dpad-down'
| 'dpad-left'
| 'dpad-right'
interface GamepadPromptProps {
prompt: GamepadPromptKey
}
interface GamepadHintItemProps {
prompt: GamepadPromptKey | readonly GamepadPromptKey[]
children: ReactNode
}
const DPAD_LABELS: Record<Extract<GamepadPromptKey, `dpad-${string}`>, string> = {
'dpad-up': '↑',
'dpad-down': '↓',
'dpad-left': '←',
'dpad-right': '→'
}
const SHOULDER_LABELS: Record<'lb' | 'rb' | 'lt' | 'rt', string> = {
lb: 'LB',
rb: 'RB',
lt: 'LT',
rt: 'RT'
}
export function GamepadPrompt({ prompt }: GamepadPromptProps): JSX.Element {
if (prompt === 'a' || prompt === 'b' || prompt === 'x' || prompt === 'y') {
return (
<span className={`gamepad-prompt gamepad-prompt--face gamepad-prompt--${prompt}`}>
{prompt.toUpperCase()}
</span>
)
}
if (prompt === 'lb' || prompt === 'rb' || prompt === 'lt' || prompt === 'rt') {
return (
<span className={`gamepad-prompt gamepad-prompt--shoulder gamepad-prompt--${prompt}`}>
{SHOULDER_LABELS[prompt]}
</span>
)
}
return (
<span className="gamepad-prompt gamepad-prompt--dpad">
{DPAD_LABELS[prompt]}
</span>
)
}
export function GamepadHintItem({ prompt, children }: GamepadHintItemProps): JSX.Element {
const prompts = Array.isArray(prompt) ? prompt : [prompt]
return (
<span className="gamepad-hint-item">
<span className="gamepad-hint-item__prompts">
{prompts.map((key) => (
<GamepadPrompt key={key} prompt={key} />
))}
</span>
<span>{children}</span>
</span>
)
}
+86 -1
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react'
import { GamepadHintItem } from '../components/GamepadPrompt'
import Sidebar, { SIDEBAR_ITEMS, type TabKey } from '../components/Sidebar'
import AccountScreen from './AccountScreen'
import BrowseScreen from './BrowseScreen'
@@ -10,6 +11,7 @@ import StreamListScreen from './StreamListScreen'
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
type Region = 'sidebar' | 'main'
type QuitChoice = 'yes' | 'no'
interface Props {
onLogout: () => void
@@ -21,6 +23,8 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
const [sidebarIndex, setSidebarIndex] = useState(0)
const [selectedChannel, setSelectedChannel] = useState<FollowedChannelInfo | null>(null)
const [selectedCategory, setSelectedCategory] = useState<GameInfo | null>(null)
const [quitDialogOpen, setQuitDialogOpen] = useState(false)
const [quitDialogChoice, setQuitDialogChoice] = useState<QuitChoice>('no')
const requestSidebar = useCallback(() => {
const idx = SIDEBAR_ITEMS.findIndex((t) => t.key === tab)
@@ -28,6 +32,24 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
setRegion('sidebar')
}, [tab])
const openQuitDialog = useCallback(() => {
setQuitDialogChoice('no')
setQuitDialogOpen(true)
}, [])
const closeQuitDialog = useCallback(() => {
setQuitDialogChoice('no')
setQuitDialogOpen(false)
}, [])
const confirmQuitDialog = useCallback(() => {
if (quitDialogChoice === 'yes') {
void window.t4sd.app.quit()
return
}
closeQuitDialog()
}, [closeQuitDialog, quitDialogChoice])
const handleSelectChannel = useCallback((ch: FollowedChannelInfo) => {
setSelectedChannel(ch)
}, [])
@@ -44,6 +66,31 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
useEffect(() => {
if (region !== 'sidebar') return
const onKey = (e: KeyboardEvent): void => {
if (quitDialogOpen) {
switch (e.key) {
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault()
setQuitDialogChoice('yes')
break
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault()
setQuitDialogChoice('no')
break
case 'Enter':
case ' ':
e.preventDefault()
confirmQuitDialog()
break
case 'Escape':
e.preventDefault()
closeQuitDialog()
break
}
return
}
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
@@ -64,11 +111,15 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
setSelectedCategory(null)
setRegion('main')
break
case 'Escape':
e.preventDefault()
openQuitDialog()
break
}
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [region, sidebarIndex])
}, [region, sidebarIndex, quitDialogOpen, openQuitDialog, closeQuitDialog, confirmQuitDialog])
const mainFocus = region === 'main'
const activeItem = SIDEBAR_ITEMS.find((item) => item.key === tab) ?? SIDEBAR_ITEMS[0]
@@ -133,6 +184,40 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
</>
)}
</div>
{quitDialogOpen && (
<div className="quit-dialog-overlay">
<div className="quit-dialog" role="dialog" aria-modal="true" aria-labelledby="quit-dialog-title">
<h2 id="quit-dialog-title" className="quit-dialog__title">App beenden?</h2>
<p className="quit-dialog__text">Möchtest du Twitch4SteamDeck wirklich schließen?</p>
<p className="quit-dialog__hint gamepad-hint-line">
<GamepadHintItem prompt={['dpad-left', 'dpad-right']}>Auswahl</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="a">Bestätigen</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="b">Abbrechen</GamepadHintItem>
</p>
<div className="quit-dialog__actions">
<button
className={`btn quit-dialog__button${quitDialogChoice === 'yes' ? ' quit-dialog__button--focused' : ''}`}
onClick={() => {
setQuitDialogChoice('yes')
void window.t4sd.app.quit()
}}
tabIndex={-1}
>
Ja
</button>
<button
className={`btn quit-dialog__button${quitDialogChoice === 'no' ? ' quit-dialog__button--focused' : ''}`}
onClick={closeQuitDialog}
tabIndex={-1}
>
Nein
</button>
</div>
</div>
</div>
)}
</div>
)
}
+18 -5
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { GamepadHintItem } from '../components/GamepadPrompt'
import LanguageBadge from '../components/LanguageBadge'
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
@@ -220,7 +221,11 @@ export default function BrowseScreen({
<h2 className="screen__title">Durchsuchen</h2>
{loadState === 'ok' && (
<div className="screen__meta">
<span className="screen__hint">[Y] Aktualisieren · [X] Kanalseite</span>
<span className="screen__hint gamepad-hint-line">
<GamepadHintItem prompt="y">Aktualisieren</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="x">Kanalseite</GamepadHintItem>
</span>
</div>
)}
</header>
@@ -247,8 +252,13 @@ export default function BrowseScreen({
<h3 className="browse-section__title">
Top Live-Streams
{focusRegion === 'shelf' && (
<span className="browse-section__hint">
{' '}· Enter Live · X Kanalseite · ↓ Kategorien
<span className="browse-section__hint gamepad-hint-line">
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="a">Live</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="x">Kanalseite</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="dpad-down">Kategorien</GamepadHintItem>
</span>
)}
</h3>
@@ -287,8 +297,11 @@ export default function BrowseScreen({
<h3 className="browse-section__title">
Kategorien
{focusRegion === 'grid' && (
<span className="browse-section__hint">
{' '}· Enter öffnen · ↑ zurück zu Streams
<span className="browse-section__hint gamepad-hint-line">
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="a">Öffnen</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="dpad-up">Zurück zu Streams</GamepadHintItem>
</span>
)}
</h3>
+7 -2
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { GamepadHintItem } from '../components/GamepadPrompt'
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
import FocusableCard from '../components/FocusableCard'
@@ -128,8 +129,12 @@ export default function CategoryScreen({ game, onSelectChannel, onBack }: Props)
<div className="category-screen__info">
<h2 className="category-screen__title">{game.name}</h2>
{loadState === 'ok' && (
<p className="category-screen__meta">
{streams.length} Streams · [Y] Aktualisieren · [X] Kanalseite
<p className="category-screen__meta gamepad-hint-line">
<span>{streams.length} Streams</span>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="y">Aktualisieren</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="x">Kanalseite</GamepadHintItem>
</p>
)}
</div>
+48 -6
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { GamepadHintItem, GamepadPrompt } from '../components/GamepadPrompt'
import type { FollowedChannelInfo, PlaybackEvent, VodChapter, VodInfo, VodProgress } from '../types/t4sd'
interface Props {
@@ -459,11 +460,29 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
{(playState === 'playing' || playState === 'paused') && (
<>
<span className="channel-screen__playing-hint">
● Wiedergabe{playState === 'paused' ? ' (Pause)' : ''} · A Pause/Resume · ← −30s · → +30s · LT −5min · RT +5min · LB Kapitel zurück · RB Kapitel vor · Y Kapitelmenü
<span className="channel-screen__playing-hint gamepad-hint-line">
<span>● Wiedergabe{playState === 'paused' ? ' (Pause)' : ''}</span>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="a">Pause/Resume</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="dpad-left">−30s</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="dpad-right">+30s</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="lt">−5min</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="rt">+5min</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="lb">Kapitel zurück</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="rb">Kapitel vor</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="y">Kapitelmenü</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="b">Stop</GamepadHintItem>
</span>
<button className="btn" onClick={() => void handleStop()}>
■ Stop (B / Escape)
■ Stop
</button>
</>
)}
@@ -483,7 +502,14 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
<h3 className="channel-screen__vods-title">
Vergangene Streams
{focusRegion === 'shelf' && (
<span className="channel-screen__vods-hint"> · ↑ zurück · A abspielen · Y Kapitel</span>
<span className="channel-screen__vods-hint gamepad-hint-line">
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="dpad-up">Zurück</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="a">Abspielen</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="y">Kapitel</GamepadHintItem>
</span>
)}
</h3>
{vodsLoading && <p className="channel-screen__vods-loading">Lade VODs…</p>}
@@ -560,12 +586,28 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
<span className="chapter-overlay__title">Kapitel wählen</span>
<span className="chapter-overlay__vod-name">{chapterPanelVod.title}</span>
</div>
<p className="chapter-overlay__hint">↑↓ navigieren · A öffnen · B schließen</p>
<p className="chapter-overlay__hint gamepad-hint-line">
<GamepadHintItem prompt={['dpad-up', 'dpad-down']}>Navigieren</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="a">Öffnen</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="b">Schließen</GamepadHintItem>
</p>
{chaptersLoading && <p className="chapter-overlay__msg">Lade Kapitel…</p>}
{!chaptersLoading && chapters.length === 0 && (
<p className="chapter-overlay__msg">
Keine Kapitel gefunden.{' '}
{(playState === 'playing' || playState === 'paused') ? 'A zum Fortsetzen.' : 'A zum Abspielen.'}
{(playState === 'playing' || playState === 'paused') ? (
<span className="gamepad-inline-action">
<GamepadPrompt prompt="a" />
<span>zum Fortsetzen.</span>
</span>
) : (
<span className="gamepad-inline-action">
<GamepadPrompt prompt="a" />
<span>zum Abspielen.</span>
</span>
)}
</p>
)}
{chapters.length > 0 && (
+4 -1
View File
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import FocusableCard from '../components/FocusableCard'
import { GamepadHintItem } from '../components/GamepadPrompt'
import type { FollowedChannelInfo } from '../types/t4sd'
interface Props {
@@ -117,7 +118,9 @@ export default function FollowingScreen({
<div className="screen__meta">
{liveCount > 0 && <span className="screen__live-count">● {liveCount} live</span>}
<span>{channels.length} Kanäle</span>
<span className="screen__hint">[Y] Aktualisieren</span>
<span className="screen__hint gamepad-hint-line">
<GamepadHintItem prompt="y">Aktualisieren</GamepadHintItem>
</span>
</div>
)}
</header>
+19 -2
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { GamepadHintItem, GamepadPrompt } from '../components/GamepadPrompt'
import { SettingsIcon } from '../components/Icons'
import {
type StreamBadgeMode,
@@ -164,7 +165,15 @@ export default function SettingsScreen({ hasFocus, onRequestSidebar }: Props): J
<h3 className="settings-section__title">Sidebar-Breite</h3>
<p className="settings-section__hint">
Breite der Seitenleiste anpassen ({SIDEBAR_MIN}–{SIDEBAR_MAX} px).
Mit Links/Rechts verschieben, Enter setzt auf Standard ({SIDEBAR_DEFAULT} px) zurück.
{' '}
<span className="gamepad-hint-line">
<GamepadHintItem prompt={['dpad-left', 'dpad-right']}>verschieben</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<span className="gamepad-inline-action">
<GamepadPrompt prompt="a" />
<span>setzt auf Standard ({SIDEBAR_DEFAULT} px) zurück.</span>
</span>
</span>
</p>
<div
@@ -193,7 +202,15 @@ export default function SettingsScreen({ hasFocus, onRequestSidebar }: Props): J
<h3 className="settings-section__title">Flaggen-Badge Abstand</h3>
<p className="settings-section__hint">
Abstand zwischen Menütext und Flaggen-Badge ({BADGE_GAP_MIN}–{BADGE_GAP_MAX} px).
Mit Links/Rechts verschieben, Enter setzt auf Standard ({BADGE_GAP_DEFAULT} px) zurück.
{' '}
<span className="gamepad-hint-line">
<GamepadHintItem prompt={['dpad-left', 'dpad-right']}>verschieben</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<span className="gamepad-inline-action">
<GamepadPrompt prompt="a" />
<span>setzt auf Standard ({BADGE_GAP_DEFAULT} px) zurück.</span>
</span>
</span>
</p>
<div
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import FocusableCard from '../components/FocusableCard'
import { GamepadHintItem } from '../components/GamepadPrompt'
import type { FollowedChannelInfo } from '../types/t4sd'
interface Props {
@@ -161,7 +162,11 @@ export default function StreamListScreen({
{loadState === 'ok' && streams.length > 0 && (
<div className="screen__meta">
<span>{streams.length} Streams</span>
<span className="screen__hint">[Y] Aktualisieren · [X] Kanalseite</span>
<span className="screen__hint gamepad-hint-line">
<GamepadHintItem prompt="y">Aktualisieren</GamepadHintItem>
<span className="gamepad-hint-separator">·</span>
<GamepadHintItem prompt="x">Kanalseite</GamepadHintItem>
</span>
</div>
)}
</header>
+155
View File
@@ -308,6 +308,96 @@ body,
font-size: 15px;
}
.gamepad-hint-line {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.gamepad-hint-separator {
color: var(--color-fg-dim);
opacity: 0.65;
}
.gamepad-hint-item {
display: inline-flex;
align-items: center;
gap: 8px;
}
.gamepad-hint-item__prompts {
display: inline-flex;
align-items: center;
gap: 4px;
}
.gamepad-inline-action {
display: inline-flex;
align-items: center;
gap: 8px;
}
.gamepad-prompt {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
min-width: 1.4em;
height: 1.4em;
padding: 0 0.45em;
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 0.72em;
font-weight: 800;
line-height: 1;
letter-spacing: 0.02em;
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.08), 0 2px 6px rgba(0, 0, 0, 0.28);
}
.gamepad-prompt--face {
width: 1.4em;
padding: 0;
border-radius: 999px;
}
.gamepad-prompt--a {
background: linear-gradient(180deg, #2fc95e 0%, #1f9d45 100%);
border-color: rgba(166, 255, 195, 0.4);
}
.gamepad-prompt--b {
background: linear-gradient(180deg, #ff6a5f 0%, #d94339 100%);
border-color: rgba(255, 194, 189, 0.4);
}
.gamepad-prompt--x {
background: linear-gradient(180deg, #4da7ff 0%, #2f74d8 100%);
border-color: rgba(192, 224, 255, 0.4);
}
.gamepad-prompt--y {
background: linear-gradient(180deg, #f8d14d 0%, #d6aa1f 100%);
border-color: rgba(255, 241, 189, 0.5);
color: #1b1b1d;
}
.gamepad-prompt--shoulder {
border-radius: 999px;
background: linear-gradient(180deg, #2f3138 0%, #23252b 100%);
border-color: rgba(255, 255, 255, 0.18);
min-width: 2.4em;
}
.gamepad-prompt--dpad {
border-radius: 10px;
background: linear-gradient(180deg, #343741 0%, #24262d 100%);
border-color: rgba(255, 255, 255, 0.18);
width: 1.45em;
padding: 0;
font-size: 0.8em;
}
.screen__state {
flex: 1;
display: flex;
@@ -1350,3 +1440,68 @@ body,
font-variant-numeric: tabular-nums;
flex-shrink: 0;
}
/* ── Quit Dialog ─────────────────────────────────── */
.quit-dialog-overlay {
position: fixed;
inset: 0;
z-index: 250;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.62);
backdrop-filter: blur(3px);
}
.quit-dialog {
width: 520px;
max-width: calc(100vw - 48px);
padding: 32px 36px;
border-radius: var(--radius);
border: 1px solid rgba(255, 255, 255, 0.12);
background: linear-gradient(180deg, rgba(31, 31, 35, 0.98) 0%, rgba(24, 24, 27, 0.98) 100%);
box-shadow: 0 28px 72px rgba(0, 0, 0, 0.72);
display: flex;
flex-direction: column;
gap: 14px;
}
.quit-dialog__title {
margin: 0;
font-size: 34px;
font-weight: 700;
}
.quit-dialog__text {
margin: 0;
color: var(--color-fg-dim);
font-size: 19px;
line-height: 1.5;
}
.quit-dialog__hint {
margin: 0;
color: var(--color-fg-dim);
font-size: 16px;
}
.quit-dialog__actions {
display: flex;
gap: 14px;
margin-top: 8px;
}
.quit-dialog__button {
flex: 1;
min-width: 0;
background: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.08);
transition: transform 0.12s, background 0.12s, border-color 0.12s, box-shadow 0.12s;
}
.quit-dialog__button--focused {
background: rgba(145, 71, 255, 0.22);
border-color: var(--color-focus);
box-shadow: 0 0 0 3px rgba(191, 148, 255, 0.22), 0 14px 28px rgba(0, 0, 0, 0.32);
transform: translateY(-1px);
}
+3
View File
@@ -72,6 +72,9 @@ export interface TopStreamsResult {
export interface T4sdApi {
appVersion: string
app: {
quit: () => Promise<void>
}
auth: {
isConfigured: () => Promise<boolean>
getStatus: () => Promise<AuthStatus>