mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-18 07:16:24 +00:00
feat(tv): add user switching from settings
This commit is contained in:
@@ -5,6 +5,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { TVPasswordEntryModal } from "@/components/login/TVPasswordEntryModal";
|
||||
import { TVPINEntryModal } from "@/components/login/TVPINEntryModal";
|
||||
import type { TVOptionItem } from "@/components/tv";
|
||||
import {
|
||||
TVLogoutButton,
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
} from "@/components/tv";
|
||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import { useTVOptionModal } from "@/hooks/useTVOptionModal";
|
||||
import { useTVUserSwitchModal } from "@/hooks/useTVUserSwitchModal";
|
||||
import { APP_LANGUAGES } from "@/i18n";
|
||||
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
@@ -25,15 +28,21 @@ import {
|
||||
TVTypographyScale,
|
||||
useSettings,
|
||||
} from "@/utils/atoms/settings";
|
||||
import {
|
||||
getPreviousServers,
|
||||
type SavedServer,
|
||||
type SavedServerAccount,
|
||||
} from "@/utils/secureCredentials";
|
||||
|
||||
export default function SettingsTV() {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const { logout } = useJellyfin();
|
||||
const { logout, loginWithSavedCredential, loginWithPassword } = useJellyfin();
|
||||
const [user] = useAtom(userAtom);
|
||||
const [api] = useAtom(apiAtom);
|
||||
const { showOptions } = useTVOptionModal();
|
||||
const { showUserSwitchModal } = useTVUserSwitchModal();
|
||||
const typography = useScaledTVTypography();
|
||||
|
||||
// Local state for OpenSubtitles API key (only commit on blur)
|
||||
@@ -41,6 +50,89 @@ export default function SettingsTV() {
|
||||
settings.openSubtitlesApiKey || "",
|
||||
);
|
||||
|
||||
// PIN/Password modal state for user switching
|
||||
const [pinModalVisible, setPinModalVisible] = useState(false);
|
||||
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
|
||||
const [selectedServer, setSelectedServer] = useState<SavedServer | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedAccount, setSelectedAccount] =
|
||||
useState<SavedServerAccount | null>(null);
|
||||
|
||||
// Track if any modal is open to disable background focus
|
||||
const isAnyModalOpen = pinModalVisible || passwordModalVisible;
|
||||
|
||||
// Get current server and other accounts
|
||||
const currentServer = useMemo(() => {
|
||||
if (!api?.basePath) return null;
|
||||
const servers = getPreviousServers();
|
||||
return servers.find((s) => s.address === api.basePath) || null;
|
||||
}, [api?.basePath]);
|
||||
|
||||
const otherAccounts = useMemo(() => {
|
||||
if (!currentServer || !user?.Id) return [];
|
||||
return currentServer.accounts.filter(
|
||||
(account) => account.userId !== user.Id,
|
||||
);
|
||||
}, [currentServer, user?.Id]);
|
||||
|
||||
const hasOtherAccounts = otherAccounts.length > 0;
|
||||
|
||||
// Handle account selection from modal
|
||||
const handleAccountSelect = (account: SavedServerAccount) => {
|
||||
if (!currentServer) return;
|
||||
|
||||
if (account.securityType === "none") {
|
||||
// Direct login with saved credential
|
||||
loginWithSavedCredential(currentServer.address, account.userId);
|
||||
} else if (account.securityType === "pin") {
|
||||
// Show PIN modal
|
||||
setSelectedServer(currentServer);
|
||||
setSelectedAccount(account);
|
||||
setPinModalVisible(true);
|
||||
} else if (account.securityType === "password") {
|
||||
// Show password modal
|
||||
setSelectedServer(currentServer);
|
||||
setSelectedAccount(account);
|
||||
setPasswordModalVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle successful PIN entry
|
||||
const handlePinSuccess = async () => {
|
||||
setPinModalVisible(false);
|
||||
if (selectedServer && selectedAccount) {
|
||||
await loginWithSavedCredential(
|
||||
selectedServer.address,
|
||||
selectedAccount.userId,
|
||||
);
|
||||
}
|
||||
setSelectedServer(null);
|
||||
setSelectedAccount(null);
|
||||
};
|
||||
|
||||
// Handle password submission
|
||||
const handlePasswordSubmit = async (password: string) => {
|
||||
if (selectedServer && selectedAccount) {
|
||||
await loginWithPassword(
|
||||
selectedServer.address,
|
||||
selectedAccount.username,
|
||||
password,
|
||||
);
|
||||
}
|
||||
setPasswordModalVisible(false);
|
||||
setSelectedServer(null);
|
||||
setSelectedAccount(null);
|
||||
};
|
||||
|
||||
// Handle switch user button press
|
||||
const handleSwitchUser = () => {
|
||||
if (!currentServer || !user?.Id) return;
|
||||
showUserSwitchModal(currentServer, user.Id, {
|
||||
onAccountSelect: handleAccountSelect,
|
||||
});
|
||||
};
|
||||
|
||||
const currentAudioTranscode =
|
||||
settings.audioTranscodeMode || AudioTranscodeMode.Auto;
|
||||
const currentSubtitleMode =
|
||||
@@ -269,6 +361,16 @@ export default function SettingsTV() {
|
||||
{t("home.settings.settings_title")}
|
||||
</Text>
|
||||
|
||||
{/* Account Section */}
|
||||
<TVSectionHeader title={t("home.settings.switch_user.account")} />
|
||||
<TVSettingsOptionButton
|
||||
label={t("home.settings.switch_user.switch_user")}
|
||||
value={user?.Name || "-"}
|
||||
onPress={handleSwitchUser}
|
||||
disabled={!hasOtherAccounts || isAnyModalOpen}
|
||||
isFirst
|
||||
/>
|
||||
|
||||
{/* Audio Section */}
|
||||
<TVSectionHeader title={t("home.settings.audio.audio_title")} />
|
||||
<TVSettingsOptionButton
|
||||
@@ -282,7 +384,6 @@ export default function SettingsTV() {
|
||||
updateSettings({ audioTranscodeMode: value }),
|
||||
})
|
||||
}
|
||||
isFirst
|
||||
/>
|
||||
|
||||
{/* Subtitles Section */}
|
||||
@@ -570,6 +671,37 @@ export default function SettingsTV() {
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* PIN Entry Modal */}
|
||||
<TVPINEntryModal
|
||||
visible={pinModalVisible}
|
||||
onClose={() => {
|
||||
setPinModalVisible(false);
|
||||
setSelectedAccount(null);
|
||||
setSelectedServer(null);
|
||||
}}
|
||||
onSuccess={handlePinSuccess}
|
||||
onForgotPIN={() => {
|
||||
setPinModalVisible(false);
|
||||
setSelectedAccount(null);
|
||||
setSelectedServer(null);
|
||||
}}
|
||||
serverUrl={selectedServer?.address || ""}
|
||||
userId={selectedAccount?.userId || ""}
|
||||
username={selectedAccount?.username || ""}
|
||||
/>
|
||||
|
||||
{/* Password Entry Modal */}
|
||||
<TVPasswordEntryModal
|
||||
visible={passwordModalVisible}
|
||||
onClose={() => {
|
||||
setPasswordModalVisible(false);
|
||||
setSelectedAccount(null);
|
||||
setSelectedServer(null);
|
||||
}}
|
||||
onSubmit={handlePasswordSubmit}
|
||||
username={selectedAccount?.username || ""}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
174
app/(auth)/tv-user-switch-modal.tsx
Normal file
174
app/(auth)/tv-user-switch-modal.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { BlurView } from "expo-blur";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Animated,
|
||||
Easing,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TVFocusGuideView,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { TVUserCard } from "@/components/tv/TVUserCard";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { tvUserSwitchModalAtom } from "@/utils/atoms/tvUserSwitchModal";
|
||||
import type { SavedServerAccount } from "@/utils/secureCredentials";
|
||||
import { store } from "@/utils/store";
|
||||
|
||||
export default function TVUserSwitchModalPage() {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const modalState = useAtomValue(tvUserSwitchModalAtom);
|
||||
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const firstCardRef = useRef<View>(null);
|
||||
|
||||
const overlayOpacity = useRef(new Animated.Value(0)).current;
|
||||
const sheetTranslateY = useRef(new Animated.Value(200)).current;
|
||||
|
||||
// Animate in on mount and cleanup atom on unmount
|
||||
useEffect(() => {
|
||||
overlayOpacity.setValue(0);
|
||||
sheetTranslateY.setValue(200);
|
||||
|
||||
Animated.parallel([
|
||||
Animated.timing(overlayOpacity, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
easing: Easing.out(Easing.quad),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(sheetTranslateY, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
|
||||
// Delay focus setup to allow layout
|
||||
const timer = setTimeout(() => setIsReady(true), 100);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
// Clear the atom on unmount to prevent stale callbacks from being retained
|
||||
store.set(tvUserSwitchModalAtom, null);
|
||||
};
|
||||
}, [overlayOpacity, sheetTranslateY]);
|
||||
|
||||
// Request focus on the first card when ready
|
||||
useEffect(() => {
|
||||
if (isReady && firstCardRef.current) {
|
||||
const timer = setTimeout(() => {
|
||||
(firstCardRef.current as any)?.requestTVFocus?.();
|
||||
}, 50);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isReady]);
|
||||
|
||||
const handleSelect = (account: SavedServerAccount) => {
|
||||
modalState?.onAccountSelect(account);
|
||||
store.set(tvUserSwitchModalAtom, null);
|
||||
router.back();
|
||||
};
|
||||
|
||||
// If no modal state, just return null
|
||||
if (!modalState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheetContainer,
|
||||
{ transform: [{ translateY: sheetTranslateY }] },
|
||||
]}
|
||||
>
|
||||
<BlurView intensity={80} tint='dark' style={styles.blurContainer}>
|
||||
<TVFocusGuideView
|
||||
autoFocus
|
||||
trapFocusUp
|
||||
trapFocusDown
|
||||
trapFocusLeft
|
||||
trapFocusRight
|
||||
style={styles.content}
|
||||
>
|
||||
<Text style={styles.title}>
|
||||
{t("home.settings.switch_user.title")}
|
||||
</Text>
|
||||
<Text style={styles.subtitle}>{modalState.serverName}</Text>
|
||||
{isReady && (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
{modalState.accounts.map((account, index) => {
|
||||
const isCurrent = account.userId === modalState.currentUserId;
|
||||
return (
|
||||
<TVUserCard
|
||||
key={account.userId}
|
||||
ref={index === 0 ? firstCardRef : undefined}
|
||||
username={account.username}
|
||||
securityType={account.securityType}
|
||||
hasTVPreferredFocus={index === 0}
|
||||
isCurrent={isCurrent}
|
||||
onPress={() => handleSelect(account)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
)}
|
||||
</TVFocusGuideView>
|
||||
</BlurView>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
sheetContainer: {
|
||||
width: "100%",
|
||||
},
|
||||
blurContainer: {
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
overflow: "hidden",
|
||||
},
|
||||
content: {
|
||||
paddingTop: 24,
|
||||
paddingBottom: 50,
|
||||
overflow: "visible",
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: "500",
|
||||
color: "rgba(255,255,255,0.6)",
|
||||
marginBottom: 4,
|
||||
paddingHorizontal: 48,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: "rgba(255,255,255,0.4)",
|
||||
marginBottom: 16,
|
||||
paddingHorizontal: 48,
|
||||
},
|
||||
scrollView: {
|
||||
overflow: "visible",
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 48,
|
||||
paddingVertical: 20,
|
||||
gap: 16,
|
||||
},
|
||||
});
|
||||
@@ -488,6 +488,14 @@ function Layout() {
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='(auth)/tv-user-switch-modal'
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "transparentModal",
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Toaster
|
||||
duration={4000}
|
||||
|
||||
174
components/tv/TVUserCard.tsx
Normal file
174
components/tv/TVUserCard.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Animated, Pressable, View } from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import type { AccountSecurityType } from "@/utils/secureCredentials";
|
||||
import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
|
||||
|
||||
export interface TVUserCardProps {
|
||||
username: string;
|
||||
securityType: AccountSecurityType;
|
||||
hasTVPreferredFocus?: boolean;
|
||||
isCurrent?: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export const TVUserCard = React.forwardRef<View, TVUserCardProps>(
|
||||
(
|
||||
{
|
||||
username,
|
||||
securityType,
|
||||
hasTVPreferredFocus = false,
|
||||
isCurrent = false,
|
||||
onPress,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const typography = useScaledTVTypography();
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({ scaleAmount: isCurrent ? 1.02 : 1.05 });
|
||||
|
||||
const getSecurityIcon = (): keyof typeof Ionicons.glyphMap => {
|
||||
switch (securityType) {
|
||||
case "pin":
|
||||
return "keypad";
|
||||
case "password":
|
||||
return "lock-closed";
|
||||
default:
|
||||
return "key";
|
||||
}
|
||||
};
|
||||
|
||||
const getSecurityText = (): string => {
|
||||
switch (securityType) {
|
||||
case "pin":
|
||||
return t("save_account.pin_code");
|
||||
case "password":
|
||||
return t("save_account.password");
|
||||
default:
|
||||
return t("save_account.no_protection");
|
||||
}
|
||||
};
|
||||
|
||||
const getBackgroundColor = () => {
|
||||
if (isCurrent) {
|
||||
return focused ? "rgba(255,255,255,0.15)" : "rgba(255,255,255,0.04)";
|
||||
}
|
||||
return focused ? "#fff" : "rgba(255,255,255,0.08)";
|
||||
};
|
||||
|
||||
const getTextColor = () => {
|
||||
if (isCurrent) {
|
||||
return "rgba(255,255,255,0.4)";
|
||||
}
|
||||
return focused ? "#000" : "#fff";
|
||||
};
|
||||
|
||||
const getSecondaryColor = () => {
|
||||
if (isCurrent) {
|
||||
return "rgba(255,255,255,0.25)";
|
||||
}
|
||||
return focused ? "rgba(0,0,0,0.5)" : "rgba(255,255,255,0.5)";
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
ref={ref}
|
||||
onPress={isCurrent ? undefined : onPress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={hasTVPreferredFocus}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: getBackgroundColor(),
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
gap: 14,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* User Avatar */}
|
||||
<View
|
||||
style={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
backgroundColor: isCurrent
|
||||
? "rgba(255,255,255,0.08)"
|
||||
: focused
|
||||
? "rgba(0,0,0,0.1)"
|
||||
: "rgba(255,255,255,0.15)",
|
||||
borderRadius: 22,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name='person' size={24} color={getTextColor()} />
|
||||
</View>
|
||||
|
||||
{/* Text column */}
|
||||
<View style={{ gap: 4 }}>
|
||||
{/* Username */}
|
||||
<View
|
||||
style={{ flexDirection: "row", alignItems: "center", gap: 8 }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: typography.callout,
|
||||
color: getTextColor(),
|
||||
fontWeight: "600",
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{username}
|
||||
</Text>
|
||||
{isCurrent && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: typography.callout - 4,
|
||||
color: "rgba(255,255,255,0.3)",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
({t("home.settings.switch_user.current")})
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Security indicator */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={getSecurityIcon()}
|
||||
size={12}
|
||||
color={getSecondaryColor()}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: typography.callout - 4,
|
||||
color: getSecondaryColor(),
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{getSecurityText()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -65,3 +65,6 @@ export { TVThemeMusicIndicator } from "./TVThemeMusicIndicator";
|
||||
// Subtitle sheet components
|
||||
export type { TVTrackCardProps } from "./TVTrackCard";
|
||||
export { TVTrackCard } from "./TVTrackCard";
|
||||
// User switching
|
||||
export type { TVUserCardProps } from "./TVUserCard";
|
||||
export { TVUserCard } from "./TVUserCard";
|
||||
|
||||
@@ -47,6 +47,7 @@ export const TVSettingsOptionButton: React.FC<TVSettingsOptionButtonProps> = ({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
opacity: disabled ? 0.4 : 1,
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
42
hooks/useTVUserSwitchModal.ts
Normal file
42
hooks/useTVUserSwitchModal.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useCallback } from "react";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { tvUserSwitchModalAtom } from "@/utils/atoms/tvUserSwitchModal";
|
||||
import type {
|
||||
SavedServer,
|
||||
SavedServerAccount,
|
||||
} from "@/utils/secureCredentials";
|
||||
import { store } from "@/utils/store";
|
||||
|
||||
interface UseTVUserSwitchModalOptions {
|
||||
onAccountSelect: (account: SavedServerAccount) => void;
|
||||
}
|
||||
|
||||
export function useTVUserSwitchModal() {
|
||||
const router = useRouter();
|
||||
|
||||
const showUserSwitchModal = useCallback(
|
||||
(
|
||||
server: SavedServer,
|
||||
currentUserId: string,
|
||||
options: UseTVUserSwitchModalOptions,
|
||||
) => {
|
||||
// Need at least 2 accounts (current + at least one other)
|
||||
if (server.accounts.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(tvUserSwitchModalAtom, {
|
||||
serverUrl: server.address,
|
||||
serverName: server.name || server.address,
|
||||
accounts: server.accounts,
|
||||
currentUserId,
|
||||
onAccountSelect: options.onAccountSelect,
|
||||
});
|
||||
|
||||
router.push("/(auth)/tv-user-switch-modal");
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
return { showUserSwitchModal };
|
||||
}
|
||||
@@ -389,6 +389,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
try {
|
||||
const response = await getUserApi(apiInstance).getCurrentUser();
|
||||
|
||||
// Clear React Query cache to prevent data from previous account lingering
|
||||
queryClient.clear();
|
||||
storage.remove("REACT_QUERY_OFFLINE_CACHE");
|
||||
|
||||
// Token is valid, update state
|
||||
setApi(apiInstance);
|
||||
setUser(response.data);
|
||||
@@ -437,6 +441,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
const auth = await apiInstance.authenticateUserByName(username, password);
|
||||
|
||||
if (auth.data.AccessToken && auth.data.User) {
|
||||
// Clear React Query cache to prevent data from previous account lingering
|
||||
queryClient.clear();
|
||||
storage.remove("REACT_QUERY_OFFLINE_CACHE");
|
||||
|
||||
setUser(auth.data.User);
|
||||
storage.set("user", JSON.stringify(auth.data.User));
|
||||
setApi(jellyfin.createApi(serverUrl, auth.data.AccessToken));
|
||||
|
||||
@@ -112,6 +112,12 @@
|
||||
"settings": {
|
||||
"settings_title": "Settings",
|
||||
"log_out_button": "Log Out",
|
||||
"switch_user": {
|
||||
"title": "Switch User",
|
||||
"account": "Account",
|
||||
"switch_user": "Switch User",
|
||||
"current": "current"
|
||||
},
|
||||
"categories": {
|
||||
"title": "Categories"
|
||||
},
|
||||
|
||||
12
utils/atoms/tvUserSwitchModal.ts
Normal file
12
utils/atoms/tvUserSwitchModal.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { atom } from "jotai";
|
||||
import type { SavedServerAccount } from "@/utils/secureCredentials";
|
||||
|
||||
export type TVUserSwitchModalState = {
|
||||
serverUrl: string;
|
||||
serverName: string;
|
||||
accounts: SavedServerAccount[];
|
||||
currentUserId: string;
|
||||
onAccountSelect: (account: SavedServerAccount) => void;
|
||||
} | null;
|
||||
|
||||
export const tvUserSwitchModalAtom = atom<TVUserSwitchModalState>(null);
|
||||
Reference in New Issue
Block a user