From 93d117640a08a62607ede9b9e19cc10242acad9a Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 24 Nov 2024 19:34:49 +0100 Subject: [PATCH] fix: change to mmkv and fix downloads with VLC --- app/(auth)/(tabs)/(home)/downloads.tsx | 64 ++++++++++++++++++-------- app/(auth)/player/player.tsx | 50 ++++++++++++-------- app/_layout.tsx | 22 ++++----- hooks/useImageStorage.ts | 8 ++-- hooks/useRemuxHlsToMp4.ts | 20 ++++---- providers/DownloadProvider.tsx | 40 +++++++--------- providers/JellyfinProvider.tsx | 57 +++++++++++++---------- utils/atoms/filters.ts | 33 ++++++------- utils/atoms/queue.ts | 2 - utils/atoms/settings.ts | 27 ++++------- utils/device.ts | 12 ++--- utils/log.ts | 28 +++++------ 12 files changed, 191 insertions(+), 172 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/downloads.tsx b/app/(auth)/(tabs)/(home)/downloads.tsx index ab61ad8d..02109e99 100644 --- a/app/(auth)/(tabs)/(home)/downloads.tsx +++ b/app/(auth)/(tabs)/(home)/downloads.tsx @@ -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 = () => { ); -}; +} -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(), + }, + ] + ); +} diff --git a/app/(auth)/player/player.tsx b/app/(auth)/player/player.tsx index 4666209e..48fd9fff 100644 --- a/app/(auth)/player/player.tsx +++ b/app/(auth)/player/player.tsx @@ -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() { ); - 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 ( + + + + + + ); + return ( { 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); diff --git a/hooks/useImageStorage.ts b/hooks/useImageStorage.ts index 56b44ba0..f379de1c 100644 --- a/hooks/useImageStorage.ts +++ b/hooks/useImageStorage.ts @@ -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 diff --git a/hooks/useRemuxHlsToMp4.ts b/hooks/useRemuxHlsToMp4.ts index ffba7c7c..d2e5388b 100644 --- a/hooks/useRemuxHlsToMp4.ts +++ b/hooks/useRemuxHlsToMp4.ts @@ -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. diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index a8dbf663..b5b84c1b 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -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; @@ -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 => { 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 => { + 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 { + 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 { + 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( diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index a6af0322..03f32f89 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -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; diff --git a/utils/atoms/filters.ts b/utils/atoms/filters.ts index 9d758e26..e2c9e60c 100644 --- a/utils/atoms/filters.ts +++ b/utils/atoms/filters.ts @@ -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.Ascending, ]); -/** - * Sort preferences with persistence - */ export interface SortPreference { [libraryId: string]: SortByOption; } @@ -86,15 +83,15 @@ export const sortByPreferenceAtom = atomWithStorage( "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", 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; }; - diff --git a/utils/atoms/queue.ts b/utils/atoms/queue.ts index 2950d55a..8bd45ffa 100644 --- a/utils/atoms/queue.ts +++ b/utils/atoms/queue.ts @@ -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 { diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 584f9ed8..5d47c3dd 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -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 => { +const loadSettings = (): Settings => { const defaultValues: Settings = { autoRotate: true, forceLandscapeInVideoPlayer: false, @@ -113,7 +106,7 @@ const loadSettings = async (): Promise => { }; try { - const jsonValue = await AsyncStorage.getItem("settings"); + const jsonValue = storage.getString("settings"); const loadedValues: Partial = jsonValue != null ? JSON.parse(jsonValue) : {}; @@ -124,30 +117,28 @@ const loadSettings = async (): Promise => { } }; -// 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(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) => { + const updateSettings = (update: Partial) => { if (settings) { const newSettings = { ...settings, ...update }; setSettings(newSettings); - await saveSettings(newSettings); + saveSettings(newSettings); } }; diff --git a/utils/device.ts b/utils/device.ts index 3968b02f..29a988a5 100644 --- a/utils/device.ts +++ b/utils/device.ts @@ -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; }; diff --git a/utils/log.ts b/utils/log.ts index 0f8af54a..0072c82c 100644 --- a/utils/log.ts +++ b/utils/log.ts @@ -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 => { - 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;