mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-02-02 00:18:08 +00:00
feat(tv): add configurable inactivity timeout with auto-logout
This commit is contained in:
@@ -24,6 +24,7 @@ import { APP_LANGUAGES } from "@/i18n";
|
||||
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
AudioTranscodeMode,
|
||||
InactivityTimeout,
|
||||
type MpvCacheMode,
|
||||
TVTypographyScale,
|
||||
useSettings,
|
||||
@@ -300,6 +301,57 @@ export default function SettingsTV() {
|
||||
[t, currentLanguage],
|
||||
);
|
||||
|
||||
// Inactivity timeout options (TV security feature)
|
||||
const currentInactivityTimeout =
|
||||
settings.inactivityTimeout ?? InactivityTimeout.Disabled;
|
||||
|
||||
const inactivityTimeoutOptions: TVOptionItem<InactivityTimeout>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t("home.settings.security.inactivity_timeout.disabled"),
|
||||
value: InactivityTimeout.Disabled,
|
||||
selected: currentInactivityTimeout === InactivityTimeout.Disabled,
|
||||
},
|
||||
{
|
||||
label: t("home.settings.security.inactivity_timeout.1_minute"),
|
||||
value: InactivityTimeout.OneMinute,
|
||||
selected: currentInactivityTimeout === InactivityTimeout.OneMinute,
|
||||
},
|
||||
{
|
||||
label: t("home.settings.security.inactivity_timeout.5_minutes"),
|
||||
value: InactivityTimeout.FiveMinutes,
|
||||
selected: currentInactivityTimeout === InactivityTimeout.FiveMinutes,
|
||||
},
|
||||
{
|
||||
label: t("home.settings.security.inactivity_timeout.15_minutes"),
|
||||
value: InactivityTimeout.FifteenMinutes,
|
||||
selected: currentInactivityTimeout === InactivityTimeout.FifteenMinutes,
|
||||
},
|
||||
{
|
||||
label: t("home.settings.security.inactivity_timeout.30_minutes"),
|
||||
value: InactivityTimeout.ThirtyMinutes,
|
||||
selected: currentInactivityTimeout === InactivityTimeout.ThirtyMinutes,
|
||||
},
|
||||
{
|
||||
label: t("home.settings.security.inactivity_timeout.1_hour"),
|
||||
value: InactivityTimeout.OneHour,
|
||||
selected: currentInactivityTimeout === InactivityTimeout.OneHour,
|
||||
},
|
||||
{
|
||||
label: t("home.settings.security.inactivity_timeout.4_hours"),
|
||||
value: InactivityTimeout.FourHours,
|
||||
selected: currentInactivityTimeout === InactivityTimeout.FourHours,
|
||||
},
|
||||
{
|
||||
label: t("home.settings.security.inactivity_timeout.24_hours"),
|
||||
value: InactivityTimeout.TwentyFourHours,
|
||||
selected:
|
||||
currentInactivityTimeout === InactivityTimeout.TwentyFourHours,
|
||||
},
|
||||
],
|
||||
[t, currentInactivityTimeout],
|
||||
);
|
||||
|
||||
// Get display labels for option buttons
|
||||
const audioTranscodeLabel = useMemo(() => {
|
||||
const option = audioTranscodeModeOptions.find((o) => o.selected);
|
||||
@@ -337,6 +389,13 @@ export default function SettingsTV() {
|
||||
return option?.label || t("home.settings.languages.system");
|
||||
}, [currentLanguage, t]);
|
||||
|
||||
const inactivityTimeoutLabel = useMemo(() => {
|
||||
const option = inactivityTimeoutOptions.find((o) => o.selected);
|
||||
return (
|
||||
option?.label || t("home.settings.security.inactivity_timeout.disabled")
|
||||
);
|
||||
}, [inactivityTimeoutOptions, t]);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: "#000000" }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
@@ -371,6 +430,21 @@ export default function SettingsTV() {
|
||||
isFirst
|
||||
/>
|
||||
|
||||
{/* Security Section */}
|
||||
<TVSectionHeader title={t("home.settings.security.title")} />
|
||||
<TVSettingsOptionButton
|
||||
label={t("home.settings.security.inactivity_timeout.title")}
|
||||
value={inactivityTimeoutLabel}
|
||||
onPress={() =>
|
||||
showOptions({
|
||||
title: t("home.settings.security.inactivity_timeout.title"),
|
||||
options: inactivityTimeoutOptions,
|
||||
onSelect: (value) =>
|
||||
updateSettings({ inactivityTimeout: value }),
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Audio Section */}
|
||||
<TVSectionHeader title={t("home.settings.audio.audio_title")} />
|
||||
<TVSettingsOptionButton
|
||||
|
||||
273
app/_layout.tsx
273
app/_layout.tsx
@@ -14,6 +14,7 @@ import { enableTVMenuKeyInterception } from "@/hooks/useTVBackHandler";
|
||||
import i18n from "@/i18n";
|
||||
import { DownloadProvider } from "@/providers/DownloadProvider";
|
||||
import { GlobalModalProvider } from "@/providers/GlobalModalProvider";
|
||||
import { InactivityProvider } from "@/providers/InactivityProvider";
|
||||
import { IntroSheetProvider } from "@/providers/IntroSheetProvider";
|
||||
import {
|
||||
apiAtom,
|
||||
@@ -385,143 +386,145 @@ function Layout() {
|
||||
}}
|
||||
>
|
||||
<JellyfinProvider>
|
||||
<ServerUrlProvider>
|
||||
<NetworkStatusProvider>
|
||||
<PlaySettingsProvider>
|
||||
<LogProvider>
|
||||
<WebSocketProvider>
|
||||
<DownloadProvider>
|
||||
<MusicPlayerProvider>
|
||||
<GlobalModalProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<IntroSheetProvider>
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<SystemBars style='light' hidden={false} />
|
||||
<Stack initialRouteName='(auth)/(tabs)'>
|
||||
<Stack.Screen
|
||||
name='(auth)/(tabs)'
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "",
|
||||
header: () => null,
|
||||
<InactivityProvider>
|
||||
<ServerUrlProvider>
|
||||
<NetworkStatusProvider>
|
||||
<PlaySettingsProvider>
|
||||
<LogProvider>
|
||||
<WebSocketProvider>
|
||||
<DownloadProvider>
|
||||
<MusicPlayerProvider>
|
||||
<GlobalModalProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<IntroSheetProvider>
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<SystemBars style='light' hidden={false} />
|
||||
<Stack initialRouteName='(auth)/(tabs)'>
|
||||
<Stack.Screen
|
||||
name='(auth)/(tabs)'
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "",
|
||||
header: () => null,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/player'
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "",
|
||||
header: () => null,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/now-playing'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "modal",
|
||||
gestureEnabled: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='login'
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: "",
|
||||
headerTransparent: Platform.OS === "ios",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name='+not-found' />
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-option-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-subtitle-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-request-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-season-select-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-series-season-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='tv-account-action-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='tv-account-select-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-user-switch-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Toaster
|
||||
duration={4000}
|
||||
toastOptions={{
|
||||
style: {
|
||||
backgroundColor: "#262626",
|
||||
borderColor: "#363639",
|
||||
borderWidth: 1,
|
||||
},
|
||||
titleStyle: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
closeButton
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/player'
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "",
|
||||
header: () => null,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/now-playing'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "modal",
|
||||
gestureEnabled: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='login'
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: "",
|
||||
headerTransparent: Platform.OS === "ios",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name='+not-found' />
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-option-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-subtitle-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-request-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-season-select-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-series-season-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='tv-account-action-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='tv-account-select-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-user-switch-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Toaster
|
||||
duration={4000}
|
||||
toastOptions={{
|
||||
style: {
|
||||
backgroundColor: "#262626",
|
||||
borderColor: "#363639",
|
||||
borderWidth: 1,
|
||||
},
|
||||
titleStyle: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
closeButton
|
||||
/>
|
||||
{!Platform.isTV && <GlobalModal />}
|
||||
</ThemeProvider>
|
||||
</IntroSheetProvider>
|
||||
</BottomSheetModalProvider>
|
||||
</GlobalModalProvider>
|
||||
</MusicPlayerProvider>
|
||||
</DownloadProvider>
|
||||
</WebSocketProvider>
|
||||
</LogProvider>
|
||||
</PlaySettingsProvider>
|
||||
</NetworkStatusProvider>
|
||||
</ServerUrlProvider>
|
||||
{!Platform.isTV && <GlobalModal />}
|
||||
</ThemeProvider>
|
||||
</IntroSheetProvider>
|
||||
</BottomSheetModalProvider>
|
||||
</GlobalModalProvider>
|
||||
</MusicPlayerProvider>
|
||||
</DownloadProvider>
|
||||
</WebSocketProvider>
|
||||
</LogProvider>
|
||||
</PlaySettingsProvider>
|
||||
</NetworkStatusProvider>
|
||||
</ServerUrlProvider>
|
||||
</InactivityProvider>
|
||||
</JellyfinProvider>
|
||||
</PersistQueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { Animated, Easing } from "react-native";
|
||||
import { useInactivity } from "@/providers/InactivityProvider";
|
||||
|
||||
export interface UseTVFocusAnimationOptions {
|
||||
scaleAmount?: number;
|
||||
@@ -24,6 +25,7 @@ export const useTVFocusAnimation = ({
|
||||
}: UseTVFocusAnimationOptions = {}): UseTVFocusAnimationReturn => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const scale = useRef(new Animated.Value(1)).current;
|
||||
const { resetInactivityTimer } = useInactivity();
|
||||
|
||||
const animateTo = useCallback(
|
||||
(value: number) => {
|
||||
@@ -40,8 +42,9 @@ export const useTVFocusAnimation = ({
|
||||
const handleFocus = useCallback(() => {
|
||||
setFocused(true);
|
||||
animateTo(scaleAmount);
|
||||
resetInactivityTimer();
|
||||
onFocus?.();
|
||||
}, [animateTo, scaleAmount, onFocus]);
|
||||
}, [animateTo, scaleAmount, resetInactivityTimer, onFocus]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setFocused(false);
|
||||
|
||||
186
providers/InactivityProvider.tsx
Normal file
186
providers/InactivityProvider.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import type React from "react";
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { AppState, type AppStateStatus, Platform } from "react-native";
|
||||
import { useJellyfin } from "@/providers/JellyfinProvider";
|
||||
import { InactivityTimeout, useSettings } from "@/utils/atoms/settings";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
|
||||
const INACTIVITY_LAST_ACTIVITY_KEY = "INACTIVITY_LAST_ACTIVITY";
|
||||
|
||||
interface InactivityContextValue {
|
||||
resetInactivityTimer: () => void;
|
||||
}
|
||||
|
||||
const InactivityContext = createContext<InactivityContextValue | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
/**
|
||||
* TV-only provider that tracks user inactivity and auto-logs out
|
||||
* when the configured timeout is exceeded.
|
||||
*
|
||||
* Features:
|
||||
* - Tracks last activity timestamp (persisted to MMKV)
|
||||
* - Resets timer on any focus change (via resetInactivityTimer)
|
||||
* - Handles app backgrounding: logs out immediately if timeout exceeded while away
|
||||
* - No-op on mobile platforms
|
||||
*/
|
||||
export const InactivityProvider: React.FC<{ children: ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { settings } = useSettings();
|
||||
const { logout } = useJellyfin();
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
|
||||
|
||||
const timeoutMs = settings.inactivityTimeout ?? InactivityTimeout.Disabled;
|
||||
const isEnabled = Platform.isTV && timeoutMs > 0;
|
||||
|
||||
const clearTimer = useCallback(() => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateLastActivity = useCallback(() => {
|
||||
if (!isEnabled) return;
|
||||
storage.set(INACTIVITY_LAST_ACTIVITY_KEY, Date.now());
|
||||
}, [isEnabled]);
|
||||
|
||||
const getLastActivity = useCallback((): number => {
|
||||
return storage.getNumber(INACTIVITY_LAST_ACTIVITY_KEY) ?? Date.now();
|
||||
}, []);
|
||||
|
||||
const startTimer = useCallback(
|
||||
(remainingMs?: number) => {
|
||||
if (!isEnabled) return;
|
||||
|
||||
clearTimer();
|
||||
|
||||
const delay = remainingMs ?? timeoutMs;
|
||||
timerRef.current = setTimeout(() => {
|
||||
logout();
|
||||
storage.remove(INACTIVITY_LAST_ACTIVITY_KEY);
|
||||
}, delay);
|
||||
},
|
||||
[isEnabled, timeoutMs, clearTimer, logout],
|
||||
);
|
||||
|
||||
const resetInactivityTimer = useCallback(() => {
|
||||
if (!isEnabled) return;
|
||||
|
||||
updateLastActivity();
|
||||
startTimer();
|
||||
}, [isEnabled, updateLastActivity, startTimer]);
|
||||
|
||||
// Handle app state changes (background/foreground)
|
||||
useEffect(() => {
|
||||
if (!isEnabled) return;
|
||||
|
||||
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
||||
const wasBackground =
|
||||
appStateRef.current === "background" ||
|
||||
appStateRef.current === "inactive";
|
||||
const isNowActive = nextAppState === "active";
|
||||
|
||||
if (wasBackground && isNowActive) {
|
||||
// App returned to foreground - check if timeout exceeded
|
||||
const lastActivity = getLastActivity();
|
||||
const elapsed = Date.now() - lastActivity;
|
||||
|
||||
if (elapsed >= timeoutMs) {
|
||||
// Timeout exceeded while backgrounded - logout immediately
|
||||
logout();
|
||||
storage.remove(INACTIVITY_LAST_ACTIVITY_KEY);
|
||||
} else {
|
||||
// Restart timer with remaining time
|
||||
const remainingMs = timeoutMs - elapsed;
|
||||
startTimer(remainingMs);
|
||||
}
|
||||
} else if (nextAppState === "background" || nextAppState === "inactive") {
|
||||
// App going to background - clear timer (time continues via timestamp)
|
||||
clearTimer();
|
||||
}
|
||||
|
||||
appStateRef.current = nextAppState;
|
||||
};
|
||||
|
||||
const subscription = AppState.addEventListener(
|
||||
"change",
|
||||
handleAppStateChange,
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, [isEnabled, timeoutMs, getLastActivity, startTimer, clearTimer, logout]);
|
||||
|
||||
// Initialize timer when enabled or timeout changes
|
||||
useEffect(() => {
|
||||
if (!isEnabled) {
|
||||
clearTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we should logout based on last activity
|
||||
const lastActivity = getLastActivity();
|
||||
const elapsed = Date.now() - lastActivity;
|
||||
|
||||
if (elapsed >= timeoutMs) {
|
||||
// Already timed out - logout
|
||||
logout();
|
||||
storage.remove(INACTIVITY_LAST_ACTIVITY_KEY);
|
||||
} else {
|
||||
// Start timer with remaining time
|
||||
const remainingMs = timeoutMs - elapsed;
|
||||
startTimer(remainingMs);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimer();
|
||||
};
|
||||
}, [isEnabled, timeoutMs, getLastActivity, startTimer, clearTimer, logout]);
|
||||
|
||||
// Reset activity on initial mount when enabled
|
||||
useEffect(() => {
|
||||
if (isEnabled) {
|
||||
updateLastActivity();
|
||||
startTimer();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const contextValue: InactivityContextValue = {
|
||||
resetInactivityTimer,
|
||||
};
|
||||
|
||||
return (
|
||||
<InactivityContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</InactivityContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to access the inactivity reset function.
|
||||
* Returns a no-op function if not within the provider (safe on mobile).
|
||||
*/
|
||||
export const useInactivity = (): InactivityContextValue => {
|
||||
const context = useContext(InactivityContext);
|
||||
|
||||
// Return a no-op if not within provider (e.g., on mobile)
|
||||
if (!context) {
|
||||
return {
|
||||
resetInactivityTimer: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -481,6 +481,21 @@
|
||||
"error_deleting_files": "Error Deleting Files",
|
||||
"background_downloads_enabled": "Background downloads enabled",
|
||||
"background_downloads_disabled": "Background downloads disabled"
|
||||
},
|
||||
"security": {
|
||||
"title": "Security",
|
||||
"inactivity_timeout": {
|
||||
"title": "Inactivity Timeout",
|
||||
"description": "Auto logout after inactivity",
|
||||
"disabled": "Disabled",
|
||||
"1_minute": "1 minute",
|
||||
"5_minutes": "5 minutes",
|
||||
"15_minutes": "15 minutes",
|
||||
"30_minutes": "30 minutes",
|
||||
"1_hour": "1 hour",
|
||||
"4_hours": "4 hours",
|
||||
"24_hours": "24 hours"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
|
||||
@@ -154,6 +154,18 @@ export enum AudioTranscodeMode {
|
||||
AllowAll = "passthrough", // Direct play all audio formats
|
||||
}
|
||||
|
||||
// Inactivity timeout for TV - auto logout after period of no activity
|
||||
export enum InactivityTimeout {
|
||||
Disabled = 0,
|
||||
OneMinute = 60000,
|
||||
FiveMinutes = 300000,
|
||||
FifteenMinutes = 900000,
|
||||
ThirtyMinutes = 1800000,
|
||||
OneHour = 3600000,
|
||||
FourHours = 14400000,
|
||||
TwentyFourHours = 86400000,
|
||||
}
|
||||
|
||||
// MPV cache mode - controls how caching is enabled
|
||||
export type MpvCacheMode = "auto" | "yes" | "no";
|
||||
|
||||
@@ -234,6 +246,8 @@ export type Settings = {
|
||||
audioTranscodeMode: AudioTranscodeMode;
|
||||
// OpenSubtitles API key for client-side subtitle fetching
|
||||
openSubtitlesApiKey?: string;
|
||||
// TV-only: Inactivity timeout for auto-logout
|
||||
inactivityTimeout: InactivityTimeout;
|
||||
};
|
||||
|
||||
export interface Lockable<T> {
|
||||
@@ -329,6 +343,8 @@ export const defaultValues: Settings = {
|
||||
preferLocalAudio: true,
|
||||
// Audio transcoding mode
|
||||
audioTranscodeMode: AudioTranscodeMode.Auto,
|
||||
// TV-only: Inactivity timeout (disabled by default)
|
||||
inactivityTimeout: InactivityTimeout.Disabled,
|
||||
};
|
||||
|
||||
const loadSettings = (): Partial<Settings> => {
|
||||
|
||||
Reference in New Issue
Block a user