mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
some sentry fixes maximum update limti reached
This commit is contained in:
parent
c01528b309
commit
f05366ae45
6 changed files with 186 additions and 85 deletions
|
|
@ -48,8 +48,8 @@ RCT_EXTERN_METHOD(showAirPlayPicker:(nonnull NSNumber *)node)
|
||||||
|
|
||||||
@interface RCT_EXTERN_MODULE(KSPlayerModule, RCTEventEmitter)
|
@interface RCT_EXTERN_MODULE(KSPlayerModule, RCTEventEmitter)
|
||||||
|
|
||||||
RCT_EXTERN_METHOD(getTracks:(nonnull NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
RCT_EXTERN_METHOD(getTracks:(NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||||
RCT_EXTERN_METHOD(getAirPlayState:(nonnull NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
RCT_EXTERN_METHOD(getAirPlayState:(NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||||
RCT_EXTERN_METHOD(showAirPlayPicker:(nonnull NSNumber *)nodeTag)
|
RCT_EXTERN_METHOD(showAirPlayPicker:(NSNumber *)nodeTag)
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,11 @@ class KSPlayerModule: RCTEventEmitter {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func getTracks(_ nodeTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
@objc func getTracks(_ nodeTag: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||||
|
guard let nodeTag = nodeTag else {
|
||||||
|
reject("INVALID_ARGUMENT", "nodeTag must not be nil", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
|
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
|
||||||
viewManager.getTracks(nodeTag, resolve: resolve, reject: reject)
|
viewManager.getTracks(nodeTag, resolve: resolve, reject: reject)
|
||||||
|
|
@ -35,7 +39,11 @@ class KSPlayerModule: RCTEventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func getAirPlayState(_ nodeTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
@objc func getAirPlayState(_ nodeTag: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||||
|
guard let nodeTag = nodeTag else {
|
||||||
|
reject("INVALID_ARGUMENT", "nodeTag must not be nil", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
|
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
|
||||||
viewManager.getAirPlayState(nodeTag, resolve: resolve, reject: reject)
|
viewManager.getAirPlayState(nodeTag, resolve: resolve, reject: reject)
|
||||||
|
|
@ -45,7 +53,11 @@ class KSPlayerModule: RCTEventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func showAirPlayPicker(_ nodeTag: NSNumber) {
|
@objc func showAirPlayPicker(_ nodeTag: NSNumber?) {
|
||||||
|
guard let nodeTag = nodeTag else {
|
||||||
|
print("[KSPlayerModule] showAirPlayPicker called with nil nodeTag")
|
||||||
|
return
|
||||||
|
}
|
||||||
print("[KSPlayerModule] showAirPlayPicker called for nodeTag: \(nodeTag)")
|
print("[KSPlayerModule] showAirPlayPicker called for nodeTag: \(nodeTag)")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
|
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
|
||||||
|
|
|
||||||
|
|
@ -543,7 +543,11 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => a - b);
|
const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => {
|
||||||
|
if (a === 0) return 1;
|
||||||
|
if (b === 0) return -1;
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[
|
<View style={[
|
||||||
|
|
@ -660,7 +664,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
{ color: currentTheme.colors.highEmphasis }
|
{ color: currentTheme.colors.highEmphasis }
|
||||||
]
|
]
|
||||||
]} numberOfLines={1}>
|
]} numberOfLines={1}>
|
||||||
Season {season}
|
{season === 0 ? 'Specials' : `Season ${season}`}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -723,7 +727,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
]
|
]
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
Season {season}
|
{season === 0 ? 'Specials' : `Season ${season}`}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,9 @@ const KSPlayer = forwardRef<KSPlayerRef, KSPlayerProps>((props, ref) => {
|
||||||
getTracks: async () => {
|
getTracks: async () => {
|
||||||
if (nativeRef.current) {
|
if (nativeRef.current) {
|
||||||
const node = findNodeHandle(nativeRef.current);
|
const node = findNodeHandle(nativeRef.current);
|
||||||
return await KSPlayerModule.getTracks(node);
|
if (node) {
|
||||||
|
return await KSPlayerModule.getTracks(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { audioTracks: [], textTracks: [] };
|
return { audioTracks: [], textTracks: [] };
|
||||||
},
|
},
|
||||||
|
|
@ -153,15 +155,21 @@ const KSPlayer = forwardRef<KSPlayerRef, KSPlayerProps>((props, ref) => {
|
||||||
getAirPlayState: async () => {
|
getAirPlayState: async () => {
|
||||||
if (nativeRef.current) {
|
if (nativeRef.current) {
|
||||||
const node = findNodeHandle(nativeRef.current);
|
const node = findNodeHandle(nativeRef.current);
|
||||||
return await KSPlayerModule.getAirPlayState(node);
|
if (node) {
|
||||||
|
return await KSPlayerModule.getAirPlayState(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { allowsExternalPlayback: false, usesExternalPlaybackWhileExternalScreenIsActive: false, isExternalPlaybackActive: false };
|
return { allowsExternalPlayback: false, usesExternalPlaybackWhileExternalScreenIsActive: false, isExternalPlaybackActive: false };
|
||||||
},
|
},
|
||||||
showAirPlayPicker: () => {
|
showAirPlayPicker: () => {
|
||||||
if (nativeRef.current) {
|
if (nativeRef.current) {
|
||||||
const node = findNodeHandle(nativeRef.current);
|
const node = findNodeHandle(nativeRef.current);
|
||||||
console.log('[KSPlayerComponent] Calling showAirPlayPicker with node:', node);
|
if (node) {
|
||||||
KSPlayerModule.showAirPlayPicker(node);
|
console.log('[KSPlayerComponent] Calling showAirPlayPicker with node:', node);
|
||||||
|
KSPlayerModule.showAirPlayPicker(node);
|
||||||
|
} else {
|
||||||
|
console.warn('[KSPlayerComponent] Cannot call showAirPlayPicker: node is null');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('[KSPlayerComponent] nativeRef.current is null');
|
console.log('[KSPlayerComponent] nativeRef.current is null');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1181,14 +1181,59 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
|
|
||||||
// Determine initial season only once per series
|
// Determine initial season only once per series
|
||||||
const seasons = Object.keys(groupedAddonEpisodes).map(Number);
|
const seasons = Object.keys(groupedAddonEpisodes).map(Number);
|
||||||
const firstSeason = Math.min(...seasons);
|
const nonZeroSeasons = seasons.filter(s => s !== 0);
|
||||||
|
const firstSeason = nonZeroSeasons.length > 0 ? Math.min(...nonZeroSeasons) : Math.min(...seasons);
|
||||||
if (!initializedSeasonRef.current) {
|
if (!initializedSeasonRef.current) {
|
||||||
const nextSeason = firstSeason;
|
// Check for watch progress to auto-select season
|
||||||
if (selectedSeason !== nextSeason) {
|
let selectedSeasonNumber = firstSeason;
|
||||||
logger.log(`📺 Setting season ${nextSeason} as selected (${groupedAddonEpisodes[nextSeason]?.length || 0} episodes)`);
|
try {
|
||||||
setSelectedSeason(nextSeason);
|
const allProgress = await storageService.getAllWatchProgress();
|
||||||
|
let mostRecentEpisodeId = '';
|
||||||
|
let mostRecentTimestamp = 0;
|
||||||
|
Object.entries(allProgress).forEach(([key, progress]) => {
|
||||||
|
if (key.includes(`series:${id}:`)) {
|
||||||
|
const episodeId = key.split(`series:${id}:`)[1];
|
||||||
|
if (progress.lastUpdated > mostRecentTimestamp && progress.currentTime > 0) {
|
||||||
|
mostRecentTimestamp = progress.lastUpdated;
|
||||||
|
mostRecentEpisodeId = episodeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mostRecentEpisodeId) {
|
||||||
|
// Try to parse season from ID or find matching episode
|
||||||
|
const parts = mostRecentEpisodeId.split(':');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
// Format: showId:season:episode
|
||||||
|
const watchProgressSeason = parseInt(parts[1], 10);
|
||||||
|
if (groupedAddonEpisodes[watchProgressSeason]) {
|
||||||
|
selectedSeasonNumber = watchProgressSeason;
|
||||||
|
logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for ${mostRecentEpisodeId}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Try to find by stremioId
|
||||||
|
const allEpisodesList = Object.values(groupedAddonEpisodes).flat();
|
||||||
|
const episode = allEpisodesList.find(ep => ep.stremioId === mostRecentEpisodeId);
|
||||||
|
if (episode) {
|
||||||
|
selectedSeasonNumber = episode.season_number;
|
||||||
|
logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for episode with stremioId ${mostRecentEpisodeId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No watch progress, try persistent storage
|
||||||
|
selectedSeasonNumber = getSeason(id, firstSeason);
|
||||||
|
logger.log(`[useMetadata] No watch progress found, using persistent season ${selectedSeasonNumber}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[useMetadata] Error checking watch progress for season selection:', error);
|
||||||
|
selectedSeasonNumber = getSeason(id, firstSeason);
|
||||||
}
|
}
|
||||||
setEpisodes(groupedAddonEpisodes[nextSeason] || []);
|
|
||||||
|
if (selectedSeason !== selectedSeasonNumber) {
|
||||||
|
logger.log(`📺 Setting season ${selectedSeasonNumber} as selected`);
|
||||||
|
setSelectedSeason(selectedSeasonNumber);
|
||||||
|
}
|
||||||
|
setEpisodes(groupedAddonEpisodes[selectedSeasonNumber] || []);
|
||||||
initializedSeasonRef.current = true;
|
initializedSeasonRef.current = true;
|
||||||
} else {
|
} else {
|
||||||
// Keep current selection; refresh episode list for selected season
|
// Keep current selection; refresh episode list for selected season
|
||||||
|
|
@ -1238,8 +1283,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
|
|
||||||
setGroupedEpisodes(transformedEpisodes);
|
setGroupedEpisodes(transformedEpisodes);
|
||||||
|
|
||||||
// Get the first available season as fallback
|
// Get the first available season as fallback (preferring non-zero seasons)
|
||||||
const firstSeason = Math.min(...Object.keys(allEpisodes).map(Number));
|
const availableSeasons = Object.keys(allEpisodes).map(Number);
|
||||||
|
const nonZeroSeasons = availableSeasons.filter(s => s !== 0);
|
||||||
|
const firstSeason = nonZeroSeasons.length > 0 ? Math.min(...nonZeroSeasons) : Math.min(...availableSeasons);
|
||||||
|
|
||||||
if (!initializedSeasonRef.current) {
|
if (!initializedSeasonRef.current) {
|
||||||
// Check for watch progress to auto-select season
|
// Check for watch progress to auto-select season
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ const HomeScreen = () => {
|
||||||
const [hasAddons, setHasAddons] = useState<boolean | null>(null);
|
const [hasAddons, setHasAddons] = useState<boolean | null>(null);
|
||||||
const [hintVisible, setHintVisible] = useState(false);
|
const [hintVisible, setHintVisible] = useState(false);
|
||||||
const totalCatalogsRef = useRef(0);
|
const totalCatalogsRef = useRef(0);
|
||||||
const [visibleCatalogCount, setVisibleCatalogCount] = useState(5); // Reduced for memory
|
const [visibleCatalogCount, setVisibleCatalogCount] = useState(5); // Reduced for memory
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
// Stabilize insets to prevent iOS layout shifts
|
// Stabilize insets to prevent iOS layout shifts
|
||||||
|
|
@ -147,7 +147,7 @@ const HomeScreen = () => {
|
||||||
}, 100);
|
}, 100);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [insets.top]);
|
}, [insets.top]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
featuredContent,
|
featuredContent,
|
||||||
allFeaturedContent,
|
allFeaturedContent,
|
||||||
|
|
@ -158,43 +158,49 @@ const HomeScreen = () => {
|
||||||
refreshFeatured
|
refreshFeatured
|
||||||
} = useFeaturedContent();
|
} = useFeaturedContent();
|
||||||
|
|
||||||
|
// Guard to prevent overlapping fetch calls
|
||||||
|
const isFetchingRef = useRef(false);
|
||||||
|
|
||||||
// Progressive catalog loading function with performance optimizations
|
// Progressive catalog loading function with performance optimizations
|
||||||
const loadCatalogsProgressively = useCallback(async () => {
|
const loadCatalogsProgressively = useCallback(async () => {
|
||||||
|
if (isFetchingRef.current) return;
|
||||||
|
isFetchingRef.current = true;
|
||||||
|
|
||||||
setCatalogsLoading(true);
|
setCatalogsLoading(true);
|
||||||
setCatalogs([]);
|
setCatalogs([]);
|
||||||
setLoadedCatalogCount(0);
|
setLoadedCatalogCount(0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check cache first
|
// Check cache first
|
||||||
let catalogSettings: Record<string, boolean> = {};
|
let catalogSettings: Record<string, boolean> = {};
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (cachedCatalogSettings && (now - catalogSettingsCacheTimestamp) < CATALOG_SETTINGS_CACHE_TTL) {
|
if (cachedCatalogSettings && (now - catalogSettingsCacheTimestamp) < CATALOG_SETTINGS_CACHE_TTL) {
|
||||||
catalogSettings = cachedCatalogSettings;
|
catalogSettings = cachedCatalogSettings;
|
||||||
} else {
|
} else {
|
||||||
// Load from storage
|
// Load from storage
|
||||||
const catalogSettingsJson = await mmkvStorage.getItem(CATALOG_SETTINGS_KEY);
|
const catalogSettingsJson = await mmkvStorage.getItem(CATALOG_SETTINGS_KEY);
|
||||||
catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {};
|
catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {};
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
cachedCatalogSettings = catalogSettings;
|
cachedCatalogSettings = catalogSettings;
|
||||||
catalogSettingsCacheTimestamp = now;
|
catalogSettingsCacheTimestamp = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [addons, addonManifests] = await Promise.all([
|
const [addons, addonManifests] = await Promise.all([
|
||||||
catalogService.getAllAddons(),
|
catalogService.getAllAddons(),
|
||||||
stremioService.getInstalledAddonsAsync()
|
stremioService.getInstalledAddonsAsync()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Set hasAddons state based on whether we have any addons - ensure on main thread
|
// Set hasAddons state based on whether we have any addons - ensure on main thread
|
||||||
InteractionManager.runAfterInteractions(() => {
|
InteractionManager.runAfterInteractions(() => {
|
||||||
setHasAddons(addons.length > 0);
|
setHasAddons(addons.length > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create placeholder array with proper order and track indices
|
// Create placeholder array with proper order and track indices
|
||||||
let catalogIndex = 0;
|
let catalogIndex = 0;
|
||||||
const catalogQueue: (() => Promise<void>)[] = [];
|
const catalogQueue: (() => Promise<void>)[] = [];
|
||||||
|
|
||||||
// Launch all catalog loaders in parallel
|
// Launch all catalog loaders in parallel
|
||||||
const launchAllCatalogs = () => {
|
const launchAllCatalogs = () => {
|
||||||
while (catalogQueue.length > 0) {
|
while (catalogQueue.length > 0) {
|
||||||
|
|
@ -204,18 +210,18 @@ const HomeScreen = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const addon of addons) {
|
for (const addon of addons) {
|
||||||
if (addon.catalogs) {
|
if (addon.catalogs) {
|
||||||
for (const catalog of addon.catalogs) {
|
for (const catalog of addon.catalogs) {
|
||||||
// Check if this catalog is enabled (default to true if no setting exists)
|
// Check if this catalog is enabled (default to true if no setting exists)
|
||||||
const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`;
|
const settingKey = `${addon.id}:${catalog.type}:${catalog.id}`;
|
||||||
const isEnabled = catalogSettings[settingKey] ?? true;
|
const isEnabled = catalogSettings[settingKey] ?? true;
|
||||||
|
|
||||||
// Only load enabled catalogs
|
// Only load enabled catalogs
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
const currentIndex = catalogIndex;
|
const currentIndex = catalogIndex;
|
||||||
|
|
||||||
const catalogLoader = async () => {
|
const catalogLoader = async () => {
|
||||||
try {
|
try {
|
||||||
const manifest = addonManifests.find((a: any) => a.id === addon.id);
|
const manifest = addonManifests.find((a: any) => a.id === addon.id);
|
||||||
|
|
@ -226,7 +232,7 @@ const HomeScreen = () => {
|
||||||
// Aggressively limit items per catalog on Android to reduce memory usage
|
// Aggressively limit items per catalog on Android to reduce memory usage
|
||||||
const limit = Platform.OS === 'android' ? 18 : 30;
|
const limit = Platform.OS === 'android' ? 18 : 30;
|
||||||
const limitedMetas = metas.slice(0, limit);
|
const limitedMetas = metas.slice(0, limit);
|
||||||
|
|
||||||
const items = limitedMetas.map((meta: any) => ({
|
const items = limitedMetas.map((meta: any) => ({
|
||||||
id: meta.id,
|
id: meta.id,
|
||||||
type: meta.type,
|
type: meta.type,
|
||||||
|
|
@ -267,7 +273,7 @@ const HomeScreen = () => {
|
||||||
displayName = `${displayName} ${contentType}`;
|
displayName = `${displayName} ${contentType}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const catalogContent = {
|
const catalogContent = {
|
||||||
addon: addon.id,
|
addon: addon.id,
|
||||||
type: catalog.type,
|
type: catalog.type,
|
||||||
|
|
@ -275,7 +281,7 @@ const HomeScreen = () => {
|
||||||
name: displayName,
|
name: displayName,
|
||||||
items
|
items
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the catalog at its specific position - ensure on main thread
|
// Update the catalog at its specific position - ensure on main thread
|
||||||
InteractionManager.runAfterInteractions(() => {
|
InteractionManager.runAfterInteractions(() => {
|
||||||
setCatalogs(prevCatalogs => {
|
setCatalogs(prevCatalogs => {
|
||||||
|
|
@ -296,26 +302,37 @@ const HomeScreen = () => {
|
||||||
if (prev === 0) {
|
if (prev === 0) {
|
||||||
setCatalogsLoading(false);
|
setCatalogsLoading(false);
|
||||||
}
|
}
|
||||||
|
// ** Crucial: If all catalogs processed, release the fetch guard **
|
||||||
|
if (next >= totalCatalogsRef.current) {
|
||||||
|
isFetchingRef.current = false;
|
||||||
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
catalogQueue.push(catalogLoader);
|
catalogQueue.push(catalogLoader);
|
||||||
catalogIndex++;
|
catalogIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
totalCatalogsRef.current = catalogIndex;
|
totalCatalogsRef.current = catalogIndex;
|
||||||
|
|
||||||
|
// If no catalogs to load, release locks immediately
|
||||||
|
if (catalogIndex === 0) {
|
||||||
|
setCatalogsLoading(false);
|
||||||
|
isFetchingRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize catalogs array with proper length - ensure on main thread
|
// Initialize catalogs array with proper length - ensure on main thread
|
||||||
InteractionManager.runAfterInteractions(() => {
|
InteractionManager.runAfterInteractions(() => {
|
||||||
setCatalogs(new Array(catalogIndex).fill(null));
|
setCatalogs(new Array(catalogIndex).fill(null));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start all catalog requests in parallel
|
// Start all catalog requests in parallel
|
||||||
launchAllCatalogs();
|
launchAllCatalogs();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -323,6 +340,7 @@ const HomeScreen = () => {
|
||||||
InteractionManager.runAfterInteractions(() => {
|
InteractionManager.runAfterInteractions(() => {
|
||||||
setCatalogsLoading(false);
|
setCatalogsLoading(false);
|
||||||
});
|
});
|
||||||
|
isFetchingRef.current = false;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -371,7 +389,7 @@ const HomeScreen = () => {
|
||||||
// Also show a global toast for consistency across screens
|
// Also show a global toast for consistency across screens
|
||||||
// showInfo('Sign In Available', 'You can sign in anytime from Settings → Account');
|
// showInfo('Sign In Available', 'You can sign in anytime from Settings → Account');
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
if (hideTimer) clearTimeout(hideTimer);
|
if (hideTimer) clearTimeout(hideTimer);
|
||||||
|
|
@ -389,10 +407,10 @@ const HomeScreen = () => {
|
||||||
setShowHeroSection(settings.showHeroSection);
|
setShowHeroSection(settings.showHeroSection);
|
||||||
setFeaturedContentSource(settings.featuredContentSource);
|
setFeaturedContentSource(settings.featuredContentSource);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Subscribe to settings changes
|
// Subscribe to settings changes
|
||||||
const unsubscribe = settingsEmitter.addListener(handleSettingsChange);
|
const unsubscribe = settingsEmitter.addListener(handleSettingsChange);
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [settings.showHeroSection, settings.featuredContentSource]);
|
}, [settings.showHeroSection, settings.featuredContentSource]);
|
||||||
|
|
||||||
|
|
@ -409,12 +427,12 @@ const HomeScreen = () => {
|
||||||
StatusBar.setHidden(false);
|
StatusBar.setHidden(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
statusBarConfig();
|
statusBarConfig();
|
||||||
|
|
||||||
// Unlock orientation to allow free rotation
|
// Unlock orientation to allow free rotation
|
||||||
ScreenOrientation.unlockAsync().catch(() => {});
|
ScreenOrientation.unlockAsync().catch(() => { });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Stop trailer when screen loses focus (navigating to other screens)
|
// Stop trailer when screen loses focus (navigating to other screens)
|
||||||
setTrailerPlaying(false);
|
setTrailerPlaying(false);
|
||||||
|
|
@ -450,12 +468,12 @@ const HomeScreen = () => {
|
||||||
StatusBar.setTranslucent(false);
|
StatusBar.setTranslucent(false);
|
||||||
StatusBar.setBackgroundColor(currentTheme.colors.darkBackground);
|
StatusBar.setBackgroundColor(currentTheme.colors.darkBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up any lingering timeouts
|
// Clean up any lingering timeouts
|
||||||
if (refreshTimeoutRef.current) {
|
if (refreshTimeoutRef.current) {
|
||||||
clearTimeout(refreshTimeoutRef.current);
|
clearTimeout(refreshTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't clear FastImage cache on unmount - it causes broken images on remount
|
// Don't clear FastImage cache on unmount - it causes broken images on remount
|
||||||
// FastImage's native libraries (SDWebImage/Glide) handle memory automatically
|
// FastImage's native libraries (SDWebImage/Glide) handle memory automatically
|
||||||
// Cache clearing only happens on app background (see AppState handler above)
|
// Cache clearing only happens on app background (see AppState handler above)
|
||||||
|
|
@ -468,11 +486,11 @@ const HomeScreen = () => {
|
||||||
// Balanced preload images function using FastImage
|
// Balanced preload images function using FastImage
|
||||||
const preloadImages = useCallback(async (content: StreamingContent[]) => {
|
const preloadImages = useCallback(async (content: StreamingContent[]) => {
|
||||||
if (!content.length) return;
|
if (!content.length) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Moderate prefetching for better performance balance
|
// Moderate prefetching for better performance balance
|
||||||
const MAX_IMAGES = 10; // Preload 10 most important images
|
const MAX_IMAGES = 10; // Preload 10 most important images
|
||||||
|
|
||||||
// Only preload poster images (skip banner and logo entirely)
|
// Only preload poster images (skip banner and logo entirely)
|
||||||
const posterImages = content.slice(0, MAX_IMAGES)
|
const posterImages = content.slice(0, MAX_IMAGES)
|
||||||
.map(item => item.poster)
|
.map(item => item.poster)
|
||||||
|
|
@ -499,24 +517,24 @@ const HomeScreen = () => {
|
||||||
|
|
||||||
const handlePlayStream = useCallback(async (stream: Stream) => {
|
const handlePlayStream = useCallback(async (stream: Stream) => {
|
||||||
if (!featuredContent) return;
|
if (!featuredContent) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Don't clear cache before player - causes broken images on return
|
// Don't clear cache before player - causes broken images on return
|
||||||
// FastImage's native libraries handle memory efficiently
|
// FastImage's native libraries handle memory efficiently
|
||||||
|
|
||||||
// Lock orientation to landscape before navigation to prevent glitches
|
// Lock orientation to landscape before navigation to prevent glitches
|
||||||
try {
|
try {
|
||||||
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
|
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
|
||||||
|
|
||||||
// Longer delay to ensure orientation is fully set before navigation
|
// Longer delay to ensure orientation is fully set before navigation
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
} catch (orientationError) {
|
} catch (orientationError) {
|
||||||
// If orientation lock fails, continue anyway but log it
|
// If orientation lock fails, continue anyway but log it
|
||||||
logger.warn('[HomeScreen] Orientation lock failed:', orientationError);
|
logger.warn('[HomeScreen] Orientation lock failed:', orientationError);
|
||||||
// Still add a small delay
|
// Still add a small delay
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
navigation.navigate(Platform.OS === 'ios' ? 'PlayerIOS' : 'PlayerAndroid', {
|
navigation.navigate(Platform.OS === 'ios' ? 'PlayerIOS' : 'PlayerAndroid', {
|
||||||
uri: stream.url,
|
uri: stream.url,
|
||||||
title: featuredContent.name,
|
title: featuredContent.name,
|
||||||
|
|
@ -528,7 +546,7 @@ const HomeScreen = () => {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[HomeScreen] Error in handlePlayStream:', error);
|
logger.error('[HomeScreen] Error in handlePlayStream:', error);
|
||||||
|
|
||||||
// Fallback: navigate anyway
|
// Fallback: navigate anyway
|
||||||
navigation.navigate(Platform.OS === 'ios' ? 'PlayerIOS' : 'PlayerAndroid', {
|
navigation.navigate(Platform.OS === 'ios' ? 'PlayerIOS' : 'PlayerAndroid', {
|
||||||
uri: stream.url,
|
uri: stream.url,
|
||||||
|
|
@ -545,9 +563,9 @@ const HomeScreen = () => {
|
||||||
const refreshContinueWatching = useCallback(async () => {
|
const refreshContinueWatching = useCallback(async () => {
|
||||||
if (continueWatchingRef.current) {
|
if (continueWatchingRef.current) {
|
||||||
try {
|
try {
|
||||||
const hasContent = await continueWatchingRef.current.refresh();
|
const hasContent = await continueWatchingRef.current.refresh();
|
||||||
setHasContinueWatching(hasContent);
|
setHasContinueWatching(hasContent);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (__DEV__) console.error('[HomeScreen] Error refreshing continue watching:', error);
|
if (__DEV__) console.error('[HomeScreen] Error refreshing continue watching:', error);
|
||||||
setHasContinueWatching(false);
|
setHasContinueWatching(false);
|
||||||
|
|
@ -555,19 +573,31 @@ const HomeScreen = () => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Use refs to track state for event listeners without triggering re-effects
|
||||||
|
const catalogsLengthRef = useRef(catalogs.length);
|
||||||
|
const catalogsLoadingRef = useRef(catalogsLoading);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
catalogsLengthRef.current = catalogs.length;
|
||||||
|
}, [catalogs.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
catalogsLoadingRef.current = catalogsLoading;
|
||||||
|
}, [catalogsLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = navigation.addListener('focus', () => {
|
const unsubscribe = navigation.addListener('focus', () => {
|
||||||
// Only refresh continue watching section on focus
|
// Only refresh continue watching section on focus
|
||||||
refreshContinueWatching();
|
refreshContinueWatching();
|
||||||
// Don't reload catalogs unless they haven't been loaded yet
|
// Don't reload catalogs unless they haven't been loaded yet
|
||||||
// Catalogs will be refreshed through context updates when addons change
|
// Uses refs to avoid re-binding the listener on every state change
|
||||||
if (catalogs.length === 0 && !catalogsLoading) {
|
if (catalogsLengthRef.current === 0 && !catalogsLoadingRef.current) {
|
||||||
loadCatalogsProgressively();
|
loadCatalogsProgressively();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [navigation, refreshContinueWatching, loadCatalogsProgressively, catalogs.length, catalogsLoading]);
|
}, [navigation, refreshContinueWatching, loadCatalogsProgressively]);
|
||||||
|
|
||||||
// Memoize the loading screen to prevent unnecessary re-renders
|
// Memoize the loading screen to prevent unnecessary re-renders
|
||||||
const renderLoadingScreen = useMemo(() => {
|
const renderLoadingScreen = useMemo(() => {
|
||||||
|
|
@ -603,7 +633,7 @@ const HomeScreen = () => {
|
||||||
|
|
||||||
// Only show a limited number of catalogs initially for performance
|
// Only show a limited number of catalogs initially for performance
|
||||||
const catalogsToShow = catalogs.slice(0, visibleCatalogCount);
|
const catalogsToShow = catalogs.slice(0, visibleCatalogCount);
|
||||||
|
|
||||||
catalogsToShow.forEach((catalog, index) => {
|
catalogsToShow.forEach((catalog, index) => {
|
||||||
if (catalog) {
|
if (catalog) {
|
||||||
data.push({ type: 'catalog', catalog, key: `${catalog.addon}-${catalog.id}-${index}` });
|
data.push({ type: 'catalog', catalog, key: `${catalog.addon}-${catalog.id}-${index}` });
|
||||||
|
|
@ -637,7 +667,7 @@ const HomeScreen = () => {
|
||||||
// Memoize individual section components to prevent re-renders
|
// Memoize individual section components to prevent re-renders
|
||||||
const memoizedFeaturedContent = useMemo(() => {
|
const memoizedFeaturedContent = useMemo(() => {
|
||||||
const heroStyleToUse = settings.heroStyle;
|
const heroStyleToUse = settings.heroStyle;
|
||||||
|
|
||||||
// AppleTVHero is only available on mobile devices (not tablets)
|
// AppleTVHero is only available on mobile devices (not tablets)
|
||||||
if (heroStyleToUse === 'appletv' && !isTablet) {
|
if (heroStyleToUse === 'appletv' && !isTablet) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -694,7 +724,7 @@ const HomeScreen = () => {
|
||||||
const lastToggleRef = useRef(0);
|
const lastToggleRef = useRef(0);
|
||||||
const scrollAnimationFrameRef = useRef<number | null>(null);
|
const scrollAnimationFrameRef = useRef<number | null>(null);
|
||||||
const isScrollingRef = useRef(false);
|
const isScrollingRef = useRef(false);
|
||||||
|
|
||||||
const toggleHeader = useCallback((hide: boolean) => {
|
const toggleHeader = useCallback((hide: boolean) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastToggleRef.current < 120) return; // debounce
|
if (now - lastToggleRef.current < 120) return; // debounce
|
||||||
|
|
@ -783,26 +813,26 @@ const HomeScreen = () => {
|
||||||
const handleScroll = useCallback((event: any) => {
|
const handleScroll = useCallback((event: any) => {
|
||||||
// Persist the event before using requestAnimationFrame to prevent event pooling issues
|
// Persist the event before using requestAnimationFrame to prevent event pooling issues
|
||||||
event.persist();
|
event.persist();
|
||||||
|
|
||||||
// Cancel any pending animation frame
|
// Cancel any pending animation frame
|
||||||
if (scrollAnimationFrameRef.current !== null) {
|
if (scrollAnimationFrameRef.current !== null) {
|
||||||
cancelAnimationFrame(scrollAnimationFrameRef.current);
|
cancelAnimationFrame(scrollAnimationFrameRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture scroll values immediately before async operation
|
// Capture scroll values immediately before async operation
|
||||||
const scrollYValue = event.nativeEvent.contentOffset.y;
|
const scrollYValue = event.nativeEvent.contentOffset.y;
|
||||||
|
|
||||||
// Update shared value for parallax (on UI thread)
|
// Update shared value for parallax (on UI thread)
|
||||||
scrollY.value = scrollYValue;
|
scrollY.value = scrollYValue;
|
||||||
|
|
||||||
// Use requestAnimationFrame to throttle scroll handling
|
// Use requestAnimationFrame to throttle scroll handling
|
||||||
scrollAnimationFrameRef.current = requestAnimationFrame(() => {
|
scrollAnimationFrameRef.current = requestAnimationFrame(() => {
|
||||||
const y = scrollYValue;
|
const y = scrollYValue;
|
||||||
const dy = y - lastScrollYRef.current;
|
const dy = y - lastScrollYRef.current;
|
||||||
lastScrollYRef.current = y;
|
lastScrollYRef.current = y;
|
||||||
|
|
||||||
isScrollingRef.current = Math.abs(dy) > 0;
|
isScrollingRef.current = Math.abs(dy) > 0;
|
||||||
|
|
||||||
if (y <= 10) {
|
if (y <= 10) {
|
||||||
toggleHeader(false);
|
toggleHeader(false);
|
||||||
return;
|
return;
|
||||||
|
|
@ -813,7 +843,7 @@ const HomeScreen = () => {
|
||||||
} else if (dy < -6) {
|
} else if (dy < -6) {
|
||||||
toggleHeader(false); // scrolling up
|
toggleHeader(false); // scrolling up
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollAnimationFrameRef.current = null;
|
scrollAnimationFrameRef.current = null;
|
||||||
});
|
});
|
||||||
}, [toggleHeader]);
|
}, [toggleHeader]);
|
||||||
|
|
@ -823,9 +853,9 @@ const HomeScreen = () => {
|
||||||
const contentContainerStyle = useMemo(() => {
|
const contentContainerStyle = useMemo(() => {
|
||||||
const heroStyleToUse = settings.heroStyle;
|
const heroStyleToUse = settings.heroStyle;
|
||||||
const isUsingAppleTVHero = heroStyleToUse === 'appletv' && !isTablet && showHeroSection;
|
const isUsingAppleTVHero = heroStyleToUse === 'appletv' && !isTablet && showHeroSection;
|
||||||
|
|
||||||
return StyleSheet.flatten([
|
return StyleSheet.flatten([
|
||||||
styles.scrollContent,
|
styles.scrollContent,
|
||||||
{ paddingTop: isUsingAppleTVHero ? 0 : stableInsetsTop }
|
{ paddingTop: isUsingAppleTVHero ? 0 : stableInsetsTop }
|
||||||
]);
|
]);
|
||||||
}, [stableInsetsTop, settings.heroStyle, isTablet, showHeroSection]);
|
}, [stableInsetsTop, settings.heroStyle, isTablet, showHeroSection]);
|
||||||
|
|
@ -833,9 +863,9 @@ const HomeScreen = () => {
|
||||||
// Memoize the main content section
|
// Memoize the main content section
|
||||||
const renderMainContent = useMemo(() => {
|
const renderMainContent = useMemo(() => {
|
||||||
if (isLoading) return null;
|
if (isLoading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
barStyle="light-content"
|
barStyle="light-content"
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
|
|
@ -882,13 +912,13 @@ const calculatePosterLayout = (screenWidth: number) => {
|
||||||
const MAX_POSTER_WIDTH = 130; // Reduced maximum for more posters
|
const MAX_POSTER_WIDTH = 130; // Reduced maximum for more posters
|
||||||
const LEFT_PADDING = 16; // Left padding
|
const LEFT_PADDING = 16; // Left padding
|
||||||
const SPACING = 8; // Space between posters
|
const SPACING = 8; // Space between posters
|
||||||
|
|
||||||
// Calculate available width for posters (reserve space for left padding)
|
// Calculate available width for posters (reserve space for left padding)
|
||||||
const availableWidth = screenWidth - LEFT_PADDING;
|
const availableWidth = screenWidth - LEFT_PADDING;
|
||||||
|
|
||||||
// Try different numbers of full posters to find the best fit
|
// Try different numbers of full posters to find the best fit
|
||||||
let bestLayout = { numFullPosters: 3, posterWidth: 120 };
|
let bestLayout = { numFullPosters: 3, posterWidth: 120 };
|
||||||
|
|
||||||
for (let n = 3; n <= 6; n++) {
|
for (let n = 3; n <= 6; n++) {
|
||||||
// Calculate poster width needed for N full posters + 0.25 partial poster
|
// Calculate poster width needed for N full posters + 0.25 partial poster
|
||||||
// Formula: N * posterWidth + (N-1) * spacing + 0.25 * posterWidth = availableWidth - rightPadding
|
// Formula: N * posterWidth + (N-1) * spacing + 0.25 * posterWidth = availableWidth - rightPadding
|
||||||
|
|
@ -896,12 +926,12 @@ const calculatePosterLayout = (screenWidth: number) => {
|
||||||
// We'll use minimal right padding (8px) to maximize space
|
// We'll use minimal right padding (8px) to maximize space
|
||||||
const usableWidth = availableWidth - 8;
|
const usableWidth = availableWidth - 8;
|
||||||
const posterWidth = (usableWidth - (n - 1) * SPACING) / (n + 0.25);
|
const posterWidth = (usableWidth - (n - 1) * SPACING) / (n + 0.25);
|
||||||
|
|
||||||
if (posterWidth >= MIN_POSTER_WIDTH && posterWidth <= MAX_POSTER_WIDTH) {
|
if (posterWidth >= MIN_POSTER_WIDTH && posterWidth <= MAX_POSTER_WIDTH) {
|
||||||
bestLayout = { numFullPosters: n, posterWidth };
|
bestLayout = { numFullPosters: n, posterWidth };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
numFullPosters: bestLayout.numFullPosters,
|
numFullPosters: bestLayout.numFullPosters,
|
||||||
posterWidth: bestLayout.posterWidth,
|
posterWidth: bestLayout.posterWidth,
|
||||||
|
|
@ -966,7 +996,7 @@ const styles = StyleSheet.create<any>({
|
||||||
},
|
},
|
||||||
placeholderPoster: {
|
placeholderPoster: {
|
||||||
width: POSTER_WIDTH,
|
width: POSTER_WIDTH,
|
||||||
aspectRatio: 2/3,
|
aspectRatio: 2 / 3,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
marginRight: 2,
|
marginRight: 2,
|
||||||
},
|
},
|
||||||
|
|
@ -1203,7 +1233,7 @@ const styles = StyleSheet.create<any>({
|
||||||
},
|
},
|
||||||
contentItem: {
|
contentItem: {
|
||||||
width: POSTER_WIDTH,
|
width: POSTER_WIDTH,
|
||||||
aspectRatio: 2/3,
|
aspectRatio: 2 / 3,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue