diff --git a/apps/expo/package.json b/apps/expo/package.json index ca5ca12..c4c83bf 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -24,7 +24,6 @@ "@movie-web/tmdb": "*", "@octokit/rest": "^20.0.2", "@react-native-anywhere/polyfill-base64": "0.0.1-alpha.0", - "@react-native-async-storage/async-storage": "1.21.0", "@react-navigation/native": "^6.1.9", "@tamagui/animations-moti": "^1.91.4", "@tamagui/babel-plugin": "^1.91.4", @@ -62,6 +61,7 @@ "react-native-context-menu-view": "^1.14.1", "react-native-gesture-handler": "~2.14.1", "react-native-ios-modal": "^0.1.8", + "react-native-mmkv": "^2.12.2", "react-native-modal": "^13.0.1", "react-native-quick-base64": "^2.0.8", "react-native-quick-crypto": "^0.6.1", diff --git a/apps/expo/src/app/(tabs)/settings.tsx b/apps/expo/src/app/(tabs)/settings.tsx index 2ceb02e..0f8bb3e 100644 --- a/apps/expo/src/app/(tabs)/settings.tsx +++ b/apps/expo/src/app/(tabs)/settings.tsx @@ -1,5 +1,5 @@ import type { SelectProps } from "tamagui"; -import React, { useEffect, useState } from "react"; +import React from "react"; import { TouchableOpacity } from "react-native-gesture-handler"; import * as Application from "expo-application"; import * as Linking from "expo-linking"; @@ -23,7 +23,7 @@ import ScreenLayout from "~/components/layout/ScreenLayout"; import { MWSelect } from "~/components/ui/Select"; import { MWSwitch } from "~/components/ui/Switch"; import { checkForUpdate } from "~/lib/update"; -import { getGestureControls, saveGestureControls } from "~/settings"; +import { usePlayerSettingsStore } from "~/stores/settings"; import { useThemeStore } from "~/stores/theme"; const themeOptions: ThemeStoreOption[] = [ @@ -35,18 +35,11 @@ const themeOptions: ThemeStoreOption[] = [ ]; export default function SettingsScreen() { - const [gestureControlsEnabled, setGestureControlsEnabled] = useState(true); + const { gestureControls, setGestureControls } = usePlayerSettingsStore(); const toastController = useToastController(); - useEffect(() => { - void getGestureControls().then((enabled) => { - setGestureControlsEnabled(enabled); - }); - }, []); - - const handleGestureControlsToggle = async (isEnabled: boolean) => { - setGestureControlsEnabled(isEnabled); - await saveGestureControls(isEnabled); + const handleGestureControlsToggle = (isEnabled: boolean) => { + setGestureControls(isEnabled); }; const handleVersionPress = async () => { @@ -78,7 +71,7 @@ export default function SettingsScreen() { diff --git a/apps/expo/src/components/player/VideoPlayer.tsx b/apps/expo/src/components/player/VideoPlayer.tsx index d927336..3905dbb 100644 --- a/apps/expo/src/components/player/VideoPlayer.tsx +++ b/apps/expo/src/components/player/VideoPlayer.tsx @@ -23,9 +23,9 @@ import { useBrightness } from "~/hooks/player/useBrightness"; import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed"; import { usePlayer } from "~/hooks/player/usePlayer"; import { useVolume } from "~/hooks/player/useVolume"; -import { getGestureControls } from "~/settings"; import { useAudioTrackStore } from "~/stores/audio"; import { usePlayerStore } from "~/stores/player/store"; +import { usePlayerSettingsStore } from "~/stores/settings"; import { CaptionRenderer } from "./CaptionRenderer"; import { ControlsOverlay } from "./ControlsOverlay"; @@ -62,13 +62,7 @@ export const VideoPlayer = () => { const toggleAudio = usePlayerStore((state) => state.toggleAudio); const toggleState = usePlayerStore((state) => state.toggleState); - const [gestureControlsEnabled, setGestureControlsEnabled] = useState(true); - - useEffect(() => { - void getGestureControls().then((enabled) => { - setGestureControlsEnabled(enabled); - }); - }, []); + const { gestureControls } = usePlayerSettingsStore(); const updateResizeMode = (newMode: ResizeMode) => { setResizeMode(newMode); @@ -85,7 +79,7 @@ export const VideoPlayer = () => { }); const doubleTapGesture = Gesture.Tap() - .enabled(gestureControlsEnabled && isIdle) + .enabled(gestureControls && isIdle) .numberOfTaps(2) .onEnd(() => { runOnJS(toggleAudio)(); @@ -95,7 +89,7 @@ export const VideoPlayer = () => { const screenHalfWidth = Dimensions.get("window").width / 2; const panGesture = Gesture.Pan() - .enabled(gestureControlsEnabled && isIdle) + .enabled(gestureControls && isIdle) .onStart((event) => { if (event.x > screenHalfWidth) { runOnJS(setShowVolumeOverlay)(true); diff --git a/apps/expo/src/hooks/DownloadManagerContext.tsx b/apps/expo/src/hooks/DownloadManagerContext.tsx index 9e02380..b757a2b 100644 --- a/apps/expo/src/hooks/DownloadManagerContext.tsx +++ b/apps/expo/src/hooks/DownloadManagerContext.tsx @@ -5,7 +5,7 @@ import * as FileSystem from "expo-file-system"; import * as MediaLibrary from "expo-media-library"; import { useToastController } from "@tamagui/toast"; -import { loadDownloadHistory, saveDownloadHistory } from "~/settings"; +import { useDownloadHistoryStore } from "~/stores/settings"; export interface DownloadItem { id: string; @@ -48,8 +48,8 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({ const toastController = useToastController(); useEffect(() => { - const initializeDownloads = async () => { - const storedDownloads = await loadDownloadHistory(); + const initializeDownloads = () => { + const { downloads: storedDownloads } = useDownloadHistoryStore.getState(); if (storedDownloads) { setDownloads(storedDownloads); } @@ -59,7 +59,7 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({ }, []); useEffect(() => { - void saveDownloadHistory(downloads.slice(0, 10)); + useDownloadHistoryStore.setState({ downloads }); }, [downloads]); const startDownload = async ( @@ -195,7 +195,7 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({ const removeDownload = (id: string) => { const updatedDownloads = downloads.filter((download) => download.id !== id); setDownloads(updatedDownloads); - void saveDownloadHistory(updatedDownloads); + useDownloadHistoryStore.setState({ downloads: updatedDownloads }); }; return ( diff --git a/apps/expo/src/settings/index.ts b/apps/expo/src/settings/index.ts deleted file mode 100644 index b1293cb..0000000 --- a/apps/expo/src/settings/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; - -import type { DownloadItem } from "~/hooks/DownloadManagerContext"; -import type { ThemeStoreOption } from "~/stores/theme"; - -interface ThemeSettings { - theme: ThemeStoreOption; -} - -interface PlayerSettings { - gestureControls: boolean; -} - -interface Settings { - themes?: ThemeSettings; - player?: PlayerSettings; -} - -const settingsKey = "settings"; - -const saveSettings = async (newSettings: Partial) => { - const settings = await loadSettings(); - const mergedSettings = { ...settings, ...newSettings }; - await AsyncStorage.setItem(settingsKey, JSON.stringify(mergedSettings)); -}; - -const loadSettings = async (): Promise => { - const json = await AsyncStorage.getItem(settingsKey); - return json ? (JSON.parse(json) as Settings) : null; -}; - -export const getTheme = async (): Promise => { - const settings = await loadSettings(); - return settings?.themes?.theme ?? "main"; -}; - -export const saveTheme = async (newTheme: ThemeStoreOption) => { - const existingSettings = await loadSettings(); - const settings: Settings = existingSettings?.themes?.theme - ? { - themes: { theme: newTheme }, - } - : { themes: { theme: "main" } }; - await saveSettings(settings); -}; - -interface DownloadHistory { - downloads: DownloadItem[]; -} - -const downloadHistoryKey = "downloadHistory"; - -export const saveDownloadHistory = async (downloads: DownloadItem[]) => { - const json = await AsyncStorage.getItem(downloadHistoryKey); - const settings = json - ? (JSON.parse(json) as DownloadHistory) - : { downloads: [] }; - settings.downloads = downloads; - await AsyncStorage.setItem(downloadHistoryKey, JSON.stringify(settings)); -}; - -export const loadDownloadHistory = async (): Promise => { - const json = await AsyncStorage.getItem(downloadHistoryKey); - const settings = json - ? (JSON.parse(json) as DownloadHistory) - : { downloads: [] }; - return settings.downloads; -}; - -export const getGestureControls = async (): Promise => { - const settings = await loadSettings(); - return settings?.player?.gestureControls ?? true; -}; - -export const saveGestureControls = async (gestureControls: boolean) => { - const settings = (await loadSettings()) ?? {}; - - if (!settings.player) { - settings.player = { gestureControls: true }; - } - - settings.player.gestureControls = gestureControls; - await saveSettings(settings); -}; diff --git a/apps/expo/src/stores/settings/index.ts b/apps/expo/src/stores/settings/index.ts new file mode 100644 index 0000000..588318b --- /dev/null +++ b/apps/expo/src/stores/settings/index.ts @@ -0,0 +1,86 @@ +import type { StateStorage } from "zustand/middleware"; +import { MMKV } from "react-native-mmkv"; +import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; + +import type { DownloadItem } from "~/hooks/DownloadManagerContext"; +import type { ThemeStoreOption } from "~/stores/theme"; + +const storage = new MMKV(); + +const zustandStorage: StateStorage = { + getItem: (name: string): string | null => { + const value = storage.getString(name); + return value ?? null; + }, + setItem: (name: string, value: string): void => { + storage.set(name, value); + }, + removeItem: (name: string): void => { + storage.delete(name); + }, +}; + +interface ThemeStoreState { + theme: ThemeStoreOption; + setTheme: (theme: ThemeStoreOption) => void; +} + +export const useThemeSettingsStore = create< + ThemeStoreState, + [["zustand/persist", ThemeStoreState]] +>( + persist( + (set) => ({ + theme: "main", + setTheme: (theme: ThemeStoreOption) => set({ theme }), + }), + { + name: "theme-settings", + storage: createJSONStorage(() => zustandStorage), + }, + ), +); + +interface PlayerStoreState { + gestureControls: boolean; + setGestureControls: (enabled: boolean) => void; +} + +export const usePlayerSettingsStore = create< + PlayerStoreState, + [["zustand/persist", PlayerStoreState]] +>( + persist( + (set) => ({ + gestureControls: true, + setGestureControls: (enabled: boolean) => + set({ gestureControls: enabled }), + }), + { + name: "player-settings", + storage: createJSONStorage(() => zustandStorage), + }, + ), +); + +interface DownloadHistoryStoreState { + downloads: DownloadItem[]; + setDownloads: (downloads: DownloadItem[]) => void; +} + +export const useDownloadHistoryStore = create< + DownloadHistoryStoreState, + [["zustand/persist", DownloadHistoryStoreState]] +>( + persist( + (set) => ({ + downloads: [], + setDownloads: (downloads: DownloadItem[]) => set({ downloads }), + }), + { + name: "download-history", + storage: createJSONStorage(() => zustandStorage), + }, + ), +); diff --git a/apps/expo/src/stores/theme/index.ts b/apps/expo/src/stores/theme/index.ts index f70a28f..93e2847 100644 --- a/apps/expo/src/stores/theme/index.ts +++ b/apps/expo/src/stores/theme/index.ts @@ -2,7 +2,7 @@ import { setAlternateAppIcon } from "expo-alternate-app-icons"; import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; -import { getTheme, saveTheme } from "~/settings"; +import { useThemeSettingsStore } from "~/stores/settings"; export type ThemeStoreOption = "main" | "blue" | "gray" | "red" | "teal"; @@ -13,25 +13,16 @@ export interface ThemeStore { export const useThemeStore = create( immer((set) => { - void getTheme().then((savedTheme) => { - set((s) => { - s.theme = savedTheme; - }); - }); + const { theme, setTheme: updateTheme } = useThemeSettingsStore.getState(); return { - theme: "main", + theme, setTheme: (newTheme) => { - saveTheme(newTheme) - .then(() => { - set((s) => { - s.theme = newTheme; - void setAlternateAppIcon(newTheme); - }); - }) - .catch((error) => { - console.error("Failed to save theme:", error); - }); + updateTheme(newTheme); + set((state) => { + state.theme = newTheme; + void setAlternateAppIcon(newTheme); + }); }, }; }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a334dd..e98f426 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,9 +44,6 @@ importers: '@react-native-anywhere/polyfill-base64': specifier: 0.0.1-alpha.0 version: 0.0.1-alpha.0 - '@react-native-async-storage/async-storage': - specifier: 1.21.0 - version: 1.21.0(react-native@0.73.6) '@react-navigation/native': specifier: ^6.1.9 version: 6.1.9(react-native@0.73.6)(react@18.2.0) @@ -158,6 +155,9 @@ importers: react-native-ios-modal: specifier: ^0.1.8 version: 0.1.8(react-native@0.73.6)(react@18.2.0) + react-native-mmkv: + specifier: ^2.12.2 + version: 2.12.2(react-native@0.73.6)(react@18.2.0) react-native-modal: specifier: ^13.0.1 version: 13.0.1(react-native@0.73.6)(react@18.2.0) @@ -3041,15 +3041,6 @@ packages: base-64: 0.1.0 dev: false - /@react-native-async-storage/async-storage@1.21.0(react-native@0.73.6): - resolution: {integrity: sha512-JL0w36KuFHFCvnbOXRekqVAUplmOyT/OuCQkogo6X98MtpSaJOKEAeZnYO8JB0U/RIEixZaGI5px73YbRm/oag==} - peerDependencies: - react-native: ^0.0.0-0 || >=0.60 <1.0 - dependencies: - merge-options: 3.0.4 - react-native: 0.73.6(@babel/core@7.23.9)(@babel/preset-env@7.23.9)(react@18.2.0) - dev: false - /@react-native-community/cli-clean@12.3.6: resolution: {integrity: sha512-gUU29ep8xM0BbnZjwz9MyID74KKwutq9x5iv4BCr2im6nly4UMf1B1D+V225wR7VcDGzbgWjaezsJShLLhC5ig==} dependencies: @@ -9089,11 +9080,6 @@ packages: engines: {node: '>=0.10.0'} dev: false - /is-plain-obj@2.1.0: - resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} - engines: {node: '>=8'} - dev: false - /is-plain-object@2.0.4: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} @@ -10017,13 +10003,6 @@ packages: resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==} dev: false - /merge-options@3.0.4: - resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} - engines: {node: '>=10'} - dependencies: - is-plain-obj: 2.1.0 - dev: false - /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -11543,6 +11522,16 @@ packages: react-native: 0.73.6(@babel/core@7.23.9)(@babel/preset-env@7.23.9)(react@18.2.0) dev: false + /react-native-mmkv@2.12.2(react-native@0.73.6)(react@18.2.0): + resolution: {integrity: sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg==} + peerDependencies: + react: '*' + react-native: '>=0.71.0' + dependencies: + react: 18.2.0 + react-native: 0.73.6(@babel/core@7.23.9)(@babel/preset-env@7.23.9)(react@18.2.0) + dev: false + /react-native-modal@13.0.1(react-native@0.73.6)(react@18.2.0): resolution: {integrity: sha512-UB+mjmUtf+miaG/sDhOikRfBOv0gJdBU2ZE1HtFWp6UixW9jCk/bhGdHUgmZljbPpp0RaO/6YiMmQSSK3kkMaw==} peerDependencies: