Added granular control for TMDB Enrichment

This commit is contained in:
tapframe 2025-12-29 19:48:26 +05:30
parent 832e5368be
commit fd6e29a8ec
8 changed files with 570 additions and 298 deletions

View file

@ -757,16 +757,26 @@ extension KSPlayerView: KSPlayerLayerDelegate {
print("KSPlayerView: [READY TO PLAY] Found subtitle part: start=\(firstPart.start), end=\(firstPart.end), text='\(firstPart.text?.string ?? "nil")'") print("KSPlayerView: [READY TO PLAY] Found subtitle part: start=\(firstPart.start), end=\(firstPart.end), text='\(firstPart.text?.string ?? "nil")'")
} }
// Auto-select first enabled subtitle if none selected // Only auto-select first enabled subtitle if textTrack prop is NOT set to -1 (disabled)
if self.playerView.srtControl.selectedSubtitleInfo == nil { // If React Native explicitly set textTrack=-1, user wants subtitles off
self.playerView.srtControl.selectedSubtitleInfo = self.playerView.srtControl.subtitleInfos.first { $0.isEnabled } if self.textTrack.intValue != -1 {
if let selected = self.playerView.srtControl.selectedSubtitleInfo { // Auto-select first enabled subtitle if none selected
print("KSPlayerView: [READY TO PLAY] Auto-selected subtitle: \(selected.name)") if self.playerView.srtControl.selectedSubtitleInfo == nil {
self.playerView.srtControl.selectedSubtitleInfo = self.playerView.srtControl.subtitleInfos.first { $0.isEnabled }
if let selected = self.playerView.srtControl.selectedSubtitleInfo {
print("KSPlayerView: [READY TO PLAY] Auto-selected subtitle: \(selected.name)")
} else {
print("KSPlayerView: [READY TO PLAY] No enabled subtitle found for auto-selection")
}
} else { } else {
print("KSPlayerView: [READY TO PLAY] No enabled subtitle found for auto-selection") print("KSPlayerView: [READY TO PLAY] Subtitle already selected: \(self.playerView.srtControl.selectedSubtitleInfo?.name ?? "unknown")")
} }
} else { } else {
print("KSPlayerView: [READY TO PLAY] Subtitle already selected: \(self.playerView.srtControl.selectedSubtitleInfo?.name ?? "unknown")") print("KSPlayerView: [READY TO PLAY] textTrack=-1 (disabled), skipping auto-selection")
// Ensure subtitles are disabled
self.playerView.srtControl.selectedSubtitleInfo = nil
self.playerView.subtitleLabel.isHidden = true
self.playerView.subtitleBackView.isHidden = true
} }
} }
} else { } else {

View file

@ -11,7 +11,7 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
2AA769395C1242F225F875AF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */; }; 2AA769395C1242F225F875AF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
564F8559E25775FFA08707DA /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18260C6F06D8D6DAF4C1D17E /* libPods-Nuvio.a */; }; 72D4090694139E0DAD9B066E /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */; };
9FBA88F42E86ECD700892850 /* KSPlayerViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */; }; 9FBA88F42E86ECD700892850 /* KSPlayerViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */; };
9FBA88F52E86ECD700892850 /* KSPlayerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */; }; 9FBA88F52E86ECD700892850 /* KSPlayerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */; };
9FBA88F62E86ECD700892850 /* KSPlayerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */; }; 9FBA88F62E86ECD700892850 /* KSPlayerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */; };
@ -24,12 +24,12 @@
13B07F961A680F5B00A75B9A /* Nuvio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nuvio.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07F961A680F5B00A75B9A /* Nuvio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nuvio.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nuvio/Images.xcassets; sourceTree = "<group>"; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nuvio/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nuvio/Info.plist; sourceTree = "<group>"; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nuvio/Info.plist; sourceTree = "<group>"; };
18260C6F06D8D6DAF4C1D17E /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = "<group>"; };
2118C3C63E4B7D66EAC534DE /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = "<group>"; }; 406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; };
49055D6E250FAFA21141FE49 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = "<group>"; }; 49055D6E250FAFA21141FE49 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift"; sourceTree = "<group>"; }; 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
73BB213C2E9EEAC700EC03F8 /* NuvioRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NuvioRelease.entitlements; path = Nuvio/NuvioRelease.entitlements; sourceTree = "<group>"; }; 73BB213C2E9EEAC700EC03F8 /* NuvioRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NuvioRelease.entitlements; path = Nuvio/NuvioRelease.entitlements; sourceTree = "<group>"; };
7F2FA62198C389C99926AA47 /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = "<group>"; }; 819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = "<group>"; };
9FBA88F02E86ECD700892850 /* KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSPlayerManager.m; sourceTree = "<group>"; }; 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSPlayerManager.m; sourceTree = "<group>"; };
9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerModule.swift; sourceTree = "<group>"; }; 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerModule.swift; sourceTree = "<group>"; };
9FBA88F22E86ECD700892850 /* KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerView.swift; sourceTree = "<group>"; }; 9FBA88F22E86ECD700892850 /* KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerView.swift; sourceTree = "<group>"; };
@ -46,7 +46,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
564F8559E25775FFA08707DA /* libPods-Nuvio.a in Frameworks */, 72D4090694139E0DAD9B066E /* libPods-Nuvio.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -76,7 +76,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
18260C6F06D8D6DAF4C1D17E /* libPods-Nuvio.a */, 406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -131,8 +131,8 @@
D90A3959C97EE9926C513293 /* Pods */ = { D90A3959C97EE9926C513293 /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2118C3C63E4B7D66EAC534DE /* Pods-Nuvio.debug.xcconfig */, 819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */,
7F2FA62198C389C99926AA47 /* Pods-Nuvio.release.xcconfig */, 15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@ -152,15 +152,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nuvio" */; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nuvio" */;
buildPhases = ( buildPhases = (
4A10611824FCBAA4C1793637 /* [CP] Check Pods Manifest.lock */, E060D0359630A7C0F4793812 /* [CP] Check Pods Manifest.lock */,
99A79B70155E84EE1FB7F466 /* [Expo] Configure project */, 99A79B70155E84EE1FB7F466 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */, 13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */, 13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */, 9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */,
EE80421364369BBCA82253B9 /* [CP] Embed Pods Frameworks */, B84E40F81DF927F34032D68B /* [CP] Embed Pods Frameworks */,
778B42B39FEE5454E4D24252 /* [CP] Copy Pods Resources */, AA47AE6072D35F0490B53926 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -234,29 +234,45 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n/bin/sh `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"` `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n/bin/sh `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"` `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
}; };
4A10611824FCBAA4C1793637 /* [CP] Check Pods Manifest.lock */ = { 99A79B70155E84EE1FB7F466 /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
); );
inputPaths = ( inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock", "$(SRCROOT)/.xcode.env",
"${PODS_ROOT}/Manifest.lock", "$(SRCROOT)/.xcode.env.local",
"$(SRCROOT)/Nuvio/Nuvio.entitlements",
"$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/expo-configure-project.sh",
); );
name = "[CP] Check Pods Manifest.lock"; name = "[Expo] Configure project";
outputFileListPaths = ( outputFileListPaths = (
); );
outputPaths = ( outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Nuvio-checkManifestLockResult.txt", "$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Nuvio/expo-configure-project.sh\"\n";
showEnvVarsInLog = 0;
}; };
778B42B39FEE5454E4D24252 /* [CP] Copy Pods Resources */ = { 9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Upload Debug Symbols to Sentry";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";
};
AA47AE6072D35F0490B53926 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -368,45 +384,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-resources.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
99A79B70155E84EE1FB7F466 /* [Expo] Configure project */ = { B84E40F81DF927F34032D68B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)/.xcode.env",
"$(SRCROOT)/.xcode.env.local",
"$(SRCROOT)/Nuvio/Nuvio.entitlements",
"$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/expo-configure-project.sh",
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
"$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Nuvio/expo-configure-project.sh\"\n";
};
9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Upload Debug Symbols to Sentry";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";
};
EE80421364369BBCA82253B9 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -428,6 +406,28 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
E060D0359630A7C0F4793812 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Nuvio-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@ -449,7 +449,7 @@
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = { 13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 2118C3C63E4B7D66EAC534DE /* Pods-Nuvio.debug.xcconfig */; baseConfigurationReference = 819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
@ -487,7 +487,7 @@
}; };
13B07F951A680F5B00A75B9A /* Release */ = { 13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7F2FA62198C389C99926AA47 /* Pods-Nuvio.release.xcconfig */; baseConfigurationReference = 15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;

View file

@ -33,6 +33,7 @@ interface SubtitleModalsProps {
loadWyzieSubtitle: (subtitle: WyzieSubtitle) => void; loadWyzieSubtitle: (subtitle: WyzieSubtitle) => void;
selectTextTrack: (trackId: number) => void; selectTextTrack: (trackId: number) => void;
disableCustomSubtitles: () => void; disableCustomSubtitles: () => void;
setSubtitlesAutoSelect?: (autoSelect: boolean) => void;
increaseSubtitleSize: () => void; increaseSubtitleSize: () => void;
decreaseSubtitleSize: () => void; decreaseSubtitleSize: () => void;
toggleSubtitleBackground: () => void; toggleSubtitleBackground: () => void;
@ -89,6 +90,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
subtitleOutlineWidth, setSubtitleOutlineWidth, subtitleAlign, setSubtitleAlign, subtitleOutlineWidth, setSubtitleOutlineWidth, subtitleAlign, setSubtitleAlign,
subtitleBottomOffset, setSubtitleBottomOffset, subtitleLetterSpacing, setSubtitleLetterSpacing, subtitleBottomOffset, setSubtitleBottomOffset, subtitleLetterSpacing, setSubtitleLetterSpacing,
subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier, subtitleOffsetSec, setSubtitleOffsetSec, subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier, subtitleOffsetSec, setSubtitleOffsetSec,
setSubtitlesAutoSelect,
}) => { }) => {
const { width, height } = useWindowDimensions(); const { width, height } = useWindowDimensions();
const isIos = Platform.OS === 'ios'; const isIos = Platform.OS === 'ios';
@ -157,7 +159,12 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
{activeTab === 'built-in' && ( {activeTab === 'built-in' && (
<View style={{ gap: 8 }}> <View style={{ gap: 8 }}>
<TouchableOpacity <TouchableOpacity
onPress={() => { selectTextTrack(-1); setSelectedOnlineSubtitleId(null); }} onPress={() => {
selectTextTrack(-1);
setSelectedOnlineSubtitleId(null);
// Disable auto-select for future playback sessions
setSubtitlesAutoSelect?.(false);
}}
style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === -1 ? 'white' : 'rgba(242, 184, 181)' }} style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === -1 ? 'white' : 'rgba(242, 184, 181)' }}
> >
<Text style={{ color: selectedTextTrack === -1 ? 'black' : 'rgba(96, 20, 16)', fontWeight: '600' }}>None</Text> <Text style={{ color: selectedTextTrack === -1 ? 'black' : 'rgba(96, 20, 16)', fontWeight: '600' }}>None</Text>
@ -165,7 +172,12 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
{ksTextTracks.map((track) => ( {ksTextTracks.map((track) => (
<TouchableOpacity <TouchableOpacity
key={track.id} key={track.id}
onPress={() => { selectTextTrack(track.id); setSelectedOnlineSubtitleId(null); }} onPress={() => {
selectTextTrack(track.id);
setSelectedOnlineSubtitleId(null);
// Enable auto-select for future playback sessions when user selects a subtitle
setSubtitlesAutoSelect?.(true);
}}
style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === track.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }} style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === track.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}
> >
<Text style={{ color: selectedTextTrack === track.id ? 'black' : 'white' }}>{getTrackDisplayName(track)}</Text> <Text style={{ color: selectedTextTrack === track.id ? 'black' : 'white' }}>{getTrackDisplayName(track)}</Text>
@ -186,7 +198,12 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
availableSubtitles.map((sub) => ( availableSubtitles.map((sub) => (
<TouchableOpacity <TouchableOpacity
key={sub.id} key={sub.id}
onPress={() => { setSelectedOnlineSubtitleId(sub.id); loadWyzieSubtitle(sub); }} onPress={() => {
setSelectedOnlineSubtitleId(sub.id);
loadWyzieSubtitle(sub);
// Enable auto-select for future playback sessions when user selects a subtitle
setSubtitlesAutoSelect?.(true);
}}
style={{ padding: 5, paddingLeft: 8, paddingRight: 10, borderRadius: 12, backgroundColor: selectedOnlineSubtitleId === sub.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }} style={{ padding: 5, paddingLeft: 8, paddingRight: 10, borderRadius: 12, backgroundColor: selectedOnlineSubtitleId === sub.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}
> >
<View> <View>
@ -235,49 +252,49 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
{/* Quick Presets - Hidden for ExoPlayer internal subtitles */} {/* Quick Presets - Hidden for ExoPlayer internal subtitles */}
{!isExoPlayerInternal && ( {!isExoPlayerInternal && (
<View style={{ backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 16, padding: sectionPad }}> <View style={{ backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 16, padding: sectionPad }}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 10 }}> <View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 10 }}>
<MaterialIcons name="star" size={16} color="rgba(255,255,255,0.7)" /> <MaterialIcons name="star" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginLeft: 6, fontWeight: '600' }}>Quick Presets</Text> <Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginLeft: 6, fontWeight: '600' }}>Quick Presets</Text>
</View>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
<TouchableOpacity
onPress={() => {
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
setSubtitleAlign('center'); setSubtitleBottomOffset(10); setSubtitleLetterSpacing(0);
setSubtitleLineHeightMultiplier(1.2);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
>
<Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 11 : 12 }}>Default</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setSubtitleTextColor('#FFD700'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleBgOpacity(0.3); setSubtitleTextShadow(false);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,215,0,0.12)', borderWidth: 1, borderColor: 'rgba(255,215,0,0.35)' }}
>
<Text style={{ color: '#FFD700', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Yellow</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setSubtitleTextColor('#FFFFFF'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(3); setSubtitleBgOpacity(0.0); setSubtitleTextShadow(false); setSubtitleLetterSpacing(0.5);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(34,197,94,0.12)', borderWidth: 1, borderColor: 'rgba(34,197,94,0.35)' }}
>
<Text style={{ color: '#22C55E', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>High Contrast</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.6); setSubtitleTextShadow(true); setSubtitleOutline(true); setSubtitleAlign('center'); setSubtitleLineHeightMultiplier(1.3);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(59,130,246,0.12)', borderWidth: 1, borderColor: 'rgba(59,130,246,0.35)' }}
>
<Text style={{ color: '#3B82F6', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Large</Text>
</TouchableOpacity>
</View>
</View> </View>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
<TouchableOpacity
onPress={() => {
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
setSubtitleAlign('center'); setSubtitleBottomOffset(10); setSubtitleLetterSpacing(0);
setSubtitleLineHeightMultiplier(1.2);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
>
<Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 11 : 12 }}>Default</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setSubtitleTextColor('#FFD700'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleBgOpacity(0.3); setSubtitleTextShadow(false);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,215,0,0.12)', borderWidth: 1, borderColor: 'rgba(255,215,0,0.35)' }}
>
<Text style={{ color: '#FFD700', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Yellow</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setSubtitleTextColor('#FFFFFF'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(3); setSubtitleBgOpacity(0.0); setSubtitleTextShadow(false); setSubtitleLetterSpacing(0.5);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(34,197,94,0.12)', borderWidth: 1, borderColor: 'rgba(34,197,94,0.35)' }}
>
<Text style={{ color: '#22C55E', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>High Contrast</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.6); setSubtitleTextShadow(true); setSubtitleOutline(true); setSubtitleAlign('center'); setSubtitleLineHeightMultiplier(1.3);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(59,130,246,0.12)', borderWidth: 1, borderColor: 'rgba(59,130,246,0.35)' }}
>
<Text style={{ color: '#3B82F6', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Large</Text>
</TouchableOpacity>
</View>
</View>
)} )}
{/* Core controls */} {/* Core controls */}
@ -305,18 +322,18 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</View> </View>
{/* Show Background - Not supported on ExoPlayer internal subtitles */} {/* Show Background - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && ( {!isExoPlayerInternal && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}> <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="layers" size={16} color="rgba(255,255,255,0.7)" /> <MaterialIcons name="layers" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Show Background</Text> <Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Show Background</Text>
</View>
<TouchableOpacity
style={{ width: isCompact ? 48 : 54, height: isCompact ? 28 : 30, backgroundColor: subtitleBackground ? 'white' : 'rgba(255,255,255,0.25)', borderRadius: 15, justifyContent: 'center', alignItems: subtitleBackground ? 'flex-end' : 'flex-start', paddingHorizontal: 3 }}
onPress={toggleSubtitleBackground}
>
<View style={{ width: 24, height: 24, backgroundColor: subtitleBackground ? 'black' : 'white', borderRadius: 12 }} />
</TouchableOpacity>
</View> </View>
<TouchableOpacity
style={{ width: isCompact ? 48 : 54, height: isCompact ? 28 : 30, backgroundColor: subtitleBackground ? 'white' : 'rgba(255,255,255,0.25)', borderRadius: 15, justifyContent: 'center', alignItems: subtitleBackground ? 'flex-end' : 'flex-start', paddingHorizontal: 3 }}
onPress={toggleSubtitleBackground}
>
<View style={{ width: 24, height: 24, backgroundColor: subtitleBackground ? 'black' : 'white', borderRadius: 12 }} />
</TouchableOpacity>
</View>
)} )}
</View> </View>
@ -328,30 +345,30 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</View> </View>
{/* Text Color - Not supported on ExoPlayer internal subtitles */} {/* Text Color - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && ( {!isExoPlayerInternal && (
<View style={{ marginTop: 8, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ marginTop: 8, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}> <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="palette" size={16} color="rgba(255,255,255,0.7)" /> <MaterialIcons name="palette" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: 'white', marginLeft: 8, fontWeight: '600' }}>Text Color</Text> <Text style={{ color: 'white', marginLeft: 8, fontWeight: '600' }}>Text Color</Text>
</View>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' }}>
{['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => (
<TouchableOpacity key={c} onPress={() => setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} />
))}
</View>
</View> </View>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' }}>
{['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => (
<TouchableOpacity key={c} onPress={() => setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} />
))}
</View>
</View>
)} )}
{/* Align - Not supported on ExoPlayer internal subtitles */} {/* Align - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && ( {!isExoPlayerInternal && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Align</Text> <Text style={{ color: 'white', fontWeight: '600' }}>Align</Text>
<View style={{ flexDirection: 'row', gap: 8 }}> <View style={{ flexDirection: 'row', gap: 8 }}>
{([{ key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' }] as const).map(a => ( {([{ key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' }] as const).map(a => (
<TouchableOpacity key={a.key} onPress={() => setSubtitleAlign(a.key)} style={{ paddingHorizontal: isCompact ? 8 : 10, paddingVertical: isCompact ? 4 : 6, borderRadius: 8, backgroundColor: subtitleAlign === a.key ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}> <TouchableOpacity key={a.key} onPress={() => setSubtitleAlign(a.key)} style={{ paddingHorizontal: isCompact ? 8 : 10, paddingVertical: isCompact ? 4 : 6, borderRadius: 8, backgroundColor: subtitleAlign === a.key ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}>
<MaterialIcons name={a.icon as any} size={18} color="#FFFFFF" /> <MaterialIcons name={a.icon as any} size={18} color="#FFFFFF" />
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View>
</View> </View>
</View>
)} )}
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Bottom Offset</Text> <Text style={{ color: 'white', fontWeight: '600' }}>Bottom Offset</Text>
@ -369,20 +386,20 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</View> </View>
{/* Background Opacity - Not supported on ExoPlayer internal subtitles */} {/* Background Opacity - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && ( {!isExoPlayerInternal && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Background Opacity</Text> <Text style={{ color: 'white', fontWeight: '600' }}>Background Opacity</Text>
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}> <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
<TouchableOpacity onPress={() => setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> <TouchableOpacity onPress={() => setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
<MaterialIcons name="remove" color="#fff" size={18} /> <MaterialIcons name="remove" color="#fff" size={18} />
</TouchableOpacity> </TouchableOpacity>
<View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}> <View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleBgOpacity.toFixed(1)}</Text> <Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleBgOpacity.toFixed(1)}</Text>
</View>
<TouchableOpacity onPress={() => setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
<MaterialIcons name="add" color="#fff" size={18} />
</TouchableOpacity>
</View> </View>
<TouchableOpacity onPress={() => setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
<MaterialIcons name="add" color="#fff" size={18} />
</TouchableOpacity>
</View> </View>
</View>
)} )}
{!isUsingInternalSubtitle && ( {!isUsingInternalSubtitle && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
@ -452,23 +469,23 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
)} )}
{/* Timing Offset - Not supported on ExoPlayer internal subtitles */} {/* Timing Offset - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && ( {!isExoPlayerInternal && (
<View style={{ marginTop: 4 }}> <View style={{ marginTop: 4 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Timing Offset (s)</Text> <Text style={{ color: 'white', fontWeight: '600' }}>Timing Offset (s)</Text>
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}> <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
<TouchableOpacity onPress={() => setSubtitleOffsetSec(+(subtitleOffsetSec - 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> <TouchableOpacity onPress={() => setSubtitleOffsetSec(+(subtitleOffsetSec - 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
<MaterialIcons name="remove" color="#fff" size={18} /> <MaterialIcons name="remove" color="#fff" size={18} />
</TouchableOpacity> </TouchableOpacity>
<View style={{ minWidth: 60, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}> <View style={{ minWidth: 60, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleOffsetSec.toFixed(1)}</Text> <Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleOffsetSec.toFixed(1)}</Text>
</View>
<TouchableOpacity onPress={() => setSubtitleOffsetSec(+(subtitleOffsetSec + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
<MaterialIcons name="add" color="#fff" size={18} />
</TouchableOpacity>
</View> </View>
<TouchableOpacity onPress={() => setSubtitleOffsetSec(+(subtitleOffsetSec + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
<MaterialIcons name="add" color="#fff" size={18} />
</TouchableOpacity>
</View> </View>
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 11, marginTop: 6 }}>Nudge subtitles earlier (-) or later (+) to sync if needed.</Text>
</View> </View>
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 11, marginTop: 6 }}>Nudge subtitles earlier (-) or later (+) to sync if needed.</Text>
</View>
)} )}
<View style={{ alignItems: 'flex-end', marginTop: 8 }}> <View style={{ alignItems: 'flex-end', marginTop: 8 }}>
<TouchableOpacity <TouchableOpacity

View file

@ -402,8 +402,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (__DEV__) logger.log('[loadCast] Starting cast fetch for:', id); if (__DEV__) logger.log('[loadCast] Starting cast fetch for:', id);
setLoadingCast(true); setLoadingCast(true);
try { try {
if (!settings.enrichMetadataWithTMDB) { // Check both master switch AND granular cast setting
if (__DEV__) logger.log('[loadCast] TMDB enrichment disabled by settings'); if (!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichCast) {
if (__DEV__) logger.log('[loadCast] TMDB cast enrichment disabled by settings');
// Check if we have addon cast data available // Check if we have addon cast data available
if (metadata?.addonCast && metadata.addonCast.length > 0) { if (metadata?.addonCast && metadata.addonCast.length > 0) {
@ -908,8 +909,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// Centralized logo fetching logic // Centralized logo fetching logic
try { try {
if (settings.enrichMetadataWithTMDB) { // Check both master switch AND granular logos setting
// Only use TMDB logos when enrichment is ON if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichLogos) {
// Only use TMDB logos when both enrichment AND logos option are ON
const tmdbService = TMDBService.getInstance(); const tmdbService = TMDBService.getInstance();
const preferredLanguage = settings.tmdbLanguagePreference || 'en'; const preferredLanguage = settings.tmdbLanguagePreference || 'en';
const contentType = type === 'series' ? 'tv' : 'movie'; const contentType = type === 'series' ? 'tv' : 'movie';
@ -940,12 +942,13 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (__DEV__) console.log('[useMetadata] No TMDB ID found for logo, will show text title'); if (__DEV__) console.log('[useMetadata] No TMDB ID found for logo, will show text title');
} }
} else { } else {
// When enrichment is OFF, keep addon logo or undefined // When enrichment or logos is OFF, keep addon logo or undefined
finalMetadata.logo = finalMetadata.logo || undefined; finalMetadata.logo = finalMetadata.logo || undefined;
if (__DEV__) { if (__DEV__) {
console.log('[useMetadata] TMDB enrichment disabled, using addon logo:', { console.log('[useMetadata] TMDB logo enrichment disabled, using addon logo:', {
hasAddonLogo: !!finalMetadata.logo, hasAddonLogo: !!finalMetadata.logo,
enrichmentEnabled: false enrichmentEnabled: settings.enrichMetadataWithTMDB,
logosEnabled: settings.tmdbEnrichLogos
}); });
} }
} }
@ -961,8 +964,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
(finalMetadata as any).addonLogo = addonLogo; (finalMetadata as any).addonLogo = addonLogo;
} }
// Clear banner field if TMDB enrichment is enabled to prevent flash // Clear banner field if TMDB banner enrichment is enabled to prevent flash
if (settings.enrichMetadataWithTMDB) { if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichBanners) {
finalMetadata = { finalMetadata = {
...finalMetadata, ...finalMetadata,
banner: undefined, // Let useMetadataAssets handle banner via TMDB banner: undefined, // Let useMetadataAssets handle banner via TMDB
@ -1114,8 +1117,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (__DEV__) logger.log(`📺 Processed addon episodes into ${Object.keys(groupedAddonEpisodes).length} seasons`); if (__DEV__) logger.log(`📺 Processed addon episodes into ${Object.keys(groupedAddonEpisodes).length} seasons`);
// Fetch season posters from TMDB only if enrichment is enabled; otherwise skip quietly // Fetch season posters from TMDB only if enrichment AND season posters are enabled
if (settings.enrichMetadataWithTMDB) { if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichSeasonPosters) {
try { try {
const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null); const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null);
if (tmdbIdToUse) { if (tmdbIdToUse) {
@ -1140,11 +1143,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
logger.error('Failed to fetch TMDB season posters for addon episodes:', error); logger.error('Failed to fetch TMDB season posters for addon episodes:', error);
} }
} else { } else {
if (__DEV__) logger.log('[loadSeriesData] TMDB enrichment disabled; skipping season poster fetch'); if (__DEV__) logger.log('[loadSeriesData] TMDB season poster enrichment disabled; skipping season poster fetch');
} }
// If localized TMDB text is enabled, merge episode names/overviews per language // If localized TMDB text is enabled AND episode enrichment is enabled, merge episode names/overviews per language
if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) { if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichEpisodes && settings.useTmdbLocalizedMetadata) {
try { try {
const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null); const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null);
if (tmdbIdToUse) { if (tmdbIdToUse) {
@ -1241,8 +1244,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
} }
// Try to get TMDB ID for additional metadata (cast, etc.) but don't override episodes // Try to get TMDB ID for additional metadata (cast, etc.) but don't override episodes
if (!settings.enrichMetadataWithTMDB) { // Skip TMDB episode fallback if enrichment or episode enrichment is disabled
if (__DEV__) logger.log('[loadSeriesData] TMDB enrichment disabled; skipping TMDB episode fallback (preserving current episodes)'); if (!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichEpisodes) {
if (__DEV__) logger.log('[loadSeriesData] TMDB episode enrichment disabled; skipping TMDB episode fallback (preserving current episodes)');
return; return;
} }
const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id); const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id);
@ -2053,7 +2057,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
useEffect(() => { useEffect(() => {
const fetchTmdbIdAndRecommendations = async () => { const fetchTmdbIdAndRecommendations = async () => {
if (!settings.enrichMetadataWithTMDB) { if (!settings.enrichMetadataWithTMDB) {
if (__DEV__) console.log('[useMetadata] enrichment disabled; skip TMDB id extraction and certification (extract path)'); if (__DEV__) console.log('[useMetadata] enrichment disabled; skip TMDB id extraction (extract path)');
return; return;
} }
if (metadata && !tmdbId) { if (metadata && !tmdbId) {
@ -2063,17 +2067,22 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (fetchedTmdbId) { if (fetchedTmdbId) {
if (__DEV__) console.log('[useMetadata] extracted TMDB id from content id', { id, fetchedTmdbId }); if (__DEV__) console.log('[useMetadata] extracted TMDB id from content id', { id, fetchedTmdbId });
setTmdbId(fetchedTmdbId); setTmdbId(fetchedTmdbId);
// Fetch certification // Fetch certification only if granular setting is enabled
const certification = await tmdbService.getCertification(type, fetchedTmdbId); if (settings.tmdbEnrichCertification) {
if (certification) { const certification = await tmdbService.getCertification(type, fetchedTmdbId);
if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification }); if (certification) {
setMetadata(prev => prev ? { if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification });
...prev, setMetadata(prev => prev ? {
tmdbId: fetchedTmdbId, ...prev,
certification tmdbId: fetchedTmdbId,
} : null); certification
} : null);
} else {
if (__DEV__) console.warn('[useMetadata] certification not returned from TMDB (extract path)', { type, fetchedTmdbId });
}
} else { } else {
if (__DEV__) console.warn('[useMetadata] certification not returned from TMDB (extract path)', { type, fetchedTmdbId }); // Just set the TMDB ID without certification
setMetadata(prev => prev ? { ...prev, tmdbId: fetchedTmdbId } : null);
} }
} else { } else {
if (__DEV__) console.warn('[useMetadata] Could not determine TMDB ID for recommendations / certification', { id }); if (__DEV__) console.warn('[useMetadata] Could not determine TMDB ID for recommendations / certification', { id });
@ -2089,8 +2098,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
useEffect(() => { useEffect(() => {
if (tmdbId) { if (tmdbId) {
if (settings.enrichMetadataWithTMDB) { // Check both master switch AND granular recommendations setting
if (__DEV__) console.log('[useMetadata] tmdbId available; loading recommendations and enabling certification checks', { tmdbId }); if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichRecommendations) {
if (__DEV__) console.log('[useMetadata] tmdbId available; loading recommendations', { tmdbId });
loadRecommendations(); loadRecommendations();
} }
// Reset recommendations when tmdbId changes // Reset recommendations when tmdbId changes
@ -2099,21 +2109,23 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
setLoadingRecommendations(true); setLoadingRecommendations(true);
}; };
} }
}, [tmdbId, loadRecommendations, settings.enrichMetadataWithTMDB]); }, [tmdbId, loadRecommendations, settings.enrichMetadataWithTMDB, settings.tmdbEnrichRecommendations]);
// Load addon cast data when metadata is available and TMDB enrichment is disabled // Load addon cast data when metadata is available and TMDB cast enrichment is disabled
useEffect(() => { useEffect(() => {
if (!settings.enrichMetadataWithTMDB && metadata?.addonCast && metadata.addonCast.length > 0) { // Load addon cast if master switch is off OR if cast enrichment specifically is off
if ((!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichCast) && metadata?.addonCast && metadata.addonCast.length > 0) {
if (__DEV__) logger.log('[useMetadata] Loading addon cast data after metadata loaded'); if (__DEV__) logger.log('[useMetadata] Loading addon cast data after metadata loaded');
loadCast(); loadCast();
} }
}, [metadata, settings.enrichMetadataWithTMDB]); }, [metadata, settings.enrichMetadataWithTMDB, settings.tmdbEnrichCast]);
// Ensure certification is attached whenever a TMDB id is known and metadata lacks it // Ensure certification is attached whenever a TMDB id is known and metadata lacks it
useEffect(() => { useEffect(() => {
const maybeAttachCertification = async () => { const maybeAttachCertification = async () => {
if (!settings.enrichMetadataWithTMDB) { // Check both master switch AND granular certification setting
if (__DEV__) console.log('[useMetadata] enrichment disabled; skip certification (attach path)'); if (!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichCertification) {
if (__DEV__) console.log('[useMetadata] certification enrichment disabled; skip (attach path)');
return; return;
} }
try { try {
@ -2142,12 +2154,18 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
} }
}; };
maybeAttachCertification(); maybeAttachCertification();
}, [tmdbId, metadata, type, settings.enrichMetadataWithTMDB]); }, [tmdbId, metadata, type, settings.enrichMetadataWithTMDB, settings.tmdbEnrichCertification]);
// Fetch TMDB networks/production companies when TMDB ID is available and enrichment is enabled // Fetch TMDB networks/production companies when TMDB ID is available and enrichment is enabled
const productionInfoFetchedRef = useRef<string | null>(null); const productionInfoFetchedRef = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
if (!tmdbId || !settings.enrichMetadataWithTMDB || !metadata) { // Check if any of the relevant granular settings are enabled
const anyProductionEnrichmentEnabled = settings.tmdbEnrichProductionInfo ||
settings.tmdbEnrichTvDetails ||
settings.tmdbEnrichMovieDetails ||
settings.tmdbEnrichCollections;
if (!tmdbId || !settings.enrichMetadataWithTMDB || !metadata || !anyProductionEnrichmentEnabled) {
return; return;
} }
@ -2175,7 +2193,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
tmdbId, tmdbId,
useLocalized: settings.useTmdbLocalizedMetadata, useLocalized: settings.useTmdbLocalizedMetadata,
lang: settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en', lang: settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en',
hasExistingNetworks: !!(metadata as any).networks hasExistingNetworks: !!(metadata as any).networks,
productionInfoEnabled: settings.tmdbEnrichProductionInfo,
tvDetailsEnabled: settings.tmdbEnrichTvDetails,
movieDetailsEnabled: settings.tmdbEnrichMovieDetails,
collectionsEnabled: settings.tmdbEnrichCollections
}); });
if (type === 'series') { if (type === 'series') {
@ -2187,8 +2209,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
hasNetworks: !!showDetails.networks, hasNetworks: !!showDetails.networks,
networksCount: showDetails.networks?.length || 0 networksCount: showDetails.networks?.length || 0
}); });
// Fetch networks // Fetch networks only if production info is enabled
if (showDetails.networks) { if (settings.tmdbEnrichProductionInfo && showDetails.networks) {
productionInfo = Array.isArray(showDetails.networks) productionInfo = Array.isArray(showDetails.networks)
? showDetails.networks ? showDetails.networks
.map((n: any) => ({ .map((n: any) => ({
@ -2200,30 +2222,32 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
: []; : [];
} }
// Fetch additional TV details // Fetch additional TV details only if TV details is enabled
const tvDetails = { if (settings.tmdbEnrichTvDetails) {
status: showDetails.status, const tvDetails = {
firstAirDate: showDetails.first_air_date, status: showDetails.status,
lastAirDate: showDetails.last_air_date, firstAirDate: showDetails.first_air_date,
numberOfSeasons: showDetails.number_of_seasons, lastAirDate: showDetails.last_air_date,
numberOfEpisodes: showDetails.number_of_episodes, numberOfSeasons: showDetails.number_of_seasons,
episodeRunTime: showDetails.episode_run_time, numberOfEpisodes: showDetails.number_of_episodes,
type: showDetails.type, episodeRunTime: showDetails.episode_run_time,
originCountry: showDetails.origin_country, type: showDetails.type,
originalLanguage: showDetails.original_language, originCountry: showDetails.origin_country,
createdBy: showDetails.created_by?.map(creator => ({ originalLanguage: showDetails.original_language,
id: creator.id, createdBy: showDetails.created_by?.map(creator => ({
name: creator.name, id: creator.id,
profile_path: creator.profile_path || undefined name: creator.name,
})), profile_path: creator.profile_path || undefined
}; })),
};
// Update metadata with TV details // Update metadata with TV details
setMetadata((prev: any) => ({ setMetadata((prev: any) => ({
...prev, ...prev,
tmdbId, tmdbId,
tvDetails tvDetails
})); }));
}
} }
} else if (type === 'movie') { } else if (type === 'movie') {
// Fetch production companies and additional details for movies // Fetch production companies and additional details for movies
@ -2234,8 +2258,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
hasProductionCompanies: !!movieDetails.production_companies, hasProductionCompanies: !!movieDetails.production_companies,
productionCompaniesCount: movieDetails.production_companies?.length || 0 productionCompaniesCount: movieDetails.production_companies?.length || 0
}); });
// Fetch production companies // Fetch production companies only if production info is enabled
if (movieDetails.production_companies) { if (settings.tmdbEnrichProductionInfo && movieDetails.production_companies) {
productionInfo = Array.isArray(movieDetails.production_companies) productionInfo = Array.isArray(movieDetails.production_companies)
? movieDetails.production_companies ? movieDetails.production_companies
.map((c: any) => ({ .map((c: any) => ({
@ -2247,27 +2271,29 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
: []; : [];
} }
// Fetch additional movie details // Fetch additional movie details only if movie details is enabled
const movieDetailsObj = { if (settings.tmdbEnrichMovieDetails) {
status: movieDetails.status, const movieDetailsObj = {
releaseDate: movieDetails.release_date, status: movieDetails.status,
runtime: movieDetails.runtime, releaseDate: movieDetails.release_date,
budget: movieDetails.budget, runtime: movieDetails.runtime,
revenue: movieDetails.revenue, budget: movieDetails.budget,
originalLanguage: movieDetails.original_language, revenue: movieDetails.revenue,
originCountry: movieDetails.production_countries?.map((c: any) => c.iso_3166_1), originalLanguage: movieDetails.original_language,
tagline: movieDetails.tagline, originCountry: movieDetails.production_countries?.map((c: any) => c.iso_3166_1),
}; tagline: movieDetails.tagline,
};
// Update metadata with movie details // Update metadata with movie details
setMetadata((prev: any) => ({ setMetadata((prev: any) => ({
...prev, ...prev,
tmdbId, tmdbId,
movieDetails: movieDetailsObj movieDetails: movieDetailsObj
})); }));
}
// Fetch collection data if movie belongs to a collection // Fetch collection data if movie belongs to a collection AND collections is enabled
if (movieDetails.belongs_to_collection) { if (settings.tmdbEnrichCollections && movieDetails.belongs_to_collection) {
setLoadingCollection(true); setLoadingCollection(true);
try { try {
const collectionDetails = await tmdbService.getCollectionDetails( const collectionDetails = await tmdbService.getCollectionDetails(
@ -2365,7 +2391,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
}; };
fetchProductionInfo(); fetchProductionInfo();
}, [tmdbId, settings.enrichMetadataWithTMDB, metadata, type]); }, [tmdbId, settings.enrichMetadataWithTMDB, metadata, type, settings.tmdbEnrichProductionInfo, settings.tmdbEnrichTvDetails, settings.tmdbEnrichMovieDetails, settings.tmdbEnrichCollections]);
// Reset tmdbId when id changes // Reset tmdbId when id changes
useEffect(() => { useEffect(() => {

View file

@ -120,8 +120,8 @@ export const useMetadataAssets = (
setLoadingBanner(true); setLoadingBanner(true);
} }
// If enrichment is disabled, use addon banner and don't fetch from external sources // If enrichment or banner enrichment is disabled, use addon banner and don't fetch from external sources
if (!settings.enrichMetadataWithTMDB) { if (!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichBanners) {
const addonBanner = metadata?.banner || null; const addonBanner = metadata?.banner || null;
if (isMountedRef.current && addonBanner && addonBanner !== bannerImage) { if (isMountedRef.current && addonBanner && addonBanner !== bannerImage) {
setBannerImage(addonBanner); setBannerImage(addonBanner);
@ -247,7 +247,7 @@ export const useMetadataAssets = (
pendingFetchRef.current = fetchPromise; pendingFetchRef.current = fetchPromise;
return fetchPromise; return fetchPromise;
}, [metadata, id, type, imdbId, settings.logoSourcePreference, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB, foundTmdbId, bannerImage, bannerSource]); }, [metadata, id, type, imdbId, settings.logoSourcePreference, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB, settings.tmdbEnrichBanners, foundTmdbId, bannerImage, bannerSource]);
// Fetch banner when needed // Fetch banner when needed
useEffect(() => { useEffect(() => {
@ -267,6 +267,6 @@ export const useMetadataAssets = (
setBannerImage, setBannerImage,
bannerSource, bannerSource,
logoLoadError: false, logoLoadError: false,
setLogoLoadError: () => {}, setLogoLoadError: () => { },
}; };
}; };

View file

@ -78,8 +78,20 @@ export interface AppSettings {
// AI // AI
aiChatEnabled: boolean; // Enable/disable Ask AI and AI features aiChatEnabled: boolean; // Enable/disable Ask AI and AI features
// Metadata enrichment // Metadata enrichment
enrichMetadataWithTMDB: boolean; // Use TMDB to enrich metadata (cast, certification, posters, fallbacks) enrichMetadataWithTMDB: boolean; // Master switch - use TMDB to enrich metadata
useTmdbLocalizedMetadata: boolean; // Use TMDB localized metadata (titles, overviews) per tmdbLanguagePreference useTmdbLocalizedMetadata: boolean; // Use TMDB localized metadata (titles, overviews) per tmdbLanguagePreference
// Granular TMDB enrichment controls (only apply when enrichMetadataWithTMDB is true)
tmdbEnrichCast: boolean; // Use TMDB cast data (actors, directors, crew)
tmdbEnrichLogos: boolean; // Use TMDB title logos
tmdbEnrichBanners: boolean; // Use TMDB backdrop/banner images
tmdbEnrichCertification: boolean; // Show TMDB content certification (PG-13, R, etc.)
tmdbEnrichRecommendations: boolean; // Show TMDB recommendations
tmdbEnrichEpisodes: boolean; // Use TMDB episode data (thumbnails, info, fallbacks)
tmdbEnrichSeasonPosters: boolean; // Use TMDB season posters
tmdbEnrichProductionInfo: boolean; // Show networks/production companies with logos
tmdbEnrichMovieDetails: boolean; // Show movie details (budget, revenue, tagline, etc.)
tmdbEnrichTvDetails: boolean; // Show TV details (status, seasons count, networks, etc.)
tmdbEnrichCollections: boolean; // Show movie collections/franchises
// Trakt integration // Trakt integration
showTraktComments: boolean; // Show Trakt comments in metadata screens showTraktComments: boolean; // Show Trakt comments in metadata screens
// Continue Watching behavior // Continue Watching behavior
@ -147,6 +159,18 @@ export const DEFAULT_SETTINGS: AppSettings = {
// Metadata enrichment // Metadata enrichment
enrichMetadataWithTMDB: true, enrichMetadataWithTMDB: true,
useTmdbLocalizedMetadata: false, useTmdbLocalizedMetadata: false,
// Granular TMDB enrichment controls (all enabled by default for backward compatibility)
tmdbEnrichCast: true,
tmdbEnrichLogos: true,
tmdbEnrichBanners: true,
tmdbEnrichCertification: true,
tmdbEnrichRecommendations: true,
tmdbEnrichEpisodes: true,
tmdbEnrichSeasonPosters: true,
tmdbEnrichProductionInfo: true,
tmdbEnrichMovieDetails: true,
tmdbEnrichTvDetails: true,
tmdbEnrichCollections: true,
// Trakt integration // Trakt integration
showTraktComments: true, // Show Trakt comments by default when authenticated showTraktComments: true, // Show Trakt comments by default when authenticated
// Continue Watching behavior // Continue Watching behavior

View file

@ -1310,7 +1310,7 @@ const styles = StyleSheet.create({
flexGrow: 1, flexGrow: 1,
width: '100%', width: '100%',
paddingTop: 8, paddingTop: 8,
paddingBottom: 100, paddingBottom: 32,
}, },
// Tablet-specific styles // Tablet-specific styles

View file

@ -651,6 +651,201 @@ const TMDBSettingsScreen = () => {
</View> </View>
</> </>
)} )}
{/* Granular Enrichment Options */}
<View style={styles.divider} />
<Text style={[styles.settingTitle, { color: currentTheme.colors.text, marginBottom: 4 }]}>Enrichment Options</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis, marginBottom: 16 }]}>
Control which data is fetched from TMDb. Disabled options will use addon data if available.
</Text>
{/* Cast & Crew */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Cast & Crew</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Actors, directors, writers with profile photos
</Text>
</View>
<Switch
value={settings.tmdbEnrichCast}
onValueChange={(v) => updateSetting('tmdbEnrichCast', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
{/* Title Logos */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Title Logos</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
High-quality title treatment images
</Text>
</View>
<Switch
value={settings.tmdbEnrichLogos}
onValueChange={(v) => updateSetting('tmdbEnrichLogos', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
{/* Banners/Backdrops */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Banners & Backdrops</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
High-resolution backdrop images
</Text>
</View>
<Switch
value={settings.tmdbEnrichBanners}
onValueChange={(v) => updateSetting('tmdbEnrichBanners', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
{/* Certification */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Content Certification</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Age ratings (PG-13, R, TV-MA, etc.)
</Text>
</View>
<Switch
value={settings.tmdbEnrichCertification}
onValueChange={(v) => updateSetting('tmdbEnrichCertification', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
{/* Recommendations */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Recommendations</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Similar content suggestions
</Text>
</View>
<Switch
value={settings.tmdbEnrichRecommendations}
onValueChange={(v) => updateSetting('tmdbEnrichRecommendations', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
{/* Episode Data */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Episode Data</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Episode thumbnails, info & fallbacks for TV shows
</Text>
</View>
<Switch
value={settings.tmdbEnrichEpisodes}
onValueChange={(v) => updateSetting('tmdbEnrichEpisodes', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
{/* Season Posters */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Season Posters</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Season-specific poster images
</Text>
</View>
<Switch
value={settings.tmdbEnrichSeasonPosters}
onValueChange={(v) => updateSetting('tmdbEnrichSeasonPosters', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
{/* Production Info */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Production Info</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Networks & production companies with logos
</Text>
</View>
<Switch
value={settings.tmdbEnrichProductionInfo}
onValueChange={(v) => updateSetting('tmdbEnrichProductionInfo', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
{/* Movie Details */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Movie Details</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Budget, revenue, runtime, tagline
</Text>
</View>
<Switch
value={settings.tmdbEnrichMovieDetails}
onValueChange={(v) => updateSetting('tmdbEnrichMovieDetails', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
{/* TV Details */}
<View style={styles.settingRow}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>TV Show Details</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Status, seasons count, networks, creators
</Text>
</View>
<Switch
value={settings.tmdbEnrichTvDetails}
onValueChange={(v) => updateSetting('tmdbEnrichTvDetails', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
{/* Collections */}
<View style={[styles.settingRow, { marginBottom: 0 }]}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Movie Collections</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Franchise movies (Marvel, Star Wars, etc.)
</Text>
</View>
<Switch
value={settings.tmdbEnrichCollections}
onValueChange={(v) => updateSetting('tmdbEnrichCollections', v)}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.white : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
</> </>
)} )}
</View> </View>