complete migration to mmkv

This commit is contained in:
tapframe 2025-10-26 12:42:34 +05:30
parent e5e77508b8
commit 49b814a36d
54 changed files with 621 additions and 459 deletions

1
.gitignore vendored
View file

@ -73,3 +73,4 @@ SDK54_UPGRADE_SUMMARY.md
build-and-publish-app-releases.sh
bottomnav.md
/TrailerServices
mmkv.md

View file

@ -34,13 +34,13 @@ import UpdatePopup from './src/components/UpdatePopup';
import MajorUpdateOverlay from './src/components/MajorUpdateOverlay';
import { useGithubMajorUpdate } from './src/hooks/useGithubMajorUpdate';
import { useUpdatePopup } from './src/hooks/useUpdatePopup';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Sentry from '@sentry/react-native';
import UpdateService from './src/services/updateService';
import { memoryMonitorService } from './src/services/memoryMonitorService';
import { aiService } from './src/services/aiService';
import { AccountProvider, useAccount } from './src/contexts/AccountContext';
import { ToastProvider } from './src/contexts/ToastContext';
import { mmkvStorage } from './src/services/mmkvStorage';
Sentry.init({
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
@ -104,7 +104,7 @@ const ThemedApp = () => {
const initializeApp = async () => {
try {
// Check onboarding status
const onboardingCompleted = await AsyncStorage.getItem('hasCompletedOnboarding');
const onboardingCompleted = await mmkvStorage.getItem('hasCompletedOnboarding');
setHasCompletedOnboarding(onboardingCompleted === 'true');
// Initialize update service

View file

@ -297,7 +297,6 @@
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/KSPlayer/KSPlayer_KSPlayer.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
@ -340,7 +339,6 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/KSPlayer_KSPlayer.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
@ -461,7 +459,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = "Nuvio";
PRODUCT_NAME = Nuvio;
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -492,8 +490,8 @@
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app";
PRODUCT_NAME = "Nuvio";
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = Nuvio;
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";

View file

@ -404,7 +404,56 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- MMKVCore (2.2.4)
- MobileVLCKit (3.6.1b1)
- NitroMmkv (4.0.0):
- hermes-engine
- MMKVCore (= 2.2.4)
- NitroModules
- RCTRequired
- RCTTypeSafety
- React-callinvoker
- React-Core
- React-Core-prebuilt
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- NitroModules (0.31.2):
- hermes-engine
- RCTRequired
- RCTTypeSafety
- React-callinvoker
- React-Core
- React-Core-prebuilt
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- RCTDeprecation (0.81.4)
- RCTRequired (0.81.4)
- RCTTypeSafety (0.81.4):
@ -2292,28 +2341,6 @@ PODS:
- React-utils (= 0.81.4)
- ReactNativeDependencies
- ReactNativeDependencies (0.81.4)
- RNCAsyncStorage (2.2.0):
- hermes-engine
- RCTRequired
- RCTTypeSafety
- React-Core
- React-Core-prebuilt
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- RNCPicker (2.11.1):
- hermes-engine
- RCTRequired
@ -2725,6 +2752,8 @@ DEPENDENCIES:
- KSPlayer (from `https://github.com/kingslay/KSPlayer.git`, branch `main`)
- Libass (from `https://github.com/kingslay/FFmpegKit.git`, branch `main`)
- lottie-react-native (from `../node_modules/lottie-react-native`)
- NitroMmkv (from `../node_modules/react-native-mmkv`)
- NitroModules (from `../node_modules/react-native-nitro-modules`)
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
@ -2798,7 +2827,6 @@ DEPENDENCIES:
- ReactCodegen (from `build/generated/ios`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- ReactNativeDependencies (from `../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCPicker (from `../node_modules/@react-native-picker/picker`)"
- "RNFastImage (from `../node_modules/@d11/react-native-fast-image`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
@ -2816,6 +2844,7 @@ SPEC REPOS:
- libdav1d
- libwebp
- lottie-ios
- MMKVCore
- MobileVLCKit
- ReachabilitySwift
- SDWebImage
@ -2916,6 +2945,10 @@ EXTERNAL SOURCES:
:git: https://github.com/kingslay/FFmpegKit.git
lottie-react-native:
:path: "../node_modules/lottie-react-native"
NitroMmkv:
:path: "../node_modules/react-native-mmkv"
NitroModules:
:path: "../node_modules/react-native-nitro-modules"
RCTDeprecation:
:path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
RCTRequired:
@ -3060,8 +3093,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon"
ReactNativeDependencies:
:podspec: "../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec"
RNCAsyncStorage:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNCPicker:
:path: "../node_modules/@react-native-picker/picker"
RNFastImage:
@ -3145,7 +3176,10 @@ SPEC CHECKSUMS:
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
lottie-react-native: cbe3d931a7c24f7891a8e8032c2bb9b2373c4b9c
MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df
MobileVLCKit: 2d9c7c373393ae43086aeeff890bf0b1afc15c5c
NitroMmkv: 7fe66a61d5acab6516098a64f42af575595e7566
NitroModules: 8c4eca403e6f45f474608d24cd11ab664ed2961c
RCTDeprecation: 7487d6dda857ccd4cb3dd6ecfccdc3170e85dcbc
RCTRequired: 54128b7df8be566881d48c7234724a78cb9b6157
RCTTypeSafety: d2b07797a79e45d7b19e1cd2f53c79ab419fe217
@ -3219,7 +3253,6 @@ SPEC CHECKSUMS:
ReactCodegen: a15ad48730e9fb2a51a4c9f61fe1ed253dfcf10f
ReactCommon: 149b6c05126f2e99f2ed0d3c63539369546f8cae
ReactNativeDependencies: ed6d1e64802b150399f04f1d5728ec16b437251e
RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4
RNCPicker: a7170edbcbf8288de8edb2502e08e7fc757fa755
RNFastImage: 42a769cd260a7686b1db32a9f7d754333bad4e77
RNGestureHandler: 2914750df066d89bf9d8f48a10ad5f0051108ac3

59
package-lock.json generated
View file

@ -19,7 +19,6 @@
"@gorhom/bottom-sheet": "^5.2.6",
"@legendapp/list": "^2.0.13",
"@lottiefiles/dotlottie-react": "^0.6.5",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/blur": "^4.4.1",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-community/slider": "5.0.1",
@ -74,6 +73,8 @@
"react-native-image-colors": "^2.5.0",
"react-native-immersive-mode": "^2.0.2",
"react-native-markdown-display": "^7.0.2",
"react-native-mmkv": "^4.0.0",
"react-native-nitro-modules": "^0.31.2",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "^4.1.1",
"react-native-safe-area-context": "~5.6.0",
@ -2884,18 +2885,6 @@
"integrity": "sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw==",
"license": "MIT"
},
"node_modules/@react-native-async-storage/async-storage": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
"integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.65 <1.0"
}
},
"node_modules/@react-native-community/blur": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@react-native-community/blur/-/blur-4.4.1.tgz",
@ -7936,15 +7925,6 @@
"node": ">=0.12.0"
}
},
"node_modules/is-plain-obj": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@ -9007,18 +8987,6 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
"node_modules/merge-options": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
"license": "MIT",
"dependencies": {
"is-plain-obj": "^2.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -10850,6 +10818,29 @@
"react-native": ">=0.50.4"
}
},
"node_modules/react-native-mmkv": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.0.0.tgz",
"integrity": "sha512-Osoy8as2ZLzO1TTsKxc4tX14Qk19qRVMWnS4ZVBwxie9Re5cjt7rqlpDkJczK3H/y3z70EQ6rmKI/cNMCLGAYQ==",
"hasInstallScript": true,
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-nitro-modules": "*"
}
},
"node_modules/react-native-nitro-modules": {
"version": "0.31.2",
"resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.31.2.tgz",
"integrity": "sha512-UTG1kfLq5XDlR1OkhrNLNMbU6EvxIdLp4RUWT4PY8eBKyykOq8/kjc3fYMYxVEUZG+mrujRSMNXKeqHfiMUhOQ==",
"hasInstallScript": true,
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-paper": {
"version": "5.14.5",
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.14.5.tgz",

View file

@ -19,7 +19,6 @@
"@gorhom/bottom-sheet": "^5.2.6",
"@legendapp/list": "^2.0.13",
"@lottiefiles/dotlottie-react": "^0.6.5",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/blur": "^4.4.1",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-community/slider": "5.0.1",
@ -74,6 +73,8 @@
"react-native-image-colors": "^2.5.0",
"react-native-immersive-mode": "^2.0.2",
"react-native-markdown-display": "^7.0.2",
"react-native-mmkv": "^4.0.0",
"react-native-nitro-modules": "^0.31.2",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "^4.1.1",
"react-native-safe-area-context": "~5.6.0",

View file

@ -8,7 +8,7 @@ import { useTheme } from '../../contexts/ThemeContext';
import { useSettings } from '../../hooks/useSettings';
import { catalogService, StreamingContent } from '../../services/catalogService';
import { DropUpMenu } from './DropUpMenu';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../../services/mmkvStorage';
import { storageService } from '../../services/storageService';
import { TraktService } from '../../services/traktService';
import { useTraktContext } from '../../contexts/TraktContext';
@ -97,7 +97,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
// Load watched state from AsyncStorage when item changes
useEffect(() => {
const updateWatched = () => {
AsyncStorage.getItem(`watched:${item.type}:${item.id}`).then(val => setIsWatched(val === 'true'));
mmkvStorage.getItem(`watched:${item.type}:${item.id}`).then((val: string | null) => setIsWatched(val === 'true'));
};
updateWatched();
const sub = DeviceEventEmitter.addListener('watchedStatusChanged', updateWatched);
@ -163,7 +163,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
const targetWatched = !isWatched;
setIsWatched(targetWatched);
try {
await AsyncStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false');
await mmkvStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false');
} catch {}
showInfo(targetWatched ? 'Marked as Watched' : 'Marked as Unwatched', targetWatched ? 'Item marked as watched' : 'Item marked as unwatched');
setTimeout(() => {

View file

@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'
import { View, Text, StyleSheet, ActivityIndicator, Image, Animated, Dimensions } from 'react-native';
import { useTheme } from '../../contexts/ThemeContext';
import { useMDBListRatings } from '../../hooks/useMDBListRatings';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../../services/mmkvStorage';
import { isMDBListEnabled, RATING_PROVIDERS_STORAGE_KEY } from '../../screens/MDBListSettingsScreen';
// Import SVG icons
@ -124,7 +124,7 @@ export const RatingsSection: React.FC<RatingsSectionProps> = ({ imdbId, type })
const loadProviderSettings = async () => {
try {
const savedSettings = await AsyncStorage.getItem(RATING_PROVIDERS_STORAGE_KEY);
const savedSettings = await mmkvStorage.getItem(RATING_PROVIDERS_STORAGE_KEY);
if (savedSettings) {
setEnabledProviders(JSON.parse(savedSettings));
} else {

View file

@ -13,7 +13,7 @@ import { useFocusEffect } from '@react-navigation/native';
import Animated, { FadeIn, FadeOut, SlideInRight, SlideOutLeft } from 'react-native-reanimated';
import { TraktService } from '../../services/traktService';
import { logger } from '../../utils/logger';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../../services/mmkvStorage';
// Enhanced responsive breakpoints for Seasons Section
const BREAKPOINTS = {
@ -186,7 +186,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
useEffect(() => {
const loadViewModePreference = async () => {
try {
const savedMode = await AsyncStorage.getItem('global_season_view_mode');
const savedMode = await mmkvStorage.getItem('global_season_view_mode');
if (savedMode === 'text' || savedMode === 'posters') {
setSeasonViewMode(savedMode);
if (__DEV__) console.log('[SeriesContent] Loaded global view mode:', savedMode);
@ -215,7 +215,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
// Update view mode without animations
const updateViewMode = (newMode: 'posters' | 'text') => {
setSeasonViewMode(newMode);
AsyncStorage.setItem('global_season_view_mode', newMode).catch(error => {
mmkvStorage.setItem('global_season_view_mode', newMode).catch((error: any) => {
if (__DEV__) console.log('[SeriesContent] Error saving global view mode preference:', error);
});
};

View file

@ -10,7 +10,7 @@ import RNImmersiveMode from 'react-native-immersive-mode';
import * as ScreenOrientation from 'expo-screen-orientation';
import { storageService } from '../../services/storageService';
import { logger } from '../../utils/logger';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../../services/mmkvStorage';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { useTraktAutosync } from '../../hooks/useTraktAutosync';
@ -235,7 +235,7 @@ const AndroidVideoPlayer: React.FC = () => {
// Load speed settings from storage
const loadSpeedSettings = useCallback(async () => {
try {
const saved = await AsyncStorage.getItem(SPEED_SETTINGS_KEY);
const saved = await mmkvStorage.getItem(SPEED_SETTINGS_KEY);
if (saved) {
const settings = JSON.parse(saved);
if (typeof settings.holdToSpeedEnabled === 'boolean') {
@ -257,7 +257,7 @@ const AndroidVideoPlayer: React.FC = () => {
holdToSpeedEnabled,
holdToSpeedValue,
};
await AsyncStorage.setItem(SPEED_SETTINGS_KEY, JSON.stringify(settings));
await mmkvStorage.setItem(SPEED_SETTINGS_KEY, JSON.stringify(settings));
} catch (error) {
logger.warn('[AndroidVideoPlayer] Error saving speed settings:', error);
}
@ -2278,7 +2278,7 @@ const AndroidVideoPlayer: React.FC = () => {
return;
}
// One-time migrate legacy key if present
const legacy = await AsyncStorage.getItem(SUBTITLE_SIZE_KEY);
const legacy = await mmkvStorage.getItem(SUBTITLE_SIZE_KEY);
if (legacy) {
const migrated = parseInt(legacy, 10);
if (!Number.isNaN(migrated) && migrated > 0) {
@ -2288,7 +2288,7 @@ const AndroidVideoPlayer: React.FC = () => {
await storageService.saveSubtitleSettings(merged);
} catch {}
}
try { await AsyncStorage.removeItem(SUBTITLE_SIZE_KEY); } catch {}
try { await mmkvStorage.removeItem(SUBTITLE_SIZE_KEY); } catch {}
return;
}
// If no saved settings, use responsive default

View file

@ -9,7 +9,7 @@ import RNImmersiveMode from 'react-native-immersive-mode';
import * as ScreenOrientation from 'expo-screen-orientation';
import { storageService } from '../../services/storageService';
import { logger } from '../../utils/logger';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../../services/mmkvStorage';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import Slider from '@react-native-community/slider';
@ -1616,7 +1616,7 @@ const KSPlayerCore: React.FC = () => {
return;
}
// One-time migrate legacy key if present
const legacy = await AsyncStorage.getItem(SUBTITLE_SIZE_KEY);
const legacy = await mmkvStorage.getItem(SUBTITLE_SIZE_KEY);
if (legacy) {
const migrated = parseInt(legacy, 10);
if (!Number.isNaN(migrated) && migrated > 0) {
@ -1626,7 +1626,7 @@ const KSPlayerCore: React.FC = () => {
await storageService.saveSubtitleSettings(merged);
} catch {}
}
try { await AsyncStorage.removeItem(SUBTITLE_SIZE_KEY); } catch {}
try { await mmkvStorage.removeItem(SUBTITLE_SIZE_KEY); } catch {}
return;
}
// If no saved settings, use responsive default

View file

@ -1,7 +1,7 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { AppState } from 'react-native';
import * as FileSystem from 'expo-file-system/legacy';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { notificationService } from '../services/notificationService';
export type DownloadStatus = 'downloading' | 'completed' | 'paused' | 'error' | 'queued';
@ -142,7 +142,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
useEffect(() => {
(async () => {
try {
const raw = await AsyncStorage.getItem(STORAGE_KEY);
const raw = await mmkvStorage.getItem(STORAGE_KEY);
if (raw) {
const list = JSON.parse(raw) as Array<Partial<DownloadItem>>;
// Mark any in-progress as paused on restore (cannot resume across sessions reliably)
@ -216,7 +216,7 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
}, []);
useEffect(() => {
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(downloads)).catch(() => {});
mmkvStorage.setItem(STORAGE_KEY, JSON.stringify(downloads)).catch(() => {});
}, [downloads]);
const updateDownload = useCallback((id: string, updater: (d: DownloadItem) => DownloadItem) => {

View file

@ -1,5 +1,5 @@
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { settingsEmitter } from '../hooks/useSettings';
import { colors as defaultColors } from '../styles/colors';
@ -168,11 +168,11 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
useEffect(() => {
const loadThemes = async () => {
try {
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const appSettingsJson = await AsyncStorage.getItem(`@user:${scope}:app_settings`);
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const appSettingsJson = await mmkvStorage.getItem(`@user:${scope}:app_settings`);
const appSettings = appSettingsJson ? JSON.parse(appSettingsJson) : {};
const savedThemeId = appSettings.themeId || (await AsyncStorage.getItem(CURRENT_THEME_KEY));
const customThemesJson = appSettings.customThemes ? JSON.stringify(appSettings.customThemes) : await AsyncStorage.getItem(CUSTOM_THEMES_KEY);
const savedThemeId = appSettings.themeId || (await mmkvStorage.getItem(CURRENT_THEME_KEY));
const customThemesJson = appSettings.customThemes ? JSON.stringify(appSettings.customThemes) : await mmkvStorage.getItem(CUSTOM_THEMES_KEY);
const customThemes = customThemesJson ? JSON.parse(customThemesJson) : [];
const allThemes = [...DEFAULT_THEMES, ...customThemes];
setAvailableThemes(allThemes);
@ -195,13 +195,13 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
if (theme) {
setCurrentThemeState(theme);
// Persist into scoped app_settings and legacy key for backward compat
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const key = `@user:${scope}:app_settings`;
let settings = {} as any;
try { settings = JSON.parse((await AsyncStorage.getItem(key)) || '{}'); } catch {}
try { settings = JSON.parse((await mmkvStorage.getItem(key)) || '{}'); } catch {}
settings.themeId = themeId;
await AsyncStorage.setItem(key, JSON.stringify(settings));
await AsyncStorage.setItem(CURRENT_THEME_KEY, themeId);
await mmkvStorage.setItem(key, JSON.stringify(settings));
await mmkvStorage.setItem(CURRENT_THEME_KEY, themeId);
// Do not emit global settings sync for themes (sync on app restart only)
}
};
@ -225,20 +225,20 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
const updatedAllThemes = [...DEFAULT_THEMES, ...updatedCustomThemes];
// Save to storage (scoped app_settings + legacy key)
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const key = `@user:${scope}:app_settings`;
let settings = {} as any;
try { settings = JSON.parse((await AsyncStorage.getItem(key)) || '{}'); } catch {}
try { settings = JSON.parse((await mmkvStorage.getItem(key)) || '{}'); } catch {}
settings.customThemes = updatedCustomThemes;
await AsyncStorage.setItem(key, JSON.stringify(settings));
await AsyncStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updatedCustomThemes));
await mmkvStorage.setItem(key, JSON.stringify(settings));
await mmkvStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updatedCustomThemes));
// Update state
setAvailableThemes(updatedAllThemes);
// Set as current theme
setCurrentThemeState(newTheme);
await AsyncStorage.setItem(CURRENT_THEME_KEY, id);
await mmkvStorage.setItem(CURRENT_THEME_KEY, id);
// Do not emit global settings sync for themes
} catch (error) {
if (__DEV__) console.error('Failed to add custom theme:', error);
@ -262,13 +262,13 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
const updatedAllThemes = [...DEFAULT_THEMES, ...updatedCustomThemes];
// Save to storage (scoped app_settings + legacy key)
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const key = `@user:${scope}:app_settings`;
let settings = {} as any;
try { settings = JSON.parse((await AsyncStorage.getItem(key)) || '{}'); } catch {}
try { settings = JSON.parse((await mmkvStorage.getItem(key)) || '{}'); } catch {}
settings.customThemes = updatedCustomThemes;
await AsyncStorage.setItem(key, JSON.stringify(settings));
await AsyncStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updatedCustomThemes));
await mmkvStorage.setItem(key, JSON.stringify(settings));
await mmkvStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updatedCustomThemes));
// Update state
setAvailableThemes(updatedAllThemes);
@ -298,13 +298,13 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
const updatedAllThemes = [...DEFAULT_THEMES, ...customThemes];
// Save to storage (scoped app_settings + legacy key)
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const key = `@user:${scope}:app_settings`;
let settings = {} as any;
try { settings = JSON.parse((await AsyncStorage.getItem(key)) || '{}'); } catch {}
try { settings = JSON.parse((await mmkvStorage.getItem(key)) || '{}'); } catch {}
settings.customThemes = customThemes;
await AsyncStorage.setItem(key, JSON.stringify(settings));
await AsyncStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(customThemes));
await mmkvStorage.setItem(key, JSON.stringify(settings));
await mmkvStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(customThemes));
// Update state
setAvailableThemes(updatedAllThemes);
@ -312,7 +312,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
// Reset to default theme if current theme was deleted
if (currentTheme.id === themeId) {
setCurrentThemeState(DEFAULT_THEMES[0]);
await AsyncStorage.setItem(CURRENT_THEME_KEY, DEFAULT_THEMES[0].id);
await mmkvStorage.setItem(CURRENT_THEME_KEY, DEFAULT_THEMES[0].id);
}
// Do not emit global settings sync for themes
} catch (error) {

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { logger } from '../utils/logger';
const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
@ -27,7 +27,7 @@ export function useCustomCatalogNames() {
setIsLoading(true);
try {
const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
const savedCustomNamesJson = await mmkvStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
const loadedNames = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {};
setCustomNames(loadedNames);
// Update cache

View file

@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { AppState } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { StreamingContent, catalogService } from '../services/catalogService';
import { tmdbService } from '../services/tmdbService';
import { logger } from '../utils/logger';
@ -241,7 +241,7 @@ export function useFeaturedContent() {
// Safety guard: if nothing came back within a reasonable time, stop loading
if (!formattedContent || formattedContent.length === 0) {
// Fall back to any cached featured item so UI can render something
const cachedJson = await AsyncStorage.getItem(STORAGE_KEY).catch(() => null);
const cachedJson = await mmkvStorage.getItem(STORAGE_KEY).catch(() => null);
if (cachedJson) {
try {
const parsed = JSON.parse(cachedJson);
@ -270,7 +270,7 @@ export function useFeaturedContent() {
// Persist cache for fast startup (skipped when cache disabled)
if (!DISABLE_CACHE) {
try {
await AsyncStorage.setItem(
await mmkvStorage.setItem(
STORAGE_KEY,
JSON.stringify({
ts: now,
@ -285,7 +285,7 @@ export function useFeaturedContent() {
setFeaturedContent(null);
// Clear persisted cache on empty (skipped when cache disabled)
if (!DISABLE_CACHE) {
try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {}
try { await mmkvStorage.removeItem(STORAGE_KEY); } catch {}
}
}
} catch (error) {
@ -310,7 +310,7 @@ export function useFeaturedContent() {
let cancelled = false;
(async () => {
try {
const json = await AsyncStorage.getItem(STORAGE_KEY);
const json = await mmkvStorage.getItem(STORAGE_KEY);
if (!json) return;
const parsed = JSON.parse(json);
if (cancelled) return;

View file

@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import * as Updates from 'expo-updates';
import { getDisplayedAppVersion } from '../utils/version';
import { fetchLatestGithubRelease, isAnyUpgrade } from '../services/githubReleaseService';
@ -29,7 +29,7 @@ export function useGithubMajorUpdate(): MajorUpdateData {
const info = await fetchLatestGithubRelease();
if (!info?.tag_name) return;
const dismissed = await AsyncStorage.getItem(DISMISSED_KEY);
const dismissed = await mmkvStorage.getItem(DISMISSED_KEY);
if (dismissed === info.tag_name) return;
// "Later" is session-only now, no persisted snooze
@ -51,7 +51,7 @@ export function useGithubMajorUpdate(): MajorUpdateData {
}, [check]);
const onDismiss = useCallback(async () => {
if (latestTag) await AsyncStorage.setItem(DISMISSED_KEY, latestTag);
if (latestTag) await mmkvStorage.setItem(DISMISSED_KEY, latestTag);
setVisible(false);
}, [latestTag]);

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { StreamingContent } from '../services/catalogService';
import { catalogService } from '../services/catalogService';
@ -13,14 +13,14 @@ export const useLibrary = () => {
const loadLibraryItems = useCallback(async () => {
try {
setLoading(true);
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const scopedKey = `@user:${scope}:stremio-library`;
let storedItems = await AsyncStorage.getItem(scopedKey);
let storedItems = await mmkvStorage.getItem(scopedKey);
if (!storedItems) {
// migrate legacy into scoped
const legacy = await AsyncStorage.getItem(LEGACY_LIBRARY_STORAGE_KEY);
const legacy = await mmkvStorage.getItem(LEGACY_LIBRARY_STORAGE_KEY);
if (legacy) {
await AsyncStorage.setItem(scopedKey, legacy);
await mmkvStorage.setItem(scopedKey, legacy);
storedItems = legacy;
}
}
@ -50,11 +50,11 @@ export const useLibrary = () => {
acc[`${item.type}:${item.id}`] = item;
return acc;
}, {} as Record<string, StreamingContent>);
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const scopedKey = `@user:${scope}:stremio-library`;
await AsyncStorage.setItem(scopedKey, JSON.stringify(itemsObject));
await mmkvStorage.setItem(scopedKey, JSON.stringify(itemsObject));
// keep legacy for backward-compat
await AsyncStorage.setItem(LEGACY_LIBRARY_STORAGE_KEY, JSON.stringify(itemsObject));
await mmkvStorage.setItem(LEGACY_LIBRARY_STORAGE_KEY, JSON.stringify(itemsObject));
} catch (error) {
if (__DEV__) console.error('Error saving library items:', error);
}

View file

@ -9,7 +9,7 @@ import { Cast, Episode, GroupedEpisodes, GroupedStreams } from '../types/metadat
import { TMDBService } from '../services/tmdbService';
import { logger } from '../utils/logger';
import { usePersistentSeasons } from './usePersistentSeasons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { Stream } from '../types/metadata';
import { storageService } from '../services/storageService';
import { useSettings } from './useSettings';

View file

@ -3,7 +3,7 @@ import { logger } from '../utils/logger';
import { TMDBService } from '../services/tmdbService';
import { isTmdbUrl } from '../utils/logoUtils';
import FastImage from '@d11/react-native-fast-image';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
// Cache for image availability checks
const imageAvailabilityCache: Record<string, boolean> = {};
@ -17,7 +17,7 @@ const checkImageAvailability = async (url: string): Promise<boolean> => {
// Check AsyncStorage cache
try {
const cachedResult = await AsyncStorage.getItem(`image_available:${url}`);
const cachedResult = await mmkvStorage.getItem(`image_available:${url}`);
if (cachedResult !== null) {
const isAvailable = cachedResult === 'true';
imageAvailabilityCache[url] = isAvailable;
@ -35,7 +35,7 @@ const checkImageAvailability = async (url: string): Promise<boolean> => {
// Update caches
imageAvailabilityCache[url] = isAvailable;
try {
await AsyncStorage.setItem(`image_available:${url}`, isAvailable ? 'true' : 'false');
await mmkvStorage.setItem(`image_available:${url}`, isAvailable ? 'true' : 'false');
} catch (error) {
// Ignore AsyncStorage errors
}

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { logger } from '../utils/logger';
const SEASONS_STORAGE_KEY = 'selected_seasons';
@ -27,7 +27,7 @@ export function usePersistentSeasons() {
setIsLoading(true);
try {
const savedSeasonsJson = await AsyncStorage.getItem(SEASONS_STORAGE_KEY);
const savedSeasonsJson = await mmkvStorage.getItem(SEASONS_STORAGE_KEY);
const loadedSeasons = savedSeasonsJson ? JSON.parse(savedSeasonsJson) : {};
setSelectedSeasons(loadedSeasons);
// Update cache
@ -63,7 +63,7 @@ export function usePersistentSeasons() {
setSelectedSeasons(updatedSeasons);
// Save to AsyncStorage
await AsyncStorage.setItem(SEASONS_STORAGE_KEY, JSON.stringify(updatedSeasons));
await mmkvStorage.setItem(SEASONS_STORAGE_KEY, JSON.stringify(updatedSeasons));
} catch (error) {
logger.error('Failed to save selected season:', error);
}

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
// Simple event emitter for settings changes
class SettingsEventEmitter {
@ -168,11 +168,11 @@ export const useSettings = () => {
const loadSettings = async () => {
try {
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`;
const [scopedJson, legacyJson] = await Promise.all([
AsyncStorage.getItem(scopedKey),
AsyncStorage.getItem(SETTINGS_STORAGE_KEY),
mmkvStorage.getItem(scopedKey),
mmkvStorage.getItem(SETTINGS_STORAGE_KEY),
]);
const parsedScoped = scopedJson ? JSON.parse(scopedJson) : null;
const parsedLegacy = legacyJson ? JSON.parse(legacyJson) : null;
@ -182,10 +182,10 @@ export const useSettings = () => {
// Fallback: scan any existing user-scoped settings if current scope not set yet
if (!merged) {
try {
const allKeys = await AsyncStorage.getAllKeys();
const allKeys = await mmkvStorage.getAllKeys();
const candidateKeys = (allKeys || []).filter(k => k.endsWith(`:${SETTINGS_STORAGE_KEY}`));
if (candidateKeys.length > 0) {
const pairs = await AsyncStorage.multiGet(candidateKeys);
const pairs = await mmkvStorage.multiGet(candidateKeys);
for (const [, value] of pairs) {
if (value) {
try {
@ -221,15 +221,15 @@ export const useSettings = () => {
) => {
const newSettings = { ...settings, [key]: value };
try {
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`;
// Write to both scoped key (multi-user aware) and legacy key for backward compatibility
await Promise.all([
AsyncStorage.setItem(scopedKey, JSON.stringify(newSettings)),
AsyncStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings)),
]);
// Ensure a current scope exists to avoid future loads missing the chosen scope
await AsyncStorage.setItem('@user:current', scope);
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`;
// Write to both scoped key (multi-user aware) and legacy key for backward compatibility
await Promise.all([
mmkvStorage.setItem(scopedKey, JSON.stringify(newSettings)),
mmkvStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings)),
]);
// Ensure a current scope exists to avoid future loads missing the chosen scope
await mmkvStorage.setItem('@user:current', scope);
setSettings(newSettings);
if (__DEV__) console.log(`Setting updated: ${key}`, value);

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { useTraktIntegration } from './useTraktIntegration';
import { logger } from '../utils/logger';
@ -35,9 +35,9 @@ export function useTraktAutosyncSettings() {
try {
setIsLoading(true);
const [enabled, frequency, threshold] = await Promise.all([
AsyncStorage.getItem(TRAKT_AUTOSYNC_ENABLED_KEY),
AsyncStorage.getItem(TRAKT_SYNC_FREQUENCY_KEY),
AsyncStorage.getItem(TRAKT_COMPLETION_THRESHOLD_KEY)
mmkvStorage.getItem(TRAKT_AUTOSYNC_ENABLED_KEY),
mmkvStorage.getItem(TRAKT_SYNC_FREQUENCY_KEY),
mmkvStorage.getItem(TRAKT_COMPLETION_THRESHOLD_KEY)
]);
setSettings({
@ -56,7 +56,7 @@ export function useTraktAutosyncSettings() {
// Save individual setting
const saveSetting = useCallback(async (key: string, value: any) => {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
await mmkvStorage.setItem(key, JSON.stringify(value));
} catch (error) {
logger.error('[useTraktAutosyncSettings] Error saving setting:', error);
}

View file

@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
import { Platform } from 'react-native';
import { toastService } from '../services/toastService';
import UpdateService, { UpdateInfo } from '../services/updateService';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
interface UseUpdatePopupReturn {
showUpdatePopup: boolean;
@ -30,7 +30,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
try {
// Check if user has dismissed the popup for this version
const dismissedVersion = await AsyncStorage.getItem(UPDATE_POPUP_STORAGE_KEY);
const dismissedVersion = await mmkvStorage.getItem(UPDATE_POPUP_STORAGE_KEY);
const currentVersion = updateInfo.manifest?.id;
if (dismissedVersion === currentVersion && !forceCheck) {
@ -38,7 +38,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
}
// Check if user chose "later" recently (within 6 hours)
const updateLaterTimestamp = await AsyncStorage.getItem(UPDATE_LATER_STORAGE_KEY);
const updateLaterTimestamp = await mmkvStorage.getItem(UPDATE_LATER_STORAGE_KEY);
if (updateLaterTimestamp && !forceCheck) {
const laterTime = parseInt(updateLaterTimestamp);
const now = Date.now();
@ -91,7 +91,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
const handleUpdateLater = useCallback(async () => {
try {
// Store timestamp when user chose "later"
await AsyncStorage.setItem(UPDATE_LATER_STORAGE_KEY, Date.now().toString());
await mmkvStorage.setItem(UPDATE_LATER_STORAGE_KEY, Date.now().toString());
setShowUpdatePopup(false);
} catch (error) {
if (__DEV__) console.error('Error storing update later preference:', error);
@ -104,7 +104,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
// Store the current version ID so we don't show popup again for this version
const currentVersion = updateInfo.manifest?.id;
if (currentVersion) {
await AsyncStorage.setItem(UPDATE_POPUP_STORAGE_KEY, currentVersion);
await mmkvStorage.setItem(UPDATE_POPUP_STORAGE_KEY, currentVersion);
}
setShowUpdatePopup(false);
} catch (error) {
@ -150,7 +150,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
// Check if user hasn't dismissed this version
(async () => {
try {
const dismissedVersion = await AsyncStorage.getItem(UPDATE_POPUP_STORAGE_KEY);
const dismissedVersion = await mmkvStorage.getItem(UPDATE_POPUP_STORAGE_KEY);
const currentVersion = updateInfo.manifest?.id;
if (dismissedVersion !== currentVersion) {
@ -175,7 +175,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
const timer = setTimeout(() => {
(async () => {
try {
const lastCheckTs = await AsyncStorage.getItem(UPDATE_LAST_CHECK_TS_KEY);
const lastCheckTs = await mmkvStorage.getItem(UPDATE_LAST_CHECK_TS_KEY);
const last = lastCheckTs ? parseInt(lastCheckTs, 10) : 0;
const now = Date.now();
const sixHours = 6 * 60 * 60 * 1000; // Reduced from 24 hours
@ -183,7 +183,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
return; // Throttle: only auto-check once per 6h
}
await checkForUpdates();
await AsyncStorage.setItem(UPDATE_LAST_CHECK_TS_KEY, String(now));
await mmkvStorage.setItem(UPDATE_LAST_CHECK_TS_KEY, String(now));
} catch {
// ignore
}

View file

@ -3,7 +3,7 @@ import { NavigationContainer, DefaultTheme as NavigationDefaultTheme, DarkTheme
import { createNativeStackNavigator, NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack';
import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { useColorScheme, Platform, Animated, StatusBar, TouchableOpacity, View, Text, AppState, Easing, Dimensions } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { PaperProvider, MD3DarkTheme, MD3LightTheme, adaptNavigationTheme } from 'react-native-paper';
import type { MD3Theme } from 'react-native-paper';
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
@ -524,7 +524,7 @@ const MainTabs = () => {
let mounted = true;
const load = async () => {
try {
const flag = await AsyncStorage.getItem('@update_badge_pending');
const flag = await mmkvStorage.getItem('@update_badge_pending');
if (mounted) setHasUpdateBadge(flag === 'true');
} catch {}
};

View file

@ -14,7 +14,7 @@ import {
Switch,
} from 'react-native';
import CustomAlert from '../components/CustomAlert';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { useNavigation } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
@ -80,7 +80,7 @@ const AISettingsScreen: React.FC = () => {
const loadApiKey = async () => {
try {
const savedKey = await AsyncStorage.getItem('openrouter_api_key');
const savedKey = await mmkvStorage.getItem('openrouter_api_key');
if (savedKey) {
setApiKey(savedKey);
setIsKeySet(true);
@ -103,7 +103,7 @@ const AISettingsScreen: React.FC = () => {
setLoading(true);
try {
await AsyncStorage.setItem('openrouter_api_key', apiKey.trim());
await mmkvStorage.setItem('openrouter_api_key', apiKey.trim());
setIsKeySet(true);
openAlert('Success', 'OpenRouter API key saved successfully!');
} catch (error) {
@ -124,7 +124,7 @@ const AISettingsScreen: React.FC = () => {
label: 'Remove',
onPress: async () => {
try {
await AsyncStorage.removeItem('openrouter_api_key');
await mmkvStorage.removeItem('openrouter_api_key');
setApiKey('');
setIsKeySet(false);
openAlert('Success', 'API key removed successfully');

View file

@ -27,7 +27,7 @@ import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator';
import { logger } from '../utils/logger';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { BlurView as ExpoBlurView } from 'expo-blur';
import CustomAlert from '../components/CustomAlert';
@ -671,7 +671,7 @@ const AddonsScreen = () => {
});
// Get catalog settings to determine enabled count
const catalogSettingsJson = await AsyncStorage.getItem('catalog_settings');
const catalogSettingsJson = await mmkvStorage.getItem('catalog_settings');
if (catalogSettingsJson) {
const catalogSettings = JSON.parse(catalogSettingsJson);
const disabledCount = Object.entries(catalogSettings)

View file

@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { View, TextInput, Text, TouchableOpacity, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Dimensions, Animated, Easing, Keyboard } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { LinearGradient } from 'expo-linear-gradient';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
@ -196,7 +196,7 @@ const AuthScreen: React.FC = () => {
const handleSkipAuth = async () => {
try {
await AsyncStorage.setItem('showLoginHintToastOnce', 'true');
await mmkvStorage.setItem('showLoginHintToastOnce', 'true');
} catch {}
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' as never }] } as any);
};

View file

@ -38,7 +38,7 @@ if (Platform.OS === 'ios') {
}
import { logger } from '../utils/logger';
import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { catalogService, DataSource, StreamingContent } from '../services/catalogService';
import { tmdbService } from '../services/tmdbService';
@ -262,7 +262,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
useEffect(() => {
(async () => {
try {
const pref = await AsyncStorage.getItem('catalog_mobile_columns');
const pref = await mmkvStorage.getItem('catalog_mobile_columns');
if (pref === '2') setMobileColumnsPref(2);
else if (pref === '3') setMobileColumnsPref(3);
else setMobileColumnsPref('auto');

View file

@ -15,7 +15,7 @@ import {
Pressable,
Button,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { useNavigation } from '@react-navigation/native';
import { useTheme } from '../contexts/ThemeContext';
import { stremioService } from '../services/stremioService';
@ -294,11 +294,11 @@ const CatalogSettingsScreen = () => {
const availableCatalogs: CatalogSetting[] = [];
// Get saved enable/disable settings
const savedSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
const savedSettingsJson = await mmkvStorage.getItem(CATALOG_SETTINGS_KEY);
const savedEnabledSettings: { [key: string]: boolean } = savedSettingsJson ? JSON.parse(savedSettingsJson) : {};
// Get saved custom names
const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
const savedCustomNamesJson = await mmkvStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
const savedCustomNames: { [key: string]: string } = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {};
// Process each addon's catalogs
@ -371,7 +371,7 @@ const CatalogSettingsScreen = () => {
// Load mobile columns preference (phones only)
try {
const pref = await AsyncStorage.getItem(CATALOG_MOBILE_COLUMNS_KEY);
const pref = await mmkvStorage.getItem(CATALOG_MOBILE_COLUMNS_KEY);
if (pref === '2') setMobileColumns(2);
else if (pref === '3') setMobileColumns(3);
else setMobileColumns('auto');
@ -395,7 +395,7 @@ const CatalogSettingsScreen = () => {
const key = `${setting.addonId}:${setting.type}:${setting.catalogId}`;
settingsObj[key] = setting.enabled;
});
await AsyncStorage.setItem(CATALOG_SETTINGS_KEY, JSON.stringify(settingsObj));
await mmkvStorage.setItem(CATALOG_SETTINGS_KEY, JSON.stringify(settingsObj));
// Small delay to ensure AsyncStorage has fully persisted before triggering refresh
setTimeout(() => {
@ -461,7 +461,7 @@ const CatalogSettingsScreen = () => {
const settingKey = `${catalogToRename.addonId}:${catalogToRename.type}:${catalogToRename.catalogId}`;
try {
const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
const savedCustomNamesJson = await mmkvStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
const customNames: { [key: string]: string } = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {};
const trimmedNewName = currentRenameValue.trim();
@ -472,7 +472,7 @@ const CatalogSettingsScreen = () => {
customNames[settingKey] = trimmedNewName;
}
await AsyncStorage.setItem(CATALOG_CUSTOM_NAMES_KEY, JSON.stringify(customNames));
await mmkvStorage.setItem(CATALOG_CUSTOM_NAMES_KEY, JSON.stringify(customNames));
// Clear in-memory cache so new name is used immediately
try { clearCustomNameCache(); } catch {}
@ -550,7 +550,7 @@ const CatalogSettingsScreen = () => {
style={[styles.optionChip, mobileColumns === 'auto' && styles.optionChipSelected]}
onPress={async () => {
try {
await AsyncStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, 'auto');
await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, 'auto');
setMobileColumns('auto');
} catch {}
}}
@ -562,7 +562,7 @@ const CatalogSettingsScreen = () => {
style={[styles.optionChip, mobileColumns === 2 && styles.optionChipSelected]}
onPress={async () => {
try {
await AsyncStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '2');
await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '2');
setMobileColumns(2);
} catch {}
}}
@ -574,7 +574,7 @@ const CatalogSettingsScreen = () => {
style={[styles.optionChip, mobileColumns === 3 && styles.optionChipSelected]}
onPress={async () => {
try {
await AsyncStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '3');
await mmkvStorage.setItem(CATALOG_MOBILE_COLUMNS_KEY, '3');
setMobileColumns(3);
} catch {}
}}

View file

@ -14,7 +14,7 @@ import {
FlatList,
ActivityIndicator
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import FastImage from '@d11/react-native-fast-image';
@ -108,8 +108,8 @@ const ContributorsScreen: React.FC = () => {
// Check cache first (unless refreshing)
if (!isRefresh) {
try {
const cachedData = await AsyncStorage.getItem('github_contributors');
const cacheTimestamp = await AsyncStorage.getItem('github_contributors_timestamp');
const cachedData = await mmkvStorage.getItem('github_contributors');
const cacheTimestamp = await mmkvStorage.getItem('github_contributors_timestamp');
const now = Date.now();
const ONE_HOUR = 60 * 60 * 1000; // 1 hour cache
@ -124,8 +124,8 @@ const ContributorsScreen: React.FC = () => {
return;
} else {
// Remove invalid cache
await AsyncStorage.removeItem('github_contributors');
await AsyncStorage.removeItem('github_contributors_timestamp');
await mmkvStorage.removeItem('github_contributors');
await mmkvStorage.removeItem('github_contributors_timestamp');
if (__DEV__) console.log('Removed invalid contributors cache');
}
}
@ -134,8 +134,8 @@ const ContributorsScreen: React.FC = () => {
if (__DEV__) console.error('Cache read error:', cacheError);
// Remove corrupted cache
try {
await AsyncStorage.removeItem('github_contributors');
await AsyncStorage.removeItem('github_contributors_timestamp');
await mmkvStorage.removeItem('github_contributors');
await mmkvStorage.removeItem('github_contributors_timestamp');
} catch {}
}
}
@ -145,16 +145,16 @@ const ContributorsScreen: React.FC = () => {
setContributors(data);
// Only cache valid data
try {
await AsyncStorage.setItem('github_contributors', JSON.stringify(data));
await AsyncStorage.setItem('github_contributors_timestamp', Date.now().toString());
await mmkvStorage.setItem('github_contributors', JSON.stringify(data));
await mmkvStorage.setItem('github_contributors_timestamp', Date.now().toString());
} catch (cacheError) {
if (__DEV__) console.error('Cache write error:', cacheError);
}
} else {
// Clear any existing cache if we get invalid data
try {
await AsyncStorage.removeItem('github_contributors');
await AsyncStorage.removeItem('github_contributors_timestamp');
await mmkvStorage.removeItem('github_contributors');
await mmkvStorage.removeItem('github_contributors_timestamp');
} catch {}
setError('Unable to load contributors. This might be due to GitHub API rate limits.');
}
@ -171,12 +171,12 @@ const ContributorsScreen: React.FC = () => {
// Clear any invalid cache on mount
const clearInvalidCache = async () => {
try {
const cachedData = await AsyncStorage.getItem('github_contributors');
const cachedData = await mmkvStorage.getItem('github_contributors');
if (cachedData) {
const parsedData = JSON.parse(cachedData);
if (!parsedData || !Array.isArray(parsedData) || parsedData.length === 0) {
await AsyncStorage.removeItem('github_contributors');
await AsyncStorage.removeItem('github_contributors_timestamp');
await mmkvStorage.removeItem('github_contributors');
await mmkvStorage.removeItem('github_contributors_timestamp');
if (__DEV__) console.log('Cleared invalid cache on mount');
}
}

View file

@ -58,7 +58,7 @@ import { useTheme } from '../contexts/ThemeContext';
import type { Theme } from '../contexts/ThemeContext';
import { useLoading } from '../contexts/LoadingContext';
import * as ScreenOrientation from 'expo-screen-orientation';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useToast } from '../contexts/ToastContext';
import FirstTimeWelcome from '../components/FirstTimeWelcome';
@ -155,7 +155,7 @@ const HomeScreen = () => {
try {
const [addons, catalogSettingsJson, addonManifests] = await Promise.all([
catalogService.getAllAddons(),
AsyncStorage.getItem(CATALOG_SETTINGS_KEY),
mmkvStorage.getItem(CATALOG_SETTINGS_KEY),
stremioService.getInstalledAddonsAsync()
]);
@ -347,10 +347,10 @@ const HomeScreen = () => {
let hideTimer: any;
(async () => {
try {
const flag = await AsyncStorage.getItem('showLoginHintToastOnce');
const flag = await mmkvStorage.getItem('showLoginHintToastOnce');
if (flag === 'true') {
setHintVisible(true);
await AsyncStorage.removeItem('showLoginHintToastOnce');
await mmkvStorage.removeItem('showLoginHintToastOnce');
hideTimer = setTimeout(() => setHintVisible(false), 2000);
// Also show a global toast for consistency across screens
// showInfo('Sign In Available', 'You can sign in anytime from Settings → Account');

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { DeviceEventEmitter } from 'react-native';
import { Share } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { useToast } from '../contexts/ToastContext';
import DropUpMenu from '../components/home/DropUpMenu';
import {
@ -289,7 +289,7 @@ const LibraryScreen = () => {
traktId: typeof (item as any).traktId === 'number' ? (item as any).traktId : 0,
};
const key = `watched:${item.type}:${item.id}`;
const watched = await AsyncStorage.getItem(key);
const watched = await mmkvStorage.getItem(key);
return {
...libraryItem,
watched: watched === 'true'
@ -323,7 +323,7 @@ const LibraryScreen = () => {
traktId: typeof (item as any).traktId === 'number' ? (item as any).traktId : 0,
};
const key = `watched:${item.type}:${item.id}`;
const watched = await AsyncStorage.getItem(key);
const watched = await mmkvStorage.getItem(key);
return {
...libraryItem,
watched: watched === 'true'
@ -1008,7 +1008,7 @@ const LibraryScreen = () => {
// Use AsyncStorage to store watched status by key
const key = `watched:${selectedItem.type}:${selectedItem.id}`;
const newWatched = !selectedItem.watched;
await AsyncStorage.setItem(key, newWatched ? 'true' : 'false');
await mmkvStorage.setItem(key, newWatched ? 'true' : 'false');
showInfo(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', newWatched ? 'Item marked as watched' : 'Item marked as unwatched');
// Instantly update local state
setLibraryItems(prev => prev.map(item =>

View file

@ -18,7 +18,7 @@ import {
import CustomAlert from '../components/CustomAlert';
import { useNavigation } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { useTheme } from '../contexts/ThemeContext';
import { logger } from '../utils/logger';
import { RATING_PROVIDERS } from '../components/metadata/RatingsSection';
@ -31,7 +31,7 @@ const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
// Function to check if MDBList is enabled
export const isMDBListEnabled = async (): Promise<boolean> => {
try {
const enabledSetting = await AsyncStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
const enabledSetting = await mmkvStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
return enabledSetting === 'true';
} catch (error) {
logger.error('[MDBList] Error checking if MDBList is enabled:', error);
@ -48,7 +48,7 @@ export const getMDBListAPIKey = async (): Promise<string | null> => {
return null;
}
return await AsyncStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
return await mmkvStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
} catch (error) {
logger.error('[MDBList] Error retrieving API key:', error);
return null;
@ -388,14 +388,14 @@ const MDBListSettingsScreen = () => {
const loadMdbListEnabledSetting = async () => {
logger.log('[MDBListSettingsScreen] Loading MDBList enabled setting');
try {
const savedSetting = await AsyncStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
const savedSetting = await mmkvStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
if (savedSetting !== null) {
setIsMdbListEnabled(savedSetting === 'true');
logger.log('[MDBListSettingsScreen] MDBList enabled setting:', savedSetting === 'true');
} else {
// Default to disabled if no setting found
setIsMdbListEnabled(false);
await AsyncStorage.setItem(MDBLIST_ENABLED_STORAGE_KEY, 'false');
await mmkvStorage.setItem(MDBLIST_ENABLED_STORAGE_KEY, 'false');
logger.log('[MDBListSettingsScreen] MDBList enabled setting not found, defaulting to false');
}
} catch (error) {
@ -409,7 +409,7 @@ const MDBListSettingsScreen = () => {
try {
const newValue = !isMdbListEnabled;
setIsMdbListEnabled(newValue);
await AsyncStorage.setItem(MDBLIST_ENABLED_STORAGE_KEY, newValue.toString());
await mmkvStorage.setItem(MDBLIST_ENABLED_STORAGE_KEY, newValue.toString());
logger.log('[MDBListSettingsScreen] MDBList enabled set to:', newValue);
} catch (error) {
logger.error('[MDBListSettingsScreen] Failed to save MDBList enabled setting:', error);
@ -419,7 +419,7 @@ const MDBListSettingsScreen = () => {
const loadApiKey = async () => {
logger.log('[MDBListSettingsScreen] Loading API key from storage');
try {
const savedKey = await AsyncStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
const savedKey = await mmkvStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
logger.log('[MDBListSettingsScreen] API key status:', savedKey ? 'Found' : 'Not found');
if (savedKey) {
setApiKey(savedKey);
@ -438,7 +438,7 @@ const MDBListSettingsScreen = () => {
const loadProviderSettings = async () => {
try {
const savedSettings = await AsyncStorage.getItem(RATING_PROVIDERS_STORAGE_KEY);
const savedSettings = await mmkvStorage.getItem(RATING_PROVIDERS_STORAGE_KEY);
if (savedSettings) {
setEnabledProviders(JSON.parse(savedSettings));
} else {
@ -448,7 +448,7 @@ const MDBListSettingsScreen = () => {
return acc;
}, {} as Record<string, boolean>);
setEnabledProviders(defaultSettings);
await AsyncStorage.setItem(RATING_PROVIDERS_STORAGE_KEY, JSON.stringify(defaultSettings));
await mmkvStorage.setItem(RATING_PROVIDERS_STORAGE_KEY, JSON.stringify(defaultSettings));
}
} catch (error) {
logger.error('[MDBListSettingsScreen] Failed to load provider settings:', error);
@ -462,7 +462,7 @@ const MDBListSettingsScreen = () => {
[providerId]: !enabledProviders[providerId]
};
setEnabledProviders(newSettings);
await AsyncStorage.setItem(RATING_PROVIDERS_STORAGE_KEY, JSON.stringify(newSettings));
await mmkvStorage.setItem(RATING_PROVIDERS_STORAGE_KEY, JSON.stringify(newSettings));
} catch (error) {
logger.error('[MDBListSettingsScreen] Failed to save provider settings:', error);
}
@ -481,7 +481,7 @@ const MDBListSettingsScreen = () => {
}
logger.log('[MDBListSettingsScreen] Saving API key');
await AsyncStorage.setItem(MDBLIST_API_KEY_STORAGE_KEY, trimmedKey);
await mmkvStorage.setItem(MDBLIST_API_KEY_STORAGE_KEY, trimmedKey);
setIsKeySet(true);
setTestResult({ success: true, message: 'API key saved successfully.' });
logger.log('[MDBListSettingsScreen] API key saved successfully');
@ -506,7 +506,7 @@ const MDBListSettingsScreen = () => {
onPress: async () => {
logger.log('[MDBListSettingsScreen] Proceeding with API key clear');
try {
await AsyncStorage.removeItem(MDBLIST_API_KEY_STORAGE_KEY);
await mmkvStorage.removeItem(MDBLIST_API_KEY_STORAGE_KEY);
setApiKey('');
setIsKeySet(false);
setTestResult(null);

View file

@ -27,7 +27,7 @@ import Animated, {
import { useTheme } from '../contexts/ThemeContext';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
const { width, height } = Dimensions.get('window');
@ -125,8 +125,8 @@ const OnboardingScreen = () => {
// Skip login: proceed to app and show a one-time hint toast
(async () => {
try {
await AsyncStorage.setItem('hasCompletedOnboarding', 'true');
await AsyncStorage.setItem('showLoginHintToastOnce', 'true');
await mmkvStorage.setItem('hasCompletedOnboarding', 'true');
await mmkvStorage.setItem('showLoginHintToastOnce', 'true');
} catch {}
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] });
})();
@ -134,7 +134,7 @@ const OnboardingScreen = () => {
const handleGetStarted = async () => {
try {
await AsyncStorage.setItem('hasCompletedOnboarding', 'true');
await mmkvStorage.setItem('hasCompletedOnboarding', 'true');
// After onboarding, go directly to main app
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] });
} catch (error) {

View file

@ -15,7 +15,7 @@ import { useNavigation } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import { useTraktContext } from '../contexts/TraktContext';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import CustomAlert from '../components/CustomAlert';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
@ -60,7 +60,7 @@ const ProfilesScreen: React.FC = () => {
const loadProfiles = useCallback(async () => {
try {
setIsLoading(true);
const storedProfiles = await AsyncStorage.getItem(PROFILE_STORAGE_KEY);
const storedProfiles = await mmkvStorage.getItem(PROFILE_STORAGE_KEY);
if (storedProfiles) {
setProfiles(JSON.parse(storedProfiles));
} else {
@ -72,7 +72,7 @@ const ProfilesScreen: React.FC = () => {
createdAt: new Date().getTime()
};
setProfiles([defaultProfile]);
await AsyncStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify([defaultProfile]));
await mmkvStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify([defaultProfile]));
}
} catch (error) {
if (__DEV__) console.error('Error loading profiles:', error);
@ -99,7 +99,7 @@ const ProfilesScreen: React.FC = () => {
// Save profiles to AsyncStorage
const saveProfiles = useCallback(async (updatedProfiles: Profile[]) => {
try {
await AsyncStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(updatedProfiles));
await mmkvStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(updatedProfiles));
} catch (error) {
if (__DEV__) console.error('Error saving profiles:', error);
openAlert('Error', 'Failed to save profiles');

View file

@ -26,7 +26,7 @@ import FastImage from '@d11/react-native-fast-image';
import debounce from 'lodash/debounce';
import { DropUpMenu } from '../components/home/DropUpMenu';
import { DeviceEventEmitter, Share } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import Animated, {
FadeIn,
FadeOut,
@ -250,7 +250,7 @@ const SearchScreen = () => {
const found = items.find((libItem: any) => libItem.id === selectedItem.id && libItem.type === selectedItem.type);
setIsSaved(!!found);
// Check watched status
const val = await AsyncStorage.getItem(`watched:${selectedItem.type}:${selectedItem.id}`);
const val = await mmkvStorage.getItem(`watched:${selectedItem.type}:${selectedItem.id}`);
setIsWatched(val === 'true');
})();
}, [selectedItem]);
@ -349,7 +349,7 @@ const SearchScreen = () => {
const loadRecentSearches = async () => {
try {
const savedSearches = await AsyncStorage.getItem(RECENT_SEARCHES_KEY);
const savedSearches = await mmkvStorage.getItem(RECENT_SEARCHES_KEY);
if (savedSearches) {
setRecentSearches(JSON.parse(savedSearches));
}
@ -367,7 +367,7 @@ const SearchScreen = () => {
].slice(0, MAX_RECENT_SEARCHES);
// Save to AsyncStorage
AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches));
mmkvStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches));
return newRecentSearches;
});
@ -533,7 +533,7 @@ const SearchScreen = () => {
const newRecentSearches = [...recentSearches];
newRecentSearches.splice(index, 1);
setRecentSearches(newRecentSearches);
AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches));
mmkvStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches));
}}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
style={styles.recentSearchDeleteButton}
@ -558,7 +558,7 @@ const SearchScreen = () => {
const [watched, setWatched] = React.useState(false);
React.useEffect(() => {
const updateWatched = () => {
AsyncStorage.getItem(`watched:${item.type}:${item.id}`).then(val => setWatched(val === 'true'));
mmkvStorage.getItem(`watched:${item.type}:${item.id}`).then(val => setWatched(val === 'true'));
};
updateWatched();
const sub = DeviceEventEmitter.addListener('watchedStatusChanged', updateWatched);
@ -977,7 +977,7 @@ const SearchScreen = () => {
case 'watched': {
const key = `watched:${selectedItem.type}:${selectedItem.id}`;
const newWatched = !isWatched;
await AsyncStorage.setItem(key, newWatched ? 'true' : 'false');
await mmkvStorage.setItem(key, newWatched ? 'true' : 'false');
setIsWatched(newWatched);
break;
}

View file

@ -14,7 +14,7 @@ import {
Linking,
Clipboard
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import FastImage from '@d11/react-native-fast-image';
@ -271,7 +271,7 @@ const SettingsScreen: React.FC = () => {
let mounted = true;
(async () => {
try {
const flag = await AsyncStorage.getItem('@update_badge_pending');
const flag = await mmkvStorage.getItem('@update_badge_pending');
if (mounted) setHasUpdateBadge(flag === 'true');
} catch {}
})();
@ -329,7 +329,7 @@ const SettingsScreen: React.FC = () => {
});
// Load saved catalog settings
const catalogSettingsJson = await AsyncStorage.getItem('catalog_settings');
const catalogSettingsJson = await mmkvStorage.getItem('catalog_settings');
if (catalogSettingsJson) {
const catalogSettings = JSON.parse(catalogSettingsJson);
// Filter out _lastUpdate key and count only explicitly disabled catalogs
@ -344,11 +344,11 @@ const SettingsScreen: React.FC = () => {
}
// Check MDBList API key status
const mdblistKey = await AsyncStorage.getItem('mdblist_api_key');
const mdblistKey = await mmkvStorage.getItem('mdblist_api_key');
setMdblistKeySet(!!mdblistKey);
// Check OpenRouter API key status
const openRouterKey = await AsyncStorage.getItem('openrouter_api_key');
const openRouterKey = await mmkvStorage.getItem('openrouter_api_key');
setOpenRouterKeySet(!!openRouterKey);
// Load GitHub total downloads (initial load only, polling happens in useEffect)
@ -459,7 +459,7 @@ const SettingsScreen: React.FC = () => {
label: 'Clear',
onPress: async () => {
try {
await AsyncStorage.removeItem('mdblist_cache');
await mmkvStorage.removeItem('mdblist_cache');
openAlert('Success', 'MDBList cache has been cleared.');
} catch (error) {
openAlert('Error', 'Could not clear MDBList cache.');
@ -746,7 +746,7 @@ const SettingsScreen: React.FC = () => {
icon="refresh-ccw"
onPress={async () => {
try {
await AsyncStorage.removeItem('hasCompletedOnboarding');
await mmkvStorage.removeItem('hasCompletedOnboarding');
openAlert('Success', 'Onboarding has been reset. Restart the app to see the onboarding flow.');
} catch (error) {
openAlert('Error', 'Failed to reset onboarding.');
@ -768,7 +768,7 @@ const SettingsScreen: React.FC = () => {
label: 'Clear',
onPress: async () => {
try {
await AsyncStorage.clear();
await mmkvStorage.clear();
openAlert('Success', 'All data cleared. Please restart the app.');
} catch (error) {
openAlert('Error', 'Failed to clear data.');
@ -823,7 +823,7 @@ const SettingsScreen: React.FC = () => {
badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined}
onPress={async () => {
if (Platform.OS === 'android') {
try { await AsyncStorage.removeItem('@update_badge_pending'); } catch {}
try { await mmkvStorage.removeItem('@update_badge_pending'); } catch {}
setHasUpdateBadge(false);
}
navigation.navigate('Update');

View file

@ -20,7 +20,7 @@ import {
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import FastImage from '@d11/react-native-fast-image';
import { tmdbService } from '../services/tmdbService';
import { useSettings } from '../hooks/useSettings';
@ -124,8 +124,8 @@ const TMDBSettingsScreen = () => {
logger.log('[TMDBSettingsScreen] Loading settings from storage');
try {
const [savedKey, savedUseCustomKey] = await Promise.all([
AsyncStorage.getItem(TMDB_API_KEY_STORAGE_KEY),
AsyncStorage.getItem(USE_CUSTOM_TMDB_API_KEY)
mmkvStorage.getItem(TMDB_API_KEY_STORAGE_KEY),
mmkvStorage.getItem(USE_CUSTOM_TMDB_API_KEY)
]);
logger.log('[TMDBSettingsScreen] API key status:', savedKey ? 'Found' : 'Not found');
@ -164,8 +164,8 @@ const TMDBSettingsScreen = () => {
// Test the API key to make sure it works
if (await testApiKey(trimmedKey)) {
logger.log('[TMDBSettingsScreen] API key test successful, saving key');
await AsyncStorage.setItem(TMDB_API_KEY_STORAGE_KEY, trimmedKey);
await AsyncStorage.setItem(USE_CUSTOM_TMDB_API_KEY, 'true');
await mmkvStorage.setItem(TMDB_API_KEY_STORAGE_KEY, trimmedKey);
await mmkvStorage.setItem(USE_CUSTOM_TMDB_API_KEY, 'true');
setIsKeySet(true);
setUseCustomKey(true);
setTestResult({ success: true, message: 'API key verified and saved successfully.' });
@ -217,8 +217,8 @@ const TMDBSettingsScreen = () => {
onPress: async () => {
logger.log('[TMDBSettingsScreen] Proceeding with API key clear');
try {
await AsyncStorage.removeItem(TMDB_API_KEY_STORAGE_KEY);
await AsyncStorage.setItem(USE_CUSTOM_TMDB_API_KEY, 'false');
await mmkvStorage.removeItem(TMDB_API_KEY_STORAGE_KEY);
await mmkvStorage.setItem(USE_CUSTOM_TMDB_API_KEY, 'false');
setApiKey('');
setIsKeySet(false);
setUseCustomKey(false);
@ -237,7 +237,7 @@ const TMDBSettingsScreen = () => {
const toggleUseCustomKey = async (value: boolean) => {
logger.log('[TMDBSettingsScreen] Toggle use custom key:', value);
try {
await AsyncStorage.setItem(USE_CUSTOM_TMDB_API_KEY, value ? 'true' : 'false');
await mmkvStorage.setItem(USE_CUSTOM_TMDB_API_KEY, value ? 'true' : 'false');
setUseCustomKey(value);
if (!value) {
@ -370,7 +370,7 @@ const TMDBSettingsScreen = () => {
const handleShowSelect = (show: typeof EXAMPLE_SHOWS[0]) => {
setSelectedShow(show);
try {
AsyncStorage.setItem('tmdb_settings_selected_show', show.imdbId);
mmkvStorage.setItem('tmdb_settings_selected_show', show.imdbId);
} catch (e) {
if (__DEV__) console.error('Error saving selected show:', e);
}
@ -420,7 +420,7 @@ const TMDBSettingsScreen = () => {
useEffect(() => {
const loadSelectedShow = async () => {
try {
const savedShowId = await AsyncStorage.getItem('tmdb_settings_selected_show');
const savedShowId = await mmkvStorage.getItem('tmdb_settings_selected_show');
if (savedShowId) {
const foundShow = EXAMPLE_SHOWS.find(show => show.imdbId === savedShowId);
if (foundShow) {

View file

@ -20,7 +20,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import UpdateService from '../services/updateService';
import CustomAlert from '../components/CustomAlert';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { useGithubMajorUpdate } from '../hooks/useGithubMajorUpdate';
import { getDisplayedAppVersion } from '../utils/version';
import { isAnyUpgrade } from '../services/githubReleaseService';
@ -146,7 +146,7 @@ const UpdateScreen: React.FC = () => {
if (Platform.OS === 'android') {
// ensure badge clears when entering this screen
(async () => {
try { await AsyncStorage.removeItem('@update_badge_pending'); } catch {}
try { await mmkvStorage.removeItem('@update_badge_pending'); } catch {}
})();
}
checkForUpdates();

View file

@ -1,4 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
export type AuthUser = {
id: string;
@ -30,13 +30,13 @@ class AccountService {
}
async signOut(): Promise<void> {
await AsyncStorage.removeItem(USER_DATA_KEY);
await AsyncStorage.setItem(USER_SCOPE_KEY, 'local');
await mmkvStorage.removeItem(USER_DATA_KEY);
await mmkvStorage.setItem(USER_SCOPE_KEY, 'local');
}
async getCurrentUser(): Promise<AuthUser | null> {
try {
const userData = await AsyncStorage.getItem(USER_DATA_KEY);
const userData = await mmkvStorage.getItem(USER_DATA_KEY);
if (!userData) return null;
return JSON.parse(userData);
} catch {
@ -50,7 +50,7 @@ class AccountService {
if (!currentUser) return 'Not authenticated';
const updatedUser = { ...currentUser, ...partial };
await AsyncStorage.setItem(USER_DATA_KEY, JSON.stringify(updatedUser));
await mmkvStorage.setItem(USER_DATA_KEY, JSON.stringify(updatedUser));
return null;
} catch {
return 'Failed to update profile';
@ -61,8 +61,8 @@ class AccountService {
const user = await this.getCurrentUser();
if (user?.id) return user.id;
// Guest scope
const scope = (await AsyncStorage.getItem(USER_SCOPE_KEY)) || 'local';
if (!scope) await AsyncStorage.setItem(USER_SCOPE_KEY, 'local');
const scope = (await mmkvStorage.getItem(USER_SCOPE_KEY)) || 'local';
if (!scope) await mmkvStorage.setItem(USER_SCOPE_KEY, 'local');
return scope || 'local';
}
}

View file

@ -1,4 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
export interface ChatMessage {
id: string;
@ -112,7 +112,7 @@ class AIService {
async initialize(): Promise<boolean> {
try {
this.apiKey = await AsyncStorage.getItem('openrouter_api_key');
this.apiKey = await mmkvStorage.getItem('openrouter_api_key');
return !!this.apiKey;
} catch (error) {
if (__DEV__) console.error('Failed to initialize AI service:', error);

View file

@ -1,4 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
import * as FileSystem from 'expo-file-system/legacy';
import { Platform } from 'react-native';
import { logger } from '../utils/logger';
@ -373,7 +373,7 @@ export class BackupService {
// Private helper methods for data collection
private async getUserScope(): Promise<string> {
try {
const scope = await AsyncStorage.getItem('@user:current');
const scope = await mmkvStorage.getItem('@user:current');
return scope || 'local';
} catch {
return 'local';
@ -384,7 +384,7 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:app_settings`;
const settingsJson = await AsyncStorage.getItem(scopedKey);
const settingsJson = await mmkvStorage.getItem(scopedKey);
return settingsJson ? JSON.parse(settingsJson) : DEFAULT_SETTINGS;
} catch (error) {
logger.error('[BackupService] Failed to get settings:', error);
@ -396,7 +396,7 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:stremio-library`;
const libraryJson = await AsyncStorage.getItem(scopedKey);
const libraryJson = await mmkvStorage.getItem(scopedKey);
if (libraryJson) {
const parsed = JSON.parse(libraryJson);
return Array.isArray(parsed) ? parsed : Object.values(parsed);
@ -411,14 +411,14 @@ export class BackupService {
private async getWatchProgress(): Promise<Record<string, any>> {
try {
const scope = await this.getUserScope();
const allKeys = await AsyncStorage.getAllKeys();
const allKeys = await mmkvStorage.getAllKeys();
const watchProgressKeys = allKeys.filter(key =>
key.startsWith(`@user:${scope}:@watch_progress:`)
);
const watchProgress: Record<string, any> = {};
if (watchProgressKeys.length > 0) {
const pairs = await AsyncStorage.multiGet(watchProgressKeys);
const pairs = await mmkvStorage.multiGet(watchProgressKeys);
for (const [key, value] of pairs) {
if (value) {
watchProgress[key] = JSON.parse(value);
@ -436,7 +436,7 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:stremio-addons`;
const addonsJson = await AsyncStorage.getItem(scopedKey);
const addonsJson = await mmkvStorage.getItem(scopedKey);
return addonsJson ? JSON.parse(addonsJson) : [];
} catch (error) {
logger.error('[BackupService] Failed to get addons:', error);
@ -446,7 +446,7 @@ export class BackupService {
private async getDownloads(): Promise<DownloadItem[]> {
try {
const downloadsJson = await AsyncStorage.getItem('downloads_state_v1');
const downloadsJson = await mmkvStorage.getItem('downloads_state_v1');
return downloadsJson ? JSON.parse(downloadsJson) : [];
} catch (error) {
logger.error('[BackupService] Failed to get downloads:', error);
@ -458,11 +458,11 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:@subtitle_settings`;
const subtitlesJson = await AsyncStorage.getItem(scopedKey);
const subtitlesJson = await mmkvStorage.getItem(scopedKey);
let subtitleSettings = subtitlesJson ? JSON.parse(subtitlesJson) : {};
// Also check for legacy subtitle size preference
const legacySubtitleSize = await AsyncStorage.getItem('@subtitle_size_preference');
const legacySubtitleSize = await mmkvStorage.getItem('@subtitle_size_preference');
if (legacySubtitleSize && !subtitleSettings.subtitleSize) {
const legacySize = parseInt(legacySubtitleSize, 10);
if (!Number.isNaN(legacySize) && legacySize > 0) {
@ -481,7 +481,7 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:@wp_tombstones`;
const tombstonesJson = await AsyncStorage.getItem(scopedKey);
const tombstonesJson = await mmkvStorage.getItem(scopedKey);
return tombstonesJson ? JSON.parse(tombstonesJson) : {};
} catch (error) {
logger.error('[BackupService] Failed to get tombstones:', error);
@ -493,7 +493,7 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:@continue_watching_removed`;
const removedJson = await AsyncStorage.getItem(scopedKey);
const removedJson = await mmkvStorage.getItem(scopedKey);
return removedJson ? JSON.parse(removedJson) : {};
} catch (error) {
logger.error('[BackupService] Failed to get continue watching removed:', error);
@ -504,14 +504,14 @@ export class BackupService {
private async getContentDuration(): Promise<Record<string, number>> {
try {
const scope = await this.getUserScope();
const allKeys = await AsyncStorage.getAllKeys();
const allKeys = await mmkvStorage.getAllKeys();
const durationKeys = allKeys.filter(key =>
key.startsWith(`@user:${scope}:@content_duration:`)
);
const contentDuration: Record<string, number> = {};
if (durationKeys.length > 0) {
const pairs = await AsyncStorage.multiGet(durationKeys);
const pairs = await mmkvStorage.multiGet(durationKeys);
for (const [key, value] of pairs) {
if (value) {
contentDuration[key] = JSON.parse(value);
@ -527,7 +527,7 @@ export class BackupService {
private async getSyncQueue(): Promise<any[]> {
try {
const syncQueueJson = await AsyncStorage.getItem('@sync_queue');
const syncQueueJson = await mmkvStorage.getItem('@sync_queue');
return syncQueueJson ? JSON.parse(syncQueueJson) : [];
} catch (error) {
logger.error('[BackupService] Failed to get sync queue:', error);
@ -538,7 +538,7 @@ export class BackupService {
private async getTraktSettings(): Promise<any> {
try {
// Get general Trakt settings
const traktSettingsJson = await AsyncStorage.getItem('trakt_settings');
const traktSettingsJson = await mmkvStorage.getItem('trakt_settings');
const traktSettings = traktSettingsJson ? JSON.parse(traktSettingsJson) : {};
// Get authentication tokens
@ -550,12 +550,12 @@ export class BackupService {
syncFrequency,
completionThreshold
] = await Promise.all([
AsyncStorage.getItem('trakt_access_token'),
AsyncStorage.getItem('trakt_refresh_token'),
AsyncStorage.getItem('trakt_token_expiry'),
AsyncStorage.getItem('trakt_autosync_enabled'),
AsyncStorage.getItem('trakt_sync_frequency'),
AsyncStorage.getItem('trakt_completion_threshold')
mmkvStorage.getItem('trakt_access_token'),
mmkvStorage.getItem('trakt_refresh_token'),
mmkvStorage.getItem('trakt_token_expiry'),
mmkvStorage.getItem('trakt_autosync_enabled'),
mmkvStorage.getItem('trakt_sync_frequency'),
mmkvStorage.getItem('trakt_completion_threshold')
]);
return {
@ -583,21 +583,21 @@ export class BackupService {
private async getLocalScrapers(): Promise<any> {
try {
// Get main scraper configurations
const localScrapersJson = await AsyncStorage.getItem('local-scrapers');
const localScrapersJson = await mmkvStorage.getItem('local-scrapers');
// Get repository settings
const repoUrl = await AsyncStorage.getItem('scraper-repository-url');
const repositories = await AsyncStorage.getItem('scraper-repositories');
const currentRepo = await AsyncStorage.getItem('current-repository-id');
const scraperSettings = await AsyncStorage.getItem('scraper-settings');
const repoUrl = await mmkvStorage.getItem('scraper-repository-url');
const repositories = await mmkvStorage.getItem('scraper-repositories');
const currentRepo = await mmkvStorage.getItem('current-repository-id');
const scraperSettings = await mmkvStorage.getItem('scraper-settings');
// Get all scraper code cache keys
const allKeys = await AsyncStorage.getAllKeys();
const allKeys = await mmkvStorage.getAllKeys();
const scraperCodeKeys = allKeys.filter(key => key.startsWith('scraper-code-'));
const scraperCode: Record<string, string> = {};
if (scraperCodeKeys.length > 0) {
const codePairs = await AsyncStorage.multiGet(scraperCodeKeys);
const codePairs = await mmkvStorage.multiGet(scraperCodeKeys);
for (const [key, value] of codePairs) {
if (value) {
scraperCode[key] = value;
@ -622,8 +622,8 @@ export class BackupService {
private async getApiKeys(): Promise<{ mdblistApiKey?: string; openRouterApiKey?: string }> {
try {
const [mdblistKey, openRouterKey] = await Promise.all([
AsyncStorage.getItem('mdblist_api_key'),
AsyncStorage.getItem('openrouter_api_key')
mmkvStorage.getItem('mdblist_api_key'),
mmkvStorage.getItem('openrouter_api_key')
]);
return {
@ -638,7 +638,7 @@ export class BackupService {
private async getCatalogSettings(): Promise<any> {
try {
const catalogSettingsJson = await AsyncStorage.getItem('catalog_settings');
const catalogSettingsJson = await mmkvStorage.getItem('catalog_settings');
return catalogSettingsJson ? JSON.parse(catalogSettingsJson) : null;
} catch (error) {
logger.error('[BackupService] Failed to get catalog settings:', error);
@ -653,9 +653,9 @@ export class BackupService {
// Try scoped key first, then legacy keys
const [scopedOrder, legacyOrder, localOrder] = await Promise.all([
AsyncStorage.getItem(scopedKey),
AsyncStorage.getItem('stremio-addon-order'),
AsyncStorage.getItem('@user:local:stremio-addon-order')
mmkvStorage.getItem(scopedKey),
mmkvStorage.getItem('stremio-addon-order'),
mmkvStorage.getItem('@user:local:stremio-addon-order')
]);
const orderJson = scopedOrder || legacyOrder || localOrder;
@ -668,7 +668,7 @@ export class BackupService {
private async getRemovedAddons(): Promise<string[]> {
try {
const removedAddonsJson = await AsyncStorage.getItem('user_removed_addons');
const removedAddonsJson = await mmkvStorage.getItem('user_removed_addons');
return removedAddonsJson ? JSON.parse(removedAddonsJson) : [];
} catch (error) {
logger.error('[BackupService] Failed to get removed addons:', error);
@ -678,7 +678,7 @@ export class BackupService {
private async getGlobalSeasonViewMode(): Promise<string | undefined> {
try {
const mode = await AsyncStorage.getItem('global_season_view_mode');
const mode = await mmkvStorage.getItem('global_season_view_mode');
return mode || undefined;
} catch (error) {
logger.error('[BackupService] Failed to get global season view mode:', error);
@ -688,7 +688,7 @@ export class BackupService {
private async getHasCompletedOnboarding(): Promise<boolean | undefined> {
try {
const value = await AsyncStorage.getItem('hasCompletedOnboarding');
const value = await mmkvStorage.getItem('hasCompletedOnboarding');
return value === 'true' ? true : value === 'false' ? false : undefined;
} catch (error) {
logger.error('[BackupService] Failed to get has completed onboarding:', error);
@ -698,7 +698,7 @@ export class BackupService {
private async getShowLoginHintToastOnce(): Promise<boolean | undefined> {
try {
const value = await AsyncStorage.getItem('showLoginHintToastOnce');
const value = await mmkvStorage.getItem('showLoginHintToastOnce');
return value === 'true' ? true : value === 'false' ? false : undefined;
} catch (error) {
logger.error('[BackupService] Failed to get show login hint toast once:', error);
@ -711,7 +711,7 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:app_settings`;
await AsyncStorage.setItem(scopedKey, JSON.stringify(settings));
await mmkvStorage.setItem(scopedKey, JSON.stringify(settings));
logger.info('[BackupService] Settings restored');
} catch (error) {
logger.error('[BackupService] Failed to restore settings:', error);
@ -722,7 +722,7 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:stremio-library`;
await AsyncStorage.setItem(scopedKey, JSON.stringify(library));
await mmkvStorage.setItem(scopedKey, JSON.stringify(library));
logger.info('[BackupService] Library restored');
} catch (error) {
logger.error('[BackupService] Failed to restore library:', error);
@ -732,7 +732,7 @@ export class BackupService {
private async restoreWatchProgress(watchProgress: Record<string, any>): Promise<void> {
try {
const pairs: [string, string][] = Object.entries(watchProgress).map(([key, value]) => [key, JSON.stringify(value)]);
await AsyncStorage.multiSet(pairs);
await mmkvStorage.multiSet(pairs);
logger.info('[BackupService] Watch progress restored');
} catch (error) {
logger.error('[BackupService] Failed to restore watch progress:', error);
@ -743,7 +743,7 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:stremio-addons`;
await AsyncStorage.setItem(scopedKey, JSON.stringify(addons));
await mmkvStorage.setItem(scopedKey, JSON.stringify(addons));
logger.info('[BackupService] Addons restored');
} catch (error) {
logger.error('[BackupService] Failed to restore addons:', error);
@ -752,7 +752,7 @@ export class BackupService {
private async restoreDownloads(downloads: DownloadItem[]): Promise<void> {
try {
await AsyncStorage.setItem('downloads_state_v1', JSON.stringify(downloads));
await mmkvStorage.setItem('downloads_state_v1', JSON.stringify(downloads));
logger.info('[BackupService] Downloads restored');
} catch (error) {
logger.error('[BackupService] Failed to restore downloads:', error);
@ -763,11 +763,11 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:@subtitle_settings`;
await AsyncStorage.setItem(scopedKey, JSON.stringify(subtitles));
await mmkvStorage.setItem(scopedKey, JSON.stringify(subtitles));
// Also restore legacy subtitle size preference for backward compatibility
if (subtitles && typeof subtitles.subtitleSize === 'number') {
await AsyncStorage.setItem('@subtitle_size_preference', subtitles.subtitleSize.toString());
await mmkvStorage.setItem('@subtitle_size_preference', subtitles.subtitleSize.toString());
}
logger.info('[BackupService] Subtitle settings restored');
@ -780,7 +780,7 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:@wp_tombstones`;
await AsyncStorage.setItem(scopedKey, JSON.stringify(tombstones));
await mmkvStorage.setItem(scopedKey, JSON.stringify(tombstones));
logger.info('[BackupService] Tombstones restored');
} catch (error) {
logger.error('[BackupService] Failed to restore tombstones:', error);
@ -791,7 +791,7 @@ export class BackupService {
try {
const scope = await this.getUserScope();
const scopedKey = `@user:${scope}:@continue_watching_removed`;
await AsyncStorage.setItem(scopedKey, JSON.stringify(removed));
await mmkvStorage.setItem(scopedKey, JSON.stringify(removed));
logger.info('[BackupService] Continue watching removed restored');
} catch (error) {
logger.error('[BackupService] Failed to restore continue watching removed:', error);
@ -801,7 +801,7 @@ export class BackupService {
private async restoreContentDuration(contentDuration: Record<string, number>): Promise<void> {
try {
const pairs: [string, string][] = Object.entries(contentDuration).map(([key, value]) => [key, JSON.stringify(value)]);
await AsyncStorage.multiSet(pairs);
await mmkvStorage.multiSet(pairs);
logger.info('[BackupService] Content duration restored');
} catch (error) {
logger.error('[BackupService] Failed to restore content duration:', error);
@ -810,7 +810,7 @@ export class BackupService {
private async restoreSyncQueue(syncQueue: any[]): Promise<void> {
try {
await AsyncStorage.setItem('@sync_queue', JSON.stringify(syncQueue));
await mmkvStorage.setItem('@sync_queue', JSON.stringify(syncQueue));
logger.info('[BackupService] Sync queue restored');
} catch (error) {
logger.error('[BackupService] Failed to restore sync queue:', error);
@ -824,22 +824,22 @@ export class BackupService {
const { authentication, autosync, ...generalSettings } = traktSettings;
// Restore general settings
await AsyncStorage.setItem('trakt_settings', JSON.stringify(generalSettings));
await mmkvStorage.setItem('trakt_settings', JSON.stringify(generalSettings));
// Restore authentication tokens if available
if (authentication) {
const tokenPromises = [];
if (authentication.accessToken) {
tokenPromises.push(AsyncStorage.setItem('trakt_access_token', authentication.accessToken));
tokenPromises.push(mmkvStorage.setItem('trakt_access_token', authentication.accessToken));
}
if (authentication.refreshToken) {
tokenPromises.push(AsyncStorage.setItem('trakt_refresh_token', authentication.refreshToken));
tokenPromises.push(mmkvStorage.setItem('trakt_refresh_token', authentication.refreshToken));
}
if (authentication.tokenExpiry) {
tokenPromises.push(AsyncStorage.setItem('trakt_token_expiry', authentication.tokenExpiry.toString()));
tokenPromises.push(mmkvStorage.setItem('trakt_token_expiry', authentication.tokenExpiry.toString()));
}
await Promise.all(tokenPromises);
@ -850,15 +850,15 @@ export class BackupService {
const autosyncPromises = [];
if (autosync.enabled !== undefined) {
autosyncPromises.push(AsyncStorage.setItem('trakt_autosync_enabled', JSON.stringify(autosync.enabled)));
autosyncPromises.push(mmkvStorage.setItem('trakt_autosync_enabled', JSON.stringify(autosync.enabled)));
}
if (autosync.frequency !== undefined) {
autosyncPromises.push(AsyncStorage.setItem('trakt_sync_frequency', autosync.frequency.toString()));
autosyncPromises.push(mmkvStorage.setItem('trakt_sync_frequency', autosync.frequency.toString()));
}
if (autosync.completionThreshold !== undefined) {
autosyncPromises.push(AsyncStorage.setItem('trakt_completion_threshold', autosync.completionThreshold.toString()));
autosyncPromises.push(mmkvStorage.setItem('trakt_completion_threshold', autosync.completionThreshold.toString()));
}
await Promise.all(autosyncPromises);
@ -875,31 +875,31 @@ export class BackupService {
try {
// Restore main scraper configurations
if (localScrapers.scrapers) {
await AsyncStorage.setItem('local-scrapers', JSON.stringify(localScrapers.scrapers));
await mmkvStorage.setItem('local-scrapers', JSON.stringify(localScrapers.scrapers));
}
// Restore repository settings
if (localScrapers.repositoryUrl) {
await AsyncStorage.setItem('scraper-repository-url', localScrapers.repositoryUrl);
await mmkvStorage.setItem('scraper-repository-url', localScrapers.repositoryUrl);
}
if (localScrapers.repositories) {
await AsyncStorage.setItem('scraper-repositories', JSON.stringify(localScrapers.repositories));
await mmkvStorage.setItem('scraper-repositories', JSON.stringify(localScrapers.repositories));
}
if (localScrapers.currentRepository) {
await AsyncStorage.setItem('current-repository-id', localScrapers.currentRepository);
await mmkvStorage.setItem('current-repository-id', localScrapers.currentRepository);
}
if (localScrapers.scraperSettings) {
await AsyncStorage.setItem('scraper-settings', JSON.stringify(localScrapers.scraperSettings));
await mmkvStorage.setItem('scraper-settings', JSON.stringify(localScrapers.scraperSettings));
}
// Restore scraper code cache
if (localScrapers.scraperCode && typeof localScrapers.scraperCode === 'object') {
const codePairs: [string, string][] = Object.entries(localScrapers.scraperCode).map(([key, value]) => [key, value as string]);
if (codePairs.length > 0) {
await AsyncStorage.multiSet(codePairs);
await mmkvStorage.multiSet(codePairs);
}
}
@ -914,11 +914,11 @@ export class BackupService {
const setPromises: Promise<void>[] = [];
if (apiKeys.mdblistApiKey) {
setPromises.push(AsyncStorage.setItem('mdblist_api_key', apiKeys.mdblistApiKey));
setPromises.push(mmkvStorage.setItem('mdblist_api_key', apiKeys.mdblistApiKey));
}
if (apiKeys.openRouterApiKey) {
setPromises.push(AsyncStorage.setItem('openrouter_api_key', apiKeys.openRouterApiKey));
setPromises.push(mmkvStorage.setItem('openrouter_api_key', apiKeys.openRouterApiKey));
}
await Promise.all(setPromises);
@ -930,7 +930,7 @@ export class BackupService {
private async restoreCatalogSettings(catalogSettings: any): Promise<void> {
try {
await AsyncStorage.setItem('catalog_settings', JSON.stringify(catalogSettings));
await mmkvStorage.setItem('catalog_settings', JSON.stringify(catalogSettings));
logger.info('[BackupService] Catalog settings restored');
} catch (error) {
logger.error('[BackupService] Failed to restore catalog settings:', error);
@ -944,8 +944,8 @@ export class BackupService {
// Restore to both scoped and legacy keys for compatibility
await Promise.all([
AsyncStorage.setItem(scopedKey, JSON.stringify(addonOrder)),
AsyncStorage.setItem('stremio-addon-order', JSON.stringify(addonOrder))
mmkvStorage.setItem(scopedKey, JSON.stringify(addonOrder)),
mmkvStorage.setItem('stremio-addon-order', JSON.stringify(addonOrder))
]);
logger.info('[BackupService] Addon order restored');
@ -956,7 +956,7 @@ export class BackupService {
private async restoreRemovedAddons(removedAddons: string[]): Promise<void> {
try {
await AsyncStorage.setItem('user_removed_addons', JSON.stringify(removedAddons));
await mmkvStorage.setItem('user_removed_addons', JSON.stringify(removedAddons));
logger.info('[BackupService] Removed addons restored');
} catch (error) {
logger.error('[BackupService] Failed to restore removed addons:', error);
@ -965,7 +965,7 @@ export class BackupService {
private async restoreGlobalSeasonViewMode(mode: string): Promise<void> {
try {
await AsyncStorage.setItem('global_season_view_mode', mode);
await mmkvStorage.setItem('global_season_view_mode', mode);
logger.info('[BackupService] Global season view mode restored');
} catch (error) {
logger.error('[BackupService] Failed to restore global season view mode:', error);
@ -974,7 +974,7 @@ export class BackupService {
private async restoreHasCompletedOnboarding(value: boolean): Promise<void> {
try {
await AsyncStorage.setItem('hasCompletedOnboarding', value.toString());
await mmkvStorage.setItem('hasCompletedOnboarding', value.toString());
logger.info('[BackupService] Has completed onboarding restored');
} catch (error) {
logger.error('[BackupService] Failed to restore has completed onboarding:', error);
@ -983,7 +983,7 @@ export class BackupService {
private async restoreShowLoginHintToastOnce(value: boolean): Promise<void> {
try {
await AsyncStorage.setItem('showLoginHintToastOnce', value.toString());
await mmkvStorage.setItem('showLoginHintToastOnce', value.toString());
logger.info('[BackupService] Show login hint toast once restored');
} catch (error) {
logger.error('[BackupService] Failed to restore show login hint toast once:', error);

View file

@ -1,6 +1,6 @@
import { stremioService, Meta, Manifest } from './stremioService';
import { notificationService } from './notificationService';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
import axios from 'axios';
import { TMDBService } from './tmdbService';
import { logger } from '../utils/logger';
@ -164,9 +164,9 @@ class CatalogService {
private async initializeScope(): Promise<void> {
try {
const currentScope = await AsyncStorage.getItem('@user:current');
const currentScope = await mmkvStorage.getItem('@user:current');
if (!currentScope) {
await AsyncStorage.setItem('@user:current', 'local');
await mmkvStorage.setItem('@user:current', 'local');
logger.log('[CatalogService] Initialized @user:current scope to "local"');
}
} catch (error) {
@ -183,14 +183,14 @@ class CatalogService {
private async loadLibrary(): Promise<void> {
try {
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const scopedKey = `@user:${scope}:stremio-library`;
let storedLibrary = (await AsyncStorage.getItem(scopedKey));
let storedLibrary = (await mmkvStorage.getItem(scopedKey));
if (!storedLibrary) {
// Fallback: read legacy and migrate into scoped
storedLibrary = await AsyncStorage.getItem(this.LEGACY_LIBRARY_KEY);
storedLibrary = await mmkvStorage.getItem(this.LEGACY_LIBRARY_KEY);
if (storedLibrary) {
await AsyncStorage.setItem(scopedKey, storedLibrary);
await mmkvStorage.setItem(scopedKey, storedLibrary);
}
}
if (storedLibrary) {
@ -201,7 +201,7 @@ class CatalogService {
this.library = {};
}
// Ensure @user:current is set to prevent future scope issues
await AsyncStorage.setItem('@user:current', scope);
await mmkvStorage.setItem('@user:current', scope);
} catch (error: any) {
logger.error('Failed to load library:', error);
this.library = {};
@ -210,11 +210,11 @@ class CatalogService {
private async saveLibrary(): Promise<void> {
try {
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const scopedKey = `@user:${scope}:stremio-library`;
const libraryData = JSON.stringify(this.library);
await AsyncStorage.setItem(scopedKey, libraryData);
await AsyncStorage.setItem(this.LEGACY_LIBRARY_KEY, libraryData);
await mmkvStorage.setItem(scopedKey, libraryData);
await mmkvStorage.setItem(this.LEGACY_LIBRARY_KEY, libraryData);
logger.log(`[CatalogService] Library saved successfully with ${Object.keys(this.library).length} items to scope: ${scope}`);
} catch (error: any) {
logger.error('Failed to save library:', error);
@ -223,7 +223,7 @@ class CatalogService {
private async loadRecentContent(): Promise<void> {
try {
const storedRecentContent = await AsyncStorage.getItem(this.RECENT_CONTENT_KEY);
const storedRecentContent = await mmkvStorage.getItem(this.RECENT_CONTENT_KEY);
if (storedRecentContent) {
this.recentContent = JSON.parse(storedRecentContent);
}
@ -234,7 +234,7 @@ class CatalogService {
private async saveRecentContent(): Promise<void> {
try {
await AsyncStorage.setItem(this.RECENT_CONTENT_KEY, JSON.stringify(this.recentContent));
await mmkvStorage.setItem(this.RECENT_CONTENT_KEY, JSON.stringify(this.recentContent));
} catch (error: any) {
logger.error('Failed to save recent content:', error);
}
@ -265,7 +265,7 @@ class CatalogService {
const addons = await this.getAllAddons();
// Load enabled/disabled settings
const catalogSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
const catalogSettingsJson = await mmkvStorage.getItem(CATALOG_SETTINGS_KEY);
const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {};
// Create an array of promises for all catalog fetches
@ -546,7 +546,7 @@ class CatalogService {
*/
async getDataSourcePreference(): Promise<DataSource> {
try {
const dataSource = await AsyncStorage.getItem(DATA_SOURCE_KEY);
const dataSource = await mmkvStorage.getItem(DATA_SOURCE_KEY);
return dataSource as DataSource || DataSource.STREMIO_ADDONS;
} catch (error) {
logger.error('Failed to get data source preference:', error);
@ -559,7 +559,7 @@ class CatalogService {
*/
async setDataSourcePreference(dataSource: DataSource): Promise<void> {
try {
await AsyncStorage.setItem(DATA_SOURCE_KEY, dataSource);
await mmkvStorage.setItem(DATA_SOURCE_KEY, dataSource);
} catch (error) {
logger.error('Failed to set data source preference:', error);
}

View file

@ -1,4 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
import axios from 'axios';
import { Platform } from 'react-native';
import { logger } from '../utils/logger';
@ -101,13 +101,13 @@ class LocalScraperService {
try {
// Load repositories
const repositoriesData = await AsyncStorage.getItem(this.REPOSITORIES_KEY);
const repositoriesData = await mmkvStorage.getItem(this.REPOSITORIES_KEY);
if (repositoriesData) {
const repos = JSON.parse(repositoriesData);
this.repositories = new Map(Object.entries(repos));
} else {
// Migrate from old single repository format or create default tapframe repository
const storedRepoUrl = await AsyncStorage.getItem(this.REPOSITORY_KEY);
const storedRepoUrl = await mmkvStorage.getItem(this.REPOSITORY_KEY);
if (storedRepoUrl) {
const defaultRepo: RepositoryInfo = {
id: 'default',
@ -127,7 +127,7 @@ class LocalScraperService {
}
// Load current repository
const currentRepoId = await AsyncStorage.getItem('current-repository-id');
const currentRepoId = await mmkvStorage.getItem('current-repository-id');
if (currentRepoId && this.repositories.has(currentRepoId)) {
this.currentRepositoryId = currentRepoId;
const currentRepo = this.repositories.get(currentRepoId)!;
@ -142,7 +142,7 @@ class LocalScraperService {
}
// Load installed scrapers
const storedScrapers = await AsyncStorage.getItem(this.STORAGE_KEY);
const storedScrapers = await mmkvStorage.getItem(this.STORAGE_KEY);
if (storedScrapers) {
const scrapers: ScraperInfo[] = JSON.parse(storedScrapers);
const validScrapers: ScraperInfo[] = [];
@ -194,14 +194,14 @@ class LocalScraperService {
// Save cleaned scrapers back to storage if any were filtered out
if (validScrapers.length !== scrapers.length) {
logger.log('[LocalScraperService] Cleaned up invalid scrapers, saving valid ones');
await AsyncStorage.setItem(this.STORAGE_KEY, JSON.stringify(validScrapers));
await mmkvStorage.setItem(this.STORAGE_KEY, JSON.stringify(validScrapers));
// Clean up cached code for removed scrapers
const validScraperIds = new Set(validScrapers.map(s => s.id));
const removedScrapers = scrapers.filter(s => s.id && !validScraperIds.has(s.id));
for (const removedScraper of removedScrapers) {
try {
await AsyncStorage.removeItem(`scraper-code-${removedScraper.id}`);
await mmkvStorage.removeItem(`scraper-code-${removedScraper.id}`);
logger.log('[LocalScraperService] Removed cached code for invalid scraper:', removedScraper.id);
} catch (error) {
logger.error('[LocalScraperService] Failed to remove cached code for', removedScraper.id, ':', error);
@ -244,7 +244,7 @@ class LocalScraperService {
// Set repository URL
async setRepositoryUrl(url: string): Promise<void> {
this.repositoryUrl = url;
await AsyncStorage.setItem(this.REPOSITORY_KEY, url);
await mmkvStorage.setItem(this.REPOSITORY_KEY, url);
logger.log('[LocalScraperService] Repository URL set to:', url);
}
@ -328,7 +328,7 @@ class LocalScraperService {
} else {
// No repositories left, clear current repository
this.currentRepositoryId = '';
await AsyncStorage.removeItem('current-repository-id');
await mmkvStorage.removeItem('current-repository-id');
}
}
@ -340,7 +340,7 @@ class LocalScraperService {
for (const scraperId of scrapersToRemove) {
this.installedScrapers.delete(scraperId);
this.scraperCode.delete(scraperId);
await AsyncStorage.removeItem(`scraper-code-${scraperId}`);
await mmkvStorage.removeItem(`scraper-code-${scraperId}`);
}
this.repositories.delete(id);
@ -360,7 +360,7 @@ class LocalScraperService {
this.repositoryUrl = repo.url;
this.repositoryName = repo.name;
await AsyncStorage.setItem('current-repository-id', id);
await mmkvStorage.setItem('current-repository-id', id);
// Refresh the repository to get its scrapers
try {
@ -450,7 +450,7 @@ class LocalScraperService {
private async saveRepositories(): Promise<void> {
const reposObject = Object.fromEntries(this.repositories);
await AsyncStorage.setItem(this.REPOSITORIES_KEY, JSON.stringify(reposObject));
await mmkvStorage.setItem(this.REPOSITORIES_KEY, JSON.stringify(reposObject));
}
@ -504,7 +504,7 @@ class LocalScraperService {
const scraper = this.installedScrapers.get(scraperId);
if (scraper && scraper.repositoryId === this.currentRepositoryId) {
this.scraperCode.delete(scraperId);
await AsyncStorage.removeItem(`scraper-code-${scraperId}`);
await mmkvStorage.removeItem(`scraper-code-${scraperId}`);
logger.log('[LocalScraperService] Cleared cached code for scraper:', scraper.name);
}
}
@ -551,7 +551,7 @@ class LocalScraperService {
this.installedScrapers.delete(scraperId);
this.scraperCode.delete(scraperId);
// Remove from AsyncStorage cache
await AsyncStorage.removeItem(`scraper-code-${scraperId}`);
await mmkvStorage.removeItem(`scraper-code-${scraperId}`);
}
}
@ -571,7 +571,7 @@ class LocalScraperService {
logger.log('[LocalScraperService] Removing platform-incompatible scraper:', scraperInfo.name);
this.installedScrapers.delete(scraperInfo.id);
this.scraperCode.delete(scraperInfo.id);
await AsyncStorage.removeItem(`scraper-code-${scraperInfo.id}`);
await mmkvStorage.removeItem(`scraper-code-${scraperInfo.id}`);
}
}
}
@ -675,7 +675,7 @@ class LocalScraperService {
// Cache scraper code locally
private async cacheScraperCode(scraperId: string, code: string): Promise<void> {
try {
await AsyncStorage.setItem(`scraper-code-${scraperId}`, code);
await mmkvStorage.setItem(`scraper-code-${scraperId}`, code);
} catch (error) {
logger.error('[LocalScraperService] Failed to cache scraper code:', error);
}
@ -685,7 +685,7 @@ class LocalScraperService {
private async loadScraperCode(): Promise<void> {
for (const [scraperId] of this.installedScrapers) {
try {
const cachedCode = await AsyncStorage.getItem(`scraper-code-${scraperId}`);
const cachedCode = await mmkvStorage.getItem(`scraper-code-${scraperId}`);
if (cachedCode) {
this.scraperCode.set(scraperId, cachedCode);
}
@ -699,7 +699,7 @@ class LocalScraperService {
private async saveInstalledScrapers(): Promise<void> {
try {
const scrapers = Array.from(this.installedScrapers.values());
await AsyncStorage.setItem(this.STORAGE_KEY, JSON.stringify(scrapers));
await mmkvStorage.setItem(this.STORAGE_KEY, JSON.stringify(scrapers));
} catch (error) {
logger.error('[LocalScraperService] Failed to save scrapers:', error);
}
@ -716,7 +716,7 @@ class LocalScraperService {
await this.ensureInitialized();
try {
if (!this.scraperSettingsCache) {
const raw = await AsyncStorage.getItem(this.SCRAPER_SETTINGS_KEY);
const raw = await mmkvStorage.getItem(this.SCRAPER_SETTINGS_KEY);
this.scraperSettingsCache = raw ? JSON.parse(raw) : {};
}
const cache = this.scraperSettingsCache || {};
@ -731,13 +731,13 @@ class LocalScraperService {
await this.ensureInitialized();
try {
if (!this.scraperSettingsCache) {
const raw = await AsyncStorage.getItem(this.SCRAPER_SETTINGS_KEY);
const raw = await mmkvStorage.getItem(this.SCRAPER_SETTINGS_KEY);
this.scraperSettingsCache = raw ? JSON.parse(raw) : {};
}
const cache = this.scraperSettingsCache || {};
cache[scraperId] = settings || {};
this.scraperSettingsCache = cache;
await AsyncStorage.setItem(this.SCRAPER_SETTINGS_KEY, JSON.stringify(cache));
await mmkvStorage.setItem(this.SCRAPER_SETTINGS_KEY, JSON.stringify(cache));
} catch (error) {
logger.error('[LocalScraperService] Failed to set scraper settings for', scraperId, error);
}
@ -1000,12 +1000,12 @@ class LocalScraperService {
// This is a simplified sandbox - in production, you'd want more security
try {
// Get URL validation setting from AsyncStorage
const settingsData = await AsyncStorage.getItem('app_settings');
const settingsData = await mmkvStorage.getItem('app_settings');
const settings = settingsData ? JSON.parse(settingsData) : {};
const urlValidationEnabled = settings.enableScraperUrlValidation ?? true;
// Load per-scraper settings for this run
const allScraperSettingsRaw = await AsyncStorage.getItem(this.SCRAPER_SETTINGS_KEY);
const allScraperSettingsRaw = await mmkvStorage.getItem(this.SCRAPER_SETTINGS_KEY);
const allScraperSettings = allScraperSettingsRaw ? JSON.parse(allScraperSettingsRaw) : {};
const perScraperSettings = (params && params.scraperId && allScraperSettings[params.scraperId]) ? allScraperSettings[params.scraperId] : (params?.settings || {});
@ -1320,12 +1320,12 @@ class LocalScraperService {
this.scraperCode.clear();
// Clear from storage
await AsyncStorage.removeItem(this.STORAGE_KEY);
await mmkvStorage.removeItem(this.STORAGE_KEY);
// Clear cached code
const keys = await AsyncStorage.getAllKeys();
const keys = await mmkvStorage.getAllKeys();
const scraperCodeKeys = keys.filter(key => key.startsWith('scraper-code-'));
await AsyncStorage.multiRemove(scraperCodeKeys);
await mmkvStorage.multiRemove(scraperCodeKeys);
logger.log('[LocalScraperService] All scrapers cleared');
}
@ -1383,9 +1383,9 @@ class LocalScraperService {
}
// Get user settings from AsyncStorage (scoped with fallback)
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scopedSettingsJson = await AsyncStorage.getItem(`@user:${scope}:app_settings`);
const legacySettingsJson = await AsyncStorage.getItem('app_settings');
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const scopedSettingsJson = await mmkvStorage.getItem(`@user:${scope}:app_settings`);
const legacySettingsJson = await mmkvStorage.getItem('app_settings');
const settingsData = scopedSettingsJson || legacySettingsJson;
const settings = settingsData ? JSON.parse(settingsData) : {};

View file

@ -1,4 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
import { logger } from '../utils/logger';
import {
MDBLIST_API_KEY_STORAGE_KEY,
@ -38,7 +38,7 @@ export class MDBListService {
async initialize(): Promise<void> {
try {
// First check if MDBList is enabled
const enabledSetting = await AsyncStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
const enabledSetting = await mmkvStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
const wasEnabled = this.enabled;
this.enabled = enabledSetting === null || enabledSetting === 'true';
logger.log('[MDBListService] MDBList enabled:', this.enabled);
@ -55,7 +55,7 @@ export class MDBListService {
return;
}
const newApiKey = await AsyncStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
const newApiKey = await mmkvStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
// Reset error counter when API key changes
if (newApiKey !== this.apiKey) {
this.apiKeyErrorCount = 0;
@ -91,7 +91,7 @@ export class MDBListService {
if (!this.enabled) {
// Try to refresh enabled status in case it was changed
try {
const enabledSetting = await AsyncStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
const enabledSetting = await mmkvStorage.getItem(MDBLIST_ENABLED_STORAGE_KEY);
this.enabled = enabledSetting === null || enabledSetting === 'true';
} catch (error) {
// Ignore error and keep current state

138
src/services/mmkvStorage.ts Normal file
View file

@ -0,0 +1,138 @@
import { createMMKV } from 'react-native-mmkv';
import { logger } from '../utils/logger';
class MMKVStorage {
private static instance: MMKVStorage;
private storage = createMMKV();
private constructor() {}
public static getInstance(): MMKVStorage {
if (!MMKVStorage.instance) {
MMKVStorage.instance = new MMKVStorage();
}
return MMKVStorage.instance;
}
// AsyncStorage-compatible API
async getItem(key: string): Promise<string | null> {
try {
const value = this.storage.getString(key);
return value ?? null;
} catch (error) {
logger.error(`[MMKVStorage] Error getting item ${key}:`, error);
return null;
}
}
async setItem(key: string, value: string): Promise<void> {
try {
this.storage.set(key, value);
} catch (error) {
logger.error(`[MMKVStorage] Error setting item ${key}:`, error);
}
}
async removeItem(key: string): Promise<void> {
try {
// MMKV V4 uses 'remove' method, not 'delete'
if (this.storage.contains(key)) {
this.storage.remove(key);
}
} catch (error) {
logger.error(`[MMKVStorage] Error removing item ${key}:`, error);
}
}
async getAllKeys(): Promise<string[]> {
try {
const keys = this.storage.getAllKeys();
return Array.from(keys) as string[];
} catch (error) {
logger.error('[MMKVStorage] Error getting all keys:', error);
return [];
}
}
async multiGet(keys: string[]): Promise<[string, string | null][]> {
try {
const results: [string, string | null][] = [];
for (const key of keys) {
const value = this.storage.getString(key);
results.push([key, value ?? null]);
}
return results;
} catch (error) {
logger.error('[MMKVStorage] Error in multiGet:', error);
return keys.map(key => [key, null] as [string, string | null]);
}
}
async clear(): Promise<void> {
try {
this.storage.clearAll();
} catch (error) {
logger.error('[MMKVStorage] Error clearing storage:', error);
}
}
// Direct MMKV access methods (for performance-critical operations)
getString(key: string): string | undefined {
return this.storage.getString(key);
}
setString(key: string, value: string): void {
this.storage.set(key, value);
}
getNumber(key: string): number | undefined {
return this.storage.getNumber(key);
}
setNumber(key: string, value: number): void {
this.storage.set(key, value);
}
getBoolean(key: string): boolean | undefined {
return this.storage.getBoolean(key);
}
setBoolean(key: string, value: boolean): void {
this.storage.set(key, value);
}
contains(key: string): boolean {
return this.storage.contains(key);
}
delete(key: string): void {
if (this.storage.contains(key)) {
this.storage.remove(key);
}
}
// Additional AsyncStorage-compatible methods
async multiSet(keyValuePairs: [string, string][]): Promise<void> {
try {
for (const [key, value] of keyValuePairs) {
this.storage.set(key, value);
}
} catch (error) {
logger.error('[MMKVStorage] Error in multiSet:', error);
}
}
async multiRemove(keys: string[]): Promise<void> {
try {
for (const key of keys) {
if (this.storage.contains(key)) {
this.storage.remove(key);
}
}
} catch (error) {
logger.error('[MMKVStorage] Error in multiRemove:', error);
}
}
}
export const mmkvStorage = MMKVStorage.getInstance();

View file

@ -1,6 +1,6 @@
import * as Notifications from 'expo-notifications';
import { Platform, AppState, AppStateStatus } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
import { parseISO, differenceInHours, isToday, addDays, isAfter, startOfToday } from 'date-fns';
import { stremioService } from './stremioService';
import { catalogService } from './catalogService';
@ -101,7 +101,7 @@ class NotificationService {
private async loadSettings(): Promise<void> {
try {
const storedSettings = await AsyncStorage.getItem(NOTIFICATION_SETTINGS_KEY);
const storedSettings = await mmkvStorage.getItem(NOTIFICATION_SETTINGS_KEY);
if (storedSettings) {
this.settings = { ...DEFAULT_NOTIFICATION_SETTINGS, ...JSON.parse(storedSettings) };
@ -113,7 +113,7 @@ class NotificationService {
private async saveSettings(): Promise<void> {
try {
await AsyncStorage.setItem(NOTIFICATION_SETTINGS_KEY, JSON.stringify(this.settings));
await mmkvStorage.setItem(NOTIFICATION_SETTINGS_KEY, JSON.stringify(this.settings));
} catch (error) {
logger.error('Error saving notification settings:', error);
}
@ -121,7 +121,7 @@ class NotificationService {
private async loadScheduledNotifications(): Promise<void> {
try {
const storedNotifications = await AsyncStorage.getItem(NOTIFICATION_STORAGE_KEY);
const storedNotifications = await mmkvStorage.getItem(NOTIFICATION_STORAGE_KEY);
if (storedNotifications) {
this.scheduledNotifications = JSON.parse(storedNotifications);
@ -133,7 +133,7 @@ class NotificationService {
private async saveScheduledNotifications(): Promise<void> {
try {
await AsyncStorage.setItem(NOTIFICATION_STORAGE_KEY, JSON.stringify(this.scheduledNotifications));
await mmkvStorage.setItem(NOTIFICATION_STORAGE_KEY, JSON.stringify(this.scheduledNotifications));
} catch (error) {
logger.error('Error saving scheduled notifications:', error);
}

View file

@ -1,4 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
import { logger } from '../utils/logger';
// Define the structure of cached data
@ -32,7 +32,7 @@ class RobustCalendarCache {
private async getCachedData<T>(key: string, libraryItems: any[], traktCollections: TraktCollections): Promise<T | null> {
try {
const storedCache = await AsyncStorage.getItem(key);
const storedCache = await mmkvStorage.getItem(key);
if (!storedCache) return null;
const cache: CachedData<T> = JSON.parse(storedCache);
@ -73,7 +73,7 @@ class RobustCalendarCache {
logger.log(`[Cache] Saving successful data to cache for key ${key}`);
}
await AsyncStorage.setItem(key, JSON.stringify(cache));
await mmkvStorage.setItem(key, JSON.stringify(cache));
} catch (error) {
logger.error(`[Cache] Error setting cached data for key ${key}:`, error);
}

View file

@ -1,4 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
import { logger } from '../utils/logger';
interface WatchProgress {
@ -35,7 +35,7 @@ class StorageService {
private async getUserScope(): Promise<string> {
try {
const scope = await AsyncStorage.getItem('@user:current');
const scope = await mmkvStorage.getItem('@user:current');
return scope || 'local';
} catch {
return 'local';
@ -79,10 +79,10 @@ class StorageService {
): Promise<void> {
try {
const key = await this.getTombstonesKeyScoped();
const json = (await AsyncStorage.getItem(key)) || '{}';
const json = (await mmkvStorage.getItem(key)) || '{}';
const map = JSON.parse(json) as Record<string, number>;
map[this.buildWpKeyString(id, type, episodeId)] = deletedAtMs || Date.now();
await AsyncStorage.setItem(key, JSON.stringify(map));
await mmkvStorage.setItem(key, JSON.stringify(map));
} catch {}
}
@ -93,12 +93,12 @@ class StorageService {
): Promise<void> {
try {
const key = await this.getTombstonesKeyScoped();
const json = (await AsyncStorage.getItem(key)) || '{}';
const json = (await mmkvStorage.getItem(key)) || '{}';
const map = JSON.parse(json) as Record<string, number>;
const k = this.buildWpKeyString(id, type, episodeId);
if (map[k] != null) {
delete map[k];
await AsyncStorage.setItem(key, JSON.stringify(map));
await mmkvStorage.setItem(key, JSON.stringify(map));
}
} catch {}
}
@ -106,7 +106,7 @@ class StorageService {
public async getWatchProgressTombstones(): Promise<Record<string, number>> {
try {
const key = await this.getTombstonesKeyScoped();
const json = (await AsyncStorage.getItem(key)) || '{}';
const json = (await mmkvStorage.getItem(key)) || '{}';
return JSON.parse(json) as Record<string, number>;
} catch {
return {};
@ -120,10 +120,10 @@ class StorageService {
): Promise<void> {
try {
const key = await this.getContinueWatchingRemovedKeyScoped();
const json = (await AsyncStorage.getItem(key)) || '{}';
const json = (await mmkvStorage.getItem(key)) || '{}';
const map = JSON.parse(json) as Record<string, number>;
map[this.buildWpKeyString(id, type)] = removedAtMs || Date.now();
await AsyncStorage.setItem(key, JSON.stringify(map));
await mmkvStorage.setItem(key, JSON.stringify(map));
} catch (error) {
logger.error('Error adding continue watching removed item:', error);
}
@ -135,12 +135,12 @@ class StorageService {
): Promise<void> {
try {
const key = await this.getContinueWatchingRemovedKeyScoped();
const json = (await AsyncStorage.getItem(key)) || '{}';
const json = (await mmkvStorage.getItem(key)) || '{}';
const map = JSON.parse(json) as Record<string, number>;
const k = this.buildWpKeyString(id, type);
if (map[k] != null) {
delete map[k];
await AsyncStorage.setItem(key, JSON.stringify(map));
await mmkvStorage.setItem(key, JSON.stringify(map));
}
} catch (error) {
logger.error('Error removing continue watching removed item:', error);
@ -150,7 +150,7 @@ class StorageService {
public async getContinueWatchingRemoved(): Promise<Record<string, number>> {
try {
const key = await this.getContinueWatchingRemovedKeyScoped();
const json = (await AsyncStorage.getItem(key)) || '{}';
const json = (await mmkvStorage.getItem(key)) || '{}';
return JSON.parse(json) as Record<string, number>;
} catch (error) {
logger.error('Error getting continue watching removed items:', error);
@ -176,7 +176,7 @@ class StorageService {
): Promise<void> {
try {
const key = await this.getContentDurationKeyScoped(id, type, episodeId);
await AsyncStorage.setItem(key, duration.toString());
await mmkvStorage.setItem(key, duration.toString());
} catch (error) {
logger.error('Error setting content duration:', error);
}
@ -189,7 +189,7 @@ class StorageService {
): Promise<number | null> {
try {
const key = await this.getContentDurationKeyScoped(id, type, episodeId);
const data = await AsyncStorage.getItem(key);
const data = await mmkvStorage.getItem(key);
return data ? parseFloat(data) : null;
} catch (error) {
logger.error('Error getting content duration:', error);
@ -262,7 +262,7 @@ class StorageService {
? progress.lastUpdated
: Date.now();
const updated = { ...progress, lastUpdated: timestamp };
await AsyncStorage.setItem(key, JSON.stringify(updated));
await mmkvStorage.setItem(key, JSON.stringify(updated));
// Notify subscribers; allow forcing immediate notification
if (options?.forceNotify) {
@ -332,7 +332,7 @@ class StorageService {
): Promise<WatchProgress | null> {
try {
const key = await this.getWatchProgressKeyScoped(id, type, episodeId);
const data = await AsyncStorage.getItem(key);
const data = await mmkvStorage.getItem(key);
return data ? JSON.parse(data) : null;
} catch (error) {
logger.error('Error getting watch progress:', error);
@ -347,7 +347,7 @@ class StorageService {
): Promise<void> {
try {
const key = await this.getWatchProgressKeyScoped(id, type, episodeId);
await AsyncStorage.removeItem(key);
await mmkvStorage.removeItem(key);
await this.addWatchProgressTombstone(id, type, episodeId);
// Notify subscribers
this.notifyWatchProgressSubscribers();
@ -362,9 +362,9 @@ class StorageService {
try {
const scope = await this.getUserScope();
const prefix = `@user:${scope}:${this.WATCH_PROGRESS_KEY}`;
const keys = await AsyncStorage.getAllKeys();
const keys = await mmkvStorage.getAllKeys();
const watchProgressKeys = keys.filter(key => key.startsWith(prefix));
const pairs = await AsyncStorage.multiGet(watchProgressKeys);
const pairs = await mmkvStorage.multiGet(watchProgressKeys);
return pairs.reduce((acc, [key, value]) => {
if (value) {
acc[key.replace(prefix, '')] = JSON.parse(value);
@ -644,7 +644,7 @@ class StorageService {
public async saveSubtitleSettings(settings: Record<string, any>): Promise<void> {
try {
const key = await this.getSubtitleSettingsKeyScoped();
await AsyncStorage.setItem(key, JSON.stringify(settings));
await mmkvStorage.setItem(key, JSON.stringify(settings));
} catch (error) {
logger.error('Error saving subtitle settings:', error);
}
@ -653,7 +653,7 @@ class StorageService {
public async getSubtitleSettings(): Promise<Record<string, any> | null> {
try {
const key = await this.getSubtitleSettingsKeyScoped();
const data = await AsyncStorage.getItem(key);
const data = await mmkvStorage.getItem(key);
return data ? JSON.parse(data) : null;
} catch (error) {
logger.error('Error loading subtitle settings:', error);

View file

@ -1,4 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
import { logger } from '../utils/logger';
export interface CachedStream {
@ -59,7 +59,7 @@ class StreamCacheService {
expiresAt: now + ttl
};
await AsyncStorage.setItem(cacheKey, JSON.stringify(cacheEntry));
await mmkvStorage.setItem(cacheKey, JSON.stringify(cacheEntry));
logger.log(`💾 [StreamCache] Saved stream cache for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
logger.log(`💾 [StreamCache] Cache key: ${cacheKey}`);
logger.log(`💾 [StreamCache] Stream URL: ${stream.url}`);
@ -78,7 +78,7 @@ class StreamCacheService {
const cacheKey = this.getCacheKey(id, type, episodeId);
logger.log(`🔍 [StreamCache] Looking for cached stream with key: ${cacheKey}`);
const cachedData = await AsyncStorage.getItem(cacheKey);
const cachedData = await mmkvStorage.getItem(cacheKey);
if (!cachedData) {
logger.log(`❌ [StreamCache] No cached data found for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
@ -116,7 +116,7 @@ class StreamCacheService {
async removeCachedStream(id: string, type: string, episodeId?: string): Promise<void> {
try {
const cacheKey = this.getCacheKey(id, type, episodeId);
await AsyncStorage.removeItem(cacheKey);
await mmkvStorage.removeItem(cacheKey);
logger.log(`🗑️ [StreamCache] Removed cached stream for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
} catch (error) {
logger.warn('[StreamCache] Failed to remove cached stream:', error);
@ -128,11 +128,11 @@ class StreamCacheService {
*/
async clearAllCachedStreams(): Promise<void> {
try {
const allKeys = await AsyncStorage.getAllKeys();
const allKeys = await mmkvStorage.getAllKeys();
const cacheKeys = allKeys.filter(key => key.startsWith(CACHE_KEY_PREFIX));
for (const key of cacheKeys) {
await AsyncStorage.removeItem(key);
await mmkvStorage.removeItem(key);
}
logger.log(`🧹 [StreamCache] Cleared ${cacheKeys.length} cached streams`);
@ -174,7 +174,7 @@ class StreamCacheService {
*/
async getCacheInfo(): Promise<{ totalCached: number; expiredCount: number; validCount: number }> {
try {
const allKeys = await AsyncStorage.getAllKeys();
const allKeys = await mmkvStorage.getAllKeys();
const cacheKeys = allKeys.filter((key: string) => key.startsWith(CACHE_KEY_PREFIX));
let expiredCount = 0;
@ -183,7 +183,7 @@ class StreamCacheService {
for (const key of cacheKeys) {
try {
const cachedData = await AsyncStorage.getItem(key);
const cachedData = await mmkvStorage.getItem(key);
if (cachedData) {
const cacheEntry: StreamCacheEntry = JSON.parse(cachedData);
if (now > cacheEntry.expiresAt) {

View file

@ -1,5 +1,5 @@
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
import { logger } from '../utils/logger';
import EventEmitter from 'eventemitter3';
import { localScraperService } from './localScraperService';
@ -321,11 +321,11 @@ class StremioService {
if (this.initialized) return;
try {
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
// Prefer scoped storage, but fall back to legacy keys to preserve older installs
let storedAddons = await AsyncStorage.getItem(`@user:${scope}:${this.STORAGE_KEY}`);
if (!storedAddons) storedAddons = await AsyncStorage.getItem(this.STORAGE_KEY);
if (!storedAddons) storedAddons = await AsyncStorage.getItem(`@user:local:${this.STORAGE_KEY}`);
let storedAddons = await mmkvStorage.getItem(`@user:${scope}:${this.STORAGE_KEY}`);
if (!storedAddons) storedAddons = await mmkvStorage.getItem(this.STORAGE_KEY);
if (!storedAddons) storedAddons = await mmkvStorage.getItem(`@user:local:${this.STORAGE_KEY}`);
if (storedAddons) {
const parsed = JSON.parse(storedAddons);
@ -425,9 +425,9 @@ class StremioService {
}
// Load addon order if exists (scoped first, then legacy, then @user:local for migration safety)
let storedOrder = await AsyncStorage.getItem(`@user:${scope}:${this.ADDON_ORDER_KEY}`);
if (!storedOrder) storedOrder = await AsyncStorage.getItem(this.ADDON_ORDER_KEY);
if (!storedOrder) storedOrder = await AsyncStorage.getItem(`@user:local:${this.ADDON_ORDER_KEY}`);
let storedOrder = await mmkvStorage.getItem(`@user:${scope}:${this.ADDON_ORDER_KEY}`);
if (!storedOrder) storedOrder = await mmkvStorage.getItem(this.ADDON_ORDER_KEY);
if (!storedOrder) storedOrder = await mmkvStorage.getItem(`@user:local:${this.ADDON_ORDER_KEY}`);
if (storedOrder) {
this.addonOrder = JSON.parse(storedOrder);
// Filter out any ids that aren't in installedAddons
@ -507,11 +507,11 @@ class StremioService {
private async saveInstalledAddons(): Promise<void> {
try {
const addonsArray = Array.from(this.installedAddons.values());
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
// Write to both scoped and legacy keys for compatibility
await Promise.all([
AsyncStorage.setItem(`@user:${scope}:${this.STORAGE_KEY}`, JSON.stringify(addonsArray)),
AsyncStorage.setItem(this.STORAGE_KEY, JSON.stringify(addonsArray)),
mmkvStorage.setItem(`@user:${scope}:${this.STORAGE_KEY}`, JSON.stringify(addonsArray)),
mmkvStorage.setItem(this.STORAGE_KEY, JSON.stringify(addonsArray)),
]);
} catch (error) {
// Continue even if save fails
@ -520,11 +520,11 @@ class StremioService {
private async saveAddonOrder(): Promise<void> {
try {
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
// Write to both scoped and legacy keys for compatibility
await Promise.all([
AsyncStorage.setItem(`@user:${scope}:${this.ADDON_ORDER_KEY}`, JSON.stringify(this.addonOrder)),
AsyncStorage.setItem(this.ADDON_ORDER_KEY, JSON.stringify(this.addonOrder)),
mmkvStorage.setItem(`@user:${scope}:${this.ADDON_ORDER_KEY}`, JSON.stringify(this.addonOrder)),
mmkvStorage.setItem(this.ADDON_ORDER_KEY, JSON.stringify(this.addonOrder)),
]);
} catch (error) {
// Continue even if save fails
@ -625,7 +625,7 @@ class StremioService {
// Check if user has explicitly removed an addon
async hasUserRemovedAddon(addonId: string): Promise<boolean> {
try {
const removedAddons = await AsyncStorage.getItem('user_removed_addons');
const removedAddons = await mmkvStorage.getItem('user_removed_addons');
if (!removedAddons) return false;
const removedList = JSON.parse(removedAddons);
return Array.isArray(removedList) && removedList.includes(addonId);
@ -637,13 +637,13 @@ class StremioService {
// Mark an addon as removed by user
private async markAddonAsRemovedByUser(addonId: string): Promise<void> {
try {
const removedAddons = await AsyncStorage.getItem('user_removed_addons');
const removedAddons = await mmkvStorage.getItem('user_removed_addons');
let removedList = removedAddons ? JSON.parse(removedAddons) : [];
if (!Array.isArray(removedList)) removedList = [];
if (!removedList.includes(addonId)) {
removedList.push(addonId);
await AsyncStorage.setItem('user_removed_addons', JSON.stringify(removedList));
await mmkvStorage.setItem('user_removed_addons', JSON.stringify(removedList));
}
} catch (error) {
// Silently fail - this is not critical functionality
@ -653,14 +653,14 @@ class StremioService {
// Remove an addon from the user removed list (allows reinstallation)
async unmarkAddonAsRemovedByUser(addonId: string): Promise<void> {
try {
const removedAddons = await AsyncStorage.getItem('user_removed_addons');
const removedAddons = await mmkvStorage.getItem('user_removed_addons');
if (!removedAddons) return;
let removedList = JSON.parse(removedAddons);
if (!Array.isArray(removedList)) return;
const updatedList = removedList.filter(id => id !== addonId);
await AsyncStorage.setItem('user_removed_addons', JSON.stringify(updatedList));
await mmkvStorage.setItem('user_removed_addons', JSON.stringify(updatedList));
} catch (error) {
// Silently fail - this is not critical functionality
}
@ -669,7 +669,7 @@ class StremioService {
// Clean up removed addon from all storage locations
private async cleanupRemovedAddonFromStorage(addonId: string): Promise<void> {
try {
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
// Remove from all possible addon order storage keys
const keys = [
@ -679,12 +679,12 @@ class StremioService {
];
for (const key of keys) {
const storedOrder = await AsyncStorage.getItem(key);
const storedOrder = await mmkvStorage.getItem(key);
if (storedOrder) {
const order = JSON.parse(storedOrder);
if (Array.isArray(order)) {
const updatedOrder = order.filter(id => id !== addonId);
await AsyncStorage.setItem(key, JSON.stringify(updatedOrder));
await mmkvStorage.setItem(key, JSON.stringify(updatedOrder));
}
}
}
@ -1054,9 +1054,9 @@ class StremioService {
// Check if local scrapers are enabled and execute them first
try {
// Load settings from AsyncStorage directly (scoped with fallback)
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const settingsJson = (await AsyncStorage.getItem(`@user:${scope}:app_settings`))
|| (await AsyncStorage.getItem('app_settings'));
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const settingsJson = (await mmkvStorage.getItem(`@user:${scope}:app_settings`))
|| (await mmkvStorage.getItem('app_settings'));
const rawSettings = settingsJson ? JSON.parse(settingsJson) : {};
const settings: AppSettings = { ...DEFAULT_SETTINGS, ...rawSettings };

View file

@ -1,5 +1,5 @@
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
// TMDB API configuration
const DEFAULT_API_KEY = 'd131017ccc6e5462a81c9304d21476de';
@ -124,8 +124,8 @@ export class TMDBService {
private async loadApiKey() {
try {
const [savedKey, savedUseCustomKey] = await Promise.all([
AsyncStorage.getItem(TMDB_API_KEY_STORAGE_KEY),
AsyncStorage.getItem(USE_CUSTOM_TMDB_API_KEY)
mmkvStorage.getItem(TMDB_API_KEY_STORAGE_KEY),
mmkvStorage.getItem(USE_CUSTOM_TMDB_API_KEY)
]);
this.useCustomKey = savedUseCustomKey === 'true';

View file

@ -1,4 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from './mmkvStorage';
import { AppState, AppStateStatus } from 'react-native';
import { logger } from '../utils/logger';
@ -599,7 +599,7 @@ export class TraktService {
*/
private async loadCompletionThreshold(): Promise<void> {
try {
const thresholdStr = await AsyncStorage.getItem('@trakt_completion_threshold');
const thresholdStr = await mmkvStorage.getItem('@trakt_completion_threshold');
if (thresholdStr) {
const threshold = parseInt(thresholdStr, 10);
if (!isNaN(threshold) && threshold >= 50 && threshold <= 100) {
@ -681,9 +681,9 @@ export class TraktService {
try {
const [accessToken, refreshToken, tokenExpiry] = await Promise.all([
AsyncStorage.getItem(TRAKT_ACCESS_TOKEN_KEY),
AsyncStorage.getItem(TRAKT_REFRESH_TOKEN_KEY),
AsyncStorage.getItem(TRAKT_TOKEN_EXPIRY_KEY)
mmkvStorage.getItem(TRAKT_ACCESS_TOKEN_KEY),
mmkvStorage.getItem(TRAKT_REFRESH_TOKEN_KEY),
mmkvStorage.getItem(TRAKT_TOKEN_EXPIRY_KEY)
]);
this.accessToken = accessToken;
@ -810,7 +810,7 @@ export class TraktService {
this.tokenExpiry = Date.now() + (expiresIn * 1000);
try {
await AsyncStorage.multiSet([
await mmkvStorage.multiSet([
[TRAKT_ACCESS_TOKEN_KEY, accessToken],
[TRAKT_REFRESH_TOKEN_KEY, refreshToken],
[TRAKT_TOKEN_EXPIRY_KEY, this.tokenExpiry.toString()]
@ -833,7 +833,7 @@ export class TraktService {
this.refreshToken = null;
this.tokenExpiry = 0;
await AsyncStorage.multiRemove([
await mmkvStorage.multiRemove([
TRAKT_ACCESS_TOKEN_KEY,
TRAKT_REFRESH_TOKEN_KEY,
TRAKT_TOKEN_EXPIRY_KEY

View file

@ -1,4 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mmkvStorage } from '../services/mmkvStorage';
import { logger } from './logger';
const CATALOG_CUSTOM_NAMES_KEY = 'catalog_custom_names';
@ -17,7 +17,7 @@ async function loadCustomNamesIfNeeded(): Promise<{ [key: string]: string }> {
try {
logger.info('Loading custom catalog names from storage...');
const savedCustomNamesJson = await AsyncStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
const savedCustomNamesJson = await mmkvStorage.getItem(CATALOG_CUSTOM_NAMES_KEY);
// Assign parsed object or empty object if null/error
customNamesCache = savedCustomNamesJson ? JSON.parse(savedCustomNamesJson) : {};
cacheTimestamp = now; // Set timestamp only on successful load