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")'")
}
// Auto-select first enabled subtitle if none selected
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)")
// Only auto-select first enabled subtitle if textTrack prop is NOT set to -1 (disabled)
// If React Native explicitly set textTrack=-1, user wants subtitles off
if self.textTrack.intValue != -1 {
// Auto-select first enabled subtitle if none selected
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 {
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 {
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 {

View file

@ -11,7 +11,7 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
2AA769395C1242F225F875AF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */; };
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 */; };
9FBA88F52E86ECD700892850 /* KSPlayerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */; };
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; };
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>"; };
18260C6F06D8D6DAF4C1D17E /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
@ -46,7 +46,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
564F8559E25775FFA08707DA /* libPods-Nuvio.a in Frameworks */,
72D4090694139E0DAD9B066E /* libPods-Nuvio.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -76,7 +76,7 @@
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
18260C6F06D8D6DAF4C1D17E /* libPods-Nuvio.a */,
406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */,
);
name = Frameworks;
sourceTree = "<group>";
@ -131,8 +131,8 @@
D90A3959C97EE9926C513293 /* Pods */ = {
isa = PBXGroup;
children = (
2118C3C63E4B7D66EAC534DE /* Pods-Nuvio.debug.xcconfig */,
7F2FA62198C389C99926AA47 /* Pods-Nuvio.release.xcconfig */,
819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */,
15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -152,15 +152,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nuvio" */;
buildPhases = (
4A10611824FCBAA4C1793637 /* [CP] Check Pods Manifest.lock */,
E060D0359630A7C0F4793812 /* [CP] Check Pods Manifest.lock */,
99A79B70155E84EE1FB7F466 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */,
EE80421364369BBCA82253B9 /* [CP] Embed Pods Frameworks */,
778B42B39FEE5454E4D24252 /* [CP] Copy Pods Resources */,
B84E40F81DF927F34032D68B /* [CP] Embed Pods Frameworks */,
AA47AE6072D35F0490B53926 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -234,29 +234,45 @@
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";
};
4A10611824FCBAA4C1793637 /* [CP] Check Pods Manifest.lock */ = {
99A79B70155E84EE1FB7F466 /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
"$(SRCROOT)/.xcode.env",
"$(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 = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Nuvio-checkManifestLockResult.txt",
"$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift",
);
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;
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";
};
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;
buildActionMask = 2147483647;
files = (
@ -368,45 +384,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-resources.sh\"\n";
showEnvVarsInLog = 0;
};
99A79B70155E84EE1FB7F466 /* [Expo] Configure project */ = {
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 */ = {
B84E40F81DF927F34032D68B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -428,6 +406,28 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh\"\n";
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 */
/* Begin PBXSourcesBuildPhase section */
@ -449,7 +449,7 @@
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 2118C3C63E4B7D66EAC534DE /* Pods-Nuvio.debug.xcconfig */;
baseConfigurationReference = 819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
@ -487,7 +487,7 @@
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7F2FA62198C389C99926AA47 /* Pods-Nuvio.release.xcconfig */;
baseConfigurationReference = 15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;

View file

@ -33,6 +33,7 @@ interface SubtitleModalsProps {
loadWyzieSubtitle: (subtitle: WyzieSubtitle) => void;
selectTextTrack: (trackId: number) => void;
disableCustomSubtitles: () => void;
setSubtitlesAutoSelect?: (autoSelect: boolean) => void;
increaseSubtitleSize: () => void;
decreaseSubtitleSize: () => void;
toggleSubtitleBackground: () => void;
@ -89,6 +90,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
subtitleOutlineWidth, setSubtitleOutlineWidth, subtitleAlign, setSubtitleAlign,
subtitleBottomOffset, setSubtitleBottomOffset, subtitleLetterSpacing, setSubtitleLetterSpacing,
subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier, subtitleOffsetSec, setSubtitleOffsetSec,
setSubtitlesAutoSelect,
}) => {
const { width, height } = useWindowDimensions();
const isIos = Platform.OS === 'ios';
@ -157,7 +159,12 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
{activeTab === 'built-in' && (
<View style={{ gap: 8 }}>
<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)' }}
>
<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) => (
<TouchableOpacity
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' }}
>
<Text style={{ color: selectedTextTrack === track.id ? 'black' : 'white' }}>{getTrackDisplayName(track)}</Text>
@ -186,7 +198,12 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
availableSubtitles.map((sub) => (
<TouchableOpacity
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' }}
>
<View>
@ -235,49 +252,49 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
{/* Quick Presets - Hidden for ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
<View style={{ backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 16, padding: sectionPad }}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 10 }}>
<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>
<View style={{ backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 16, padding: sectionPad }}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 10 }}>
<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>
</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 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 */}
@ -305,18 +322,18 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</View>
{/* Show Background - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="layers" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Show Background</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="layers" size={16} color="rgba(255,255,255,0.7)" />
<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>
<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>
@ -328,30 +345,30 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</View>
{/* Text Color - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
<View style={{ marginTop: 8, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="palette" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: 'white', marginLeft: 8, fontWeight: '600' }}>Text Color</Text>
<View style={{ marginTop: 8, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="palette" size={16} color="rgba(255,255,255,0.7)" />
<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 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 */}
{!isExoPlayerInternal && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Align</Text>
<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 => (
<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" />
</TouchableOpacity>
))}
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Align</Text>
<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 => (
<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" />
</TouchableOpacity>
))}
</View>
</View>
</View>
)}
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Bottom Offset</Text>
@ -369,20 +386,20 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</View>
{/* Background Opacity - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Background Opacity</Text>
<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' }}>
<MaterialIcons name="remove" color="#fff" size={18} />
</TouchableOpacity>
<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>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Background Opacity</Text>
<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' }}>
<MaterialIcons name="remove" color="#fff" size={18} />
</TouchableOpacity>
<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>
</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>
<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>
)}
{!isUsingInternalSubtitle && (
<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 */}
{!isExoPlayerInternal && (
<View style={{ marginTop: 4 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Timing Offset (s)</Text>
<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' }}>
<MaterialIcons name="remove" color="#fff" size={18} />
</TouchableOpacity>
<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>
<View style={{ marginTop: 4 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Timing Offset (s)</Text>
<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' }}>
<MaterialIcons name="remove" color="#fff" size={18} />
</TouchableOpacity>
<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>
</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>
<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>
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 11, marginTop: 6 }}>Nudge subtitles earlier (-) or later (+) to sync if needed.</Text>
</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 }}>
<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);
setLoadingCast(true);
try {
if (!settings.enrichMetadataWithTMDB) {
if (__DEV__) logger.log('[loadCast] TMDB enrichment disabled by settings');
// Check both master switch AND granular cast setting
if (!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichCast) {
if (__DEV__) logger.log('[loadCast] TMDB cast enrichment disabled by settings');
// Check if we have addon cast data available
if (metadata?.addonCast && metadata.addonCast.length > 0) {
@ -908,8 +909,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// Centralized logo fetching logic
try {
if (settings.enrichMetadataWithTMDB) {
// Only use TMDB logos when enrichment is ON
// Check both master switch AND granular logos setting
if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichLogos) {
// Only use TMDB logos when both enrichment AND logos option are ON
const tmdbService = TMDBService.getInstance();
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
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');
}
} 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;
if (__DEV__) {
console.log('[useMetadata] TMDB enrichment disabled, using addon logo:', {
console.log('[useMetadata] TMDB logo enrichment disabled, using addon 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;
}
// Clear banner field if TMDB enrichment is enabled to prevent flash
if (settings.enrichMetadataWithTMDB) {
// Clear banner field if TMDB banner enrichment is enabled to prevent flash
if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichBanners) {
finalMetadata = {
...finalMetadata,
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`);
// Fetch season posters from TMDB only if enrichment is enabled; otherwise skip quietly
if (settings.enrichMetadataWithTMDB) {
// Fetch season posters from TMDB only if enrichment AND season posters are enabled
if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichSeasonPosters) {
try {
const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null);
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);
}
} 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 (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) {
// If localized TMDB text is enabled AND episode enrichment is enabled, merge episode names/overviews per language
if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichEpisodes && settings.useTmdbLocalizedMetadata) {
try {
const tmdbIdToUse = tmdbId || (id.startsWith('tt') ? await tmdbService.findTMDBIdByIMDB(id) : null);
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
if (!settings.enrichMetadataWithTMDB) {
if (__DEV__) logger.log('[loadSeriesData] TMDB enrichment disabled; skipping TMDB episode fallback (preserving current episodes)');
// Skip TMDB episode fallback if enrichment or episode enrichment is disabled
if (!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichEpisodes) {
if (__DEV__) logger.log('[loadSeriesData] TMDB episode enrichment disabled; skipping TMDB episode fallback (preserving current episodes)');
return;
}
const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id);
@ -2053,7 +2057,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
useEffect(() => {
const fetchTmdbIdAndRecommendations = async () => {
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;
}
if (metadata && !tmdbId) {
@ -2063,17 +2067,22 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (fetchedTmdbId) {
if (__DEV__) console.log('[useMetadata] extracted TMDB id from content id', { id, fetchedTmdbId });
setTmdbId(fetchedTmdbId);
// Fetch certification
const certification = await tmdbService.getCertification(type, fetchedTmdbId);
if (certification) {
if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification });
setMetadata(prev => prev ? {
...prev,
tmdbId: fetchedTmdbId,
certification
} : null);
// Fetch certification only if granular setting is enabled
if (settings.tmdbEnrichCertification) {
const certification = await tmdbService.getCertification(type, fetchedTmdbId);
if (certification) {
if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification });
setMetadata(prev => prev ? {
...prev,
tmdbId: fetchedTmdbId,
certification
} : null);
} else {
if (__DEV__) console.warn('[useMetadata] certification not returned from TMDB (extract path)', { type, fetchedTmdbId });
}
} 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 {
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(() => {
if (tmdbId) {
if (settings.enrichMetadataWithTMDB) {
if (__DEV__) console.log('[useMetadata] tmdbId available; loading recommendations and enabling certification checks', { tmdbId });
// Check both master switch AND granular recommendations setting
if (settings.enrichMetadataWithTMDB && settings.tmdbEnrichRecommendations) {
if (__DEV__) console.log('[useMetadata] tmdbId available; loading recommendations', { tmdbId });
loadRecommendations();
}
// Reset recommendations when tmdbId changes
@ -2099,21 +2109,23 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
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(() => {
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');
loadCast();
}
}, [metadata, settings.enrichMetadataWithTMDB]);
}, [metadata, settings.enrichMetadataWithTMDB, settings.tmdbEnrichCast]);
// Ensure certification is attached whenever a TMDB id is known and metadata lacks it
useEffect(() => {
const maybeAttachCertification = async () => {
if (!settings.enrichMetadataWithTMDB) {
if (__DEV__) console.log('[useMetadata] enrichment disabled; skip certification (attach path)');
// Check both master switch AND granular certification setting
if (!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichCertification) {
if (__DEV__) console.log('[useMetadata] certification enrichment disabled; skip (attach path)');
return;
}
try {
@ -2142,12 +2154,18 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
}
};
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
const productionInfoFetchedRef = useRef<string | null>(null);
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;
}
@ -2175,7 +2193,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
tmdbId,
useLocalized: settings.useTmdbLocalizedMetadata,
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') {
@ -2187,8 +2209,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
hasNetworks: !!showDetails.networks,
networksCount: showDetails.networks?.length || 0
});
// Fetch networks
if (showDetails.networks) {
// Fetch networks only if production info is enabled
if (settings.tmdbEnrichProductionInfo && showDetails.networks) {
productionInfo = Array.isArray(showDetails.networks)
? showDetails.networks
.map((n: any) => ({
@ -2200,30 +2222,32 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
: [];
}
// Fetch additional TV details
const tvDetails = {
status: showDetails.status,
firstAirDate: showDetails.first_air_date,
lastAirDate: showDetails.last_air_date,
numberOfSeasons: showDetails.number_of_seasons,
numberOfEpisodes: showDetails.number_of_episodes,
episodeRunTime: showDetails.episode_run_time,
type: showDetails.type,
originCountry: showDetails.origin_country,
originalLanguage: showDetails.original_language,
createdBy: showDetails.created_by?.map(creator => ({
id: creator.id,
name: creator.name,
profile_path: creator.profile_path || undefined
})),
};
// Fetch additional TV details only if TV details is enabled
if (settings.tmdbEnrichTvDetails) {
const tvDetails = {
status: showDetails.status,
firstAirDate: showDetails.first_air_date,
lastAirDate: showDetails.last_air_date,
numberOfSeasons: showDetails.number_of_seasons,
numberOfEpisodes: showDetails.number_of_episodes,
episodeRunTime: showDetails.episode_run_time,
type: showDetails.type,
originCountry: showDetails.origin_country,
originalLanguage: showDetails.original_language,
createdBy: showDetails.created_by?.map(creator => ({
id: creator.id,
name: creator.name,
profile_path: creator.profile_path || undefined
})),
};
// Update metadata with TV details
setMetadata((prev: any) => ({
...prev,
tmdbId,
tvDetails
}));
// Update metadata with TV details
setMetadata((prev: any) => ({
...prev,
tmdbId,
tvDetails
}));
}
}
} else if (type === 'movie') {
// Fetch production companies and additional details for movies
@ -2234,8 +2258,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
hasProductionCompanies: !!movieDetails.production_companies,
productionCompaniesCount: movieDetails.production_companies?.length || 0
});
// Fetch production companies
if (movieDetails.production_companies) {
// Fetch production companies only if production info is enabled
if (settings.tmdbEnrichProductionInfo && movieDetails.production_companies) {
productionInfo = Array.isArray(movieDetails.production_companies)
? movieDetails.production_companies
.map((c: any) => ({
@ -2247,27 +2271,29 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
: [];
}
// Fetch additional movie details
const movieDetailsObj = {
status: movieDetails.status,
releaseDate: movieDetails.release_date,
runtime: movieDetails.runtime,
budget: movieDetails.budget,
revenue: movieDetails.revenue,
originalLanguage: movieDetails.original_language,
originCountry: movieDetails.production_countries?.map((c: any) => c.iso_3166_1),
tagline: movieDetails.tagline,
};
// Fetch additional movie details only if movie details is enabled
if (settings.tmdbEnrichMovieDetails) {
const movieDetailsObj = {
status: movieDetails.status,
releaseDate: movieDetails.release_date,
runtime: movieDetails.runtime,
budget: movieDetails.budget,
revenue: movieDetails.revenue,
originalLanguage: movieDetails.original_language,
originCountry: movieDetails.production_countries?.map((c: any) => c.iso_3166_1),
tagline: movieDetails.tagline,
};
// Update metadata with movie details
setMetadata((prev: any) => ({
...prev,
tmdbId,
movieDetails: movieDetailsObj
}));
// Update metadata with movie details
setMetadata((prev: any) => ({
...prev,
tmdbId,
movieDetails: movieDetailsObj
}));
}
// Fetch collection data if movie belongs to a collection
if (movieDetails.belongs_to_collection) {
// Fetch collection data if movie belongs to a collection AND collections is enabled
if (settings.tmdbEnrichCollections && movieDetails.belongs_to_collection) {
setLoadingCollection(true);
try {
const collectionDetails = await tmdbService.getCollectionDetails(
@ -2365,7 +2391,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
};
fetchProductionInfo();
}, [tmdbId, settings.enrichMetadataWithTMDB, metadata, type]);
}, [tmdbId, settings.enrichMetadataWithTMDB, metadata, type, settings.tmdbEnrichProductionInfo, settings.tmdbEnrichTvDetails, settings.tmdbEnrichMovieDetails, settings.tmdbEnrichCollections]);
// Reset tmdbId when id changes
useEffect(() => {

View file

@ -14,7 +14,7 @@ const checkImageAvailability = async (url: string): Promise<boolean> => {
if (imageAvailabilityCache[url] !== undefined) {
return imageAvailabilityCache[url];
}
// Check AsyncStorage cache
try {
const cachedResult = await mmkvStorage.getItem(`image_available:${url}`);
@ -31,7 +31,7 @@ const checkImageAvailability = async (url: string): Promise<boolean> => {
try {
const response = await fetch(url, { method: 'HEAD' });
const isAvailable = response.ok;
// Update caches
imageAvailabilityCache[url] = isAvailable;
try {
@ -39,7 +39,7 @@ const checkImageAvailability = async (url: string): Promise<boolean> => {
} catch (error) {
// Ignore AsyncStorage errors
}
return isAvailable;
} catch (error) {
return false;
@ -47,9 +47,9 @@ const checkImageAvailability = async (url: string): Promise<boolean> => {
};
export const useMetadataAssets = (
metadata: any,
id: string,
type: string,
metadata: any,
id: string,
type: string,
imdbId: string | null,
settings: any,
setMetadata: (metadata: any) => void
@ -58,22 +58,22 @@ export const useMetadataAssets = (
const [bannerImage, setBannerImage] = useState<string | null>(null);
const [loadingBanner, setLoadingBanner] = useState<boolean>(false);
const forcedBannerRefreshDone = useRef<boolean>(false);
// Add source tracking to prevent mixing sources
const [bannerSource, setBannerSource] = useState<'tmdb' | 'metahub' | 'default' | null>(null);
// For TMDB ID tracking
const [foundTmdbId, setFoundTmdbId] = useState<string | null>(null);
const isMountedRef = useRef(true);
// CRITICAL: AbortController to cancel in-flight requests when component unmounts
const abortControllerRef = useRef(new AbortController());
// Track pending requests to prevent duplicate concurrent API calls
const pendingFetchRef = useRef<Promise<void> | null>(null);
// Cleanup on unmount
useEffect(() => {
return () => {
@ -82,12 +82,12 @@ export const useMetadataAssets = (
abortControllerRef.current.abort();
};
}, []);
useEffect(() => {
abortControllerRef.current = new AbortController();
}, [id, type]);
// Force reset when preference changes
useEffect(() => {
// Reset all cached data when preference changes
@ -101,7 +101,7 @@ export const useMetadataAssets = (
// Optimized banner fetching with race condition fixes
const fetchBanner = useCallback(async () => {
if (!metadata || !isMountedRef.current) return;
// Prevent concurrent fetch requests for the same metadata
if (pendingFetchRef.current) {
try {
@ -110,18 +110,18 @@ export const useMetadataAssets = (
// Previous request failed, allow new attempt
}
}
// Create a promise to track this fetch operation
const fetchPromise = (async () => {
try {
if (!isMountedRef.current) return;
if (isMountedRef.current) {
setLoadingBanner(true);
}
// If enrichment is disabled, use addon banner and don't fetch from external sources
if (!settings.enrichMetadataWithTMDB) {
// If enrichment or banner enrichment is disabled, use addon banner and don't fetch from external sources
if (!settings.enrichMetadataWithTMDB || !settings.tmdbEnrichBanners) {
const addonBanner = metadata?.banner || null;
if (isMountedRef.current && addonBanner && addonBanner !== bannerImage) {
setBannerImage(addonBanner);
@ -132,15 +132,15 @@ export const useMetadataAssets = (
}
return;
}
try {
const currentPreference = settings.logoSourcePreference || 'tmdb';
const contentType = type === 'series' ? 'tv' : 'movie';
// Collect final state before updating to prevent intermediate null states
let finalBanner: string | null = bannerImage; // Start with current to prevent flicker
let bannerSourceType: 'tmdb' | 'default' = (bannerSource === 'tmdb' || bannerSource === 'default') ? bannerSource : 'default';
// TMDB path only
if (currentPreference === 'tmdb') {
let tmdbId = null;
@ -163,24 +163,24 @@ export const useMetadataAssets = (
logger.debug('[useMetadataAssets] TMDB ID lookup failed:', error);
}
}
if (tmdbId && isMountedRef.current) {
try {
const tmdbService = TMDBService.getInstance();
const endpoint = contentType === 'tv' ? 'tv' : 'movie';
// Fetch details (AbortSignal will be used for future implementations)
const details = endpoint === 'movie'
? await tmdbService.getMovieDetails(tmdbId)
const details = endpoint === 'movie'
? await tmdbService.getMovieDetails(tmdbId)
: await tmdbService.getTVShowDetails(Number(tmdbId));
// Only update if request wasn't aborted and component is still mounted
if (!isMountedRef.current) return;
if (details?.backdrop_path) {
finalBanner = tmdbService.getImageUrl(details.backdrop_path);
bannerSourceType = 'tmdb';
// Preload the image
if (finalBanner) {
FastImage.preload([{ uri: finalBanner }]);
@ -196,10 +196,10 @@ export const useMetadataAssets = (
// Request was cancelled, don't update state
return;
}
// Only update state if still mounted after error
if (!isMountedRef.current) return;
logger.debug('[useMetadataAssets] TMDB details fetch failed:', error);
// Keep current banner on error instead of setting to null
finalBanner = bannerImage || metadata?.banner || null;
@ -207,27 +207,27 @@ export const useMetadataAssets = (
}
}
}
// Final fallback to metadata banner only
if (!finalBanner) {
finalBanner = metadata?.banner || null;
bannerSourceType = 'default';
}
// CRITICAL: Batch all state updates into a single call to prevent race conditions
// This ensures the native view hierarchy doesn't receive conflicting unmount/remount signals
if (isMountedRef.current && (finalBanner !== bannerImage || bannerSourceType !== bannerSource)) {
setBannerImage(finalBanner);
setBannerSource(bannerSourceType);
}
if (isMountedRef.current) {
forcedBannerRefreshDone.current = true;
}
} catch (error) {
// Outer catch for any unexpected errors
if (!isMountedRef.current) return;
logger.error('[useMetadataAssets] Unexpected error in banner fetch:', error);
// Use current banner on error, don't set to null
const defaultBanner = bannerImage || metadata?.banner || null;
@ -244,17 +244,17 @@ export const useMetadataAssets = (
pendingFetchRef.current = null;
}
})();
pendingFetchRef.current = 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
useEffect(() => {
if (!isMountedRef.current) return;
const currentPreference = settings.logoSourcePreference || 'tmdb';
if (bannerSource !== currentPreference && !forcedBannerRefreshDone.current) {
fetchBanner();
}
@ -267,6 +267,6 @@ export const useMetadataAssets = (
setBannerImage,
bannerSource,
logoLoadError: false,
setLogoLoadError: () => {},
setLogoLoadError: () => { },
};
};

View file

@ -78,8 +78,20 @@ export interface AppSettings {
// AI
aiChatEnabled: boolean; // Enable/disable Ask AI and AI features
// 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
// 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
showTraktComments: boolean; // Show Trakt comments in metadata screens
// Continue Watching behavior
@ -147,6 +159,18 @@ export const DEFAULT_SETTINGS: AppSettings = {
// Metadata enrichment
enrichMetadataWithTMDB: true,
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
showTraktComments: true, // Show Trakt comments by default when authenticated
// Continue Watching behavior

View file

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

View file

@ -651,6 +651,201 @@ const TMDBSettingsScreen = () => {
</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>