mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 15:43:08 +01:00
feat: add HLS cache toggle and cache management to Settings
Chromium's HTTP disk cache accumulates HLS segments indefinitely on Steam Deck — single-use segments have ~0% cache-hit rate but cause unbounded SSD writes and disk growth. This wires up a runtime toggle (default: cache disabled) so the user can control the behaviour without restarting the app. - Introduce src/main/prefs/hlsCachePref.ts as a shared mutable so index.ts and handlers.ts don't form a circular import - Restructure onHeadersReceived in setupTwitchCors: CORS-bypass and no-store now run as independent blocks; no-store fires only for HLS extensions (.m3u8/.ts/.m4s/.aac, path before ?) when the toggle is off - Add IPC handlers app:get-cache-size, app:clear-cache, app:set-hls-cache-enabled; wire through preload bridge and t4sd.d.ts - Extend SettingsContext with hlsCacheEnabled (default false); push persisted value to main on mount and on every change - Add "Speicher" section to SettingsScreen with HLS toggle row and cache-size display + clear button; gamepad-navigable
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twitch4steamdeck",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.5",
|
||||
"description": "Twitch-Client für das Steam Deck (Live & VOD, Big-Screen-UI)",
|
||||
"main": "out/main/index.js",
|
||||
"author": "twitch4steamdeck",
|
||||
|
||||
+22
-8
@@ -5,6 +5,7 @@ import { HelixClient } from './twitch/helixClient'
|
||||
import { PlaybackService } from './playback/playbackService'
|
||||
import { registerIpcHandlers } from './ipc/handlers'
|
||||
import { startGamepadReader } from './input/gamepadReader'
|
||||
import { getHlsCacheEnabled } from './prefs/hlsCachePref'
|
||||
|
||||
const isDev = !app.isPackaged
|
||||
|
||||
@@ -53,11 +54,18 @@ function createWindow(): BrowserWindow {
|
||||
return win
|
||||
}
|
||||
|
||||
function isHlsStreamContent(url: string): boolean {
|
||||
const pathEnd = url.indexOf('?')
|
||||
const path = pathEnd === -1 ? url : url.slice(0, pathEnd)
|
||||
return /\.(m3u8|ts|m4s|aac)$/i.test(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS-Bypass für Twitch CDN und HLS-Streams.
|
||||
* hls.js im Renderer sendet Requests von localhost/file:// — Twitch CDN erwartet
|
||||
* Origin: https://www.twitch.tv. Wir setzen Origin + Referer auf ausgehenden Requests
|
||||
* und ergänzen CORS-Response-Header, damit Chromium die Antworten akzeptiert.
|
||||
* Zusätzlich: cache-control: no-store für HLS-Inhalte wenn hlsCacheEnabled=false.
|
||||
*/
|
||||
function setupTwitchCors(): void {
|
||||
const isTwitchCdn = (url: string): boolean =>
|
||||
@@ -75,18 +83,24 @@ function setupTwitchCors(): void {
|
||||
})
|
||||
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
if (isTwitchCdn(details.url)) {
|
||||
const headers: Record<string, string[]> = {}
|
||||
for (const [k, v] of Object.entries(details.responseHeaders ?? {})) {
|
||||
headers[k.toLowerCase()] = Array.isArray(v) ? v : [v]
|
||||
}
|
||||
const isCdn = isTwitchCdn(details.url)
|
||||
const applyNoStore = isHlsStreamContent(details.url) && !getHlsCacheEnabled()
|
||||
|
||||
if (!isCdn && !applyNoStore) { callback({}); return }
|
||||
|
||||
const headers: Record<string, string[]> = {}
|
||||
for (const [k, v] of Object.entries(details.responseHeaders ?? {})) {
|
||||
headers[k.toLowerCase()] = Array.isArray(v) ? v : [v]
|
||||
}
|
||||
if (isCdn) {
|
||||
headers['access-control-allow-origin'] = ['*']
|
||||
headers['access-control-allow-headers'] = ['*']
|
||||
headers['access-control-allow-methods'] = ['GET, HEAD, OPTIONS']
|
||||
callback({ responseHeaders: headers })
|
||||
} else {
|
||||
callback({})
|
||||
}
|
||||
if (applyNoStore) {
|
||||
headers['cache-control'] = ['no-store']
|
||||
}
|
||||
callback({ responseHeaders: headers })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron'
|
||||
import { app, BrowserWindow, ipcMain, session } from 'electron'
|
||||
import { setHlsCacheEnabled } from '../prefs/hlsCachePref'
|
||||
import { appendFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import type { AuthEvent, AuthService } from '../auth/authService'
|
||||
@@ -35,7 +36,11 @@ export const IPC = {
|
||||
/** main → renderer */
|
||||
playbackEvent: 'playback:event',
|
||||
/** main → renderer: HLS-URL + Metadaten für den Renderer-seitigen Video-Player */
|
||||
playbackHlsUrl: 'playback:hls-url'
|
||||
playbackHlsUrl: 'playback:hls-url',
|
||||
|
||||
appGetCacheSize: 'app:get-cache-size',
|
||||
appClearCache: 'app:clear-cache',
|
||||
appSetHlsCacheEnabled: 'app:set-hls-cache-enabled'
|
||||
} as const
|
||||
|
||||
export function registerIpcHandlers(
|
||||
@@ -47,6 +52,12 @@ export function registerIpcHandlers(
|
||||
app.quit()
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC.appGetCacheSize, () => session.defaultSession.getCacheSize())
|
||||
ipcMain.handle(IPC.appClearCache, () => session.defaultSession.clearCache())
|
||||
ipcMain.handle(IPC.appSetHlsCacheEnabled, (_e, enabled: boolean) => {
|
||||
setHlsCacheEnabled(Boolean(enabled))
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC.authStatus, () => auth.getStatus())
|
||||
ipcMain.handle(IPC.authConfigured, () => auth.isConfigured())
|
||||
ipcMain.handle(IPC.authStart, () => auth.startDeviceFlow())
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
let hlsCacheEnabled = false
|
||||
|
||||
export function getHlsCacheEnabled(): boolean {
|
||||
return hlsCacheEnabled
|
||||
}
|
||||
|
||||
export function setHlsCacheEnabled(enabled: boolean): void {
|
||||
hlsCacheEnabled = enabled
|
||||
}
|
||||
@@ -26,6 +26,10 @@ const IPC = {
|
||||
playbackEvent: 'playback:event',
|
||||
playbackHlsUrl: 'playback:hls-url',
|
||||
|
||||
appGetCacheSize: 'app:get-cache-size',
|
||||
appClearCache: 'app:clear-cache',
|
||||
appSetHlsCacheEnabled: 'app:set-hls-cache-enabled',
|
||||
|
||||
gamepadInput: 'gamepad-input'
|
||||
} as const
|
||||
|
||||
@@ -119,7 +123,10 @@ export interface TopStreamsResult {
|
||||
const api = {
|
||||
appVersion: process.env.APP_VERSION ?? process.env.npm_package_version ?? '0.0.0-dev',
|
||||
app: {
|
||||
quit: (): Promise<void> => ipcRenderer.invoke(IPC.appQuit)
|
||||
quit: (): Promise<void> => ipcRenderer.invoke(IPC.appQuit),
|
||||
getCacheSize: (): Promise<number> => ipcRenderer.invoke(IPC.appGetCacheSize),
|
||||
clearCache: (): Promise<void> => ipcRenderer.invoke(IPC.appClearCache),
|
||||
setHlsCacheEnabled: (enabled: boolean): Promise<void> => ipcRenderer.invoke(IPC.appSetHlsCacheEnabled, enabled)
|
||||
},
|
||||
auth: {
|
||||
isConfigured: (): Promise<boolean> => ipcRenderer.invoke(IPC.authConfigured),
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface AppSettings {
|
||||
streamBadgeMode: StreamBadgeMode
|
||||
sidebarWidth: number
|
||||
badgeGap: number
|
||||
hlsCacheEnabled: boolean
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 't4sd:settings'
|
||||
@@ -18,7 +19,8 @@ const BADGE_GAP_DEFAULT = 6
|
||||
const DEFAULTS: AppSettings = {
|
||||
streamBadgeMode: 'both',
|
||||
sidebarWidth: SIDEBAR_DEFAULT,
|
||||
badgeGap: BADGE_GAP_DEFAULT
|
||||
badgeGap: BADGE_GAP_DEFAULT,
|
||||
hlsCacheEnabled: false
|
||||
}
|
||||
|
||||
export { SIDEBAR_MIN, SIDEBAR_MAX, SIDEBAR_DEFAULT, BADGE_GAP_MIN, BADGE_GAP_MAX, BADGE_GAP_DEFAULT }
|
||||
@@ -36,7 +38,8 @@ function loadSettings(): AppSettings {
|
||||
? (parsed.streamBadgeMode as StreamBadgeMode)
|
||||
: DEFAULTS.streamBadgeMode,
|
||||
sidebarWidth: sw >= SIDEBAR_MIN && sw <= SIDEBAR_MAX ? sw : SIDEBAR_DEFAULT,
|
||||
badgeGap: bg >= BADGE_GAP_MIN && bg <= BADGE_GAP_MAX ? bg : BADGE_GAP_DEFAULT
|
||||
badgeGap: bg >= BADGE_GAP_MIN && bg <= BADGE_GAP_MAX ? bg : BADGE_GAP_DEFAULT,
|
||||
hlsCacheEnabled: typeof parsed.hlsCacheEnabled === 'boolean' ? parsed.hlsCacheEnabled : DEFAULTS.hlsCacheEnabled
|
||||
}
|
||||
} catch {
|
||||
return DEFAULTS
|
||||
@@ -56,6 +59,7 @@ interface SettingsCtx {
|
||||
setStreamBadgeMode: (mode: StreamBadgeMode) => void
|
||||
setSidebarWidth: (width: number) => void
|
||||
setBadgeGap: (gap: number) => void
|
||||
setHlsCacheEnabled: (enabled: boolean) => void
|
||||
}
|
||||
|
||||
const SettingsContext = createContext<SettingsCtx | null>(null)
|
||||
@@ -66,6 +70,7 @@ export function SettingsProvider({ children }: { children: React.ReactNode }): J
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty('--sidebar-width', `${settings.sidebarWidth}px`)
|
||||
document.documentElement.style.setProperty('--badge-gap', `${settings.badgeGap}px`)
|
||||
void window.t4sd.app.setHlsCacheEnabled(settings.hlsCacheEnabled)
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const setStreamBadgeMode = useCallback((mode: StreamBadgeMode) => {
|
||||
@@ -96,8 +101,17 @@ export function SettingsProvider({ children }: { children: React.ReactNode }): J
|
||||
document.documentElement.style.setProperty('--badge-gap', `${clamped}px`)
|
||||
}, [])
|
||||
|
||||
const setHlsCacheEnabled = useCallback((enabled: boolean) => {
|
||||
setSettings((prev) => {
|
||||
const next = { ...prev, hlsCacheEnabled: enabled }
|
||||
saveSettings(next)
|
||||
return next
|
||||
})
|
||||
void window.t4sd.app.setHlsCacheEnabled(enabled)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={{ settings, setStreamBadgeMode, setSidebarWidth, setBadgeGap }}>
|
||||
<SettingsContext.Provider value={{ settings, setStreamBadgeMode, setSidebarWidth, setBadgeGap, setHlsCacheEnabled }}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { GamepadHintItem, GamepadPrompt } from '../components/GamepadPrompt'
|
||||
import { SettingsIcon } from '../components/Icons'
|
||||
import {
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
SIDEBAR_DEFAULT,
|
||||
BADGE_GAP_MIN,
|
||||
BADGE_GAP_MAX,
|
||||
BADGE_GAP_DEFAULT
|
||||
BADGE_GAP_DEFAULT,
|
||||
} from '../context/SettingsContext'
|
||||
|
||||
interface Props {
|
||||
@@ -33,17 +33,65 @@ const OPTIONS: Option[] = [
|
||||
const SIDEBAR_STEP = 10
|
||||
const BADGE_GAP_STEP = 2
|
||||
|
||||
/** Total focusable rows: badge options + sidebar slider + badge gap slider */
|
||||
const MB = 1024 * 1024
|
||||
const CACHE_THRESHOLD_UNUSUAL = 500 * MB
|
||||
const CACHE_THRESHOLD_HIGH = 1024 * MB
|
||||
|
||||
type CacheStatus = 'normal' | 'unusual' | 'high'
|
||||
|
||||
function formatCacheSize(bytes: number): string {
|
||||
if (bytes < MB) return `${(bytes / 1024).toFixed(0)} KB`
|
||||
const mb = bytes / MB
|
||||
if (mb < 1000) return `${mb.toFixed(0)} MB`
|
||||
return `${(mb / 1024).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
function classifyCache(bytes: number): { status: CacheStatus; label: string } {
|
||||
if (bytes >= CACHE_THRESHOLD_HIGH) return { status: 'high', label: 'Leeren empfohlen' }
|
||||
if (bytes >= CACHE_THRESHOLD_UNUSUAL) return { status: 'unusual', label: 'Ungewöhnlich groß' }
|
||||
return { status: 'normal', label: 'Normal' }
|
||||
}
|
||||
|
||||
/** Total focusable rows: badge options + sidebar slider + badge gap slider + hls toggle + cache action */
|
||||
const SIDEBAR_SLIDER_ROW = OPTIONS.length
|
||||
const BADGE_GAP_SLIDER_ROW = OPTIONS.length + 1
|
||||
const TOTAL_ROWS = OPTIONS.length + 2
|
||||
const HLS_CACHE_TOGGLE_ROW = OPTIONS.length + 2
|
||||
const CACHE_ACTION_ROW = OPTIONS.length + 3
|
||||
const TOTAL_ROWS = OPTIONS.length + 4
|
||||
|
||||
export default function SettingsScreen({ hasFocus, onRequestSidebar }: Props): JSX.Element {
|
||||
const { settings, setStreamBadgeMode, setSidebarWidth, setBadgeGap } = useSettings()
|
||||
const { settings, setStreamBadgeMode, setSidebarWidth, setBadgeGap, setHlsCacheEnabled } = useSettings()
|
||||
const [focusedIndex, setFocusedIndex] = useState(() =>
|
||||
Math.max(0, OPTIONS.findIndex((o) => o.mode === settings.streamBadgeMode))
|
||||
)
|
||||
|
||||
const [cacheSize, setCacheSize] = useState<number | null>(null)
|
||||
const [clearing, setClearing] = useState(false)
|
||||
|
||||
const refreshCacheSize = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const size = await window.t4sd.app.getCacheSize()
|
||||
setCacheSize(size)
|
||||
} catch {
|
||||
setCacheSize(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void refreshCacheSize()
|
||||
}, [refreshCacheSize])
|
||||
|
||||
const handleClearCache = useCallback(async (): Promise<void> => {
|
||||
if (clearing) return
|
||||
setClearing(true)
|
||||
try {
|
||||
await window.t4sd.app.clearCache()
|
||||
await refreshCacheSize()
|
||||
} finally {
|
||||
setClearing(false)
|
||||
}
|
||||
}, [clearing, refreshCacheSize])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasFocus) return
|
||||
const onKey = (e: KeyboardEvent): void => {
|
||||
@@ -88,6 +136,10 @@ export default function SettingsScreen({ hasFocus, onRequestSidebar }: Props): J
|
||||
setSidebarWidth(SIDEBAR_DEFAULT)
|
||||
} else if (focusedIndex === BADGE_GAP_SLIDER_ROW) {
|
||||
setBadgeGap(BADGE_GAP_DEFAULT)
|
||||
} else if (focusedIndex === HLS_CACHE_TOGGLE_ROW) {
|
||||
setHlsCacheEnabled(!settings.hlsCacheEnabled)
|
||||
} else if (focusedIndex === CACHE_ACTION_ROW) {
|
||||
void handleClearCache()
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -95,7 +147,7 @@ export default function SettingsScreen({ hasFocus, onRequestSidebar }: Props): J
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [hasFocus, focusedIndex, settings.sidebarWidth, settings.badgeGap, onRequestSidebar, setStreamBadgeMode, setSidebarWidth, setBadgeGap])
|
||||
}, [hasFocus, focusedIndex, settings.sidebarWidth, settings.badgeGap, settings.hlsCacheEnabled, onRequestSidebar, setStreamBadgeMode, setSidebarWidth, setBadgeGap, setHlsCacheEnabled, handleClearCache])
|
||||
|
||||
const rowRefs = useRef<Array<HTMLElement | null>>([])
|
||||
|
||||
@@ -107,6 +159,10 @@ export default function SettingsScreen({ hasFocus, onRequestSidebar }: Props): J
|
||||
|
||||
const sidebarSliderFocused = hasFocus && focusedIndex === SIDEBAR_SLIDER_ROW
|
||||
const badgeGapSliderFocused = hasFocus && focusedIndex === BADGE_GAP_SLIDER_ROW
|
||||
const hlsCacheToggleFocused = hasFocus && focusedIndex === HLS_CACHE_TOGGLE_ROW
|
||||
const cacheActionFocused = hasFocus && focusedIndex === CACHE_ACTION_ROW
|
||||
|
||||
const cacheClassification = cacheSize !== null ? classifyCache(cacheSize) : null
|
||||
|
||||
return (
|
||||
<div className="screen settings-screen">
|
||||
@@ -226,6 +282,68 @@ export default function SettingsScreen({ hasFocus, onRequestSidebar }: Props): J
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h3 className="settings-section__title">Speicher</h3>
|
||||
<p className="settings-section__hint">
|
||||
HLS-Segmente (Live & VODs) sind Single-Use — sie werden nach dem Abspielen nie
|
||||
wieder abgerufen. Caching erzeugt auf dem Steam Deck unnötiges Disk-Wachstum und
|
||||
Akku-Belastung. Thumbnails und API-Antworten bleiben immer gecacht.
|
||||
Cache-Größe: Normal unter 500 MB · ungewöhnlich ab 500 MB · Leeren empfohlen ab 1 GB.
|
||||
</p>
|
||||
|
||||
<button
|
||||
ref={(el) => { rowRefs.current[HLS_CACHE_TOGGLE_ROW] = el }}
|
||||
className={[
|
||||
'settings-option',
|
||||
settings.hlsCacheEnabled ? 'settings-option--active' : '',
|
||||
hlsCacheToggleFocused ? 'settings-option--focused' : ''
|
||||
].filter(Boolean).join(' ')}
|
||||
onClick={() => {
|
||||
setFocusedIndex(HLS_CACHE_TOGGLE_ROW)
|
||||
setHlsCacheEnabled(!settings.hlsCacheEnabled)
|
||||
}}
|
||||
tabIndex={hlsCacheToggleFocused ? 0 : -1}
|
||||
>
|
||||
<span className="settings-option__radio" aria-hidden="true">
|
||||
{settings.hlsCacheEnabled ? '●' : '○'}
|
||||
</span>
|
||||
<span className="settings-option__label">HLS-Cache (Live & VODs)</span>
|
||||
<span className="settings-option__preview">
|
||||
{settings.hlsCacheEnabled
|
||||
? 'Aktiv — Segmente werden gecacht'
|
||||
: 'Inaktiv — kein Disk-Wachstum (empfohlen)'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
ref={(el) => { rowRefs.current[CACHE_ACTION_ROW] = el }}
|
||||
className={[
|
||||
'settings-cache',
|
||||
cacheActionFocused ? 'settings-cache--focused' : ''
|
||||
].filter(Boolean).join(' ')}
|
||||
onClick={() => {
|
||||
setFocusedIndex(CACHE_ACTION_ROW)
|
||||
void handleClearCache()
|
||||
}}
|
||||
disabled={clearing}
|
||||
tabIndex={cacheActionFocused ? 0 : -1}
|
||||
>
|
||||
<span className="settings-cache__info">
|
||||
<span className="settings-cache__size">
|
||||
{cacheSize === null ? '…' : formatCacheSize(cacheSize)}
|
||||
</span>
|
||||
{cacheClassification && (
|
||||
<span className={`settings-cache__pill settings-cache__pill--${cacheClassification.status}`}>
|
||||
{cacheClassification.label}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="settings-cache__action">
|
||||
{clearing ? 'Wird geleert …' : 'Cache leeren'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!hasFocus && (
|
||||
<div className="screen__state" style={{ flex: 0 }}>
|
||||
<SettingsIcon style={{ width: 48, height: 48, opacity: 0.2 }} />
|
||||
|
||||
@@ -1306,6 +1306,80 @@ body,
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Settings Cache ──────────────────────────────── */
|
||||
.settings-cache {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 16px 20px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-bg-elev);
|
||||
border: 2px solid transparent;
|
||||
color: var(--color-fg);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
margin-top: 8px;
|
||||
transition: background 0.1s, border-color 0.1s, opacity 0.1s;
|
||||
}
|
||||
|
||||
.settings-cache:hover:not(:disabled) {
|
||||
background: var(--color-bg-elev2);
|
||||
}
|
||||
|
||||
.settings-cache:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.settings-cache--focused {
|
||||
border-color: var(--color-focus);
|
||||
box-shadow: 0 0 0 2px rgba(191, 148, 255, 0.25);
|
||||
}
|
||||
|
||||
.settings-cache__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.settings-cache__size {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--color-fg);
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.settings-cache__pill {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.settings-cache__pill--normal {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--color-fg-dim);
|
||||
}
|
||||
|
||||
.settings-cache__pill--unusual {
|
||||
background: rgba(255, 184, 60, 0.18);
|
||||
color: #ffc56b;
|
||||
}
|
||||
|
||||
.settings-cache__pill--high {
|
||||
background: rgba(255, 95, 95, 0.18);
|
||||
color: #ff8585;
|
||||
}
|
||||
|
||||
.settings-cache__action {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.settings-screen {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
Vendored
+3
@@ -91,6 +91,9 @@ export interface T4sdApi {
|
||||
appVersion: string
|
||||
app: {
|
||||
quit: () => Promise<void>
|
||||
getCacheSize: () => Promise<number>
|
||||
clearCache: () => Promise<void>
|
||||
setHlsCacheEnabled: (enabled: boolean) => Promise<void>
|
||||
}
|
||||
auth: {
|
||||
isConfigured: () => Promise<boolean>
|
||||
|
||||
Reference in New Issue
Block a user