mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-28 12:01:53 +00:00
feat: choose download location (sd card)
This commit is contained in:
208
components/settings/StorageLocationPicker.tsx
Normal file
208
components/settings/StorageLocationPicker.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
type BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetScrollView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { forwardRef, useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { toast } from "sonner-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import type { StorageLocation } from "@/modules";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getAvailableStorageLocations } from "@/utils/storage";
|
||||
|
||||
interface StorageLocationPickerProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const StorageLocationPicker = forwardRef<
|
||||
BottomSheetModal,
|
||||
StorageLocationPickerProps
|
||||
>(({ onClose }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [selectedId, setSelectedId] = useState<string | undefined>(
|
||||
settings.downloadStorageLocation || "internal",
|
||||
);
|
||||
|
||||
const { data: locations, isLoading } = useQuery({
|
||||
queryKey: ["storageLocations"],
|
||||
queryFn: getAvailableStorageLocations,
|
||||
enabled: Platform.OS === "android",
|
||||
});
|
||||
|
||||
const handleSelect = (location: StorageLocation) => {
|
||||
setSelectedId(location.id);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
updateSettings({ downloadStorageLocation: selectedId });
|
||||
toast.success(
|
||||
t("settings.storage.storage_location_updated", {
|
||||
defaultValue: "Storage location updated",
|
||||
}),
|
||||
);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
if (Platform.OS !== "android") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={ref}
|
||||
enableDynamicSizing
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
backgroundStyle={{
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
enablePanDownToClose
|
||||
enableDismissOnClose
|
||||
>
|
||||
<BottomSheetScrollView
|
||||
style={{
|
||||
paddingLeft: Math.max(16, insets.left),
|
||||
paddingRight: Math.max(16, insets.right),
|
||||
}}
|
||||
>
|
||||
<View className='px-4 pt-2'>
|
||||
<Text className='text-lg font-semibold mb-1'>
|
||||
{t("settings.storage.select_storage_location", {
|
||||
defaultValue: "Select Storage Location",
|
||||
})}
|
||||
</Text>
|
||||
<Text className='text-sm text-neutral-500 mb-4'>
|
||||
{t("settings.storage.existing_downloads_note", {
|
||||
defaultValue:
|
||||
"Existing downloads will remain in their current location",
|
||||
})}
|
||||
</Text>
|
||||
|
||||
{isLoading ? (
|
||||
<View className='items-center justify-center py-8'>
|
||||
<ActivityIndicator size='large' />
|
||||
<Text className='mt-4 text-neutral-500'>
|
||||
{t("settings.storage.loading_storage", {
|
||||
defaultValue: "Loading storage options...",
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
) : !locations || locations.length === 0 ? (
|
||||
<View className='items-center justify-center py-8'>
|
||||
<Text className='text-neutral-500'>
|
||||
{t("settings.storage.no_storage_found", {
|
||||
defaultValue: "No storage locations found",
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{locations.map((location) => {
|
||||
const isSelected = selectedId === location.id;
|
||||
const freeSpaceGB = (location.freeSpace / 1024 ** 3).toFixed(2);
|
||||
const totalSpaceGB = (location.totalSpace / 1024 ** 3).toFixed(
|
||||
2,
|
||||
);
|
||||
const usedPercent = (
|
||||
((location.totalSpace - location.freeSpace) /
|
||||
location.totalSpace) *
|
||||
100
|
||||
).toFixed(0);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={location.id}
|
||||
onPress={() => handleSelect(location)}
|
||||
className={`p-4 mb-2 rounded-lg ${
|
||||
isSelected
|
||||
? "bg-purple-600/20 border border-purple-600"
|
||||
: "bg-neutral-800"
|
||||
}`}
|
||||
>
|
||||
<View className='flex-row items-center justify-between'>
|
||||
<View className='flex-1'>
|
||||
<View className='flex-row items-center'>
|
||||
<Text className='text-base font-semibold'>
|
||||
{location.label}
|
||||
</Text>
|
||||
{location.type === "external" && (
|
||||
<View className='ml-2 px-2 py-0.5 bg-blue-600/30 rounded'>
|
||||
<Text className='text-xs text-blue-400'>
|
||||
{t("settings.storage.removable", {
|
||||
defaultValue: "Removable",
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text className='text-sm text-neutral-500 mt-1'>
|
||||
{t("settings.storage.space_info", {
|
||||
defaultValue:
|
||||
"{{free}} GB free of {{total}} GB ({{used}}% used)",
|
||||
free: freeSpaceGB,
|
||||
total: totalSpaceGB,
|
||||
used: usedPercent,
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
{isSelected && (
|
||||
<View className='w-6 h-6 rounded-full bg-purple-600 items-center justify-center ml-2'>
|
||||
<Text className='text-white text-xs'>✓</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
|
||||
<View className='flex-row gap-x-2 py-4'>
|
||||
<TouchableOpacity
|
||||
onPress={onClose}
|
||||
className='flex-1 py-3 rounded-lg bg-neutral-800 items-center'
|
||||
>
|
||||
<Text className='text-white font-semibold'>
|
||||
{t("common.cancel", { defaultValue: "Cancel" })}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={handleConfirm}
|
||||
className='flex-1 py-3 rounded-lg bg-purple-600 items-center'
|
||||
disabled={!selectedId}
|
||||
>
|
||||
<Text className='text-white font-semibold'>
|
||||
{t("common.confirm", { defaultValue: "Confirm" })}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetModal>
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BottomSheetModal } from "@gorhom/bottom-sheet";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View } from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
@@ -6,14 +8,19 @@ import { Text } from "@/components/common/Text";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getStorageLabel } from "@/utils/storage";
|
||||
import { ListGroup } from "../list/ListGroup";
|
||||
import { ListItem } from "../list/ListItem";
|
||||
import { StorageLocationPicker } from "./StorageLocationPicker";
|
||||
|
||||
export const StorageSettings = () => {
|
||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||
const { settings } = useSettings();
|
||||
const { t } = useTranslation();
|
||||
const successHapticFeedback = useHaptic("success");
|
||||
const errorHapticFeedback = useHaptic("error");
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
|
||||
const { data: size } = useQuery({
|
||||
queryKey: ["appSize"],
|
||||
@@ -29,6 +36,12 @@ export const StorageSettings = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { data: storageLabel } = useQuery({
|
||||
queryKey: ["storageLabel", settings.downloadStorageLocation],
|
||||
queryFn: () => getStorageLabel(settings.downloadStorageLocation),
|
||||
enabled: Platform.OS === "android",
|
||||
});
|
||||
|
||||
const onDeleteClicked = async () => {
|
||||
try {
|
||||
await deleteAllFiles();
|
||||
@@ -102,14 +115,32 @@ export const StorageSettings = () => {
|
||||
</View>
|
||||
</View>
|
||||
{!Platform.isTV && (
|
||||
<ListGroup>
|
||||
<ListItem
|
||||
textColor='red'
|
||||
onPress={onDeleteClicked}
|
||||
title={t("home.settings.storage.delete_all_downloaded_files")}
|
||||
/>
|
||||
</ListGroup>
|
||||
<>
|
||||
{Platform.OS === "android" && (
|
||||
<ListGroup>
|
||||
<ListItem
|
||||
title={t("settings.storage.download_location", {
|
||||
defaultValue: "Download Location",
|
||||
})}
|
||||
value={storageLabel || "Internal Storage"}
|
||||
onPress={() => bottomSheetModalRef.current?.present()}
|
||||
/>
|
||||
</ListGroup>
|
||||
)}
|
||||
<ListGroup>
|
||||
<ListItem
|
||||
textColor='red'
|
||||
onPress={onDeleteClicked}
|
||||
title={t("home.settings.storage.delete_all_downloaded_files")}
|
||||
/>
|
||||
</ListGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
<StorageLocationPicker
|
||||
ref={bottomSheetModalRef}
|
||||
onClose={() => bottomSheetModalRef.current?.dismiss()}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user