mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
local backup restore feature
This commit is contained in:
parent
87aa913f5f
commit
bfacc4a1ee
9 changed files with 1479 additions and 5 deletions
|
|
@ -302,6 +302,25 @@
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/KSPlayer/KSPlayer_KSPlayer.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/KSPlayer/KSPlayer_KSPlayer.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Brands.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Regular.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Solid.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf",
|
||||||
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/ReachabilitySwift.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/ReachabilitySwift.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||||
|
|
@ -328,6 +347,25 @@
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/KSPlayer_KSPlayer.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/KSPlayer_KSPlayer.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Brands.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Regular.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Solid.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Fontisto.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Foundation.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialCommunityIcons.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Octicons.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReachabilitySwift.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReachabilitySwift.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||||
|
|
@ -526,7 +564,10 @@
|
||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
OTHER_LDFLAGS = "$(inherited) ";
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
" ",
|
||||||
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||||
|
|
@ -581,7 +622,10 @@
|
||||||
);
|
);
|
||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
OTHER_LDFLAGS = "$(inherited) ";
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
" ",
|
||||||
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
USE_HERMES = true;
|
USE_HERMES = true;
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,8 @@ PODS:
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoDevice (7.0.3):
|
- ExpoDevice (7.0.3):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
- ExpoDocumentPicker (14.0.7):
|
||||||
|
- ExpoModulesCore
|
||||||
- ExpoFileSystem (18.0.12):
|
- ExpoFileSystem (18.0.12):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoFont (13.0.4):
|
- ExpoFont (13.0.4):
|
||||||
|
|
@ -302,6 +304,8 @@ PODS:
|
||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
|
- ExpoSharing (14.0.7):
|
||||||
|
- ExpoModulesCore
|
||||||
- ExpoSystemUI (4.0.9):
|
- ExpoSystemUI (4.0.9):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoWebBrowser (14.0.2):
|
- ExpoWebBrowser (14.0.2):
|
||||||
|
|
@ -2453,6 +2457,27 @@ PODS:
|
||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
|
- RNVectorIcons (10.3.0):
|
||||||
|
- DoubleConversion
|
||||||
|
- glog
|
||||||
|
- hermes-engine
|
||||||
|
- RCT-Folly (= 2024.10.14.00)
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-ImageManager
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- Yoga
|
||||||
- SDWebImage (5.19.7):
|
- SDWebImage (5.19.7):
|
||||||
- SDWebImage/Core (= 5.19.7)
|
- SDWebImage/Core (= 5.19.7)
|
||||||
- SDWebImage/Core (5.19.7)
|
- SDWebImage/Core (5.19.7)
|
||||||
|
|
@ -2485,6 +2510,7 @@ DEPENDENCIES:
|
||||||
- ExpoBrightness (from `../node_modules/expo-brightness/ios`)
|
- ExpoBrightness (from `../node_modules/expo-brightness/ios`)
|
||||||
- ExpoCrypto (from `../node_modules/expo-crypto/ios`)
|
- ExpoCrypto (from `../node_modules/expo-crypto/ios`)
|
||||||
- ExpoDevice (from `../node_modules/expo-device/ios`)
|
- ExpoDevice (from `../node_modules/expo-device/ios`)
|
||||||
|
- ExpoDocumentPicker (from `../node_modules/expo-document-picker/ios`)
|
||||||
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||||
- ExpoFont (from `../node_modules/expo-font/ios`)
|
- ExpoFont (from `../node_modules/expo-font/ios`)
|
||||||
- ExpoHaptics (from `../node_modules/expo-haptics/ios`)
|
- ExpoHaptics (from `../node_modules/expo-haptics/ios`)
|
||||||
|
|
@ -2497,6 +2523,7 @@ DEPENDENCIES:
|
||||||
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
|
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
|
||||||
- ExpoRandom (from `../node_modules/expo-random/ios`)
|
- ExpoRandom (from `../node_modules/expo-random/ios`)
|
||||||
- ExpoScreenOrientation (from `../node_modules/expo-screen-orientation/ios`)
|
- ExpoScreenOrientation (from `../node_modules/expo-screen-orientation/ios`)
|
||||||
|
- ExpoSharing (from `../node_modules/expo-sharing/ios`)
|
||||||
- ExpoSystemUI (from `../node_modules/expo-system-ui/ios`)
|
- ExpoSystemUI (from `../node_modules/expo-system-ui/ios`)
|
||||||
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
||||||
- EXStructuredHeaders (from `../node_modules/expo-structured-headers/ios`)
|
- EXStructuredHeaders (from `../node_modules/expo-structured-headers/ios`)
|
||||||
|
|
@ -2584,6 +2611,7 @@ DEPENDENCIES:
|
||||||
- RNScreens (from `../node_modules/react-native-screens`)
|
- RNScreens (from `../node_modules/react-native-screens`)
|
||||||
- "RNSentry (from `../node_modules/@sentry/react-native`)"
|
- "RNSentry (from `../node_modules/@sentry/react-native`)"
|
||||||
- RNSVG (from `../node_modules/react-native-svg`)
|
- RNSVG (from `../node_modules/react-native-svg`)
|
||||||
|
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
|
||||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
|
|
@ -2639,6 +2667,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/expo-crypto/ios"
|
:path: "../node_modules/expo-crypto/ios"
|
||||||
ExpoDevice:
|
ExpoDevice:
|
||||||
:path: "../node_modules/expo-device/ios"
|
:path: "../node_modules/expo-device/ios"
|
||||||
|
ExpoDocumentPicker:
|
||||||
|
:path: "../node_modules/expo-document-picker/ios"
|
||||||
ExpoFileSystem:
|
ExpoFileSystem:
|
||||||
:path: "../node_modules/expo-file-system/ios"
|
:path: "../node_modules/expo-file-system/ios"
|
||||||
ExpoFont:
|
ExpoFont:
|
||||||
|
|
@ -2663,6 +2693,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/expo-random/ios"
|
:path: "../node_modules/expo-random/ios"
|
||||||
ExpoScreenOrientation:
|
ExpoScreenOrientation:
|
||||||
:path: "../node_modules/expo-screen-orientation/ios"
|
:path: "../node_modules/expo-screen-orientation/ios"
|
||||||
|
ExpoSharing:
|
||||||
|
:path: "../node_modules/expo-sharing/ios"
|
||||||
ExpoSystemUI:
|
ExpoSystemUI:
|
||||||
:path: "../node_modules/expo-system-ui/ios"
|
:path: "../node_modules/expo-system-ui/ios"
|
||||||
ExpoWebBrowser:
|
ExpoWebBrowser:
|
||||||
|
|
@ -2837,6 +2869,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/@sentry/react-native"
|
:path: "../node_modules/@sentry/react-native"
|
||||||
RNSVG:
|
RNSVG:
|
||||||
:path: "../node_modules/react-native-svg"
|
:path: "../node_modules/react-native-svg"
|
||||||
|
RNVectorIcons:
|
||||||
|
:path: "../node_modules/react-native-vector-icons"
|
||||||
Yoga:
|
Yoga:
|
||||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||||
|
|
||||||
|
|
@ -2874,6 +2908,7 @@ SPEC CHECKSUMS:
|
||||||
ExpoBrightness: c0011699a3225c869666e266326774a6fb6a9075
|
ExpoBrightness: c0011699a3225c869666e266326774a6fb6a9075
|
||||||
ExpoCrypto: e97e864c8d7b9ce4a000bca45dddb93544a1b2b4
|
ExpoCrypto: e97e864c8d7b9ce4a000bca45dddb93544a1b2b4
|
||||||
ExpoDevice: d36ab4186b6799a28fd449bb9a1c77455f23fd1a
|
ExpoDevice: d36ab4186b6799a28fd449bb9a1c77455f23fd1a
|
||||||
|
ExpoDocumentPicker: 2200eefc2817f19315fa18f0147e0b80ece86926
|
||||||
ExpoFileSystem: 42d363d3b96f9afab980dcef60d5657a4443c655
|
ExpoFileSystem: 42d363d3b96f9afab980dcef60d5657a4443c655
|
||||||
ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188
|
ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188
|
||||||
ExpoHaptics: 8d199b2f33245ea85289ff6c954c7ee7c00a5b5d
|
ExpoHaptics: 8d199b2f33245ea85289ff6c954c7ee7c00a5b5d
|
||||||
|
|
@ -2886,6 +2921,7 @@ SPEC CHECKSUMS:
|
||||||
ExpoModulesCore: c25d77625038b1968ea1afefc719862c0d8dd993
|
ExpoModulesCore: c25d77625038b1968ea1afefc719862c0d8dd993
|
||||||
ExpoRandom: d1444df65007bdd4070009efd5dab18e20bf0f00
|
ExpoRandom: d1444df65007bdd4070009efd5dab18e20bf0f00
|
||||||
ExpoScreenOrientation: af8b31d3164239a4ef3ea0b32bd63fb65df70d58
|
ExpoScreenOrientation: af8b31d3164239a4ef3ea0b32bd63fb65df70d58
|
||||||
|
ExpoSharing: 032c01bb034319e2374badf082ae935be866d2e9
|
||||||
ExpoSystemUI: b82a45cf0f6a4fa18d07c46deba8725dd27688b4
|
ExpoSystemUI: b82a45cf0f6a4fa18d07c46deba8725dd27688b4
|
||||||
ExpoWebBrowser: a212e6b480d8857d3e441fba51e0c968333803b3
|
ExpoWebBrowser: a212e6b480d8857d3e441fba51e0c968333803b3
|
||||||
EXStructuredHeaders: 09c70347b282e3d2507e25fb4c747b1b885f87f6
|
EXStructuredHeaders: 09c70347b282e3d2507e25fb4c747b1b885f87f6
|
||||||
|
|
@ -2976,6 +3012,7 @@ SPEC CHECKSUMS:
|
||||||
RNScreens: 362f4c861dd155f898908d5035d97b07a3f1a9d1
|
RNScreens: 362f4c861dd155f898908d5035d97b07a3f1a9d1
|
||||||
RNSentry: ac7beae04304d95491a512b5abf6926d4501c73c
|
RNSentry: ac7beae04304d95491a512b5abf6926d4501c73c
|
||||||
RNSVG: b889dc9c1948eeea0576a16cc405c91c37a12c19
|
RNSVG: b889dc9c1948eeea0576a16cc405c91c37a12c19
|
||||||
|
RNVectorIcons: c95fdae217b0ed388f2b4d7ed7a4edc457c1df47
|
||||||
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
||||||
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
|
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
|
||||||
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
||||||
|
|
|
||||||
20
package-lock.json
generated
20
package-lock.json
generated
|
|
@ -42,6 +42,7 @@
|
||||||
"expo-crypto": "~14.0.2",
|
"expo-crypto": "~14.0.2",
|
||||||
"expo-dev-client": "~5.0.20",
|
"expo-dev-client": "~5.0.20",
|
||||||
"expo-device": "~7.0.3",
|
"expo-device": "~7.0.3",
|
||||||
|
"expo-document-picker": "^14.0.7",
|
||||||
"expo-file-system": "~18.0.12",
|
"expo-file-system": "~18.0.12",
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.7",
|
"expo-image": "~2.0.7",
|
||||||
|
|
@ -52,6 +53,7 @@
|
||||||
"expo-notifications": "~0.29.14",
|
"expo-notifications": "~0.29.14",
|
||||||
"expo-random": "^14.0.1",
|
"expo-random": "^14.0.1",
|
||||||
"expo-screen-orientation": "~8.0.4",
|
"expo-screen-orientation": "~8.0.4",
|
||||||
|
"expo-sharing": "^14.0.7",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
"expo-system-ui": "^4.0.9",
|
"expo-system-ui": "^4.0.9",
|
||||||
"expo-updates": "~0.27.4",
|
"expo-updates": "~0.27.4",
|
||||||
|
|
@ -7880,6 +7882,15 @@
|
||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-document-picker": {
|
||||||
|
"version": "14.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.7.tgz",
|
||||||
|
"integrity": "sha512-81Jh8RDD0GYBUoSTmIBq30hXXjmkDV1ZY2BNIp1+3HR5PDSh2WmdhD/Ezz5YFsv46hIXHsQc+Kh1q8vn6OLT9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-eas-client": {
|
"node_modules/expo-eas-client": {
|
||||||
"version": "0.13.3",
|
"version": "0.13.3",
|
||||||
"resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-0.13.3.tgz",
|
"resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-0.13.3.tgz",
|
||||||
|
|
@ -8132,6 +8143,15 @@
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-sharing": {
|
||||||
|
"version": "14.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-14.0.7.tgz",
|
||||||
|
"integrity": "sha512-t/5tR8ZJNH6tMkHXlF7453UafNIfrpfTG+THN9EMLC4Wsi4bJuESPm3NdmWDg2D4LDALJI/LQo0iEnLAd5Sp4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-status-bar": {
|
"node_modules/expo-status-bar": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-2.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"expo-crypto": "~14.0.2",
|
"expo-crypto": "~14.0.2",
|
||||||
"expo-dev-client": "~5.0.20",
|
"expo-dev-client": "~5.0.20",
|
||||||
"expo-device": "~7.0.3",
|
"expo-device": "~7.0.3",
|
||||||
|
"expo-document-picker": "^14.0.7",
|
||||||
"expo-file-system": "~18.0.12",
|
"expo-file-system": "~18.0.12",
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.7",
|
"expo-image": "~2.0.7",
|
||||||
|
|
@ -52,6 +53,7 @@
|
||||||
"expo-notifications": "~0.29.14",
|
"expo-notifications": "~0.29.14",
|
||||||
"expo-random": "^14.0.1",
|
"expo-random": "^14.0.1",
|
||||||
"expo-screen-orientation": "~8.0.4",
|
"expo-screen-orientation": "~8.0.4",
|
||||||
|
"expo-sharing": "^14.0.7",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
"expo-system-ui": "^4.0.9",
|
"expo-system-ui": "^4.0.9",
|
||||||
"expo-updates": "~0.27.4",
|
"expo-updates": "~0.27.4",
|
||||||
|
|
|
||||||
310
src/components/BackupRestoreSettings.tsx
Normal file
310
src/components/BackupRestoreSettings.tsx
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
ActivityIndicator,
|
||||||
|
Platform,
|
||||||
|
} from 'react-native';
|
||||||
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
import * as DocumentPicker from 'expo-document-picker';
|
||||||
|
import * as Sharing from 'expo-sharing';
|
||||||
|
import { backupService, BackupOptions } from '../services/backupService';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
import CustomAlert from './CustomAlert';
|
||||||
|
|
||||||
|
interface BackupRestoreSettingsProps {
|
||||||
|
isTablet?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BackupRestoreSettings: React.FC<BackupRestoreSettingsProps> = ({ isTablet = false }) => {
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Alert state
|
||||||
|
const [alertVisible, setAlertVisible] = useState(false);
|
||||||
|
const [alertTitle, setAlertTitle] = useState('');
|
||||||
|
const [alertMessage, setAlertMessage] = useState('');
|
||||||
|
const [alertActions, setAlertActions] = useState<Array<{ label: string; onPress: () => void; style?: object }>>([]);
|
||||||
|
|
||||||
|
const openAlert = (
|
||||||
|
title: string,
|
||||||
|
message: string,
|
||||||
|
actions?: Array<{ label: string; onPress: () => void; style?: object }>
|
||||||
|
) => {
|
||||||
|
setAlertTitle(title);
|
||||||
|
setAlertMessage(message);
|
||||||
|
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]);
|
||||||
|
setAlertVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Create backup
|
||||||
|
const handleCreateBackup = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
// First, get backup preview to show what will be backed up
|
||||||
|
setIsLoading(true);
|
||||||
|
const preview = await backupService.getBackupPreview();
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Calculate total without downloads
|
||||||
|
const totalWithoutDownloads = preview.library + preview.watchProgress + preview.addons + preview.scrapers;
|
||||||
|
|
||||||
|
openAlert(
|
||||||
|
'Create Backup',
|
||||||
|
`Backup Contents:\n\n` +
|
||||||
|
`Library: ${preview.library} items\n` +
|
||||||
|
`Watch Progress: ${preview.watchProgress} entries\n` +
|
||||||
|
`Addons: ${preview.addons} installed\n` +
|
||||||
|
`Plugins: ${preview.scrapers} configurations\n\n` +
|
||||||
|
`Total: ${totalWithoutDownloads} items\n\n` +
|
||||||
|
`This backup includes all your app settings, themes, and integration data.`,
|
||||||
|
[
|
||||||
|
{ label: 'Cancel', onPress: () => {} },
|
||||||
|
{
|
||||||
|
label: 'Create Backup',
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const backupOptions: BackupOptions = {
|
||||||
|
includeLibrary: true,
|
||||||
|
includeWatchProgress: true,
|
||||||
|
includeDownloads: true,
|
||||||
|
includeAddons: true,
|
||||||
|
includeSettings: true,
|
||||||
|
includeTraktData: true,
|
||||||
|
includeLocalScrapers: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileUri = await backupService.createBackup(backupOptions);
|
||||||
|
|
||||||
|
// Share the backup file
|
||||||
|
if (await Sharing.isAvailableAsync()) {
|
||||||
|
await Sharing.shareAsync(fileUri, {
|
||||||
|
mimeType: 'application/json',
|
||||||
|
dialogTitle: 'Share Nuvio Backup',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openAlert(
|
||||||
|
'Backup Created',
|
||||||
|
'Your backup has been created and is ready to share.',
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupRestoreSettings] Failed to create backup:', error);
|
||||||
|
openAlert(
|
||||||
|
'Backup Failed',
|
||||||
|
`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupRestoreSettings] Failed to get backup preview:', error);
|
||||||
|
openAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to prepare backup information. Please try again.',
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [openAlert]);
|
||||||
|
|
||||||
|
// Restore backup
|
||||||
|
const handleRestoreBackup = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await DocumentPicker.getDocumentAsync({
|
||||||
|
type: 'application/json',
|
||||||
|
copyToCacheDirectory: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.canceled || !result.assets?.[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileUri = result.assets[0].uri;
|
||||||
|
|
||||||
|
// Validate backup file
|
||||||
|
const backupInfo = await backupService.getBackupInfo(fileUri);
|
||||||
|
|
||||||
|
openAlert(
|
||||||
|
'Confirm Restore',
|
||||||
|
`This will restore your data from a backup created on ${new Date(backupInfo.timestamp || 0).toLocaleDateString()}.\n\nThis action will overwrite your current data. Are you sure you want to continue?`,
|
||||||
|
[
|
||||||
|
{ label: 'Cancel', onPress: () => {} },
|
||||||
|
{
|
||||||
|
label: 'Restore',
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const restoreOptions: BackupOptions = {
|
||||||
|
includeLibrary: true,
|
||||||
|
includeWatchProgress: true,
|
||||||
|
includeDownloads: true,
|
||||||
|
includeAddons: true,
|
||||||
|
includeSettings: true,
|
||||||
|
includeTraktData: true,
|
||||||
|
includeLocalScrapers: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await backupService.restoreBackup(fileUri, restoreOptions);
|
||||||
|
|
||||||
|
openAlert(
|
||||||
|
'Restore Complete',
|
||||||
|
'Your data has been successfully restored. Please restart the app to see all changes.',
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupRestoreSettings] Failed to restore backup:', error);
|
||||||
|
openAlert(
|
||||||
|
'Restore Failed',
|
||||||
|
`Failed to restore backup: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupRestoreSettings] Failed to pick backup file:', error);
|
||||||
|
openAlert(
|
||||||
|
'File Selection Failed',
|
||||||
|
`Failed to select backup file: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [openAlert]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const formatDate = (timestamp: number): string => {
|
||||||
|
return new Date(timestamp).toLocaleDateString() + ' ' + new Date(timestamp).toLocaleTimeString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<CustomAlert
|
||||||
|
visible={alertVisible}
|
||||||
|
title={alertTitle}
|
||||||
|
message={alertMessage}
|
||||||
|
actions={alertActions}
|
||||||
|
onClose={() => setAlertVisible(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Backup Actions */}
|
||||||
|
<View style={[styles.section, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
|
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
|
Backup & Restore
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.actionButton,
|
||||||
|
{
|
||||||
|
backgroundColor: currentTheme.colors.primary,
|
||||||
|
opacity: isLoading ? 0.6 : 1
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
onPress={handleCreateBackup}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<ActivityIndicator color="white" size="small" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MaterialIcons name="backup" size={20} color="white" />
|
||||||
|
<Text style={styles.actionButtonText}>Create Backup</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.actionButton,
|
||||||
|
{
|
||||||
|
backgroundColor: currentTheme.colors.secondary,
|
||||||
|
opacity: isLoading ? 0.6 : 1
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
onPress={handleRestoreBackup}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="restore" size={20} color="white" />
|
||||||
|
<Text style={styles.actionButtonText}>Restore from Backup</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Info Section */}
|
||||||
|
<View style={[styles.section, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
|
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
|
About Backups
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
|
• Backups include all your data: library, watch progress, settings, addons, downloads, and plugins{'\n'}
|
||||||
|
• Backup files are stored locally on your device{'\n'}
|
||||||
|
• Share your backup to transfer data between devices{'\n'}
|
||||||
|
• Restoring will overwrite your current data
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginVertical: 8,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
sectionHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
actionButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
actionButtonText: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
infoText: {
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default BackupRestoreSettings;
|
||||||
|
|
||||||
|
|
@ -52,11 +52,13 @@ import CastMoviesScreen from '../screens/CastMoviesScreen';
|
||||||
import UpdateScreen from '../screens/UpdateScreen';
|
import UpdateScreen from '../screens/UpdateScreen';
|
||||||
import AISettingsScreen from '../screens/AISettingsScreen';
|
import AISettingsScreen from '../screens/AISettingsScreen';
|
||||||
import AIChatScreen from '../screens/AIChatScreen';
|
import AIChatScreen from '../screens/AIChatScreen';
|
||||||
|
import BackupScreen from '../screens/BackupScreen';
|
||||||
|
|
||||||
// Stack navigator types
|
// Stack navigator types
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
Onboarding: undefined;
|
Onboarding: undefined;
|
||||||
MainTabs: undefined;
|
MainTabs: undefined;
|
||||||
|
Backup: undefined;
|
||||||
Home: undefined;
|
Home: undefined;
|
||||||
Library: undefined;
|
Library: undefined;
|
||||||
Settings: undefined;
|
Settings: undefined;
|
||||||
|
|
@ -1312,8 +1314,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="AISettings"
|
name="AISettings"
|
||||||
component={AISettingsScreen}
|
component={AISettingsScreen}
|
||||||
options={{
|
options={{
|
||||||
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
||||||
|
|
@ -1327,6 +1329,22 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Stack.Screen
|
||||||
|
name="Backup"
|
||||||
|
component={BackupScreen}
|
||||||
|
options={{
|
||||||
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
||||||
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
||||||
|
presentation: 'card',
|
||||||
|
gestureEnabled: true,
|
||||||
|
gestureDirection: 'horizontal',
|
||||||
|
headerShown: false,
|
||||||
|
contentStyle: {
|
||||||
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="AIChat"
|
name="AIChat"
|
||||||
component={AIChatScreen}
|
component={AIChatScreen}
|
||||||
|
|
|
||||||
358
src/screens/BackupScreen.tsx
Normal file
358
src/screens/BackupScreen.tsx
Normal file
|
|
@ -0,0 +1,358 @@
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
ActivityIndicator,
|
||||||
|
Platform,
|
||||||
|
SafeAreaView,
|
||||||
|
StatusBar,
|
||||||
|
ScrollView,
|
||||||
|
} from 'react-native';
|
||||||
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
import * as DocumentPicker from 'expo-document-picker';
|
||||||
|
import * as Sharing from 'expo-sharing';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { backupService, BackupOptions } from '../services/backupService';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
import CustomAlert from '../components/CustomAlert';
|
||||||
|
|
||||||
|
const BackupScreen: React.FC = () => {
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
// Alert state
|
||||||
|
const [alertVisible, setAlertVisible] = useState(false);
|
||||||
|
const [alertTitle, setAlertTitle] = useState('');
|
||||||
|
const [alertMessage, setAlertMessage] = useState('');
|
||||||
|
const [alertActions, setAlertActions] = useState<Array<{ label: string; onPress: () => void; style?: object }>>([]);
|
||||||
|
|
||||||
|
const openAlert = (
|
||||||
|
title: string,
|
||||||
|
message: string,
|
||||||
|
actions?: Array<{ label: string; onPress: () => void; style?: object }>
|
||||||
|
) => {
|
||||||
|
setAlertTitle(title);
|
||||||
|
setAlertMessage(message);
|
||||||
|
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]);
|
||||||
|
setAlertVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create backup
|
||||||
|
const handleCreateBackup = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
// First, get backup preview to show what will be backed up
|
||||||
|
setIsLoading(true);
|
||||||
|
const preview = await backupService.getBackupPreview();
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Calculate total without downloads
|
||||||
|
const totalWithoutDownloads = preview.library + preview.watchProgress + preview.addons + preview.scrapers;
|
||||||
|
|
||||||
|
openAlert(
|
||||||
|
'Create Backup',
|
||||||
|
`Backup Contents:\n\n` +
|
||||||
|
`Library: ${preview.library} items\n` +
|
||||||
|
`Watch Progress: ${preview.watchProgress} entries\n` +
|
||||||
|
`Addons: ${preview.addons} installed\n` +
|
||||||
|
`Plugins: ${preview.scrapers} configurations\n\n` +
|
||||||
|
`Total: ${totalWithoutDownloads} items\n\n` +
|
||||||
|
`This backup includes all your app settings, themes, and integration data.`,
|
||||||
|
[
|
||||||
|
{ label: 'Cancel', onPress: () => {} },
|
||||||
|
{
|
||||||
|
label: 'Create Backup',
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const backupOptions: BackupOptions = {
|
||||||
|
includeLibrary: true,
|
||||||
|
includeWatchProgress: true,
|
||||||
|
includeDownloads: true,
|
||||||
|
includeAddons: true,
|
||||||
|
includeSettings: true,
|
||||||
|
includeTraktData: true,
|
||||||
|
includeLocalScrapers: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileUri = await backupService.createBackup(backupOptions);
|
||||||
|
|
||||||
|
// Share the backup file
|
||||||
|
if (await Sharing.isAvailableAsync()) {
|
||||||
|
await Sharing.shareAsync(fileUri, {
|
||||||
|
mimeType: 'application/json',
|
||||||
|
dialogTitle: 'Share Nuvio Backup',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openAlert(
|
||||||
|
'Backup Created',
|
||||||
|
'Your backup has been created and is ready to share.',
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupScreen] Failed to create backup:', error);
|
||||||
|
openAlert(
|
||||||
|
'Backup Failed',
|
||||||
|
`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupScreen] Failed to get backup preview:', error);
|
||||||
|
openAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to prepare backup information. Please try again.',
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [openAlert]);
|
||||||
|
|
||||||
|
// Restore backup
|
||||||
|
const handleRestoreBackup = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await DocumentPicker.getDocumentAsync({
|
||||||
|
type: 'application/json',
|
||||||
|
copyToCacheDirectory: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.canceled || !result.assets?.[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileUri = result.assets[0].uri;
|
||||||
|
|
||||||
|
// Validate backup file
|
||||||
|
const backupInfo = await backupService.getBackupInfo(fileUri);
|
||||||
|
|
||||||
|
openAlert(
|
||||||
|
'Confirm Restore',
|
||||||
|
`This will restore your data from a backup created on ${new Date(backupInfo.timestamp || 0).toLocaleDateString()}.\n\nThis action will overwrite your current data. Are you sure you want to continue?`,
|
||||||
|
[
|
||||||
|
{ label: 'Cancel', onPress: () => {} },
|
||||||
|
{
|
||||||
|
label: 'Restore',
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const restoreOptions: BackupOptions = {
|
||||||
|
includeLibrary: true,
|
||||||
|
includeWatchProgress: true,
|
||||||
|
includeDownloads: true,
|
||||||
|
includeAddons: true,
|
||||||
|
includeSettings: true,
|
||||||
|
includeTraktData: true,
|
||||||
|
includeLocalScrapers: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await backupService.restoreBackup(fileUri, restoreOptions);
|
||||||
|
|
||||||
|
openAlert(
|
||||||
|
'Restore Complete',
|
||||||
|
'Your data has been successfully restored. Please restart the app to see all changes.',
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupScreen] Failed to restore backup:', error);
|
||||||
|
openAlert(
|
||||||
|
'Restore Failed',
|
||||||
|
`Failed to restore backup: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupScreen] Failed to pick backup file:', error);
|
||||||
|
openAlert(
|
||||||
|
'File Selection Failed',
|
||||||
|
`Failed to select backup file: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
[{ label: 'OK', onPress: () => {} }]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [openAlert]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
|
<StatusBar barStyle="light-content" />
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.backButton}
|
||||||
|
onPress={() => navigation.goBack()}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.white} />
|
||||||
|
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.headerActions}>
|
||||||
|
{/* Empty for now, but keeping structure consistent */}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>
|
||||||
|
Backup & Restore
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<ScrollView
|
||||||
|
style={styles.scrollView}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
|
>
|
||||||
|
<View style={styles.content}>
|
||||||
|
<CustomAlert
|
||||||
|
visible={alertVisible}
|
||||||
|
title={alertTitle}
|
||||||
|
message={alertMessage}
|
||||||
|
actions={alertActions}
|
||||||
|
onClose={() => setAlertVisible(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Backup Actions */}
|
||||||
|
<View style={[styles.section, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
|
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
|
Backup & Restore
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.actionButton,
|
||||||
|
{
|
||||||
|
backgroundColor: currentTheme.colors.primary,
|
||||||
|
opacity: isLoading ? 0.6 : 1
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
onPress={handleCreateBackup}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<ActivityIndicator color="white" size="small" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MaterialIcons name="backup" size={20} color="white" />
|
||||||
|
<Text style={styles.actionButtonText}>Create Backup</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.actionButton,
|
||||||
|
{
|
||||||
|
backgroundColor: currentTheme.colors.secondary,
|
||||||
|
opacity: isLoading ? 0.6 : 1
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
onPress={handleRestoreBackup}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="restore" size={20} color="white" />
|
||||||
|
<Text style={styles.actionButtonText}>Restore from Backup</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Info Section */}
|
||||||
|
<View style={[styles.section, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
|
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
|
About Backups
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
|
• Backups include all your data: library, watch progress, settings, addons, downloads, and plugins{'\n'}
|
||||||
|
• Backup files are stored locally on your device{'\n'}
|
||||||
|
• Share your backup to transfer data between devices{'\n'}
|
||||||
|
• Restoring will overwrite your current data
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
|
||||||
|
},
|
||||||
|
headerActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
backText: {
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: '400',
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 34,
|
||||||
|
fontWeight: '700',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingBottom: 16,
|
||||||
|
paddingTop: 8,
|
||||||
|
},
|
||||||
|
scrollView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
marginBottom: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
actionButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
actionButtonText: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
infoText: {
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default BackupScreen;
|
||||||
|
|
@ -49,6 +49,7 @@ const SETTINGS_CATEGORIES = [
|
||||||
{ id: 'integrations', title: 'Integrations', icon: 'extension' as keyof typeof MaterialIcons.glyphMap },
|
{ id: 'integrations', title: 'Integrations', icon: 'extension' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'ai', title: 'AI Assistant', icon: 'smart-toy' as keyof typeof MaterialIcons.glyphMap },
|
{ id: 'ai', title: 'AI Assistant', icon: 'smart-toy' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'playback', title: 'Playback', icon: 'play-circle-outline' as keyof typeof MaterialIcons.glyphMap },
|
{ id: 'playback', title: 'Playback', icon: 'play-circle-outline' as keyof typeof MaterialIcons.glyphMap },
|
||||||
|
{ id: 'backup', title: 'Backup & Restore', icon: 'save' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'updates', title: 'Updates', icon: 'system-update' as keyof typeof MaterialIcons.glyphMap },
|
{ id: 'updates', title: 'Updates', icon: 'system-update' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'about', title: 'About', icon: 'info-outline' as keyof typeof MaterialIcons.glyphMap },
|
{ id: 'about', title: 'About', icon: 'info-outline' as keyof typeof MaterialIcons.glyphMap },
|
||||||
{ id: 'developer', title: 'Developer', icon: 'code' as keyof typeof MaterialIcons.glyphMap },
|
{ id: 'developer', title: 'Developer', icon: 'code' as keyof typeof MaterialIcons.glyphMap },
|
||||||
|
|
@ -197,7 +198,7 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
|
||||||
Settings
|
Settings
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}>
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
@ -432,6 +433,8 @@ const SettingsScreen: React.FC = () => {
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const renderCategoryContent = (categoryId: string) => {
|
const renderCategoryContent = (categoryId: string) => {
|
||||||
switch (categoryId) {
|
switch (categoryId) {
|
||||||
case 'account':
|
case 'account':
|
||||||
|
|
@ -746,6 +749,21 @@ const SettingsScreen: React.FC = () => {
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
case 'backup':
|
||||||
|
return (
|
||||||
|
<SettingsCard title="BACKUP & RESTORE" isTablet={isTablet}>
|
||||||
|
<SettingItem
|
||||||
|
title="Backup & Restore"
|
||||||
|
description="Create and restore app backups"
|
||||||
|
icon="backup"
|
||||||
|
renderControl={ChevronRight}
|
||||||
|
onPress={() => navigation.navigate('Backup')}
|
||||||
|
isLast={true}
|
||||||
|
isTablet={isTablet}
|
||||||
|
/>
|
||||||
|
</SettingsCard>
|
||||||
|
);
|
||||||
|
|
||||||
case 'updates':
|
case 'updates':
|
||||||
return (
|
return (
|
||||||
<SettingsCard title="UPDATES" isTablet={isTablet}>
|
<SettingsCard title="UPDATES" isTablet={isTablet}>
|
||||||
|
|
@ -881,6 +899,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
{renderCategoryContent('integrations')}
|
{renderCategoryContent('integrations')}
|
||||||
{renderCategoryContent('ai')}
|
{renderCategoryContent('ai')}
|
||||||
{renderCategoryContent('playback')}
|
{renderCategoryContent('playback')}
|
||||||
|
{renderCategoryContent('backup')}
|
||||||
{renderCategoryContent('updates')}
|
{renderCategoryContent('updates')}
|
||||||
{renderCategoryContent('about')}
|
{renderCategoryContent('about')}
|
||||||
{renderCategoryContent('developer')}
|
{renderCategoryContent('developer')}
|
||||||
|
|
|
||||||
666
src/services/backupService.ts
Normal file
666
src/services/backupService.ts
Normal file
|
|
@ -0,0 +1,666 @@
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
import { AppSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
|
||||||
|
import { StreamingContent } from './catalogService';
|
||||||
|
import { DownloadItem } from '../contexts/DownloadsContext';
|
||||||
|
|
||||||
|
export interface BackupData {
|
||||||
|
version: string;
|
||||||
|
timestamp: number;
|
||||||
|
appVersion: string;
|
||||||
|
platform: 'ios' | 'android';
|
||||||
|
userScope: string;
|
||||||
|
data: {
|
||||||
|
settings: AppSettings;
|
||||||
|
library: StreamingContent[];
|
||||||
|
watchProgress: Record<string, any>;
|
||||||
|
addons: any[];
|
||||||
|
downloads: DownloadItem[];
|
||||||
|
subtitles: any;
|
||||||
|
tombstones: Record<string, number>;
|
||||||
|
continueWatchingRemoved: string[];
|
||||||
|
contentDuration: Record<string, number>;
|
||||||
|
syncQueue: any[];
|
||||||
|
traktSettings?: any;
|
||||||
|
localScrapers?: {
|
||||||
|
scrapers: any;
|
||||||
|
repositoryUrl?: string;
|
||||||
|
repositories: any;
|
||||||
|
currentRepository?: string;
|
||||||
|
scraperSettings: any;
|
||||||
|
scraperCode: Record<string, string>;
|
||||||
|
};
|
||||||
|
customThemes?: any[];
|
||||||
|
};
|
||||||
|
metadata: {
|
||||||
|
totalItems: number;
|
||||||
|
libraryCount: number;
|
||||||
|
watchProgressCount: number;
|
||||||
|
downloadsCount: number;
|
||||||
|
addonsCount: number;
|
||||||
|
scrapersCount?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupOptions {
|
||||||
|
includeLibrary?: boolean;
|
||||||
|
includeWatchProgress?: boolean;
|
||||||
|
includeDownloads?: boolean;
|
||||||
|
includeAddons?: boolean;
|
||||||
|
includeSettings?: boolean;
|
||||||
|
includeTraktData?: boolean;
|
||||||
|
includeLocalScrapers?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BackupService {
|
||||||
|
private static instance: BackupService;
|
||||||
|
private readonly BACKUP_VERSION = '1.0.0';
|
||||||
|
private readonly BACKUP_FILENAME_PREFIX = 'nuvio_backup_';
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): BackupService {
|
||||||
|
if (!BackupService.instance) {
|
||||||
|
BackupService.instance = new BackupService();
|
||||||
|
}
|
||||||
|
return BackupService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a comprehensive backup of all user data
|
||||||
|
*/
|
||||||
|
public async createBackup(options: BackupOptions = {}): Promise<string> {
|
||||||
|
try {
|
||||||
|
logger.info('[BackupService] Starting backup creation...');
|
||||||
|
|
||||||
|
const userScope = await this.getUserScope();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const filename = `${this.BACKUP_FILENAME_PREFIX}${timestamp}.json`;
|
||||||
|
|
||||||
|
// Collect all data
|
||||||
|
const backupData: BackupData = {
|
||||||
|
version: this.BACKUP_VERSION,
|
||||||
|
timestamp,
|
||||||
|
appVersion: '1.0.0', // You might want to get this from package.json
|
||||||
|
platform: Platform.OS as 'ios' | 'android',
|
||||||
|
userScope,
|
||||||
|
data: {
|
||||||
|
settings: await this.getSettings(),
|
||||||
|
library: options.includeLibrary !== false ? await this.getLibrary() : [],
|
||||||
|
watchProgress: options.includeWatchProgress !== false ? await this.getWatchProgress() : {},
|
||||||
|
addons: options.includeAddons !== false ? await this.getAddons() : [],
|
||||||
|
downloads: options.includeDownloads !== false ? await this.getDownloads() : [],
|
||||||
|
subtitles: await this.getSubtitleSettings(),
|
||||||
|
tombstones: await this.getTombstones(),
|
||||||
|
continueWatchingRemoved: await this.getContinueWatchingRemoved(),
|
||||||
|
contentDuration: await this.getContentDuration(),
|
||||||
|
syncQueue: await this.getSyncQueue(),
|
||||||
|
traktSettings: options.includeTraktData !== false ? await this.getTraktSettings() : undefined,
|
||||||
|
localScrapers: options.includeLocalScrapers !== false ? await this.getLocalScrapers() : undefined,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
totalItems: 0,
|
||||||
|
libraryCount: 0,
|
||||||
|
watchProgressCount: 0,
|
||||||
|
downloadsCount: 0,
|
||||||
|
addonsCount: 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate metadata
|
||||||
|
backupData.metadata.libraryCount = backupData.data.library.length;
|
||||||
|
backupData.metadata.watchProgressCount = Object.keys(backupData.data.watchProgress).length;
|
||||||
|
backupData.metadata.downloadsCount = backupData.data.downloads.length;
|
||||||
|
backupData.metadata.addonsCount = backupData.data.addons.length;
|
||||||
|
|
||||||
|
// Count scraper items if available
|
||||||
|
const scraperCount = backupData.data.localScrapers?.scrapers ?
|
||||||
|
Object.keys(backupData.data.localScrapers.scrapers).length : 0;
|
||||||
|
backupData.metadata.scrapersCount = scraperCount;
|
||||||
|
|
||||||
|
backupData.metadata.totalItems =
|
||||||
|
backupData.metadata.libraryCount +
|
||||||
|
backupData.metadata.watchProgressCount +
|
||||||
|
backupData.metadata.downloadsCount +
|
||||||
|
backupData.metadata.addonsCount +
|
||||||
|
scraperCount;
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
const fileUri = `${FileSystem.documentDirectory}${filename}`;
|
||||||
|
await FileSystem.writeAsStringAsync(fileUri, JSON.stringify(backupData, null, 2));
|
||||||
|
|
||||||
|
logger.info(`[BackupService] Backup created successfully: ${filename}`);
|
||||||
|
logger.info(`[BackupService] Backup contains: ${backupData.metadata.totalItems} items`);
|
||||||
|
|
||||||
|
return fileUri;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to create backup:', error);
|
||||||
|
throw new Error(`Failed to create backup: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get backup preview information without creating the backup
|
||||||
|
*/
|
||||||
|
public async getBackupPreview(): Promise<{
|
||||||
|
library: number;
|
||||||
|
watchProgress: number;
|
||||||
|
addons: number;
|
||||||
|
downloads: number;
|
||||||
|
scrapers: number;
|
||||||
|
total: number;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const [
|
||||||
|
libraryData,
|
||||||
|
watchProgressData,
|
||||||
|
addonsData,
|
||||||
|
downloadsData,
|
||||||
|
scrapersData
|
||||||
|
] = await Promise.all([
|
||||||
|
this.getLibrary(),
|
||||||
|
this.getWatchProgress(),
|
||||||
|
this.getAddons(),
|
||||||
|
this.getDownloads(),
|
||||||
|
this.getLocalScrapers()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const libraryCount = Array.isArray(libraryData) ? libraryData.length : 0;
|
||||||
|
const watchProgressCount = Object.keys(watchProgressData).length;
|
||||||
|
const addonsCount = Array.isArray(addonsData) ? addonsData.length : 0;
|
||||||
|
const downloadsCount = Array.isArray(downloadsData) ? downloadsData.length : 0;
|
||||||
|
const scrapersCount = scrapersData.scrapers ? Object.keys(scrapersData.scrapers).length : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
library: libraryCount,
|
||||||
|
watchProgress: watchProgressCount,
|
||||||
|
addons: addonsCount,
|
||||||
|
downloads: downloadsCount,
|
||||||
|
scrapers: scrapersCount,
|
||||||
|
total: libraryCount + watchProgressCount + addonsCount + downloadsCount + scrapersCount
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get backup preview:', error);
|
||||||
|
return { library: 0, watchProgress: 0, addons: 0, downloads: 0, scrapers: 0, total: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore data from a backup file
|
||||||
|
*/
|
||||||
|
public async restoreBackup(fileUri: string, options: BackupOptions = {}): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info('[BackupService] Starting backup restore...');
|
||||||
|
|
||||||
|
// Read and validate backup file
|
||||||
|
const backupContent = await FileSystem.readAsStringAsync(fileUri);
|
||||||
|
const backupData: BackupData = JSON.parse(backupContent);
|
||||||
|
|
||||||
|
// Validate backup format
|
||||||
|
this.validateBackupData(backupData);
|
||||||
|
|
||||||
|
logger.info(`[BackupService] Restoring backup from ${backupData.timestamp}`);
|
||||||
|
logger.info(`[BackupService] Backup contains: ${backupData.metadata.totalItems} items`);
|
||||||
|
|
||||||
|
// Restore data based on options
|
||||||
|
if (options.includeSettings !== false) {
|
||||||
|
await this.restoreSettings(backupData.data.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includeLibrary !== false) {
|
||||||
|
await this.restoreLibrary(backupData.data.library);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includeWatchProgress !== false) {
|
||||||
|
await this.restoreWatchProgress(backupData.data.watchProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includeAddons !== false) {
|
||||||
|
await this.restoreAddons(backupData.data.addons);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includeDownloads !== false) {
|
||||||
|
await this.restoreDownloads(backupData.data.downloads);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includeTraktData !== false && backupData.data.traktSettings) {
|
||||||
|
await this.restoreTraktSettings(backupData.data.traktSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includeLocalScrapers !== false && backupData.data.localScrapers) {
|
||||||
|
await this.restoreLocalScrapers(backupData.data.localScrapers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore additional data
|
||||||
|
await this.restoreSubtitleSettings(backupData.data.subtitles);
|
||||||
|
await this.restoreTombstones(backupData.data.tombstones);
|
||||||
|
await this.restoreContinueWatchingRemoved(backupData.data.continueWatchingRemoved);
|
||||||
|
await this.restoreContentDuration(backupData.data.contentDuration);
|
||||||
|
await this.restoreSyncQueue(backupData.data.syncQueue);
|
||||||
|
|
||||||
|
logger.info('[BackupService] Backup restore completed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore backup:', error);
|
||||||
|
throw new Error(`Failed to restore backup: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get backup file info without loading full data
|
||||||
|
*/
|
||||||
|
public async getBackupInfo(fileUri: string): Promise<Partial<BackupData>> {
|
||||||
|
try {
|
||||||
|
const backupContent = await FileSystem.readAsStringAsync(fileUri);
|
||||||
|
const backupData: BackupData = JSON.parse(backupContent);
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: backupData.version,
|
||||||
|
timestamp: backupData.timestamp,
|
||||||
|
appVersion: backupData.appVersion,
|
||||||
|
platform: backupData.platform,
|
||||||
|
userScope: backupData.userScope,
|
||||||
|
metadata: backupData.metadata
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to read backup info:', error);
|
||||||
|
throw new Error(`Invalid backup file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all backup files in the document directory
|
||||||
|
*/
|
||||||
|
public async listBackups(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const files = await FileSystem.readDirectoryAsync(FileSystem.documentDirectory!);
|
||||||
|
return files
|
||||||
|
.filter(file => file.startsWith(this.BACKUP_FILENAME_PREFIX) && file.endsWith('.json'))
|
||||||
|
.sort((a, b) => b.localeCompare(a)); // Sort by filename (newest first)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to list backups:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a backup file
|
||||||
|
*/
|
||||||
|
public async deleteBackup(fileUri: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await FileSystem.deleteAsync(fileUri);
|
||||||
|
logger.info('[BackupService] Backup file deleted:', fileUri);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to delete backup:', error);
|
||||||
|
throw new Error(`Failed to delete backup: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private helper methods for data collection
|
||||||
|
private async getUserScope(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const scope = await AsyncStorage.getItem('@user:current');
|
||||||
|
return scope || 'local';
|
||||||
|
} catch {
|
||||||
|
return 'local';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSettings(): Promise<AppSettings> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:app_settings`;
|
||||||
|
const settingsJson = await AsyncStorage.getItem(scopedKey);
|
||||||
|
return settingsJson ? JSON.parse(settingsJson) : DEFAULT_SETTINGS;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get settings:', error);
|
||||||
|
return DEFAULT_SETTINGS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLibrary(): Promise<StreamingContent[]> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:stremio-library`;
|
||||||
|
const libraryJson = await AsyncStorage.getItem(scopedKey);
|
||||||
|
if (libraryJson) {
|
||||||
|
const parsed = JSON.parse(libraryJson);
|
||||||
|
return Array.isArray(parsed) ? parsed : Object.values(parsed);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get library:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getWatchProgress(): Promise<Record<string, any>> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const allKeys = await AsyncStorage.getAllKeys();
|
||||||
|
const watchProgressKeys = allKeys.filter(key =>
|
||||||
|
key.startsWith(`@user:${scope}:@watch_progress:`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const watchProgress: Record<string, any> = {};
|
||||||
|
if (watchProgressKeys.length > 0) {
|
||||||
|
const pairs = await AsyncStorage.multiGet(watchProgressKeys);
|
||||||
|
for (const [key, value] of pairs) {
|
||||||
|
if (value) {
|
||||||
|
watchProgress[key] = JSON.parse(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return watchProgress;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get watch progress:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAddons(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:stremio-addons`;
|
||||||
|
const addonsJson = await AsyncStorage.getItem(scopedKey);
|
||||||
|
return addonsJson ? JSON.parse(addonsJson) : [];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get addons:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getDownloads(): Promise<DownloadItem[]> {
|
||||||
|
try {
|
||||||
|
const downloadsJson = await AsyncStorage.getItem('downloads_state_v1');
|
||||||
|
return downloadsJson ? JSON.parse(downloadsJson) : [];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get downloads:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSubtitleSettings(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:@subtitle_settings`;
|
||||||
|
const subtitlesJson = await AsyncStorage.getItem(scopedKey);
|
||||||
|
return subtitlesJson ? JSON.parse(subtitlesJson) : {};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get subtitle settings:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getTombstones(): Promise<Record<string, number>> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:@wp_tombstones`;
|
||||||
|
const tombstonesJson = await AsyncStorage.getItem(scopedKey);
|
||||||
|
return tombstonesJson ? JSON.parse(tombstonesJson) : {};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get tombstones:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getContinueWatchingRemoved(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:@continue_watching_removed`;
|
||||||
|
const removedJson = await AsyncStorage.getItem(scopedKey);
|
||||||
|
return removedJson ? JSON.parse(removedJson) : [];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get continue watching removed:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getContentDuration(): Promise<Record<string, number>> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const allKeys = await AsyncStorage.getAllKeys();
|
||||||
|
const durationKeys = allKeys.filter(key =>
|
||||||
|
key.startsWith(`@user:${scope}:@content_duration:`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentDuration: Record<string, number> = {};
|
||||||
|
if (durationKeys.length > 0) {
|
||||||
|
const pairs = await AsyncStorage.multiGet(durationKeys);
|
||||||
|
for (const [key, value] of pairs) {
|
||||||
|
if (value) {
|
||||||
|
contentDuration[key] = JSON.parse(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contentDuration;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get content duration:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSyncQueue(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const syncQueueJson = await AsyncStorage.getItem('@sync_queue');
|
||||||
|
return syncQueueJson ? JSON.parse(syncQueueJson) : [];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get sync queue:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getTraktSettings(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const traktSettingsJson = await AsyncStorage.getItem('trakt_settings');
|
||||||
|
return traktSettingsJson ? JSON.parse(traktSettingsJson) : {};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get Trakt settings:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLocalScrapers(): Promise<any> {
|
||||||
|
try {
|
||||||
|
// Get main scraper configurations
|
||||||
|
const localScrapersJson = await AsyncStorage.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');
|
||||||
|
|
||||||
|
// Get all scraper code cache keys
|
||||||
|
const allKeys = await AsyncStorage.getAllKeys();
|
||||||
|
const scraperCodeKeys = allKeys.filter(key => key.startsWith('scraper-code-'));
|
||||||
|
const scraperCode: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (scraperCodeKeys.length > 0) {
|
||||||
|
const codePairs = await AsyncStorage.multiGet(scraperCodeKeys);
|
||||||
|
for (const [key, value] of codePairs) {
|
||||||
|
if (value) {
|
||||||
|
scraperCode[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrapers: localScrapersJson ? JSON.parse(localScrapersJson) : {},
|
||||||
|
repositoryUrl: repoUrl,
|
||||||
|
repositories: repositories ? JSON.parse(repositories) : {},
|
||||||
|
currentRepository: currentRepo,
|
||||||
|
scraperSettings: scraperSettings ? JSON.parse(scraperSettings) : {},
|
||||||
|
scraperCode: scraperCode
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to get local scrapers:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private helper methods for data restoration
|
||||||
|
private async restoreSettings(settings: AppSettings): Promise<void> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:app_settings`;
|
||||||
|
await AsyncStorage.setItem(scopedKey, JSON.stringify(settings));
|
||||||
|
logger.info('[BackupService] Settings restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore settings:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restoreLibrary(library: StreamingContent[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:stremio-library`;
|
||||||
|
await AsyncStorage.setItem(scopedKey, JSON.stringify(library));
|
||||||
|
logger.info('[BackupService] Library restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore library:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restoreWatchProgress(watchProgress: Record<string, any>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const pairs: [string, string][] = Object.entries(watchProgress).map(([key, value]) => [key, JSON.stringify(value)]);
|
||||||
|
await AsyncStorage.multiSet(pairs);
|
||||||
|
logger.info('[BackupService] Watch progress restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore watch progress:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restoreAddons(addons: any[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:stremio-addons`;
|
||||||
|
await AsyncStorage.setItem(scopedKey, JSON.stringify(addons));
|
||||||
|
logger.info('[BackupService] Addons restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore addons:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restoreDownloads(downloads: DownloadItem[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem('downloads_state_v1', JSON.stringify(downloads));
|
||||||
|
logger.info('[BackupService] Downloads restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore downloads:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restoreSubtitleSettings(subtitles: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:@subtitle_settings`;
|
||||||
|
await AsyncStorage.setItem(scopedKey, JSON.stringify(subtitles));
|
||||||
|
logger.info('[BackupService] Subtitle settings restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore subtitle settings:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restoreTombstones(tombstones: Record<string, number>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:@wp_tombstones`;
|
||||||
|
await AsyncStorage.setItem(scopedKey, JSON.stringify(tombstones));
|
||||||
|
logger.info('[BackupService] Tombstones restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore tombstones:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restoreContinueWatchingRemoved(removed: string[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
const scope = await this.getUserScope();
|
||||||
|
const scopedKey = `@user:${scope}:@continue_watching_removed`;
|
||||||
|
await AsyncStorage.setItem(scopedKey, JSON.stringify(removed));
|
||||||
|
logger.info('[BackupService] Continue watching removed restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore continue watching removed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restoreContentDuration(contentDuration: Record<string, number>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const pairs: [string, string][] = Object.entries(contentDuration).map(([key, value]) => [key, JSON.stringify(value)]);
|
||||||
|
await AsyncStorage.multiSet(pairs);
|
||||||
|
logger.info('[BackupService] Content duration restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore content duration:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restoreSyncQueue(syncQueue: any[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem('@sync_queue', JSON.stringify(syncQueue));
|
||||||
|
logger.info('[BackupService] Sync queue restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore sync queue:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restoreTraktSettings(traktSettings: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem('trakt_settings', JSON.stringify(traktSettings));
|
||||||
|
logger.info('[BackupService] Trakt settings restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore Trakt settings:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async restoreLocalScrapers(localScrapers: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Restore main scraper configurations
|
||||||
|
if (localScrapers.scrapers) {
|
||||||
|
await AsyncStorage.setItem('local-scrapers', JSON.stringify(localScrapers.scrapers));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore repository settings
|
||||||
|
if (localScrapers.repositoryUrl) {
|
||||||
|
await AsyncStorage.setItem('scraper-repository-url', localScrapers.repositoryUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localScrapers.repositories) {
|
||||||
|
await AsyncStorage.setItem('scraper-repositories', JSON.stringify(localScrapers.repositories));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localScrapers.currentRepository) {
|
||||||
|
await AsyncStorage.setItem('current-repository-id', localScrapers.currentRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localScrapers.scraperSettings) {
|
||||||
|
await AsyncStorage.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('[BackupService] Local scrapers and plugin settings restored');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupService] Failed to restore local scrapers:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateBackupData(backupData: any): void {
|
||||||
|
if (!backupData.version || !backupData.timestamp || !backupData.data) {
|
||||||
|
throw new Error('Invalid backup file format');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupData.version !== this.BACKUP_VERSION) {
|
||||||
|
throw new Error(`Unsupported backup version: ${backupData.version}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const backupService = BackupService.getInstance();
|
||||||
Loading…
Reference in a new issue