mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-16 09:23:07 +01:00
Compare commits
4 Commits
fix/nav-do
...
feat/quick
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cdfebe908 | ||
|
|
2e79815d78 | ||
|
|
43ce50fe70 | ||
|
|
bd0eef0407 |
@@ -71,7 +71,7 @@ export default function AppearanceHideLibrariesPage() {
|
||||
))}
|
||||
</ListGroup>
|
||||
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
||||
{t("home.settings.other.select_liraries_you_want_to_hide")}
|
||||
{t("home.settings.other.select_libraries_you_want_to_hide")}
|
||||
</Text>
|
||||
</DisabledSetting>
|
||||
</ScrollView>
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function HideLibrariesPage() {
|
||||
))}
|
||||
</ListGroup>
|
||||
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
||||
{t("home.settings.other.select_liraries_you_want_to_hide")}
|
||||
{t("home.settings.other.select_libraries_you_want_to_hide")}
|
||||
</Text>
|
||||
</DisabledSetting>
|
||||
);
|
||||
|
||||
@@ -1318,7 +1318,7 @@ export default function DirectPlayerPage() {
|
||||
console.error("Video Error:", e.nativeEvent);
|
||||
Alert.alert(
|
||||
t("player.error"),
|
||||
t("player.an_error_occured_while_playing_the_video"),
|
||||
t("player.an_error_occurred_while_playing_the_video"),
|
||||
);
|
||||
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
||||
}}
|
||||
|
||||
@@ -85,7 +85,8 @@ configureReanimatedLogger({
|
||||
if (!Platform.isTV) {
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowAlert: true,
|
||||
shouldShowBanner: true,
|
||||
shouldShowList: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: false,
|
||||
}),
|
||||
@@ -350,9 +351,12 @@ function Layout() {
|
||||
notificationListener.current =
|
||||
Notifications?.addNotificationReceivedListener(
|
||||
(notification: Notification) => {
|
||||
// Log only the title — serializing the whole notification touches
|
||||
// the deprecated dataString getter (deprecation warning) and dumps
|
||||
// noisy payloads into the console.
|
||||
console.log(
|
||||
"Notification received while app running",
|
||||
notification,
|
||||
"Notification received while app running:",
|
||||
notification.request.content.title,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
3
bun.lock
3
bun.lock
@@ -31,6 +31,7 @@
|
||||
"expo-brightness": "~56.0.5",
|
||||
"expo-build-properties": "~56.0.18",
|
||||
"expo-camera": "~56.0.8",
|
||||
"expo-clipboard": "~56.0.4",
|
||||
"expo-constants": "~56.0.18",
|
||||
"expo-crypto": "~56.0.4",
|
||||
"expo-dev-client": "~56.0.20",
|
||||
@@ -1001,6 +1002,8 @@
|
||||
|
||||
"expo-camera": ["expo-camera@56.0.8", "", { "dependencies": { "barcode-detector": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-UDOpUUMisFRmCv1XQV1MJCKGAH2CsIC1Rs6P9Bbc6JLVmbxEKAd5dK68y6cScOdWURxVfJ0PRcjYnSuc8ayyIQ=="],
|
||||
|
||||
"expo-clipboard": ["expo-clipboard@56.0.4", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-qb4DYlkiowHYHaUYVT2FN9nk/nI1xShXOUYsI7J9dVpQCOHcGFjCBPX1VAvEW4Ye4/Aagd6IuhOVAq/+scBOiA=="],
|
||||
|
||||
"expo-constants": ["expo-constants@56.0.18", "", { "dependencies": { "@expo/env": "~2.3.0" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-8AMtbDGl/WVPnWlmbpGmvcdnNCy9E4PFnwdVwj600vljkMDPSxcAcjw8GVXEPk3PpZ+ngTqsrkltWyj0UKYAxw=="],
|
||||
|
||||
"expo-crypto": ["expo-crypto@56.0.4", "", { "peerDependencies": { "expo": "*" } }, "sha512-fRNEhoXRXgAWBpe3/hq5X+KXTit3OZqdiAGts1YvNEUHQb+H5591mpPac0Yw+sZg9pXcrjRnzo5AxvZaENpc7g=="],
|
||||
|
||||
@@ -16,9 +16,12 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
const router = useRouter();
|
||||
|
||||
// Keyed on SeriesId so recycled FlashList cells re-read the correct poster
|
||||
// instead of freezing the first-rendered series' image (empty deps bug).
|
||||
const base64Image = useMemo(() => {
|
||||
return storage.getString(items[0].SeriesId!);
|
||||
}, []);
|
||||
const seriesId = items[0]?.SeriesId;
|
||||
return seriesId ? storage.getString(seriesId) : undefined;
|
||||
}, [items[0]?.SeriesId]);
|
||||
|
||||
const deleteSeries = useCallback(
|
||||
async () =>
|
||||
|
||||
@@ -20,10 +20,11 @@ import { Button } from "@/components/Button";
|
||||
import { Input } from "@/components/common/Input";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery";
|
||||
import { QuickConnectCodeModal } from "@/components/login/QuickConnectCodeModal";
|
||||
import { PreviousServersList } from "@/components/PreviousServersList";
|
||||
import { SaveAccountModal } from "@/components/SaveAccountModal";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
|
||||
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||
import type {
|
||||
AccountSecurityType,
|
||||
SavedServer,
|
||||
@@ -37,11 +38,13 @@ export const Login: React.FC = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const navigation = useNavigation();
|
||||
const params = useLocalSearchParams();
|
||||
const user = useAtomValue(userAtom);
|
||||
const {
|
||||
setServer,
|
||||
login,
|
||||
removeServer,
|
||||
initiateQuickConnect,
|
||||
stopQuickConnectPolling,
|
||||
loginWithSavedCredential,
|
||||
loginWithPassword,
|
||||
} = useJellyfin();
|
||||
@@ -64,6 +67,32 @@ export const Login: React.FC = () => {
|
||||
password: _password || "",
|
||||
});
|
||||
|
||||
// Quick Connect code shown in the in-app sheet while polling for authorization
|
||||
const [quickConnectCode, setQuickConnectCode] = useState<string | null>(null);
|
||||
|
||||
// Close the code sheet as soon as the session is authorized — the native
|
||||
// Alert used before had no programmatic dismiss and stayed open after login.
|
||||
useEffect(() => {
|
||||
if (user) setQuickConnectCode(null);
|
||||
}, [user]);
|
||||
|
||||
// Stop Quick Connect polling when leaving the login page (parity with TVLogin)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
stopQuickConnectPolling();
|
||||
};
|
||||
}, [stopQuickConnectPolling]);
|
||||
|
||||
// Going back to server selection keeps this component mounted (same screen,
|
||||
// different state), so the unmount cleanup above doesn't run. Without this a
|
||||
// code authorized after leaving would silently log the user in later.
|
||||
useEffect(() => {
|
||||
if (!api?.basePath) {
|
||||
stopQuickConnectPolling();
|
||||
setQuickConnectCode(null);
|
||||
}
|
||||
}, [api?.basePath, stopQuickConnectPolling]);
|
||||
|
||||
// Save account state
|
||||
const [saveAccount, setSaveAccount] = useState(false);
|
||||
const [showSaveModal, setShowSaveModal] = useState(false);
|
||||
@@ -146,7 +175,7 @@ export const Login: React.FC = () => {
|
||||
} else {
|
||||
Alert.alert(
|
||||
t("login.connection_failed"),
|
||||
t("login.an_unexpected_error_occured"),
|
||||
t("login.an_unexpected_error_occurred"),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -259,15 +288,7 @@ export const Login: React.FC = () => {
|
||||
try {
|
||||
const code = await initiateQuickConnect();
|
||||
if (code) {
|
||||
Alert.alert(
|
||||
t("login.quick_connect"),
|
||||
t("login.enter_code_to_login", { code: code }),
|
||||
[
|
||||
{
|
||||
text: t("login.got_it"),
|
||||
},
|
||||
],
|
||||
);
|
||||
setQuickConnectCode(code);
|
||||
}
|
||||
} catch (_error) {
|
||||
Alert.alert(
|
||||
@@ -402,7 +423,7 @@ export const Login: React.FC = () => {
|
||||
{t("server.enter_url_to_jellyfin_server")}
|
||||
</Text>
|
||||
<Input
|
||||
aria-label='Server URL'
|
||||
aria-label={t("server.server_url")}
|
||||
placeholder={t("server.server_url_placeholder")}
|
||||
onChangeText={setServerURL}
|
||||
value={serverURL}
|
||||
@@ -453,6 +474,13 @@ export const Login: React.FC = () => {
|
||||
onSave={handleSaveAccountConfirm}
|
||||
username={pendingLogin?.username || credentials.username}
|
||||
/>
|
||||
|
||||
{/* Dismissing only hides the code — polling continues so the login still
|
||||
completes if the code is authorized from another device afterwards. */}
|
||||
<QuickConnectCodeModal
|
||||
code={quickConnectCode}
|
||||
onClose={() => setQuickConnectCode(null)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
137
components/login/QuickConnectCodeModal.tsx
Normal file
137
components/login/QuickConnectCodeModal.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { requireOptionalNativeModule } from "expo-modules-core";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { toast } from "sonner-native";
|
||||
import { Button } from "../Button";
|
||||
import { Text } from "../common/Text";
|
||||
|
||||
interface Props {
|
||||
/** The Quick Connect code to display, or null when hidden. */
|
||||
code: string | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the Quick Connect code while the app polls for authorization.
|
||||
* In-app sheet instead of a native Alert so it can dismiss itself once the
|
||||
* session is authorized — a native alert has no programmatic dismiss and
|
||||
* lingers over the app after login completes.
|
||||
*/
|
||||
export const QuickConnectCodeModal: React.FC<Props> = ({ code, onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const snapPoints = useMemo(() => ["50%"], []);
|
||||
const isPresentedRef = useRef(false);
|
||||
|
||||
// Keep the last code around so the dismiss animation doesn't flash empty
|
||||
// when the parent clears the code to close the sheet.
|
||||
const lastCodeRef = useRef<string | null>(null);
|
||||
if (code) lastCodeRef.current = code;
|
||||
|
||||
useEffect(() => {
|
||||
if (code) {
|
||||
bottomSheetModalRef.current?.present();
|
||||
} else if (isPresentedRef.current) {
|
||||
bottomSheetModalRef.current?.dismiss();
|
||||
isPresentedRef.current = false;
|
||||
}
|
||||
}, [code]);
|
||||
|
||||
const handleSheetChanges = useCallback(
|
||||
(index: number) => {
|
||||
if (index >= 0) {
|
||||
isPresentedRef.current = true;
|
||||
} else if (index === -1 && isPresentedRef.current) {
|
||||
isPresentedRef.current = false;
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const copyCode = useCallback(async () => {
|
||||
const value = code ?? lastCodeRef.current;
|
||||
if (!value) return;
|
||||
// Builds that don't ship the expo-clipboard native module yet: probe with
|
||||
// requireOptionalNativeModule (returns null instead of throwing/logging)
|
||||
// and skip — importing the JS wrapper there would error out.
|
||||
if (!requireOptionalNativeModule("ExpoClipboard")) return;
|
||||
const Clipboard = await import("expo-clipboard");
|
||||
await Clipboard.setStringAsync(value);
|
||||
toast.success(t("login.code_copied"));
|
||||
}, [code, t]);
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
snapPoints={snapPoints}
|
||||
onChange={handleSheetChanges}
|
||||
handleIndicatorStyle={{ backgroundColor: "white" }}
|
||||
backgroundStyle={{ backgroundColor: "#171717" }}
|
||||
backdropComponent={renderBackdrop}
|
||||
>
|
||||
<BottomSheetView
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingLeft: Math.max(16, insets.left),
|
||||
paddingRight: Math.max(16, insets.right),
|
||||
paddingBottom: Math.max(16, insets.bottom),
|
||||
}}
|
||||
>
|
||||
<View className='flex-1'>
|
||||
<Text className='font-bold text-2xl text-neutral-100'>
|
||||
{t("login.quick_connect")}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
className='mt-6 p-6 border border-neutral-800 rounded-xl bg-neutral-900 flex flex-row items-center justify-center'
|
||||
onPress={copyCode}
|
||||
>
|
||||
<Text
|
||||
className='text-center font-bold text-5xl text-neutral-100'
|
||||
style={{ letterSpacing: 10 }}
|
||||
>
|
||||
{code ?? lastCodeRef.current}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name='copy-outline'
|
||||
size={22}
|
||||
color='white'
|
||||
style={{ opacity: 0.4, marginLeft: 16 }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text className='mt-2 text-neutral-500 text-center text-xs'>
|
||||
{t("login.tap_code_to_copy")}
|
||||
</Text>
|
||||
<Text className='mt-3 mb-5 text-neutral-400 text-center px-4'>
|
||||
{t("login.quick_connect_instructions")}
|
||||
</Text>
|
||||
<Button className='mt-auto' color='purple' onPress={onClose}>
|
||||
{t("login.got_it")}
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheetView>
|
||||
</BottomSheetModal>
|
||||
);
|
||||
};
|
||||
@@ -437,7 +437,7 @@ export const TVLogin: React.FC = () => {
|
||||
} else {
|
||||
Alert.alert(
|
||||
t("login.connection_failed"),
|
||||
t("login.an_unexpected_error_occured"),
|
||||
t("login.an_unexpected_error_occurred"),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -499,7 +499,7 @@ export const TVLogin: React.FC = () => {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("login.an_unexpected_error_occured");
|
||||
: t("login.an_unexpected_error_occurred");
|
||||
Alert.alert(t("login.connection_failed"), message);
|
||||
goToQRScreen();
|
||||
} finally {
|
||||
@@ -523,7 +523,7 @@ export const TVLogin: React.FC = () => {
|
||||
} else {
|
||||
Alert.alert(
|
||||
t("login.connection_failed"),
|
||||
t("login.an_unexpected_error_occured"),
|
||||
t("login.an_unexpected_error_occurred"),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -768,7 +768,7 @@ export const TVLogin: React.FC = () => {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("login.an_unexpected_error_occured");
|
||||
: t("login.an_unexpected_error_occurred");
|
||||
Alert.alert(t("login.connection_failed"), message);
|
||||
goToQRScreen();
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
@@ -5,11 +6,13 @@ import {
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { requireOptionalNativeModule } from "expo-modules-core";
|
||||
import { useAtom } from "jotai";
|
||||
import type React from "react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Alert, Platform, View, type ViewProps } from "react-native";
|
||||
import { Pressable } from "react-native-gesture-handler";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { Button } from "../Button";
|
||||
@@ -58,7 +61,7 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
||||
successHapticFeedback();
|
||||
Alert.alert(
|
||||
t("home.settings.quick_connect.success"),
|
||||
t("home.settings.quick_connect.quick_connect_autorized"),
|
||||
t("home.settings.quick_connect.quick_connect_authorized"),
|
||||
);
|
||||
setQuickConnectCode(undefined);
|
||||
bottomSheetModalRef?.current?.close();
|
||||
@@ -79,6 +82,15 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
||||
}
|
||||
}, [api, user, quickConnectCode]);
|
||||
|
||||
const pasteCode = useCallback(async () => {
|
||||
// Builds without the expo-clipboard native module: probe first (no-op).
|
||||
if (!requireOptionalNativeModule("ExpoClipboard")) return;
|
||||
const Clipboard = await import("expo-clipboard");
|
||||
const text = await Clipboard.getStringAsync();
|
||||
const digits = (text || "").replace(/\D/g, "").slice(0, 6);
|
||||
if (digits) setQuickConnectCode(digits);
|
||||
}, []);
|
||||
|
||||
if (isTv) return null;
|
||||
|
||||
return (
|
||||
@@ -130,6 +142,15 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
||||
style={{ paddingHorizontal: 16 }}
|
||||
autoFocus
|
||||
/>
|
||||
<Pressable
|
||||
onPress={pasteCode}
|
||||
className='flex-row items-center justify-center self-center'
|
||||
>
|
||||
<Feather name='clipboard' size={15} color='#a3a3a3' />
|
||||
<Text className='text-neutral-400 ml-2'>
|
||||
{t("home.settings.quick_connect.paste_code")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View } from "react-native";
|
||||
import { Alert, Platform, View } from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
@@ -12,6 +12,7 @@ import { ListItem } from "../list/ListItem";
|
||||
export const StorageSettings = () => {
|
||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const successHapticFeedback = useHaptic("success");
|
||||
const errorHapticFeedback = useHaptic("error");
|
||||
|
||||
@@ -27,16 +28,38 @@ export const StorageSettings = () => {
|
||||
used: (app.total - app.remaining) / app.total,
|
||||
};
|
||||
},
|
||||
// Keep the bar moving while a download is writing to disk.
|
||||
refetchInterval: 10 * 1000,
|
||||
});
|
||||
|
||||
const onDeleteClicked = async () => {
|
||||
try {
|
||||
await deleteAllFiles();
|
||||
successHapticFeedback();
|
||||
} catch (_e) {
|
||||
errorHapticFeedback();
|
||||
toast.error(t("home.settings.toasts.error_deleting_files"));
|
||||
}
|
||||
const onDeleteClicked = () => {
|
||||
Alert.alert(
|
||||
t("home.settings.storage.delete_all_downloaded_files_confirm"),
|
||||
t("home.settings.storage.delete_all_downloaded_files_confirm_desc"),
|
||||
[
|
||||
{
|
||||
text: t("common.cancel"),
|
||||
style: "cancel",
|
||||
},
|
||||
{
|
||||
text: t("common.ok"),
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
try {
|
||||
await deleteAllFiles();
|
||||
successHapticFeedback();
|
||||
} catch (_e) {
|
||||
errorHapticFeedback();
|
||||
toast.error(t("home.settings.toasts.error_deleting_files"));
|
||||
} finally {
|
||||
// Reflect the freed space immediately instead of waiting for
|
||||
// the next poll.
|
||||
queryClient.invalidateQueries({ queryKey: ["appSize"] });
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const calculatePercentage = (value: number, total: number) => {
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"expo-brightness": "~56.0.5",
|
||||
"expo-build-properties": "~56.0.18",
|
||||
"expo-camera": "~56.0.8",
|
||||
"expo-clipboard": "~56.0.4",
|
||||
"expo-constants": "~56.0.18",
|
||||
"expo-crypto": "~56.0.4",
|
||||
"expo-dev-client": "~56.0.20",
|
||||
|
||||
@@ -96,5 +96,24 @@ export function getDownloadedItemSize(id: string): number {
|
||||
*/
|
||||
export function calculateTotalDownloadedSize(): number {
|
||||
const items = getAllDownloadedItems();
|
||||
return items.reduce((sum, item) => sum + (item.videoFileSize || 0), 0);
|
||||
return items.reduce((sum, item) => {
|
||||
// Trickplay bytes count too — getDownloadedItemSize models per-item size
|
||||
// as video + trickplay, the total must match.
|
||||
const trickplaySize = item.trickPlayData?.size ?? 0;
|
||||
// Read the live file size on disk so the total reflects actual usage and
|
||||
// self-heals items whose stored videoFileSize is 0 (old schema, or
|
||||
// `fileInfo.size` was undefined at download time). Fall back to the stored
|
||||
// value if the file can't be stat'd.
|
||||
if (item.videoFilePath) {
|
||||
try {
|
||||
const file = new File(filePathToUri(item.videoFilePath));
|
||||
if (file.exists) {
|
||||
return sum + (file.size ?? item.videoFileSize ?? 0) + trickplaySize;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to stat downloaded file for size:", error);
|
||||
}
|
||||
}
|
||||
return sum + (item.videoFileSize ?? 0) + trickplaySize;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -289,7 +289,24 @@ export function useDownloadOperations({
|
||||
);
|
||||
|
||||
const appSizeUsage = useCallback(async () => {
|
||||
const totalSize = calculateTotalDownloadedSize();
|
||||
let totalSize = calculateTotalDownloadedSize();
|
||||
|
||||
// Also count in-progress downloads (they write straight to their final
|
||||
// path) so the growing file shows up as app usage instead of drifting
|
||||
// into the generic device share until completion.
|
||||
for (const process of processes) {
|
||||
try {
|
||||
const file = new File(
|
||||
Paths.document,
|
||||
`${generateFilename(process.item)}.mp4`,
|
||||
);
|
||||
if (file.exists) {
|
||||
totalSize += file.size ?? 0;
|
||||
}
|
||||
} catch {
|
||||
// File not created yet — ignore.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const [freeDiskStorage, totalDiskCapacity] = await Promise.all([
|
||||
@@ -310,7 +327,7 @@ export function useDownloadOperations({
|
||||
appSize: totalSize,
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
}, [processes]);
|
||||
|
||||
return {
|
||||
startBackgroundDownload,
|
||||
|
||||
@@ -386,7 +386,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
default:
|
||||
throw new Error(
|
||||
t(
|
||||
"login.an_unexpected_error_occured_did_you_enter_the_correct_url",
|
||||
"login.an_unexpected_error_occurred_did_you_enter_the_correct_url",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,18 +12,21 @@
|
||||
"login_button": "Log in",
|
||||
"quick_connect": "Quick Connect",
|
||||
"enter_code_to_login": "Enter code {{code}} to log in",
|
||||
"quick_connect_instructions": "Enter this code on a signed-in device — you'll be logged in automatically.",
|
||||
"tap_code_to_copy": "Tap the code to copy it",
|
||||
"code_copied": "Code copied",
|
||||
"failed_to_initiate_quick_connect": "Failed to initiate Quick Connect",
|
||||
"got_it": "Got it",
|
||||
"connection_failed": "Connection failed",
|
||||
"could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
|
||||
"an_unexpected_error_occured": "An unexpected error occurred",
|
||||
"an_unexpected_error_occurred": "An unexpected error occurred",
|
||||
"change_server": "Change server",
|
||||
"invalid_username_or_password": "Invalid username or password",
|
||||
"user_does_not_have_permission_to_log_in": "User does not have permission to log in",
|
||||
"server_is_taking_too_long_to_respond_try_again_later": "Server is taking too long to respond, try again later",
|
||||
"server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
|
||||
"there_is_a_server_error": "There is a server error",
|
||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
|
||||
"an_unexpected_error_occurred_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
|
||||
"too_old_server_text": "Unsupported Jellyfin server discovered",
|
||||
"too_old_server_description": "Please update Jellyfin to the latest version"
|
||||
},
|
||||
@@ -33,6 +36,7 @@
|
||||
"connect_button": "Connect",
|
||||
"previous_servers": "Previous servers",
|
||||
"clear_button": "Clear all",
|
||||
"server_url": "Server URL",
|
||||
"swipe_to_remove": "Swipe to remove",
|
||||
"search_for_local_servers": "Search for local servers",
|
||||
"searching": "Searching...",
|
||||
@@ -188,10 +192,11 @@
|
||||
"authorize_button": "Authorize Quick Connect",
|
||||
"enter_the_quick_connect_code": "Enter the Quick Connect code...",
|
||||
"success": "Success",
|
||||
"quick_connect_autorized": "Quick Connect authorized",
|
||||
"quick_connect_authorized": "Quick Connect authorized",
|
||||
"error": "Error",
|
||||
"invalid_code": "Invalid code",
|
||||
"authorize": "Authorize"
|
||||
"authorize": "Authorize",
|
||||
"paste_code": "Paste code"
|
||||
},
|
||||
"media_controls": {
|
||||
"media_controls_title": "Media controls",
|
||||
@@ -298,7 +303,7 @@
|
||||
"show_custom_menu_links": "Show custom menu links",
|
||||
"show_large_home_carousel": "Show large home carousel (beta)",
|
||||
"hide_libraries": "Hide libraries",
|
||||
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
||||
"select_libraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
||||
"disable_haptic_feedback": "Disable haptic feedback",
|
||||
"default_quality": "Default quality",
|
||||
"default_playback_speed": "Default playback speed",
|
||||
@@ -384,6 +389,8 @@
|
||||
"device_usage": "Device {{availableSpace}}%",
|
||||
"size_used": "{{used}} of {{total}} used",
|
||||
"delete_all_downloaded_files": "Delete all downloaded files",
|
||||
"delete_all_downloaded_files_confirm": "Delete All Downloaded Files?",
|
||||
"delete_all_downloaded_files_confirm_desc": "Are you sure you want to delete all downloaded files? This action cannot be undone.",
|
||||
"music_cache_title": "Music cache",
|
||||
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
|
||||
"clear_music_cache": "Clear music cache",
|
||||
@@ -598,7 +605,7 @@
|
||||
"mpv_player_title": "MPV player",
|
||||
"error": "Error",
|
||||
"failed_to_get_stream_url": "Failed to get the stream URL",
|
||||
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
||||
"an_error_occurred_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
|
||||
"client_error": "Client error",
|
||||
"could_not_create_stream_for_chromecast": "Could not create a stream for Chromecast",
|
||||
"message_from_server": "Message from server: {{message}}",
|
||||
|
||||
Reference in New Issue
Block a user