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: