mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 08:03:26 +00:00
refactor: use mmkv and zustand persist middleware for main storage
This commit is contained in:
@@ -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",
|
||||
|
@@ -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() {
|
||||
<Label minWidth={110}>Gesture controls</Label>
|
||||
<Separator minHeight={20} vertical />
|
||||
<MWSwitch
|
||||
checked={gestureControlsEnabled}
|
||||
checked={gestureControls}
|
||||
onCheckedChange={handleGestureControlsToggle}
|
||||
>
|
||||
<MWSwitch.Thumb animation="quicker" />
|
||||
|
@@ -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);
|
||||
|
@@ -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 (
|
||||
|
@@ -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);
|
||||
};
|
86
apps/expo/src/stores/settings/index.ts
Normal file
86
apps/expo/src/stores/settings/index.ts
Normal 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),
|
||||
},
|
||||
),
|
||||
);
|
@@ -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<ThemeStore>((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);
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
Reference in New Issue
Block a user