refactor: use mmkv and zustand persist middleware for main storage

This commit is contained in:
Adrian Castro
2024-03-25 16:07:22 +01:00
parent 0554dd13bc
commit 784628952a
8 changed files with 123 additions and 154 deletions

View File

@@ -24,7 +24,6 @@
"@movie-web/tmdb": "*", "@movie-web/tmdb": "*",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@react-native-anywhere/polyfill-base64": "0.0.1-alpha.0", "@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", "@react-navigation/native": "^6.1.9",
"@tamagui/animations-moti": "^1.91.4", "@tamagui/animations-moti": "^1.91.4",
"@tamagui/babel-plugin": "^1.91.4", "@tamagui/babel-plugin": "^1.91.4",
@@ -62,6 +61,7 @@
"react-native-context-menu-view": "^1.14.1", "react-native-context-menu-view": "^1.14.1",
"react-native-gesture-handler": "~2.14.1", "react-native-gesture-handler": "~2.14.1",
"react-native-ios-modal": "^0.1.8", "react-native-ios-modal": "^0.1.8",
"react-native-mmkv": "^2.12.2",
"react-native-modal": "^13.0.1", "react-native-modal": "^13.0.1",
"react-native-quick-base64": "^2.0.8", "react-native-quick-base64": "^2.0.8",
"react-native-quick-crypto": "^0.6.1", "react-native-quick-crypto": "^0.6.1",

View File

