This commit is contained in:
Fredrik Burmester
2024-10-01 22:59:33 +02:00
parent 0acc1f03f0
commit 1df7d8e8fe
7 changed files with 124 additions and 127 deletions

View File

@@ -43,11 +43,6 @@
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
]
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
"expo-font",

View File

@@ -23,6 +23,7 @@ import { useAtom } from "jotai";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
Platform,
RefreshControl,
SafeAreaView,
ScrollView,
@@ -345,7 +346,8 @@ export default function index() {
contentContainerStyle={{
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom: insets.bottom,
paddingBottom:
Platform.OS === "android" ? insets.bottom + 65 : insets.bottom,
}}
className="flex flex-col space-y-4"
>

View File

@@ -105,7 +105,6 @@ export default function settings() {
Haptics.notificationAsync(
Haptics.NotificationFeedbackType.Success
);
toast.success("All files deleted");
} catch (e) {
Haptics.notificationAsync(
Haptics.NotificationFeedbackType.Error

View File

@@ -1,7 +1,6 @@
import { DownloadProvider } from "@/providers/DownloadProvider";
import {
getOrSetDeviceId,
getServerUrlFromStorage,
getTokenFromStoraage,
JellyfinProvider,
} from "@/providers/JellyfinProvider";
@@ -9,36 +8,36 @@ import { JobQueueProvider } from "@/providers/JobQueueProvider";
import { PlaybackProvider } from "@/providers/PlaybackProvider";
import { orientationAtom } from "@/utils/atoms/orientation";
import { Settings, useSettings } from "@/utils/atoms/settings";
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import {
checkForExistingDownloads,
completeHandler,
download,
} from "@kesha-antonov/react-native-background-downloader";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { DarkTheme, ThemeProvider } from "@react-navigation/native";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import * as BackgroundFetch from "expo-background-fetch";
import * as FileSystem from "expo-file-system";
import { useFonts } from "expo-font";
import { useKeepAwake } from "expo-keep-awake";
import * as Linking from "expo-linking";
import * as Notifications from "expo-notifications";
import { router, Stack } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation";
import * as SplashScreen from "expo-splash-screen";
import { StatusBar } from "expo-status-bar";
import * as TaskManager from "expo-task-manager";
import { Provider as JotaiProvider, useAtom } from "jotai";
import { useEffect, useRef } from "react";
import { AppState } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import "react-native-reanimated";
import { Toaster } from "sonner-native";
import * as TaskManager from "expo-task-manager";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as BackgroundFetch from "expo-background-fetch";
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
import * as FileSystem from "expo-file-system";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import * as Notifications from "expo-notifications";
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
SplashScreen.preventAutoHideAsync();
@@ -145,16 +144,6 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
})
.begin(() => {
console.log("TaskManager ~ Download started: ", job.id);
Notifications.scheduleNotificationAsync({
content: {
title: job.item.Name,
body: "Download started",
data: {
url: `/downloads`,
},
},
trigger: null,
});
})
.done(() => {
console.log("TaskManager ~ Download completed: ", job.id);

View File

@@ -1,13 +1,18 @@
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
import {
DefaultLanguageOption,
DownloadOptions,
ScreenOrientationEnum,
useSettings,
} from "@/utils/atoms/settings";
BACKGROUND_FETCH_TASK,
registerBackgroundFetchAsync,
unregisterBackgroundFetchAsync,
} from "@/utils/background-tasks";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import * as BackgroundFetch from "expo-background-fetch";
import * as ScreenOrientation from "expo-screen-orientation";
import * as TaskManager from "expo-task-manager";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import {
ActivityIndicator,
Linking,
@@ -16,23 +21,13 @@ import {
View,
ViewProps,
} from "react-native";
import { toast } from "sonner-native";
import * as DropdownMenu from "zeego/dropdown-menu";
import { Button } from "../Button";
import { Input } from "../common/Input";
import { Text } from "../common/Text";
import { Loader } from "../Loader";
import { Input } from "../common/Input";
import { useEffect, useState } from "react";
import { Button } from "../Button";
import { MediaToggles } from "./MediaToggles";
import * as ScreenOrientation from "expo-screen-orientation";
import { opacity } from "react-native-reanimated/lib/typescript/reanimated2/Colors";
import { useDownload } from "@/providers/DownloadProvider";
import * as BackgroundFetch from "expo-background-fetch";
import * as TaskManager from "expo-task-manager";
import {
BACKGROUND_FETCH_TASK,
registerBackgroundFetchAsync,
unregisterBackgroundFetchAsync,
} from "@/utils/background-tasks";
interface Props extends ViewProps {}
@@ -52,40 +47,24 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
/********************
* Background task
*******************/
const [isRegistered, setIsRegistered] = useState<boolean | null>(null);
const [status, setStatus] =
useState<BackgroundFetch.BackgroundFetchStatus | null>(null);
useEffect(() => {
checkStatusAsync();
}, []);
const checkStatusAsync = async () => {
const status = await BackgroundFetch.getStatusAsync();
const isRegistered = await TaskManager.isTaskRegisteredAsync(
BACKGROUND_FETCH_TASK
);
setStatus(status);
setIsRegistered(isRegistered);
await BackgroundFetch.getStatusAsync();
await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK);
};
const toggleFetchTask = async () => {
if (isRegistered) {
console.log("Unregistering task");
await unregisterBackgroundFetchAsync();
updateSettings({
autoDownload: false,
});
useEffect(() => {
if (settings?.autoDownload) {
registerBackgroundFetchAsync();
} else {
console.log("Registering task");
await registerBackgroundFetchAsync();
updateSettings({
autoDownload: true,
});
unregisterBackgroundFetchAsync();
}
checkStatusAsync();
};
}, [settings?.autoDownload]);
/**********************
*********************/
@@ -571,14 +550,10 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
finished optimizing on the server.
</Text>
</View>
{isRegistered === null ? (
<ActivityIndicator size="small" color="white" />
) : (
<Switch
value={isRegistered}
onValueChange={(value) => toggleFetchTask()}
/>
)}
<Switch
value={settings.autoDownload}
onValueChange={(value) => updateSettings({ autoDownload: value })}
/>
</View>
<View
pointerEvents={
@@ -612,6 +587,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
color="purple"
className="h-12 mt-2"
onPress={() => {
toast.success("Saved");
updateSettings({
optimizedVersionsServerUrl:
optimizedVersionsServerUrl.length === 0

View File

@@ -1,6 +1,7 @@
{
"cli": {
"version": ">= 9.1.0"
"version": ">= 9.1.0",
"appVersionSource": "local"
},
"build": {
"development": {

View File

@@ -37,6 +37,7 @@ import React, {
import { AppState, AppStateStatus } from "react-native";
import { toast } from "sonner-native";
import { apiAtom } from "./JellyfinProvider";
import * as Notifications from "expo-notifications";
function onAppStateChange(status: AppStateStatus) {
focusManager.setFocused(status === "active");
@@ -93,7 +94,20 @@ function useDownloadProvider() {
url,
});
jobs.forEach((job) => {
// Local downloading processes that are still valid
const downloadingProcesses = processes
.filter((p) => p.status === "downloading")
.filter((p) => jobs.some((j) => j.id === p.id));
const updatedProcesses = jobs.filter(
(j) => !downloadingProcesses.some((p) => p.id === j.id)
);
setProcesses([...updatedProcesses, ...downloadingProcesses]);
// Go though new jobs and compare them to old jobs
// if new job is now completed, start download.
for (let job of jobs) {
const process = processes.find((p) => p.id === job.id);
if (
process &&
@@ -112,20 +126,19 @@ function useDownloadProvider() {
},
},
});
Notifications.scheduleNotificationAsync({
content: {
title: job.item.Name,
body: `${job.item.Name} is ready to be downloaded`,
data: {
url: `/downloads`,
},
},
trigger: null,
});
}
}
});
// Local downloading processes that are still valid
const downloadingProcesses = processes
.filter((p) => p.status === "downloading")
.filter((p) => jobs.some((j) => j.id === p.id));
const updatedProcesses = jobs.filter(
(j) => !downloadingProcesses.some((p) => p.id === j.id)
);
setProcesses([...updatedProcesses, ...downloadingProcesses]);
}
return jobs;
},
@@ -137,19 +150,7 @@ function useDownloadProvider() {
useEffect(() => {
const checkIfShouldStartDownload = async () => {
if (processes.length === 0) return;
const tasks = await checkForExistingDownloads();
// if (settings?.autoDownload) {
// for (let i = 0; i < processes.length; i++) {
// const job = processes[i];
// if (job.status === "completed") {
// // Check if the download is already in progress
// if (tasks.find((task) => task.id === job.id)) continue;
// await startDownload(job);
// continue;
// }
// }
// }
await checkForExistingDownloads();
};
checkIfShouldStartDownload();
@@ -178,6 +179,7 @@ function useDownloadProvider() {
async (process: JobStatus) => {
if (!process?.item.Id || !authHeader) throw new Error("No item id");
console.log("[0] Setting process to downloading");
setProcesses((prev) =>
prev.map((p) =>
p.id === process.id
@@ -352,38 +354,71 @@ function useDownloadProvider() {
const deleteAllFiles = async (): Promise<void> => {
try {
const baseDirectory = FileSystem.documentDirectory;
await deleteLocalFiles();
await removeDownloadedItemsFromStorage();
await cancelAllServerJobs();
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] });
toast.success("All files, folders, and jobs deleted successfully");
} catch (error) {
console.error("Failed to delete all files, folders, and jobs:", error);
toast.error("An error occurred while deleting files and jobs");
}
};
if (!baseDirectory) {
throw new Error("Base directory not found");
}
const deleteLocalFiles = async (): Promise<void> => {
const baseDirectory = FileSystem.documentDirectory;
if (!baseDirectory) {
throw new Error("Base directory not found");
}
const dirContents = await FileSystem.readDirectoryAsync(baseDirectory);
for (const item of dirContents) {
const itemPath = `${baseDirectory}${item}`;
const itemInfo = await FileSystem.getInfoAsync(itemPath);
if (itemInfo.exists) {
const dirContents = await FileSystem.readDirectoryAsync(baseDirectory);
for (const item of dirContents) {
const itemPath = `${baseDirectory}${item}`;
const itemInfo = await FileSystem.getInfoAsync(itemPath);
if (itemInfo.exists) {
if (itemInfo.isDirectory) {
await FileSystem.deleteAsync(itemPath, { idempotent: true });
} else {
await FileSystem.deleteAsync(itemPath, { idempotent: true });
}
}
}
};
const removeDownloadedItemsFromStorage = async (): Promise<void> => {
try {
await AsyncStorage.removeItem("downloadedItems");
if (!authHeader) throw new Error("No auth header");
if (!settings?.optimizedVersionsServerUrl)
throw new Error("No server url");
cancelAllJobs({
authHeader,
url: settings?.optimizedVersionsServerUrl,
deviceId: await getOrSetDeviceId(),
});
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] });
toast.success("All files and folders deleted successfully");
} catch (error) {
console.error("Failed to delete all files and folders:", error);
console.error(
"Failed to remove downloadedItems from AsyncStorage:",
error
);
throw error;
}
};
const cancelAllServerJobs = async (): Promise<void> => {
if (!authHeader) {
throw new Error("No auth header available");
}
if (!settings?.optimizedVersionsServerUrl) {
throw new Error("No server URL configured");
}
const deviceId = await getOrSetDeviceId();
if (!deviceId) {
throw new Error("Failed to get device ID");
}
try {
await cancelAllJobs({
authHeader,
url: settings.optimizedVersionsServerUrl,
deviceId,
});
} catch (error) {
console.error("Failed to cancel all server jobs:", error);
throw error;
}
};