fix: change to mmkv and fix downloads with VLC

This commit is contained in:
Fredrik Burmester
2024-11-24 19:34:49 +01:00
parent 335765993d
commit 93d117640a
12 changed files with 191 additions and 172 deletions

View File

@@ -6,32 +6,42 @@ import { DownloadedItem, useDownload } from "@/providers/DownloadProvider";
import { queueAtom } from "@/utils/atoms/queue";
import { useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { router } from "expo-router";
import { useRouter } from "expo-router";
import { useAtom } from "jotai";
import { useMemo } from "react";
import { ScrollView, TouchableOpacity, View } from "react-native";
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const downloads: React.FC = () => {
export default function page() {
const [queue, setQueue] = useAtom(queueAtom);
const { removeProcess, downloadedFiles } = useDownload();
const router = useRouter();
const [settings] = useSettings();
const movies = useMemo(
() => downloadedFiles?.filter((f) => f.item.Type === "Movie") || [],
[downloadedFiles]
);
const movies = useMemo(() => {
try {
return downloadedFiles?.filter((f) => f.item.Type === "Movie") || [];
} catch {
migration_20241124();
return [];
}
}, [downloadedFiles]);
const groupedBySeries = useMemo(() => {
const episodes = downloadedFiles?.filter((f) => f.item.Type === "Episode");
const series: { [key: string]: DownloadedItem[] } = {};
episodes?.forEach((e) => {
if (!series[e.item.SeriesName!]) series[e.item.SeriesName!] = [];
series[e.item.SeriesName!].push(e);
});
return Object.values(series);
try {
const episodes = downloadedFiles?.filter(
(f) => f.item.Type === "Episode"
);
const series: { [key: string]: DownloadedItem[] } = {};
episodes?.forEach((e) => {
if (!series[e.item.SeriesName!]) series[e.item.SeriesName!] = [];
series[e.item.SeriesName!].push(e);
});
return Object.values(series);
} catch {
migration_20241124();
return [];
}
}, [downloadedFiles]);
const insets = useSafeAreaInsets();
@@ -121,6 +131,24 @@ const downloads: React.FC = () => {
</View>
</ScrollView>
);
};
}
export default downloads;
function migration_20241124() {
const router = useRouter();
const { deleteAllFiles } = useDownload();
Alert.alert(
"New app version requires re-download",
"The new update reqires content to be downloaded again. Please remove all downloaded content and try again.",
[
{
text: "Back",
onPress: () => router.back(),
},
{
text: "Delete",
style: "destructive",
onPress: async () => await deleteAllFiles(),
},
]
);
}

View File

@@ -119,14 +119,14 @@ export default function page() {
queryFn: async () => {
console.log("Offline:", offline);
if (offline) {
const item = await getDownloadedItem(itemId);
if (!item?.mediaSource) return null;
const data = await getDownloadedItem(itemId);
if (!data?.mediaSource) return null;
const url = await getDownloadedFileUrl(item.item.Id!);
const url = await getDownloadedFileUrl(data.item.Id!);
if (item)
return {
mediaSource: item.mediaSource,
mediaSource: data.mediaSource,
url,
sessionId: undefined,
};
@@ -165,13 +165,13 @@ export default function page() {
const togglePlay = useCallback(
async (ms: number) => {
if (!api || !stream) return;
if (!api) return;
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
if (isPlaying) {
await videoRef.current?.pause();
if (!offline) {
if (!offline && stream) {
await getPlaystateApi(api).onPlaybackProgress({
itemId: item?.Id!,
audioStreamIndex: audioIndex ? audioIndex : undefined,
@@ -189,7 +189,7 @@ export default function page() {
console.log("Actually marked as paused");
} else {
videoRef.current?.play();
if (!offline) {
if (!offline && stream) {
await getPlaystateApi(api).onPlaybackProgress({
itemId: item?.Id!,
audioStreamIndex: audioIndex ? audioIndex : undefined,
@@ -235,6 +235,7 @@ export default function page() {
const reportPlaybackStopped = useCallback(async () => {
if (offline) return;
const currentTimeInTicks = msToTicks(progress.value);
await getPlaystateApi(api!).onPlaybackStopped({
itemId: item?.Id!,
@@ -245,9 +246,10 @@ export default function page() {
}, [api, item, mediaSourceId, stream]);
const reportPlaybackStart = useCallback(async () => {
if (!api || !stream) return;
if (offline) return;
await getPlaystateApi(api).onPlaybackStart({
if (!stream) return;
await getPlaystateApi(api!).onPlaybackStart({
itemId: item?.Id!,
audioStreamIndex: audioIndex ? audioIndex : undefined,
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
@@ -261,7 +263,6 @@ export default function page() {
async (data: ProgressUpdatePayload) => {
if (isSeeking.value === true) return;
if (isPlaybackStopped === true) return;
if (!item?.Id || !api || !stream) return;
const { currentTime } = data.nativeEvent;
@@ -275,7 +276,9 @@ export default function page() {
const currentTimeInTicks = msToTicks(currentTime);
await getPlaystateApi(api).onPlaybackProgress({
if (!item?.Id || !stream) return;
await getPlaystateApi(api!).onPlaybackProgress({
itemId: item.Id,
audioStreamIndex: audioIndex ? audioIndex : undefined,
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
@@ -343,21 +346,20 @@ export default function page() {
</View>
);
if (!stream || !item) return null;
// Preselection of audio and subtitle tracks.
let initOptions = ["--sub-text-scale=60"];
let externalTrack = { name: "", DeliveryUrl: "" };
const allSubs =
stream.mediaSource.MediaStreams?.filter((sub) => sub.Type === "Subtitle") ||
[];
stream?.mediaSource.MediaStreams?.filter(
(sub) => sub.Type === "Subtitle"
) || [];
const chosenSubtitleTrack = allSubs.find(
(sub) => sub.Index === subtitleIndex
);
const allAudio =
stream.mediaSource.MediaStreams?.filter(
stream?.mediaSource.MediaStreams?.filter(
(audio) => audio.Type === "Audio"
) || [];
const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex);
@@ -375,9 +377,8 @@ export default function page() {
};
}
if (!chosenAudioTrack) throw new Error("No audio track found");
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
if (chosenAudioTrack)
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
} else {
// Transcoded playback CASE
if (chosenSubtitleTrack?.DeliveryMethod === "Hls") {
@@ -388,6 +389,15 @@ export default function page() {
}
}
if (!item || !stream)
return (
<View className="w-screen h-screen flex flex-col items-center justify-center bg-black">
<Text className="text-white">
<Loader />
</Text>
</View>
);
return (
<View
style={{
@@ -434,7 +444,7 @@ export default function page() {
{videoRef.current && (
<Controls
mediaSource={stream.mediaSource}
mediaSource={stream?.mediaSource}
item={item}
videoRef={videoRef}
togglePlay={togglePlay}

View File

@@ -1,7 +1,7 @@
import { DownloadProvider } from "@/providers/DownloadProvider";
import {
getOrSetDeviceId,
getTokenFromStoraage,
getTokenFromStorage,
JellyfinProvider,
} from "@/providers/JellyfinProvider";
import { JobQueueProvider } from "@/providers/JobQueueProvider";
@@ -10,6 +10,7 @@ import { orientationAtom } from "@/utils/atoms/orientation";
import { Settings, useSettings } from "@/utils/atoms/settings";
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
import { writeToLog } from "@/utils/log";
import { storage } from "@/utils/mmkv";
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
@@ -19,7 +20,6 @@ import {
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";
@@ -35,10 +35,10 @@ import * as TaskManager from "expo-task-manager";
import { Provider as JotaiProvider, useAtom } from "jotai";
import { useEffect, useRef } from "react";
import { Appearance, AppState } from "react-native";
import { SystemBars } from "react-native-edge-to-edge";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import "react-native-reanimated";
import { Toaster } from "sonner-native";
import { SystemBars } from "react-native-edge-to-edge";
SplashScreen.preventAutoHideAsync();
@@ -86,7 +86,7 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
const now = Date.now();
const settingsData = await AsyncStorage.getItem("settings");
const settingsData = storage.getString("settings");
if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData;
@@ -96,8 +96,8 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
if (!settings?.autoDownload || !url)
return BackgroundFetch.BackgroundFetchResult.NoData;
const token = await getTokenFromStoraage();
const deviceId = await getOrSetDeviceId();
const token = getTokenFromStorage();
const deviceId = getOrSetDeviceId();
const baseDirectory = FileSystem.documentDirectory;
if (!token || !deviceId || !baseDirectory)
@@ -177,7 +177,7 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
const checkAndRequestPermissions = async () => {
try {
const hasAskedBefore = await AsyncStorage.getItem(
const hasAskedBefore = storage.getString(
"hasAskedForNotificationPermission"
);
@@ -192,7 +192,7 @@ const checkAndRequestPermissions = async () => {
console.log("Notification permissions denied.");
}
await AsyncStorage.setItem("hasAskedForNotificationPermission", "true");
storage.set("hasAskedForNotificationPermission", "true");
} else {
console.log("Already asked for notification permissions before.");
}
@@ -365,9 +365,9 @@ function Layout() {
);
}
async function saveDownloadedItemInfo(item: BaseItemDto) {
function saveDownloadedItemInfo(item: BaseItemDto) {
try {
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
const downloadedItems = storage.getString("downloadedItems");
let items: BaseItemDto[] = downloadedItems
? JSON.parse(downloadedItems)
: [];
@@ -379,7 +379,7 @@ async function saveDownloadedItemInfo(item: BaseItemDto) {
items.push(item);
}
await AsyncStorage.setItem("downloadedItems", JSON.stringify(items));
storage.set("downloadedItems", JSON.stringify(items));
} catch (error) {
writeToLog("ERROR", "Failed to save downloaded item information:", error);
console.error("Failed to save downloaded item information:", error);

View File

@@ -1,12 +1,10 @@
import { useState, useCallback } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as FileSystem from "expo-file-system";
import { storage } from "@/utils/mmkv";
import { useCallback } from "react";
const useImageStorage = () => {
const saveBase64Image = useCallback(async (base64: string, key: string) => {
try {
// Save the base64 string to AsyncStorage
// Save the base64 string to storage
storage.set(key, base64);
} catch (error) {
console.error("Error saving image:", error);
@@ -69,7 +67,7 @@ const useImageStorage = () => {
const loadImage = useCallback(async (key: string) => {
try {
// Retrieve the base64 string from AsyncStorage
// Retrieve the base64 string from storage
const base64Image = storage.getString(key);
if (base64Image !== null) {
// Set the loaded image state

View File

@@ -1,21 +1,19 @@
import { useCallback } from "react";
import { useAtom, useAtomValue } from "jotai";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as FileSystem from "expo-file-system";
import { FFmpegKit, FFmpegKitConfig } from "ffmpeg-kit-react-native";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getItemImage } from "@/utils/getItemImage";
import { writeToLog } from "@/utils/log";
import {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models";
import { writeToLog } from "@/utils/log";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner-native";
import { useDownload } from "@/providers/DownloadProvider";
import * as FileSystem from "expo-file-system";
import { useRouter } from "expo-router";
import { JobStatus } from "@/utils/optimize-server";
import { FFmpegKit, FFmpegKitConfig } from "ffmpeg-kit-react-native";
import { useAtomValue } from "jotai";
import { useCallback } from "react";
import { toast } from "sonner-native";
import useImageStorage from "./useImageStorage";
import { getItemImage } from "@/utils/getItemImage";
import { apiAtom } from "@/providers/JellyfinProvider";
/**
* Custom hook for remuxing HLS to MP4 using FFmpeg.

View File

@@ -19,7 +19,7 @@ import {
download,
setConfig,
} from "@kesha-antonov/react-native-background-downloader";
import AsyncStorage from "@react-native-async-storage/async-storage";
import MMKV from "react-native-mmkv";
import {
focusManager,
QueryClient,
@@ -45,6 +45,7 @@ import { apiAtom } from "./JellyfinProvider";
import * as Notifications from "expo-notifications";
import { getItemImage } from "@/utils/getItemImage";
import useImageStorage from "@/hooks/useImageStorage";
import { storage } from "@/utils/mmkv";
export type DownloadedItem = {
item: Partial<BaseItemDto>;
@@ -109,7 +110,6 @@ function useDownloadProvider() {
url,
});
// Local downloading processes that are still valid
const downloadingProcesses = processes
.filter((p) => p.status === "downloading")
.filter((p) => jobs.some((j) => j.id === p.id));
@@ -120,8 +120,6 @@ function useDownloadProvider() {
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 (
@@ -314,7 +312,6 @@ function useDownloadProvider() {
const fileExtension = mediaSource.TranscodingContainer;
const deviceId = await getOrSetDeviceId();
// Save poster to disk
const itemImage = getItemImage({
item,
api,
@@ -324,7 +321,6 @@ function useDownloadProvider() {
});
await saveImage(item.Id, itemImage?.uri);
// POST to start optimization job on the server
const response = await axios.post(
settings?.optimizedVersionsServerUrl + "optimize-version",
{
@@ -391,7 +387,7 @@ function useDownloadProvider() {
const deleteAllFiles = async (): Promise<void> => {
try {
await deleteLocalFiles();
await removeDownloadedItemsFromStorage();
removeDownloadedItemsFromStorage();
await cancelAllServerJobs();
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] });
toast.success("All files, folders, and jobs deleted successfully");
@@ -421,12 +417,12 @@ function useDownloadProvider() {
}
};
const removeDownloadedItemsFromStorage = async (): Promise<void> => {
const removeDownloadedItemsFromStorage = (): void => {
try {
await AsyncStorage.removeItem("downloadedItems");
storage.delete("downloadedItems");
} catch (error) {
console.error(
"Failed to remove downloadedItems from AsyncStorage:",
"Failed to remove downloadedItems from storage:",
error
);
throw error;
@@ -482,27 +478,25 @@ function useDownloadProvider() {
}
}
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
const downloadedItems = storage.getString("downloadedItems");
if (downloadedItems) {
let items = JSON.parse(downloadedItems);
items = items.filter((item: any) => item.Id !== id);
await AsyncStorage.setItem("downloadedItems", JSON.stringify(items));
storage.set("downloadedItems", JSON.stringify(items));
}
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] });
} catch (error) {
console.error(
`Failed to delete file and AsyncStorage entry for ID ${id}:`,
`Failed to delete file and storage entry for ID ${id}:`,
error
);
}
};
async function getDownloadedItem(
itemId: string
): Promise<DownloadedItem | null> {
function getDownloadedItem(itemId: string): DownloadedItem | null {
try {
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
const downloadedItems = storage.getString("downloadedItems");
if (downloadedItems) {
const items: DownloadedItem[] = JSON.parse(downloadedItems);
const item = items.find((i) => i.item.Id === itemId);
@@ -515,9 +509,9 @@ function useDownloadProvider() {
}
}
async function getAllDownloadedItems(): Promise<DownloadedItem[]> {
function getAllDownloadedItems(): DownloadedItem[] {
try {
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
const downloadedItems = storage.getString("downloadedItems");
if (downloadedItems) {
return JSON.parse(downloadedItems) as DownloadedItem[];
} else {
@@ -529,9 +523,9 @@ function useDownloadProvider() {
}
}
async function saveDownloadedItemInfo(item: BaseItemDto) {
function saveDownloadedItemInfo(item: BaseItemDto) {
try {
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
const downloadedItems = storage.getString("downloadedItems");
let items: DownloadedItem[] = downloadedItems
? JSON.parse(downloadedItems)
: [];
@@ -555,8 +549,8 @@ function useDownloadProvider() {
deleteDownloadItemInfoFromDiskTmp(item.Id!);
await AsyncStorage.setItem("downloadedItems", JSON.stringify(items));
await queryClient.invalidateQueries({ queryKey: ["downloadedItems"] });
storage.set("downloadedItems", JSON.stringify(items));
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] });
refetch();
} catch (error) {
console.error(

View File

@@ -2,7 +2,6 @@ import { useInterval } from "@/hooks/useInterval";
import { Api, Jellyfin } from "@jellyfin/sdk";
import { UserDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getUserApi } from "@jellyfin/sdk/lib/utils/api";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useMutation, useQuery } from "@tanstack/react-query";
import axios, { AxiosError } from "axios";
import { router, useSegments } from "expo-router";
@@ -18,6 +17,8 @@ import React, {
} from "react";
import { Platform } from "react-native";
import uuid from "react-native-uuid";
import MMKV from "react-native-mmkv";
import { storage } from "@/utils/mmkv";
interface Server {
address: string;
@@ -48,7 +49,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
useEffect(() => {
(async () => {
const id = await getOrSetDeviceId();
const id = getOrSetDeviceId();
setJellyfin(
() =>
new Jellyfin({
@@ -138,8 +139,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
const { AccessToken, User } = authResponse.data;
api.accessToken = AccessToken;
setUser(User);
await AsyncStorage.setItem("token", AccessToken);
await AsyncStorage.setItem("user", JSON.stringify(User));
storage.set("token", AccessToken);
storage.set("user", JSON.stringify(User));
return true;
}
}
@@ -172,7 +173,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
if (!apiInstance?.basePath) throw new Error("Failed to connect");
setApi(apiInstance);
await AsyncStorage.setItem("serverUrl", server.address);
storage.set("serverUrl", server.address);
},
onError: (error) => {
console.error("Failed to set server:", error);
@@ -181,7 +182,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
const removeServerMutation = useMutation({
mutationFn: async () => {
await AsyncStorage.removeItem("serverUrl");
storage.delete("serverUrl");
setApi(null);
},
onError: (error) => {
@@ -204,9 +205,9 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
if (auth.data.AccessToken && auth.data.User) {
setUser(auth.data.User);
await AsyncStorage.setItem("user", JSON.stringify(auth.data.User));
storage.set("user", JSON.stringify(auth.data.User));
setApi(jellyfin.createApi(api?.basePath, auth.data?.AccessToken));
await AsyncStorage.setItem("token", auth.data?.AccessToken);
storage.set("token", auth.data?.AccessToken);
}
} catch (error) {
if (axios.isAxiosError(error)) {
@@ -241,7 +242,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
const logoutMutation = useMutation({
mutationFn: async () => {
await AsyncStorage.removeItem("token");
storage.delete("token");
setUser(null);
},
onError: (error) => {
@@ -258,13 +259,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
],
queryFn: async () => {
try {
const token = await getTokenFromStoraage();
const serverUrl = await getServerUrlFromStorage();
const user = JSON.parse(
(await getUserFromStorage()) as string
) as UserDto;
if (serverUrl && token && user.Id && jellyfin) {
const token = getTokenFromStorage();
const serverUrl = getServerUrlFromStorage();
const user = getUserFromStorage();
if (serverUrl && token && user?.Id && jellyfin) {
const apiInstance = jellyfin.createApi(serverUrl, token);
setApi(apiInstance);
setUser(user);
@@ -273,6 +271,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
return true;
} catch (e) {
console.error(e);
return false;
}
},
staleTime: 0,
@@ -321,24 +320,32 @@ function useProtectedRoute(user: UserDto | null, loading = false) {
}, [user, segments, loading]);
}
export async function getTokenFromStoraage() {
return await AsyncStorage.getItem("token");
export function getTokenFromStorage(): string | null {
return storage.getString("token") || null;
}
export async function getUserFromStorage() {
return await AsyncStorage.getItem("user");
export function getUserFromStorage(): UserDto | null {
const userStr = storage.getString("user");
if (userStr) {
try {
return JSON.parse(userStr) as UserDto;
} catch (e) {
console.error(e);
}
}
return null;
}
export async function getServerUrlFromStorage() {
return await AsyncStorage.getItem("serverUrl");
export function getServerUrlFromStorage(): string | null {
return storage.getString("serverUrl") || null;
}
export async function getOrSetDeviceId() {
let deviceId = await AsyncStorage.getItem("deviceId");
export function getOrSetDeviceId(): string {
let deviceId = storage.getString("deviceId");
if (!deviceId) {
deviceId = uuid.v4() as string;
await AsyncStorage.setItem("deviceId", deviceId);
storage.set("deviceId", deviceId);
}
return deviceId;

View File

@@ -1,6 +1,6 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { atom } from "jotai";
import { atomWithStorage, createJSONStorage } from "jotai/utils";
import { atomWithStorage } from "jotai/utils";
import { storage } from "../mmkv";
export enum SortByOption {
Default = "Default",
@@ -68,9 +68,6 @@ export const sortOrderAtom = atom<SortOrderOption[]>([
SortOrderOption.Ascending,
]);
/**
* Sort preferences with persistence
*/
export interface SortPreference {
[libraryId: string]: SortByOption;
}
@@ -86,15 +83,15 @@ export const sortByPreferenceAtom = atomWithStorage<SortPreference>(
"sortByPreference",
defaultSortPreference,
{
getItem: async (key) => {
const value = await AsyncStorage.getItem(key);
getItem: (key) => {
const value = storage.getString(key);
return value ? JSON.parse(value) : null;
},
setItem: async (key, value) => {
await AsyncStorage.setItem(key, JSON.stringify(value));
setItem: (key, value) => {
storage.set(key, JSON.stringify(value));
},
removeItem: async (key) => {
await AsyncStorage.removeItem(key);
removeItem: (key) => {
storage.delete(key);
},
}
);
@@ -103,20 +100,19 @@ export const sortOrderPreferenceAtom = atomWithStorage<SortOrderPreference>(
"sortOrderPreference",
defaultSortOrderPreference,
{
getItem: async (key) => {
const value = await AsyncStorage.getItem(key);
getItem: (key) => {
const value = storage.getString(key);
return value ? JSON.parse(value) : null;
},
setItem: async (key, value) => {
await AsyncStorage.setItem(key, JSON.stringify(value));
setItem: (key, value) => {
storage.set(key, JSON.stringify(value));
},
removeItem: async (key) => {
await AsyncStorage.removeItem(key);
removeItem: (key) => {
storage.delete(key);
},
}
);
// Helper functions to get and set sort preferences
export const getSortByPreference = (
libraryId: string,
preferences: SortPreference
@@ -130,4 +126,3 @@ export const getSortOrderPreference = (
) => {
return preferences?.[libraryId] || null;
};

View File

@@ -1,7 +1,5 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { atom, useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { useEffect } from "react";
export interface Job {

View File

@@ -1,7 +1,7 @@
import { atom, useAtom } from "jotai";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useEffect } from "react";
import * as ScreenOrientation from "expo-screen-orientation";
import { storage } from "../mmkv";
export type DownloadQuality = "original" | "high" | "low";
@@ -75,15 +75,8 @@ export type Settings = {
downloadMethod: "optimized" | "remux";
autoDownload: boolean;
};
/**
*
* The settings atom is a Jotai atom that stores the user's settings.
* It is initialized with a default value of null, which indicates that the settings have not been loaded yet.
* The settings are loaded from AsyncStorage when the atom is read for the first time.
*
*/
const loadSettings = async (): Promise<Settings> => {
const loadSettings = (): Settings => {
const defaultValues: Settings = {
autoRotate: true,
forceLandscapeInVideoPlayer: false,
@@ -113,7 +106,7 @@ const loadSettings = async (): Promise<Settings> => {
};
try {
const jsonValue = await AsyncStorage.getItem("settings");
const jsonValue = storage.getString("settings");
const loadedValues: Partial<Settings> =
jsonValue != null ? JSON.parse(jsonValue) : {};
@@ -124,30 +117,28 @@ const loadSettings = async (): Promise<Settings> => {
}
};
// Utility function to save settings to AsyncStorage
const saveSettings = async (settings: Settings) => {
const saveSettings = (settings: Settings) => {
const jsonValue = JSON.stringify(settings);
await AsyncStorage.setItem("settings", jsonValue);
storage.set("settings", jsonValue);
};
// Create an atom to store the settings in memory
export const settingsAtom = atom<Settings | null>(null);
// A hook to manage settings, loading them on initial mount and providing a way to update them
export const useSettings = () => {
const [settings, setSettings] = useAtom(settingsAtom);
useEffect(() => {
if (settings === null) {
loadSettings().then(setSettings);
const loadedSettings = loadSettings();
setSettings(loadedSettings);
}
}, [settings, setSettings]);
const updateSettings = async (update: Partial<Settings>) => {
const updateSettings = (update: Partial<Settings>) => {
if (settings) {
const newSettings = { ...settings, ...update };
setSettings(newSettings);
await saveSettings(newSettings);
saveSettings(newSettings);
}
};

View File

@@ -1,19 +1,19 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import uuid from "react-native-uuid";
import { storage } from "./mmkv";
export const getOrSetDeviceId = async () => {
let deviceId = await AsyncStorage.getItem("deviceId");
export const getOrSetDeviceId = () => {
let deviceId = storage.getString("deviceId");
if (!deviceId) {
deviceId = uuid.v4() as string;
await AsyncStorage.setItem("deviceId", deviceId);
storage.set("deviceId", deviceId);
}
return deviceId;
};
export const getDeviceId = async () => {
let deviceId = await AsyncStorage.getItem("deviceId");
export const getDeviceId = () => {
let deviceId = storage.getString("deviceId");
return deviceId || null;
};

View File

@@ -1,5 +1,5 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { atomWithStorage, createJSONStorage } from "jotai/utils";
import { storage } from "./mmkv";
type LogLevel = "INFO" | "WARN" | "ERROR";
@@ -10,14 +10,14 @@ interface LogEntry {
data?: any;
}
const asyncStorage = createJSONStorage(() => AsyncStorage);
const logsAtom = atomWithStorage("logs", [], asyncStorage);
const mmkvStorage = createJSONStorage(() => ({
getItem: (key: string) => storage.getString(key) || null,
setItem: (key: string, value: string) => storage.set(key, value),
removeItem: (key: string) => storage.delete(key),
}));
const logsAtom = atomWithStorage("logs", [], mmkvStorage);
export const writeToLog = async (
level: LogLevel,
message: string,
data?: any
) => {
export const writeToLog = (level: LogLevel, message: string, data?: any) => {
const newEntry: LogEntry = {
timestamp: new Date().toISOString(),
level: level,
@@ -25,23 +25,23 @@ export const writeToLog = async (
data: data,
};
const currentLogs = await AsyncStorage.getItem("logs");
const currentLogs = storage.getString("logs");
const logs: LogEntry[] = currentLogs ? JSON.parse(currentLogs) : [];
logs.push(newEntry);
const maxLogs = 100;
const recentLogs = logs.slice(Math.max(logs.length - maxLogs, 0));
await AsyncStorage.setItem("logs", JSON.stringify(recentLogs));
storage.set("logs", JSON.stringify(recentLogs));
};
export const readFromLog = async (): Promise<LogEntry[]> => {
const logs = await AsyncStorage.getItem("logs");
export const readFromLog = (): LogEntry[] => {
const logs = storage.getString("logs");
return logs ? JSON.parse(logs) : [];
};
export const clearLogs = async () => {
await AsyncStorage.removeItem("logs");
export const clearLogs = () => {
storage.delete("logs");
};
export default logsAtom;