@@ -1,5 +1,5 @@
import type { SelectProps } from "tamagui"; import type { SelectProps } from "tamagui";
import React, { useEffect, useState } from "react"; import React from "react";
import { TouchableOpacity } from "react-native-gesture-handler"; import { TouchableOpacity } from "react-native-gesture-handler";
import * as Application from "expo-application"; import * as Application from "expo-application";
import * as Linking from "expo-linking"; import * as Linking from "expo-linking";
@@ -23,7 +23,7 @@ import ScreenLayout from "~/components/layout/ScreenLayout";
import { MWSelect } from "~/components/ui/Select"; import { MWSelect } from "~/components/ui/Select";
import { MWSwitch } from "~/components/ui/Switch"; import { MWSwitch } from "~/components/ui/Switch";
import { checkForUpdate } from "~/lib/update"; import { checkForUpdate } from "~/lib/update";
import { getGestureControls, saveGestureControls } from "~/settings"; import { usePlayerSettingsStore } from "~/stores/settings";
import { useThemeStore } from "~/stores/theme"; import { useThemeStore } from "~/stores/theme";
const themeOptions: ThemeStoreOption[] = [ const themeOptions: ThemeStoreOption[] = [
@@ -35,18 +35,11 @@ const themeOptions: ThemeStoreOption[] = [
]; ];
export default function SettingsScreen() { export default function SettingsScreen() {
const [gestureControlsEnabled, setGestureControlsEnabled] = useState(true); const { gestureControls, setGestureControls } = usePlayerSettingsStore();
const toastController = useToastController(); const toastController = useToastController();
useEffect(() => { const handleGestureControlsToggle = (isEnabled: boolean) => {
void getGestureControls().then((enabled) => { setGestureControls(isEnabled);
setGestureControlsEnabled(enabled);
});
}, []);
const handleGestureControlsToggle = async (isEnabled: boolean) => {
setGestureControlsEnabled(isEnabled);
await saveGestureControls(isEnabled);
}; };
const handleVersionPress = async () => { const handleVersionPress = async () => {
@@ -78,7 +71,7 @@ export default function SettingsScreen() {
<Label minWidth={110}>Gesture controls</Label> <Label minWidth={110}>Gesture controls</Label>
<Separator minHeight={20} vertical /> <Separator minHeight={20} vertical />
<MWSwitch <MWSwitch
checked={gestureControlsEnabled} checked={gestureControls}
onCheckedChange={handleGestureControlsToggle} onCheckedChange={handleGestureControlsToggle}
> >
<MWSwitch.Thumb animation="quicker" /> <MWSwitch.Thumb animation="quicker" />

View File

@@ -23,9 +23,9 @@ import { useBrightness } from "~/hooks/player/useBrightness";
import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed"; import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed";
import { usePlayer } from "~/hooks/player/usePlayer"; import { usePlayer } from "~/hooks/player/usePlayer";
import { useVolume } from "~/hooks/player/useVolume"; import { useVolume } from "~/hooks/player/useVolume";
import { getGestureControls } from "~/settings";
import { useAudioTrackStore } from "~/stores/audio"; import { useAudioTrackStore } from "~/stores/audio";
import { usePlayerStore } from "~/stores/player/store"; import { usePlayerStore } from "~/stores/player/store";
import { usePlayerSettingsStore } from "~/stores/settings";
import { CaptionRenderer } from "./CaptionRenderer"; import { CaptionRenderer } from "./CaptionRenderer";
import { ControlsOverlay } from "./ControlsOverlay"; import { ControlsOverlay } from "./ControlsOverlay";
@@ -62,13 +62,7 @@ export const VideoPlayer = () => {
const toggleAudio = usePlayerStore((state) => state.toggleAudio); const toggleAudio = usePlayerStore((state) => state.toggleAudio);
const toggleState = usePlayerStore((state) => state.toggleState); const toggleState = usePlayerStore((state) => state.toggleState);
const [gestureControlsEnabled, setGestureControlsEnabled] = useState(true); const { gestureControls } = usePlayerSettingsStore();
useEffect(() => {
void getGestureControls().then((enabled) => {
setGestureControlsEnabled(enabled);
});
}, []);
const updateResizeMode = (newMode: ResizeMode) => { const updateResizeMode = (newMode: ResizeMode) => {
setResizeMode(newMode); setResizeMode(newMode);
@@ -85,7 +79,7 @@ export const VideoPlayer = () => {
}); });
const doubleTapGesture = Gesture.Tap() const doubleTapGesture = Gesture.Tap()
.enabled(gestureControlsEnabled && isIdle) .enabled(gestureControls && isIdle)
.numberOfTaps(2) .numberOfTaps(2)
.onEnd(() => { .onEnd(() => {
runOnJS(toggleAudio)(); runOnJS(toggleAudio)();
@@ -95,7 +89,7 @@ export const VideoPlayer = () => {
const screenHalfWidth = Dimensions.get("window").width / 2; const screenHalfWidth = Dimensions.get("window").width / 2;
const panGesture = Gesture.Pan() const panGesture = Gesture.Pan()
.enabled(gestureControlsEnabled && isIdle) .enabled(gestureControls && isIdle)
.onStart((event) => { .onStart((event) => {
if (event.x > screenHalfWidth) { if (event.x > screenHalfWidth) {
runOnJS(setShowVolumeOverlay)(true); runOnJS(setShowVolumeOverlay)(true);

View File

@@ -5,7 +5,7 @@ import * as FileSystem from "expo-file-system";
import * as MediaLibrary from "expo-media-library"; import * as MediaLibrary from "expo-media-library";
import { useToastController } from "@tamagui/toast"; import { useToastController } from "@tamagui/toast";
import { loadDownloadHistory, saveDownloadHistory } from "~/settings"; import { useDownloadHistoryStore } from "~/stores/settings";
export interface DownloadItem { export interface DownloadItem {
id: string; id: string;
@@ -48,8 +48,8 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
const toastController = useToastController(); const toastController = useToastController();
useEffect(() => { useEffect(() => {
const initializeDownloads = async () => { const initializeDownloads = () => {
const storedDownloads = await loadDownloadHistory(); const { downloads: storedDownloads } = useDownloadHistoryStore.getState();
if (storedDownloads) { if (storedDownloads) {
setDownloads(storedDownloads); setDownloads(storedDownloads);
} }
@@ -59,7 +59,7 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
}, []); }, []);
useEffect(() => { useEffect(() => {
void saveDownloadHistory(downloads.slice(0, 10)); useDownloadHistoryStore.setState({ downloads });
}, [downloads]); }, [downloads]);
const startDownload = async ( const startDownload = async (
@@ -195,7 +195,7 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
const removeDownload = (id: string) => { const removeDownload = (id: string) => {
const updatedDownloads = downloads.filter((download) => download.id !== id); const updatedDownloads = downloads.filter((download) => download.id !== id);
setDownloads(updatedDownloads); setDownloads(updatedDownloads);
void saveDownloadHistory(updatedDownloads); useDownloadHistoryStore.setState({ downloads: updatedDownloads });
}; };
return ( return (

View File

@@ -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<Settings>) => {
const settings = await loadSettings();
const mergedSettings = { ...settings, ...newSettings };
await AsyncStorage.setItem(settingsKey, JSON.stringify(mergedSettings));
};
const loadSettings = async (): Promise<Settings | null> => {
const json = await AsyncStorage.getItem(settingsKey);
return json ? (JSON.parse(json) as Settings) : null;
};
export const getTheme = async (): Promise<ThemeStoreOption> => {
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<DownloadItem[]> => {
const json = await AsyncStorage.getItem(downloadHistoryKey);
const settings = json
? (JSON.parse(json) as DownloadHistory)
: { downloads: [] };
return settings.downloads;
};
export const getGestureControls = async (): Promise<boolean> => {
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);
};

View File

@@ -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),
},
),
);

View File

@@ -2,7 +2,7 @@ import { setAlternateAppIcon } from "expo-alternate-app-icons";
import { create } from "zustand"; import { create } from "zustand";
import { immer } from "zustand/middleware/immer"; import { immer } from "zustand/middleware/immer";
import { getTheme, saveTheme } from "~/settings"; import { useThemeSettingsStore } from "~/stores/settings";
export type ThemeStoreOption = "main" | "blue" | "gray" | "red" | "teal"; export type ThemeStoreOption = "main" | "blue" | "gray" | "red" | "teal";
@@ -13,25 +13,16 @@ export interface ThemeStore {
export const useThemeStore = create( export const useThemeStore = create(
immer<ThemeStore>((set) => { immer<ThemeStore>((set) => {
void getTheme().then((savedTheme) => { const { theme, setTheme: updateTheme } = useThemeSettingsStore.getState();
set((s) => {
s.theme = savedTheme;
});
});
return { return {
theme: "main", theme,
setTheme: (newTheme) => { setTheme: (newTheme) => {
saveTheme(newTheme) updateTheme(newTheme);
.then(() => { set((state) => {
set((s) => { state.theme = newTheme;
s.theme = newTheme; void setAlternateAppIcon(newTheme);
void setAlternateAppIcon(newTheme); });
});
})
.catch((error) => {
console.error("Failed to save theme:", error);
});
}, },
}; };
}), }),

37
pnpm-lock.yaml generated
View File

@@ -44,9 +44,6 @@ importers:
'@react-native-anywhere/polyfill-base64': '@react-native-anywhere/polyfill-base64':
specifier: 0.0.1-alpha.0 specifier: 0.0.1-alpha.0
version: 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': '@react-navigation/native':
specifier: ^6.1.9 specifier: ^6.1.9
version: 6.1.9(react-native@0.73.6)(react@18.2.0) version: 6.1.9(react-native@0.73.6)(react@18.2.0)
@@ -158,6 +155,9 @@ importers:
react-native-ios-modal: react-native-ios-modal:
specifier: ^0.1.8 specifier: ^0.1.8
version: 0.1.8(react-native@0.73.6)(react@18.2.0) 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: react-native-modal:
specifier: ^13.0.1 specifier: ^13.0.1
version: 13.0.1(react-native@0.73.6)(react@18.2.0) version: 13.0.1(react-native@0.73.6)(react@18.2.0)
@@ -3041,15 +3041,6 @@ packages:
base-64: 0.1.0 base-64: 0.1.0
dev: false 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: /@react-native-community/cli-clean@12.3.6:
resolution: {integrity: sha512-gUU29ep8xM0BbnZjwz9MyID74KKwutq9x5iv4BCr2im6nly4UMf1B1D+V225wR7VcDGzbgWjaezsJShLLhC5ig==} resolution: {integrity: sha512-gUU29ep8xM0BbnZjwz9MyID74KKwutq9x5iv4BCr2im6nly4UMf1B1D+V225wR7VcDGzbgWjaezsJShLLhC5ig==}
dependencies: dependencies:
@@ -9089,11 +9080,6 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: false 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: /is-plain-object@2.0.4:
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -10017,13 +10003,6 @@ packages:
resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==} resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==}
dev: false 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: /merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 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) react-native: 0.73.6(@babel/core@7.23.9)(@babel/preset-env@7.23.9)(react@18.2.0)
dev: false 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): /react-native-modal@13.0.1(react-native@0.73.6)(react@18.2.0):
resolution: {integrity: sha512-UB+mjmUtf+miaG/sDhOikRfBOv0gJdBU2ZE1HtFWp6UixW9jCk/bhGdHUgmZljbPpp0RaO/6YiMmQSSK3kkMaw==} resolution: {integrity: sha512-UB+mjmUtf+miaG/sDhOikRfBOv0gJdBU2ZE1HtFWp6UixW9jCk/bhGdHUgmZljbPpp0RaO/6YiMmQSSK3kkMaw==}
peerDependencies: peerDependencies: