diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fea5dd4c..c18f16e7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,12 +2,12 @@ + - @@ -21,7 +21,7 @@ - + @@ -37,4 +37,4 @@ - + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 9d7137b1..3cc4c5b8 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,7 +4,7 @@ buildscript { ext { buildToolsVersion = "35.0.0" minSdkVersion = 24 - compileSdkVersion = 35 + compileSdkVersion = 36 targetSdkVersion = 35 castFrameworkVersion = "22.1.0" ndkVersion = "29.0.14206865" // Required for libmpv AAR built with NDK r29 diff --git a/android/sentry.properties b/android/sentry.properties index ae003a4b..5581e03d 100644 --- a/android/sentry.properties +++ b/android/sentry.properties @@ -1,4 +1,4 @@ defaults.url=https://sentry.io/ defaults.org=tapframe defaults.project=react-native -auth.token=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 +# Using SENTRY_AUTH_TOKEN environment variable \ No newline at end of file diff --git a/ios/LiveActivity/Info.plist b/ios/LiveActivity/Info.plist index d1a218aa..0f118fb7 100644 --- a/ios/LiveActivity/Info.plist +++ b/ios/LiveActivity/Info.plist @@ -7,7 +7,5 @@ NSExtensionPointIdentifier com.apple.widgetkit-extension - RCTNewArchEnabled - diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index d10fbf13..32767529 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; }; F285A1620F5847BA863124AF /* LiveActivity.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EF8716173E0148BD82B233B7 /* LiveActivity.appex */; }; + 797799D4F9144A9E8D2AB90D /* LiveActivity.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 49DDF70A2BBD4320BBD94B1B /* LiveActivity.appex */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -39,6 +40,13 @@ remoteGlobalIDString = 0EA489F2BF6143F1BA7B8485; remoteInfo = LiveActivity; }; + 7A41A1F529994F0C8801F1A5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EFB756D21A05453EA489278C; + remoteInfo = LiveActivity; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -48,6 +56,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + 797799D4F9144A9E8D2AB90D /* LiveActivity.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -93,6 +102,16 @@ name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; + CC1B793274FE428D8531E950 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + name = "Embed Foundation Extensions"; + dstPath = ""; + dstSubfolderSpec = 13; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -128,6 +147,7 @@ EF8716173E0148BD82B233B7 /* LiveActivity.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; fileEncoding = 9; includeInIndex = 0; path = LiveActivity.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Nuvio/AppDelegate.swift; sourceTree = ""; }; F11748442D0722820044C1D9 /* Nuvio-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Nuvio-Bridging-Header.h"; path = "Nuvio/Nuvio-Bridging-Header.h"; sourceTree = ""; }; + 49DDF70A2BBD4320BBD94B1B /* LiveActivity.appex */ = {isa = PBXFileReference; name = "LiveActivity.appex"; path = "LiveActivity.appex"; sourceTree = BUILT_PRODUCTS_DIR; fileEncoding = undefined; lastKnownFileType = undefined; explicitFileType = wrapper.app-extension; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -146,6 +166,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A2537B59C29048BFB082D5F3 /* Embed Foundation Extensions */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -213,6 +240,7 @@ B9F3EB198DED443D980ADFB3 /* LiveActivity */, C05E525650E143FB85ED7622 /* LiveActivity */, D05210A39FF14E649D77F8A8 /* LiveActivity */, + 2EAF711C6AB246A0A253E404 /* LiveActivity */, ); indentWidth = 2; sourceTree = ""; @@ -224,6 +252,7 @@ children = ( 13B07F961A680F5B00A75B9A /* Nuvio.app */, EF8716173E0148BD82B233B7 /* LiveActivity.appex */, + 49DDF70A2BBD4320BBD94B1B /* LiveActivity.appex */, ); name = Products; sourceTree = ""; @@ -295,6 +324,26 @@ name = Nuvio; sourceTree = ""; }; + 2EAF711C6AB246A0A253E404 /* LiveActivity */ = { + isa = PBXGroup; + children = ( + 8034143A77A946B5A793F967 /* Color+hex.swift */, + 3A48D8A298DD48928E8D0A02 /* Date+toTimerInterval.swift */, + 26957CDD392E4E9390811D0D /* Image+dynamic.swift */, + 2F448294A36E433E924078C1 /* LiveActivityView.swift */, + AD48662BB71E4C9C9E340289 /* LiveActivityWidget.swift */, + A83D742B36224176A0AB3B25 /* LiveActivityWidgetBundle.swift */, + 324373F393774A9CA40DE22E /* View+applyIfPresent.swift */, + 373D1473F5A74CBC9DBD108B /* View+applyWidgetURL.swift */, + 3396D68881EF486E99FD480A /* ViewHelpers.swift */, + 0E13CE4BDE2F4555806AE753 /* Info.plist */, + 0F1D0037D1F24E60BDB57628 /* Assets.xcassets */, + 2DE29A8A87D24662BEFFF849 /* LiveActivity.entitlements */, + ); + name = LiveActivity; + path = LiveActivity; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -333,17 +382,36 @@ 571AD3FB23F14FC7BE6A1E44 /* Embed Foundation Extensions */, 13CD9594FB5C4FE4A6794089 /* Embed Foundation Extensions */, F1058FE7710A45FABC0689A7 /* Embed Foundation Extensions */, + CC1B793274FE428D8531E950 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( 8410CAE82E604DD1A187EDA2 /* PBXTargetDependency */, + 523C5D7CB8E740A5A0BF4322 /* PBXTargetDependency */, ); name = Nuvio; productName = Nuvio; productReference = 13B07F961A680F5B00A75B9A /* Nuvio.app */; productType = "com.apple.product-type.application"; }; + EFB756D21A05453EA489278C /* LiveActivity */ = { + isa = PBXNativeTarget; + name = LiveActivity; + productName = LiveActivity; + productReference = 49DDF70A2BBD4320BBD94B1B; + productType = "com.apple.product-type.app-extension"; + buildConfigurationList = F11E3E24512A427FB847D2F6; + buildPhases = ( + 784E2472974841CD88391F31 /* Embed Foundation Extensions */, + A2537B59C29048BFB082D5F3 /* Embed Foundation Extensions */, + B06A5B524C284D9FAFC33F3C /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + ); + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -362,6 +430,9 @@ LastSwiftMigration = 1250; ProvisioningStyle = Automatic; }; + EFB756D21A05453EA489278C = { + LastSwiftMigration = 1250; + }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Nuvio" */; @@ -379,6 +450,7 @@ targets = ( 13B07F861A680F5B00A75B9A /* Nuvio */, 0EA489F2BF6143F1BA7B8485 /* LiveActivity */, + EFB756D21A05453EA489278C /* LiveActivity */, ); }; /* End PBXProject section */ @@ -403,6 +475,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + B06A5B524C284D9FAFC33F3C /* Embed Foundation Extensions */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 730F1CE72F24B27100EF7E51 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -649,6 +729,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 784E2472974841CD88391F31 /* Embed Foundation Extensions */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 730F1CDE2F24B27100EF7E51 /* "Color+hex.swift" in Sources */, + 730F1CDF2F24B27100EF7E51 /* "Date+toTimerInterval.swift" in Sources */, + 730F1CE02F24B27100EF7E51 /* "Image+dynamic.swift" in Sources */, + 730F1CE12F24B27100EF7E51 /* LiveActivityView.swift in Sources */, + 730F1CE22F24B27100EF7E51 /* LiveActivityWidget.swift in Sources */, + 730F1CE32F24B27100EF7E51 /* LiveActivityWidgetBundle.swift in Sources */, + 730F1CE42F24B27100EF7E51 /* "View+applyIfPresent.swift" in Sources */, + 730F1CE52F24B27100EF7E51 /* "View+applyWidgetURL.swift" in Sources */, + 730F1CE62F24B27100EF7E51 /* ViewHelpers.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -657,6 +753,11 @@ target = 0EA489F2BF6143F1BA7B8485 /* LiveActivity */; targetProxy = 55A0DD628D7F4F4F88B4A001 /* PBXContainerItemProxy */; }; + 523C5D7CB8E740A5A0BF4322 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EFB756D21A05453EA489278C /* LiveActivity */; + targetProxy = 7A41A1F529994F0C8801F1A5 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -690,7 +791,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub; - PRODUCT_NAME = Nuvio; + PRODUCT_NAME = "Nuvio"; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -725,7 +826,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub; - PRODUCT_NAME = Nuvio; + PRODUCT_NAME = "Nuvio"; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -900,6 +1001,46 @@ }; name = Release; }; + B062D46778AF40DE92953986 /* Debug */ = { + name = Debug; + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + INFOPLIST_FILE = LiveActivity/Info.plist; + CURRENT_PROJECT_VERSION = "37"; + IPHONEOS_DEPLOYMENT_TARGET = "16.2"; + PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.hub.LiveActivity"; + GENERATE_INFOPLIST_FILE = "YES"; + INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + MARKETING_VERSION = "1.4.1"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + CODE_SIGN_ENTITLEMENTS = "LiveActivity/LiveActivity.entitlements"; + APPLICATION_EXTENSION_API_ONLY = "YES"; + }; + }; + 67617475B4F443CBBCE1A6FD /* Release */ = { + name = Release; + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + INFOPLIST_FILE = LiveActivity/Info.plist; + CURRENT_PROJECT_VERSION = "37"; + IPHONEOS_DEPLOYMENT_TARGET = "16.2"; + PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.hub.LiveActivity"; + GENERATE_INFOPLIST_FILE = "YES"; + INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + MARKETING_VERSION = "1.4.1"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + CODE_SIGN_ENTITLEMENTS = "LiveActivity/LiveActivity.entitlements"; + APPLICATION_EXTENSION_API_ONLY = "YES"; + }; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -930,6 +1071,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + F11E3E24512A427FB847D2F6 /* Build configuration list for PBXNativeTarget "LiveActivity" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B062D46778AF40DE92953986 /* Debug */, + 67617475B4F443CBBCE1A6FD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist index f136eb2a..3686025e 100644 --- a/ios/Nuvio/Info.plist +++ b/ios/Nuvio/Info.plist @@ -1,107 +1,107 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Nuvio - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.2.10 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLSchemes - - nuvio - com.nuvio.app - - - - CFBundleURLSchemes - - exp+nuvio - - - - CFBundleVersion - 37 - LSMinimumSystemVersion - 12.0 - LSRequiresIPhoneOS - - LSSupportsOpeningDocumentsInPlace - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSBonjourServices - - _http._tcp - _googlecast._tcp - _CC1AD845._googlecast._tcp - - NSLocalNetworkUsageDescription - Nuvio uses the local network to discover Cast-enabled devices on your WiFi network and to connect to local media servers. - NSMicrophoneUsageDescription - This app does not require microphone access. - NSSupportsLiveActivities - - NSSupportsLiveActivitiesFrequentUpdates - - RCTNewArchEnabled - - RCTRootViewBackgroundColor - 4278322180 - UIBackgroundModes - - audio - - UIFileSharingEnabled - - UILaunchStoryboardName - SplashScreen - UIRequiredDeviceCapabilities - - arm64 - - UIRequiresFullScreen - - UIStatusBarStyle - UIStatusBarStyleDefault - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Dark - UIViewControllerBasedStatusBarAppearance - - - + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Nuvio + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.4.1 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + nuvio + com.nuvio.hub + + + + CFBundleURLSchemes + + exp+nuvio + + + + CFBundleVersion + 37 + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBonjourServices + + _http._tcp + _googlecast._tcp + _CC1AD845._googlecast._tcp + + NSLocalNetworkUsageDescription + Nuvio uses the local network to discover Cast-enabled devices on your WiFi network and to connect to local media servers. + NSMicrophoneUsageDescription + This app does not require microphone access. + NSSupportsLiveActivities + + NSSupportsLiveActivitiesFrequentUpdates + + RCTNewArchEnabled + + RCTRootViewBackgroundColor + 4278322180 + UIBackgroundModes + + audio + + UIFileSharingEnabled + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Dark + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/ios/Nuvio/Supporting/Expo.plist b/ios/Nuvio/Supporting/Expo.plist index ac3efa6f..b1896b67 100644 --- a/ios/Nuvio/Supporting/Expo.plist +++ b/ios/Nuvio/Supporting/Expo.plist @@ -9,7 +9,7 @@ EXUpdatesLaunchWaitMs 30000 EXUpdatesRuntimeVersion - 1.3.6 + 1.4.1 EXUpdatesURL https://ota.nuvioapp.space/api/manifest diff --git a/ios/sentry.properties b/ios/sentry.properties index ae003a4b..5581e03d 100644 --- a/ios/sentry.properties +++ b/ios/sentry.properties @@ -1,4 +1,4 @@ defaults.url=https://sentry.io/ defaults.org=tapframe defaults.project=react-native -auth.token=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 +# Using SENTRY_AUTH_TOKEN environment variable \ No newline at end of file diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt index ec131ade..5a6b554a 100644 --- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt +++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt @@ -1,9 +1,12 @@ package com.brentvatne.exoplayer +import android.os.Build import android.content.Context import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.SurfaceView import android.view.View import android.view.View.MeasureSpec import android.widget.FrameLayout @@ -20,13 +23,21 @@ import androidx.media3.ui.PlayerView import androidx.media3.ui.SubtitleView import com.brentvatne.common.api.ResizeMode import com.brentvatne.common.api.SubtitleStyle +import com.brentvatne.common.api.ViewType +import com.brentvatne.react.R @UnstableApi class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) { private var localStyle = SubtitleStyle() + private var currentViewType = ViewType.VIEW_TYPE_SURFACE private var pendingResizeMode: Int? = null + private var player: ExoPlayer? = null + private var showSubtitleButton = false + private var shutterColor = Color.TRANSPARENT + private var controllerVisibilityListener: PlayerView.ControllerVisibilityListener? = null + private var fullscreenButtonClickListener: PlayerView.FullscreenButtonClickListener? = null private val liveBadge: TextView = TextView(context).apply { text = "LIVE" setTextColor(Color.WHITE) @@ -39,21 +50,7 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute visibility = View.GONE } - private val playerView = PlayerView(context).apply { - layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - setShutterBackgroundColor(Color.TRANSPARENT) - useController = true - controllerAutoShow = true - controllerHideOnTouch = true - controllerShowTimeoutMs = 5000 - // Don't show subtitle button by default - will be enabled when tracks are available - setShowSubtitleButton(false) - // Enable proper surface view handling to prevent rendering issues - setUseArtwork(false) - setDefaultArtwork(null) - // Ensure proper video scaling - start with FIT mode - resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT - } + private var playerView = createPlayerView(currentViewType) /** * Subtitles rendered in a full-size overlay (NOT inside PlayerView's content frame). @@ -110,6 +107,8 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute } fun setPlayer(player: ExoPlayer?) { + this.player?.removeListener(playerListener) + this.player = player playerView.player = player player?.addListener(playerListener) } @@ -120,7 +119,7 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute ResizeMode.RESIZE_MODE_FIXED_WIDTH -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT ResizeMode.RESIZE_MODE_FILL -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL - ResizeMode.RESIZE_MODE_ZOOM -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM + ResizeMode.RESIZE_MODE_CENTER_CROP -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM else -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT } if (playerView.width > 0 && playerView.height > 0) { @@ -136,7 +135,20 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute fun getPlayerView(): PlayerView = playerView + fun isPlaying(): Boolean = playerView.player?.isPlaying == true + + fun setControllerVisibilityListener(listener: PlayerView.ControllerVisibilityListener?) { + controllerVisibilityListener = listener + playerView.setControllerVisibilityListener(listener) + } + + fun setFullscreenButtonClickListener(listener: PlayerView.FullscreenButtonClickListener?) { + fullscreenButtonClickListener = listener + playerView.setFullscreenButtonClickListener(listener) + } + fun setShowSubtitleButton(show: Boolean) { + showSubtitleButton = show playerView.setShowSubtitleButton(show) } @@ -166,6 +178,47 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute playerView.showController() } + fun updateSurfaceView(@ViewType.ViewType viewType: Int) { + if (currentViewType == viewType) { + return + } + + currentViewType = viewType + + val previousPlayerView = playerView + val previousLayoutParams = previousPlayerView.layoutParams ?: LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT + ) + val previousResizeMode = previousPlayerView.resizeMode + val previousUseController = previousPlayerView.useController + val previousControllerAutoShow = previousPlayerView.controllerAutoShow + val previousControllerHideOnTouch = previousPlayerView.controllerHideOnTouch + val previousControllerShowTimeoutMs = previousPlayerView.controllerShowTimeoutMs + + val replacementPlayerView = createPlayerView(viewType).apply { + layoutParams = previousLayoutParams + resizeMode = previousResizeMode + useController = previousUseController + controllerAutoShow = previousControllerAutoShow + controllerHideOnTouch = previousControllerHideOnTouch + controllerShowTimeoutMs = previousControllerShowTimeoutMs + setShowSubtitleButton(showSubtitleButton) + setControllerVisibilityListener(controllerVisibilityListener) + setFullscreenButtonClickListener(fullscreenButtonClickListener) + setShutterBackgroundColor(shutterColor) + player = this@ExoPlayerView.player + } + + removeView(previousPlayerView) + playerView = replacementPlayerView + addView(playerView, 0, previousLayoutParams) + + updateSubtitleRenderingMode() + applySubtitleStyle(localStyle) + playerView.requestLayout() + } + fun setSubtitleStyle(style: SubtitleStyle) { localStyle = style applySubtitleStyle(localStyle) @@ -287,6 +340,7 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute } fun setShutterColor(color: Int) { + shutterColor = color playerView.setShutterBackgroundColor(color) } @@ -328,4 +382,28 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute applySubtitleStyle(localStyle) } } + + private fun createPlayerView(@ViewType.ViewType viewType: Int): PlayerView { + val layoutRes = when (viewType) { + ViewType.VIEW_TYPE_TEXTURE -> R.layout.exo_player_view_texture + else -> R.layout.exo_player_view_surface + } + + return (LayoutInflater.from(context).inflate(layoutRes, this, false) as PlayerView).apply { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + setShutterBackgroundColor(shutterColor) + useController = true + controllerAutoShow = true + controllerHideOnTouch = true + controllerShowTimeoutMs = 5000 + setShowSubtitleButton(showSubtitleButton) + setUseArtwork(false) + setDefaultArtwork(null) + resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT + + if (viewType == ViewType.VIEW_TYPE_SURFACE_SECURE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + (videoSurfaceView as? SurfaceView)?.setSecure(true) + } + } + } } diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 3bd58165..773535a9 100644 --- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1567,6 +1567,11 @@ public class ReactExoplayerView extends FrameLayout implements Track audioTrack = exoplayerTrackToGenericTrack(format, groupIndex, selection, group); audioTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); audioTrack.setSelected(isSelected); + // Encode channel count into title so JS can read it e.g. "English|ch:6" + if (format.channelCount != Format.NO_VALUE && format.channelCount > 0) { + String existing = audioTrack.getTitle() != null ? audioTrack.getTitle() : ""; + audioTrack.setTitle(existing + "|ch:" + format.channelCount); + } audioTracks.add(audioTrack); } @@ -1753,7 +1758,11 @@ public class ReactExoplayerView extends FrameLayout implements Track track = new Track(); track.setIndex(groupIndex); track.setLanguage(format.language != null ? format.language : "unknown"); - track.setTitle(format.label != null ? format.label : "Track " + (groupIndex + 1)); + String baseTitle = format.label != null ? format.label : ""; + if (format.channelCount != Format.NO_VALUE && format.channelCount > 0) { + baseTitle = baseTitle + "|ch:" + format.channelCount; + } + track.setTitle(baseTitle); track.setSelected(false); // Don't report selection status - let PlayerView handle it if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType); track.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); @@ -2127,7 +2136,8 @@ public class ReactExoplayerView extends FrameLayout implements } private void selectTextTrackInternal(String type, String value) { - if (player == null || trackSelector == null) return; + if (player == null || trackSelector == null) + return; DebugLog.d(TAG, "selectTextTrackInternal: type=" + type + ", value=" + value); @@ -2146,6 +2156,10 @@ public class ReactExoplayerView extends FrameLayout implements if (textRendererIndex != C.INDEX_UNSET) { TrackGroupArray groups = info.getTrackGroups(textRendererIndex); boolean trackFound = false; + // react-native-video uses a flattened `textTracks` list on the JS side. + // For HLS/DASH, each TrackGroup often contains a single track at index 0, + // so comparing against `trackIndex` alone makes only the first subtitle selectable. + int flattenedIndex = 0; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup group = groups.get(groupIndex); @@ -2159,10 +2173,12 @@ public class ReactExoplayerView extends FrameLayout implements isMatch = true; } else if ("index".equals(type)) { int targetIndex = ReactBridgeUtils.safeParseInt(value, -1); - if (targetIndex == trackIndex) { + if (targetIndex == flattenedIndex) { isMatch = true; } } + + flattenedIndex++; if (isMatch) { TrackSelectionOverride override = new TrackSelectionOverride(group, diff --git a/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_surface.xml b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_surface.xml new file mode 100644 index 00000000..4ea3c30d --- /dev/null +++ b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_surface.xml @@ -0,0 +1,6 @@ + + diff --git a/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_texture.xml b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_texture.xml new file mode 100644 index 00000000..53c1909b --- /dev/null +++ b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_texture.xml @@ -0,0 +1,6 @@ + + diff --git a/package-lock.json b/package-lock.json index e7c7a877..a1da7c37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,34 +11,34 @@ "dependencies": { "@adrianso/react-native-device-brightness": "^1.2.7", "@backpackapp-io/react-native-toast": "^0.15.1", - "@bottom-tabs/react-navigation": "^1.0.2", + "@bottom-tabs/react-navigation": "^1.1.0", "@d11/react-native-fast-image": "^8.13.0", "@expo/env": "^2.0.7", "@expo/metro-runtime": "~6.1.2", "@expo/vector-icons": "^15.0.2", - "@gorhom/bottom-sheet": "^5.2.6", - "@kesha-antonov/react-native-background-downloader": "^4.4.5", - "@legendapp/list": "^2.0.13", - "@lottiefiles/dotlottie-react": "^0.13.5", + "@gorhom/bottom-sheet": "^5.2.8", + "@kesha-antonov/react-native-background-downloader": "^4.5.3", + "@legendapp/list": "^2.0.19", + "@lottiefiles/dotlottie-react": "^0.18.5", "@react-native-community/blur": "^4.4.1", - "@react-native-community/netinfo": "^11.4.1", - "@react-native-community/slider": "^5.1.1", + "@react-native-community/netinfo": "^12.0.1", + "@react-native-community/slider": "^5.1.2", "@react-native-picker/picker": "^2.11.4", - "@react-navigation/bottom-tabs": "^7.3.10", - "@react-navigation/native": "^7.1.6", - "@react-navigation/native-stack": "^7.3.10", - "@react-navigation/stack": "^7.2.10", - "@sentry/react-native": "^7.6.0", - "@shopify/flash-list": "^2.2.0", - "@shopify/react-native-skia": "^2.4.14", - "@types/lodash": "^4.17.16", - "@types/react-native-video": "^5.0.20", - "axios": "^1.12.2", - "axios-cookiejar-support": "^6.0.4", + "@react-navigation/bottom-tabs": "^7.15.5", + "@react-navigation/native": "^7.1.33", + "@react-navigation/native-stack": "^7.14.5", + "@react-navigation/stack": "^7.8.5", + "@sentry/react-native": "^8.4.0", + "@shopify/flash-list": "^2.3.0", + "@shopify/react-native-skia": "^2.5.1", + "@types/lodash": "^4.17.24", + "@types/react-native-video": "^5.0.21", + "axios": "^1.13.6", + "axios-cookiejar-support": "^6.0.5", "cheerio-without-node-native": "^0.20.2", "crypto-js": "^4.2.0", "date-fns": "^4.1.0", - "eventemitter3": "^5.0.1", + "eventemitter3": "^5.0.4", "expo": "^54", "expo-application": "~7.0.7", "expo-auth-session": "~7.0.8", @@ -66,47 +66,47 @@ "expo-system-ui": "~6.0.7", "expo-updates": "~29.0.12", "expo-web-browser": "~15.0.8", - "i18next": "^25.7.3", + "i18next": "^25.8.18", "intl-pluralrules": "^2.0.1", - "lodash": "^4.17.21", - "lottie-react-native": "~7.3.1", - "posthog-react-native": "^4.4.0", + "lodash": "^4.17.23", + "lottie-react-native": "~7.3.6", + "posthog-react-native": "^4.37.2", "react": "19.1.0", "react-dom": "19.1.0", - "react-i18next": "^16.5.1", + "react-i18next": "^16.5.8", "react-native": "0.81.4", - "react-native-boost": "^0.6.2", - "react-native-bottom-tabs": "^1.0.2", - "react-native-gesture-handler": "^2.29.1", + "react-native-boost": "^1.0.0", + "react-native-bottom-tabs": "^1.1.0", + "react-native-gesture-handler": "^2.30.0", "react-native-get-random-values": "^2.0.0", "react-native-google-cast": "^4.9.1", - "react-native-image-colors": "^2.5.0", + "react-native-image-colors": "^2.6.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.2.0", + "react-native-mmkv": "^4.2.0", + "react-native-nitro-modules": "^0.35.0", + "react-native-paper": "^5.15.0", + "react-native-reanimated": "^4.2.2", "react-native-reanimated-carousel": "^4.0.3", - "react-native-safe-area-context": "~5.6.0", - "react-native-screens": "^4.18.0", - "react-native-svg": "^15.12.1", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "^4.24.0", + "react-native-svg": "^15.15.3", "react-native-url-polyfill": "^3.0.0", "react-native-vector-icons": "^10.3.0", "react-native-video": "^6.19.0", - "react-native-web": "^0.21.0", + "react-native-web": "^0.21.2", "react-native-wheel-color-picker": "^1.3.1", - "react-native-worklets": "^0.7.1" + "react-native-worklets": "^0.7.4" }, "devDependencies": { - "@babel/core": "^7.25.2", + "@babel/core": "^7.29.0", "@types/crypto-js": "^4.2.2", "@types/react": "~18.3.12", "@types/react-native": "^0.72.8", "@types/react-native-vector-icons": "^6.4.18", "babel-plugin-transform-remove-console": "^6.9.4", "patch-package": "^8.0.1", - "react-native-svg-transformer": "^1.5.0", + "react-native-svg-transformer": "^1.5.3", "typescript": "^5.9.3", "xcode": "^3.0.1" } @@ -132,12 +132,12 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -146,29 +146,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -185,13 +185,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -213,12 +213,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -229,17 +229,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", - "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.5", + "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "engines": { @@ -267,16 +267,16 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.7.tgz", + "integrity": "sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" + "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -305,27 +305,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -347,9 +347,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -373,14 +373,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -430,27 +430,27 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", - "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2" + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -543,12 +543,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -558,14 +558,14 @@ } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz", - "integrity": "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", + "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-decorators": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-decorators": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -641,12 +641,12 @@ } }, "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", - "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -668,12 +668,12 @@ } }, "node_modules/@babel/plugin-syntax-export-default-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.27.1.tgz", - "integrity": "sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.28.6.tgz", + "integrity": "sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -683,12 +683,12 @@ } }, "node_modules/@babel/plugin-syntax-flow": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", - "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", + "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -698,12 +698,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -737,12 +737,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -854,12 +854,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -884,14 +884,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -901,13 +901,13 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { @@ -918,12 +918,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", - "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -933,13 +933,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -949,13 +949,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -965,17 +965,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -985,13 +985,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1096,12 +1096,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", - "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1111,13 +1111,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1127,13 +1127,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1143,12 +1143,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1158,12 +1158,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1173,16 +1173,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.4" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1192,12 +1192,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1207,12 +1207,12 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", - "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { @@ -1238,13 +1238,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1254,14 +1254,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1286,16 +1286,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", - "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1366,12 +1366,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1381,13 +1381,13 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", - "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", + "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", @@ -1416,12 +1416,12 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { @@ -1462,16 +1462,16 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", - "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1" + "@babel/plugin-syntax-typescript": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1536,40 +1536,40 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1578,17 +1578,17 @@ }, "node_modules/@babel/traverse--for-generate-function-map": { "name": "@babel/traverse", - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1596,9 +1596,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1700,25 +1700,25 @@ } }, "node_modules/@expo/cli": { - "version": "54.0.19", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.19.tgz", - "integrity": "sha512-Za+Ena29uYkq2c1Lbh+r3VrooR/mW7c9dahoH4WvL1T9ttbfAeu7sJmCuWZo88bZ4bFsOpE5fYne71DK11iSrQ==", + "version": "54.0.23", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.23.tgz", + "integrity": "sha512-km0h72SFfQCmVycH/JtPFTVy69w6Lx1cHNDmfLfQqgKFYeeHTjx7LVDP4POHCtNxFP2UeRazrygJhlh4zz498g==", "license": "MIT", "dependencies": { "@0no-co/graphql.web": "^1.0.8", - "@expo/code-signing-certificates": "^0.0.5", - "@expo/config": "~12.0.12", - "@expo/config-plugins": "~54.0.3", + "@expo/code-signing-certificates": "^0.0.6", + "@expo/config": "~12.0.13", + "@expo/config-plugins": "~54.0.4", "@expo/devcert": "^1.2.1", "@expo/env": "~2.0.8", "@expo/image-utils": "^0.8.8", "@expo/json-file": "^10.0.8", - "@expo/metro": "~54.1.0", - "@expo/metro-config": "~54.0.11", + "@expo/metro": "~54.2.0", + "@expo/metro-config": "~54.0.14", "@expo/osascript": "^2.3.8", - "@expo/package-manager": "^1.9.9", + "@expo/package-manager": "^1.9.10", "@expo/plist": "^0.4.8", - "@expo/prebuild-config": "^54.0.7", + "@expo/prebuild-config": "^54.0.8", "@expo/schema-utils": "^0.1.8", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", @@ -1743,7 +1743,7 @@ "glob": "^13.0.0", "lan-network": "^0.1.6", "minimatch": "^9.0.0", - "node-forge": "^1.3.1", + "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", @@ -1786,10 +1786,23 @@ } } }, + "node_modules/@expo/cli/node_modules/@expo/env": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.11.tgz", + "integrity": "sha512-xV+ps6YCW7XIPVUwFVCRN2nox09dnRwy8uIjwHWTODu0zFw4kp4omnVkl0OOjuu2XOe7tdgAHxikrkJt9xB/7Q==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "getenv": "^2.0.0" + } + }, "node_modules/@expo/cli/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1798,33 +1811,23 @@ "node": ">=10" } }, - "node_modules/@expo/cli/node_modules/undici": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", - "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, "node_modules/@expo/code-signing-certificates": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz", - "integrity": "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", + "integrity": "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==", "license": "MIT", "dependencies": { - "node-forge": "^1.2.1", - "nullthrows": "^1.1.1" + "node-forge": "^1.3.3" } }, "node_modules/@expo/config": { - "version": "12.0.12", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.12.tgz", - "integrity": "sha512-X2MW86+ulLpMGvdgfvpl2EOBAKUlwvnvoPwdaZeeyWufGopn1nTUeh4C9gMsplDaP1kXv9sLXVhOoUoX6g9PvQ==", + "version": "12.0.13", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz", + "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "~7.10.4", - "@expo/config-plugins": "~54.0.3", + "@expo/config-plugins": "~54.0.4", "@expo/config-types": "^54.0.10", "@expo/json-file": "^10.0.8", "deepmerge": "^4.3.1", @@ -1861,9 +1864,9 @@ } }, "node_modules/@expo/config-plugins/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1888,9 +1891,9 @@ } }, "node_modules/@expo/config/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1940,16 +1943,17 @@ } }, "node_modules/@expo/env": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.8.tgz", - "integrity": "sha512-5VQD6GT8HIMRaSaB5JFtOXuvfDVU80YtZIuUT/GDhUF782usIXY13Tn3IdDz1Tm/lqA9qnRZQ1BF4t7LlvdJPA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.1.1.tgz", + "integrity": "sha512-rVvHC4I6xlPcg+mAO09ydUi2Wjv1ZytpLmHOSzvXzBAz9mMrJggqCe4s4dubjJvi/Ino/xQCLhbaLCnTtLpikg==", "license": "MIT", "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", - "dotenv": "~16.4.5", - "dotenv-expand": "~11.0.6", "getenv": "^2.0.0" + }, + "engines": { + "node": ">=20.12.0" } }, "node_modules/@expo/fingerprint": { @@ -1975,9 +1979,9 @@ } }, "node_modules/@expo/fingerprint/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1987,9 +1991,9 @@ } }, "node_modules/@expo/image-utils": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.8.tgz", - "integrity": "sha512-HHHaG4J4nKjTtVa1GG9PCh763xlETScfEyNxxOvfTRr8IKPJckjTyqSLEtdJoFNJ1vqiABEjW7tqGhqGibZLeA==", + "version": "0.8.12", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.12.tgz", + "integrity": "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", @@ -1998,16 +2002,13 @@ "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", - "resolve-global": "^1.0.0", - "semver": "^7.6.0", - "temp-dir": "~2.0.0", - "unique-string": "~2.0.0" + "semver": "^7.6.0" } }, "node_modules/@expo/image-utils/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2017,57 +2018,50 @@ } }, "node_modules/@expo/json-file": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.8.tgz", - "integrity": "sha512-9LOTh1PgKizD1VXfGQ88LtDH0lRwq9lsTb4aichWTWSWqy3Ugfkhfm3BhzBIkJJfQQ5iJu3m/BoRlEIjoCGcnQ==", + "version": "10.0.12", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.12.tgz", + "integrity": "sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "~7.10.4", + "@babel/code-frame": "^7.20.0", "json5": "^2.2.3" } }, - "node_modules/@expo/json-file/node_modules/@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, "node_modules/@expo/metro": { - "version": "54.1.0", - "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.1.0.tgz", - "integrity": "sha512-MgdeRNT/LH0v1wcO0TZp9Qn8zEF0X2ACI0wliPtv5kXVbXWI+yK9GyrstwLAiTXlULKVIg3HVSCCvmLu0M3tnw==", + "version": "54.2.0", + "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.2.0.tgz", + "integrity": "sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w==", "license": "MIT", "dependencies": { - "metro": "0.83.2", - "metro-babel-transformer": "0.83.2", - "metro-cache": "0.83.2", - "metro-cache-key": "0.83.2", - "metro-config": "0.83.2", - "metro-core": "0.83.2", - "metro-file-map": "0.83.2", - "metro-resolver": "0.83.2", - "metro-runtime": "0.83.2", - "metro-source-map": "0.83.2", - "metro-transform-plugins": "0.83.2", - "metro-transform-worker": "0.83.2" + "metro": "0.83.3", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-config": "0.83.3", + "metro-core": "0.83.3", + "metro-file-map": "0.83.3", + "metro-minify-terser": "0.83.3", + "metro-resolver": "0.83.3", + "metro-runtime": "0.83.3", + "metro-source-map": "0.83.3", + "metro-symbolicate": "0.83.3", + "metro-transform-plugins": "0.83.3", + "metro-transform-worker": "0.83.3" } }, "node_modules/@expo/metro-config": { - "version": "54.0.11", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.11.tgz", - "integrity": "sha512-Bmht6VW9w6Wk49EFqkMzYpICV++Q3Kuqh2KygjH/e5mj/9wHSCWLkmJYmUn0XaOo4bm6BwOp/hO3r5YNKP3AeQ==", + "version": "54.0.14", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.14.tgz", + "integrity": "sha512-hxpLyDfOR4L23tJ9W1IbJJsG7k4lv2sotohBm/kTYyiG+pe1SYCAWsRmgk+H42o/wWf/HQjE5k45S5TomGLxNA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", - "@expo/config": "~12.0.12", + "@expo/config": "~12.0.13", "@expo/env": "~2.0.8", "@expo/json-file": "~10.0.8", - "@expo/metro": "~54.1.0", + "@expo/metro": "~54.2.0", "@expo/spawn-async": "^1.7.2", "browserslist": "^4.25.0", "chalk": "^4.1.0", @@ -2092,6 +2086,19 @@ } } }, + "node_modules/@expo/metro-config/node_modules/@expo/env": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.11.tgz", + "integrity": "sha512-xV+ps6YCW7XIPVUwFVCRN2nox09dnRwy8uIjwHWTODu0zFw4kp4omnVkl0OOjuu2XOe7tdgAHxikrkJt9xB/7Q==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "getenv": "^2.0.0" + } + }, "node_modules/@expo/metro-runtime": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-6.1.2.tgz", @@ -2116,25 +2123,24 @@ } }, "node_modules/@expo/osascript": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.3.8.tgz", - "integrity": "sha512-/TuOZvSG7Nn0I8c+FcEaoHeBO07yu6vwDgk7rZVvAXoeAK5rkA09jRyjYsZo+0tMEFaToBeywA6pj50Mb3ny9w==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.4.2.tgz", + "integrity": "sha512-/XP7PSYF2hzOZzqfjgkoWtllyeTN8dW3aM4P6YgKcmmPikKL5FdoyQhti4eh6RK5a5VrUXJTOlTNIpIHsfB5Iw==", "license": "MIT", "dependencies": { - "@expo/spawn-async": "^1.7.2", - "exec-async": "^2.2.0" + "@expo/spawn-async": "^1.7.2" }, "engines": { "node": ">=12" } }, "node_modules/@expo/package-manager": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.9.tgz", - "integrity": "sha512-Nv5THOwXzPprMJwbnXU01iXSrCp3vJqly9M4EJ2GkKko9Ifer2ucpg7x6OUsE09/lw+npaoUnHMXwkw7gcKxlg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.10.3.tgz", + "integrity": "sha512-ZuXiK/9fCrIuLjPSe1VYmfp0Sa85kCMwd8QQpgyi5ufppYKRtLBg14QOgUqj8ZMbJTxE0xqzd0XR7kOs3vAK9A==", "license": "MIT", "dependencies": { - "@expo/json-file": "^10.0.8", + "@expo/json-file": "^10.0.12", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", @@ -2154,14 +2160,14 @@ } }, "node_modules/@expo/prebuild-config": { - "version": "54.0.7", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.7.tgz", - "integrity": "sha512-cKqBsiwcFFzpDWgtvemrCqJULJRLDLKo2QMF74NusoGNpfPI3vQVry1iwnYLeGht02AeD3dvfhpqBczD3wchxA==", + "version": "54.0.8", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.8.tgz", + "integrity": "sha512-EA7N4dloty2t5Rde+HP0IEE+nkAQiu4A/+QGZGT9mFnZ5KKjPPkqSyYcRvP5bhQE10D+tvz6X0ngZpulbMdbsg==", "license": "MIT", "dependencies": { - "@expo/config": "~12.0.11", - "@expo/config-plugins": "~54.0.3", - "@expo/config-types": "^54.0.9", + "@expo/config": "~12.0.13", + "@expo/config-plugins": "~54.0.4", + "@expo/config-types": "^54.0.10", "@expo/image-utils": "^0.8.8", "@expo/json-file": "^10.0.8", "@react-native/normalize-colors": "0.81.5", @@ -2175,9 +2181,9 @@ } }, "node_modules/@expo/prebuild-config/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2217,9 +2223,9 @@ "license": "MIT" }, "node_modules/@expo/vector-icons": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.0.3.tgz", - "integrity": "sha512-SBUyYKphmlfUBqxSfDdJ3jAdEVSALS2VUPOUyqn48oZmb2TL/O7t7/PQm5v4NQujYEPLPMTLn9KVw6H7twwbTA==", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz", + "integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==", "license": "MIT", "peerDependencies": { "expo-font": ">=14.0.4", @@ -2234,29 +2240,19 @@ "license": "MIT" }, "node_modules/@expo/xcpretty": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.3.2.tgz", - "integrity": "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.1.tgz", + "integrity": "sha512-KZNxZvnGCtiM2aYYZ6Wz0Ix5r47dAvpNLApFtZWnSoERzAdOMzVBOPysBoM0JlF6FKWZ8GPqgn6qt3dV/8Zlpg==", "license": "BSD-3-Clause", "dependencies": { - "@babel/code-frame": "7.10.4", + "@babel/code-frame": "^7.20.0", "chalk": "^4.1.0", - "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, - "node_modules/@expo/xcpretty/node_modules/@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, "node_modules/@gorhom/bottom-sheet": { "version": "5.2.8", "resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-5.2.8.tgz", @@ -2302,27 +2298,6 @@ "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==", "license": "MIT" }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -2378,19 +2353,6 @@ "node": ">=6" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", @@ -2404,45 +2366,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -2744,18 +2667,18 @@ } }, "node_modules/@kesha-antonov/react-native-background-downloader": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@kesha-antonov/react-native-background-downloader/-/react-native-background-downloader-4.4.5.tgz", - "integrity": "sha512-OrQdhDhroRFiUKfoX6AoPV7qgA/UzAJljI/980NvPK4okux36qGKzN2BX/sRL6emv3MNQSKyKifjxYq/TpCq0Q==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/@kesha-antonov/react-native-background-downloader/-/react-native-background-downloader-4.5.3.tgz", + "integrity": "sha512-cFo1ZJCMYcgHbsxpXWVnNgQ68it9VmJL55m2tdzcmBjwqIbiugPLBTARpGtZZSJsUW2UX063MVUxXrxZSxiDXw==", "license": "Apache-2.0", "peerDependencies": { "react-native": ">=0.57.0" } }, "node_modules/@legendapp/list": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/@legendapp/list/-/list-2.0.18.tgz", - "integrity": "sha512-Uo51s+9u+QvQCathLFEckb+OK2eXECuhHo+e+Gn+GlSR4V8tClvCSHOOdagsT/Dsto05jC9Yt5onhqxjLENn7A==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/@legendapp/list/-/list-2.0.19.tgz", + "integrity": "sha512-zDWg8yg0smKxxk+M7gwAbZAnf5uczohPA+IjqLSkImz7+e9ytxeT0Mq35RBO9RTKODOXfV/aIgm1uqUHLBEdmg==", "license": "MIT", "dependencies": { "use-sync-external-store": "^1.5.0" @@ -2766,27 +2689,27 @@ } }, "node_modules/@lottiefiles/dotlottie-react": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.13.5.tgz", - "integrity": "sha512-4U5okwjRqDPkjB572hfZtLXJ/LGfCo6vDwUB2KIPEUoSgqbIlw+UrbnaqVp3GS+dRvhMD27F2JObpHpYRlpF0Q==", + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.18.5.tgz", + "integrity": "sha512-Hw4thvVOHJDbXbtEF4X3/P0C2xaTGdQxOQnlTdcw+pcLwTgsb2O7+sW8TpUJz2fAgzGAEUpW8O5KmN+eibiRaw==", "license": "MIT", "dependencies": { - "@lottiefiles/dotlottie-web": "0.44.0" + "@lottiefiles/dotlottie-web": "0.66.1" }, "peerDependencies": { "react": "^17 || ^18 || ^19" } }, "node_modules/@lottiefiles/dotlottie-web": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.44.0.tgz", - "integrity": "sha512-IUWKVciDJI/BMWDWnh7j0Ngd0N8q9ySRAwm84aDqIE07qpmdZ7x1rkIpBaU1yHSNqNYHeh1Rxsl+LC3CY4f0KA==", + "version": "0.66.1", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.66.1.tgz", + "integrity": "sha512-DhXVlWpnTLmlrDamqSJR/PsMFTFBhe4a04MRojAsgtk3FmEEbIluWA6qbYAKbjvj8o6Nju8f+R9fhOxL+iWd1w==", "license": "MIT" }, "node_modules/@posthog/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.7.1.tgz", - "integrity": "sha512-kjK0eFMIpKo9GXIbts8VtAknsoZ18oZorANdtuTj1CbgS28t4ZVq//HAWhnxEuXRTrtkd+SUJ6Ux3j2Af8NCuA==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.3.tgz", + "integrity": "sha512-nehG2nig9qiU4lEUIyfXQLaBnylm5wdDiIBsp2tBFJX5BcUHNAXSwpkHjKLQ9TDfik0HW1HwZ2mY/3hJgJNToQ==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6" @@ -2803,18 +2726,19 @@ } }, "node_modules/@react-native-community/netinfo": { - "version": "11.4.1", - "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz", - "integrity": "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-12.0.1.tgz", + "integrity": "sha512-P/3caXIvfYSJG8AWJVefukg+ZGRPs+M4Lp3pNJtgcTYoJxCjWrKQGNnCkj/Cz//zWa/avGed0i/wzm0T8vV2IQ==", "license": "MIT", "peerDependencies": { + "react": "*", "react-native": ">=0.59" } }, "node_modules/@react-native-community/slider": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-5.1.1.tgz", - "integrity": "sha512-W98If/LnTaziU3/0h5+G1LvJaRhMc6iLQBte6UWa4WBIHDMaDPglNBIFKcCXc9Dxp83W+f+5Wv22Olq9M2HJYA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-5.1.2.tgz", + "integrity": "sha512-UV/MjCyCtSjS5BQDrrGIMmCXm309xEG6XbR0Dj65kzTraJSVDxSjQS2uBUXgX+5SZUOCzCxzv3OufOZBdtQY4w==", "license": "MIT" }, "node_modules/@react-native-picker/picker": { @@ -2946,7 +2870,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -2964,9 +2888,9 @@ } }, "node_modules/@react-native/codegen/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -3037,9 +2961,9 @@ } }, "node_modules/@react-native/community-cli-plugin/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3136,17 +3060,17 @@ } }, "node_modules/@react-navigation/bottom-tabs": { - "version": "7.8.12", - "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.8.12.tgz", - "integrity": "sha512-efVt5ydHK+b4ZtjmN81iduaO5dPCmzhLBFwjCR8pV4x4VzUfJmtUJizLqTXpT3WatHdeon2gDPwhhoelsvu/JA==", + "version": "7.15.5", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.5.tgz", + "integrity": "sha512-wQHredlCrRmShWQ1vF4HUcLdaiJ8fUgnbaeQH7BJ7MQVQh4mdzab0IOY/4QSmUyNRB350oyu1biTycyQ5FKWMQ==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.2", + "@react-navigation/elements": "^2.9.10", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { - "@react-navigation/native": "^7.1.25", + "@react-navigation/native": "^7.1.33", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -3177,12 +3101,12 @@ } }, "node_modules/@react-navigation/core": { - "version": "7.13.6", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.13.6.tgz", - "integrity": "sha512-7QG29HAWOR8wYuPkfTN8L2Po+kE1xn3nsi2sS35sGngq8HYZRHfXvxrhrAZYfFnFq2hUtOhcXnSS6vEWU/5rmA==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.16.1.tgz", + "integrity": "sha512-xhquoyhKdqDfiL7LuupbwYnmauUGfVFGDEJO34m26k8zSN1eDjQ2stBZcHN8ILOI1PrG9885nf8ZmfaQxPS0ww==", "license": "MIT", "dependencies": { - "@react-navigation/routers": "^7.5.2", + "@react-navigation/routers": "^7.5.3", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", @@ -3196,9 +3120,9 @@ } }, "node_modules/@react-navigation/elements": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.2.tgz", - "integrity": "sha512-J1GltOAGowNLznEphV/kr4zs0U7mUBO1wVA2CqpkN8ePBsoxrAmsd+T5sEYUCXN9KgTDFvc6IfcDqrGSQngd/g==", + "version": "2.9.10", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.10.tgz", + "integrity": "sha512-N8tuBekzTRb0pkMHFJGvmC6Q5OisSbt6gzvw7RHMnp4NDo5auVllT12sWFaTXf8mTduaLKNSrD/NZNaOqThCBg==", "license": "MIT", "dependencies": { "color": "^4.2.3", @@ -3207,7 +3131,7 @@ }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", - "@react-navigation/native": "^7.1.25", + "@react-navigation/native": "^7.1.33", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" @@ -3242,12 +3166,12 @@ } }, "node_modules/@react-navigation/native": { - "version": "7.1.25", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.25.tgz", - "integrity": "sha512-zQeWK9txDePWbYfqTs0C6jeRdJTm/7VhQtW/1IbJNDi9/rFIRzZule8bdQPAnf8QWUsNujRmi1J9OG/hhfbalg==", + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.33.tgz", + "integrity": "sha512-DpFdWGcgLajKZ1TuIvDNQsblN2QaUFWpTQaB8v7WRP9Mix8H/6TFoIrZd93pbymI2hybd6UYrD+lI408eWVcfw==", "license": "MIT", "dependencies": { - "@react-navigation/core": "^7.13.6", + "@react-navigation/core": "^7.16.1", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", @@ -3259,18 +3183,18 @@ } }, "node_modules/@react-navigation/native-stack": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.8.6.tgz", - "integrity": "sha512-eBY92xb4H53c9jiWriKMOZmQ/Tu9w1qcUrgOA/qjQOvJFbgKF9D6y3e4UuBaDQzjWjLEDZLaiwXe8cwXRb46mg==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.5.tgz", + "integrity": "sha512-NuyMf21kKk3jODvYgpcDA+HwyWr/KEj72ciqquyEupZlsmQ3WNUGgdaixEB3A19+iPOvHLQzDLcoTrrqZk8Leg==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.2", + "@react-navigation/elements": "^2.9.10", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { - "@react-navigation/native": "^7.1.25", + "@react-navigation/native": "^7.1.33", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -3301,26 +3225,26 @@ } }, "node_modules/@react-navigation/routers": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.2.tgz", - "integrity": "sha512-kymreY5aeTz843E+iPAukrsOtc7nabAH6novtAPREmmGu77dQpfxPB2ZWpKb5nRErIRowp1kYRoN2Ckl+S6JYw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz", + "integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==", "license": "MIT", "dependencies": { "nanoid": "^3.3.11" } }, "node_modules/@react-navigation/stack": { - "version": "7.6.12", - "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-7.6.12.tgz", - "integrity": "sha512-hq5d+lWUwBnwPcBNyUYHiirzRuiD5YhQDIgZWzRConfcRwI/qwFW5+5bCCJ3fQZnNlP05UA4ZlI6r1ysU6y6ww==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-7.8.5.tgz", + "integrity": "sha512-ZOD1gUhWpbI+1PD5mKZFnLBh3Vfq2bqhO5/NeEruaQwNdXkiiHpi59OUKMnFRQURjjYXf/skTM9hJa6zHdiyFw==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.2", + "@react-navigation/elements": "^2.9.10", "color": "^4.2.3", "use-latest-callback": "^0.2.4" }, "peerDependencies": { - "@react-navigation/native": "^7.1.25", + "@react-navigation/native": "^7.1.33", "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", @@ -3352,131 +3276,130 @@ } }, "node_modules/@sentry-internal/browser-utils": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.26.0.tgz", - "integrity": "sha512-rPg1+JZlfp912pZONQAWZzbSaZ9L6R2VrMcCEa+2e2Gqk9um4b+LqF5RQWZsbt5Z0n0azSy/KQ6zAe/zTPXSOg==", + "version": "10.43.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.43.0.tgz", + "integrity": "sha512-8zYTnzhAPvNkVH1Irs62wl0J/c+0QcJ62TonKnzpSFUUD3V5qz8YDZbjIDGfxy+1EB9fO0sxtddKCzwTHF/MbQ==", "license": "MIT", "dependencies": { - "@sentry/core": "10.26.0" + "@sentry/core": "10.43.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.26.0.tgz", - "integrity": "sha512-0vk9eQP0CXD7Y2WkcCIWHaAqnXOAi18/GupgWLnbB2kuQVYVtStWxtW+OWRe8W/XwSnZ5m6JBTVeokuk/O16DQ==", + "version": "10.43.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.43.0.tgz", + "integrity": "sha512-YoXuwluP6eOcQxTeTtaWb090++MrLyWOVsUTejzUQQ6LFL13Jwt+bDPF1kvBugMq4a7OHw/UNKQfd6//rZMn2g==", "license": "MIT", "dependencies": { - "@sentry/core": "10.26.0" + "@sentry/core": "10.43.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.26.0.tgz", - "integrity": "sha512-FMySQnY2/p0dVtFUBgUO+aMdK2ovqnd7Q/AkvMQUsN/5ulyj6KZx3JX3CqOqRtAr1izoCe4Kh2pi5t//sQmvsg==", + "version": "10.43.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.43.0.tgz", + "integrity": "sha512-khCXlGrlH1IU7P5zCEAJFestMeH97zDVCekj8OsNNDtN/1BmCJ46k6Xi0EqAUzdJgrOLJeLdoYdgtiIjovZ8Sg==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.26.0", - "@sentry/core": "10.26.0" + "@sentry-internal/browser-utils": "10.43.0", + "@sentry/core": "10.43.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.26.0.tgz", - "integrity": "sha512-vs7d/P+8M1L1JVAhhJx2wo15QDhqAipnEQvuRZ6PV7LUcS1un9/Vx49FMxpIkx6JcKADJVwtXrS1sX2hoNT/kw==", + "version": "10.43.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.43.0.tgz", + "integrity": "sha512-ZIw1UNKOFXo1LbPCJPMAx9xv7D8TMZQusLDUgb6BsPQJj0igAuwd7KRGTkjjgnrwBp2O/sxcQFRhQhknWk7QPg==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "10.26.0", - "@sentry/core": "10.26.0" + "@sentry-internal/replay": "10.43.0", + "@sentry/core": "10.43.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/babel-plugin-component-annotate": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.1.tgz", - "integrity": "sha512-aSIk0vgBqv7PhX6/Eov+vlI4puCE0bRXzUG5HdCsHBpAfeMkI8Hva6kSOusnzKqs8bf04hU7s3Sf0XxGTj/1AA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-5.1.1.tgz", + "integrity": "sha512-x2wEpBHwsTyTF2rWsLKJlzrRF1TTIGOfX+ngdE+Yd5DBkoS58HwQv824QOviPGQRla4/ypISqAXzjdDPL/zalg==", "license": "MIT", "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@sentry/browser": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.26.0.tgz", - "integrity": "sha512-uvV4hnkt8bh8yP0disJ0fszy8FdnkyGtzyIVKdeQZbNUefwbDhd3H0KJrAHhJ5ocULMH3B+dipdPmw2QXbEflg==", + "version": "10.43.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.43.0.tgz", + "integrity": "sha512-2V3I3sXi3SMeiZpKixd9ztokSgK27cmvsD9J5oyOyjhGLTW/6QKCwHbKnluMgQMXq20nixQk5zN4wRjRUma3sg==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.26.0", - "@sentry-internal/feedback": "10.26.0", - "@sentry-internal/replay": "10.26.0", - "@sentry-internal/replay-canvas": "10.26.0", - "@sentry/core": "10.26.0" + "@sentry-internal/browser-utils": "10.43.0", + "@sentry-internal/feedback": "10.43.0", + "@sentry-internal/replay": "10.43.0", + "@sentry-internal/replay-canvas": "10.43.0", + "@sentry/core": "10.43.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/cli": { - "version": "2.58.2", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.2.tgz", - "integrity": "sha512-U4u62V4vaTWF+o40Mih8aOpQKqKUbZQt9A3LorIJwaE3tO3XFLRI70eWtW2se1Qmy0RZ74zB14nYcFNFl2t4Rw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-3.3.3.tgz", + "integrity": "sha512-4CZtfgiOraX+BntMjYQhfLDArXwpqt3sEo5Zdj2pqWSZSd4yI3ncfQ21CsxLcI/sUQrjmD5Vzidu4/1OShyxtA==", "hasInstallScript": true, - "license": "BSD-3-Clause", + "license": "FSL-1.1-MIT", "dependencies": { - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.7", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", + "undici": "^6.22.0", "which": "^2.0.2" }, "bin": { "sentry-cli": "bin/sentry-cli" }, "engines": { - "node": ">= 10" + "node": ">= 18" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.58.2", - "@sentry/cli-linux-arm": "2.58.2", - "@sentry/cli-linux-arm64": "2.58.2", - "@sentry/cli-linux-i686": "2.58.2", - "@sentry/cli-linux-x64": "2.58.2", - "@sentry/cli-win32-arm64": "2.58.2", - "@sentry/cli-win32-i686": "2.58.2", - "@sentry/cli-win32-x64": "2.58.2" + "@sentry/cli-darwin": "3.3.3", + "@sentry/cli-linux-arm": "3.3.3", + "@sentry/cli-linux-arm64": "3.3.3", + "@sentry/cli-linux-i686": "3.3.3", + "@sentry/cli-linux-x64": "3.3.3", + "@sentry/cli-win32-arm64": "3.3.3", + "@sentry/cli-win32-i686": "3.3.3", + "@sentry/cli-win32-x64": "3.3.3" } }, "node_modules/@sentry/cli-darwin": { - "version": "2.58.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.2.tgz", - "integrity": "sha512-MArsb3zLhA2/cbd4rTm09SmTpnEuZCoZOpuZYkrpDw1qzBVJmRFA1W1hGAQ9puzBIk/ubY3EUhhzuU3zN2uD6w==", - "license": "BSD-3-Clause", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-3.3.3.tgz", + "integrity": "sha512-P8DoL79eX5fhKCfBHHl7xwwTShDPOb2drJC8lizZ3v1iS1JLPrNweM1KEzDefR30zH1wghbLSwsYv/svWdM3wA==", + "license": "FSL-1.1-MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/@sentry/cli-linux-arm": { - "version": "2.58.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.2.tgz", - "integrity": "sha512-HU9lTCzcHqCz/7Mt5n+cv+nFuJdc1hGD2h35Uo92GgxX3/IujNvOUfF+nMX9j6BXH6hUt73R5c0Ycq9+a3Parg==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-3.3.3.tgz", + "integrity": "sha512-a7o/huozveLIImXHe0HDwEMVhvDopOP2tLcopvV7sQsVE8f/QOShR5FudKjmiaZz2opdLzPJO9pv5WuF9jAZPg==", "cpu": [ "arm" ], - "license": "BSD-3-Clause", + "license": "FSL-1.1-MIT", "optional": true, "os": [ "linux", @@ -3484,17 +3407,17 @@ "android" ], "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/@sentry/cli-linux-arm64": { - "version": "2.58.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.2.tgz", - "integrity": "sha512-ay3OeObnbbPrt45cjeUyQjsx5ain1laj1tRszWj37NkKu55NZSp4QCg1gGBZ0gBGhckI9nInEsmKtix00alw2g==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.3.3.tgz", + "integrity": "sha512-9jaX9RGyTpjo9u2urNi5ciBDpRdTt107YJpFXev+BFHJ6Lwz/owgRuYzPRfAen8hKkOOFheZ3iy07kl576eZzw==", "cpu": [ "arm64" ], - "license": "BSD-3-Clause", + "license": "FSL-1.1-MIT", "optional": true, "os": [ "linux", @@ -3502,18 +3425,18 @@ "android" ], "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/@sentry/cli-linux-i686": { - "version": "2.58.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.2.tgz", - "integrity": "sha512-CN9p0nfDFsAT1tTGBbzOUGkIllwS3hygOUyTK7LIm9z+UHw5uNgNVqdM/3Vg+02ymjkjISNB3/+mqEM5osGXdA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-3.3.3.tgz", + "integrity": "sha512-VngQYzR2kDm2oojCuYF20ebLTK8HKvEwxe785J6gxob8Ef9JvZkERyUqENYppBa9aVgN0pandqPAqOECWykTMA==", "cpu": [ "x86", "ia32" ], - "license": "BSD-3-Clause", + "license": "FSL-1.1-MIT", "optional": true, "os": [ "linux", @@ -3521,17 +3444,17 @@ "android" ], "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/@sentry/cli-linux-x64": { - "version": "2.58.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.2.tgz", - "integrity": "sha512-oX/LLfvWaJO50oBVOn4ZvG2SDWPq0MN8SV9eg5tt2nviq+Ryltfr7Rtoo+HfV+eyOlx1/ZXhq9Wm7OT3cQuz+A==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-3.3.3.tgz", + "integrity": "sha512-rBxXQeIYGefUNI2cXHxEr0y3bhxDQjOD4G6j/gqLz/Dj+l8gJ/iKP64kTudnoViNIpn0pdYccG69th7zmzM/Fg==", "cpu": [ "x64" ], - "license": "BSD-3-Clause", + "license": "FSL-1.1-MIT", "optional": true, "os": [ "linux", @@ -3539,76 +3462,75 @@ "android" ], "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/@sentry/cli-win32-arm64": { - "version": "2.58.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.2.tgz", - "integrity": "sha512-+cl3x2HPVMpoSVGVM1IDWlAEREZrrVQj4xBb0TRKII7g3hUxRsAIcsrr7+tSkie++0FuH4go/b5fGAv51OEF3w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.3.3.tgz", + "integrity": "sha512-c52g+YS6BO0rzH8AEHqQPmpqZrw0GJjMWqy0tQ5jcqaGdaLVnxk0mMEubv8R6Dv5MR2LShoKjiNsaeVfrWIMUg==", "cpu": [ "arm64" ], - "license": "BSD-3-Clause", + "license": "FSL-1.1-MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/@sentry/cli-win32-i686": { - "version": "2.58.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.2.tgz", - "integrity": "sha512-omFVr0FhzJ8oTJSg1Kf+gjLgzpYklY0XPfLxZ5iiMiYUKwF5uo1RJRdkUOiEAv0IqpUKnmKcmVCLaDxsWclB7Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-3.3.3.tgz", + "integrity": "sha512-DygYzSY/+tS7oFj/mfeg/yzYxsQx3fO8cI+IWc2pns/at+JcJ9O5xyM/x/q55wOxpnwla7RL1D3rsqK2mqkYfg==", "cpu": [ "x86", "ia32" ], - "license": "BSD-3-Clause", + "license": "FSL-1.1-MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/@sentry/cli-win32-x64": { - "version": "2.58.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.2.tgz", - "integrity": "sha512-2NAFs9UxVbRztQbgJSP5i8TB9eJQ7xraciwj/93djrSMHSEbJ0vC47TME0iifgvhlHMs5vqETOKJtfbbpQAQFA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-3.3.3.tgz", + "integrity": "sha512-i0glPcHwkqbVA2Y+0Yz7CD/l8TSkfft1a+lTU9yk/+DDU8WGkyArEAxAji9bGo4p+k5HIFC8OC2MwpKdcdFM4Q==", "cpu": [ "x64" ], - "license": "BSD-3-Clause", + "license": "FSL-1.1-MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/@sentry/core": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.26.0.tgz", - "integrity": "sha512-TjDe5QI37SLuV0q3nMOH8JcPZhv2e85FALaQMIhRILH9Ce6G7xW5GSjmH91NUVq8yc3XtiqYlz/EenEZActc4Q==", + "version": "10.43.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.43.0.tgz", + "integrity": "sha512-l0SszQAPiQGWl/ferw8GP3ALyHXiGiRKJaOvNmhGO+PrTQyZTZ6OYyPnGijAFRg58dE1V3RCH/zw5d2xSUIiNg==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.26.0.tgz", - "integrity": "sha512-Qi0/FVXAalwQNr8zp0tocViH3+MRelW8ePqj3TdMzapkbXRuh07czdGgw8Zgobqcb7l4rRCRAUo2sl/H3KVkIw==", + "version": "10.43.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.43.0.tgz", + "integrity": "sha512-shvErEpJ41i0Q3lIZl0CDWYQ7m8yHLi7ECG0gFvN8zf8pEdl5grQIOoe3t/GIUzcpCcor16F148ATmKJJypc/Q==", "license": "MIT", "dependencies": { - "@sentry/browser": "10.26.0", - "@sentry/core": "10.26.0", - "hoist-non-react-statics": "^3.3.2" + "@sentry/browser": "10.43.0", + "@sentry/core": "10.43.0" }, "engines": { "node": ">=18" @@ -3618,19 +3540,22 @@ } }, "node_modules/@sentry/react-native": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-7.7.0.tgz", - "integrity": "sha512-D+gqiw88mOnouY+Pd8A3wcUDOilPOIcypBPw7WL9v+K1jM12Snf6sosEG4xgFFMXoK+GSsYAeC5MR0skD/b+Zg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-8.4.0.tgz", + "integrity": "sha512-B2aClcAwCR2BBovYX2ND9kOwOpo44gFu1kaQx0gqQtMVePh7/VNRDmF+XGM3CCg9GBH2C1OoGQRiDmyMO1eG8A==", "license": "MIT", "dependencies": { - "@sentry/babel-plugin-component-annotate": "4.6.1", - "@sentry/browser": "10.26.0", - "@sentry/cli": "2.58.2", - "@sentry/core": "10.26.0", - "@sentry/react": "10.26.0", - "@sentry/types": "10.26.0" + "@sentry/babel-plugin-component-annotate": "5.1.1", + "@sentry/browser": "10.43.0", + "@sentry/cli": "3.3.3", + "@sentry/core": "10.43.0", + "@sentry/react": "10.43.0", + "@sentry/types": "10.43.0" }, "bin": { + "sentry-eas-build-on-complete": "scripts/eas-build-hook.js", + "sentry-eas-build-on-error": "scripts/eas-build-hook.js", + "sentry-eas-build-on-success": "scripts/eas-build-hook.js", "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" }, "peerDependencies": { @@ -3645,21 +3570,21 @@ } }, "node_modules/@sentry/types": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-10.26.0.tgz", - "integrity": "sha512-mDpG7lnOJppbk9iKrnuvkuiCTbh3aBAlUK4NZxZNLOSI0SeefYXHRAcri89BqWZ/MT98sQLU+Hf+rlwrwq38/A==", + "version": "10.43.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-10.43.0.tgz", + "integrity": "sha512-AbKGWFGmDJkl0F7yvNTqZEovMJTAEdVbsZC/Zy6w2PFUk7pHUtIJQ5DXkBxJ9QVZhOjGmHQRLKvXYaeXmI/6PA==", "license": "MIT", "dependencies": { - "@sentry/core": "10.26.0" + "@sentry/core": "10.43.0" }, "engines": { "node": ">=18" } }, "node_modules/@shopify/flash-list": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-2.2.0.tgz", - "integrity": "sha512-mL61IofcfBNRZ/qazIf+pghGULkcZUQ7EZNldH1JBbIjtDb25ADSiQrt62ZTnRz0H5+bPFEZUmN9+WChHzX8pw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-2.3.0.tgz", + "integrity": "sha512-DR7VuN8KJHTYj9zv1/IhpqrMBMQyeeW/DCWCbVQAAkWhHrc6ylIbXOY+qK93CuHABV+dNHXK/3V6p4wCSW/+wA==", "license": "MIT", "peerDependencies": { "@babel/runtime": "*", @@ -3668,13 +3593,16 @@ } }, "node_modules/@shopify/react-native-skia": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@shopify/react-native-skia/-/react-native-skia-2.4.14.tgz", - "integrity": "sha512-zFxjAQbfrdOxoNJoaOCZQzZliuAWXjFkrNZv2PtofG2RAUPWIxWmk2J/oOROpTwXgkmh1JLvFp3uONccTXUthQ==", - "hasInstallScript": true, + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@shopify/react-native-skia/-/react-native-skia-2.5.1.tgz", + "integrity": "sha512-o1P9rFVEqWkEPLg3we6byTBK1ykKtx1GuVCUMSGfrlgKIIZ22WnQhLGSjL2cl2+a4P9li7zxXa5wlMVWTKdVCQ==", "license": "MIT", "dependencies": { "canvaskit-wasm": "0.40.0", + "react-native-skia-android": "144.3.0", + "react-native-skia-apple-ios": "144.3.0", + "react-native-skia-apple-macos": "144.3.0", + "react-native-skia-apple-tvos": "144.3.0", "react-reconciler": "0.31.0" }, "bin": { @@ -3695,9 +3623,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "license": "MIT" }, "node_modules/@sinonjs/commons": { @@ -3984,16 +3912,6 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4082,18 +4000,18 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==", + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz", - "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/prop-types": { @@ -4103,9 +4021,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4207,9 +4125,9 @@ } }, "node_modules/@vibrant/color": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vibrant/color/-/color-4.0.0.tgz", - "integrity": "sha512-S9ItdqS1135wTXoIIqAJu8df9dqlOo6Boc5Y4MGsBTu9UmUOvOwfj5b4Ga6S5yrLAKmKYIactkz7zYJdMddkig==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vibrant/color/-/color-4.0.4.tgz", + "integrity": "sha512-Fq2tAszz4QOPWfHZ+KuEAchXUD8i594BM2fOJt8dI/fvYbiVoBycBF/BlNH6F4IWBubxXoPqD4JmmAHvFYbNew==", "license": "MIT", "funding": { "type": "github", @@ -4217,16 +4135,16 @@ } }, "node_modules/@vibrant/core": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vibrant/core/-/core-4.0.0.tgz", - "integrity": "sha512-fqlVRUTDjEws9VNKvI3cDXM4wUT7fMFS+cVqEjJk3im+R5EvjJzPF6OAbNhfPzW04NvHNE555eY9FfhYuX3PRw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vibrant/core/-/core-4.0.4.tgz", + "integrity": "sha512-yZ0XSpW2biKyaJPpBC31AVYgn7NseKSO2q3KNMmDrkL2qC6TEWsBMnSQ28n0m///chZELXpQLx1CCOsWg5pj8w==", "license": "MIT", "dependencies": { - "@vibrant/color": "^4.0.0", - "@vibrant/generator": "^4.0.0", - "@vibrant/image": "^4.0.0", - "@vibrant/quantizer": "^4.0.0", - "@vibrant/worker": "^4.0.0" + "@vibrant/color": "^4.0.4", + "@vibrant/generator": "^4.0.4", + "@vibrant/image": "^4.0.4", + "@vibrant/quantizer": "^4.0.4", + "@vibrant/worker": "^4.0.4" }, "funding": { "type": "github", @@ -4234,13 +4152,13 @@ } }, "node_modules/@vibrant/generator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vibrant/generator/-/generator-4.0.0.tgz", - "integrity": "sha512-CqKAjmgHVDXJVo3Q5+9pUJOvksR7cN3bzx/6MbURYh7lA4rhsIewkUK155M6q0vfcUN3ETi/eTneCi0tLuM2Sg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vibrant/generator/-/generator-4.0.4.tgz", + "integrity": "sha512-rwq8PnlpKdch4YqaA1FAwdm71gKE2cMrUsbu72TqRFGa8rpP1roaZlQCVXIIwElXVc3r9axZyAcqyTLaMjhrTg==", "license": "MIT", "dependencies": { - "@vibrant/color": "^4.0.0", - "@vibrant/types": "^4.0.0" + "@vibrant/color": "^4.0.4", + "@vibrant/types": "^4.0.4" }, "funding": { "type": "github", @@ -4248,13 +4166,13 @@ } }, "node_modules/@vibrant/generator-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@vibrant/generator-default/-/generator-default-4.0.3.tgz", - "integrity": "sha512-HZlfp19sDokODEkZF4p70QceARHgjP3a1Dmxg+dlblYMJM98jPq+azA0fzqKNR7R17JJNHxexpJEepEsNlG0gw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vibrant/generator-default/-/generator-default-4.0.4.tgz", + "integrity": "sha512-QeVDeH2dz9lityvJCb84Ml4hlBTElwCpU7SVpiDFBh6gPoCLnzcb1H9G4NgG3hOlAPyrBM+Ivq1Pg+1lZj5Ywg==", "license": "MIT", "dependencies": { - "@vibrant/color": "^4.0.0", - "@vibrant/generator": "^4.0.0" + "@vibrant/color": "^4.0.4", + "@vibrant/generator": "^4.0.4" }, "funding": { "type": "github", @@ -4262,12 +4180,12 @@ } }, "node_modules/@vibrant/image": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vibrant/image/-/image-4.0.0.tgz", - "integrity": "sha512-Asv/7R/L701norosgvbjOVkodFiwcFihkXixA/gbAd6C+5GCts1Wm1NPk14FNKnM7eKkfAN+0wwPkdOH+PY/YA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vibrant/image/-/image-4.0.4.tgz", + "integrity": "sha512-NBIJj7umfDRVpFjJHQo1AFSCWCzQyjfil+Yxu7W62PEL72GPCif0CDiglPkvVF8QhDLmnx/x1k3LIBb9jWF2sw==", "license": "MIT", "dependencies": { - "@vibrant/color": "^4.0.0" + "@vibrant/color": "^4.0.4" }, "funding": { "type": "github", @@ -4275,12 +4193,12 @@ } }, "node_modules/@vibrant/image-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vibrant/image-browser/-/image-browser-4.0.0.tgz", - "integrity": "sha512-mXckzvJWiP575Y/wNtP87W/TPgyJoGlPBjW4E9YmNS6n4Jb6RqyHQA0ZVulqDslOxjSsihDzY7gpAORRclaoLg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vibrant/image-browser/-/image-browser-4.0.4.tgz", + "integrity": "sha512-7qVyAm+z9t98iwMDzUgGCwgRg0KBB5RXQFgiO2Um5Izd1wO7BKC0SHVEz2k7sRx3XNfBf+JExp8quPrvSz17gg==", "license": "MIT", "dependencies": { - "@vibrant/image": "^4.0.0" + "@vibrant/image": "^4.0.4" }, "funding": { "type": "github", @@ -4288,15 +4206,15 @@ } }, "node_modules/@vibrant/image-node": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vibrant/image-node/-/image-node-4.0.0.tgz", - "integrity": "sha512-m7yfnQtmo2y8z+tOjRFBx6q/qGnhl/ax2uCaj4TBkm4TtXfR4Dsn90wT6OWXmCFFzxIKHXKKEBShkxR+4RHseA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vibrant/image-node/-/image-node-4.0.4.tgz", + "integrity": "sha512-aG8Ukt9oTa6FWaAV5oBKsBetkKASWH31hZiFJ2R1291f3TZlphUyQTJz5TubucIRsCEl4dgG1xyxFPgse2IABA==", "license": "MIT", "dependencies": { "@jimp/custom": "^0.22.12", "@jimp/plugin-resize": "^0.22.12", "@jimp/types": "^0.22.12", - "@vibrant/image": "^4.0.0" + "@vibrant/image": "^4.0.4" }, "funding": { "type": "github", @@ -4304,14 +4222,14 @@ } }, "node_modules/@vibrant/quantizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vibrant/quantizer/-/quantizer-4.0.0.tgz", - "integrity": "sha512-YDGxmCv/RvHFtZghDlVRwH5GMxdGGozWS1JpUOUt73/F5zAKGiiier8F31K1npIXARn6/Gspvg/Rbg7qqyEr2A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vibrant/quantizer/-/quantizer-4.0.4.tgz", + "integrity": "sha512-722CooC2W4mlBiv+zyAsIrIvARnMCN/P2Muo8bnWd0SQlVWFtQnFxJWGOUPOPS4DGe3pGoqmNfvS0let4dICZQ==", "license": "MIT", "dependencies": { - "@vibrant/color": "^4.0.0", - "@vibrant/image": "^4.0.0", - "@vibrant/types": "^4.0.0" + "@vibrant/color": "^4.0.4", + "@vibrant/image": "^4.0.4", + "@vibrant/types": "^4.0.4" }, "funding": { "type": "github", @@ -4319,14 +4237,14 @@ } }, "node_modules/@vibrant/quantizer-mmcq": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vibrant/quantizer-mmcq/-/quantizer-mmcq-4.0.0.tgz", - "integrity": "sha512-TZqNiRoGGyCP8fH1XE6rvhFwLNv9D8MP1Xhz3K8tsuUweC6buWax3qLfrfEnkhtQnPJHaqvTfTOlIIXVMfRpow==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vibrant/quantizer-mmcq/-/quantizer-mmcq-4.0.4.tgz", + "integrity": "sha512-/1CNnM96J8K+OBCWNUzywo6VdnmdFJyiKO+ty/nkfe8H0NseOEHIL7PrVtWGgtsb0rh2uTAq2rjXv65TfgPy8g==", "license": "MIT", "dependencies": { - "@vibrant/color": "^4.0.0", - "@vibrant/image": "^4.0.0", - "@vibrant/quantizer": "^4.0.0" + "@vibrant/color": "^4.0.4", + "@vibrant/image": "^4.0.4", + "@vibrant/quantizer": "^4.0.4" }, "funding": { "type": "github", @@ -4334,9 +4252,9 @@ } }, "node_modules/@vibrant/types": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vibrant/types/-/types-4.0.0.tgz", - "integrity": "sha512-tA5TAbuROXcPkt+PWjmGfoaiEXyySVaNnCZovf6vXhCbMdrTTCQXvNCde2geiVl6YwtuU/Qrj9iZxS5jZ6yVIw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vibrant/types/-/types-4.0.4.tgz", + "integrity": "sha512-Qq3mVTJamn7yD4OBgBEUKaxfDlm3sxBK55N7dH3XzI9Ey7KR00R06uwtqOcEJMsziWTEXdYN3VUlYaj2Tkt7hw==", "license": "MIT", "funding": { "type": "github", @@ -4344,12 +4262,12 @@ } }, "node_modules/@vibrant/worker": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@vibrant/worker/-/worker-4.0.0.tgz", - "integrity": "sha512-nSaZZwWQKOgN/nPYUAIRF0/uoa7KpK91A+gjLmZZDgfN1enqxaiihmn+75ayNadW0c6cxAEpEFEHTONR5u9tMw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vibrant/worker/-/worker-4.0.4.tgz", + "integrity": "sha512-Q/R6PYhSMWCXEk/IcXbWIzIu7Z4b58ABkGvcdF8Y+q/7g+KnpxKW5x/jfQ/6ciyYSby13wZZoEdNr3QQVgsdBQ==", "license": "MIT", "dependencies": { - "@vibrant/types": "^4.0.0" + "@vibrant/types": "^4.0.4" }, "funding": { "type": "github", @@ -4444,9 +4362,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -4649,13 +4567,13 @@ "optional": true }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -4731,13 +4649,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.16.tgz", + "integrity": "sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.7", "semver": "^6.3.1" }, "peerDependencies": { @@ -4758,12 +4676,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.7.tgz", + "integrity": "sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" + "@babel/helper-define-polyfill-provider": "^0.6.7" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -4836,9 +4754,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "54.0.8", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.8.tgz", - "integrity": "sha512-3ZJ4Q7uQpm8IR/C9xbKhE/IUjGpLm+OIjF8YCedLgqoe/wN1Ns2wLT7HwG6ZXXb6/rzN8IMCiKFQ2F93qlN6GA==", + "version": "54.0.10", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.10.tgz", + "integrity": "sha512-wTt7POavLFypLcPW/uC5v8y+mtQKDJiyGLzYCjqr9tx0Qc3vCXcDKk1iCFIj/++Iy5CWhhTflEa7VvVPNWeCfw==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -4927,12 +4845,15 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.7", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz", - "integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==", + "version": "2.10.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", + "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bcrypt-pbkdf": { @@ -5197,9 +5118,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001760", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", - "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "version": "1.0.30001778", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", "funding": [ { "type": "opencollective", @@ -5573,12 +5494,12 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", - "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.0" + "browserslist": "^4.28.1" }, "funding": { "type": "opencollective", @@ -5647,15 +5568,6 @@ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "license": "MIT" }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -6048,9 +5960,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -6255,9 +6167,9 @@ "license": "MIT" }, "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/events": { @@ -6269,40 +6181,34 @@ "node": ">=0.8.x" } }, - "node_modules/exec-async": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz", - "integrity": "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==", - "license": "MIT" - }, "node_modules/exif-parser": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" }, "node_modules/expo": { - "version": "54.0.29", - "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.29.tgz", - "integrity": "sha512-9C90gyOzV83y2S3XzCbRDCuKYNaiyCzuP9ketv46acHCEZn+QTamPK/DobdghoSiofCmlfoaiD6/SzfxDiHMnw==", + "version": "54.0.33", + "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz", + "integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "54.0.19", - "@expo/config": "~12.0.12", + "@expo/cli": "54.0.23", + "@expo/config": "~12.0.13", "@expo/config-plugins": "~54.0.4", "@expo/devtools": "0.1.8", "@expo/fingerprint": "0.15.4", - "@expo/metro": "~54.1.0", - "@expo/metro-config": "54.0.11", + "@expo/metro": "~54.2.0", + "@expo/metro-config": "54.0.14", "@expo/vector-icons": "^15.0.3", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~54.0.8", - "expo-asset": "~12.0.11", - "expo-constants": "~18.0.12", + "babel-preset-expo": "~54.0.10", + "expo-asset": "~12.0.12", + "expo-constants": "~18.0.13", "expo-file-system": "~19.0.21", - "expo-font": "~14.0.10", + "expo-font": "~14.0.11", "expo-keep-awake": "~15.0.8", - "expo-modules-autolinking": "3.0.23", + "expo-modules-autolinking": "3.0.24", "expo-modules-core": "3.0.29", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", @@ -6342,13 +6248,13 @@ } }, "node_modules/expo-asset": { - "version": "12.0.11", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.11.tgz", - "integrity": "sha512-pnK/gQ5iritDPBeK54BV35ZpG7yeW5DtgGvJHruIXkyDT9BCoQq3i0AAxfcWG/e4eiRmTzAt5kNVYFJi48uo+A==", + "version": "12.0.12", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz", + "integrity": "sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.8.8", - "expo-constants": "~18.0.11" + "expo-constants": "~18.0.12" }, "peerDependencies": { "expo": "*", @@ -6407,12 +6313,12 @@ } }, "node_modules/expo-constants": { - "version": "18.0.12", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.12.tgz", - "integrity": "sha512-WzcKYMVNRRu4NcSzfIVRD5aUQFnSpTZgXFrlWmm19xJoDa4S3/PQNi6PNTBRc49xz9h8FT7HMxRKaC8lr0gflA==", + "version": "18.0.13", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz", + "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==", "license": "MIT", "dependencies": { - "@expo/config": "~12.0.12", + "@expo/config": "~12.0.13", "@expo/env": "~2.0.8" }, "peerDependencies": { @@ -6420,6 +6326,19 @@ "react-native": "*" } }, + "node_modules/expo-constants/node_modules/@expo/env": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.11.tgz", + "integrity": "sha512-xV+ps6YCW7XIPVUwFVCRN2nox09dnRwy8uIjwHWTODu0zFw4kp4omnVkl0OOjuu2XOe7tdgAHxikrkJt9xB/7Q==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "getenv": "^2.0.0" + } + }, "node_modules/expo-crypto": { "version": "15.0.8", "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.8.tgz", @@ -6521,9 +6440,9 @@ } }, "node_modules/expo-font": { - "version": "14.0.10", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.10.tgz", - "integrity": "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q==", + "version": "14.0.11", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz", + "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==", "license": "MIT", "dependencies": { "fontfaceobserver": "^2.1.0" @@ -6535,9 +6454,9 @@ } }, "node_modules/expo-glass-effect": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/expo-glass-effect/-/expo-glass-effect-0.1.8.tgz", - "integrity": "sha512-9Cp17ax0Fpugue8+Bd7Ndl/dSAvGmt4bQ5mQLw9zc1A2lctUse3cEg9nI7TnDJiwKf+A/VAPN6+3K12JVMYgZg==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/expo-glass-effect/-/expo-glass-effect-0.1.9.tgz", + "integrity": "sha512-mDnoQKrvdkKEk1kUDgCKRdw1fHsouG25BclBico9lZSrLb7HpfGla3jnlz9rYGqXIiO8i9BTxpsmFhDnc1/4hg==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -6591,12 +6510,12 @@ } }, "node_modules/expo-linking": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.10.tgz", - "integrity": "sha512-0EKtn4Sk6OYmb/5ZqK8riO0k1Ic+wyT3xExbmDvUYhT7p/cKqlVUExMuOIAt3Cx3KUUU1WCgGmdd493W/D5XjA==", + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.11.tgz", + "integrity": "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA==", "license": "MIT", "dependencies": { - "expo-constants": "~18.0.11", + "expo-constants": "~18.0.12", "invariant": "^2.2.4" }, "peerDependencies": { @@ -6642,9 +6561,9 @@ } }, "node_modules/expo-modules-autolinking": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.23.tgz", - "integrity": "sha512-YZnaE0G+52xftjH5nsIRaWsoVBY38SQCECclpdgLisdbRY/6Mzo7ndokjauOv3mpFmzMZACHyJNu1YSAffQwTg==", + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz", + "integrity": "sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", @@ -6687,9 +6606,9 @@ } }, "node_modules/expo-notifications": { - "version": "0.32.15", - "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.15.tgz", - "integrity": "sha512-gnJcauheC2S0Wl0RuJaFkaBRVzCG011j5hlG0TEbsuOCPBuB/F30YEk8yurK8Psv+zHkVfeiJ5AC+nL0LWk0WA==", + "version": "0.32.16", + "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.16.tgz", + "integrity": "sha512-QQD/UA6v7LgvwIJ+tS7tSvqJZkdp0nCSj9MxsDk/jU1GttYdK49/5L2LvE/4U0H7sNBz1NZAyhDZozg8xgBLXw==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.8.8", @@ -6698,7 +6617,7 @@ "assert": "^2.0.0", "badgin": "^1.1.5", "expo-application": "~7.0.8", - "expo-constants": "~18.0.12" + "expo-constants": "~18.0.13" }, "peerDependencies": { "expo": "*", @@ -6787,12 +6706,12 @@ } }, "node_modules/expo-updates": { - "version": "29.0.15", - "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-29.0.15.tgz", - "integrity": "sha512-6Qj+g56nnCksKKnEPQFm19dfWvYB5EggQNN3SaLbIj4LI40k/pjQwqYStEuwTU+Ow+PG0AqxIhQ3NvgVPEzLvg==", + "version": "29.0.16", + "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-29.0.16.tgz", + "integrity": "sha512-E9/fxRz/Eurtc7hxeI/6ZPyHH3To9Xoccm1kXoICZTRojmuTo+dx0Xv53UHyHn4G5zGMezyaKF2Qtj3AKcT93w==", "license": "MIT", "dependencies": { - "@expo/code-signing-certificates": "0.0.5", + "@expo/code-signing-certificates": "^0.0.6", "@expo/plist": "^0.4.8", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", @@ -7059,19 +6978,16 @@ "license": "MIT" }, "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", + "locate-path": "^5.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/find-yarn-workspace-root": { @@ -7322,49 +7238,58 @@ } }, "node_modules/glob": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", - "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", - "license": "MIT", - "dependencies": { - "ini": "^1.3.4" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -7409,9 +7334,9 @@ } }, "node_modules/har-validator/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "optional": true, "dependencies": { @@ -7589,25 +7514,29 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -7629,31 +7558,6 @@ "npm": ">=1.3.7" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/hyphenate-style-name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", @@ -7661,9 +7565,9 @@ "license": "BSD-3-Clause" }, "node_modules/i18next": { - "version": "25.7.3", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz", - "integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==", + "version": "25.8.18", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.18.tgz", + "integrity": "sha512-lzY5X83BiL5AP77+9DydbrqkQHFN9hUzWGjqjLpPcp5ZOzuu1aSoKaU3xbBLSjWx9dAzW431y+d+aogxOZaKRA==", "funding": [ { "type": "individual", @@ -7680,7 +7584,7 @@ ], "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4" + "@babel/runtime": "^7.28.6" }, "peerDependencies": { "typescript": "^5" @@ -8295,20 +8199,6 @@ "xml-name-validator": ">= 2.0.1 < 3.0.0" } }, - "node_modules/jsdom/node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -8496,9 +8386,9 @@ "license": "MIT" }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -8511,23 +8401,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -8545,9 +8435,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -8565,9 +8455,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -8585,9 +8475,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -8605,9 +8495,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -8625,9 +8515,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], @@ -8645,9 +8535,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], @@ -8665,9 +8555,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -8685,9 +8575,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -8705,9 +8595,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -8725,9 +8615,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -8760,24 +8650,21 @@ } }, "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "p-locate": "^4.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.debounce": { @@ -8888,9 +8775,9 @@ } }, "node_modules/lottie-react-native": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/lottie-react-native/-/lottie-react-native-7.3.4.tgz", - "integrity": "sha512-XUh7eGFb7ID8JRdU6U4N4cYQeYmjtdQRvd8ZXJ6xrdSsn5gZD0c79ITOREPcwJg4YupBFHgyV1GXdAHQP+KYUQ==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/lottie-react-native/-/lottie-react-native-7.3.6.tgz", + "integrity": "sha512-TevFHRvFURh6GlaqLKrSNXuKAxvBvFCiXfS7FXQI1K/ikOStgAwWLFPGjW0i1qB2/VzPACKmRs+535VjHUZZZQ==", "license": "Apache-2.0", "peerDependencies": { "@lottiefiles/dotlottie-react": "^0.13.5", @@ -9006,9 +8893,9 @@ "license": "MIT" }, "node_modules/metro": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.2.tgz", - "integrity": "sha512-HQgs9H1FyVbRptNSMy/ImchTTE5vS2MSqLoOo7hbDoBq6hPPZokwJvBMwrYSxdjQZmLXz2JFZtdvS+ZfgTc9yw==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz", + "integrity": "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", @@ -9032,18 +8919,18 @@ "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.83.2", - "metro-cache": "0.83.2", - "metro-cache-key": "0.83.2", - "metro-config": "0.83.2", - "metro-core": "0.83.2", - "metro-file-map": "0.83.2", - "metro-resolver": "0.83.2", - "metro-runtime": "0.83.2", - "metro-source-map": "0.83.2", - "metro-symbolicate": "0.83.2", - "metro-transform-plugins": "0.83.2", - "metro-transform-worker": "0.83.2", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-config": "0.83.3", + "metro-core": "0.83.3", + "metro-file-map": "0.83.3", + "metro-resolver": "0.83.3", + "metro-runtime": "0.83.3", + "metro-source-map": "0.83.3", + "metro-symbolicate": "0.83.3", + "metro-transform-plugins": "0.83.3", + "metro-transform-worker": "0.83.3", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", @@ -9060,9 +8947,9 @@ } }, "node_modules/metro-babel-transformer": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.2.tgz", - "integrity": "sha512-rirY1QMFlA1uxH3ZiNauBninwTioOgwChnRdDcbB4tgRZ+bGX9DiXoh9QdpppiaVKXdJsII932OwWXGGV4+Nlw==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.3.tgz", + "integrity": "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -9090,24 +8977,24 @@ } }, "node_modules/metro-cache": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.2.tgz", - "integrity": "sha512-Z43IodutUZeIS7OTH+yQFjc59QlFJ6s5OvM8p2AP9alr0+F8UKr8ADzFzoGKoHefZSKGa4bJx7MZJLF6GwPDHQ==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz", + "integrity": "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==", "license": "MIT", "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", - "metro-core": "0.83.2" + "metro-core": "0.83.3" }, "engines": { "node": ">=20.19.4" } }, "node_modules/metro-cache-key": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.2.tgz", - "integrity": "sha512-3EMG/GkGKYoTaf5RqguGLSWRqGTwO7NQ0qXKmNBjr0y6qD9s3VBXYlwB+MszGtmOKsqE9q3FPrE5Nd9Ipv7rZw==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.3.tgz", + "integrity": "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -9130,18 +9017,18 @@ } }, "node_modules/metro-config": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.2.tgz", - "integrity": "sha512-1FjCcdBe3e3D08gSSiU9u3Vtxd7alGH3x/DNFqWDFf5NouX4kLgbVloDDClr1UrLz62c0fHh2Vfr9ecmrOZp+g==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.3.tgz", + "integrity": "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==", "license": "MIT", "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", - "metro": "0.83.2", - "metro-cache": "0.83.2", - "metro-core": "0.83.2", - "metro-runtime": "0.83.2", + "metro": "0.83.3", + "metro-cache": "0.83.3", + "metro-core": "0.83.3", + "metro-runtime": "0.83.3", "yaml": "^2.6.1" }, "engines": { @@ -9149,23 +9036,23 @@ } }, "node_modules/metro-core": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.2.tgz", - "integrity": "sha512-8DRb0O82Br0IW77cNgKMLYWUkx48lWxUkvNUxVISyMkcNwE/9ywf1MYQUE88HaKwSrqne6kFgCSA/UWZoUT0Iw==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.3.tgz", + "integrity": "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", - "metro-resolver": "0.83.2" + "metro-resolver": "0.83.3" }, "engines": { "node": ">=20.19.4" } }, "node_modules/metro-file-map": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.2.tgz", - "integrity": "sha512-cMSWnEqZrp/dzZIEd7DEDdk72PXz6w5NOKriJoDN9p1TDQ5nAYrY2lHi8d6mwbcGLoSlWmpPyny9HZYFfPWcGQ==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.3.tgz", + "integrity": "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -9183,9 +9070,9 @@ } }, "node_modules/metro-minify-terser": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.2.tgz", - "integrity": "sha512-zvIxnh7U0JQ7vT4quasKsijId3dOAWgq+ip2jF/8TMrPUqQabGrs04L2dd0haQJ+PA+d4VvK/bPOY8X/vL2PWw==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.3.tgz", + "integrity": "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", @@ -9196,9 +9083,9 @@ } }, "node_modules/metro-resolver": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.2.tgz", - "integrity": "sha512-Yf5mjyuiRE/Y+KvqfsZxrbHDA15NZxyfg8pIk0qg47LfAJhpMVEX+36e6ZRBq7KVBqy6VDX5Sq55iHGM4xSm7Q==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.3.tgz", + "integrity": "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -9208,9 +9095,9 @@ } }, "node_modules/metro-runtime": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.2.tgz", - "integrity": "sha512-nnsPtgRvFbNKwemqs0FuyFDzXLl+ezuFsUXDbX8o0SXOfsOPijqiQrf3kuafO1Zx1aUWf4NOrKJMAQP5EEHg9A==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.3.tgz", + "integrity": "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", @@ -9221,9 +9108,9 @@ } }, "node_modules/metro-source-map": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.2.tgz", - "integrity": "sha512-5FL/6BSQvshIKjXOennt9upFngq2lFvDakZn5LfauIVq8+L4sxXewIlSTcxAtzbtjAIaXeOSVMtCJ5DdfCt9AA==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.3.tgz", + "integrity": "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.3", @@ -9231,9 +9118,9 @@ "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-symbolicate": "0.83.2", + "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", - "ob1": "0.83.2", + "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" }, @@ -9251,14 +9138,14 @@ } }, "node_modules/metro-symbolicate": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.2.tgz", - "integrity": "sha512-KoU9BLwxxED6n33KYuQQuc5bXkIxF3fSwlc3ouxrrdLWwhu64muYZNQrukkWzhVKRNFIXW7X2iM8JXpi2heIPw==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz", + "integrity": "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-source-map": "0.83.2", + "metro-source-map": "0.83.3", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" @@ -9280,9 +9167,9 @@ } }, "node_modules/metro-transform-plugins": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.2.tgz", - "integrity": "sha512-5WlW25WKPkiJk2yA9d8bMuZrgW7vfA4f4MBb9ZeHbTB3eIAoNN8vS8NENgG/X/90vpTB06X66OBvxhT3nHwP6A==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz", + "integrity": "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -9297,9 +9184,9 @@ } }, "node_modules/metro-transform-worker": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.2.tgz", - "integrity": "sha512-G5DsIg+cMZ2KNfrdLnWMvtppb3+Rp1GMyj7Bvd9GgYc/8gRmvq1XVEF9XuO87Shhb03kFhGqMTgZerz3hZ1v4Q==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.3.tgz", + "integrity": "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -9307,13 +9194,13 @@ "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", - "metro": "0.83.2", - "metro-babel-transformer": "0.83.2", - "metro-cache": "0.83.2", - "metro-cache-key": "0.83.2", - "metro-minify-terser": "0.83.2", - "metro-source-map": "0.83.2", - "metro-transform-plugins": "0.83.2", + "metro": "0.83.3", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-minify-terser": "0.83.3", + "metro-source-map": "0.83.3", + "metro-transform-plugins": "0.83.3", "nullthrows": "^1.1.1" }, "engines": { @@ -9448,12 +9335,12 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -9472,10 +9359,10 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -9601,23 +9488,23 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "license": "MIT" }, "node_modules/node-vibrant": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/node-vibrant/-/node-vibrant-4.0.3.tgz", - "integrity": "sha512-kzoIuJK90BH/k65Avt077JCX4Nhqz1LNc8cIOm2rnYEvFdJIYd8b3SQwU1MTpzcHtr8z8jxkl1qdaCfbP3olFg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/node-vibrant/-/node-vibrant-4.0.4.tgz", + "integrity": "sha512-hA/pUXBE9TJ41G9FlTkzeqD5JdxgvvPGYZb/HNpdkaxxXUEnP36imSolZ644JuPun+lTd+FpWWtBpTYdp2noQA==", "license": "MIT", "dependencies": { "@types/node": "^18.15.3", - "@vibrant/core": "^4.0.0", - "@vibrant/generator-default": "^4.0.3", - "@vibrant/image-browser": "^4.0.0", - "@vibrant/image-node": "^4.0.0", - "@vibrant/quantizer-mmcq": "^4.0.0" + "@vibrant/core": "^4.0.4", + "@vibrant/generator-default": "^4.0.4", + "@vibrant/image-browser": "^4.0.4", + "@vibrant/image-node": "^4.0.4", + "@vibrant/quantizer-mmcq": "^4.0.4" }, "funding": { "type": "github", @@ -9664,9 +9551,9 @@ } }, "node_modules/npm-package-arg/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9708,9 +9595,9 @@ } }, "node_modules/ob1": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.2.tgz", - "integrity": "sha512-XlK3w4M+dwd1g1gvHzVbxiXEbUllRONEgcF2uEO0zm4nxa0eKlh41c6N65q1xbiDOeKKda1tvNOAD33fNjyvCg==", + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.3.tgz", + "integrity": "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -9959,15 +9846,27 @@ } }, "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" + "p-limit": "^2.2.0" }, "engines": { - "node": ">=10" + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10078,9 +9977,9 @@ } }, "node_modules/patch-package/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -10141,25 +10040,25 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -10301,12 +10200,12 @@ "license": "MIT" }, "node_modules/posthog-react-native": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/posthog-react-native/-/posthog-react-native-4.14.3.tgz", - "integrity": "sha512-oooOqCcWSRmychTrU5CS6lIZmIkHmk7cIw3py5G6ZRELkWN5qgGc0efflb3FENg9FLOAiwXcxMDzMSehSEKtuw==", + "version": "4.37.2", + "resolved": "https://registry.npmjs.org/posthog-react-native/-/posthog-react-native-4.37.2.tgz", + "integrity": "sha512-j7sYhA3ppqyyVAenjcI/Zwud9IBK7qymZ6WZvkXz4x3M+CHx9NyXBUyXWoOiQWZrrfz3FBGMky2ij8H6+vShaA==", "license": "MIT", "dependencies": { - "@posthog/core": "1.7.1" + "@posthog/core": "1.23.3" }, "peerDependencies": { "@react-native-async-storage/async-storage": ">=1.0.0", @@ -10315,7 +10214,7 @@ "expo-device": ">= 4.0.0", "expo-file-system": ">= 13.0.0", "expo-localization": ">= 11.0.0", - "posthog-react-native-session-replay": ">= 1.2.0", + "posthog-react-native-session-replay": ">= 1.5.0", "react-native-device-info": ">= 10.0.0", "react-native-localize": ">= 3.0.0", "react-native-navigation": ">= 6.0.0", @@ -10520,9 +10419,9 @@ } }, "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", "license": "BSD-3-Clause", "optional": true, "engines": { @@ -10645,9 +10544,9 @@ } }, "node_modules/react-i18next": { - "version": "16.5.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.1.tgz", - "integrity": "sha512-Hks6UIRZWW4c+qDAnx1csVsCGYeIR4MoBGQgJ+NUoNnO6qLxXuf8zu0xdcinyXUORgGzCdRsexxO1Xzv3sTdnw==", + "version": "16.5.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.8.tgz", + "integrity": "sha512-2ABeHHlakxVY+LSirD+OiERxFL6+zip0PaHo979bgwzeHg27Sqc82xxXWIrSFmfWX0ZkrvXMHwhsi/NGUf5VQg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -10672,9 +10571,9 @@ } }, "node_modules/react-is": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", - "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", "license": "MIT" }, "node_modules/react-native": { @@ -10735,9 +10634,9 @@ } }, "node_modules/react-native-boost": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/react-native-boost/-/react-native-boost-0.6.2.tgz", - "integrity": "sha512-6w9PdGvFzyI1dyN516+mLfFF5vETPsjoc26rUFlzWav7PNbC7WV0KyfTBr0q/cDjZkWLMleWQZkGTqSQ1H4PHg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/react-native-boost/-/react-native-boost-1.0.0.tgz", + "integrity": "sha512-7swm2Au61jzAqS8DTV0whvRX3hdesvWPBm3iZv/o6ogCuM6Vk4wtauIL06jyYta81kcyDKV8q5mCEbI6OL5JXA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.0", @@ -10747,19 +10646,40 @@ }, "peerDependencies": { "react": "*", - "react-native": "*" + "react-native": ">=0.83.0" + } + }, + "node_modules/react-native-boost/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/react-native-boost/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/react-native-boost/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -10796,9 +10716,9 @@ } }, "node_modules/react-native-gesture-handler": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.29.1.tgz", - "integrity": "sha512-du3qmv0e3Sm7qsd9SfmHps+AggLiylcBBQ8ztz7WUtd8ZjKs5V3kekAbi9R2W9bRLSg47Ntp4GGMYZOhikQdZA==", + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz", + "integrity": "sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA==", "license": "MIT", "dependencies": { "@egjs/hammerjs": "^2.0.17", @@ -10833,9 +10753,9 @@ } }, "node_modules/react-native-image-colors": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/react-native-image-colors/-/react-native-image-colors-2.5.1.tgz", - "integrity": "sha512-7+M1pu9Q1TDEGSbXfSwFIFUoGW1Ffmwfjbx2QQM895C2gvOzUsdwSS1ae856l6vvj7UWFbGZr1LpQi0VK6Xl4w==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-native-image-colors/-/react-native-image-colors-2.6.0.tgz", + "integrity": "sha512-MbBPmRpp2yy8h5W7KUreByP96pey0J9habHaRSN/67O0hlR/5Izpt370BNHQVQogfHrRXfV4d8n6ZLn/2ga7Bg==", "license": "MIT", "dependencies": { "node-vibrant": "^4.0.3" @@ -10856,9 +10776,9 @@ } }, "node_modules/react-native-is-edge-to-edge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", - "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz", + "integrity": "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==", "license": "MIT", "peerDependencies": { "react": "*", @@ -10882,9 +10802,9 @@ } }, "node_modules/react-native-mmkv": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.1.0.tgz", - "integrity": "sha512-ia76WnU6dkLZxFkSSflxqFgHT2pIaML763aucEu7nMglF41oEWTdTtBu0o8a1cxbhZOaONk6KF8RQp5fLvPitA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.2.0.tgz", + "integrity": "sha512-LB1GeqkSJtu33li0nzgvo4W2RbsGfObXgQG2LBmbxfQP3sX+Q13OANoeKnJ1vwU8v4xNn4jWrA4vl3GuOt1/Rw==", "license": "MIT", "peerDependencies": { "react": "*", @@ -10893,10 +10813,9 @@ } }, "node_modules/react-native-nitro-modules": { - "version": "0.31.10", - "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.31.10.tgz", - "integrity": "sha512-hcvjTu9YJE9fMmnAUvhG8CxvYLpOuMQ/2eyi/S6GyrecezF6Rmk/uRQEL6v09BRFWA/xRVZNQVulQPS+2HS3mQ==", - "hasInstallScript": true, + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.35.0.tgz", + "integrity": "sha512-Eho1yEcLbsteGpBFn2XZOp5FIptnEciWzuYBW49S0jo41Un2LeyesIO/MqYLY/c5o7D9Fw9th4pxGtV7OAb0+g==", "license": "MIT", "peerDependencies": { "react": "*", @@ -10904,9 +10823,9 @@ } }, "node_modules/react-native-paper": { - "version": "5.14.5", - "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.14.5.tgz", - "integrity": "sha512-eaIH5bUQjJ/mYm4AkI6caaiyc7BcHDwX6CqNDi6RIxfxfWxROsHpll1oBuwn/cFvknvA8uEAkqLk/vzVihI3AQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.15.0.tgz", + "integrity": "sha512-I/1CQLfW9VM0Oo5I5dQI/hjgf1I6q2S1wwgzAdsv6whAQ3zO97GWHwtgNh9se9j8zBOJ86afPTQKxxUL0IJd9A==", "license": "MIT", "workspaces": [ "example", @@ -10959,9 +10878,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.2.0.tgz", - "integrity": "sha512-frhu5b8/m/VvaMWz48V8RxcsXnE3hrlErQ5chr21MzAeDCpY4X14sQjvm+jvu3aOI+7Cz2atdRpyhhIuqxVaXg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.2.2.tgz", + "integrity": "sha512-o3kKvdD8cVlg12Z4u3jv0MFAt53QV4k7gD9OLwQqU8eZLyd8QvaOjVZIghMZhC2pjP93uUU44PlO5JgF8S4Vxw==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "1.2.1", @@ -10985,6 +10904,16 @@ "react-native-reanimated": ">=3.0.0" } }, + "node_modules/react-native-reanimated/node_modules/react-native-is-edge-to-edge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", + "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-reanimated/node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -10998,9 +10927,9 @@ } }, "node_modules/react-native-safe-area-context": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", - "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz", + "integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==", "license": "MIT", "peerDependencies": { "react": "*", @@ -11008,9 +10937,9 @@ } }, "node_modules/react-native-screens": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.18.0.tgz", - "integrity": "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.24.0.tgz", + "integrity": "sha512-SyoiGaDofiyGPFrUkn1oGsAzkRuX1JUvTD9YQQK3G1JGQ5VWkvHgYSsc1K9OrLsDQxN7NmV71O0sHCAh8cBetA==", "license": "MIT", "dependencies": { "react-freeze": "^1.0.0", @@ -11021,10 +10950,34 @@ "react-native": "*" } }, + "node_modules/react-native-skia-android": { + "version": "144.3.0", + "resolved": "https://registry.npmjs.org/react-native-skia-android/-/react-native-skia-android-144.3.0.tgz", + "integrity": "sha512-0XsW8JQAgNBNvCwp/ypOKDF5CjKOlCTBfj43gYXK/clystG7e4y3XF/4v0vw9Slekj9BSQruHqGYpfs5hWgO/g==", + "license": "MIT" + }, + "node_modules/react-native-skia-apple-ios": { + "version": "144.3.0", + "resolved": "https://registry.npmjs.org/react-native-skia-apple-ios/-/react-native-skia-apple-ios-144.3.0.tgz", + "integrity": "sha512-gY8M6IiYhQR9J1xHvAbH4M5pcgS3wsBal/jETcwb7LHaPuxiRkig3Y3Bcl4yK5JgHomcrl18oWVj3xh3UqR2cg==", + "license": "MIT" + }, + "node_modules/react-native-skia-apple-macos": { + "version": "144.3.0", + "resolved": "https://registry.npmjs.org/react-native-skia-apple-macos/-/react-native-skia-apple-macos-144.3.0.tgz", + "integrity": "sha512-+2kYg+kfVRJX/FAsg+jIcvDQHTG9DzkI+b9GG4XdqHG/ixR4yd5opZ95l9rjLJuRGhnE+AB6StAz5zhjWFhHuw==", + "license": "MIT" + }, + "node_modules/react-native-skia-apple-tvos": { + "version": "144.3.0", + "resolved": "https://registry.npmjs.org/react-native-skia-apple-tvos/-/react-native-skia-apple-tvos-144.3.0.tgz", + "integrity": "sha512-Xq4N1xlIzZQgx/6LfkHbSCn5poo0O1D/5LjKmQvfVOrUkOe2Det4OdXrmbsVLFaeh7soB6Bn290s31agD011tw==", + "license": "MIT" + }, "node_modules/react-native-svg": { - "version": "15.15.1", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.1.tgz", - "integrity": "sha512-ZUD1xwc3Hwo4cOmOLumjJVoc7lEf9oQFlHnLmgccLC19fNm6LVEdtB+Cnip6gEi0PG3wfvVzskViEtrySQP8Fw==", + "version": "15.15.3", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.3.tgz", + "integrity": "sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==", "license": "MIT", "dependencies": { "css-select": "^5.1.0", @@ -11037,9 +10990,9 @@ } }, "node_modules/react-native-svg-transformer": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/react-native-svg-transformer/-/react-native-svg-transformer-1.5.2.tgz", - "integrity": "sha512-eW4hOtrd30s4SRdN4X1XYxTCu1czsxDGQKmfQ3RFbZMN5yw4ZmiKGGr+lXbQW4uDGZvSoGd9FHL1f+rgGoKg8Q==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/react-native-svg-transformer/-/react-native-svg-transformer-1.5.3.tgz", + "integrity": "sha512-M4uFg5pUt35OMgjD4rWWbwd6PmxV96W7r/gQTTa+iZA5B+jO6aURhzAZGLHSrg1Kb91cKG0Rildy9q1WJvYstg==", "dev": true, "license": "MIT", "dependencies": { @@ -11291,9 +11244,9 @@ } }, "node_modules/react-native-worklets": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.7.1.tgz", - "integrity": "sha512-KNsvR48ULg73QhTlmwPbdJLPsWcyBotrGPsrDRDswb5FYpQaJEThUKc2ncXE4UM5dn/ewLoQHjSjLaKUVPxPhA==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.7.4.tgz", + "integrity": "sha512-NYOdM1MwBb3n+AtMqy1tFy3Mn8DliQtd8sbzAVRf9Gc+uvQ0zRfxN7dS8ZzoyX7t6cyQL5THuGhlnX+iFlQTag==", "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "7.27.1", @@ -11314,6 +11267,57 @@ "react-native": "*" } }, + "node_modules/react-native-worklets/node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/react-native-worklets/node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/react-native-worklets/node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/react-native-worklets/node_modules/@babel/plugin-transform-optional-chaining": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", @@ -11434,7 +11438,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -11452,9 +11456,9 @@ } }, "node_modules/react-native/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -11464,9 +11468,9 @@ } }, "node_modules/react-native/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11713,20 +11717,6 @@ "node": ">= 0.12" } }, - "node_modules/request/node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -11796,22 +11786,10 @@ "node": ">=8" } }, - "node_modules/resolve-global": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", - "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", - "license": "MIT", - "dependencies": { - "global-dirs": "^0.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/resolve-workspace-root": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.0.tgz", - "integrity": "sha512-IsaBUZETJD5WsI11Wt8PKHwaIe45or6pwNc8yflvLJ4DWtImK9kuLoH5kUva/2Mmx/RdIyr4aONNSa2v9LTJsw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.1.tgz", + "integrity": "sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==", "license": "MIT" }, "node_modules/resolve.exports": { @@ -11866,7 +11844,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -11884,9 +11862,9 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -11946,10 +11924,13 @@ "optional": true }, "node_modules/sax": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", - "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", - "license": "BlueOak-1.0.0" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/scheduler": { "version": "0.26.0", @@ -11967,9 +11948,9 @@ } }, "node_modules/send": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", - "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -11978,13 +11959,13 @@ "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" @@ -12027,9 +12008,9 @@ } }, "node_modules/send/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -12045,35 +12026,20 @@ } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.19.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/serve-static/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-static/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/serve-static/node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -12083,60 +12049,6 @@ "node": ">= 0.8" } }, - "node_modules/serve-static/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static/node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12598,19 +12510,19 @@ "license": "MIT" }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" @@ -12767,9 +12679,9 @@ "optional": true }, "node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", + "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -12791,15 +12703,6 @@ "node": ">=18" } }, - "node_modules/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -12817,9 +12720,9 @@ } }, "node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -12835,9 +12738,9 @@ } }, "node_modules/terser/node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -12880,7 +12783,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -12898,9 +12801,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -13030,6 +12933,20 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -13146,10 +13063,19 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "license": "MIT" }, + "node_modules/undici": { + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz", + "integrity": "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -13192,18 +13118,6 @@ "node": ">=4" } }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "license": "MIT", - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -13224,9 +13138,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", - "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -13486,9 +13400,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -13571,9 +13485,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 8bff9af6..0d691e38 100644 --- a/package.json +++ b/package.json @@ -12,34 +12,34 @@ "dependencies": { "@adrianso/react-native-device-brightness": "^1.2.7", "@backpackapp-io/react-native-toast": "^0.15.1", - "@bottom-tabs/react-navigation": "^1.0.2", + "@bottom-tabs/react-navigation": "^1.1.0", "@d11/react-native-fast-image": "^8.13.0", "@expo/env": "^2.0.7", "@expo/metro-runtime": "~6.1.2", "@expo/vector-icons": "^15.0.2", - "@gorhom/bottom-sheet": "^5.2.6", - "@kesha-antonov/react-native-background-downloader": "^4.4.5", - "@legendapp/list": "^2.0.13", - "@lottiefiles/dotlottie-react": "^0.13.5", + "@gorhom/bottom-sheet": "^5.2.8", + "@kesha-antonov/react-native-background-downloader": "^4.5.3", + "@legendapp/list": "^2.0.19", + "@lottiefiles/dotlottie-react": "^0.18.5", "@react-native-community/blur": "^4.4.1", - "@react-native-community/netinfo": "^11.4.1", - "@react-native-community/slider": "^5.1.1", + "@react-native-community/netinfo": "^12.0.1", + "@react-native-community/slider": "^5.1.2", "@react-native-picker/picker": "^2.11.4", - "@react-navigation/bottom-tabs": "^7.3.10", - "@react-navigation/native": "^7.1.6", - "@react-navigation/native-stack": "^7.3.10", - "@react-navigation/stack": "^7.2.10", - "@sentry/react-native": "^7.6.0", - "@shopify/flash-list": "^2.2.0", - "@shopify/react-native-skia": "^2.4.14", - "@types/lodash": "^4.17.16", - "@types/react-native-video": "^5.0.20", - "axios": "^1.12.2", - "axios-cookiejar-support": "^6.0.4", + "@react-navigation/bottom-tabs": "^7.15.5", + "@react-navigation/native": "^7.1.33", + "@react-navigation/native-stack": "^7.14.5", + "@react-navigation/stack": "^7.8.5", + "@sentry/react-native": "^8.4.0", + "@shopify/flash-list": "^2.3.0", + "@shopify/react-native-skia": "^2.5.1", + "@types/lodash": "^4.17.24", + "@types/react-native-video": "^5.0.21", + "axios": "^1.13.6", + "axios-cookiejar-support": "^6.0.5", "cheerio-without-node-native": "^0.20.2", "crypto-js": "^4.2.0", "date-fns": "^4.1.0", - "eventemitter3": "^5.0.1", + "eventemitter3": "^5.0.4", "expo": "^54", "expo-application": "~7.0.7", "expo-auth-session": "~7.0.8", @@ -67,47 +67,47 @@ "expo-system-ui": "~6.0.7", "expo-updates": "~29.0.12", "expo-web-browser": "~15.0.8", - "i18next": "^25.7.3", + "i18next": "^25.8.18", "intl-pluralrules": "^2.0.1", - "lodash": "^4.17.21", - "lottie-react-native": "~7.3.1", - "posthog-react-native": "^4.4.0", + "lodash": "^4.17.23", + "lottie-react-native": "~7.3.6", + "posthog-react-native": "^4.37.2", "react": "19.1.0", "react-dom": "19.1.0", - "react-i18next": "^16.5.1", + "react-i18next": "^16.5.8", "react-native": "0.81.4", - "react-native-boost": "^0.6.2", - "react-native-bottom-tabs": "^1.0.2", - "react-native-gesture-handler": "^2.29.1", + "react-native-boost": "^1.0.0", + "react-native-bottom-tabs": "^1.1.0", + "react-native-gesture-handler": "^2.30.0", "react-native-get-random-values": "^2.0.0", "react-native-google-cast": "^4.9.1", - "react-native-image-colors": "^2.5.0", + "react-native-image-colors": "^2.6.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.2.0", + "react-native-mmkv": "^4.2.0", + "react-native-nitro-modules": "^0.35.0", + "react-native-paper": "^5.15.0", + "react-native-reanimated": "^4.2.2", "react-native-reanimated-carousel": "^4.0.3", - "react-native-safe-area-context": "~5.6.0", - "react-native-screens": "^4.18.0", - "react-native-svg": "^15.12.1", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "^4.24.0", + "react-native-svg": "^15.15.3", "react-native-url-polyfill": "^3.0.0", "react-native-vector-icons": "^10.3.0", "react-native-video": "^6.19.0", - "react-native-web": "^0.21.0", + "react-native-web": "^0.21.2", "react-native-wheel-color-picker": "^1.3.1", - "react-native-worklets": "^0.7.1" + "react-native-worklets": "^0.7.4" }, "devDependencies": { - "@babel/core": "^7.25.2", + "@babel/core": "^7.29.0", "@types/crypto-js": "^4.2.2", "@types/react": "~18.3.12", "@types/react-native": "^0.72.8", "@types/react-native-vector-icons": "^6.4.18", "babel-plugin-transform-remove-console": "^6.9.4", "patch-package": "^8.0.1", - "react-native-svg-transformer": "^1.5.0", + "react-native-svg-transformer": "^1.5.3", "typescript": "^5.9.3", "xcode": "^3.0.1" }, diff --git a/patches/react-native-video+6.19.0.patch b/patches/react-native-video+6.19.0.patch index 1b5e0e2f..59ecf2bb 100644 --- a/patches/react-native-video+6.19.0.patch +++ b/patches/react-native-video+6.19.0.patch @@ -1,3 +1,63 @@ +diff --git a/node_modules/react-native-video/android/.classpath b/node_modules/react-native-video/android/.classpath +new file mode 100644 +index 0000000..bbe97e5 +--- /dev/null ++++ b/node_modules/react-native-video/android/.classpath +@@ -0,0 +1,6 @@ ++ ++ ++ ++ ++ ++ +diff --git a/node_modules/react-native-video/android/.project b/node_modules/react-native-video/android/.project +new file mode 100644 +index 0000000..2633130 +--- /dev/null ++++ b/node_modules/react-native-video/android/.project +@@ -0,0 +1,34 @@ ++ ++ ++ react-native-video ++ Project react-native-video created by Buildship. ++ ++ ++ ++ ++ org.eclipse.jdt.core.javabuilder ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectbuilder ++ ++ ++ ++ ++ ++ org.eclipse.jdt.core.javanature ++ org.eclipse.buildship.core.gradleprojectnature ++ ++ ++ ++ 1772755755997 ++ ++ 30 ++ ++ org.eclipse.core.resources.regexFilterMatcher ++ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ ++ ++ ++ ++ +diff --git a/node_modules/react-native-video/android/.settings/org.eclipse.buildship.core.prefs b/node_modules/react-native-video/android/.settings/org.eclipse.buildship.core.prefs +new file mode 100644 +index 0000000..1675490 +--- /dev/null ++++ b/node_modules/react-native-video/android/.settings/org.eclipse.buildship.core.prefs +@@ -0,0 +1,2 @@ ++connection.project.dir=../../../android ++eclipse.preferences.version=1 diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt index 1ac0fd0..953eb59 100644 --- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt @@ -92,10 +152,22 @@ index 96a7887..6e5cf08 100644 } } else { diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt -index bb945fe..2d3f8ca 100644 +index bb945fe..5a6b554 100644 --- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt +++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt -@@ -10,11 +10,14 @@ import android.widget.FrameLayout +@@ -1,29 +1,43 @@ + package com.brentvatne.exoplayer + ++import android.os.Build + import android.content.Context + import android.graphics.Color + import android.graphics.drawable.GradientDrawable + import android.util.AttributeSet ++import android.view.LayoutInflater ++import android.view.SurfaceView + import android.view.View + import android.view.View.MeasureSpec + import android.widget.FrameLayout import android.widget.TextView import androidx.media3.common.Player import androidx.media3.common.Timeline @@ -109,11 +181,31 @@ index bb945fe..2d3f8ca 100644 +import androidx.media3.ui.SubtitleView import com.brentvatne.common.api.ResizeMode import com.brentvatne.common.api.SubtitleStyle ++import com.brentvatne.common.api.ViewType ++import com.brentvatne.react.R -@@ -52,15 +55,58 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute - resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT + @UnstableApi + class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + FrameLayout(context, attrs, defStyleAttr) { + + private var localStyle = SubtitleStyle() ++ private var currentViewType = ViewType.VIEW_TYPE_SURFACE + private var pendingResizeMode: Int? = null ++ private var player: ExoPlayer? = null ++ private var showSubtitleButton = false ++ private var shutterColor = Color.TRANSPARENT ++ private var controllerVisibilityListener: PlayerView.ControllerVisibilityListener? = null ++ private var fullscreenButtonClickListener: PlayerView.FullscreenButtonClickListener? = null + private val liveBadge: TextView = TextView(context).apply { + text = "LIVE" + setTextColor(Color.WHITE) +@@ -36,20 +50,39 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute + visibility = View.GONE } +- private val playerView = PlayerView(context).apply { ++ private var playerView = createPlayerView(currentViewType) ++ + /** + * Subtitles rendered in a full-size overlay (NOT inside PlayerView's content frame). + * This keeps subtitles anchored in-place even when the video surface/content frame moves @@ -122,7 +214,19 @@ index bb945fe..2d3f8ca 100644 + * Controlled by SubtitleStyle.subtitlesFollowVideo. + */ + private val overlaySubtitleView = SubtitleView(context).apply { -+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) +- setShutterBackgroundColor(Color.TRANSPARENT) +- useController = true +- controllerAutoShow = true +- controllerHideOnTouch = true +- controllerShowTimeoutMs = 5000 +- // Don't show subtitle button by default - will be enabled when tracks are available +- setShowSubtitleButton(false) +- // Enable proper surface view handling to prevent rendering issues +- setUseArtwork(false) +- setDefaultArtwork(null) +- // Ensure proper video scaling - start with FIT mode +- resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT + visibility = View.GONE + // We control styling via SubtitleStyle; don't pull Android system caption defaults. + setApplyEmbeddedStyles(true) @@ -145,10 +249,10 @@ index bb945fe..2d3f8ca 100644 + overlaySubtitleView.visibility = if (shouldShow) View.VISIBLE else View.GONE + overlaySubtitleView.alpha = 1f + } -+ } -+ + } + init { - // Add PlayerView with explicit layout parameters +@@ -57,74 +90,193 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute val playerViewLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) addView(playerView, playerViewLayoutParams) @@ -169,18 +273,144 @@ index bb945fe..2d3f8ca 100644 } fun setPlayer(player: ExoPlayer?) { -@@ -80,6 +126,10 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute - playerView.resizeMode = resizeMode - } +- val currentPlayer = playerView.player ++ this.player?.removeListener(playerListener) ++ this.player = player ++ playerView.player = player ++ player?.addListener(playerListener) ++ } + +- if (currentPlayer != null) { +- currentPlayer.removeListener(playerListener) ++ fun setResizeMode(@ResizeMode.Mode mode: Int) { ++ val resizeMode = when (mode) { ++ ResizeMode.RESIZE_MODE_FIT -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT ++ ResizeMode.RESIZE_MODE_FIXED_WIDTH -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH ++ ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT ++ ResizeMode.RESIZE_MODE_FILL -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL ++ ResizeMode.RESIZE_MODE_CENTER_CROP -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM ++ else -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT ++ } ++ if (playerView.width > 0 && playerView.height > 0) { ++ playerView.resizeMode = resizeMode ++ } else { ++ pendingResizeMode = resizeMode } -+ + +- playerView.player = player + // Re-assert subtitle rendering mode for the current style. + updateSubtitleRenderingMode() + applySubtitleStyle(localStyle) ++ } + +- if (player != null) { +- player.addListener(playerListener) ++ fun getPlayerView(): PlayerView = playerView + +- // Apply pending resize mode if we have one +- pendingResizeMode?.let { resizeMode -> +- playerView.resizeMode = resizeMode +- } +- } ++ fun isPlaying(): Boolean = playerView.player?.isPlaying == true ++ ++ fun setControllerVisibilityListener(listener: PlayerView.ControllerVisibilityListener?) { ++ controllerVisibilityListener = listener ++ playerView.setControllerVisibilityListener(listener) } - fun getPlayerView(): PlayerView = playerView -@@ -108,23 +158,63 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute +- fun getPlayerView(): PlayerView = playerView ++ fun setFullscreenButtonClickListener(listener: PlayerView.FullscreenButtonClickListener?) { ++ fullscreenButtonClickListener = listener ++ playerView.setFullscreenButtonClickListener(listener) ++ } ++ ++ fun setShowSubtitleButton(show: Boolean) { ++ showSubtitleButton = show ++ playerView.setShowSubtitleButton(show) ++ } ++ ++ fun setUseController(useController: Boolean) { ++ playerView.useController = useController ++ } ++ ++ fun setControllerHideOnTouch(hideOnTouch: Boolean) { ++ playerView.setControllerHideOnTouch(hideOnTouch) ++ } ++ ++ fun setControllerAutoShow(autoShow: Boolean) { ++ playerView.setControllerAutoShow(autoShow) ++ } ++ ++ fun setControllerShowTimeoutMs(timeoutMs: Int) { ++ playerView.controllerShowTimeoutMs = timeoutMs ++ } ++ ++ fun isControllerVisible(): Boolean = playerView.isControllerFullyVisible + +- fun setResizeMode(@ResizeMode.Mode resizeMode: Int) { +- val targetResizeMode = when (resizeMode) { +- ResizeMode.RESIZE_MODE_FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL +- ResizeMode.RESIZE_MODE_CENTER_CROP -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM +- ResizeMode.RESIZE_MODE_FIT -> AspectRatioFrameLayout.RESIZE_MODE_FIT +- ResizeMode.RESIZE_MODE_FIXED_WIDTH -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH +- ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT +- else -> AspectRatioFrameLayout.RESIZE_MODE_FIT ++ fun hideController() { ++ playerView.hideController() ++ } ++ ++ fun showController() { ++ playerView.showController() ++ } ++ ++ fun updateSurfaceView(@ViewType.ViewType viewType: Int) { ++ if (currentViewType == viewType) { ++ return + } + +- // Apply the resize mode to PlayerView immediately +- playerView.resizeMode = targetResizeMode ++ currentViewType = viewType + +- // Store it for reapplication if needed +- pendingResizeMode = targetResizeMode ++ val previousPlayerView = playerView ++ val previousLayoutParams = previousPlayerView.layoutParams ?: LayoutParams( ++ LayoutParams.MATCH_PARENT, ++ LayoutParams.MATCH_PARENT ++ ) ++ val previousResizeMode = previousPlayerView.resizeMode ++ val previousUseController = previousPlayerView.useController ++ val previousControllerAutoShow = previousPlayerView.controllerAutoShow ++ val previousControllerHideOnTouch = previousPlayerView.controllerHideOnTouch ++ val previousControllerShowTimeoutMs = previousPlayerView.controllerShowTimeoutMs ++ ++ val replacementPlayerView = createPlayerView(viewType).apply { ++ layoutParams = previousLayoutParams ++ resizeMode = previousResizeMode ++ useController = previousUseController ++ controllerAutoShow = previousControllerAutoShow ++ controllerHideOnTouch = previousControllerHideOnTouch ++ controllerShowTimeoutMs = previousControllerShowTimeoutMs ++ setShowSubtitleButton(showSubtitleButton) ++ setControllerVisibilityListener(controllerVisibilityListener) ++ setFullscreenButtonClickListener(fullscreenButtonClickListener) ++ setShutterBackgroundColor(shutterColor) ++ player = this@ExoPlayerView.player ++ } + +- // Force PlayerView to recalculate its layout +- playerView.requestLayout() ++ removeView(previousPlayerView) ++ playerView = replacementPlayerView ++ addView(playerView, 0, previousLayoutParams) + +- // Also request layout on the parent to ensure proper sizing +- requestLayout() ++ updateSubtitleRenderingMode() ++ applySubtitleStyle(localStyle) ++ playerView.requestLayout() } fun setSubtitleStyle(style: SubtitleStyle) { @@ -250,26 +480,75 @@ index bb945fe..2d3f8ca 100644 if (style.opacity != 0.0f) { subtitleView.alpha = style.opacity subtitleView.visibility = android.view.View.VISIBLE -@@ -132,7 +222,59 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute +@@ -132,157 +284,125 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute subtitleView.visibility = android.view.View.GONE } } - localStyle = style -+ +- } +- +- fun setShutterColor(color: Int) { +- playerView.setShutterBackgroundColor(color) +- } +- +- fun updateSurfaceView(viewType: Int) { +- // TODO: Implement proper surface type switching if needed +- } +- +- val isPlaying: Boolean +- get() = playerView.player?.isPlaying ?: false +- +- fun invalidateAspectRatio() { +- // PlayerView handles aspect ratio automatically through its internal AspectRatioFrameLayout +- playerView.requestLayout() +- +- // Reapply the current resize mode to ensure it's properly set +- pendingResizeMode?.let { resizeMode -> +- playerView.resizeMode = resizeMode +- } +- } + +- fun setUseController(useController: Boolean) { +- playerView.useController = useController +- if (useController) { +- // Ensure proper touch handling when controls are enabled +- playerView.controllerAutoShow = true +- playerView.controllerHideOnTouch = true +- // Show controls immediately when enabled +- playerView.showController() +- } +- } +- +- fun showController() { +- playerView.showController() +- } +- +- fun hideController() { +- playerView.hideController() +- } + // Apply the same styling to the overlay subtitle view. + run { + val subtitleView = overlaySubtitleView -+ + +- fun setControllerShowTimeoutMs(showTimeoutMs: Int) { +- playerView.controllerShowTimeoutMs = showTimeoutMs +- } + val resolvedTextColor = style.textColor ?: CaptionStyleCompat.DEFAULT.foregroundColor + val resolvedBackgroundColor = style.backgroundColor ?: Color.TRANSPARENT + val resolvedEdgeColor = style.edgeColor ?: Color.BLACK -+ + +- fun setControllerAutoShow(autoShow: Boolean) { +- playerView.controllerAutoShow = autoShow +- } + val resolvedEdgeType = when (style.edgeType?.lowercase()) { + "outline" -> CaptionStyleCompat.EDGE_TYPE_OUTLINE + "shadow" -> CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW + else -> CaptionStyleCompat.EDGE_TYPE_NONE + } -+ + +- fun setControllerHideOnTouch(hideOnTouch: Boolean) { +- playerView.controllerHideOnTouch = hideOnTouch +- } + val captionStyle = CaptionStyleCompat( + resolvedTextColor, + resolvedBackgroundColor, @@ -279,21 +558,28 @@ index bb945fe..2d3f8ca 100644 + null + ) + subtitleView.setStyle(captionStyle) -+ + +- fun setFullscreenButtonClickListener(listener: PlayerView.FullscreenButtonClickListener?) { +- playerView.setFullscreenButtonClickListener(listener) +- } + if (style.fontSize > 0) { + // Use DIP so the value matches React Native's dp-based fontSize more closely. + subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_DIP, style.fontSize.toFloat()) + } else { + subtitleView.setUserDefaultTextSize() + } -+ + +- fun setShowSubtitleButton(show: Boolean) { +- playerView.setShowSubtitleButton(show) +- } + subtitleView.setPadding( + style.paddingLeft, + style.paddingTop, + style.paddingRight, + 0 + ) -+ + +- fun isControllerVisible(): Boolean = playerView.isControllerFullyVisible + // Bottom offset relative to the full view height (stable even when video content frame moves). + val h = height.takeIf { it > 0 } ?: subtitleView.height + if (style.paddingBottom > 0 && h > 0) { @@ -303,15 +589,43 @@ index bb945fe..2d3f8ca 100644 + } else { + subtitleView.setBottomPaddingFraction(0f) + } -+ + +- fun setControllerVisibilityListener(listener: PlayerView.ControllerVisibilityListener?) { +- playerView.setControllerVisibilityListener(listener) + if (style.opacity != 0.0f) { + subtitleView.alpha = style.opacity + } + } } - fun setShutterColor(color: Int) { -@@ -223,6 +365,13 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute +- override fun addOnLayoutChangeListener(listener: View.OnLayoutChangeListener) { +- playerView.addOnLayoutChangeListener(listener) ++ fun setShutterColor(color: Int) { ++ shutterColor = color ++ playerView.setShutterBackgroundColor(color) + } + +- override fun setFocusable(focusable: Boolean) { +- playerView.isFocusable = focusable ++ fun setShowLiveBadge(show: Boolean) { ++ liveBadge.visibility = if (show) View.VISIBLE else View.GONE + } + +- private fun updateLiveUi() { +- val player = playerView.player ?: return +- val isLive = player.isCurrentMediaItemLive +- val seekable = player.isCurrentMediaItemSeekable +- +- // Show/hide badge +- liveBadge.visibility = if (isLive) View.VISIBLE else View.GONE +- +- // Disable/enable scrubbing based on seekable +- val timeBar = playerView.findViewById(androidx.media3.ui.R.id.exo_progress) +- timeBar?.isEnabled = !isLive || seekable ++ fun invalidateAspectRatio() { ++ playerView.post { ++ playerView.requestLayout() ++ } } private val playerListener = object : Player.Listener { @@ -325,998 +639,169 @@ index bb945fe..2d3f8ca 100644 override fun onTimelineChanged(timeline: Timeline, reason: Int) { playerView.post { playerView.requestLayout() -@@ -284,6 +433,9 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute - pendingResizeMode?.let { resizeMode -> - playerView.resizeMode = resizeMode +- // Reapply resize mode to ensure it's properly set after timeline changes +- pendingResizeMode?.let { resizeMode -> +- playerView.resizeMode = resizeMode +- } + } +- updateLiveUi() + } ++ } + +- override fun onEvents(player: Player, events: Player.Events) { +- if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION) || +- events.contains(Player.EVENT_IS_PLAYING_CHANGED) +- ) { +- updateLiveUi() +- } +- +- // Handle video size changes which affect aspect ratio +- if (events.contains(Player.EVENT_VIDEO_SIZE_CHANGED)) { +- pendingResizeMode?.let { resizeMode -> +- playerView.resizeMode = resizeMode +- } +- playerView.requestLayout() +- requestLayout() ++ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { ++ super.onMeasure(widthMeasureSpec, heightMeasureSpec) ++ val width = MeasureSpec.getSize(widthMeasureSpec) ++ val height = MeasureSpec.getSize(heightMeasureSpec) ++ if (width > 0 && height > 0) { ++ pendingResizeMode?.let { resizeMode -> ++ playerView.resizeMode = resizeMode } + // Re-apply bottomPaddingFraction once we have a concrete height. + updateSubtitleRenderingMode() + applySubtitleStyle(localStyle) } } - } + +- companion object { +- private const val TAG = "ExoPlayerView" +- } +- +- /** +- * React Native (Yoga) can sometimes defer layout passes that are required by +- * PlayerView for its child views (controller overlay, surface view, subtitle view, …). +- * This helper forces a second measure / layout after RN finishes, ensuring the +- * internal views receive the final size. The same approach is used in the v7 +- * implementation (see VideoView.kt) and in React Native core (Toolbar example [link]). +- */ +- private val layoutRunnable = Runnable { +- measure( +- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), +- MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) +- ) +- layout(left, top, right, bottom) +- } +- +- override fun requestLayout() { +- super.requestLayout() +- // Post a second layout pass so the ExoPlayer internal views get correct bounds. +- post(layoutRunnable) +- } +- +- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { +- super.onLayout(changed, left, top, right, bottom) ++ private fun createPlayerView(@ViewType.ViewType viewType: Int): PlayerView { ++ val layoutRes = when (viewType) { ++ ViewType.VIEW_TYPE_TEXTURE -> R.layout.exo_player_view_texture ++ else -> R.layout.exo_player_view_surface ++ } + +- if (changed) { +- pendingResizeMode?.let { resizeMode -> +- playerView.resizeMode = resizeMode ++ return (LayoutInflater.from(context).inflate(layoutRes, this, false) as PlayerView).apply { ++ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) ++ setShutterBackgroundColor(shutterColor) ++ useController = true ++ controllerAutoShow = true ++ controllerHideOnTouch = true ++ controllerShowTimeoutMs = 5000 ++ setShowSubtitleButton(showSubtitleButton) ++ setUseArtwork(false) ++ setDefaultArtwork(null) ++ resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT ++ ++ if (viewType == ViewType.VIEW_TYPE_SURFACE_SECURE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { ++ (videoSurfaceView as? SurfaceView)?.setSecure(true) + } + } + } +diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt +index b5d786b..3c7ed65 100644 +--- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt ++++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt +@@ -50,7 +50,7 @@ class FullScreenPlayerView( + if (fullscreenVideoPlayer != null) { + val window = fullscreenVideoPlayer.window + if (window != null) { +- val isPlaying = fullscreenVideoPlayer.exoPlayerView.isPlaying ++ val isPlaying = fullscreenVideoPlayer.exoPlayerView.isPlaying() + if (isPlaying) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java -index e16ac96..54221ef 100644 +index e16ac96..773535a 100644 --- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java -@@ -228,7 +228,8 @@ public class ReactExoplayerView extends FrameLayout implements - private ArrayList rootViewChildrenOriginalVisibility = new ArrayList(); - - /* -- * When user is seeking first called is on onPositionDiscontinuity -> DISCONTINUITY_REASON_SEEK -+ * When user is seeking first called is on onPositionDiscontinuity -> -+ * DISCONTINUITY_REASON_SEEK - * Then we set if to false when playback is back in onIsPlayingChanged -> true - */ - private boolean isSeeking = false; -@@ -298,7 +299,8 @@ public class ReactExoplayerView extends FrameLayout implements - lastPos = pos; - lastBufferDuration = bufferedDuration; - lastDuration = duration; -- eventEmitter.onVideoProgress.invoke(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos)); -+ eventEmitter.onVideoProgress.invoke(pos, bufferedDuration, player.getDuration(), -+ getPositionInFirstPeriodMsForCurrentWindow(pos)); - } - } - } -@@ -316,7 +318,7 @@ public class ReactExoplayerView extends FrameLayout implements - - public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) { - Timeline.Window window = new Timeline.Window(); -- if(!player.getCurrentTimeline().isEmpty()) { -+ if (!player.getCurrentTimeline().isEmpty()) { - player.getCurrentTimeline().getWindow(player.getCurrentMediaItemIndex(), window); - } - return window.windowStartTimeMs + currentPosition; -@@ -355,9 +357,9 @@ public class ReactExoplayerView extends FrameLayout implements - LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT); - exoPlayerView = new ExoPlayerView(getContext()); -- exoPlayerView.addOnLayoutChangeListener( (View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) -> -- PictureInPictureUtil.applySourceRectHint(themedReactContext, pictureInPictureParamsBuilder, exoPlayerView) -- ); -+ exoPlayerView.addOnLayoutChangeListener( -+ (View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) -> PictureInPictureUtil -+ .applySourceRectHint(themedReactContext, pictureInPictureParamsBuilder, exoPlayerView)); - exoPlayerView.setLayoutParams(layoutParams); - addView(exoPlayerView, 0, layoutParams); - -@@ -383,8 +385,10 @@ public class ReactExoplayerView extends FrameLayout implements - public void onHostPause() { - isInBackground = true; +@@ -385,7 +385,7 @@ public class ReactExoplayerView extends FrameLayout implements Activity activity = themedReactContext.getCurrentActivity(); -- boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInPictureInPictureMode(); -- boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInMultiWindowMode(); -+ boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null -+ && activity.isInPictureInPictureMode(); -+ boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null -+ && activity.isInMultiWindowMode(); - if (playInBackground || isInPictureInPicture || isInMultiWindowMode || enterPictureInPictureOnLeave) { + boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInPictureInPictureMode(); + boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInMultiWindowMode(); +- if (playInBackground || isInPictureInPicture || isInMultiWindowMode) { ++ if (playInBackground || isInPictureInPicture || isInMultiWindowMode || enterPictureInPictureOnLeave) { return; } -@@ -403,7 +407,7 @@ public class ReactExoplayerView extends FrameLayout implements - viewHasDropped = true; - } - -- //BandwidthMeter.EventListener implementation -+ // BandwidthMeter.EventListener implementation - @Override - public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) { - if (mReportBandwidth) { -@@ -411,7 +415,8 @@ public class ReactExoplayerView extends FrameLayout implements - eventEmitter.onVideoBandwidthUpdate.invoke(bitrate, 0, 0, null); - } else { - Format videoFormat = player.getVideoFormat(); -- boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270); -+ boolean isRotatedContent = videoFormat != null -+ && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270); - int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0; - int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0; - String trackId = videoFormat != null ? videoFormat.id : null; -@@ -426,7 +431,8 @@ public class ReactExoplayerView extends FrameLayout implements - * Toggling the visibility of the player control view - */ - private void togglePlayerControlVisibility() { -- if (player == null) return; -+ if (player == null) -+ return; - if (exoPlayerView.isControllerVisible()) { - exoPlayerView.hideController(); - } else { -@@ -450,7 +456,8 @@ public class ReactExoplayerView extends FrameLayout implements - } - - private void updateControllerConfig() { -- if (exoPlayerView == null) return; -+ if (exoPlayerView == null) -+ return; - - exoPlayerView.setControllerShowTimeoutMs(5000); - -@@ -461,7 +468,8 @@ public class ReactExoplayerView extends FrameLayout implements - } - - private void updateControllerVisibility() { -- if (exoPlayerView == null) return; -+ if (exoPlayerView == null) -+ return; - - exoPlayerView.setUseController(controls && !controlsConfig.getHideFullscreen()); - } -@@ -469,7 +477,7 @@ public class ReactExoplayerView extends FrameLayout implements - private void openSettings() { - AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext); - builder.setTitle(R.string.settings); -- String[] settingsOptions = {themedReactContext.getString(R.string.playback_speed)}; -+ String[] settingsOptions = { themedReactContext.getString(R.string.playback_speed) }; - builder.setItems(settingsOptions, (dialog, which) -> { - if (which == 0) { - showPlaybackSpeedOptions(); -@@ -479,7 +487,7 @@ public class ReactExoplayerView extends FrameLayout implements - } - - private void showPlaybackSpeedOptions() { -- String[] speedOptions = {"0.5x", "1.0x", "1.5x", "2.0x"}; -+ String[] speedOptions = { "0.5x", "1.0x", "1.5x", "2.0x" }; - AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext); - builder.setTitle(R.string.select_playback_speed); - -@@ -497,8 +505,10 @@ public class ReactExoplayerView extends FrameLayout implements - speed = 2.0f; - break; - default: -- speed = 1.0f;; -- }; -+ speed = 1.0f; -+ ; + setPlayWhenReady(false); +@@ -1567,6 +1567,11 @@ public class ReactExoplayerView extends FrameLayout implements + Track audioTrack = exoplayerTrackToGenericTrack(format, groupIndex, selection, group); + audioTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); + audioTrack.setSelected(isSelected); ++ // Encode channel count into title so JS can read it e.g. "English|ch:6" ++ if (format.channelCount != Format.NO_VALUE && format.channelCount > 0) { ++ String existing = audioTrack.getTitle() != null ? audioTrack.getTitle() : ""; ++ audioTrack.setTitle(existing + "|ch:" + format.channelCount); + } -+ ; - setRateModifier(speed); - }); - builder.show(); -@@ -510,24 +520,30 @@ public class ReactExoplayerView extends FrameLayout implements - - /** - * Update the layout -- * @param view view needs to update layout - * -- * This is a workaround for the open bug in react-native: ... -+ * @param view view needs to update layout -+ * -+ * This is a workaround for the open bug in react-native: ... - */ - private void reLayout(View view) { -- if (view == null) return; -+ if (view == null) -+ return; - view.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); - view.layout(view.getLeft(), view.getTop(), view.getMeasuredWidth(), view.getMeasuredHeight()); - } - - private void refreshControlsStyles() { -- if (exoPlayerView == null || player == null || !controls) return; -+ if (exoPlayerView == null || player == null || !controls) -+ return; - updateControllerVisibility(); - } - -- // Note: The following methods for live content and button visibility are no longer needed -- // as PlayerView handles controls automatically. Some functionality may need to be -+ // Note: The following methods for live content and button visibility are no -+ // longer needed -+ // as PlayerView handles controls automatically. Some functionality may need to -+ // be - // reimplemented using PlayerView's APIs if custom behavior is required. - - private void reLayoutControls() { -@@ -564,6 +580,7 @@ public class ReactExoplayerView extends FrameLayout implements - private class RNVLoadControl extends DefaultLoadControl { - private final int availableHeapInBytes; - private final Runtime runtime; -+ - public RNVLoadControl(DefaultAllocator allocator, BufferConfig config) { - super(allocator, - config.getMinBufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt() -@@ -574,7 +591,7 @@ public class ReactExoplayerView extends FrameLayout implements - : DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, - config.getBufferForPlaybackMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt() - ? config.getBufferForPlaybackMs() -- : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS , -+ : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, - config.getBufferForPlaybackAfterRebufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt() - ? config.getBufferForPlaybackAfterRebufferMs() - : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, -@@ -585,10 +602,12 @@ public class ReactExoplayerView extends FrameLayout implements - : DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS, - DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME); - runtime = Runtime.getRuntime(); -- ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(ThemedReactContext.ACTIVITY_SERVICE); -- double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble() -- ? config.getMaxHeapAllocationPercent() -- : DEFAULT_MAX_HEAP_ALLOCATION_PERCENT; -+ ActivityManager activityManager = (ActivityManager) themedReactContext -+ .getSystemService(ThemedReactContext.ACTIVITY_SERVICE); -+ double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion -+ .getBufferConfigPropUnsetDouble() -+ ? config.getMaxHeapAllocationPercent() -+ : DEFAULT_MAX_HEAP_ALLOCATION_PERCENT; - availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeap * 1024 * 1024); + audioTracks.add(audioTrack); } - -@@ -606,13 +625,15 @@ public class ReactExoplayerView extends FrameLayout implements - } - long usedMemory = runtime.totalMemory() - runtime.freeMemory(); - long freeMemory = runtime.maxMemory() - usedMemory; -- double minBufferMemoryReservePercent = source.getBufferConfig().getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble() -- ? source.getBufferConfig().getMinBufferMemoryReservePercent() -- : ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE; -+ double minBufferMemoryReservePercent = source.getBufferConfig() -+ .getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble() -+ ? source.getBufferConfig().getMinBufferMemoryReservePercent() -+ : ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE; - long reserveMemory = (long) minBufferMemoryReservePercent * runtime.maxMemory(); - long bufferedMs = bufferedDurationUs / (long) 1000; - if (reserveMemory > freeMemory && bufferedMs > 2000) { -- // We don't have enough memory in reserve so we stop buffering to allow other components to use it instead -+ // We don't have enough memory in reserve so we stop buffering to allow other -+ // components to use it instead - return false; - } - if (runtime.freeMemory() == 0) { -@@ -645,13 +666,13 @@ public class ReactExoplayerView extends FrameLayout implements - // Initialize core configuration and listeners - initializePlayerCore(self); - pipListenerUnsubscribe = PictureInPictureUtil.addLifecycleEventListener(themedReactContext, this); -- PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, this.enterPictureInPictureOnLeave); -+ PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, -+ this.enterPictureInPictureOnLeave); - } - if (!source.isLocalAssetFile() && !source.isAsset() && source.getBufferConfig().getCacheSize() > 0) { - RNVSimpleCache.INSTANCE.setSimpleCache( - this.getContext(), -- source.getBufferConfig().getCacheSize() -- ); -+ source.getBufferConfig().getCacheSize()); - useCache = true; - } else { - useCache = false; -@@ -659,7 +680,8 @@ public class ReactExoplayerView extends FrameLayout implements - if (playerNeedsSource) { - // Will force display of shutter view if needed - exoPlayerView.invalidateAspectRatio(); -- // DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread -+ // DRM session manager creation must be done on a different thread to prevent -+ // crashes so we start a new thread - ExecutorService es = Executors.newSingleThreadExecutor(); - es.execute(() -> { - // DRM initialization must run on a different thread -@@ -668,7 +690,8 @@ public class ReactExoplayerView extends FrameLayout implements - } - if (activity == null) { - DebugLog.e(TAG, "Failed to initialize Player!, null activity"); -- eventEmitter.onVideoError.invoke("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001"); -+ eventEmitter.onVideoError.invoke("Failed to initialize Player!", -+ new Exception("Current Activity is null!"), "1001"); - return; - } - -@@ -721,8 +744,7 @@ public class ReactExoplayerView extends FrameLayout implements - DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); - RNVLoadControl loadControl = new RNVLoadControl( - allocator, -- source.getBufferConfig() -- ); -+ source.getBufferConfig()); - - long initialBitrate = source.getBufferConfig().getInitialBitrate(); - if (initialBitrate > 0) { -@@ -730,11 +752,10 @@ public class ReactExoplayerView extends FrameLayout implements - this.bandwidthMeter = config.getBandwidthMeter(); - } - -- DefaultRenderersFactory renderersFactory = -- new DefaultRenderersFactory(getContext()) -- .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF) -- .setEnableDecoderFallback(true) -- .forceEnableMediaCodecAsynchronousQueueing(); -+ DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext()) -+ .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER) -+ .setEnableDecoderFallback(true) -+ .forceEnableMediaCodecAsynchronousQueueing(); - - DefaultMediaSourceFactory mediaSourceFactory; - -@@ -743,11 +764,13 @@ public class ReactExoplayerView extends FrameLayout implements - } else { - mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory); - -- mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView.getPlayerView()); -+ mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, -+ exoPlayerView.getPlayerView()); - } - - if (useCache && !disableCache) { -- mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))); -+ mediaSourceFactory -+ .setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))); - } - - player = new ExoPlayer.Builder(getContext(), renderersFactory) -@@ -772,7 +795,7 @@ public class ReactExoplayerView extends FrameLayout implements - player.setPlaybackParameters(params); - changeAudioOutput(this.audioOutput); - -- if(showNotificationControls) { -+ if (showNotificationControls) { - setupPlaybackService(); - } - } -@@ -784,8 +807,7 @@ public class ReactExoplayerView extends FrameLayout implements - Uri adTagUrl = adProps.getAdTagUrl(); - if (adTagUrl != null) { - // Create an AdsLoader. -- ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader -- .Builder(themedReactContext) -+ ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader.Builder(themedReactContext) - .setAdEventListener(this) - .setAdErrorListener(this); - -@@ -817,7 +839,8 @@ public class ReactExoplayerView extends FrameLayout implements - } - - try { -- // First check if there's a custom DRM manager registered through the plugin system -+ // First check if there's a custom DRM manager registered through the plugin -+ // system - DRMManagerSpec drmManager = ReactNativeVideoManager.Companion.getInstance().getDRMManager(); - if (drmManager == null) { - // If no custom manager is registered, use the default implementation -@@ -826,11 +849,13 @@ public class ReactExoplayerView extends FrameLayout implements - - DrmSessionManager drmSessionManager = drmManager.buildDrmSessionManager(uuid, drmProps); - if (drmSessionManager == null) { -- eventEmitter.onVideoError.invoke("Failed to build DRM session manager", new Exception("DRM session manager is null"), "3007"); -+ eventEmitter.onVideoError.invoke("Failed to build DRM session manager", -+ new Exception("DRM session manager is null"), "3007"); - } - - // Allow plugins to override the DrmSessionManager -- DrmSessionManager overriddenManager = ReactNativeVideoManager.Companion.getInstance().overrideDrmSessionManager(source, drmSessionManager); -+ DrmSessionManager overriddenManager = ReactNativeVideoManager.Companion.getInstance() -+ .overrideDrmSessionManager(source, drmSessionManager); - return overriddenManager != null ? overriddenManager : drmSessionManager; - } catch (UnsupportedDrmException ex) { - // Unsupported DRM exceptions are handled by the calling method -@@ -853,7 +878,8 @@ public class ReactExoplayerView extends FrameLayout implements - } - /// init DRM - DrmSessionManager drmSessionManager = initializePlayerDrm(); -- if (drmSessionManager == null && runningSource.getDrmProps() != null && runningSource.getDrmProps().getDrmType() != null) { -+ if (drmSessionManager == null && runningSource.getDrmProps() != null -+ && runningSource.getDrmProps().getDrmType() != null) { - // Failed to initialize DRM session manager - cannot continue - DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!"); - return; -@@ -910,7 +936,8 @@ public class ReactExoplayerView extends FrameLayout implements - } catch (UnsupportedDrmException e) { - int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported - : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME -- ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); -+ ? R.string.error_drm_unsupported_scheme -+ : R.string.error_drm_unknown); - eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003"); - } - } -@@ -955,7 +982,8 @@ public class ReactExoplayerView extends FrameLayout implements - if (playbackServiceBinder != null) { - playbackServiceBinder.getService().unregisterPlayer(player); - } -- } catch (Exception ignored) {} -+ } catch (Exception ignored) { -+ } - - playbackServiceBinder = null; - } -@@ -987,21 +1015,22 @@ public class ReactExoplayerView extends FrameLayout implements - - private void cleanupPlaybackService() { - try { -- if(player != null && playbackServiceBinder != null) { -+ if (player != null && playbackServiceBinder != null) { - playbackServiceBinder.getService().unregisterPlayer(player); - } - - playbackServiceBinder = null; - -- if(playbackServiceConnection != null) { -+ if (playbackServiceConnection != null) { - themedReactContext.unbindService(playbackServiceConnection); - } -- } catch(Exception e) { -+ } catch (Exception e) { - DebugLog.w(TAG, "Cloud not cleanup playback service"); - } - } - -- private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long cropStartMs, long cropEndMs) { -+ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, -+ long cropStartMs, long cropEndMs) { - if (uri == null) { - throw new IllegalStateException("Invalid video uri"); - } -@@ -1033,12 +1062,12 @@ public class ReactExoplayerView extends FrameLayout implements - Uri adTagUrl = source.getAdsProps().getAdTagUrl(); - if (adTagUrl != null) { - mediaItemBuilder.setAdsConfiguration( -- new MediaItem.AdsConfiguration.Builder(adTagUrl).build() -- ); -+ new MediaItem.AdsConfiguration.Builder(adTagUrl).build()); - } - } - -- MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils.getLiveConfiguration(source.getBufferConfig()); -+ MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils -+ .getLiveConfiguration(source.getBufferConfig()); - mediaItemBuilder.setLiveConfiguration(liveConfiguration.build()); - - MediaSource.Factory mediaSourceFactory; -@@ -1050,29 +1079,26 @@ public class ReactExoplayerView extends FrameLayout implements - drmProvider = new DefaultDrmSessionManagerProvider(); - } - -- - switch (type) { - case CONTENT_TYPE_SS: -- if(!BuildConfig.USE_EXOPLAYER_SMOOTH_STREAMING) { -+ if (!BuildConfig.USE_EXOPLAYER_SMOOTH_STREAMING) { - DebugLog.e("Exo Player Exception", "Smooth Streaming is not enabled!"); - throw new IllegalStateException("Smooth Streaming is not enabled!"); - } - - mediaSourceFactory = new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), -- buildDataSourceFactory(false) -- ); -+ buildDataSourceFactory(false)); - break; - case CONTENT_TYPE_DASH: -- if(!BuildConfig.USE_EXOPLAYER_DASH) { -+ if (!BuildConfig.USE_EXOPLAYER_DASH) { - DebugLog.e("Exo Player Exception", "DASH is not enabled!"); - throw new IllegalStateException("DASH is not enabled!"); - } - - mediaSourceFactory = new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), -- buildDataSourceFactory(false) -- ); -+ buildDataSourceFactory(false)); - break; - case CONTENT_TYPE_HLS: - if (!BuildConfig.USE_EXOPLAYER_HLS) { -@@ -1087,13 +1113,14 @@ public class ReactExoplayerView extends FrameLayout implements - } - - mediaSourceFactory = new HlsMediaSource.Factory( -- dataSourceFactory -- ).setAllowChunklessPreparation(source.getTextTracksAllowChunklessPreparation()); -+ dataSourceFactory) -+ .setAllowChunklessPreparation(source.getTextTracksAllowChunklessPreparation()); - break; - case CONTENT_TYPE_OTHER: - if ("asset".equals(uri.getScheme())) { - try { -- DataSource.Factory assetDataSourceFactory = DataSourceUtil.buildAssetDataSourceFactory(themedReactContext, uri); -+ DataSource.Factory assetDataSourceFactory = DataSourceUtil -+ .buildAssetDataSourceFactory(themedReactContext, uri); - mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory); - } catch (Exception e) { - throw new IllegalStateException("cannot open input file:" + uri); -@@ -1101,12 +1128,10 @@ public class ReactExoplayerView extends FrameLayout implements - } else if ("file".equals(uri.getScheme()) || - !useCache) { - mediaSourceFactory = new ProgressiveMediaSource.Factory( -- mediaDataSourceFactory -- ); -+ mediaDataSourceFactory); - } else { - mediaSourceFactory = new ProgressiveMediaSource.Factory( -- RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)) -- ); -+ RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))); - - } - break; -@@ -1125,20 +1150,19 @@ public class ReactExoplayerView extends FrameLayout implements - - if (cmcdConfigurationFactory != null) { - mediaSourceFactory = mediaSourceFactory.setCmcdConfigurationFactory( -- cmcdConfigurationFactory::createCmcdConfiguration -- ); -+ cmcdConfigurationFactory::createCmcdConfiguration); - } - - mediaSourceFactory = Objects.requireNonNullElse( - ReactNativeVideoManager.Companion.getInstance() - .overrideMediaSourceFactory(source, mediaSourceFactory, mediaDataSourceFactory), -- mediaSourceFactory -- ); -+ mediaSourceFactory); - - mediaItemBuilder.setStreamKeys(streamKeys); - - @Nullable -- final MediaItem.Builder overridenMediaItemBuilder = ReactNativeVideoManager.Companion.getInstance().overrideMediaItemBuilder(source, mediaItemBuilder); -+ final MediaItem.Builder overridenMediaItemBuilder = ReactNativeVideoManager.Companion.getInstance() -+ .overrideMediaItemBuilder(source, mediaItemBuilder); - - MediaItem mediaItem = overridenMediaItemBuilder != null - ? overridenMediaItemBuilder.build() -@@ -1147,8 +1171,7 @@ public class ReactExoplayerView extends FrameLayout implements - MediaSource mediaSource = mediaSourceFactory - .setDrmSessionManagerProvider(drmProvider) - .setLoadErrorHandlingPolicy( -- config.buildLoadErrorHandlingPolicy(source.getMinLoadRetryCount()) -- ) -+ config.buildLoadErrorHandlingPolicy(source.getMinLoadRetryCount())) - .createMediaSource(mediaItem); - - if (cropStartMs >= 0 && cropEndMs >= 0) { -@@ -1183,7 +1206,8 @@ public class ReactExoplayerView extends FrameLayout implements - } - } - -- MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder(track.getUri()) -+ MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder( -+ track.getUri()) - .setId(trackId) - .setMimeType(track.getType()) - .setLabel(label) -@@ -1194,7 +1218,8 @@ public class ReactExoplayerView extends FrameLayout implements - configBuilder.setLanguage(track.getLanguage()); - } - -- // Set selection flags - make first track default if no specific track is selected -+ // Set selection flags - make first track default if no specific track is -+ // selected - if (trackIndex == 0 && (textTrackType == null || "disabled".equals(textTrackType))) { - configBuilder.setSelectionFlags(C.SELECTION_FLAG_DEFAULT); - } else { -@@ -1204,10 +1229,12 @@ public class ReactExoplayerView extends FrameLayout implements - MediaItem.SubtitleConfiguration subtitleConfiguration = configBuilder.build(); - subtitleConfigurations.add(subtitleConfiguration); - -- DebugLog.d(TAG, "Created subtitle configuration: " + trackId + " - " + label + " (" + track.getType() + ")"); -+ DebugLog.d(TAG, -+ "Created subtitle configuration: " + trackId + " - " + label + " (" + track.getType() + ")"); - trackIndex++; - } catch (Exception e) { -- DebugLog.e(TAG, "Error creating SubtitleConfiguration for URI " + track.getUri() + ": " + e.getMessage()); -+ DebugLog.e(TAG, -+ "Error creating SubtitleConfiguration for URI " + track.getUri() + ": " + e.getMessage()); - } - } - -@@ -1220,7 +1247,7 @@ public class ReactExoplayerView extends FrameLayout implements - - private void releasePlayer() { - if (player != null) { -- if(playbackServiceBinder != null) { -+ if (playbackServiceBinder != null) { - playbackServiceBinder.getService().unregisterPlayer(player); - themedReactContext.unbindService(playbackServiceConnection); - } -@@ -1276,7 +1303,8 @@ public class ReactExoplayerView extends FrameLayout implements - case AudioManager.AUDIOFOCUS_LOSS: - view.hasAudioFocus = false; - view.eventEmitter.onAudioFocusChanged.invoke(false); -- // FIXME this pause can cause issue if content doesn't have pause capability (can happen on live channel) -+ // FIXME this pause can cause issue if content doesn't have pause capability -+ // (can happen on live channel) - if (activity != null) { - activity.runOnUiThread(view::pausePlayback); - } -@@ -1297,16 +1325,12 @@ public class ReactExoplayerView extends FrameLayout implements - if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { - // Lower the volume - if (!view.muted) { -- activity.runOnUiThread(() -> -- view.player.setVolume(view.audioVolume * 0.8f) -- ); -+ activity.runOnUiThread(() -> view.player.setVolume(view.audioVolume * 0.8f)); - } - } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - // Raise it back to normal - if (!view.muted) { -- activity.runOnUiThread(() -> -- view.player.setVolume(view.audioVolume * 1) -- ); -+ activity.runOnUiThread(() -> view.player.setVolume(view.audioVolume * 1)); - } - } - } -@@ -1379,7 +1403,8 @@ public class ReactExoplayerView extends FrameLayout implements - /** - * Returns a new DataSource factory. - * -- * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new -+ * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener -+ * to the new - * DataSource factory. - * @return A new DataSource factory. - */ -@@ -1391,12 +1416,14 @@ public class ReactExoplayerView extends FrameLayout implements - /** - * Returns a new HttpDataSource factory. - * -- * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new -- * DataSource factory. -+ * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener -+ * to the new -+ * DataSource factory. - * @return A new HttpDataSource factory. - */ - private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) { -- return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext, useBandwidthMeter ? bandwidthMeter : null, source.getHeaders()); -+ return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext, -+ useBandwidthMeter ? bandwidthMeter : null, source.getHeaders()); - } - - // AudioBecomingNoisyListener implementation -@@ -1413,11 +1440,13 @@ public class ReactExoplayerView extends FrameLayout implements - - @Override - public void onEvents(@NonNull Player player, Player.Events events) { -- if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) { -+ if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) -+ || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) { - int playbackState = player.getPlaybackState(); - boolean playWhenReady = player.getPlayWhenReady(); - String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState="; -- eventEmitter.onPlaybackRateChange.invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f); -+ eventEmitter.onPlaybackRateChange -+ .invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f); - switch (playbackState) { - case Player.STATE_IDLE: - text += "idle"; -@@ -1474,9 +1503,11 @@ public class ReactExoplayerView extends FrameLayout implements - } - - /** -- * The progress message handler will duplicate recursions of the onProgressMessage handler -- * on change of player state from any state to STATE_READY with playWhenReady is true (when -- * the video is not paused). This clears all existing messages. -+ * The progress message handler will duplicate recursions of the -+ * onProgressMessage handler -+ * on change of player state from any state to STATE_READY with playWhenReady is -+ * true (when -+ * the video is not paused). This clears all existing messages. - */ - private void clearProgressMessageHandler() { - progressHandler.removeMessages(SHOW_PROGRESS); -@@ -1495,7 +1526,8 @@ public class ReactExoplayerView extends FrameLayout implements - setSelectedTextTrack(textTrackType, textTrackValue); - } - Format videoFormat = player.getVideoFormat(); -- boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270); -+ boolean isRotatedContent = videoFormat != null -+ && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270); - int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0; - int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0; - String trackId = videoFormat != null ? videoFormat.id : null; -@@ -1504,18 +1536,19 @@ public class ReactExoplayerView extends FrameLayout implements - long duration = player.getDuration(); - long currentPosition = player.getCurrentPosition(); - ArrayList audioTracks = getAudioTrackInfo(); -- ArrayList textTracks = getTextTrackInfo(); -+ ArrayList textTracks = getTextTrackInfo(); - - if (source.getContentStartTime() != -1) { - ExecutorService es = Executors.newSingleThreadExecutor(); - es.execute(() -> { -- // To prevent ANRs caused by getVideoTrackInfo we run this on a different thread and notify the player only when we're done -+ // To prevent ANRs caused by getVideoTrackInfo we run this on a different thread -+ // and notify the player only when we're done - ArrayList videoTracks = getVideoTrackInfoFromManifest(); - if (videoTracks != null) { - isUsingContentResolution = true; - } - eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height, -- audioTracks, textTracks, videoTracks, trackId ); -+ audioTracks, textTracks, videoTracks, trackId); - - updateSubtitleButtonVisibility(); - }); -@@ -1533,9 +1566,9 @@ public class ReactExoplayerView extends FrameLayout implements - } - - private static boolean isTrackSelected(TrackSelection selection, TrackGroup group, -- int trackIndex){ -+ int trackIndex) { - return selection != null && selection.getTrackGroup() == group -- && selection.indexOf( trackIndex ) != C.INDEX_UNSET; -+ && selection.indexOf(trackIndex) != C.INDEX_UNSET; - } - - private ArrayList getAudioTrackInfo() { -@@ -1553,7 +1586,6 @@ public class ReactExoplayerView extends FrameLayout implements - TrackSelectionArray selectionArray = player.getCurrentTrackSelections(); - TrackSelection selection = selectionArray.get(C.TRACK_TYPE_AUDIO); - -- - for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) { - TrackGroup group = groups.get(groupIndex); - Format format = group.getFormat(0); -@@ -1579,7 +1611,8 @@ public class ReactExoplayerView extends FrameLayout implements - videoTrack.setHeight(format.height == Format.NO_VALUE ? 0 : format.height); - videoTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); - videoTrack.setRotation(format.rotationDegrees); -- if (format.codecs != null) videoTrack.setCodecs(format.codecs); -+ if (format.codecs != null) -+ videoTrack.setCodecs(format.codecs); - videoTrack.setTrackId(format.id == null ? String.valueOf(trackIndex) : format.id); - videoTrack.setIndex(trackIndex); - return videoTrack; -@@ -1616,7 +1649,8 @@ public class ReactExoplayerView extends FrameLayout implements - return this.getVideoTrackInfoFromManifest(0); - } - -- // We need retry count to in case where minefest request fails from poor network conditions -+ // We need retry count to in case where minefest request fails from poor network -+ // conditions - @WorkerThread - private ArrayList getVideoTrackInfoFromManifest(int retryCount) { - ExecutorService es = Executors.newSingleThreadExecutor(); -@@ -1631,18 +1665,20 @@ public class ReactExoplayerView extends FrameLayout implements - - public ArrayList call() { - ArrayList videoTracks = new ArrayList<>(); -- try { -+ try { - DashManifest manifest = DashUtil.loadManifest(this.ds, this.uri); - int periodCount = manifest.getPeriodCount(); - for (int i = 0; i < periodCount; i++) { - Period period = manifest.getPeriod(i); -- for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets.size(); adaptationIndex++) { -+ for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets -+ .size(); adaptationIndex++) { - AdaptationSet adaptation = period.adaptationSets.get(adaptationIndex); - if (adaptation.type != C.TRACK_TYPE_VIDEO) { - continue; - } - boolean hasFoundContentPeriod = false; -- for (int representationIndex = 0; representationIndex < adaptation.representations.size(); representationIndex++) { -+ for (int representationIndex = 0; representationIndex < adaptation.representations -+ .size(); representationIndex++) { - Representation representation = adaptation.representations.get(representationIndex); - Format format = representation.format; - if (isFormatSupported(format)) { -@@ -1650,7 +1686,8 @@ public class ReactExoplayerView extends FrameLayout implements - break; - } - hasFoundContentPeriod = true; -- VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format, representationIndex); -+ VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format, -+ representationIndex); - videoTracks.add(videoTrack); - } - } -@@ -1680,12 +1717,16 @@ public class ReactExoplayerView extends FrameLayout implements - return null; - } - -- private Track exoplayerTrackToGenericTrack(Format format, int trackIndex, TrackSelection selection, TrackGroup group) { -+ private Track exoplayerTrackToGenericTrack(Format format, int trackIndex, TrackSelection selection, -+ TrackGroup group) { - Track track = new Track(); - track.setIndex(trackIndex); -- if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType); -- if (format.language != null) track.setLanguage(format.language); -- if (format.label != null) track.setTitle(format.label); -+ if (format.sampleMimeType != null) -+ track.setMimeType(format.sampleMimeType); -+ if (format.language != null) -+ track.setLanguage(format.language); -+ if (format.label != null) -+ track.setTitle(format.label); - track.setSelected(isTrackSelected(selection, group, trackIndex)); - return track; - } -@@ -1755,7 +1796,8 @@ public class ReactExoplayerView extends FrameLayout implements + +@@ -1753,7 +1758,11 @@ public class ReactExoplayerView extends FrameLayout implements + Track track = new Track(); + track.setIndex(groupIndex); track.setLanguage(format.language != null ? format.language : "unknown"); - track.setTitle(format.label != null ? format.label : "Track " + (groupIndex + 1)); +- track.setTitle(format.label != null ? format.label : "Track " + (groupIndex + 1)); ++ String baseTitle = format.label != null ? format.label : ""; ++ if (format.channelCount != Format.NO_VALUE && format.channelCount > 0) { ++ baseTitle = baseTitle + "|ch:" + format.channelCount; ++ } ++ track.setTitle(baseTitle); track.setSelected(false); // Don't report selection status - let PlayerView handle it -- if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType); -+ if (format.sampleMimeType != null) -+ track.setMimeType(format.sampleMimeType); + if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType); track.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); - - tracks.add(track); -@@ -1786,8 +1828,10 @@ public class ReactExoplayerView extends FrameLayout implements - - Track textTrack = new Track(); - textTrack.setIndex(textTracks.size()); -- if (format.sampleMimeType != null) textTrack.setMimeType(format.sampleMimeType); -- if (format.language != null) textTrack.setLanguage(format.language); -+ if (format.sampleMimeType != null) -+ textTrack.setMimeType(format.sampleMimeType); -+ if (format.language != null) -+ textTrack.setLanguage(format.language); - - boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-"); - -@@ -1821,28 +1865,34 @@ public class ReactExoplayerView extends FrameLayout implements - } - - @Override -- public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) { -+ public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, -+ @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) { - if (reason == Player.DISCONTINUITY_REASON_SEEK) { - isSeeking = true; - seekPosition = newPosition.positionMs; - if (isUsingContentResolution) { -- // We need to update the selected track to make sure that it still matches user selection if track list has changed in this period -+ // We need to update the selected track to make sure that it still matches user -+ // selection if track list has changed in this period - setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); - } - } - - if (playerNeedsSource) { -- // This will only occur if the user has performed a seek whilst in the error state. Update the -- // resume position so that if the user then retries, playback will resume from the position to -+ // This will only occur if the user has performed a seek whilst in the error -+ // state. Update the -+ // resume position so that if the user then retries, playback will resume from -+ // the position to - // which they seeked. - updateResumePosition(); - } - if (isUsingContentResolution) { -- // Discontinuity events might have a different track list so we update the selected track -+ // Discontinuity events might have a different track list so we update the -+ // selected track - setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); - selectTrackWhenReady = true; - } -- // When repeat is turned on, reaching the end of the video will not cause a state change -+ // When repeat is turned on, reaching the end of the video will not cause a -+ // state change - // so we need to explicitly detect it. - if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION - && player.getRepeatMode() == Player.REPEAT_MODE_ONE) { -@@ -1890,15 +1940,17 @@ public class ReactExoplayerView extends FrameLayout implements - updateSubtitleButtonVisibility(); - } - -- - private boolean hasBuiltInTextTracks() { -- if (player == null || trackSelector == null) return false; -+ if (player == null || trackSelector == null) -+ return false; - - MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); -- if (info == null) return false; -+ if (info == null) -+ return false; - - int textRendererIndex = getTrackRendererIndex(C.TRACK_TYPE_TEXT); -- if (textRendererIndex == C.INDEX_UNSET) return false; -+ if (textRendererIndex == C.INDEX_UNSET) -+ return false; - - TrackGroupArray groups = info.getTrackGroups(textRendererIndex); - -@@ -1918,11 +1970,12 @@ public class ReactExoplayerView extends FrameLayout implements - } - - private void updateSubtitleButtonVisibility() { -- if (exoPlayerView == null) return; -+ if (exoPlayerView == null) -+ return; - - boolean hasTextTracks = (source.getSideLoadedTextTracks() != null && -- !source.getSideLoadedTextTracks().getTracks().isEmpty()) || -- hasBuiltInTextTracks(); -+ !source.getSideLoadedTextTracks().getTracks().isEmpty()) || -+ hasBuiltInTextTracks(); - - exoPlayerView.setShowSubtitleButton(hasTextTracks); - } -@@ -1942,7 +1995,8 @@ public class ReactExoplayerView extends FrameLayout implements - if (isPlaying && isSeeking) { - eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition); - } -- PictureInPictureUtil.applyPlayingStatus(themedReactContext, pictureInPictureParamsBuilder, pictureInPictureReceiver, !isPlaying); -+ PictureInPictureUtil.applyPlayingStatus(themedReactContext, pictureInPictureParamsBuilder, -+ pictureInPictureReceiver, !isPlaying); - eventEmitter.onVideoPlaybackStateChanged.invoke(isPlaying, isSeeking); - - if (isPlaying) { -@@ -1954,14 +2008,15 @@ public class ReactExoplayerView extends FrameLayout implements - public void onPlayerError(@NonNull PlaybackException e) { - String errorString = "ExoPlaybackException: " + PlaybackException.getErrorCodeName(e.errorCode); - String errorCode = "2" + e.errorCode; -- switch(e.errorCode) { -+ switch (e.errorCode) { - case PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED: - case PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED: - case PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED: - case PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR: - case PlaybackException.ERROR_CODE_DRM_UNSPECIFIED: - if (!hasDrmFailed) { -- // When DRM fails to reach the app level certificate server it will fail with a source error so we assume that it is DRM related and try one more time -+ // When DRM fails to reach the app level certificate server it will fail with a -+ // source error so we assume that it is DRM related and try one more time - hasDrmFailed = true; - playerNeedsSource = true; - updateResumePosition(); -@@ -2043,14 +2098,16 @@ public class ReactExoplayerView extends FrameLayout implements - boolean isSourceEqual = source.isEquals(this.source); - hasDrmFailed = false; - this.source = source; -- final DataSource.Factory tmpMediaDataSourceFactory = -- DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter, -- source.getHeaders()); -+ final DataSource.Factory tmpMediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory( -+ this.themedReactContext, bandwidthMeter, -+ source.getHeaders()); - - @Nullable -- final DataSource.Factory overriddenMediaDataSourceFactory = ReactNativeVideoManager.Companion.getInstance().overrideMediaDataSourceFactory(source, tmpMediaDataSourceFactory); -+ final DataSource.Factory overriddenMediaDataSourceFactory = ReactNativeVideoManager.Companion.getInstance() -+ .overrideMediaDataSourceFactory(source, tmpMediaDataSourceFactory); - -- this.mediaDataSourceFactory = Objects.requireNonNullElse(overriddenMediaDataSourceFactory, tmpMediaDataSourceFactory); -+ this.mediaDataSourceFactory = Objects.requireNonNullElse(overriddenMediaDataSourceFactory, -+ tmpMediaDataSourceFactory); - - if (source.getCmcdProps() != null) { - CMCDConfig cmcdConfig = new CMCDConfig(source.getCmcdProps()); -@@ -2069,6 +2126,7 @@ public class ReactExoplayerView extends FrameLayout implements - clearSrc(); - } - } -+ - public void clearSrc() { - if (source.getUri() != null) { - if (player != null) { -@@ -2117,7 +2175,8 @@ public class ReactExoplayerView extends FrameLayout implements - } - - public void disableTrack(int rendererIndex) { -- if (trackSelector == null) return; -+ if (trackSelector == null) -+ return; - - DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters() - .buildUpon() -@@ -2127,7 +2186,8 @@ public class ReactExoplayerView extends FrameLayout implements +@@ -2127,7 +2136,8 @@ public class ReactExoplayerView extends FrameLayout implements } private void selectTextTrackInternal(String type, String value) { - if (player == null || trackSelector == null) return; + if (player == null || trackSelector == null) + return; - + DebugLog.d(TAG, "selectTextTrackInternal: type=" + type + ", value=" + value); - -@@ -2147,6 +2207,11 @@ public class ReactExoplayerView extends FrameLayout implements + +@@ -2146,6 +2156,10 @@ public class ReactExoplayerView extends FrameLayout implements + if (textRendererIndex != C.INDEX_UNSET) { TrackGroupArray groups = info.getTrackGroups(textRendererIndex); boolean trackFound = false; - + // react-native-video uses a flattened `textTracks` list on the JS side. + // For HLS/DASH, each TrackGroup often contains a single track at index 0, + // so comparing against `trackIndex` alone makes only the first subtitle selectable. + int flattenedIndex = 0; -+ + for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup group = groups.get(groupIndex); - for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { -@@ -2159,25 +2224,28 @@ public class ReactExoplayerView extends FrameLayout implements +@@ -2159,11 +2173,13 @@ public class ReactExoplayerView extends FrameLayout implements isMatch = true; } else if ("index".equals(type)) { int targetIndex = ReactBridgeUtils.safeParseInt(value, -1); @@ -1327,313 +812,31 @@ index e16ac96..54221ef 100644 } + flattenedIndex++; -+ ++ if (isMatch) { - TrackSelectionOverride override = new TrackSelectionOverride(group, -- java.util.Arrays.asList(trackIndex)); -+ java.util.Arrays.asList(trackIndex)); - parametersBuilder.addOverride(override); - trackFound = true; - break; - } - } -- if (trackFound) break; -+ if (trackFound) -+ break; - } - - if (!trackFound) { - DebugLog.w(TAG, "Text track not found for type=" + type + ", value=" + value + -- ". Keeping current selection."); -+ ". Keeping current selection."); - } - } - } -@@ -2198,7 +2266,8 @@ public class ReactExoplayerView extends FrameLayout implements - } - - public void setSelectedTrack(int trackType, String type, String value) { -- if (player == null || trackSelector == null) return; -+ if (player == null || trackSelector == null) -+ return; - - if (controls) { - return; -@@ -2272,9 +2341,11 @@ public class ReactExoplayerView extends FrameLayout implements - usingExactMatch = true; - break; - } else if (isUsingContentResolution) { -- // When using content resolution rather than ads, we need to try and find the closest match if there is no exact match -+ // When using content resolution rather than ads, we need to try and find the -+ // closest match if there is no exact match - if (closestFormat != null) { -- if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) { -+ if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) -+ && format.height < height) { - // Higher quality match - closestFormat = format; - closestTrackIndex = j; -@@ -2285,7 +2356,8 @@ public class ReactExoplayerView extends FrameLayout implements - } - } - } -- // This is a fallback if the new period contains only higher resolutions than the user has selected -+ // This is a fallback if the new period contains only higher resolutions than -+ // the user has selected - if (closestFormat == null && isUsingContentResolution && !usingExactMatch) { - // No close match found - so we pick the lowest quality - int minHeight = Integer.MAX_VALUE; -@@ -2308,8 +2380,8 @@ public class ReactExoplayerView extends FrameLayout implements - } - } else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default - // Use system settings if possible -- CaptioningManager captioningManager -- = (CaptioningManager)themedReactContext.getSystemService(Context.CAPTIONING_SERVICE); -+ CaptioningManager captioningManager = (CaptioningManager) themedReactContext -+ .getSystemService(Context.CAPTIONING_SERVICE); - if (captioningManager != null && captioningManager.isEnabled()) { - groupIndex = getGroupIndexForDefaultLocale(groups); - } -@@ -2338,7 +2410,7 @@ public class ReactExoplayerView extends FrameLayout implements - // With only one tracks we can't remove any tracks so attempt to play it anyway - tracks = allTracks; - } else { -- tracks = new ArrayList<>(supportedFormatLength + 1); -+ tracks = new ArrayList<>(supportedFormatLength + 1); - for (int k = 0; k < allTracks.size(); k++) { - Format format = group.getFormat(k); - if (isFormatSupported(format)) { -@@ -2364,7 +2436,8 @@ public class ReactExoplayerView extends FrameLayout implements - .setRendererDisabled(rendererIndex, false); - - // Clear existing overrides for this track type to avoid conflicts -- // But be careful with audio tracks - don't clear unless explicitly selecting a different track -+ // But be careful with audio tracks - don't clear unless explicitly selecting a -+ // different track - if (trackType != C.TRACK_TYPE_AUDIO || !type.equals("default")) { - selectionParameters.clearOverridesOfType(selectionOverride.getType()); - } -@@ -2380,7 +2453,7 @@ public class ReactExoplayerView extends FrameLayout implements - selectionParameters.setForceHighestSupportedBitrate(false); - selectionParameters.setForceLowestBitrate(false); - DebugLog.d(TAG, "Audio track selection: group=" + groupIndex + ", tracks=" + tracks + -- ", override=" + selectionOverride); -+ ", override=" + selectionOverride); - } - - trackSelector.setParameters(selectionParameters.build()); -@@ -2411,7 +2484,7 @@ public class ReactExoplayerView extends FrameLayout implements - } - - private int getGroupIndexForDefaultLocale(TrackGroupArray groups) { -- if (groups.length == 0){ -+ if (groups.length == 0) { - return C.INDEX_UNSET; - } - -@@ -2432,7 +2505,8 @@ public class ReactExoplayerView extends FrameLayout implements - public void setSelectedVideoTrack(String type, String value) { - videoTrackType = type; - videoTrackValue = value; -- if (!loadVideoStarted) setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); -+ if (!loadVideoStarted) -+ setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); - } - - public void setSelectedAudioTrack(String type, String value) { -@@ -2463,9 +2537,11 @@ public class ReactExoplayerView extends FrameLayout implements - } - - public void setEnterPictureInPictureOnLeave(boolean enterPictureInPictureOnLeave) { -- this.enterPictureInPictureOnLeave = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && enterPictureInPictureOnLeave; -+ this.enterPictureInPictureOnLeave = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -+ && enterPictureInPictureOnLeave; - if (player != null) { -- PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, this.enterPictureInPictureOnLeave); -+ PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, -+ this.enterPictureInPictureOnLeave); - } - } - -@@ -2473,12 +2549,14 @@ public class ReactExoplayerView extends FrameLayout implements - eventEmitter.onPictureInPictureStatusChanged.invoke(isInPictureInPicture); - - if (fullScreenPlayerView != null && fullScreenPlayerView.isShowing()) { -- if (isInPictureInPicture) fullScreenPlayerView.hideWithoutPlayer(); -+ if (isInPictureInPicture) -+ fullScreenPlayerView.hideWithoutPlayer(); - return; - } - - Activity currentActivity = themedReactContext.getCurrentActivity(); -- if (currentActivity == null) return; -+ if (currentActivity == null) -+ return; - - View decorView = currentActivity.getWindow().getDecorView(); - ViewGroup rootView = decorView.findViewById(android.R.id.content); -@@ -2488,7 +2566,7 @@ public class ReactExoplayerView extends FrameLayout implements - LayoutParams.MATCH_PARENT); - - if (isInPictureInPicture) { -- ViewGroup parent = (ViewGroup)exoPlayerView.getParent(); -+ ViewGroup parent = (ViewGroup) exoPlayerView.getParent(); - if (parent != null) { - parent.removeView(exoPlayerView); - } -@@ -2514,10 +2592,12 @@ public class ReactExoplayerView extends FrameLayout implements - public void enterPictureInPictureMode() { - PictureInPictureParams _pipParams = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { -- ArrayList actions = PictureInPictureUtil.getPictureInPictureActions(themedReactContext, isPaused, pictureInPictureReceiver); -+ ArrayList actions = PictureInPictureUtil.getPictureInPictureActions(themedReactContext, -+ isPaused, pictureInPictureReceiver); - pictureInPictureParamsBuilder.setActions(actions); - if (player.getPlaybackState() == Player.STATE_READY) { -- pictureInPictureParamsBuilder.setAspectRatio(PictureInPictureUtil.calcPictureInPictureAspectRatio(player)); -+ pictureInPictureParamsBuilder -+ .setAspectRatio(PictureInPictureUtil.calcPictureInPictureAspectRatio(player)); - } - _pipParams = pictureInPictureParamsBuilder.build(); - } -@@ -2526,13 +2606,15 @@ public class ReactExoplayerView extends FrameLayout implements - - public void exitPictureInPictureMode() { - Activity currentActivity = themedReactContext.getCurrentActivity(); -- if (currentActivity == null) return; -+ if (currentActivity == null) -+ return; - - View decorView = currentActivity.getWindow().getDecorView(); - ViewGroup rootView = decorView.findViewById(android.R.id.content); - - if (!rootViewChildrenOriginalVisibility.isEmpty()) { -- if (exoPlayerView.getParent().equals(rootView)) rootView.removeView(exoPlayerView); -+ if (exoPlayerView.getParent().equals(rootView)) -+ rootView.removeView(exoPlayerView); - for (int i = 0; i < rootView.getChildCount(); i++) { - rootView.getChildAt(i).setVisibility(rootViewChildrenOriginalVisibility.get(i)); - } -@@ -2630,7 +2712,7 @@ public class ReactExoplayerView extends FrameLayout implements - - if (playbackServiceConnection == null && showNotificationControls) { - setupPlaybackService(); -- } else if(!showNotificationControls && playbackServiceConnection != null) { -+ } else if (!showNotificationControls && playbackServiceConnection != null) { - cleanupPlaybackService(); - } - } -@@ -2659,12 +2741,13 @@ public class ReactExoplayerView extends FrameLayout implements - } - - if (isFullscreen) { -- fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, null, new OnBackPressedCallback(true) { -- @Override -- public void handleOnBackPressed() { -- setFullscreen(false); -- } -- }, controlsConfig); -+ fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, null, -+ new OnBackPressedCallback(true) { -+ @Override -+ public void handleOnBackPressed() { -+ setFullscreen(false); -+ } -+ }, controlsConfig); - eventEmitter.onVideoFullscreenPlayerWillPresent.invoke(); - if (fullScreenPlayerView != null) { - fullScreenPlayerView.show(); -@@ -2701,7 +2784,8 @@ public class ReactExoplayerView extends FrameLayout implements - } - - @Override -- public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, @NonNull Exception e) { -+ public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, -+ @NonNull Exception e) { - DebugLog.d("DRM Info", "onDrmSessionManagerError"); - eventEmitter.onVideoError.invoke("onDrmSessionManagerError", e, "3002"); - } -@@ -2719,7 +2803,7 @@ public class ReactExoplayerView extends FrameLayout implements - /** - * Handling controls prop - * -- * @param controls Controls prop, if true enable controls, if false disable them -+ * @param controls Controls prop, if true enable controls, if false disable them - */ - public void setControls(boolean controls) { - this.controls = controls; -@@ -2728,7 +2812,7 @@ public class ReactExoplayerView extends FrameLayout implements - // Additional configuration for proper touch handling - if (controls) { - exoPlayerView.setControllerAutoShow(true); -- exoPlayerView.setControllerHideOnTouch(true); // Show controls on touch, don't hide -+ exoPlayerView.setControllerHideOnTouch(true); // Show controls on touch, don't hide - exoPlayerView.setControllerShowTimeoutMs(5000); - } - } -@@ -2761,8 +2845,7 @@ public class ReactExoplayerView extends FrameLayout implements - Map errMap = Map.of( - "message", error.getMessage(), - "code", String.valueOf(error.getErrorCode()), -- "type", String.valueOf(error.getErrorType()) -- ); -+ "type", String.valueOf(error.getErrorType())); - eventEmitter.onReceiveAdEvent.invoke("ERROR", errMap); - - handleDaiBackupStream(); -@@ -2796,10 +2879,10 @@ public class ReactExoplayerView extends FrameLayout implements - * @return The configured IMA server-side ad insertion AdsLoader - */ - private ImaServerSideAdInsertionMediaSource.AdsLoader createAdsLoader() { -- ImaServerSideAdInsertionMediaSource.AdsLoader.Builder adsLoaderBuilder = -- new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(getContext(), exoPlayerView.getPlayerView()) -- .setAdEventListener(this) -- .setAdErrorListener(this); -+ ImaServerSideAdInsertionMediaSource.AdsLoader.Builder adsLoaderBuilder = new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder( -+ getContext(), exoPlayerView.getPlayerView()) -+ .setAdEventListener(this) -+ .setAdErrorListener(this); - - return adsLoaderBuilder.build(); - } -@@ -2815,8 +2898,8 @@ public class ReactExoplayerView extends FrameLayout implements - DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(getContext()); - DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory); - -- ImaServerSideAdInsertionMediaSource.Factory adsMediaSourceFactory = -- new ImaServerSideAdInsertionMediaSource.Factory(daiAdsLoader, mediaSourceFactory); -+ ImaServerSideAdInsertionMediaSource.Factory adsMediaSourceFactory = new ImaServerSideAdInsertionMediaSource.Factory( -+ daiAdsLoader, mediaSourceFactory); - - mediaSourceFactory.setServerSideAdInsertionMediaSourceFactory(adsMediaSourceFactory); - -@@ -2850,7 +2933,8 @@ public class ReactExoplayerView extends FrameLayout implements - /** - * Requests a DAI stream from Google IMA using the ExoPlayer IMA extension. - * -- * Builds an SSAI URI based on the provided parameters and sets it on the player. -+ * Builds an SSAI URI based on the provided parameters and sets it on the -+ * player. - * Supports both VOD (contentSourceId + videoId) and Live (assetKey) streams. - * - * @param runningSource The source containing DAI properties -@@ -2883,7 +2967,8 @@ public class ReactExoplayerView extends FrameLayout implements - .build() - .buildUpon(); - } else { -- throw new IllegalArgumentException("Either assetKey (for live) or contentSourceId+videoId (for VOD) must be provided"); -+ throw new IllegalArgumentException( -+ "Either assetKey (for live) or contentSourceId+videoId (for VOD) must be provided"); - } - - Map adTagParameters = adsProps.getAdTagParameters(); -@@ -2906,7 +2991,8 @@ public class ReactExoplayerView extends FrameLayout implements - /** - * Handles fallback to backup stream when DAI stream fails. - * -- * If a backup stream URI is available in the DAI properties, it cleans up DAI resources -+ * If a backup stream URI is available in the DAI properties, it cleans up DAI -+ * resources - * and switches to the backup stream. - * - * @return true if backup stream was successfully used, false otherwise + TrackSelectionOverride override = new TrackSelectionOverride(group, + java.util.Arrays.asList(trackIndex)); +diff --git a/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_surface.xml b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_surface.xml +new file mode 100644 +index 0000000..4ea3c30 +--- /dev/null ++++ b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_surface.xml +@@ -0,0 +1,6 @@ ++ ++ +diff --git a/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_texture.xml b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_texture.xml +new file mode 100644 +index 0000000..53c1909 +--- /dev/null ++++ b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_texture.xml +@@ -0,0 +1,6 @@ ++ ++ diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 96778a91..cf5416c9 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -1,2355 +1,191 @@ -import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import { - View, - Text, - StyleSheet, - TouchableOpacity, - Dimensions, - AppState, - AppStateStatus, - ActivityIndicator, - Platform -} from 'react-native'; -import { FlatList } from 'react-native'; -import { useTranslation } from 'react-i18next'; -import Animated, { FadeIn, Layout } from 'react-native-reanimated'; -import BottomSheet, { BottomSheetModal, BottomSheetView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'; -import { Ionicons } from '@expo/vector-icons'; -import { BlurView } from 'expo-blur'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useNavigation, useFocusEffect } from '@react-navigation/native'; -import { NavigationProp } from '@react-navigation/native'; -import { RootStackParamList } from '../../navigation/AppNavigator'; -import { StreamingContent, catalogService } from '../../services/catalogService'; -import { LinearGradient } from 'expo-linear-gradient'; -import FastImage from '@d11/react-native-fast-image'; -import { useTheme } from '../../contexts/ThemeContext'; -import { storageService } from '../../services/storageService'; -import { logger } from '../../utils/logger'; +import React, { useCallback, useImperativeHandle, useRef, useState } from 'react'; +import { FlatList, Text, View } from 'react-native'; +import Animated, { FadeIn } from 'react-native-reanimated'; +import { BottomSheetModal } from '@gorhom/bottom-sheet'; import * as Haptics from 'expo-haptics'; -import { TraktService } from '../../services/traktService'; -import { SimklService } from '../../services/simklService'; -import { stremioService } from '../../services/stremioService'; -import { streamCacheService } from '../../services/streamCacheService'; -import { useSettings } from '../../hooks/useSettings'; +import { NavigationProp, useNavigation } from '@react-navigation/native'; +import { useTranslation } from 'react-i18next'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { useTheme } from '../../contexts/ThemeContext'; import { useBottomSheetBackHandler } from '../../hooks/useBottomSheetBackHandler'; -import { watchedService } from '../../services/watchedService'; +import { useSettings } from '../../hooks/useSettings'; +import { RootStackParamList } from '../../navigation/AppNavigator'; +import { ContinueWatchingActionSheet } from './continueWatching/ContinueWatchingActionSheet'; +import { ContinueWatchingPosterCard } from './continueWatching/ContinueWatchingPosterCard'; +import { ContinueWatchingWideCard } from './continueWatching/ContinueWatchingWideCard'; +import { styles } from './continueWatching/styles'; +import { ContinueWatchingItem, ContinueWatchingRef } from './continueWatching/types'; +import { useContinueWatchingData } from './continueWatching/useContinueWatchingData'; +import { useContinueWatchingLayout } from './continueWatching/useContinueWatchingLayout'; +import { useContinueWatchingNavigation } from './continueWatching/useContinueWatchingNavigation'; -// Define interface for continue watching items -interface ContinueWatchingItem extends StreamingContent { - progress: number; - lastUpdated: number; - season?: number; - episode?: number; - episodeTitle?: string; - addonId?: string; - addonPoster?: string; - addonName?: string; - addonDescription?: string; - traktPlaybackId?: number; // Trakt playback ID for removal -} - -// Define the ref interface -interface ContinueWatchingRef { - refresh: () => Promise; -} - -// Enhanced responsive breakpoints for Continue Watching section -const BREAKPOINTS = { - phone: 0, - tablet: 768, - largeTablet: 1024, - tv: 1440, -}; - -// Dynamic poster calculation based on screen width for Continue Watching section -const calculatePosterLayout = (screenWidth: number) => { - const MIN_POSTER_WIDTH = 120; // Slightly larger for continue watching items - const MAX_POSTER_WIDTH = 160; // Maximum poster width for this section - const HORIZONTAL_PADDING = 40; // Total horizontal padding/margins - - // Calculate how many posters can fit (fewer items for continue watching) - const availableWidth = screenWidth - HORIZONTAL_PADDING; - const maxColumns = Math.floor(availableWidth / MIN_POSTER_WIDTH); - - // Limit to reasonable number of columns (2-5 for continue watching) - const numColumns = Math.min(Math.max(maxColumns, 2), 5); - - // Calculate actual poster width - const posterWidth = Math.min(availableWidth / numColumns, MAX_POSTER_WIDTH); - - return { - numColumns, - posterWidth, - spacing: 12 // Space between posters - }; -}; - -const { width } = Dimensions.get('window'); -const posterLayout = calculatePosterLayout(width); -const POSTER_WIDTH = posterLayout.posterWidth; - -// Allow any known id formats (imdb 'tt...', kitsu 'kitsu:...', tmdb 'tmdb:...', or others) -const isSupportedId = (id: string): boolean => { - return typeof id === 'string' && id.length > 0; -}; - -// Function to check if an episode has been released -const isEpisodeReleased = (video: any): boolean => { - if (!video.released) return false; - - try { - const releaseDate = new Date(video.released); - const now = new Date(); - return releaseDate <= now; - } catch (error) { - // If we can't parse the date, assume it's not released - return false; - } -}; - -// Create a proper imperative handle with React.forwardRef and updated type -const ContinueWatchingSection = React.forwardRef((props, ref) => { +const ContinueWatchingSection = React.forwardRef((_, ref) => { const { t } = useTranslation(); const navigation = useNavigation>(); const { currentTheme } = useTheme(); const { settings } = useSettings(); const insets = useSafeAreaInsets(); - const [continueWatchingItems, setContinueWatchingItems] = useState([]); - const [loading, setLoading] = useState(true); - const appState = useRef(AppState.currentState); - const refreshTimerRef = useRef(null); - const pendingRefreshRef = useRef(false); - const [deletingItemId, setDeletingItemId] = useState(null); - const longPressTimeoutRef = useRef(null); - - // Bottom sheet for item actions - const actionSheetRef = useRef(null); const { onChange, onDismiss } = useBottomSheetBackHandler(); + + const actionSheetRef = useRef(null); const [selectedItem, setSelectedItem] = useState(null); - // Enhanced responsive sizing for tablets and TV screens - const [dimensions, setDimensions] = useState(Dimensions.get('window')); - const deviceWidth = dimensions.width; - const deviceHeight = dimensions.height; + const { + continueWatchingItems, + deletingItemId, + refresh, + removeItem, + } = useContinueWatchingData(); + + const { + isTablet, + isLargeTablet, + isTV, + horizontalPadding, + itemSpacing, + computedItemWidth, + computedItemHeight, + computedPosterWidth, + computedPosterHeight, + } = useContinueWatchingLayout(); + + const { handleContentPress, navigateToMetadata } = useContinueWatchingNavigation({ + navigation, + settings, + }); + + useImperativeHandle(ref, () => ({ + refresh, + }), [refresh]); - // Listen for dimension changes (orientation changes) - useEffect(() => { - const subscription = Dimensions.addEventListener('change', ({ window }) => { - setDimensions(window); - }); - - return () => subscription?.remove(); - }, []); - - // Determine device type based on width - const getDeviceType = useCallback(() => { - if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; - if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; - if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; - return 'phone'; - }, [deviceWidth]); - - const deviceType = getDeviceType(); - const isTablet = deviceType === 'tablet'; - const isLargeTablet = deviceType === 'largeTablet'; - const isTV = deviceType === 'tv'; - const isLargeScreen = isTablet || isLargeTablet || isTV; - - // Enhanced responsive sizing for continue watching items - const computedItemWidth = useMemo(() => { - switch (deviceType) { - case 'tv': - return 400; // Larger items for TV - case 'largeTablet': - return 350; // Medium-large items for large tablets - case 'tablet': - return 320; // Medium items for tablets - default: - return 280; // Original phone size - } - }, [deviceType]); - - const computedItemHeight = useMemo(() => { - switch (deviceType) { - case 'tv': - return 160; // Taller items for TV - case 'largeTablet': - return 140; // Medium-tall items for large tablets - case 'tablet': - return 130; // Medium items for tablets - default: - return 120; // Original phone height - } - }, [deviceType]); - - // Enhanced spacing and padding - const horizontalPadding = useMemo(() => { - switch (deviceType) { - case 'tv': - return 32; - case 'largeTablet': - return 28; - case 'tablet': - return 24; - default: - return 16; // phone - } - }, [deviceType]); - - const itemSpacing = useMemo(() => { - switch (deviceType) { - case 'tv': - return 20; - case 'largeTablet': - return 18; - case 'tablet': - return 16; - default: - return 16; // phone - } - }, [deviceType]); - - - - // Use a ref to track if a background refresh is in progress to avoid state updates - const isRefreshingRef = useRef(false); - - // Track recently removed items to prevent immediate re-addition - const recentlyRemovedRef = useRef>(new Set()); - const REMOVAL_IGNORE_DURATION = 10000; // 10 seconds - - // Track last Trakt sync to prevent excessive API calls - const lastTraktSyncRef = useRef(0); - const TRAKT_SYNC_COOLDOWN = 0; // disabled (always fetch Trakt playback) - - // Track last Simkl sync to prevent excessive API calls - const lastSimklSyncRef = useRef(0); - const SIMKL_SYNC_COOLDOWN = 0; // disabled (always fetch Simkl playback) - - // Track last Trakt reconcile per item (local -> Trakt catch-up) - const lastTraktReconcileRef = useRef>(new Map()); - const TRAKT_RECONCILE_COOLDOWN = 0; // 2 minutes between reconcile attempts per item - - // Debug: avoid logging the same order repeatedly - const lastOrderLogSigRef = useRef(''); - - // Cache for metadata to avoid redundant API calls - const metadataCache = useRef>({}); - const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes - - const getCachedMetadata = useCallback(async (type: string, id: string, addonId?: string) => { - const cacheKey = `${type}:${id}:${addonId || 'default'}`; - const cached = metadataCache.current[cacheKey]; - const now = Date.now(); - - if (cached && (now - cached.timestamp) < CACHE_DURATION) { - return cached; - } - - try { - const shouldFetchMeta = await stremioService.isValidContentId(type, id); - - const [metadata, basicContent, addonSpecificMeta] = await Promise.all([ - shouldFetchMeta ? stremioService.getMetaDetails(type, id) : Promise.resolve(null), - catalogService.getBasicContentDetails(type, id), - - addonId - ? stremioService.getMetaDetails(type, id, addonId).catch(() => null) - : Promise.resolve(null) - ]); - - const preferredAddonMeta = addonSpecificMeta || metadata; - - - const finalContent = basicContent ? { - ...basicContent, - ...(preferredAddonMeta?.name && { name: preferredAddonMeta.name }), - ...(preferredAddonMeta?.poster && { poster: preferredAddonMeta.poster }), - ...(preferredAddonMeta?.description && { description: preferredAddonMeta.description }), - } : null; - - - if (finalContent) { - const result = { - metadata, - basicContent: finalContent, - addonContent: preferredAddonMeta, - timestamp: now - }; - - metadataCache.current[cacheKey] = result; - return result; - } - return null; - } catch (error: any) { - return null; - } - }, []); - - - const findNextEpisode = useCallback(( - currentSeason: number, - currentEpisode: number, - videos: any[], - watchedSet?: Set, - showId?: string, - localWatchedMap?: Map, - baseTimestamp: number = 0 - ): { video: any; lastWatched: number } | null => { - if (!videos || !Array.isArray(videos)) return null; - - const sortedVideos = [...videos].sort((a, b) => { - if (a.season !== b.season) return a.season - b.season; - return a.episode - b.episode; - }); - - let latestWatchedTimestamp = baseTimestamp; - - if (localWatchedMap && showId) { - const cleanShowId = showId.startsWith('tt') ? showId : `tt${showId}`; - for (const video of sortedVideos) { - const sig1 = `${cleanShowId}:${video.season}:${video.episode}`; - const sig2 = `${showId}:${video.season}:${video.episode}`; - const t1 = localWatchedMap.get(sig1) || 0; - const t2 = localWatchedMap.get(sig2) || 0; - latestWatchedTimestamp = Math.max(latestWatchedTimestamp, t1, t2); - } - } - - const isAlreadyWatched = (season: number, episode: number): boolean => { - if (!showId) return false; - const cleanShowId = showId.startsWith('tt') ? showId : `tt${showId}`; - const sig1 = `${cleanShowId}:${season}:${episode}`; - const sig2 = `${showId}:${season}:${episode}`; - if (watchedSet && (watchedSet.has(sig1) || watchedSet.has(sig2))) return true; - if (localWatchedMap && (localWatchedMap.has(sig1) || localWatchedMap.has(sig2))) return true; - return false; - }; - - for (const video of sortedVideos) { - if (video.season < currentSeason) continue; - if (video.season === currentSeason && video.episode <= currentEpisode) continue; - - if (isAlreadyWatched(video.season, video.episode)) continue; - - if (isEpisodeReleased(video)) { - return { video, lastWatched: latestWatchedTimestamp }; - } - } - - return null; - }, []); - - - // Modified loadContinueWatching to render incrementally - const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => { - if (isRefreshingRef.current) { - pendingRefreshRef.current = true; - return; - } - - if (!isBackgroundRefresh) { - setLoading(true); - } - isRefreshingRef.current = true; - - logger.log(`[CW] loadContinueWatching start (background=${isBackgroundRefresh})`); - - const shouldPreferCandidate = (candidate: ContinueWatchingItem, existing: ContinueWatchingItem): boolean => { - const candidateUpdated = candidate.lastUpdated ?? 0; - const existingUpdated = existing.lastUpdated ?? 0; - const candidateProgress = candidate.progress ?? 0; - const existingProgress = existing.progress ?? 0; - - const sameEpisode = - candidate.type === 'movie' || - ( - candidate.type === 'series' && - existing.type === 'series' && - candidate.season !== undefined && - candidate.episode !== undefined && - existing.season !== undefined && - existing.episode !== undefined && - candidate.season === existing.season && - candidate.episode === existing.episode - ); - - // If it's the same episode/movie, prefer the higher progress (local often leads Trakt) - if (sameEpisode) { - if (candidateProgress > existingProgress + 0.5) return true; - if (existingProgress > candidateProgress + 0.5) return false; - } - - // Otherwise, prefer the most recently watched item - if (candidateUpdated !== existingUpdated) return candidateUpdated > existingUpdated; - - // Final tiebreaker - return candidateProgress > existingProgress; - }; - - const compareCwItems = (a: ContinueWatchingItem, b: ContinueWatchingItem): number => { - // Sort purely by recency — most recently watched first. - // "Up Next" placeholders (progress=0) carry the timestamp of the last watched episode - // so they naturally bubble up next to the other recently-watched items. - return (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0); - }; - - type LocalProgressEntry = { - episodeId?: string; - season?: number; - episode?: number; - progressPercent: number; - lastUpdated: number; - currentTime: number; - duration: number; - }; - - const getIdVariants = (id: string): string[] => { - const variants = new Set(); - if (typeof id !== 'string' || id.length === 0) return []; - - variants.add(id); - - if (id.startsWith('tt')) { - variants.add(id.replace(/^tt/, '')); - } else { - // Only add a tt-variant when the id looks like a bare imdb numeric id. - if (/^\d+$/.test(id)) { - variants.add(`tt${id}`); - } - } - - return Array.from(variants); - }; - - const parseEpisodeId = (episodeId?: string): { season: number; episode: number } | null => { - if (!episodeId) return null; - - const match = episodeId.match(/s(\d+)e(\d+)/i); - if (match) { - const season = parseInt(match[1], 10); - const episode = parseInt(match[2], 10); - if (!isNaN(season) && !isNaN(episode)) return { season, episode }; - } - - const parts = episodeId.split(':'); - if (parts.length >= 3) { - const seasonNum = parseInt(parts[parts.length - 2], 10); - const episodeNum = parseInt(parts[parts.length - 1], 10); - if (!isNaN(seasonNum) && !isNaN(episodeNum)) return { season: seasonNum, episode: episodeNum }; - } - - return null; - }; - - // Helper to merge a batch of items into state (dedupe by type:id, keep newest) - const mergeBatchIntoState = async (batch: ContinueWatchingItem[]) => { - if (!batch || batch.length === 0) return; - - // 1. Filter items first (async checks) - do this BEFORE any state updates - const validItems: ContinueWatchingItem[] = []; - for (const it of batch) { - // For series, use episode-specific key - const key = it.type === 'series' && it.season && it.episode - ? `${it.type}:${it.id}:${it.season}:${it.episode}` - : `${it.type}:${it.id}`; - - // Skip recently removed items - if (recentlyRemovedRef.current.has(key)) { - continue; - } - - // Skip persistently removed items (episode-specific for series) - const removeId = it.type === 'series' && it.season && it.episode - ? `${it.id}:${it.season}:${it.episode}` - : it.id; - const isRemoved = await storageService.isContinueWatchingRemoved(removeId, it.type); - if (isRemoved) { - continue; - } - - validItems.push(it); - } - - if (validItems.length === 0) return; - - // 2. Single state update for the entire batch - setContinueWatchingItems((prev) => { - const map = new Map(); - // Add existing items - for (const it of prev) { - map.set(`${it.type}:${it.id}`, it); - } - - // Merge new valid items - for (const it of validItems) { - const key = `${it.type}:${it.id}`; - const existing = map.get(key); - // Prefer local when it is ahead; otherwise, prefer newer - if (!existing || shouldPreferCandidate(it, existing)) { - map.set(key, it); - } - } - - const merged = Array.from(map.values()); - merged.sort(compareCwItems); - - return merged; - }); - }; - - try { - const traktService = TraktService.getInstance(); - const isTraktAuthed = await traktService.isAuthenticated(); - - const simklService = SimklService.getInstance(); - // Prefer Trakt if both are authenticated - const isSimklAuthed = !isTraktAuthed ? await simklService.isAuthenticated() : false; - - logger.log(`[CW] Providers authed: trakt=${isTraktAuthed} simkl=${isSimklAuthed}`); - - let groupPromises: Promise[] = []; - const allLocalItems: ContinueWatchingItem[] = []; - - // Fetch Trakt watched movies once and reuse - const traktMoviesSetPromise = (async () => { - try { - if (!isTraktAuthed) return new Set(); - if (typeof (traktService as any).getWatchedMovies === 'function') { - const watched = await (traktService as any).getWatchedMovies(); - const watchedSet = new Set(); - - if (Array.isArray(watched)) { - watched.forEach((movie: any) => { - const ids = movie?.movie?.ids; - if (!ids) return; - - const imdb = ids.imdb; - if (imdb) { - watchedSet.add(imdb.startsWith('tt') ? imdb : `tt${imdb}`); - } - if (ids.tmdb) { - watchedSet.add(ids.tmdb.toString()); - } - }); - } - return watchedSet; - } - return new Set(); - } catch { - return new Set(); - } - })(); - - // Fetch Trakt watched shows once and reuse - const traktShowsSetPromise = (async () => { - try { - if (!isTraktAuthed) return new Set(); - - if (typeof (traktService as any).getWatchedShows === 'function') { - const watched = await (traktService as any).getWatchedShows(); - const watchedSet = new Set(); - - if (Array.isArray(watched)) { - watched.forEach((show: any) => { - const ids = show?.show?.ids; - if (!ids) return; - - const imdbId = ids.imdb; - const tmdbId = ids.tmdb; - - if (show.seasons && Array.isArray(show.seasons)) { - show.seasons.forEach((season: any) => { - if (season.episodes && Array.isArray(season.episodes)) { - season.episodes.forEach((episode: any) => { - if (imdbId) { - const cleanImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; - watchedSet.add(`${cleanImdbId}:${season.number}:${episode.number}`); - } - if (tmdbId) { - watchedSet.add(`${tmdbId}:${season.number}:${episode.number}`); - } - }); - } - }); - } - }); - } - return watchedSet; - } - return new Set(); - } catch { - return new Set(); - } - })(); - - // Fetch local supervised watched items - const localWatchedShowsMapPromise = (async () => { - try { - const watched = await watchedService.getAllWatchedItems(); - const watchedMap = new Map(); - watched.forEach(item => { - if (item.content_id) { - const cleanId = item.content_id.startsWith('tt') ? item.content_id : `tt${item.content_id}`; - if (item.season != null && item.episode != null) { - watchedMap.set(`${cleanId}:${item.season}:${item.episode}`, item.watched_at); - watchedMap.set(`${item.content_id}:${item.season}:${item.episode}`, item.watched_at); - } else { - watchedMap.set(cleanId, item.watched_at); - watchedMap.set(item.content_id, item.watched_at); - } - } - }); - return watchedMap; - } catch { - return new Map(); - } - })(); - - // In Trakt mode, CW is sourced from Trakt only, but we still want to overlay local progress - // when local is ahead (scrobble lag/offline playback). - let localProgressIndex: Map | null = null; - if (isTraktAuthed || isSimklAuthed) { - try { - const allProgress = await storageService.getAllWatchProgress(); - const index = new Map(); - - for (const [key, progress] of Object.entries(allProgress)) { - const keyParts = key.split(':'); - const [type, id, ...episodeIdParts] = keyParts; - const episodeId = episodeIdParts.length > 0 ? episodeIdParts.join(':') : undefined; - - const progressPercent = - progress?.duration > 0 - ? (progress.currentTime / progress.duration) * 100 - : 0; - - if (!isFinite(progressPercent) || progressPercent <= 0) continue; - - const parsed = parseEpisodeId(episodeId); - const entry: LocalProgressEntry = { - episodeId, - season: parsed?.season, - episode: parsed?.episode, - progressPercent, - lastUpdated: progress?.lastUpdated ?? 0, - currentTime: progress?.currentTime ?? 0, - duration: progress?.duration ?? 0, - }; - - for (const idVariant of getIdVariants(id)) { - const idxKey = `${type}:${idVariant}`; - const list = index.get(idxKey); - if (list) list.push(entry); - else index.set(idxKey, [entry]); - } - } - - localProgressIndex = index; - } catch { - localProgressIndex = null; - } - } - - // Local-only mode (no Trakt, no Simkl): use local storage - if (!isTraktAuthed && !isSimklAuthed) { - const allProgress = await storageService.getAllWatchProgress(); - if (Object.keys(allProgress).length === 0) { - setContinueWatchingItems([]); - return; - } - - // Group progress items by content ID - process ONLY last 30 items - const sortedProgress = Object.entries(allProgress) - .sort(([, a], [, b]) => b.lastUpdated - a.lastUpdated) - .slice(0, 30); - - const contentGroups: Record }> = {}; - - for (const [key, progress] of sortedProgress) { - const keyParts = key.split(':'); - const [type, id, ...episodeIdParts] = keyParts; - const episodeId = episodeIdParts.length > 0 ? episodeIdParts.join(':') : undefined; - - const progressPercent = - progress.duration > 0 - ? (progress.currentTime / progress.duration) * 100 - : 0; - // Skip fully watched movies - if (type === 'movie' && progressPercent >= 85) continue; - // Skip movies with no actual progress (ensure > 0%) - if (type === 'movie' && (!isFinite(progressPercent) || progressPercent <= 0)) continue; - const contentKey = `${type}:${id}`; - if (!contentGroups[contentKey]) contentGroups[contentKey] = { type, id, episodes: [] }; - contentGroups[contentKey].episodes.push({ key, episodeId, progress, progressPercent }); - } - - // (Promises are now declared at the top of the function) - - groupPromises = Object.values(contentGroups).map(async (group) => { - try { - if (!isSupportedId(group.id)) return; - // Skip movies that are already watched on Trakt - if (group.type === 'movie') { - const watchedSet = await traktMoviesSetPromise; - const imdbId = group.id.startsWith('tt') - ? group.id - : `tt${group.id}`; - if (watchedSet.has(imdbId)) { - // Optional: sync local store to watched to prevent reappearance - try { - await storageService.setWatchProgress(group.id, 'movie', { - currentTime: 1, - duration: 1, - lastUpdated: Date.now(), - traktSynced: true, - traktProgress: 100, - } as any); - } catch (_e) { } - return; - } - } - const cachedData = await getCachedMetadata(group.type, group.id, group.episodes[0]?.progress?.addonId); - if (!cachedData?.basicContent) return; - const { metadata, basicContent } = cachedData; - - const batch: ContinueWatchingItem[] = []; - for (const episode of group.episodes) { - const { episodeId, progress, progressPercent } = episode; - - if (group.type === 'series' && progressPercent >= 85) { - // Episode is completed - find the next unwatched episode - let completedSeason: number | undefined; - let completedEpisode: number | undefined; - - if (episodeId) { - const match = episodeId.match(/s(\d+)e(\d+)/i); - if (match) { - completedSeason = parseInt(match[1], 10); - completedEpisode = parseInt(match[2], 10); - } else { - const parts = episodeId.split(':'); - if (parts.length >= 3) { - const seasonPart = parts[parts.length - 2]; - const episodePart = parts[parts.length - 1]; - const seasonNum = parseInt(seasonPart, 10); - const episodeNum = parseInt(episodePart, 10); - if (!isNaN(seasonNum) && !isNaN(episodeNum)) { - completedSeason = seasonNum; - completedEpisode = episodeNum; - } - } - } - } - - // If we have valid season/episode info, find the next episode - if (completedSeason !== undefined && completedEpisode !== undefined && metadata?.videos) { - const watchedEpisodesSet = await traktShowsSetPromise; - const localWatchedMap = await localWatchedShowsMapPromise; - const nextEpisodeResult = findNextEpisode( - completedSeason, - completedEpisode, - metadata.videos, - watchedEpisodesSet, - group.id, - localWatchedMap, - progress.lastUpdated - ); - - if (nextEpisodeResult) { - const nextEpisode = nextEpisodeResult.video; - logger.log(`📺 [ContinueWatching] Found next episode: S${nextEpisode.season}E${nextEpisode.episode} for ${basicContent.name}`); - batch.push({ - ...basicContent, - progress: 0, // Up next - no progress yet - lastUpdated: nextEpisodeResult.lastWatched, // Keep the timestamp from completed episode or watched item - season: nextEpisode.season, - episode: nextEpisode.episode, - episodeTitle: nextEpisode.title || `Episode ${nextEpisode.episode}`, - addonId: progress.addonId, - } as ContinueWatchingItem); - } - } - continue; - } - - let season: number | undefined; - let episodeNumber: number | undefined; - let episodeTitle: string | undefined; - let isWatchedOnTrakt = false; - - if (episodeId && group.type === 'series') { - let match = episodeId.match(/s(\d+)e(\d+)/i); - if (match) { - season = parseInt(match[1], 10); - episodeNumber = parseInt(match[2], 10); - episodeTitle = `Episode ${episodeNumber}`; - } else { - const parts = episodeId.split(':'); - if (parts.length >= 3) { - const seasonPart = parts[parts.length - 2]; - const episodePart = parts[parts.length - 1]; - const seasonNum = parseInt(seasonPart, 10); - const episodeNum = parseInt(episodePart, 10); - if (!isNaN(seasonNum) && !isNaN(episodeNum)) { - season = seasonNum; - episodeNumber = episodeNum; - episodeTitle = `Episode ${episodeNumber}`; - } - } - } - - // Check if this specific episode is watched on Trakt - if (season !== undefined && episodeNumber !== undefined) { - const watchedEpisodesSet = await traktShowsSetPromise; - const localWatchedMap = await localWatchedShowsMapPromise; - const rawId = group.id.replace(/^tt/, ''); - const ttId = `tt${rawId}`; - - const sig1 = `${ttId}:${season}:${episodeNumber}`; - const sig2 = `${rawId}:${season}:${episodeNumber}`; - const sig3 = `${group.id}:${season}:${episodeNumber}`; - - if (watchedEpisodesSet.has(sig1) || - watchedEpisodesSet.has(sig2) || - watchedEpisodesSet.has(sig3) || - localWatchedMap.has(sig1) || - localWatchedMap.has(sig2) || - localWatchedMap.has(sig3)) { - isWatchedOnTrakt = true; - - // Update local storage to reflect watched status - try { - await storageService.setWatchProgress( - group.id, - 'series', - { - currentTime: 1, - duration: 1, - lastUpdated: Date.now(), - traktSynced: true, - traktProgress: 100, - } as any, - episodeId - ); - } catch (_e) { } - } - } - } - - // If watched on Trakt, skip it - Trakt playback handles in-progress items - if (isWatchedOnTrakt) { - continue; - } - - batch.push({ - ...basicContent, - progress: progressPercent, - lastUpdated: progress.lastUpdated, - season, - episode: episodeNumber, - episodeTitle, - addonId: progress.addonId, - } as ContinueWatchingItem); - } - - if (batch.length > 0) allLocalItems.push(...batch); - } catch (error) { - // Continue processing other groups even if one fails - } - }); - } - - // TRAKT: fetch playback progress (in-progress items) and history, merge incrementally - const traktMergePromise = (async () => { - try { - if (!isTraktAuthed) return; - - // Check Trakt sync cooldown to prevent excessive API calls - const now = Date.now(); - if (TRAKT_SYNC_COOLDOWN > 0 && (now - lastTraktSyncRef.current) < TRAKT_SYNC_COOLDOWN) { - logger.log(`[TraktSync] Skipping Trakt sync - cooldown active (${Math.round((TRAKT_SYNC_COOLDOWN - (now - lastTraktSyncRef.current)) / 1000)}s remaining)`); - return; - } - - lastTraktSyncRef.current = now; - - // Fetch only playback progress (paused items with actual progress %) - // Removed: history items and watched shows - redundant with local logic - const playbackItems = await traktService.getPlaybackProgress(); - - try { - const top = [...playbackItems] - .sort((a, b) => new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime()) - .slice(0, 10) - .map((x) => ({ - id: x.id, - type: x.type, - progress: x.progress, - pausedAt: x.paused_at, - imdb: x.type === 'movie' ? x.movie?.ids?.imdb : x.show?.ids?.imdb, - season: x.type === 'episode' ? x.episode?.season : undefined, - episode: x.type === 'episode' ? x.episode?.number : undefined, - })); - logger.log('[CW][Trakt] top playback items:', top); - } catch { - // ignore - } - - - - const traktBatch: ContinueWatchingItem[] = []; - - // Pre-fetch watched shows so both Step 1 and Step 2 can use the watched episode sets - // This fixes "Up Next" suggesting already-watched episodes when the watched set is missing - let watchedShowsData: Awaited> = []; - // Map from showImdb -> Set of "imdb:season:episode" strings - const watchedEpisodeSetByShow = new Map>(); - try { - watchedShowsData = await traktService.getWatchedShows(); - for (const ws of watchedShowsData) { - if (!ws.show?.ids?.imdb) continue; - const imdb = ws.show.ids.imdb.startsWith('tt') ? ws.show.ids.imdb : `tt${ws.show.ids.imdb}`; - const resetAt = ws.reset_at ? new Date(ws.reset_at).getTime() : 0; - const episodeSet = new Set(); - if (ws.seasons) { - for (const season of ws.seasons) { - for (const episode of season.episodes) { - // Respect reset_at: skip episodes watched before the reset - if (resetAt > 0) { - const watchedAt = new Date(episode.last_watched_at).getTime(); - if (watchedAt < resetAt) continue; - } - episodeSet.add(`${imdb}:${season.number}:${episode.number}`); - } - } - } - watchedEpisodeSetByShow.set(imdb, episodeSet); - } - } catch { - // Non-fatal — fall back to no watched set - } - - // STEP 1: Process playback progress items (in-progress, paused) - // These have actual progress percentage from Trakt - const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000); - - // Sort by paused_at descending and take top 30 - const sortedPlaybackItems = playbackItems - .sort((a, b) => new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime()) - .slice(0, 30); - - for (const item of sortedPlaybackItems) { - try { - // Skip items with < 2% progress (accidental clicks) - if (item.progress < 2) continue; - // Skip items older than 30 days - const pausedAt = new Date(item.paused_at).getTime(); - if (pausedAt < thirtyDaysAgo) continue; - - if (item.type === 'movie' && item.movie?.ids?.imdb) { - // Skip completed movies - if (item.progress >= 85) continue; - - const imdbId = item.movie.ids.imdb.startsWith('tt') - ? item.movie.ids.imdb - : `tt${item.movie.ids.imdb}`; - - // Check if recently removed - const movieKey = `movie:${imdbId}`; - if (recentlyRemovedRef.current.has(movieKey)) continue; - - const cachedData = await getCachedMetadata('movie', imdbId); - if (!cachedData?.basicContent) continue; - - const pausedAt = new Date(item.paused_at).getTime(); - traktBatch.push({ - ...cachedData.basicContent, - id: imdbId, - type: 'movie', - progress: item.progress, - lastUpdated: pausedAt, - addonId: undefined, - traktPlaybackId: item.id, // Store playback ID for removal - } as ContinueWatchingItem); - - logger.log(`📺 [TraktPlayback] Adding movie ${item.movie.title} with ${item.progress.toFixed(1)}% progress`); - - } else if (item.type === 'episode' && item.show?.ids?.imdb && item.episode) { - const showImdb = item.show.ids.imdb.startsWith('tt') - ? item.show.ids.imdb - : `tt${item.show.ids.imdb}`; - - // Check if recently removed - const showKey = `series:${showImdb}`; - if (recentlyRemovedRef.current.has(showKey)) continue; - - const pausedAt = new Date(item.paused_at).getTime(); - - const cachedData = await getCachedMetadata('series', showImdb); - if (!cachedData?.basicContent) continue; - - // If episode is completed (>= 85%), find next episode - if (item.progress >= 85) { - const metadata = cachedData.metadata; - if (metadata?.videos) { - // Use pre-fetched watched set so already-watched episodes are skipped - const watchedSetForShow = watchedEpisodeSetByShow.get(showImdb); - const localWatchedMap = await localWatchedShowsMapPromise; - const nextEpisodeResult = findNextEpisode( - item.episode.season, - item.episode.number, - metadata.videos, - watchedSetForShow, - showImdb, - localWatchedMap, - pausedAt - ); - - if (nextEpisodeResult) { - const nextEpisode = nextEpisodeResult.video; - logger.log(`📺 [TraktPlayback] Episode completed, adding next: S${nextEpisode.season}E${nextEpisode.episode} for ${item.show.title}`); - traktBatch.push({ - ...cachedData.basicContent, - id: showImdb, - type: 'series', - progress: 0, // Up next - no progress yet - lastUpdated: nextEpisodeResult.lastWatched, - season: nextEpisode.season, - episode: nextEpisode.episode, - episodeTitle: nextEpisode.title || `Episode ${nextEpisode.episode}`, - addonId: undefined, - traktPlaybackId: item.id, - } as ContinueWatchingItem); - } - } - continue; - } - - traktBatch.push({ - ...cachedData.basicContent, - id: showImdb, - type: 'series', - progress: item.progress, - lastUpdated: pausedAt, - season: item.episode.season, - episode: item.episode.number, - episodeTitle: item.episode.title || `Episode ${item.episode.number}`, - addonId: undefined, - traktPlaybackId: item.id, // Store playback ID for removal - } as ContinueWatchingItem); - - logger.log(`📺 [TraktPlayback] Adding ${item.show.title} S${item.episode.season}E${item.episode.number} with ${item.progress.toFixed(1)}% progress`); - } - } catch (err) { - // Continue with other items - } - } - - // STEP 2: Find "Up Next" episodes using pre-fetched watched shows data - // Reuses watchedShowsData fetched before Step 1 — no extra API call - // Also respects reset_at (Bug 4 fix) and uses pre-built watched episode sets (Bug 3 fix) - try { - const thirtyDaysAgoForShows = Date.now() - (30 * 24 * 60 * 60 * 1000); - - for (const watchedShow of watchedShowsData) { - try { - if (!watchedShow.show?.ids?.imdb) continue; - - // Skip shows that haven't been watched recently - const lastWatchedAt = new Date(watchedShow.last_watched_at).getTime(); - if (lastWatchedAt < thirtyDaysAgoForShows) continue; - - const showImdb = watchedShow.show.ids.imdb.startsWith('tt') - ? watchedShow.show.ids.imdb - : `tt${watchedShow.show.ids.imdb}`; - - // Check if recently removed - const showKey = `series:${showImdb}`; - if (recentlyRemovedRef.current.has(showKey)) continue; - - const resetAt = watchedShow.reset_at ? new Date(watchedShow.reset_at).getTime() : 0; - - // Find the last watched episode (respecting reset_at) - let lastWatchedSeason = 0; - let lastWatchedEpisode = 0; - let latestEpisodeTimestamp = 0; - - if (watchedShow.seasons) { - for (const season of watchedShow.seasons) { - for (const episode of season.episodes) { - const episodeTimestamp = new Date(episode.last_watched_at).getTime(); - // Skip episodes watched before the user reset their progress - if (resetAt > 0 && episodeTimestamp < resetAt) continue; - if (episodeTimestamp > latestEpisodeTimestamp) { - latestEpisodeTimestamp = episodeTimestamp; - lastWatchedSeason = season.number; - lastWatchedEpisode = episode.number; - } - } - } - } - - if (lastWatchedSeason === 0 && lastWatchedEpisode === 0) continue; - - // Get metadata with episode list - const cachedData = await getCachedMetadata('series', showImdb); - if (!cachedData?.basicContent || !cachedData?.metadata?.videos) continue; - - // Use pre-built watched episode set (already respects reset_at) - const watchedEpisodeSet = watchedEpisodeSetByShow.get(showImdb) ?? new Set(); - const localWatchedMap = await localWatchedShowsMapPromise; - - // Find the next unwatched episode - const nextEpisodeResult = findNextEpisode( - lastWatchedSeason, - lastWatchedEpisode, - cachedData.metadata.videos, - watchedEpisodeSet, - showImdb, - localWatchedMap, - latestEpisodeTimestamp - ); - - if (nextEpisodeResult) { - const nextEpisode = nextEpisodeResult.video; - logger.log(`📺 [TraktWatched] Found Up Next: ${watchedShow.show.title} S${nextEpisode.season}E${nextEpisode.episode}`); - traktBatch.push({ - ...cachedData.basicContent, - id: showImdb, - type: 'series', - progress: 0, // Up next - no progress yet - lastUpdated: nextEpisodeResult.lastWatched, - season: nextEpisode.season, - episode: nextEpisode.episode, - episodeTitle: nextEpisode.title || `Episode ${nextEpisode.episode}`, - addonId: undefined, - } as ContinueWatchingItem); - } - } catch (err) { - // Continue with other shows - } - } - } catch (err) { - logger.warn('[TraktSync] Error fetching watched shows for Up Next:', err); - } - - // Trakt mode: show ONLY Trakt items, but override progress with local if local is higher. - if (traktBatch.length > 0) { - // Dedupe (keep in-progress over "Up Next"; then prefer most recent) - const deduped = new Map(); - for (const item of traktBatch) { - const key = `${item.type}:${item.id}`; - const existing = deduped.get(key); - if (!existing) { - deduped.set(key, item); - } else { - const existingHasProgress = (existing.progress ?? 0) > 0; - const candidateHasProgress = (item.progress ?? 0) > 0; - if (candidateHasProgress && !existingHasProgress) { - // Always prefer actual in-progress over "Up Next" placeholder - deduped.set(key, item); - } else if (!candidateHasProgress && existingHasProgress) { - // Keep existing in-progress item - } else if ((item.lastUpdated ?? 0) > (existing.lastUpdated ?? 0)) { - deduped.set(key, item); - } - } - } - - // Filter removed items - const filteredItems: ContinueWatchingItem[] = []; - for (const item of deduped.values()) { - const key = item.type === 'series' && item.season && item.episode - ? `${item.type}:${item.id}:${item.season}:${item.episode}` - : `${item.type}:${item.id}`; - if (recentlyRemovedRef.current.has(key)) continue; - - const removeId = item.type === 'series' && item.season && item.episode - ? `${item.id}:${item.season}:${item.episode}` - : item.id; - const isRemoved = await storageService.isContinueWatchingRemoved(removeId, item.type); - if (!isRemoved) filteredItems.push(item); - } - - const getLocalMatches = (item: ContinueWatchingItem): LocalProgressEntry[] => { - if (!localProgressIndex) return []; - - const typeKey = item.type; - const matches: LocalProgressEntry[] = []; - - for (const idVariant of getIdVariants(item.id)) { - const entries = localProgressIndex.get(`${typeKey}:${idVariant}`); - if (!entries || entries.length === 0) continue; - - if (item.type === 'movie') { - matches.push(...entries); - } else { - // series: only match same season/episode - if (item.season === undefined || item.episode === undefined) continue; - for (const e of entries) { - if (e.season === item.season && e.episode === item.episode) { - matches.push(e); - } - } - } - } - - return matches; - }; - - const toYearNumber = (value: any): number | undefined => { - if (typeof value === 'number' && isFinite(value)) return value; - if (typeof value === 'string') { - const parsed = parseInt(value, 10); - if (isFinite(parsed)) return parsed; - } - return undefined; - }; - - const buildTraktContentData = (item: ContinueWatchingItem): import('../../services/traktService').TraktContentData | null => { - if (item.type === 'movie') { - return { - type: 'movie', - imdbId: item.id, - title: item.name, - year: toYearNumber((item as any).year), - }; - } - - if (item.type === 'series' && item.season && item.episode) { - return { - type: 'episode', - imdbId: item.id, - title: item.episodeTitle || `S${item.season}E${item.episode}`, - season: item.season, - episode: item.episode, - showTitle: item.name, - showYear: toYearNumber((item as any).year), - showImdbId: item.id, - }; - } - - return null; - }; - - const reconcilePromises: Promise[] = []; - const reconcileLocalPromises: Promise[] = []; - - const adjustedItems = filteredItems.map((it) => { - const matches = getLocalMatches(it); - if (matches.length === 0) return it; - - const mostRecentLocal = matches.reduce((acc, cur) => { - if (!acc) return cur; - return (cur.lastUpdated ?? 0) > (acc.lastUpdated ?? 0) ? cur : acc; - }, null); - - const highestLocal = matches.reduce((acc, cur) => { - if (!acc) return cur; - return (cur.progressPercent ?? 0) > (acc.progressPercent ?? 0) ? cur : acc; - }, null); - - if (!mostRecentLocal || !highestLocal) return it; - - // Use the most recent timestamp between local and Trakt. - // Always preferring local was wrong: if you watched on another device, - // Trakt's paused_at is newer and should win for ordering purposes. - const mergedLastUpdated = Math.max( - (mostRecentLocal.lastUpdated ?? 0), - (it.lastUpdated ?? 0) - ); - - try { - logger.log('[CW][Trakt][Overlay] item/local summary', { - key: `${it.type}:${it.id}:${it.season ?? ''}:${it.episode ?? ''}`, - traktProgress: it.progress, - traktLastUpdated: it.lastUpdated, - localMostRecent: { progress: mostRecentLocal.progressPercent, lastUpdated: mostRecentLocal.lastUpdated }, - localHighest: { progress: highestLocal.progressPercent, lastUpdated: highestLocal.lastUpdated }, - mergedLastUpdated, - }); - } catch { - // ignore - } - - // Background reconcile: if local progress is ahead of Trakt OR local is newer than Trakt, - // scrobble local progress to Trakt. - // This handles missed scrobbles (local ahead) and intentional seek-back/rewatch (local newer but lower). - const localProgress = mostRecentLocal.progressPercent; - const traktProgress = it.progress ?? 0; - const traktTs = it.lastUpdated ?? 0; - const localTs = mostRecentLocal.lastUpdated ?? 0; - - const isAhead = isFinite(localProgress) && localProgress > traktProgress + 0.5; - const isLocalNewer = localTs > traktTs + 5000; // 5s guard against clock jitter - const isLocalRecent = localTs > 0 && (Date.now() - localTs) < (5 * 60 * 1000); // 5 minutes - const isDifferent = Math.abs((localProgress || 0) - (traktProgress || 0)) > 0.5; - - const isTraktAhead = isFinite(traktProgress) && traktProgress > localProgress + 0.5; - // If the user just interacted locally (seek-back/rewatch), do NOT overwrite local with Trakt. - if (isTraktAhead && !isLocalRecent && mostRecentLocal.duration > 0) { - const reconcileKey = `local:${it.type}:${it.id}:${it.season ?? ''}:${it.episode ?? ''}`; - const last = lastTraktReconcileRef.current.get(reconcileKey) ?? 0; - const now = Date.now(); - - if (now - last >= TRAKT_RECONCILE_COOLDOWN) { - lastTraktReconcileRef.current.set(reconcileKey, now); - - // Sync Trakt -> local so resume/progress UI uses the higher value. - // Only possible when we have a local duration. - const targetEpisodeId = - it.type === 'series' - ? (mostRecentLocal.episodeId || (it.season && it.episode ? `${it.id}:${it.season}:${it.episode}` : undefined)) - : undefined; - - const newCurrentTime = (traktProgress / 100) * mostRecentLocal.duration; - - reconcileLocalPromises.push( - (async () => { - try { - const existing = await storageService.getWatchProgress(it.id, it.type, targetEpisodeId); - if (!existing || !existing.duration || existing.duration <= 0) return; - - await storageService.setWatchProgress( - it.id, - it.type, - { - ...existing, - currentTime: Math.max(existing.currentTime ?? 0, newCurrentTime), - duration: existing.duration, - traktSynced: true, - traktLastSynced: Date.now(), - traktProgress: Math.max(existing.traktProgress ?? 0, traktProgress), - // Do NOT update lastUpdated here; this is a background state sync and - // should not affect "recent" ordering. - lastUpdated: existing.lastUpdated, - } as any, - targetEpisodeId, - { preserveTimestamp: true, forceWrite: true } - ); - } catch { - // ignore - } - })() - ); - } - } - - if ((isAhead || ((isLocalNewer || isLocalRecent) && isDifferent)) && localProgress >= 2) { - const reconcileKey = `${it.type}:${it.id}:${it.season ?? ''}:${it.episode ?? ''}`; - const last = lastTraktReconcileRef.current.get(reconcileKey) ?? 0; - const now = Date.now(); - if (now - last >= TRAKT_RECONCILE_COOLDOWN) { - lastTraktReconcileRef.current.set(reconcileKey, now); - - const contentData = buildTraktContentData(it); - if (contentData) { - // Trakt treats >=80% on /scrobble/stop as "watched". - // Keep in-progress items under 80 unless the user truly completed it in-app (>=85%). - const progressToSend = localProgress >= 85 ? Math.min(localProgress, 100) : Math.min(localProgress, 79.9); - - reconcilePromises.push( - traktService - .pauseWatching(contentData, progressToSend) - .catch(() => null) - ); - } - } - } - - // If local is newer/recent, prefer local progress immediately (covers seek-back/rewatch). - // Otherwise, only prefer local progress when it is ahead. - if (((isLocalNewer || isLocalRecent) && isDifferent) || localProgress > (it.progress ?? 0) + 0.5) { - return { - ...it, - progress: ((isLocalNewer || isLocalRecent) && isDifferent) ? localProgress : localProgress, - lastUpdated: mergedLastUpdated, - }; - } - - return { - ...it, - lastUpdated: mergedLastUpdated, - }; - }).filter((it) => { - // Never show completed items in Continue Watching - const p = it.progress ?? 0; - if (it.type === 'movie' && p >= 85) return false; - if (it.type === 'series' && p >= 85) return false; - return true; - }); - - // Sort by lastUpdated descending and set directly - adjustedItems.sort(compareCwItems); - - // Debug final order (only if changed) - try { - const sig = adjustedItems - .slice(0, 12) - .map((x) => `${x.type}:${x.id}:${x.season ?? ''}:${x.episode ?? ''}@${Math.round(x.lastUpdated ?? 0)}:${Math.round(x.progress ?? 0)}`) - .join('|'); - if (sig !== lastOrderLogSigRef.current) { - lastOrderLogSigRef.current = sig; - logger.log('[CW][Trakt] final CW order (top 12):', - adjustedItems.slice(0, 12).map((x) => ({ - key: `${x.type}:${x.id}:${x.season ?? ''}:${x.episode ?? ''}`, - progress: x.progress, - lastUpdated: x.lastUpdated, - })) - ); - } - } catch { - // ignore - } - - setContinueWatchingItems(adjustedItems); - - // Fire-and-forget reconcile (don't block UI) - if (reconcilePromises.length > 0) { - Promise.allSettled(reconcilePromises).catch(() => null); - } - - // Fire-and-forget local sync (Trakt -> local) - if (reconcileLocalPromises.length > 0) { - Promise.allSettled(reconcileLocalPromises).catch(() => null); - } - } - } catch (err) { - logger.error('[TraktSync] Error in Trakt merge:', err); - } - })(); - - // SIMKL: fetch playback progress (in-progress, paused) and merge similarly to Trakt - const simklMergePromise = (async () => { - try { - if (!isSimklAuthed || isTraktAuthed) return; - - const now = Date.now(); - if (SIMKL_SYNC_COOLDOWN > 0 && (now - lastSimklSyncRef.current) < SIMKL_SYNC_COOLDOWN) { - return; - } - lastSimklSyncRef.current = now; - - const playbackItems = await simklService.getPlaybackStatus(); - logger.log(`[CW][Simkl] playback items: ${playbackItems.length}`); - - const simklBatch: ContinueWatchingItem[] = []; - const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000); - - const sortedPlaybackItems = [...playbackItems] - .sort((a, b) => new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime()) - .slice(0, 30); - - for (const item of sortedPlaybackItems) { - try { - // Skip accidental clicks - if ((item.progress ?? 0) < 2) continue; - - const pausedAt = new Date(item.paused_at).getTime(); - if (pausedAt < thirtyDaysAgo) continue; - - if (item.type === 'movie' && item.movie?.ids?.imdb) { - // Skip completed movies - if (item.progress >= 85) continue; - - const imdbId = item.movie.ids.imdb.startsWith('tt') - ? item.movie.ids.imdb - : `tt${item.movie.ids.imdb}`; - - const movieKey = `movie:${imdbId}`; - if (recentlyRemovedRef.current.has(movieKey)) continue; - - const cachedData = await getCachedMetadata('movie', imdbId); - if (!cachedData?.basicContent) continue; - - simklBatch.push({ - ...cachedData.basicContent, - id: imdbId, - type: 'movie', - progress: item.progress, - lastUpdated: pausedAt, - addonId: undefined, - } as ContinueWatchingItem); - } else if (item.type === 'episode' && item.show?.ids?.imdb && item.episode) { - const showImdb = item.show.ids.imdb.startsWith('tt') - ? item.show.ids.imdb - : `tt${item.show.ids.imdb}`; - - const episodeNum = (item.episode as any).episode ?? (item.episode as any).number; - if (episodeNum === undefined || episodeNum === null) { - logger.warn('[CW][Simkl] Missing episode number in playback item, skipping', item); - continue; - } - - const showKey = `series:${showImdb}`; - if (recentlyRemovedRef.current.has(showKey)) continue; - - const cachedData = await getCachedMetadata('series', showImdb); - if (!cachedData?.basicContent) continue; - - // If episode is completed (>= 85%), find next episode - if (item.progress >= 85) { - const metadata = cachedData.metadata; - if (metadata?.videos) { - const watchedEpisodesSet = await traktShowsSetPromise; - const localWatchedMap = await localWatchedShowsMapPromise; - const nextEpisodeResult = findNextEpisode( - item.episode.season, - episodeNum, - metadata.videos, - watchedEpisodesSet, - showImdb, - localWatchedMap, - pausedAt - ); - - if (nextEpisodeResult) { - const nextEpisode = nextEpisodeResult.video; - simklBatch.push({ - ...cachedData.basicContent, - id: showImdb, - type: 'series', - progress: 0, - lastUpdated: nextEpisodeResult.lastWatched, - season: nextEpisode.season, - episode: nextEpisode.episode, - episodeTitle: nextEpisode.title || `Episode ${nextEpisode.episode}`, - addonId: undefined, - } as ContinueWatchingItem); - } - } - continue; - } - - simklBatch.push({ - ...cachedData.basicContent, - id: showImdb, - type: 'series', - progress: item.progress, - lastUpdated: pausedAt, - season: item.episode.season, - episode: episodeNum, - episodeTitle: item.episode.title || `Episode ${episodeNum}`, - addonId: undefined, - } as ContinueWatchingItem); - } - } catch { - // Continue with other items - } - } - - if (simklBatch.length === 0) { - setContinueWatchingItems([]); - return; - } - - // Dedupe (keep most recent per show/movie) - const deduped = new Map(); - for (const item of simklBatch) { - const key = `${item.type}:${item.id}`; - const existing = deduped.get(key); - if (!existing || (item.lastUpdated ?? 0) > (existing.lastUpdated ?? 0)) { - deduped.set(key, item); - } - } - - // Filter removed items - const filteredItems: ContinueWatchingItem[] = []; - for (const item of deduped.values()) { - const key = item.type === 'series' && item.season && item.episode - ? `${item.type}:${item.id}:${item.season}:${item.episode}` - : `${item.type}:${item.id}`; - if (recentlyRemovedRef.current.has(key)) continue; - - const removeId = item.type === 'series' && item.season && item.episode - ? `${item.id}:${item.season}:${item.episode}` - : item.id; - const isRemoved = await storageService.isContinueWatchingRemoved(removeId, item.type); - if (!isRemoved) filteredItems.push(item); - } - - // Overlay local progress when local is ahead or newer - const adjustedItems = filteredItems.map((it) => { - if (!localProgressIndex) return it; - - const matches: LocalProgressEntry[] = []; - for (const idVariant of getIdVariants(it.id)) { - const list = localProgressIndex.get(`${it.type}:${idVariant}`); - if (!list) continue; - for (const entry of list) { - if (it.type === 'series' && it.season !== undefined && it.episode !== undefined) { - if (entry.season === it.season && entry.episode === it.episode) { - matches.push(entry); - } - } else { - matches.push(entry); - } - } - } - - if (matches.length === 0) return it; - - const mostRecentLocal = matches.reduce((acc, cur) => { - if (!acc) return cur; - return (cur.lastUpdated ?? 0) > (acc.lastUpdated ?? 0) ? cur : acc; - }, null); - - const highestLocal = matches.reduce((acc, cur) => { - if (!acc) return cur; - return (cur.progressPercent ?? 0) > (acc.progressPercent ?? 0) ? cur : acc; - }, null); - - if (!mostRecentLocal || !highestLocal) return it; - - const localProgress = mostRecentLocal.progressPercent; - const simklProgress = it.progress ?? 0; - const localTs = mostRecentLocal.lastUpdated ?? 0; - const simklTs = it.lastUpdated ?? 0; - - const isAhead = isFinite(localProgress) && localProgress > simklProgress + 0.5; - const isLocalNewer = localTs > simklTs + 5000; - - if (isAhead || isLocalNewer) { - return { - ...it, - progress: localProgress, - lastUpdated: localTs > 0 ? localTs : it.lastUpdated, - } as ContinueWatchingItem; - } - - // Otherwise keep Simkl, but if local has a newer timestamp, use it for ordering - if (localTs > 0 && localTs > simklTs) { - return { - ...it, - lastUpdated: localTs, - } as ContinueWatchingItem; - } - - return it; - }); - - adjustedItems.sort(compareCwItems); - setContinueWatchingItems(adjustedItems); - } catch (err) { - logger.error('[SimklSync] Error in Simkl merge:', err); - } - })(); - - // Wait for all groups and provider merges to settle, then finalize loading state - await Promise.allSettled([...groupPromises, traktMergePromise, simklMergePromise]); - - if (allLocalItems.length > 0) { - const map = new Map(); - for (const it of allLocalItems) { - const key = `${it.type}:${it.id}`; - const existing = map.get(key); - if (!existing || shouldPreferCandidate(it, existing)) { - map.set(key, it); - } - } - - const sorted = Array.from(map.values()); - sorted.sort(compareCwItems); - - // Filter removed items - const filtered: ContinueWatchingItem[] = []; - for (const it of sorted) { - const key = it.type === 'series' && it.season && it.episode - ? `${it.type}:${it.id}:${it.season}:${it.episode}` - : `${it.type}:${it.id}`; - if (recentlyRemovedRef.current.has(key)) continue; - - const removeId = it.type === 'series' && it.season && it.episode - ? `${it.id}:${it.season}:${it.episode}` - : it.id; - const isRemoved = await storageService.isContinueWatchingRemoved(removeId, it.type); - if (!isRemoved) filtered.push(it); - } - - setContinueWatchingItems(filtered); - } - } catch (error) { - // Continue even if loading fails - } finally { - setLoading(false); - isRefreshingRef.current = false; - if (pendingRefreshRef.current) { - pendingRefreshRef.current = false; - setTimeout(() => { - loadContinueWatching(true); - }, 0); - } - } - }, [getCachedMetadata]); - - // Clear cache when component unmounts or when needed - useEffect(() => { - return () => { - metadataCache.current = {}; - }; - }, []); - - // Function to handle app state changes - const handleAppStateChange = useCallback((nextAppState: AppStateStatus) => { - if ( - appState.current.match(/inactive|background/) && - nextAppState === 'active' - ) { - // App has come to the foreground - force Trakt sync by resetting cooldown - lastTraktSyncRef.current = 0; // Reset cooldown to allow immediate Trakt sync - loadContinueWatching(true); - } - appState.current = nextAppState; - }, [loadContinueWatching]); - - // Set up storage event listener and app state listener - useEffect(() => { - // Add app state change listener - const subscription = AppState.addEventListener('change', handleAppStateChange); - - // Add custom event listener for watch progress updates - const watchProgressUpdateHandler = () => { - // Debounce updates to avoid too frequent refreshes - if (refreshTimerRef.current) { - clearTimeout(refreshTimerRef.current); - } - refreshTimerRef.current = setTimeout(() => { - // Only trigger background refresh for local progress updates, not Trakt sync - // This prevents the feedback loop where Trakt sync triggers more progress updates - loadContinueWatching(true); - }, 2000); // Increased debounce to reduce frequency - }; - - // Try to set up a custom event listener or use a timer as fallback - if (storageService.subscribeToWatchProgressUpdates) { - const unsubscribe = storageService.subscribeToWatchProgressUpdates(watchProgressUpdateHandler); - return () => { - subscription.remove(); - unsubscribe(); - if (refreshTimerRef.current) { - clearTimeout(refreshTimerRef.current); - } - if (longPressTimeoutRef.current) { - clearTimeout(longPressTimeoutRef.current); - } - }; - } else { - // Reduced polling frequency from 30s to 5 minutes to reduce heating and battery drain - const intervalId = setInterval(() => loadContinueWatching(true), 300000); - return () => { - subscription.remove(); - clearInterval(intervalId); - if (refreshTimerRef.current) { - clearTimeout(refreshTimerRef.current); - } - if (longPressTimeoutRef.current) { - clearTimeout(longPressTimeoutRef.current); - } - }; - } - }, [loadContinueWatching, handleAppStateChange]); - - // Initial load - useEffect(() => { - loadContinueWatching(); - const trailingRefreshId = setTimeout(() => { - loadContinueWatching(true); - }, 4000); - - return () => { - clearTimeout(trailingRefreshId); - }; - }, [loadContinueWatching]); - - // Refresh on screen focus (lightweight, no polling) - useFocusEffect( - useCallback(() => { - loadContinueWatching(true); - return () => { }; - }, [loadContinueWatching]) - ); - - // Expose the refresh function via the ref - React.useImperativeHandle(ref, () => ({ - refresh: async () => { - // Manual refresh bypasses Trakt cooldown to get fresh data - lastTraktSyncRef.current = 0; // Reset cooldown for manual refresh - await loadContinueWatching(false); - return true; - } - })); - - const handleContentPress = useCallback(async (item: ContinueWatchingItem) => { - try { - logger.log(`🎬 [ContinueWatching] User clicked on: ${item.name} (${item.type}:${item.id})`); - - // Check if cached streams are enabled in settings - if (!settings.useCachedStreams) { - logger.log(`📺 [ContinueWatching] Cached streams disabled, navigating to ${settings.openMetadataScreenWhenCacheDisabled ? 'MetadataScreen' : 'StreamsScreen'} for ${item.name}`); - - // Navigate based on the second setting - if (settings.openMetadataScreenWhenCacheDisabled) { - // Navigate to MetadataScreen - if (item.type === 'series' && item.season && item.episode) { - const episodeId = `${item.id}:${item.season}:${item.episode}`; - navigation.navigate('Metadata', { - id: item.id, - type: item.type, - episodeId: episodeId, - addonId: item.addonId - }); - } else { - navigation.navigate('Metadata', { - id: item.id, - type: item.type, - addonId: item.addonId - }); - } - } else { - // Navigate to StreamsScreen - if (item.type === 'series' && item.season && item.episode) { - const episodeId = `${item.id}:${item.season}:${item.episode}`; - navigation.navigate('Streams', { - id: item.id, - type: item.type, - episodeId: episodeId - }); - } else { - navigation.navigate('Streams', { - id: item.id, - type: item.type - }); - } - } - return; - } - - // Check if we have a cached stream for this content - const episodeId = item.type === 'series' && item.season && item.episode - ? `${item.id}:${item.season}:${item.episode}` - : undefined; - - logger.log(`🔍 [ContinueWatching] Looking for cached stream with episodeId: ${episodeId || 'none'}`); - - const cachedStream = await streamCacheService.getCachedStream(item.id, item.type, episodeId); - - if (cachedStream) { - // We have a valid cached stream, navigate directly to player - logger.log(`🚀 [ContinueWatching] Using cached stream for ${item.name}`); - - // Determine the player route based on platform - const playerRoute = Platform.OS === 'ios' ? 'PlayerIOS' : 'PlayerAndroid'; - - // Navigate directly to player with cached stream data - navigation.navigate(playerRoute as any, { - uri: cachedStream.stream.url, - title: cachedStream.metadata?.name || item.name, - episodeTitle: cachedStream.episodeTitle || (item.type === 'series' ? `Episode ${item.episode}` : undefined), - season: cachedStream.season || item.season, - episode: cachedStream.episode || item.episode, - quality: (cachedStream.stream.title?.match(/(\d+)p/) || [])[1] || undefined, - year: cachedStream.metadata?.year || item.year, - streamProvider: cachedStream.stream.addonId || cachedStream.stream.addonName || cachedStream.stream.name, - streamName: cachedStream.stream.name || cachedStream.stream.title || 'Unnamed Stream', - headers: cachedStream.stream.headers || undefined, - id: item.id, - type: item.type, - episodeId: episodeId, - imdbId: cachedStream.imdbId || cachedStream.metadata?.imdbId || item.imdb_id, - backdrop: cachedStream.metadata?.backdrop || item.banner, - videoType: undefined, // Let player auto-detect - } as any); - - return; - } - - // No cached stream or cache failed, navigate to StreamsScreen - logger.log(`📺 [ContinueWatching] No cached stream, navigating to StreamsScreen for ${item.name}`); - - if (item.type === 'series' && item.season && item.episode) { - // For series, navigate to the specific episode - navigation.navigate('Streams', { - id: item.id, - type: item.type, - episodeId: episodeId, - addonId: item.addonId - }); - } else { - // For movies or series without specific episode, navigate to main content - navigation.navigate('Streams', { - id: item.id, - type: item.type, - addonId: item.addonId - }); - } - } catch (error) { - logger.warn('[ContinueWatching] Error handling content press:', error); - // Fallback to StreamsScreen on any error - if (item.type === 'series' && item.season && item.episode) { - const episodeId = `${item.id}:${item.season}:${item.episode}`; - navigation.navigate('Streams', { - id: item.id, - type: item.type, - episodeId: episodeId - }); - } else { - navigation.navigate('Streams', { - id: item.id, - type: item.type - }); - } - } - }, [navigation, settings.useCachedStreams, settings.openMetadataScreenWhenCacheDisabled]); - - // Handle long press to show action sheet const handleLongPress = useCallback((item: ContinueWatchingItem) => { try { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); - } catch (error) { - // Ignore haptic errors + } catch { + // Ignore haptic failures. } + setSelectedItem(item); actionSheetRef.current?.present(); }, []); - // Handle view details action const handleViewDetails = useCallback(() => { if (!selectedItem) return; + actionSheetRef.current?.dismiss(); - setTimeout(() => { - if (selectedItem.type === 'series' && selectedItem.season && selectedItem.episode) { - const episodeId = `${selectedItem.id}:${selectedItem.season}:${selectedItem.episode}`; - navigation.navigate('Metadata', { - id: selectedItem.id, - type: selectedItem.type, - episodeId: episodeId, - addonId: selectedItem.addonId - }); - } else { - navigation.navigate('Metadata', { - id: selectedItem.id, - type: selectedItem.type, - addonId: selectedItem.addonId - }); - } + navigateToMetadata(selectedItem); }, 150); - }, [selectedItem, navigation]); + }, [navigateToMetadata, selectedItem]); - // Handle remove action const handleRemoveItem = useCallback(async () => { if (!selectedItem) return; + actionSheetRef.current?.dismiss(); - setDeletingItemId(selectedItem.id); try { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - - // For series episodes, remove only that episode's progress. - // Do not wipe all series entries. - const isEpisode = selectedItem.type === 'series' && selectedItem.season && selectedItem.episode; - if (isEpisode) { - const episodeId = `${selectedItem.id}:${selectedItem.season}:${selectedItem.episode}`; - await storageService.removeWatchProgress( - selectedItem.id, - selectedItem.type, - episodeId - ); - } else { - // For movies or whole series, add the base tombstone - await storageService.removeAllWatchProgressForContent( - selectedItem.id, - selectedItem.type, - { addBaseTombstone: true } - ); - } - - const traktService = TraktService.getInstance(); - const isAuthed = await traktService.isAuthenticated(); - - // Only remove playback progress from Trakt (not watch history) - // This ensures "Up Next" items don't affect Trakt watch history - if (isAuthed && selectedItem.traktPlaybackId) { - await traktService.removePlaybackItem(selectedItem.traktPlaybackId); - } - // For series, make the key episode-specific so dismissing "Up Next" - // doesn't affect other episodes - const itemKey = selectedItem.type === 'series' && selectedItem.season && selectedItem.episode - ? `${selectedItem.type}:${selectedItem.id}:${selectedItem.season}:${selectedItem.episode}` - : `${selectedItem.type}:${selectedItem.id}`; - - recentlyRemovedRef.current.add(itemKey); - - // Store with episode-specific ID for series - const removeId = selectedItem.type === 'series' && selectedItem.season && selectedItem.episode - ? `${selectedItem.id}:${selectedItem.season}:${selectedItem.episode}` - : selectedItem.id; - await storageService.addContinueWatchingRemoved(removeId, selectedItem.type); - - setTimeout(() => { - recentlyRemovedRef.current.delete(itemKey); - }, REMOVAL_IGNORE_DURATION); - setContinueWatchingItems(prev => prev.filter(i => { - // For series, also check episode match - if (i.type === 'series' && selectedItem.type === 'series') { - return !(i.id === selectedItem.id && i.season === selectedItem.season && i.episode === selectedItem.episode); - } - return i.id !== selectedItem.id; - })); - } catch (error) { - // Continue even if removal fails - } finally { - setDeletingItemId(null); - setSelectedItem(null); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } catch { + // Ignore haptic failures. } - }, [selectedItem]); - // Render backdrop for bottom sheet - const renderBackdrop = useCallback( - (props: any) => ( - { + if (settings.continueWatchingCardStyle === 'poster') { + return ( + + ); + } + + return ( + - ), + ); + }, [ + computedItemHeight, + computedItemWidth, + computedPosterHeight, + computedPosterWidth, + currentTheme, + deletingItemId, + handleContentPress, + handleLongPress, + isLargeTablet, + isTV, + isTablet, + settings.continueWatchingCardStyle, + settings.posterBorderRadius, + t, + ]); + + const keyExtractor = useCallback( + (item: ContinueWatchingItem) => `continue-${item.id}-${item.type}`, [] ); - // Compute poster dimensions for poster-style cards - const computedPosterWidth = useMemo(() => { - switch (deviceType) { - case 'tv': - return 180; - case 'largeTablet': - return 160; - case 'tablet': - return 140; - default: - return 120; - } - }, [deviceType]); + const itemSeparator = useCallback( + () => , + [itemSpacing] + ); - const computedPosterHeight = useMemo(() => { - return computedPosterWidth * 1.5; // 2:3 aspect ratio - }, [computedPosterWidth]); - - // Memoized render function for poster-style continue watching items - const renderPosterStyleItem = useCallback(({ item }: { item: ContinueWatchingItem }) => { - return ( - handleContentPress(item)} - onLongPress={() => handleLongPress(item)} - delayLongPress={800} - > - {/* Poster Image */} - - - - {/* Gradient overlay */} - - - {/* Episode Info Overlay */} - {item.type === 'series' && item.season && item.episode && ( - - - S{item.season} E{item.episode} - - - )} - - {/* Up Next Badge */} - {item.type === 'series' && item.progress === 0 && ( - - {t('home.up_next_caps')} - - )} - - {/* Progress Bar */} - {item.progress > 0 && ( - - - - - - )} - - {/* Delete Indicator Overlay */} - {deletingItemId === item.id && ( - - - - )} - - - {/* Title below poster */} - - - {item.name} - - {item.progress > 0 && ( - - {Math.round(item.progress)}% - - )} - - - ); - }, [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedPosterWidth, computedPosterHeight, isTV, isLargeTablet, settings.posterBorderRadius]); - - // Memoized render function for wide-style continue watching items - const renderWideStyleItem = useCallback(({ item }: { item: ContinueWatchingItem }) => { - return ( - handleContentPress(item)} - onLongPress={() => handleLongPress(item)} - delayLongPress={800} - > - {/* Poster Image */} - - - - {/* Delete Indicator Overlay */} - {deletingItemId === item.id && ( - - - - )} - - - {/* Content Details */} - - {(() => { - const isUpNext = item.type === 'series' && item.progress === 0; - return ( - - - {item.name} - - {isUpNext && ( - - {t('home.up_next')} - - )} - - ); - })()} - - {/* Episode Info or Year */} - {(() => { - if (item.type === 'series' && item.season && item.episode) { - return ( - - - {t('home.season', { season: item.season })} - - {item.episodeTitle && ( - - {item.episodeTitle} - - )} - - ); - } else { - return ( - - {item.year} • {item.type === 'movie' ? t('home.movie') : t('home.series')} - - ); - } - })()} - - {/* Progress Bar */} - {item.progress > 0 && ( - - - - - - {t('home.percent_watched', { percent: Math.round(item.progress) })} - - - )} - - - ); - }, [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet, settings.posterBorderRadius, t]); - - // Choose the appropriate render function based on settings - const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => { - if (settings.continueWatchingCardStyle === 'poster') { - return renderPosterStyleItem({ item }); - } - return renderWideStyleItem({ item }); - }, [settings.continueWatchingCardStyle, renderPosterStyleItem, renderWideStyleItem]); - - // Memoized key extractor - const keyExtractor = useCallback((item: ContinueWatchingItem) => `continue-${item.id}-${item.type}`, []); - - // Memoized item separator - const ItemSeparator = useCallback(() => , [itemSpacing]); - - // If no continue watching items, don't render anything if (continueWatchingItems.length === 0) { return null; } return ( - + - {t('home.continue_watching')} - + + {t('home.continue_watching')} + + @@ -2363,457 +199,30 @@ const ContinueWatchingSection = React.forwardRef((props, re styles.wideList, { paddingLeft: horizontalPadding, - paddingRight: horizontalPadding - } + paddingRight: horizontalPadding, + }, ]} - ItemSeparatorComponent={ItemSeparator} + ItemSeparatorComponent={itemSeparator} onEndReachedThreshold={0.7} - onEndReached={() => { }} + onEndReached={() => {}} removeClippedSubviews={true} /> - {/* Action Sheet Bottom Sheet */} - { setSelectedItem(null); - onDismiss(actionSheetRef); + onDismiss(actionSheetRef)(); }} onChange={onChange(actionSheetRef)} - > - - {selectedItem && ( - <> - {/* Header with poster and info */} - - - - - {selectedItem.name} - - {selectedItem.type === 'series' && selectedItem.season && selectedItem.episode ? ( - - {t('home.season', { season: selectedItem.season })} · {t('home.episode', { episode: selectedItem.episode })} - {selectedItem.episodeTitle && selectedItem.episodeTitle !== `Episode ${selectedItem.episode}` && `\n${selectedItem.episodeTitle}`} - - ) : ( - - {selectedItem.year ? `${selectedItem.type === 'movie' ? t('home.movie') : t('home.series')} · ${selectedItem.year}` : selectedItem.type === 'movie' ? t('home.movie') : t('home.series')} - - )} - {selectedItem.progress > 0 && ( - - - - - - {t('home.percent_watched', { percent: Math.round(selectedItem.progress) })} - - - )} - - - - {/* Action Buttons */} - - - - {t('home.view_details')} - - - - - {t('home.remove')} - - - - )} - - + onViewDetails={handleViewDetails} + onRemoveItem={handleRemoveItem} + /> ); }); -const styles = StyleSheet.create({ - container: { - marginBottom: 28, - paddingTop: 0, - marginTop: 12, - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 16, - }, - titleContainer: { - position: 'relative', - }, - title: { - fontSize: 24, - fontWeight: '800', - letterSpacing: 0.5, - marginBottom: 4, - }, - titleUnderline: { - position: 'absolute', - bottom: -2, - left: 0, - width: 40, - height: 3, - borderRadius: 2, - opacity: 0.8, - }, - wideList: { - paddingBottom: 8, - paddingTop: 4, - }, - wideContentItem: { - width: 280, - height: 120, - flexDirection: 'row', - borderRadius: 12, - overflow: 'hidden', - elevation: 1, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 1, - borderWidth: 1.5, - borderColor: 'rgba(255,255,255,0.15)', - }, - posterContainer: { - width: 80, - height: '100%', - position: 'relative', - }, - continueWatchingPoster: { - width: '100%', - height: '100%', - borderTopLeftRadius: 12, - borderBottomLeftRadius: 12, - }, - deletingOverlay: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: 'rgba(0,0,0,0.7)', - justifyContent: 'center', - alignItems: 'center', - borderTopLeftRadius: 12, - borderBottomLeftRadius: 12, - }, - contentDetails: { - flex: 1, - padding: 12, - justifyContent: 'space-between', - }, - titleRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - marginBottom: 4, - }, - contentTitle: { - fontSize: 16, - fontWeight: '700', - flex: 1, - marginRight: 8, - }, - progressBadge: { - paddingHorizontal: 8, - paddingVertical: 3, - borderRadius: 12, - minWidth: 44, - alignItems: 'center', - }, - progressText: { - fontSize: 12, - fontWeight: '700', - color: '#FFFFFF', - }, - episodeRow: { - marginBottom: 8, - }, - episodeText: { - fontSize: 13, - fontWeight: '600', - marginBottom: 2, - }, - episodeTitle: { - fontSize: 12, - }, - yearText: { - fontSize: 13, - fontWeight: '500', - marginBottom: 8, - }, - wideProgressContainer: { - marginTop: 'auto', - }, - wideProgressTrack: { - height: 4, - backgroundColor: 'rgba(255,255,255,0.1)', - borderRadius: 2, - marginBottom: 4, - }, - wideProgressBar: { - height: '100%', - borderRadius: 2, - }, - progressLabel: { - fontSize: 11, - fontWeight: '500', - }, - // Keep old styles for backward compatibility - list: { - paddingHorizontal: 16, - paddingBottom: 8, - paddingTop: 4, - }, - contentItem: { - width: POSTER_WIDTH, - aspectRatio: 2 / 3, - margin: 0, - borderRadius: 12, - overflow: 'hidden', - position: 'relative', - elevation: 1, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 1, - borderWidth: 1.5, - borderColor: 'rgba(255,255,255,0.15)', - }, - contentItemContainer: { - width: '100%', - height: '100%', - borderRadius: 12, - overflow: 'hidden', - position: 'relative', - }, - poster: { - width: '100%', - height: '100%', - borderRadius: 12, - }, - episodeInfoContainer: { - position: 'absolute', - bottom: 3, - left: 0, - right: 0, - padding: 4, - paddingHorizontal: 8, - }, - episodeInfo: { - fontSize: 12, - fontWeight: 'bold', - }, - progressBarContainer: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - height: 3, - backgroundColor: 'rgba(0,0,0,0.5)', - }, - progressBar: { - height: '100%', - }, - // Poster-style card styles - posterContentItem: { - overflow: 'visible', - }, - posterImageContainer: { - width: '100%', - overflow: 'hidden', - position: 'relative', - borderWidth: 1.5, - borderColor: 'rgba(255,255,255,0.15)', - elevation: 1, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 1, - }, - posterImage: { - width: '100%', - height: '100%', - }, - posterGradient: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - height: '50%', - }, - posterEpisodeOverlay: { - position: 'absolute', - bottom: 8, - left: 8, - backgroundColor: 'rgba(0,0,0,0.7)', - paddingHorizontal: 6, - paddingVertical: 3, - borderRadius: 4, - }, - posterEpisodeText: { - color: '#FFFFFF', - fontWeight: '600', - }, - posterUpNextBadge: { - position: 'absolute', - top: 8, - right: 8, - paddingHorizontal: 6, - paddingVertical: 3, - borderRadius: 4, - }, - posterUpNextText: { - color: '#FFFFFF', - fontWeight: '700', - letterSpacing: 0.5, - }, - posterProgressContainer: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - }, - posterProgressTrack: { - height: 4, - }, - posterProgressBar: { - height: '100%', - }, - posterTitleContainer: { - paddingHorizontal: 4, - paddingVertical: 8, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - posterTitle: { - fontWeight: '600', - flex: 1, - lineHeight: 18, - }, - posterProgressLabel: { - fontWeight: '500', - marginLeft: 6, - }, - // Action Sheet Styles - actionSheetContent: { - flex: 1, - paddingHorizontal: 20, - paddingTop: 8, - }, - actionSheetHeader: { - flexDirection: 'row', - marginBottom: 20, - }, - actionSheetPoster: { - width: 70, - height: 105, - borderRadius: 10, - marginRight: 16, - }, - actionSheetInfo: { - flex: 1, - justifyContent: 'center', - }, - actionSheetTitle: { - fontSize: 18, - fontWeight: '700', - marginBottom: 6, - lineHeight: 22, - }, - actionSheetSubtitle: { - fontSize: 14, - opacity: 0.8, - lineHeight: 20, - }, - actionSheetProgressContainer: { - marginTop: 10, - }, - actionSheetProgressTrack: { - height: 4, - borderRadius: 2, - overflow: 'hidden', - }, - actionSheetProgressBar: { - height: '100%', - borderRadius: 2, - }, - actionSheetProgressText: { - fontSize: 12, - marginTop: 4, - }, - actionSheetButtons: { - flexDirection: 'row', - gap: 12, - }, - actionButton: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 14, - borderRadius: 14, - gap: 8, - }, - actionButtonSecondary: { - borderWidth: 0, - }, - actionButtonText: { - fontSize: 16, - fontWeight: '600', - color: '#fff', - }, -}); - -export default React.memo(ContinueWatchingSection, (prevProps, nextProps) => { - // This component has no props that would cause re-renders - return true; -}); +export default React.memo(ContinueWatchingSection, () => true); diff --git a/src/components/home/continueWatching/ContinueWatchingActionSheet.tsx b/src/components/home/continueWatching/ContinueWatchingActionSheet.tsx new file mode 100644 index 00000000..62f8932b --- /dev/null +++ b/src/components/home/continueWatching/ContinueWatchingActionSheet.tsx @@ -0,0 +1,191 @@ +import React from 'react'; +import { Text, TouchableOpacity, View } from 'react-native'; +import { + BottomSheetBackdrop, + BottomSheetModal, + BottomSheetView, +} from '@gorhom/bottom-sheet'; +import FastImage from '@d11/react-native-fast-image'; +import { Ionicons } from '@expo/vector-icons'; +import { useTranslation } from 'react-i18next'; +import { EdgeInsets } from 'react-native-safe-area-context'; + +import { Theme } from '../../../contexts/ThemeContext'; + +import { styles } from './styles'; +import { ContinueWatchingItem } from './types'; + +interface ContinueWatchingActionSheetProps { + actionSheetRef: React.RefObject; + currentTheme: Theme; + insets: EdgeInsets; + selectedItem: ContinueWatchingItem | null; + onDismiss: () => void; + onChange: (index: number) => void; + onViewDetails: () => void; + onRemoveItem: () => void; +} + +export function ContinueWatchingActionSheet({ + actionSheetRef, + currentTheme, + insets, + selectedItem, + onDismiss, + onChange, + onViewDetails, + onRemoveItem, +}: ContinueWatchingActionSheetProps) { + const { t } = useTranslation(); + + return ( + ( + + )} + backgroundStyle={{ + backgroundColor: currentTheme.colors.darkGray || '#0A0C0C', + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + }} + handleIndicatorStyle={{ + backgroundColor: currentTheme.colors.mediumGray, + width: 40, + }} + onDismiss={onDismiss} + onChange={onChange} + > + + {selectedItem ? ( + <> + + + + + + {selectedItem.name} + + + {selectedItem.type === 'series' && selectedItem.season && selectedItem.episode ? ( + + {t('home.season', { season: selectedItem.season })} ·{' '} + {t('home.episode', { episode: selectedItem.episode })} + {selectedItem.episodeTitle && + selectedItem.episodeTitle !== `Episode ${selectedItem.episode}` + ? `\n${selectedItem.episodeTitle}` + : ''} + + ) : ( + + {selectedItem.year + ? `${selectedItem.type === 'movie' ? t('home.movie') : t('home.series')} · ${selectedItem.year}` + : selectedItem.type === 'movie' + ? t('home.movie') + : t('home.series')} + + )} + + {selectedItem.progress > 0 ? ( + + + + + + {t('home.percent_watched', { + percent: Math.round(selectedItem.progress), + })} + + + ) : null} + + + + + + + {t('home.view_details')} + + + + + + {t('home.remove')} + + + + + ) : null} + + + ); +} diff --git a/src/components/home/continueWatching/ContinueWatchingPosterCard.tsx b/src/components/home/continueWatching/ContinueWatchingPosterCard.tsx new file mode 100644 index 00000000..079d9250 --- /dev/null +++ b/src/components/home/continueWatching/ContinueWatchingPosterCard.tsx @@ -0,0 +1,164 @@ +import React from 'react'; +import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native'; +import { LinearGradient } from 'expo-linear-gradient'; +import FastImage from '@d11/react-native-fast-image'; +import { TFunction } from 'i18next'; + +import { Theme } from '../../../contexts/ThemeContext'; + +import { styles } from './styles'; +import { ContinueWatchingItem } from './types'; + +interface ContinueWatchingPosterCardProps { + item: ContinueWatchingItem; + currentTheme: Theme; + deletingItemId: string | null; + computedPosterWidth: number; + computedPosterHeight: number; + isTV: boolean; + isLargeTablet: boolean; + posterBorderRadius: number; + onPress: (item: ContinueWatchingItem) => void; + onLongPress: (item: ContinueWatchingItem) => void; + t: TFunction; +} + +export const ContinueWatchingPosterCard = React.memo(({ + item, + currentTheme, + deletingItemId, + computedPosterWidth, + computedPosterHeight, + isTV, + isLargeTablet, + posterBorderRadius, + onPress, + onLongPress, + t, +}: ContinueWatchingPosterCardProps) => ( + onPress(item)} + onLongPress={() => onLongPress(item)} + delayLongPress={800} + > + + + + + + {item.type === 'series' && item.season && item.episode ? ( + + + S{item.season} E{item.episode} + + + ) : null} + + {item.type === 'series' && item.progress === 0 ? ( + + + {t('home.up_next_caps')} + + + ) : null} + + {item.progress > 0 ? ( + + + + + + ) : null} + + {deletingItemId === item.id ? ( + + + + ) : null} + + + + + {item.name} + + {item.progress > 0 ? ( + + {Math.round(item.progress)}% + + ) : null} + + +)); + +ContinueWatchingPosterCard.displayName = 'ContinueWatchingPosterCard'; diff --git a/src/components/home/continueWatching/ContinueWatchingWideCard.tsx b/src/components/home/continueWatching/ContinueWatchingWideCard.tsx new file mode 100644 index 00000000..aa7b41b9 --- /dev/null +++ b/src/components/home/continueWatching/ContinueWatchingWideCard.tsx @@ -0,0 +1,216 @@ +import React from 'react'; +import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native'; +import FastImage from '@d11/react-native-fast-image'; +import { TFunction } from 'i18next'; + +import { Theme } from '../../../contexts/ThemeContext'; + +import { styles } from './styles'; +import { ContinueWatchingItem } from './types'; + +interface ContinueWatchingWideCardProps { + item: ContinueWatchingItem; + currentTheme: Theme; + deletingItemId: string | null; + computedItemWidth: number; + computedItemHeight: number; + isTV: boolean; + isLargeTablet: boolean; + isTablet: boolean; + posterBorderRadius: number; + onPress: (item: ContinueWatchingItem) => void; + onLongPress: (item: ContinueWatchingItem) => void; + t: TFunction; +} + +export const ContinueWatchingWideCard = React.memo(({ + item, + currentTheme, + deletingItemId, + computedItemWidth, + computedItemHeight, + isTV, + isLargeTablet, + isTablet, + posterBorderRadius, + onPress, + onLongPress, + t, +}: ContinueWatchingWideCardProps) => { + const isUpNext = item.type === 'series' && item.progress === 0; + + return ( + onPress(item)} + onLongPress={() => onLongPress(item)} + delayLongPress={800} + > + + + + {deletingItemId === item.id ? ( + + + + ) : null} + + + + + + {item.name} + + + {isUpNext ? ( + + + {t('home.up_next')} + + + ) : null} + + + {item.type === 'series' && item.season && item.episode ? ( + + + {t('home.season', { season: item.season })} + + {item.episodeTitle ? ( + + {item.episodeTitle} + + ) : null} + + ) : ( + + {item.year} • {item.type === 'movie' ? t('home.movie') : t('home.series')} + + )} + + {item.progress > 0 ? ( + + + + + + {t('home.percent_watched', { percent: Math.round(item.progress) })} + + + ) : null} + + + ); +}); + +ContinueWatchingWideCard.displayName = 'ContinueWatchingWideCard'; diff --git a/src/components/home/continueWatching/constants.ts b/src/components/home/continueWatching/constants.ts new file mode 100644 index 00000000..1f82d3b7 --- /dev/null +++ b/src/components/home/continueWatching/constants.ts @@ -0,0 +1,12 @@ +export const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +} as const; + +export const REMOVAL_IGNORE_DURATION = 10000; +export const CACHE_DURATION = 5 * 60 * 1000; +export const TRAKT_SYNC_COOLDOWN = 0; +export const SIMKL_SYNC_COOLDOWN = 0; +export const TRAKT_RECONCILE_COOLDOWN = 0; diff --git a/src/components/home/continueWatching/dataShared.ts b/src/components/home/continueWatching/dataShared.ts new file mode 100644 index 00000000..e8f98bee --- /dev/null +++ b/src/components/home/continueWatching/dataShared.ts @@ -0,0 +1,259 @@ +import { MutableRefObject } from 'react'; + +import { StreamingContent, catalogService } from '../../../services/catalogService'; +import { storageService } from '../../../services/storageService'; +import { stremioService } from '../../../services/stremioService'; +import { TraktContentData } from '../../../services/traktService'; + +import { CACHE_DURATION } from './constants'; +import { + CachedMetadataEntry, + GetCachedMetadata, + LocalProgressEntry, +} from './dataTypes'; +import { ContinueWatchingItem } from './types'; +import { + compareContinueWatchingItems, + getContinueWatchingItemKey, + getContinueWatchingRemoveId, + getIdVariants, + isEpisodeReleased, + shouldPreferContinueWatchingCandidate, + toYearNumber, +} from './utils'; + +export function createGetCachedMetadata( + metadataCache: MutableRefObject> +): GetCachedMetadata { + return async (type: string, id: string, addonId?: string) => { + const cacheKey = `${type}:${id}:${addonId || 'default'}`; + const cached = metadataCache.current[cacheKey]; + const now = Date.now(); + + if (cached && now - cached.timestamp < CACHE_DURATION) { + return cached; + } + + try { + const shouldFetchMeta = await stremioService.isValidContentId(type, id); + const [metadata, basicContent, addonSpecificMeta] = await Promise.all([ + shouldFetchMeta ? stremioService.getMetaDetails(type, id) : Promise.resolve(null), + catalogService.getBasicContentDetails(type, id), + addonId + ? stremioService.getMetaDetails(type, id, addonId).catch(() => null) + : Promise.resolve(null), + ]); + + const preferredAddonMeta = addonSpecificMeta || metadata; + + const finalContent = basicContent + ? { + ...basicContent, + ...(preferredAddonMeta?.name && { name: preferredAddonMeta.name }), + ...(preferredAddonMeta?.poster && { poster: preferredAddonMeta.poster }), + ...(preferredAddonMeta?.description && { + description: preferredAddonMeta.description, + }), + } + : null; + + if (!finalContent) { + return null; + } + + const result: CachedMetadataEntry = { + metadata, + basicContent: finalContent as StreamingContent, + addonContent: preferredAddonMeta, + timestamp: now, + }; + + metadataCache.current[cacheKey] = result; + return result; + } catch { + return null; + } + }; +} + +export async function filterRemovedItems( + items: ContinueWatchingItem[], + recentlyRemoved: Set +): Promise { + const filtered: ContinueWatchingItem[] = []; + + for (const item of items) { + if (recentlyRemoved.has(getContinueWatchingItemKey(item))) { + continue; + } + + const isRemoved = await storageService.isContinueWatchingRemoved( + getContinueWatchingRemoveId(item), + item.type + ); + + if (!isRemoved) { + filtered.push(item); + } + } + + return filtered; +} + +export function dedupeLocalItems(items: ContinueWatchingItem[]): ContinueWatchingItem[] { + const map = new Map(); + + for (const item of items) { + const key = `${item.type}:${item.id}`; + const existing = map.get(key); + + if (!existing) { + map.set(key, item); + continue; + } + + if (shouldPreferContinueWatchingCandidate(item, existing)) { + const mergedLastUpdated = Math.max(item.lastUpdated ?? 0, existing.lastUpdated ?? 0); + map.set( + key, + mergedLastUpdated !== (item.lastUpdated ?? 0) + ? { ...item, lastUpdated: mergedLastUpdated } + : item + ); + } + } + + return Array.from(map.values()).sort(compareContinueWatchingItems); +} + +export function findNextEpisode( + currentSeason: number, + currentEpisode: number, + videos: any[], + watchedSet?: Set, + showId?: string, + localWatchedMap?: Map, + baseTimestamp: number = 0 +): { video: any; lastWatched: number } | null { + if (!videos || !Array.isArray(videos)) return null; + + const sortedVideos = [...videos].sort((a, b) => { + if (a.season !== b.season) return a.season - b.season; + return a.episode - b.episode; + }); + + let latestWatchedTimestamp = baseTimestamp; + + if (localWatchedMap && showId) { + const cleanShowId = showId.startsWith('tt') ? showId : `tt${showId}`; + for (const video of sortedVideos) { + const sig1 = `${cleanShowId}:${video.season}:${video.episode}`; + const sig2 = `${showId}:${video.season}:${video.episode}`; + latestWatchedTimestamp = Math.max( + latestWatchedTimestamp, + localWatchedMap.get(sig1) || 0, + localWatchedMap.get(sig2) || 0 + ); + } + } + + const isAlreadyWatched = (season: number, episode: number): boolean => { + if (!showId) return false; + + const cleanShowId = showId.startsWith('tt') ? showId : `tt${showId}`; + const sig1 = `${cleanShowId}:${season}:${episode}`; + const sig2 = `${showId}:${season}:${episode}`; + + if (watchedSet && (watchedSet.has(sig1) || watchedSet.has(sig2))) return true; + if (localWatchedMap && (localWatchedMap.has(sig1) || localWatchedMap.has(sig2))) return true; + + return false; + }; + + for (const video of sortedVideos) { + if (video.season < currentSeason) continue; + if (video.season === currentSeason && video.episode <= currentEpisode) continue; + if (isAlreadyWatched(video.season, video.episode)) continue; + + if (isEpisodeReleased(video)) { + return { video, lastWatched: latestWatchedTimestamp }; + } + } + + return null; +} + +export function buildTraktContentData( + item: ContinueWatchingItem +): TraktContentData | null { + if (item.type === 'movie') { + return { + type: 'movie', + imdbId: item.id, + title: item.name, + year: toYearNumber((item as any).year), + }; + } + + if (item.type === 'series' && item.season && item.episode) { + return { + type: 'episode', + imdbId: item.id, + title: item.episodeTitle || `S${item.season}E${item.episode}`, + season: item.season, + episode: item.episode, + showTitle: item.name, + showYear: toYearNumber((item as any).year), + showImdbId: item.id, + }; + } + + return null; +} + +export function getLocalMatches( + item: ContinueWatchingItem, + localProgressIndex: Map | null +): LocalProgressEntry[] { + if (!localProgressIndex) return []; + + const matches: LocalProgressEntry[] = []; + + for (const idVariant of getIdVariants(item.id)) { + const entries = localProgressIndex.get(`${item.type}:${idVariant}`); + if (!entries?.length) continue; + + if (item.type === 'movie') { + matches.push(...entries); + continue; + } + + if (item.season === undefined || item.episode === undefined) continue; + + for (const entry of entries) { + if (entry.season === item.season && entry.episode === item.episode) { + matches.push(entry); + } + } + } + + return matches; +} + +export function getMostRecentLocalMatch( + matches: LocalProgressEntry[] +): LocalProgressEntry | null { + return matches.reduce((acc, cur) => { + if (!acc) return cur; + return (cur.lastUpdated ?? 0) > (acc.lastUpdated ?? 0) ? cur : acc; + }, null); +} + +export function getHighestLocalMatch( + matches: LocalProgressEntry[] +): LocalProgressEntry | null { + return matches.reduce((acc, cur) => { + if (!acc) return cur; + return (cur.progressPercent ?? 0) > (acc.progressPercent ?? 0) ? cur : acc; + }, null); +} diff --git a/src/components/home/continueWatching/dataTypes.ts b/src/components/home/continueWatching/dataTypes.ts new file mode 100644 index 00000000..4d56caf5 --- /dev/null +++ b/src/components/home/continueWatching/dataTypes.ts @@ -0,0 +1,31 @@ +import { StreamingContent } from '../../../services/catalogService'; + +import { ContinueWatchingItem } from './types'; + +export interface CachedMetadataEntry { + metadata: any; + basicContent: StreamingContent | null; + addonContent?: any; + timestamp: number; +} + +export interface LocalProgressEntry { + episodeId?: string; + season?: number; + episode?: number; + progressPercent: number; + lastUpdated: number; + currentTime: number; + duration: number; +} + +export type GetCachedMetadata = ( + type: string, + id: string, + addonId?: string +) => Promise; + +export interface LoadLocalContinueWatchingResult { + items: ContinueWatchingItem[]; + shouldClearItems: boolean; +} diff --git a/src/components/home/continueWatching/loadLocalContinueWatching.ts b/src/components/home/continueWatching/loadLocalContinueWatching.ts new file mode 100644 index 00000000..8d51bbb0 --- /dev/null +++ b/src/components/home/continueWatching/loadLocalContinueWatching.ts @@ -0,0 +1,227 @@ +import { storageService } from '../../../services/storageService'; + +import { GetCachedMetadata, LoadLocalContinueWatchingResult } from './dataTypes'; +import { ContinueWatchingItem } from './types'; +import { findNextEpisode } from './dataShared'; +import { isSupportedId, parseEpisodeId } from './utils'; + +interface LoadLocalContinueWatchingParams { + getCachedMetadata: GetCachedMetadata; + traktMoviesSetPromise: Promise>; + traktShowsSetPromise: Promise>; + localWatchedShowsMapPromise: Promise>; +} + +export async function loadLocalContinueWatching({ + getCachedMetadata, + traktMoviesSetPromise, + traktShowsSetPromise, + localWatchedShowsMapPromise, +}: LoadLocalContinueWatchingParams): Promise { + const allProgress = await storageService.getAllWatchProgress(); + if (Object.keys(allProgress).length === 0) { + return { items: [], shouldClearItems: true }; + } + + const sortedProgress = Object.entries(allProgress) + .sort(([, a], [, b]) => b.lastUpdated - a.lastUpdated) + .slice(0, 30); + + const contentGroups: Record< + string, + { + type: string; + id: string; + episodes: Array<{ + key: string; + episodeId?: string; + progress: any; + progressPercent: number; + }>; + } + > = {}; + + for (const [key, progress] of sortedProgress) { + const [type, id, ...episodeIdParts] = key.split(':'); + const episodeId = episodeIdParts.length > 0 ? episodeIdParts.join(':') : undefined; + const progressPercent = + progress.duration > 0 ? (progress.currentTime / progress.duration) * 100 : 0; + + if ( + type === 'movie' && + (progressPercent >= 85 || !isFinite(progressPercent) || progressPercent <= 0) + ) { + continue; + } + + const contentKey = `${type}:${id}`; + if (!contentGroups[contentKey]) { + contentGroups[contentKey] = { type, id, episodes: [] }; + } + + contentGroups[contentKey].episodes.push({ key, episodeId, progress, progressPercent }); + } + + const batches = await Promise.all( + Object.values(contentGroups).map(async (group) => { + try { + if (!isSupportedId(group.id)) return []; + + if (group.type === 'movie') { + const watchedSet = await traktMoviesSetPromise; + const imdbId = group.id.startsWith('tt') ? group.id : `tt${group.id}`; + + if (watchedSet.has(imdbId)) { + try { + const existingMovieProgress = group.episodes[0]?.progress; + await storageService.setWatchProgress( + group.id, + 'movie', + { + currentTime: existingMovieProgress?.currentTime ?? 1, + duration: existingMovieProgress?.duration ?? 1, + lastUpdated: existingMovieProgress?.lastUpdated ?? Date.now(), + traktSynced: true, + traktProgress: 100, + } as any, + undefined, + { preserveTimestamp: true } + ); + } catch {} + return []; + } + } + + const cachedData = await getCachedMetadata( + group.type, + group.id, + group.episodes[0]?.progress?.addonId + ); + if (!cachedData?.basicContent) return []; + + const { metadata, basicContent } = cachedData; + const batch: ContinueWatchingItem[] = []; + + for (const episode of group.episodes) { + const { episodeId, progress, progressPercent } = episode; + + if (group.type === 'series' && progressPercent >= 85) { + const parsedEpisode = parseEpisodeId(episodeId); + + if ( + parsedEpisode?.season !== undefined && + parsedEpisode?.episode !== undefined && + metadata?.videos + ) { + const watchedEpisodesSet = await traktShowsSetPromise; + const localWatchedMap = await localWatchedShowsMapPromise; + const baseTimestamp = + progress.currentTime === 1 && progress.duration === 1 + ? 0 + : progress.lastUpdated; + + const nextEpisodeResult = findNextEpisode( + parsedEpisode.season, + parsedEpisode.episode, + metadata.videos, + watchedEpisodesSet, + group.id, + localWatchedMap, + baseTimestamp + ); + + if (nextEpisodeResult) { + const nextEpisode = nextEpisodeResult.video; + batch.push({ + ...basicContent, + progress: 0, + lastUpdated: nextEpisodeResult.lastWatched, + season: nextEpisode.season, + episode: nextEpisode.episode, + episodeTitle: nextEpisode.title || `Episode ${nextEpisode.episode}`, + addonId: progress.addonId, + } as ContinueWatchingItem); + } + } + + continue; + } + + let season: number | undefined; + let episodeNumber: number | undefined; + let episodeTitle: string | undefined; + let isWatchedOnTrakt = false; + + if (episodeId && group.type === 'series') { + const parsedEpisode = parseEpisodeId(episodeId); + season = parsedEpisode?.season; + episodeNumber = parsedEpisode?.episode; + + if (episodeNumber !== undefined) { + episodeTitle = `Episode ${episodeNumber}`; + } + + if (season !== undefined && episodeNumber !== undefined) { + const watchedEpisodesSet = await traktShowsSetPromise; + const localWatchedMap = await localWatchedShowsMapPromise; + const rawId = group.id.replace(/^tt/, ''); + const ttId = `tt${rawId}`; + + const signatures = [ + `${ttId}:${season}:${episodeNumber}`, + `${rawId}:${season}:${episodeNumber}`, + `${group.id}:${season}:${episodeNumber}`, + ]; + + isWatchedOnTrakt = signatures.some( + (signature) => + watchedEpisodesSet.has(signature) || localWatchedMap.has(signature) + ); + + if (isWatchedOnTrakt) { + try { + await storageService.setWatchProgress( + group.id, + 'series', + { + currentTime: progress.currentTime ?? 1, + duration: progress.duration ?? 1, + lastUpdated: progress.lastUpdated, + traktSynced: true, + traktProgress: 100, + } as any, + episodeId, + { preserveTimestamp: true } + ); + } catch {} + } + } + } + + if (isWatchedOnTrakt) { + continue; + } + + batch.push({ + ...basicContent, + progress: progressPercent, + lastUpdated: progress.lastUpdated, + season, + episode: episodeNumber, + episodeTitle, + addonId: progress.addonId, + } as ContinueWatchingItem); + } + + return batch; + } catch { + return []; + } + }) + ); + + return { + items: batches.flat(), + shouldClearItems: false, + }; +} diff --git a/src/components/home/continueWatching/mergeSimklContinueWatching.ts b/src/components/home/continueWatching/mergeSimklContinueWatching.ts new file mode 100644 index 00000000..b18c8287 --- /dev/null +++ b/src/components/home/continueWatching/mergeSimklContinueWatching.ts @@ -0,0 +1,198 @@ +import { Dispatch, MutableRefObject, SetStateAction } from 'react'; + +import { SimklService } from '../../../services/simklService'; +import { logger } from '../../../utils/logger'; + +import { SIMKL_SYNC_COOLDOWN } from './constants'; +import { GetCachedMetadata, LocalProgressEntry } from './dataTypes'; +import { + filterRemovedItems, + findNextEpisode, + getLocalMatches, + getMostRecentLocalMatch, +} from './dataShared'; +import { ContinueWatchingItem } from './types'; +import { compareContinueWatchingItems } from './utils'; + +interface MergeSimklContinueWatchingParams { + simklService: SimklService; + getCachedMetadata: GetCachedMetadata; + localProgressIndex: Map | null; + traktShowsSetPromise: Promise>; + localWatchedShowsMapPromise: Promise>; + recentlyRemoved: Set; + lastSimklSyncRef: MutableRefObject; + setContinueWatchingItems: Dispatch>; +} + +export async function mergeSimklContinueWatching({ + simklService, + getCachedMetadata, + localProgressIndex, + traktShowsSetPromise, + localWatchedShowsMapPromise, + recentlyRemoved, + lastSimklSyncRef, + setContinueWatchingItems, +}: MergeSimklContinueWatchingParams): Promise { + const now = Date.now(); + if ( + SIMKL_SYNC_COOLDOWN > 0 && + now - lastSimklSyncRef.current < SIMKL_SYNC_COOLDOWN + ) { + return; + } + lastSimklSyncRef.current = now; + + const playbackItems = await simklService.getPlaybackStatus(); + const simklBatch: ContinueWatchingItem[] = []; + const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000); + + const sortedPlaybackItems = [...playbackItems] + .sort((a, b) => new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime()) + .slice(0, 30); + + for (const item of sortedPlaybackItems) { + try { + if ((item.progress ?? 0) < 2) continue; + + const pausedAt = new Date(item.paused_at).getTime(); + if (pausedAt < thirtyDaysAgo) continue; + + if (item.type === 'movie' && item.movie?.ids?.imdb) { + if (item.progress >= 85) continue; + + const imdbId = item.movie.ids.imdb.startsWith('tt') + ? item.movie.ids.imdb + : `tt${item.movie.ids.imdb}`; + + if (recentlyRemoved.has(`movie:${imdbId}`)) continue; + + const cachedData = await getCachedMetadata('movie', imdbId); + if (!cachedData?.basicContent) continue; + + simklBatch.push({ + ...cachedData.basicContent, + id: imdbId, + type: 'movie', + progress: item.progress, + lastUpdated: pausedAt, + addonId: undefined, + } as ContinueWatchingItem); + } else if (item.type === 'episode' && item.show?.ids?.imdb && item.episode) { + const showImdb = item.show.ids.imdb.startsWith('tt') + ? item.show.ids.imdb + : `tt${item.show.ids.imdb}`; + const episodeNum = item.episode.episode ?? item.episode.number; + + if (episodeNum === undefined || episodeNum === null) { + continue; + } + + if (recentlyRemoved.has(`series:${showImdb}`)) continue; + + const cachedData = await getCachedMetadata('series', showImdb); + if (!cachedData?.basicContent) continue; + + if (item.progress >= 85) { + if (cachedData.metadata?.videos) { + const watchedEpisodesSet = await traktShowsSetPromise; + const localWatchedMap = await localWatchedShowsMapPromise; + const nextEpisodeResult = findNextEpisode( + item.episode.season, + episodeNum, + cachedData.metadata.videos, + watchedEpisodesSet, + showImdb, + localWatchedMap, + pausedAt + ); + + if (nextEpisodeResult) { + const nextEpisode = nextEpisodeResult.video; + simklBatch.push({ + ...cachedData.basicContent, + id: showImdb, + type: 'series', + progress: 0, + lastUpdated: nextEpisodeResult.lastWatched, + season: nextEpisode.season, + episode: nextEpisode.episode, + episodeTitle: nextEpisode.title || `Episode ${nextEpisode.episode}`, + addonId: undefined, + } as ContinueWatchingItem); + } + } + + continue; + } + + simklBatch.push({ + ...cachedData.basicContent, + id: showImdb, + type: 'series', + progress: item.progress, + lastUpdated: pausedAt, + season: item.episode.season, + episode: episodeNum, + episodeTitle: item.episode.title || `Episode ${episodeNum}`, + addonId: undefined, + } as ContinueWatchingItem); + } + } catch { + // Keep processing remaining playback items. + } + } + + if (simklBatch.length === 0) { + setContinueWatchingItems([]); + return; + } + + const deduped = new Map(); + for (const item of simklBatch) { + const key = `${item.type}:${item.id}`; + const existing = deduped.get(key); + if (!existing || (item.lastUpdated ?? 0) > (existing.lastUpdated ?? 0)) { + deduped.set(key, item); + } + } + + const filteredItems = await filterRemovedItems(Array.from(deduped.values()), recentlyRemoved); + + const adjustedItems = filteredItems.map((item) => { + const matches = getLocalMatches(item, localProgressIndex); + if (matches.length === 0) return item; + + const mostRecentLocal = getMostRecentLocalMatch(matches); + if (!mostRecentLocal) return item; + + const localProgress = mostRecentLocal.progressPercent; + const simklProgress = item.progress ?? 0; + const localTs = mostRecentLocal.lastUpdated ?? 0; + const simklTs = item.lastUpdated ?? 0; + + const isAhead = isFinite(localProgress) && localProgress > simklProgress + 0.5; + const isLocalNewer = localTs > simklTs + 5000; + + if (isAhead || isLocalNewer) { + return { + ...item, + progress: localProgress, + lastUpdated: localTs > 0 ? localTs : item.lastUpdated, + } as ContinueWatchingItem; + } + + if (localTs > 0 && localTs > simklTs) { + return { + ...item, + lastUpdated: localTs, + } as ContinueWatchingItem; + } + + return item; + }); + + adjustedItems.sort(compareContinueWatchingItems); + setContinueWatchingItems(adjustedItems); +} diff --git a/src/components/home/continueWatching/mergeTraktContinueWatching.ts b/src/components/home/continueWatching/mergeTraktContinueWatching.ts new file mode 100644 index 00000000..b4b8e304 --- /dev/null +++ b/src/components/home/continueWatching/mergeTraktContinueWatching.ts @@ -0,0 +1,428 @@ +import { Dispatch, MutableRefObject, SetStateAction } from 'react'; + +import { storageService } from '../../../services/storageService'; +import { + TraktService, + TraktWatchedItem, +} from '../../../services/traktService'; +import { logger } from '../../../utils/logger'; + +import { TRAKT_RECONCILE_COOLDOWN, TRAKT_SYNC_COOLDOWN } from './constants'; +import { GetCachedMetadata, LocalProgressEntry } from './dataTypes'; +import { + buildTraktContentData, + filterRemovedItems, + findNextEpisode, + getHighestLocalMatch, + getLocalMatches, + getMostRecentLocalMatch, +} from './dataShared'; +import { ContinueWatchingItem } from './types'; +import { compareContinueWatchingItems } from './utils'; + +interface MergeTraktContinueWatchingParams { + traktService: TraktService; + getCachedMetadata: GetCachedMetadata; + localProgressIndex: Map | null; + localWatchedShowsMapPromise: Promise>; + recentlyRemoved: Set; + lastTraktSyncRef: MutableRefObject; + lastTraktReconcileRef: MutableRefObject>; + setContinueWatchingItems: Dispatch>; +} + +export async function mergeTraktContinueWatching({ + traktService, + getCachedMetadata, + localProgressIndex, + localWatchedShowsMapPromise, + recentlyRemoved, + lastTraktSyncRef, + lastTraktReconcileRef, + setContinueWatchingItems, +}: MergeTraktContinueWatchingParams): Promise { + const now = Date.now(); + if ( + TRAKT_SYNC_COOLDOWN > 0 && + now - lastTraktSyncRef.current < TRAKT_SYNC_COOLDOWN + ) { + logger.log( + `[TraktSync] Skipping Trakt sync - cooldown active (${Math.round((TRAKT_SYNC_COOLDOWN - (now - lastTraktSyncRef.current)) / 1000)}s remaining)` + ); + return; + } + + lastTraktSyncRef.current = now; + const playbackItems = await traktService.getPlaybackProgress(); + const traktBatch: ContinueWatchingItem[] = []; + + let watchedShowsData: TraktWatchedItem[] = []; + const watchedEpisodeSetByShow = new Map>(); + + try { + watchedShowsData = await traktService.getWatchedShows(); + for (const watchedShow of watchedShowsData) { + if (!watchedShow.show?.ids?.imdb) continue; + + const imdb = watchedShow.show.ids.imdb.startsWith('tt') + ? watchedShow.show.ids.imdb + : `tt${watchedShow.show.ids.imdb}`; + const resetAt = watchedShow.reset_at ? new Date(watchedShow.reset_at).getTime() : 0; + const episodeSet = new Set(); + + if (watchedShow.seasons) { + for (const season of watchedShow.seasons) { + for (const episode of season.episodes) { + if (resetAt > 0) { + const watchedAt = new Date(episode.last_watched_at).getTime(); + if (watchedAt < resetAt) continue; + } + + episodeSet.add(`${imdb}:${season.number}:${episode.number}`); + } + } + } + + watchedEpisodeSetByShow.set(imdb, episodeSet); + } + } catch { + // Continue without watched-show acceleration. + } + + const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000); + const sortedPlaybackItems = [...playbackItems] + .sort((a, b) => new Date(b.paused_at).getTime() - new Date(a.paused_at).getTime()) + .slice(0, 30); + + for (const item of sortedPlaybackItems) { + try { + if (item.progress < 2) continue; + + const pausedAt = new Date(item.paused_at).getTime(); + if (pausedAt < thirtyDaysAgo) continue; + + if (item.type === 'movie' && item.movie?.ids?.imdb) { + if (item.progress >= 85) continue; + + const imdbId = item.movie.ids.imdb.startsWith('tt') + ? item.movie.ids.imdb + : `tt${item.movie.ids.imdb}`; + + if (recentlyRemoved.has(`movie:${imdbId}`)) continue; + + const cachedData = await getCachedMetadata('movie', imdbId); + if (!cachedData?.basicContent) continue; + + traktBatch.push({ + ...cachedData.basicContent, + id: imdbId, + type: 'movie', + progress: item.progress, + lastUpdated: pausedAt, + addonId: undefined, + traktPlaybackId: item.id, + } as ContinueWatchingItem); + } else if (item.type === 'episode' && item.show?.ids?.imdb && item.episode) { + const showImdb = item.show.ids.imdb.startsWith('tt') + ? item.show.ids.imdb + : `tt${item.show.ids.imdb}`; + + if (recentlyRemoved.has(`series:${showImdb}`)) continue; + + const cachedData = await getCachedMetadata('series', showImdb); + if (!cachedData?.basicContent) continue; + + if (item.progress >= 85) { + if (cachedData.metadata?.videos) { + const watchedSetForShow = watchedEpisodeSetByShow.get(showImdb); + const localWatchedMap = await localWatchedShowsMapPromise; + const nextEpisodeResult = findNextEpisode( + item.episode.season, + item.episode.number, + cachedData.metadata.videos, + watchedSetForShow, + showImdb, + localWatchedMap, + pausedAt + ); + + if (nextEpisodeResult) { + const nextEpisode = nextEpisodeResult.video; + traktBatch.push({ + ...cachedData.basicContent, + id: showImdb, + type: 'series', + progress: 0, + lastUpdated: nextEpisodeResult.lastWatched, + season: nextEpisode.season, + episode: nextEpisode.episode, + episodeTitle: nextEpisode.title || `Episode ${nextEpisode.episode}`, + addonId: undefined, + traktPlaybackId: item.id, + } as ContinueWatchingItem); + } + } + + continue; + } + + traktBatch.push({ + ...cachedData.basicContent, + id: showImdb, + type: 'series', + progress: item.progress, + lastUpdated: pausedAt, + season: item.episode.season, + episode: item.episode.number, + episodeTitle: item.episode.title || `Episode ${item.episode.number}`, + addonId: undefined, + traktPlaybackId: item.id, + } as ContinueWatchingItem); + } + } catch { + // Continue with remaining playback items. + } + } + + try { + const thirtyDaysAgoForShows = Date.now() - (30 * 24 * 60 * 60 * 1000); + + for (const watchedShow of watchedShowsData) { + try { + if (!watchedShow.show?.ids?.imdb) continue; + + const lastWatchedAt = new Date(watchedShow.last_watched_at).getTime(); + if (lastWatchedAt < thirtyDaysAgoForShows) continue; + + const showImdb = watchedShow.show.ids.imdb.startsWith('tt') + ? watchedShow.show.ids.imdb + : `tt${watchedShow.show.ids.imdb}`; + + if (recentlyRemoved.has(`series:${showImdb}`)) continue; + + const resetAt = watchedShow.reset_at ? new Date(watchedShow.reset_at).getTime() : 0; + let lastWatchedSeason = 0; + let lastWatchedEpisode = 0; + let latestEpisodeTimestamp = 0; + + if (watchedShow.seasons) { + for (const season of watchedShow.seasons) { + for (const episode of season.episodes) { + const episodeTimestamp = new Date(episode.last_watched_at).getTime(); + if (resetAt > 0 && episodeTimestamp < resetAt) continue; + + if (episodeTimestamp > latestEpisodeTimestamp) { + latestEpisodeTimestamp = episodeTimestamp; + lastWatchedSeason = season.number; + lastWatchedEpisode = episode.number; + } + } + } + } + + if (lastWatchedSeason === 0 && lastWatchedEpisode === 0) continue; + + const cachedData = await getCachedMetadata('series', showImdb); + if (!cachedData?.basicContent || !cachedData.metadata?.videos) continue; + + const watchedEpisodeSet = watchedEpisodeSetByShow.get(showImdb) ?? new Set(); + const localWatchedMap = await localWatchedShowsMapPromise; + const nextEpisodeResult = findNextEpisode( + lastWatchedSeason, + lastWatchedEpisode, + cachedData.metadata.videos, + watchedEpisodeSet, + showImdb, + localWatchedMap, + latestEpisodeTimestamp + ); + + if (nextEpisodeResult) { + const nextEpisode = nextEpisodeResult.video; + traktBatch.push({ + ...cachedData.basicContent, + id: showImdb, + type: 'series', + progress: 0, + lastUpdated: nextEpisodeResult.lastWatched, + season: nextEpisode.season, + episode: nextEpisode.episode, + episodeTitle: nextEpisode.title || `Episode ${nextEpisode.episode}`, + addonId: undefined, + } as ContinueWatchingItem); + } + } catch { + // Continue with remaining watched shows. + } + } + } catch (err) { + logger.warn('[TraktSync] Error fetching watched shows for Up Next:', err); + } + + if (traktBatch.length === 0) { + return; + } + + const deduped = new Map(); + for (const item of traktBatch) { + const key = `${item.type}:${item.id}`; + const existing = deduped.get(key); + + if (!existing) { + deduped.set(key, item); + continue; + } + + const existingHasProgress = (existing.progress ?? 0) > 0; + const candidateHasProgress = (item.progress ?? 0) > 0; + + if (candidateHasProgress && !existingHasProgress) { + const mergedTs = Math.max(item.lastUpdated ?? 0, existing.lastUpdated ?? 0); + deduped.set( + key, + mergedTs !== (item.lastUpdated ?? 0) + ? { ...item, lastUpdated: mergedTs } + : item + ); + } else if (!candidateHasProgress && existingHasProgress) { + if ((item.lastUpdated ?? 0) > (existing.lastUpdated ?? 0)) { + deduped.set(key, { ...existing, lastUpdated: item.lastUpdated }); + } + } else if ((item.lastUpdated ?? 0) > (existing.lastUpdated ?? 0)) { + deduped.set(key, item); + } + } + + const filteredItems = await filterRemovedItems(Array.from(deduped.values()), recentlyRemoved); + const reconcilePromises: Promise[] = []; + const reconcileLocalPromises: Promise[] = []; + + const adjustedItems = filteredItems + .map((item) => { + const matches = getLocalMatches(item, localProgressIndex); + if (matches.length === 0) return item; + + const mostRecentLocal = getMostRecentLocalMatch(matches); + const highestLocal = getHighestLocalMatch(matches); + + if (!mostRecentLocal || !highestLocal) { + return item; + } + + const mergedLastUpdated = Math.max( + mostRecentLocal.lastUpdated ?? 0, + item.lastUpdated ?? 0 + ); + const localProgress = mostRecentLocal.progressPercent; + const traktProgress = item.progress ?? 0; + const traktTs = item.lastUpdated ?? 0; + const localTs = mostRecentLocal.lastUpdated ?? 0; + + const isAhead = isFinite(localProgress) && localProgress > traktProgress + 0.5; + const isLocalNewer = localTs > traktTs + 5000; + const isLocalRecent = localTs > 0 && Date.now() - localTs < 5 * 60 * 1000; + const isDifferent = Math.abs((localProgress || 0) - (traktProgress || 0)) > 0.5; + const isTraktAhead = isFinite(traktProgress) && traktProgress > localProgress + 0.5; + + if (isTraktAhead && !isLocalRecent && mostRecentLocal.duration > 0) { + const reconcileKey = `local:${item.type}:${item.id}:${item.season ?? ''}:${item.episode ?? ''}`; + const last = lastTraktReconcileRef.current.get(reconcileKey) ?? 0; + const now = Date.now(); + + if (now - last >= TRAKT_RECONCILE_COOLDOWN) { + lastTraktReconcileRef.current.set(reconcileKey, now); + + const targetEpisodeId = + item.type === 'series' + ? mostRecentLocal.episodeId || + (item.season && item.episode + ? `${item.id}:${item.season}:${item.episode}` + : undefined) + : undefined; + + const newCurrentTime = (traktProgress / 100) * mostRecentLocal.duration; + + reconcileLocalPromises.push( + (async () => { + try { + const existing = await storageService.getWatchProgress( + item.id, + item.type, + targetEpisodeId + ); + + if (!existing || !existing.duration || existing.duration <= 0) { + return; + } + + await storageService.setWatchProgress( + item.id, + item.type, + { + ...existing, + currentTime: Math.max(existing.currentTime ?? 0, newCurrentTime), + duration: existing.duration, + traktSynced: true, + traktLastSynced: Date.now(), + traktProgress: Math.max(existing.traktProgress ?? 0, traktProgress), + lastUpdated: existing.lastUpdated, + } as any, + targetEpisodeId, + { preserveTimestamp: true, forceWrite: true } + ); + } catch { + // Ignore background sync failures. + } + })() + ); + } + } + + if ((isAhead || ((isLocalNewer || isLocalRecent) && isDifferent)) && localProgress >= 2) { + const reconcileKey = `${item.type}:${item.id}:${item.season ?? ''}:${item.episode ?? ''}`; + const last = lastTraktReconcileRef.current.get(reconcileKey) ?? 0; + const now = Date.now(); + + if (now - last >= TRAKT_RECONCILE_COOLDOWN) { + lastTraktReconcileRef.current.set(reconcileKey, now); + + const contentData = buildTraktContentData(item); + if (contentData) { + const progressToSend = + localProgress >= 85 + ? Math.min(localProgress, 100) + : Math.min(localProgress, 79.9); + + reconcilePromises.push( + traktService.pauseWatching(contentData, progressToSend).catch(() => null) + ); + } + } + } + + if (((isLocalNewer || isLocalRecent) && isDifferent) || isAhead) { + return { + ...item, + progress: localProgress, + lastUpdated: mergedLastUpdated, + }; + } + + return { + ...item, + lastUpdated: mergedLastUpdated, + }; + }) + .filter((item) => (item.progress ?? 0) < 85); + + adjustedItems.sort(compareContinueWatchingItems); + setContinueWatchingItems(adjustedItems); + + if (reconcilePromises.length > 0) { + Promise.allSettled(reconcilePromises).catch(() => null); + } + + if (reconcileLocalPromises.length > 0) { + Promise.allSettled(reconcileLocalPromises).catch(() => null); + } +} diff --git a/src/components/home/continueWatching/styles.ts b/src/components/home/continueWatching/styles.ts new file mode 100644 index 00000000..e9f20c5a --- /dev/null +++ b/src/components/home/continueWatching/styles.ts @@ -0,0 +1,283 @@ +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + container: { + marginBottom: 28, + paddingTop: 0, + marginTop: 12, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 16, + }, + titleContainer: { + position: 'relative', + }, + title: { + fontSize: 24, + fontWeight: '800', + letterSpacing: 0.5, + marginBottom: 4, + }, + titleUnderline: { + position: 'absolute', + bottom: -2, + left: 0, + width: 40, + height: 3, + borderRadius: 2, + opacity: 0.8, + }, + wideList: { + paddingBottom: 8, + paddingTop: 4, + }, + wideContentItem: { + width: 280, + height: 120, + flexDirection: 'row', + borderRadius: 12, + overflow: 'hidden', + elevation: 1, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 1, + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.15)', + }, + posterContainer: { + width: 80, + height: '100%', + position: 'relative', + }, + continueWatchingPoster: { + width: '100%', + height: '100%', + borderTopLeftRadius: 12, + borderBottomLeftRadius: 12, + }, + deletingOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0,0,0,0.7)', + justifyContent: 'center', + alignItems: 'center', + borderTopLeftRadius: 12, + borderBottomLeftRadius: 12, + }, + contentDetails: { + flex: 1, + padding: 12, + justifyContent: 'space-between', + }, + titleRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 4, + }, + contentTitle: { + fontSize: 16, + fontWeight: '700', + flex: 1, + marginRight: 8, + }, + progressBadge: { + paddingHorizontal: 8, + paddingVertical: 3, + borderRadius: 12, + minWidth: 44, + alignItems: 'center', + }, + progressText: { + fontSize: 12, + fontWeight: '700', + color: '#FFFFFF', + }, + episodeRow: { + marginBottom: 8, + }, + episodeText: { + fontSize: 13, + fontWeight: '600', + marginBottom: 2, + }, + episodeTitle: { + fontSize: 12, + }, + yearText: { + fontSize: 13, + fontWeight: '500', + marginBottom: 8, + }, + wideProgressContainer: { + marginTop: 'auto', + }, + wideProgressTrack: { + height: 4, + backgroundColor: 'rgba(255,255,255,0.1)', + borderRadius: 2, + marginBottom: 4, + }, + wideProgressBar: { + height: '100%', + borderRadius: 2, + }, + progressLabel: { + fontSize: 11, + fontWeight: '500', + }, + posterContentItem: { + overflow: 'visible', + }, + posterImageContainer: { + width: '100%', + overflow: 'hidden', + position: 'relative', + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.15)', + elevation: 1, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 1, + }, + posterImage: { + width: '100%', + height: '100%', + }, + posterGradient: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: '50%', + }, + posterEpisodeOverlay: { + position: 'absolute', + bottom: 8, + left: 8, + backgroundColor: 'rgba(0,0,0,0.7)', + paddingHorizontal: 6, + paddingVertical: 3, + borderRadius: 4, + }, + posterEpisodeText: { + color: '#FFFFFF', + fontWeight: '600', + }, + posterUpNextBadge: { + position: 'absolute', + top: 8, + right: 8, + paddingHorizontal: 6, + paddingVertical: 3, + borderRadius: 4, + }, + posterUpNextText: { + color: '#FFFFFF', + fontWeight: '700', + letterSpacing: 0.5, + }, + posterProgressContainer: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + }, + posterProgressTrack: { + height: 4, + }, + posterProgressBar: { + height: '100%', + }, + posterTitleContainer: { + paddingHorizontal: 4, + paddingVertical: 8, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + }, + posterTitle: { + fontWeight: '600', + flex: 1, + lineHeight: 18, + }, + posterProgressLabel: { + fontWeight: '500', + marginLeft: 6, + }, + actionSheetContent: { + flex: 1, + paddingHorizontal: 20, + paddingTop: 8, + }, + actionSheetHeader: { + flexDirection: 'row', + marginBottom: 20, + }, + actionSheetPoster: { + width: 70, + height: 105, + borderRadius: 10, + marginRight: 16, + }, + actionSheetInfo: { + flex: 1, + justifyContent: 'center', + }, + actionSheetTitle: { + fontSize: 18, + fontWeight: '700', + marginBottom: 6, + lineHeight: 22, + }, + actionSheetSubtitle: { + fontSize: 14, + opacity: 0.8, + lineHeight: 20, + }, + actionSheetProgressContainer: { + marginTop: 10, + }, + actionSheetProgressTrack: { + height: 4, + borderRadius: 2, + overflow: 'hidden', + }, + actionSheetProgressBar: { + height: '100%', + borderRadius: 2, + }, + actionSheetProgressText: { + fontSize: 12, + marginTop: 4, + }, + actionSheetButtons: { + flexDirection: 'row', + gap: 12, + }, + actionButton: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 14, + borderRadius: 14, + gap: 8, + }, + actionButtonSecondary: { + borderWidth: 0, + }, + actionButtonText: { + fontSize: 16, + fontWeight: '600', + color: '#fff', + }, +}); diff --git a/src/components/home/continueWatching/types.ts b/src/components/home/continueWatching/types.ts new file mode 100644 index 00000000..e68a9c05 --- /dev/null +++ b/src/components/home/continueWatching/types.ts @@ -0,0 +1,20 @@ +import { StreamingContent } from '../../../services/catalogService'; + +export interface ContinueWatchingItem extends StreamingContent { + progress: number; + lastUpdated: number; + season?: number; + episode?: number; + episodeTitle?: string; + addonId?: string; + addonPoster?: string; + addonName?: string; + addonDescription?: string; + traktPlaybackId?: number; +} + +export interface ContinueWatchingRef { + refresh: () => Promise; +} + +export type ContinueWatchingDeviceType = 'phone' | 'tablet' | 'largeTablet' | 'tv'; diff --git a/src/components/home/continueWatching/useContinueWatchingData.ts b/src/components/home/continueWatching/useContinueWatchingData.ts new file mode 100644 index 00000000..a82cc4af --- /dev/null +++ b/src/components/home/continueWatching/useContinueWatchingData.ts @@ -0,0 +1,407 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { AppState, AppStateStatus } from 'react-native'; +import { useFocusEffect } from '@react-navigation/native'; + +import { SimklService } from '../../../services/simklService'; +import { storageService } from '../../../services/storageService'; +import { TraktService } from '../../../services/traktService'; +import { watchedService } from '../../../services/watchedService'; + +import { REMOVAL_IGNORE_DURATION } from './constants'; +import { + createGetCachedMetadata, + dedupeLocalItems, + filterRemovedItems, +} from './dataShared'; +import { CachedMetadataEntry, LocalProgressEntry } from './dataTypes'; +import { loadLocalContinueWatching } from './loadLocalContinueWatching'; +import { mergeSimklContinueWatching } from './mergeSimklContinueWatching'; +import { mergeTraktContinueWatching } from './mergeTraktContinueWatching'; +import { ContinueWatchingItem } from './types'; +import { getContinueWatchingItemKey, getContinueWatchingRemoveId, getIdVariants, parseEpisodeId } from './utils'; + +async function getTraktMoviesSet( + isTraktAuthed: boolean, + traktService: TraktService +): Promise> { + try { + if (!isTraktAuthed || typeof (traktService as any).getWatchedMovies !== 'function') { + return new Set(); + } + + const watched = await (traktService as any).getWatchedMovies(); + const watchedSet = new Set(); + + if (Array.isArray(watched)) { + watched.forEach((movie: any) => { + const ids = movie?.movie?.ids; + if (!ids) return; + + if (ids.imdb) { + watchedSet.add(ids.imdb.startsWith('tt') ? ids.imdb : `tt${ids.imdb}`); + } + if (ids.tmdb) { + watchedSet.add(ids.tmdb.toString()); + } + }); + } + + return watchedSet; + } catch { + return new Set(); + } +} + +async function getTraktShowsSet( + isTraktAuthed: boolean, + traktService: TraktService +): Promise> { + try { + if (!isTraktAuthed || typeof (traktService as any).getWatchedShows !== 'function') { + return new Set(); + } + + const watched = await (traktService as any).getWatchedShows(); + const watchedSet = new Set(); + + if (Array.isArray(watched)) { + watched.forEach((show: any) => { + const ids = show?.show?.ids; + if (!ids) return; + + const imdbId = ids.imdb; + const tmdbId = ids.tmdb; + + if (!Array.isArray(show.seasons)) return; + + show.seasons.forEach((season: any) => { + if (!Array.isArray(season.episodes)) return; + + season.episodes.forEach((episode: any) => { + if (imdbId) { + const cleanImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`; + watchedSet.add(`${cleanImdbId}:${season.number}:${episode.number}`); + } + if (tmdbId) { + watchedSet.add(`${tmdbId}:${season.number}:${episode.number}`); + } + }); + }); + }); + } + + return watchedSet; + } catch { + return new Set(); + } +} + +async function getLocalWatchedShowsMap(): Promise> { + try { + const watched = await watchedService.getAllWatchedItems(); + const watchedMap = new Map(); + + watched.forEach((item) => { + if (!item.content_id) return; + + const cleanId = item.content_id.startsWith('tt') + ? item.content_id + : `tt${item.content_id}`; + + if (item.season != null && item.episode != null) { + watchedMap.set(`${cleanId}:${item.season}:${item.episode}`, item.watched_at); + watchedMap.set(`${item.content_id}:${item.season}:${item.episode}`, item.watched_at); + } else { + watchedMap.set(cleanId, item.watched_at); + watchedMap.set(item.content_id, item.watched_at); + } + }); + + return watchedMap; + } catch { + return new Map(); + } +} + +async function buildLocalProgressIndex( + shouldBuild: boolean +): Promise | null> { + if (!shouldBuild) { + return null; + } + + try { + const allProgress = await storageService.getAllWatchProgress(); + const index = new Map(); + + for (const [key, progress] of Object.entries(allProgress)) { + const [type, id, ...episodeIdParts] = key.split(':'); + const episodeId = episodeIdParts.length > 0 ? episodeIdParts.join(':') : undefined; + + const progressPercent = + progress?.duration > 0 ? (progress.currentTime / progress.duration) * 100 : 0; + + if (!isFinite(progressPercent) || progressPercent <= 0) continue; + + const parsed = parseEpisodeId(episodeId); + const entry: LocalProgressEntry = { + episodeId, + season: parsed?.season, + episode: parsed?.episode, + progressPercent, + lastUpdated: progress?.lastUpdated ?? 0, + currentTime: progress?.currentTime ?? 0, + duration: progress?.duration ?? 0, + }; + + for (const idVariant of getIdVariants(id)) { + const idxKey = `${type}:${idVariant}`; + const list = index.get(idxKey); + if (list) { + list.push(entry); + } else { + index.set(idxKey, [entry]); + } + } + } + + return index; + } catch { + return null; + } +} + +export function useContinueWatchingData() { + const [continueWatchingItems, setContinueWatchingItems] = useState([]); + const [loading, setLoading] = useState(true); + const [deletingItemId, setDeletingItemId] = useState(null); + + const appState = useRef(AppState.currentState); + const refreshTimerRef = useRef(null); + const pendingRefreshRef = useRef(false); + const isRefreshingRef = useRef(false); + const recentlyRemovedRef = useRef>(new Set()); + const lastTraktSyncRef = useRef(0); + const lastSimklSyncRef = useRef(0); + const lastTraktReconcileRef = useRef>(new Map()); + const metadataCache = useRef>({}); + + const getCachedMetadata = useMemo( + () => createGetCachedMetadata(metadataCache), + [] + ); + + const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => { + if (isRefreshingRef.current) { + pendingRefreshRef.current = true; + return; + } + + if (!isBackgroundRefresh) { + setLoading(true); + } + isRefreshingRef.current = true; + + try { + const traktService = TraktService.getInstance(); + const isTraktAuthed = await traktService.isAuthenticated(); + + const simklService = SimklService.getInstance(); + const isSimklAuthed = !isTraktAuthed ? await simklService.isAuthenticated() : false; + + const traktMoviesSetPromise = getTraktMoviesSet(isTraktAuthed, traktService); + const traktShowsSetPromise = getTraktShowsSet(isTraktAuthed, traktService); + const localWatchedShowsMapPromise = getLocalWatchedShowsMap(); + const localProgressIndex = await buildLocalProgressIndex( + isTraktAuthed || isSimklAuthed + ); + + if (!isTraktAuthed && !isSimklAuthed) { + const { items, shouldClearItems } = await loadLocalContinueWatching({ + getCachedMetadata, + traktMoviesSetPromise, + traktShowsSetPromise, + localWatchedShowsMapPromise, + }); + + if (shouldClearItems) { + setContinueWatchingItems([]); + return; + } + + const filtered = await filterRemovedItems( + dedupeLocalItems(items), + recentlyRemovedRef.current + ); + setContinueWatchingItems(filtered); + return; + } + + await Promise.allSettled([ + isTraktAuthed + ? mergeTraktContinueWatching({ + traktService, + getCachedMetadata, + localProgressIndex, + localWatchedShowsMapPromise, + recentlyRemoved: recentlyRemovedRef.current, + lastTraktSyncRef, + lastTraktReconcileRef, + setContinueWatchingItems, + }) + : Promise.resolve(), + isSimklAuthed && !isTraktAuthed + ? mergeSimklContinueWatching({ + simklService, + getCachedMetadata, + localProgressIndex, + traktShowsSetPromise, + localWatchedShowsMapPromise, + recentlyRemoved: recentlyRemovedRef.current, + lastSimklSyncRef, + setContinueWatchingItems, + }) + : Promise.resolve(), + ]); + } catch { + // Keep UI usable even if sync fails. + } finally { + setLoading(false); + isRefreshingRef.current = false; + + if (pendingRefreshRef.current) { + pendingRefreshRef.current = false; + setTimeout(() => { + loadContinueWatching(true); + }, 0); + } + } + }, [getCachedMetadata]); + + useEffect(() => { + return () => { + metadataCache.current = {}; + }; + }, []); + + const handleAppStateChange = useCallback((nextAppState: AppStateStatus) => { + if (appState.current.match(/inactive|background/) && nextAppState === 'active') { + lastTraktSyncRef.current = 0; + loadContinueWatching(true); + } + + appState.current = nextAppState; + }, [loadContinueWatching]); + + useEffect(() => { + const subscription = AppState.addEventListener('change', handleAppStateChange); + + const watchProgressUpdateHandler = () => { + if (refreshTimerRef.current) { + clearTimeout(refreshTimerRef.current); + } + + refreshTimerRef.current = setTimeout(() => { + loadContinueWatching(true); + }, 2000); + }; + + if (storageService.subscribeToWatchProgressUpdates) { + const unsubscribe = + storageService.subscribeToWatchProgressUpdates(watchProgressUpdateHandler); + + return () => { + subscription.remove(); + unsubscribe(); + if (refreshTimerRef.current) { + clearTimeout(refreshTimerRef.current); + } + }; + } + + const intervalId = setInterval(() => loadContinueWatching(true), 300000); + return () => { + subscription.remove(); + clearInterval(intervalId); + if (refreshTimerRef.current) { + clearTimeout(refreshTimerRef.current); + } + }; + }, [handleAppStateChange, loadContinueWatching]); + + useEffect(() => { + loadContinueWatching(); + const trailingRefreshId = setTimeout(() => { + loadContinueWatching(true); + }, 4000); + + return () => { + clearTimeout(trailingRefreshId); + }; + }, [loadContinueWatching]); + + useFocusEffect( + useCallback(() => { + loadContinueWatching(true); + return () => {}; + }, [loadContinueWatching]) + ); + + const refresh = useCallback(async () => { + lastTraktSyncRef.current = 0; + await loadContinueWatching(false); + return true; + }, [loadContinueWatching]); + + const removeItem = useCallback(async (item: ContinueWatchingItem) => { + setDeletingItemId(item.id); + + try { + const isEpisode = item.type === 'series' && item.season && item.episode; + if (isEpisode) { + await storageService.removeWatchProgress( + item.id, + item.type, + `${item.id}:${item.season}:${item.episode}` + ); + } else { + await storageService.removeAllWatchProgressForContent(item.id, item.type, { + addBaseTombstone: true, + }); + } + + const traktService = TraktService.getInstance(); + const isAuthed = await traktService.isAuthenticated(); + if (isAuthed && item.traktPlaybackId) { + await traktService.removePlaybackItem(item.traktPlaybackId); + } + + const itemKey = getContinueWatchingItemKey(item); + recentlyRemovedRef.current.add(itemKey); + await storageService.addContinueWatchingRemoved( + getContinueWatchingRemoveId(item), + item.type + ); + + setTimeout(() => { + recentlyRemovedRef.current.delete(itemKey); + }, REMOVAL_IGNORE_DURATION); + + setContinueWatchingItems((prev) => + prev.filter((currentItem) => getContinueWatchingItemKey(currentItem) !== itemKey) + ); + } catch { + // Keep UI state stable even if provider removal fails. + } finally { + setDeletingItemId(null); + } + }, []); + + return { + continueWatchingItems, + loading, + deletingItemId, + refresh, + removeItem, + }; +} diff --git a/src/components/home/continueWatching/useContinueWatchingLayout.ts b/src/components/home/continueWatching/useContinueWatchingLayout.ts new file mode 100644 index 00000000..39668148 --- /dev/null +++ b/src/components/home/continueWatching/useContinueWatchingLayout.ts @@ -0,0 +1,93 @@ +import { useMemo } from 'react'; +import { useWindowDimensions } from 'react-native'; + +import { getDeviceType } from './utils'; + +export function useContinueWatchingLayout() { + const { width } = useWindowDimensions(); + + return useMemo(() => { + const deviceType = getDeviceType(width); + const isTablet = deviceType === 'tablet'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTV = deviceType === 'tv'; + + const computedItemWidth = (() => { + switch (deviceType) { + case 'tv': + return 400; + case 'largeTablet': + return 350; + case 'tablet': + return 320; + default: + return 280; + } + })(); + + const computedItemHeight = (() => { + switch (deviceType) { + case 'tv': + return 160; + case 'largeTablet': + return 140; + case 'tablet': + return 130; + default: + return 120; + } + })(); + + const horizontalPadding = (() => { + switch (deviceType) { + case 'tv': + return 32; + case 'largeTablet': + return 28; + case 'tablet': + return 24; + default: + return 16; + } + })(); + + const itemSpacing = (() => { + switch (deviceType) { + case 'tv': + return 20; + case 'largeTablet': + return 18; + case 'tablet': + return 16; + default: + return 16; + } + })(); + + const computedPosterWidth = (() => { + switch (deviceType) { + case 'tv': + return 180; + case 'largeTablet': + return 160; + case 'tablet': + return 140; + default: + return 120; + } + })(); + + return { + deviceType, + isTablet, + isLargeTablet, + isTV, + horizontalPadding, + itemSpacing, + computedItemWidth, + computedItemHeight, + computedPosterWidth, + computedPosterHeight: computedPosterWidth * 1.5, + }; + }, [width]); +} diff --git a/src/components/home/continueWatching/useContinueWatchingNavigation.ts b/src/components/home/continueWatching/useContinueWatchingNavigation.ts new file mode 100644 index 00000000..f3047748 --- /dev/null +++ b/src/components/home/continueWatching/useContinueWatchingNavigation.ts @@ -0,0 +1,108 @@ +import { useCallback } from 'react'; +import { Platform } from 'react-native'; +import { NavigationProp } from '@react-navigation/native'; + +import { AppSettings } from '../../../hooks/useSettings'; +import { RootStackParamList } from '../../../navigation/AppNavigator'; +import { streamCacheService } from '../../../services/streamCacheService'; +import { logger } from '../../../utils/logger'; + +import { ContinueWatchingItem } from './types'; +import { buildEpisodeId } from './utils'; + +interface ContinueWatchingNavigationParams { + navigation: NavigationProp; + settings: Pick; +} + +export function useContinueWatchingNavigation({ + navigation, + settings, +}: ContinueWatchingNavigationParams) { + const navigateToMetadata = useCallback((item: ContinueWatchingItem) => { + const episodeId = buildEpisodeId(item); + + navigation.navigate('Metadata', { + id: item.id, + type: item.type, + episodeId, + addonId: item.addonId, + }); + }, [navigation]); + + const navigateToStreams = useCallback((item: ContinueWatchingItem) => { + const episodeId = buildEpisodeId(item); + + navigation.navigate('Streams', { + id: item.id, + type: item.type, + episodeId, + addonId: item.addonId, + }); + }, [navigation]); + + const handleContentPress = useCallback(async (item: ContinueWatchingItem) => { + try { + logger.log(`🎬 [ContinueWatching] User clicked on: ${item.name} (${item.type}:${item.id})`); + + if (!settings.useCachedStreams) { + logger.log( + `📺 [ContinueWatching] Cached streams disabled, navigating to ${settings.openMetadataScreenWhenCacheDisabled ? 'MetadataScreen' : 'StreamsScreen'} for ${item.name}` + ); + + if (settings.openMetadataScreenWhenCacheDisabled) { + navigateToMetadata(item); + } else { + navigateToStreams(item); + } + return; + } + + const episodeId = buildEpisodeId(item); + logger.log(`🔍 [ContinueWatching] Looking for cached stream with episodeId: ${episodeId || 'none'}`); + + const cachedStream = await streamCacheService.getCachedStream(item.id, item.type, episodeId); + + if (!cachedStream) { + logger.log(`📺 [ContinueWatching] No cached stream, navigating to StreamsScreen for ${item.name}`); + navigateToStreams(item); + return; + } + + logger.log(`🚀 [ContinueWatching] Using cached stream for ${item.name}`); + const playerRoute = Platform.OS === 'ios' ? 'PlayerIOS' : 'PlayerAndroid'; + + navigation.navigate(playerRoute as any, { + uri: cachedStream.stream.url, + title: cachedStream.metadata?.name || item.name, + episodeTitle: + cachedStream.episodeTitle || + (item.type === 'series' ? `Episode ${item.episode}` : undefined), + season: cachedStream.season || item.season, + episode: cachedStream.episode || item.episode, + quality: (cachedStream.stream.title?.match(/(\d+)p/) || [])[1] || undefined, + year: cachedStream.metadata?.year || item.year, + streamProvider: + cachedStream.stream.addonId || + cachedStream.stream.addonName || + cachedStream.stream.name, + streamName: cachedStream.stream.name || cachedStream.stream.title || 'Unnamed Stream', + headers: cachedStream.stream.headers || undefined, + id: item.id, + type: item.type, + episodeId, + imdbId: cachedStream.imdbId || cachedStream.metadata?.imdbId || item.imdb_id, + backdrop: cachedStream.metadata?.backdrop || item.banner, + videoType: undefined, + } as any); + } catch (error) { + logger.warn('[ContinueWatching] Error handling content press:', error); + navigateToStreams(item); + } + }, [navigateToMetadata, navigateToStreams, navigation, settings.openMetadataScreenWhenCacheDisabled, settings.useCachedStreams]); + + return { + handleContentPress, + navigateToMetadata, + }; +} diff --git a/src/components/home/continueWatching/utils.ts b/src/components/home/continueWatching/utils.ts new file mode 100644 index 00000000..17467f89 --- /dev/null +++ b/src/components/home/continueWatching/utils.ts @@ -0,0 +1,129 @@ +import { BREAKPOINTS } from './constants'; +import { ContinueWatchingDeviceType, ContinueWatchingItem } from './types'; + +export const isSupportedId = (id: string): boolean => typeof id === 'string' && id.length > 0; + +export const isEpisodeReleased = (video: any): boolean => { + if (!video?.released) return false; + + try { + return new Date(video.released) <= new Date(); + } catch { + return false; + } +}; + +export const getDeviceType = (deviceWidth: number): ContinueWatchingDeviceType => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; +}; + +export const buildEpisodeId = ( + item: Pick +): string | undefined => { + if (item.type !== 'series' || !item.season || !item.episode) { + return undefined; + } + + return `${item.id}:${item.season}:${item.episode}`; +}; + +export const getContinueWatchingItemKey = ( + item: Pick +): string => { + const episodeId = buildEpisodeId(item); + return episodeId ? `${item.type}:${episodeId}` : `${item.type}:${item.id}`; +}; + +export const getContinueWatchingRemoveId = ( + item: Pick +): string => buildEpisodeId(item) ?? item.id; + +export const compareContinueWatchingItems = ( + a: ContinueWatchingItem, + b: ContinueWatchingItem +): number => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0); + +export const shouldPreferContinueWatchingCandidate = ( + candidate: ContinueWatchingItem, + existing: ContinueWatchingItem +): boolean => { + const candidateUpdated = candidate.lastUpdated ?? 0; + const existingUpdated = existing.lastUpdated ?? 0; + const candidateProgress = candidate.progress ?? 0; + const existingProgress = existing.progress ?? 0; + + const sameEpisode = + candidate.type === 'movie' || + ( + candidate.type === 'series' && + existing.type === 'series' && + candidate.season !== undefined && + candidate.episode !== undefined && + existing.season !== undefined && + existing.episode !== undefined && + candidate.season === existing.season && + candidate.episode === existing.episode + ); + + if (sameEpisode) { + if (candidateProgress > existingProgress + 0.5) return true; + if (existingProgress > candidateProgress + 0.5) return false; + } + + if (candidateUpdated !== existingUpdated) { + return candidateUpdated > existingUpdated; + } + + return candidateProgress > existingProgress; +}; + +export const getIdVariants = (id: string): string[] => { + const variants = new Set(); + if (typeof id !== 'string' || id.length === 0) return []; + + variants.add(id); + + if (id.startsWith('tt')) { + variants.add(id.replace(/^tt/, '')); + } else if (/^\d+$/.test(id)) { + variants.add(`tt${id}`); + } + + return Array.from(variants); +}; + +export const parseEpisodeId = (episodeId?: string): { season: number; episode: number } | null => { + if (!episodeId) return null; + + const match = episodeId.match(/s(\d+)e(\d+)/i); + if (match) { + const season = parseInt(match[1], 10); + const episode = parseInt(match[2], 10); + if (!isNaN(season) && !isNaN(episode)) { + return { season, episode }; + } + } + + const parts = episodeId.split(':'); + if (parts.length >= 3) { + const season = parseInt(parts[parts.length - 2], 10); + const episode = parseInt(parts[parts.length - 1], 10); + if (!isNaN(season) && !isNaN(episode)) { + return { season, episode }; + } + } + + return null; +}; + +export const toYearNumber = (value: unknown): number | undefined => { + if (typeof value === 'number' && isFinite(value)) return value; + if (typeof value === 'string') { + const parsed = parseInt(value, 10); + if (isFinite(parsed)) return parsed; + } + return undefined; +};