mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Added granular control for TMDB Enrichment
This commit is contained in:
parent
832e5368be
commit
fd6e29a8ec
8 changed files with 570 additions and 298 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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: () => { },
|
||||
};
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1310,7 +1310,7 @@ const styles = StyleSheet.create({
|
|||
flexGrow: 1,
|
||||
width: '100%',
|
||||
paddingTop: 8,
|
||||
paddingBottom: 100,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
|
||||
// Tablet-specific styles
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue