From 49b814a36d42e137138c720806474e02845ebf6e Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 26 Oct 2025 12:42:34 +0530 Subject: [PATCH] complete migration to mmkv --- .gitignore | 1 + App.tsx | 4 +- ios/Nuvio.xcodeproj/project.pbxproj | 8 +- ios/Podfile.lock | 85 +++++++---- package-lock.json | 59 ++++---- package.json | 3 +- src/components/home/ContentItem.tsx | 6 +- src/components/metadata/RatingsSection.tsx | 4 +- src/components/metadata/SeriesContent.tsx | 6 +- src/components/player/AndroidVideoPlayer.tsx | 10 +- src/components/player/KSPlayerCore.tsx | 6 +- src/contexts/DownloadsContext.tsx | 6 +- src/contexts/ThemeContext.tsx | 46 +++--- src/hooks/useCustomCatalogNames.ts | 4 +- src/hooks/useFeaturedContent.ts | 10 +- src/hooks/useGithubMajorUpdate.ts | 6 +- src/hooks/useLibrary.ts | 16 +-- src/hooks/useMetadata.ts | 2 +- src/hooks/useMetadataAssets.ts | 6 +- src/hooks/usePersistentSeasons.ts | 6 +- src/hooks/useSettings.ts | 30 ++-- src/hooks/useTraktAutosyncSettings.ts | 10 +- src/hooks/useUpdatePopup.ts | 16 +-- src/navigation/AppNavigator.tsx | 4 +- src/screens/AISettingsScreen.tsx | 8 +- src/screens/AddonsScreen.tsx | 4 +- src/screens/AuthScreen.tsx | 4 +- src/screens/CatalogScreen.tsx | 4 +- src/screens/CatalogSettingsScreen.tsx | 20 +-- src/screens/ContributorsScreen.tsx | 28 ++-- src/screens/HomeScreen.tsx | 8 +- src/screens/LibraryScreen.tsx | 8 +- src/screens/MDBListSettingsScreen.tsx | 24 ++-- src/screens/OnboardingScreen.tsx | 8 +- src/screens/ProfilesScreen.tsx | 8 +- src/screens/SearchScreen.tsx | 14 +- src/screens/SettingsScreen.tsx | 18 +-- src/screens/TMDBSettingsScreen.tsx | 20 +-- src/screens/UpdateScreen.tsx | 4 +- src/services/AccountService.ts | 14 +- src/services/aiService.ts | 4 +- src/services/backupService.ts | 144 +++++++++---------- src/services/catalogService.ts | 32 ++--- src/services/localScraperService.ts | 58 ++++---- src/services/mdblistService.ts | 8 +- src/services/mmkvStorage.ts | 138 ++++++++++++++++++ src/services/notificationService.ts | 10 +- src/services/robustCalendarCache.ts | 6 +- src/services/storageService.ts | 42 +++--- src/services/streamCacheService.ts | 16 +-- src/services/stremioService.ts | 50 +++---- src/services/tmdbService.ts | 6 +- src/services/traktService.ts | 14 +- src/utils/catalogNameUtils.ts | 4 +- 54 files changed, 621 insertions(+), 459 deletions(-) create mode 100644 src/services/mmkvStorage.ts diff --git a/.gitignore b/.gitignore index 5262efb..f520166 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ SDK54_UPGRADE_SUMMARY.md build-and-publish-app-releases.sh bottomnav.md /TrailerServices +mmkv.md diff --git a/App.tsx b/App.tsx index f9c649c..0b52a80 100644 --- a/App.tsx +++ b/App.tsx @@ -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 diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index 4bf304a..3acee97 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -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"; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 19eafe7..0d1e4b3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/package-lock.json b/package-lock.json index e9619c5..7902283 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 8ce6708..423e828 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx index d8466f6..da3b985 100644 --- a/src/components/home/ContentItem.tsx +++ b/src/components/home/ContentItem.tsx @@ -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(() => { diff --git a/src/components/metadata/RatingsSection.tsx b/src/components/metadata/RatingsSection.tsx index 8d73b14..354f56f 100644 --- a/src/components/metadata/RatingsSection.tsx +++ b/src/components/metadata/RatingsSection.tsx @@ -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 = ({ 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 { diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx index 44e8e9d..6741e5e 100644 --- a/src/components/metadata/SeriesContent.tsx +++ b/src/components/metadata/SeriesContent.tsx @@ -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 = ({ 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 = ({ // 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); }); }; diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index fc2b7ca..686ffa4 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -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 diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index 611ff5f..dd6ced4 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -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 diff --git a/src/contexts/DownloadsContext.tsx b/src/contexts/DownloadsContext.tsx index 2b7ad52..b291627 100644 --- a/src/contexts/DownloadsContext.tsx +++ b/src/contexts/DownloadsContext.tsx @@ -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>; // 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) => { diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index 8f0b9e4..a41d237 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -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) { diff --git a/src/hooks/useCustomCatalogNames.ts b/src/hooks/useCustomCatalogNames.ts index adb1d27..bcfc6c2 100644 --- a/src/hooks/useCustomCatalogNames.ts +++ b/src/hooks/useCustomCatalogNames.ts @@ -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 diff --git a/src/hooks/useFeaturedContent.ts b/src/hooks/useFeaturedContent.ts index 2f485b3..12ac4fc 100644 --- a/src/hooks/useFeaturedContent.ts +++ b/src/hooks/useFeaturedContent.ts @@ -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; diff --git a/src/hooks/useGithubMajorUpdate.ts b/src/hooks/useGithubMajorUpdate.ts index 1e311c6..af3b51f 100644 --- a/src/hooks/useGithubMajorUpdate.ts +++ b/src/hooks/useGithubMajorUpdate.ts @@ -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]); diff --git a/src/hooks/useLibrary.ts b/src/hooks/useLibrary.ts index a929306..732e661 100644 --- a/src/hooks/useLibrary.ts +++ b/src/hooks/useLibrary.ts @@ -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); - 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); } diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 5b058c2..a139b5e 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -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'; diff --git a/src/hooks/useMetadataAssets.ts b/src/hooks/useMetadataAssets.ts index ad6bd09..40122b2 100644 --- a/src/hooks/useMetadataAssets.ts +++ b/src/hooks/useMetadataAssets.ts @@ -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 = {}; @@ -17,7 +17,7 @@ const checkImageAvailability = async (url: string): Promise => { // 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 => { // 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 } diff --git a/src/hooks/usePersistentSeasons.ts b/src/hooks/usePersistentSeasons.ts index bfb6bc7..8df6cd6 100644 --- a/src/hooks/usePersistentSeasons.ts +++ b/src/hooks/usePersistentSeasons.ts @@ -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); } diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index fd36dcd..3d465a7 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -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); diff --git a/src/hooks/useTraktAutosyncSettings.ts b/src/hooks/useTraktAutosyncSettings.ts index 29d3268..9b898ee 100644 --- a/src/hooks/useTraktAutosyncSettings.ts +++ b/src/hooks/useTraktAutosyncSettings.ts @@ -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); } diff --git a/src/hooks/useUpdatePopup.ts b/src/hooks/useUpdatePopup.ts index 6b9fa16..b7caaea 100644 --- a/src/hooks/useUpdatePopup.ts +++ b/src/hooks/useUpdatePopup.ts @@ -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 } diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 58794fa..cd784d0 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -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 {} }; diff --git a/src/screens/AISettingsScreen.tsx b/src/screens/AISettingsScreen.tsx index 5a4a789..d73a51c 100644 --- a/src/screens/AISettingsScreen.tsx +++ b/src/screens/AISettingsScreen.tsx @@ -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'); diff --git a/src/screens/AddonsScreen.tsx b/src/screens/AddonsScreen.tsx index 3a4c7e5..8a08581 100644 --- a/src/screens/AddonsScreen.tsx +++ b/src/screens/AddonsScreen.tsx @@ -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) diff --git a/src/screens/AuthScreen.tsx b/src/screens/AuthScreen.tsx index fa9eda6..c890208 100644 --- a/src/screens/AuthScreen.tsx +++ b/src/screens/AuthScreen.tsx @@ -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); }; diff --git a/src/screens/CatalogScreen.tsx b/src/screens/CatalogScreen.tsx index 2bdec21..61ca850 100644 --- a/src/screens/CatalogScreen.tsx +++ b/src/screens/CatalogScreen.tsx @@ -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 = ({ 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'); diff --git a/src/screens/CatalogSettingsScreen.tsx b/src/screens/CatalogSettingsScreen.tsx index e1b4fbe..1318f87 100644 --- a/src/screens/CatalogSettingsScreen.tsx +++ b/src/screens/CatalogSettingsScreen.tsx @@ -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 {} }} diff --git a/src/screens/ContributorsScreen.tsx b/src/screens/ContributorsScreen.tsx index ae270b5..def265b 100644 --- a/src/screens/ContributorsScreen.tsx +++ b/src/screens/ContributorsScreen.tsx @@ -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'); } } diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 748c8cf..ea0a594 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -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'); diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 801a6cf..f016c2b 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -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 => diff --git a/src/screens/MDBListSettingsScreen.tsx b/src/screens/MDBListSettingsScreen.tsx index 019044b..41ffe94 100644 --- a/src/screens/MDBListSettingsScreen.tsx +++ b/src/screens/MDBListSettingsScreen.tsx @@ -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 => { 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 => { 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); 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); diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx index 50215b6..215bc2b 100644 --- a/src/screens/OnboardingScreen.tsx +++ b/src/screens/OnboardingScreen.tsx @@ -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) { diff --git a/src/screens/ProfilesScreen.tsx b/src/screens/ProfilesScreen.tsx index 8d12d12..f97e623 100644 --- a/src/screens/ProfilesScreen.tsx +++ b/src/screens/ProfilesScreen.tsx @@ -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'); diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx index c680fe9..0dc8f79 100644 --- a/src/screens/SearchScreen.tsx +++ b/src/screens/SearchScreen.tsx @@ -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; } diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index f0771d6..db9d2c8 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -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'); diff --git a/src/screens/TMDBSettingsScreen.tsx b/src/screens/TMDBSettingsScreen.tsx index 42401de..e5ae89a 100644 --- a/src/screens/TMDBSettingsScreen.tsx +++ b/src/screens/TMDBSettingsScreen.tsx @@ -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) { diff --git a/src/screens/UpdateScreen.tsx b/src/screens/UpdateScreen.tsx index e795e03..f3345e8 100644 --- a/src/screens/UpdateScreen.tsx +++ b/src/screens/UpdateScreen.tsx @@ -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(); diff --git a/src/services/AccountService.ts b/src/services/AccountService.ts index 084af94..32dc943 100644 --- a/src/services/AccountService.ts +++ b/src/services/AccountService.ts @@ -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 { - 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 { 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'; } } diff --git a/src/services/aiService.ts b/src/services/aiService.ts index 5b98dd5..e469f7b 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -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 { 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); diff --git a/src/services/backupService.ts b/src/services/backupService.ts index 5a53e8b..ddac6a1 100644 --- a/src/services/backupService.ts +++ b/src/services/backupService.ts @@ -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 { 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> { 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 = {}; 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 { 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> { 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 = {}; 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 { 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 { 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 { 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 = {}; 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 { 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 { 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 { 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 { 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 { 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): Promise { 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 { 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): Promise { 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 { 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[] = []; 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 { 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 { 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 { 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 { 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 { 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); diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index f68ac1c..c8e576a 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { 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); } diff --git a/src/services/localScraperService.ts b/src/services/localScraperService.ts index 3de4b8e..f9bf2d6 100644 --- a/src/services/localScraperService.ts +++ b/src/services/localScraperService.ts @@ -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 { 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 { 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 { 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 { 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 { 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) : {}; diff --git a/src/services/mdblistService.ts b/src/services/mdblistService.ts index b5c8709..e6959e3 100644 --- a/src/services/mdblistService.ts +++ b/src/services/mdblistService.ts @@ -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 { 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 diff --git a/src/services/mmkvStorage.ts b/src/services/mmkvStorage.ts new file mode 100644 index 0000000..02465e3 --- /dev/null +++ b/src/services/mmkvStorage.ts @@ -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 { + 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 { + try { + this.storage.set(key, value); + } catch (error) { + logger.error(`[MMKVStorage] Error setting item ${key}:`, error); + } + } + + async removeItem(key: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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(); diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index 26b2b1a..9874859 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -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 { 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 { 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 { 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 { 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); } diff --git a/src/services/robustCalendarCache.ts b/src/services/robustCalendarCache.ts index 7e9a4de..bbbe858 100644 --- a/src/services/robustCalendarCache.ts +++ b/src/services/robustCalendarCache.ts @@ -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(key: string, libraryItems: any[], traktCollections: TraktCollections): Promise { try { - const storedCache = await AsyncStorage.getItem(key); + const storedCache = await mmkvStorage.getItem(key); if (!storedCache) return null; const cache: CachedData = 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); } diff --git a/src/services/storageService.ts b/src/services/storageService.ts index 4d09480..0777109 100644 --- a/src/services/storageService.ts +++ b/src/services/storageService.ts @@ -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 { 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 { 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; 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 { 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; 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> { try { const key = await this.getTombstonesKeyScoped(); - const json = (await AsyncStorage.getItem(key)) || '{}'; + const json = (await mmkvStorage.getItem(key)) || '{}'; return JSON.parse(json) as Record; } catch { return {}; @@ -120,10 +120,10 @@ class StorageService { ): Promise { 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; 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 { 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; 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> { try { const key = await this.getContinueWatchingRemovedKeyScoped(); - const json = (await AsyncStorage.getItem(key)) || '{}'; + const json = (await mmkvStorage.getItem(key)) || '{}'; return JSON.parse(json) as Record; } catch (error) { logger.error('Error getting continue watching removed items:', error); @@ -176,7 +176,7 @@ class StorageService { ): Promise { 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 { 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 { 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 { 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): Promise { 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 | 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); diff --git a/src/services/streamCacheService.ts b/src/services/streamCacheService.ts index 2b55fce..73c7c57 100644 --- a/src/services/streamCacheService.ts +++ b/src/services/streamCacheService.ts @@ -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 { 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 { 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) { diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index 6dbd108..61fb0ae 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 }; diff --git a/src/services/tmdbService.ts b/src/services/tmdbService.ts index 89c3a65..158b422 100644 --- a/src/services/tmdbService.ts +++ b/src/services/tmdbService.ts @@ -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'; diff --git a/src/services/traktService.ts b/src/services/traktService.ts index b356147..d654855 100644 --- a/src/services/traktService.ts +++ b/src/services/traktService.ts @@ -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 { 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 diff --git a/src/utils/catalogNameUtils.ts b/src/utils/catalogNameUtils.ts index 195cb38..21ce894 100644 --- a/src/utils/catalogNameUtils.ts +++ b/src/utils/catalogNameUtils.ts @@ -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