diff --git a/package-lock.json b/package-lock.json
index 2daad366..d7d345b5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"@react-navigation/native-stack": "^7.3.10",
"@react-navigation/stack": "^7.2.10",
"@sentry/react-native": "^6.15.1",
+ "@shopify/flash-list": "^2.0.2",
"@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20",
"axios": "^1.11.0",
@@ -4309,6 +4310,20 @@
"node": ">=14.18"
}
},
+ "node_modules/@shopify/flash-list": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-2.0.2.tgz",
+ "integrity": "sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "peerDependencies": {
+ "@babel/runtime": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
diff --git a/package.json b/package.json
index bff8c85d..5bda5e4a 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@react-navigation/native-stack": "^7.3.10",
"@react-navigation/stack": "^7.2.10",
"@sentry/react-native": "^6.15.1",
+ "@shopify/flash-list": "^2.0.2",
"@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20",
"axios": "^1.11.0",
diff --git a/src/components/home/CatalogSection.tsx b/src/components/home/CatalogSection.tsx
index b7edc37e..8102e09a 100644
--- a/src/components/home/CatalogSection.tsx
+++ b/src/components/home/CatalogSection.tsx
@@ -109,7 +109,7 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
decelerationRate="fast"
snapToAlignment="start"
ItemSeparatorComponent={() => }
- initialNumToRender={3}
+ initialNumToRender={4}
maxToRenderPerBatch={2}
windowSize={3}
removeClippedSubviews={Platform.OS === 'android'}
@@ -122,7 +122,8 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
maintainVisibleContentPosition={{
minIndexForVisible: 0
}}
- onEndReachedThreshold={1}
+ onEndReachedThreshold={0.5}
+ scrollEventThrottle={16}
/>
);
@@ -177,4 +178,4 @@ const styles = StyleSheet.create({
},
});
-export default React.memo(CatalogSection);
\ No newline at end of file
+export default React.memo(CatalogSection);
\ No newline at end of file
diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx
index 1c7728cd..d65e379f 100644
--- a/src/components/home/ContentItem.tsx
+++ b/src/components/home/ContentItem.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useState, useEffect, useCallback, useRef } from 'react';
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text } from 'react-native';
import { Image as ExpoImage } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons';
@@ -53,6 +53,8 @@ const POSTER_WIDTH = posterLayout.posterWidth;
const ContentItem = React.memo(({ item, onPress }: ContentItemProps) => {
const [menuVisible, setMenuVisible] = useState(false);
const [isWatched, setIsWatched] = useState(false);
+ const [imageLoaded, setImageLoaded] = useState(false);
+ const [imageError, setImageError] = useState(false);
const { currentTheme } = useTheme();
const handleLongPress = useCallback(() => {
@@ -101,12 +103,26 @@ const ContentItem = React.memo(({ item, onPress }: ContentItemProps) => {
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.poster}
contentFit="cover"
- cachePolicy="memory"
+ cachePolicy="memory-disk"
transition={200}
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
placeholderContentFit="cover"
recyclingKey={item.id}
+ onLoad={() => {
+ setImageLoaded(true);
+ setImageError(false);
+ }}
+ onError={() => {
+ setImageError(true);
+ setImageLoaded(false);
+ }}
+ priority="low"
/>
+ {imageError && (
+
+
+
+ )}
{isWatched && (
@@ -191,12 +207,11 @@ const styles = StyleSheet.create({
padding: 4,
},
title: {
- fontSize: 14,
+ fontSize: 13,
fontWeight: '500',
marginTop: 4,
textAlign: 'center',
- fontFamily: 'SpaceMono-Regular',
}
});
-export default ContentItem;
\ No newline at end of file
+export default ContentItem;
\ No newline at end of file
diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx
index 85cc74e8..59c1cd45 100644
--- a/src/components/metadata/HeroSection.tsx
+++ b/src/components/metadata/HeroSection.tsx
@@ -799,7 +799,7 @@ const HeroSection: React.FC = ({
// Check Trakt progress first if available and user is authenticated
if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) {
const traktWatched = watchProgress.traktProgress >= 95;
- logger.log(`[HeroSection] Trakt authenticated: ${isTraktAuthenticated}, Trakt progress: ${watchProgress.traktProgress}%, Watched: ${traktWatched}`);
+ // Removed excessive logging for Trakt progress
return traktWatched;
}
@@ -807,7 +807,7 @@ const HeroSection: React.FC = ({
if (watchProgress.duration === 0) return false;
const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100;
const localWatched = progressPercent >= 85;
- logger.log(`[HeroSection] Local progress: ${progressPercent.toFixed(1)}%, Watched: ${localWatched}`);
+ // Removed excessive logging for local progress
return localWatched;
}, [watchProgress, isTraktAuthenticated]);
diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx
index cac81863..a6e3b812 100644
--- a/src/components/player/AndroidVideoPlayer.tsx
+++ b/src/components/player/AndroidVideoPlayer.tsx
@@ -16,13 +16,13 @@ import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
import { useMetadata } from '../../hooks/useMetadata';
import { useSettings } from '../../hooks/useSettings';
import { testVideoStreamUrl } from '../../utils/httpInterceptor';
+import { stremioService, Subtitle } from '../../services/stremioService';
import {
DEFAULT_SUBTITLE_SIZE,
AudioTrack,
TextTrack,
ResizeModeType,
- WyzieSubtitle,
SubtitleCue,
RESUME_PREF_KEY,
RESUME_PREF,
@@ -149,7 +149,7 @@ const AndroidVideoPlayer: React.FC = () => {
const [subtitleBackground, setSubtitleBackground] = useState(true);
const [useCustomSubtitles, setUseCustomSubtitles] = useState(false);
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState(false);
- const [availableSubtitles, setAvailableSubtitles] = useState([]);
+ const [availableSubtitles, setAvailableSubtitles] = useState([]);
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState(false);
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState(false);
const [showSourcesModal, setShowSourcesModal] = useState(false);
@@ -403,7 +403,7 @@ const AndroidVideoPlayer: React.FC = () => {
saveWatchProgress();
}, syncInterval);
- logger.log(`[AndroidVideoPlayer] Watch progress save interval set to ${syncInterval}ms (immediate sync mode)`);
+ // Removed excessive logging for watch progress save interval
setProgressSaveInterval(interval);
return () => {
@@ -498,16 +498,7 @@ const AndroidVideoPlayer: React.FC = () => {
const onLoad = (data: any) => {
try {
- // Enhanced HTTP response logging
- console.log('\nā
[AndroidVideoPlayer] HTTP RESPONSE SUCCESS:');
- console.log('š URL:', currentStreamUrl);
- console.log('š Status: 200 OK (Video Stream Loaded)');
- console.log('šŗ Duration:', data?.duration ? `${data.duration.toFixed(2)}s` : 'Unknown');
- console.log('š Resolution:', data?.naturalSize ? `${data.naturalSize.width}x${data.naturalSize.height}` : 'Unknown');
- console.log('šµ Audio Tracks:', data?.audioTracks?.length || 0);
- console.log('š Text Tracks:', data?.textTracks?.length || 0);
- console.log('ā° Response Time:', new Date().toISOString());
- console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
+ // HTTP response logging removed
if (DEBUG_MODE) {
logger.log('[AndroidVideoPlayer] Video loaded:', data);
@@ -724,15 +715,7 @@ const AndroidVideoPlayer: React.FC = () => {
const handleError = (error: any) => {
try {
- // Enhanced HTTP error response logging
- console.log('\nā [AndroidVideoPlayer] HTTP RESPONSE ERROR:');
- console.log('š URL:', currentStreamUrl);
- console.log('š Status:', error?.error?.code ? `${error.error.code} (${error.error.domain || 'Unknown Domain'})` : 'Unknown Error Code');
- console.log('š¬ Error Message:', error?.error?.localizedDescription || error?.message || 'Unknown error');
- console.log('š Error Type:', error?.error?.domain || 'Unknown');
- console.log('š Full Error Object:', JSON.stringify(error, null, 2));
- console.log('ā° Error Time:', new Date().toISOString());
- console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
+ // HTTP error response logging removed
logger.error('AndroidVideoPlayer error: ', error);
@@ -908,56 +891,89 @@ const AndroidVideoPlayer: React.FC = () => {
}
setIsLoadingSubtitleList(true);
try {
- let searchUrl = `https://sub.wyzie.ru/search?id=${targetImdbId}&encoding=utf-8&source=all`;
+ // Determine content type and ID format for Stremio
+ let contentType = 'movie';
+ let contentId = targetImdbId;
+ let videoId: string | undefined;
+
if (season && episode) {
- searchUrl += `&season=${season}&episode=${episode}`;
+ contentType = 'series';
+ videoId = `series:${targetImdbId}:${season}:${episode}`;
}
- const response = await fetch(searchUrl);
- const subtitles: WyzieSubtitle[] = await response.json();
- const uniqueSubtitles = subtitles.reduce((acc, current) => {
- const exists = acc.find(item => item.language === current.language);
+
+ logger.log(`[AndroidVideoPlayer] Fetching subtitles for ${contentType}: ${contentId}${videoId ? ` (${videoId})` : ''}`);
+
+ const subtitles = await stremioService.getSubtitles(contentType, contentId, videoId);
+
+ // Remove duplicates based on language
+ const uniqueSubtitles = subtitles.reduce((acc: Subtitle[], current: Subtitle) => {
+ const exists = acc.find((item: Subtitle) => item.lang === current.lang);
if (!exists) {
acc.push(current);
}
return acc;
- }, [] as WyzieSubtitle[]);
- uniqueSubtitles.sort((a, b) => a.display.localeCompare(b.display));
+ }, [] as Subtitle[]);
+
+ // Sort by language
+ uniqueSubtitles.sort((a: Subtitle, b: Subtitle) => a.lang.localeCompare(b.lang));
setAvailableSubtitles(uniqueSubtitles);
+
if (autoSelectEnglish) {
- const englishSubtitle = uniqueSubtitles.find(sub =>
- sub.language.toLowerCase() === 'eng' ||
- sub.language.toLowerCase() === 'en' ||
- sub.display.toLowerCase().includes('english')
+ const englishSubtitle = uniqueSubtitles.find((sub: Subtitle) =>
+ sub.lang.toLowerCase() === 'eng' ||
+ sub.lang.toLowerCase() === 'en' ||
+ sub.lang.toLowerCase().includes('english')
);
if (englishSubtitle) {
- loadWyzieSubtitle(englishSubtitle);
+ loadStremioSubtitle(englishSubtitle);
return;
}
}
- if (!autoSelectEnglish) {
+
+ if (!autoSelectEnglish && uniqueSubtitles.length > 0) {
setShowSubtitleLanguageModal(true);
}
} catch (error) {
- logger.error('[AndroidVideoPlayer] Error fetching subtitles from Wyzie API:', error);
+ logger.error('[AndroidVideoPlayer] Error fetching subtitles from Stremio addons:', error);
} finally {
setIsLoadingSubtitleList(false);
}
};
- const loadWyzieSubtitle = async (subtitle: WyzieSubtitle) => {
+ const loadStremioSubtitle = async (subtitle: Subtitle) => {
+ console.log('[AndroidVideoPlayer] Starting subtitle load, setting isLoadingSubtitles to true');
setShowSubtitleLanguageModal(false);
setIsLoadingSubtitles(true);
try {
const response = await fetch(subtitle.url);
const srtContent = await response.text();
const parsedCues = parseSRT(srtContent);
+
+ // Force a microtask delay before updating subtitle state
+ await new Promise(resolve => setTimeout(resolve, 50));
+
setCustomSubtitles(parsedCues);
setUseCustomSubtitles(true);
setSelectedTextTrack(-1);
+ logger.log(`[AndroidVideoPlayer] Loaded subtitle: ${subtitle.lang} from ${subtitle.addonName}`);
+ console.log('[AndroidVideoPlayer] Subtitle loaded successfully');
+
+ // Force a state update by triggering a seek
+ if (videoRef.current && duration > 0) {
+ const currentPos = currentTime;
+ console.log('[AndroidVideoPlayer] Forcing a micro-seek to refresh subtitle state');
+ videoRef.current.seek(currentPos);
+ }
} catch (error) {
- logger.error('[AndroidVideoPlayer] Error loading Wyzie subtitle:', error);
+ logger.error('[AndroidVideoPlayer] Error loading Stremio subtitle:', error);
+ console.log('[AndroidVideoPlayer] Subtitle loading failed:', error);
} finally {
- setIsLoadingSubtitles(false);
+ console.log('[AndroidVideoPlayer] Setting isLoadingSubtitles to false');
+ // Add a small delay to ensure state updates are processed
+ setTimeout(() => {
+ setIsLoadingSubtitles(false);
+ console.log('[AndroidVideoPlayer] isLoadingSubtitles set to false');
+ }, 100);
}
};
@@ -1004,8 +1020,10 @@ const AndroidVideoPlayer: React.FC = () => {
currentTime >= cue.start && currentTime <= cue.end
);
const newSubtitle = currentCue ? currentCue.text : '';
- setCurrentSubtitle(newSubtitle);
- }, [currentTime, customSubtitles, useCustomSubtitles]);
+ if (newSubtitle !== currentSubtitle) {
+ setCurrentSubtitle(newSubtitle);
+ }
+ }, [currentTime, customSubtitles, useCustomSubtitles, currentSubtitle]);
useEffect(() => {
loadSubtitleSize();
@@ -1073,11 +1091,10 @@ const AndroidVideoPlayer: React.FC = () => {
if (currentStreamUrl && currentStreamUrl.trim() !== '') {
const testStream = async () => {
try {
- console.log('\nš [AndroidVideoPlayer] Testing video stream URL...');
- const isValid = await testVideoStreamUrl(currentStreamUrl, headers || {});
- console.log(`ā
[AndroidVideoPlayer] Stream test result: ${isValid ? 'VALID' : 'INVALID'}`);
+ // Stream testing without verbose logging
+ await testVideoStreamUrl(currentStreamUrl, headers || {});
} catch (error) {
- console.log('ā [AndroidVideoPlayer] Stream test failed:', error);
+ // Stream test failed silently
}
};
@@ -1287,16 +1304,7 @@ const AndroidVideoPlayer: React.FC = () => {
headers: headers
} : { uri: currentStreamUrl };
- // Enhanced HTTP request logging
- console.log('\nš [AndroidVideoPlayer] HTTP REQUEST DETAILS:');
- console.log('š URL:', currentStreamUrl);
- console.log('š§ Method: GET (Video Stream)');
- console.log('š Headers:', headers ? JSON.stringify(headers, null, 2) : 'No headers');
- console.log('š¬ Stream Provider:', currentStreamProvider || 'Unknown');
- console.log('šŗ Stream Name:', currentStreamName || 'Unknown');
- console.log('šÆ Quality:', currentQuality || 'Unknown');
- console.log('ā° Timestamp:', new Date().toISOString());
- console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
+ // HTTP request logging removed
return sourceWithHeaders;
})()}
@@ -1399,7 +1407,7 @@ const AndroidVideoPlayer: React.FC = () => {
subtitleSize={subtitleSize}
subtitleBackground={subtitleBackground}
fetchAvailableSubtitles={fetchAvailableSubtitles}
- loadWyzieSubtitle={loadWyzieSubtitle}
+ loadStremioSubtitle={loadStremioSubtitle}
selectTextTrack={selectTextTrack}
increaseSubtitleSize={increaseSubtitleSize}
decreaseSubtitleSize={decreaseSubtitleSize}
diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx
index c3744db8..70a44fb5 100644
--- a/src/components/player/VideoPlayer.tsx
+++ b/src/components/player/VideoPlayer.tsx
@@ -17,13 +17,13 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync';
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
import { useMetadata } from '../../hooks/useMetadata';
import { useSettings } from '../../hooks/useSettings';
+import { stremioService, Subtitle } from '../../services/stremioService';
import {
DEFAULT_SUBTITLE_SIZE,
AudioTrack,
TextTrack,
ResizeModeType,
- WyzieSubtitle,
SubtitleCue,
RESUME_PREF_KEY,
RESUME_PREF,
@@ -160,7 +160,7 @@ const VideoPlayer: React.FC = () => {
const [subtitleBackground, setSubtitleBackground] = useState(true);
const [useCustomSubtitles, setUseCustomSubtitles] = useState(false);
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState(false);
- const [availableSubtitles, setAvailableSubtitles] = useState([]);
+ const [availableSubtitles, setAvailableSubtitles] = useState([]);
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState(false);
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState(false);
const [showSourcesModal, setShowSourcesModal] = useState(false);
@@ -434,7 +434,7 @@ const VideoPlayer: React.FC = () => {
saveWatchProgress();
}, syncInterval);
- logger.log(`[VideoPlayer] Watch progress save interval set to ${syncInterval}ms (immediate sync mode)`);
+ // Removed excessive logging for watch progress save interval
setProgressSaveInterval(interval);
return () => {
@@ -797,15 +797,7 @@ const VideoPlayer: React.FC = () => {
};
const handleError = (error: any) => {
- // Enhanced HTTP error response logging
- console.log('\nā [VideoPlayer] HTTP RESPONSE ERROR:');
- console.log('š URL:', currentStreamUrl);
- console.log('š Status:', error?.error?.code ? `${error.error.code} (${error.error.domain || 'Unknown Domain'})` : 'Unknown Error Code');
- console.log('š¬ Error Message:', error?.error?.localizedDescription || error?.message || 'Unknown error');
- console.log('š Error Type:', error?.error?.domain || 'Unknown');
- console.log('š Full Error Object:', JSON.stringify(error, null, 2));
- console.log('ā° Error Time:', new Date().toISOString());
- console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
+ // HTTP error response logging removed
logger.error('[VideoPlayer] Playback Error:', error);
@@ -915,43 +907,56 @@ const VideoPlayer: React.FC = () => {
}
setIsLoadingSubtitleList(true);
try {
- let searchUrl = `https://sub.wyzie.ru/search?id=${targetImdbId}&encoding=utf-8&source=all`;
+ // Determine content type and ID format for Stremio
+ let contentType = 'movie';
+ let contentId = targetImdbId;
+ let videoId: string | undefined;
+
if (season && episode) {
- searchUrl += `&season=${season}&episode=${episode}`;
+ contentType = 'series';
+ videoId = `series:${targetImdbId}:${season}:${episode}`;
}
- const response = await fetch(searchUrl);
- const subtitles: WyzieSubtitle[] = await response.json();
- const uniqueSubtitles = subtitles.reduce((acc, current) => {
- const exists = acc.find(item => item.language === current.language);
+
+ logger.log(`[VideoPlayer] Fetching subtitles for ${contentType}: ${contentId}${videoId ? ` (${videoId})` : ''}`);
+
+ const subtitles = await stremioService.getSubtitles(contentType, contentId, videoId);
+
+ // Remove duplicates based on language
+ const uniqueSubtitles = subtitles.reduce((acc: Subtitle[], current: Subtitle) => {
+ const exists = acc.find((item: Subtitle) => item.lang === current.lang);
if (!exists) {
acc.push(current);
}
return acc;
- }, [] as WyzieSubtitle[]);
- uniqueSubtitles.sort((a, b) => a.display.localeCompare(b.display));
+ }, [] as Subtitle[]);
+
+ // Sort by language
+ uniqueSubtitles.sort((a: Subtitle, b: Subtitle) => a.lang.localeCompare(b.lang));
setAvailableSubtitles(uniqueSubtitles);
+
if (autoSelectEnglish) {
- const englishSubtitle = uniqueSubtitles.find(sub =>
- sub.language.toLowerCase() === 'eng' ||
- sub.language.toLowerCase() === 'en' ||
- sub.display.toLowerCase().includes('english')
+ const englishSubtitle = uniqueSubtitles.find((sub: Subtitle) =>
+ sub.lang.toLowerCase() === 'eng' ||
+ sub.lang.toLowerCase() === 'en' ||
+ sub.lang.toLowerCase().includes('english')
);
if (englishSubtitle) {
- loadWyzieSubtitle(englishSubtitle);
+ loadStremioSubtitle(englishSubtitle);
return;
}
}
- if (!autoSelectEnglish) {
+
+ if (!autoSelectEnglish && uniqueSubtitles.length > 0) {
setShowSubtitleLanguageModal(true);
}
} catch (error) {
- logger.error('[VideoPlayer] Error fetching subtitles from Wyzie API:', error);
+ logger.error('[VideoPlayer] Error fetching subtitles from Stremio addons:', error);
} finally {
setIsLoadingSubtitleList(false);
}
};
- const loadWyzieSubtitle = async (subtitle: WyzieSubtitle) => {
+ const loadStremioSubtitle = async (subtitle: Subtitle) => {
setShowSubtitleLanguageModal(false);
setIsLoadingSubtitles(true);
try {
@@ -961,10 +966,14 @@ const VideoPlayer: React.FC = () => {
setCustomSubtitles(parsedCues);
setUseCustomSubtitles(true);
setSelectedTextTrack(-1);
+ logger.log(`[VideoPlayer] Loaded subtitle: ${subtitle.lang} from ${subtitle.addonName}`);
} catch (error) {
- logger.error('[VideoPlayer] Error loading Wyzie subtitle:', error);
+ logger.error('[VideoPlayer] Error loading Stremio subtitle:', error);
} finally {
- setIsLoadingSubtitles(false);
+ // Add a small delay to ensure state updates are processed
+ setTimeout(() => {
+ setIsLoadingSubtitles(false);
+ }, 100);
}
};
@@ -1004,8 +1013,10 @@ const VideoPlayer: React.FC = () => {
currentTime >= cue.start && currentTime <= cue.end
);
const newSubtitle = currentCue ? currentCue.text : '';
- setCurrentSubtitle(newSubtitle);
- }, [currentTime, customSubtitles, useCustomSubtitles]);
+ if (newSubtitle !== currentSubtitle) {
+ setCurrentSubtitle(newSubtitle);
+ }
+ }, [currentTime, customSubtitles, useCustomSubtitles, currentSubtitle]);
useEffect(() => {
loadSubtitleSize();
@@ -1281,16 +1292,7 @@ const VideoPlayer: React.FC = () => {
headers: headers
} : { uri: currentStreamUrl };
- // Enhanced HTTP request logging
- console.log('\nš [VideoPlayer] HTTP REQUEST DETAILS:');
- console.log('š URL:', currentStreamUrl);
- console.log('š§ Method: GET (Video Stream)');
- console.log('š Headers:', headers ? JSON.stringify(headers, null, 2) : 'No headers');
- console.log('š¬ Stream Provider:', currentStreamProvider || streamProvider || 'Unknown');
- console.log('šŗ Stream Name:', currentStreamName || streamName || 'Unknown');
- console.log('šÆ Quality:', currentQuality || quality || 'Unknown');
- console.log('ā° Timestamp:', new Date().toISOString());
- console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
+ // HTTP request logging removed
return sourceWithHeaders;
})()}
@@ -1386,7 +1388,7 @@ const VideoPlayer: React.FC = () => {
subtitleSize={subtitleSize}
subtitleBackground={subtitleBackground}
fetchAvailableSubtitles={fetchAvailableSubtitles}
- loadWyzieSubtitle={loadWyzieSubtitle}
+ loadStremioSubtitle={loadStremioSubtitle}
selectTextTrack={selectTextTrack}
increaseSubtitleSize={increaseSubtitleSize}
decreaseSubtitleSize={decreaseSubtitleSize}
diff --git a/src/components/player/modals/ResumeOverlay.tsx b/src/components/player/modals/ResumeOverlay.tsx
index 724a57fe..a5ff1837 100644
--- a/src/components/player/modals/ResumeOverlay.tsx
+++ b/src/components/player/modals/ResumeOverlay.tsx
@@ -28,15 +28,15 @@ export const ResumeOverlay: React.FC = ({
handleStartFromBeginning,
}) => {
useEffect(() => {
- logger.log(`[ResumeOverlay] Props changed: showOverlay=${showResumeOverlay}, resumePosition=${resumePosition}, duration=${duration}, title=${title}`);
+ // Removed excessive logging for props changes
}, [showResumeOverlay, resumePosition, duration, title]);
if (!showResumeOverlay || resumePosition === null) {
- logger.log(`[ResumeOverlay] Not showing overlay: showOverlay=${showResumeOverlay}, resumePosition=${resumePosition}`);
+ // Removed excessive logging for overlay visibility
return null;
}
- logger.log(`[ResumeOverlay] Rendering overlay for ${title} at ${resumePosition}s`);
+ // Removed excessive logging for overlay rendering
return (
@@ -91,4 +91,4 @@ export const ResumeOverlay: React.FC = ({
);
};
-export default ResumeOverlay;
\ No newline at end of file
+export default ResumeOverlay;
\ No newline at end of file
diff --git a/src/components/player/modals/SubtitleModals.tsx b/src/components/player/modals/SubtitleModals.tsx
index c2aa2ba9..5ecc5e3e 100644
--- a/src/components/player/modals/SubtitleModals.tsx
+++ b/src/components/player/modals/SubtitleModals.tsx
@@ -8,7 +8,8 @@ import Animated, {
SlideOutRight,
} from 'react-native-reanimated';
import { styles } from '../utils/playerStyles';
-import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
+import { SubtitleCue } from '../utils/playerTypes';
+import { Subtitle } from '../../../services/stremioService';
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
interface SubtitleModalsProps {
@@ -19,14 +20,14 @@ interface SubtitleModalsProps {
isLoadingSubtitleList: boolean;
isLoadingSubtitles: boolean;
customSubtitles: SubtitleCue[];
- availableSubtitles: WyzieSubtitle[];
+ availableSubtitles: Subtitle[];
vlcTextTracks: Array<{id: number, name: string, language?: string}>;
selectedTextTrack: number;
useCustomSubtitles: boolean;
subtitleSize: number;
subtitleBackground: boolean;
fetchAvailableSubtitles: () => void;
- loadWyzieSubtitle: (subtitle: WyzieSubtitle) => void;
+ loadStremioSubtitle: (subtitle: Subtitle) => void;
selectTextTrack: (trackId: number) => void;
increaseSubtitleSize: () => void;
decreaseSubtitleSize: () => void;
@@ -51,7 +52,7 @@ export const SubtitleModals: React.FC = ({
subtitleSize,
subtitleBackground,
fetchAvailableSubtitles,
- loadWyzieSubtitle,
+ loadStremioSubtitle,
selectTextTrack,
increaseSubtitleSize,
decreaseSubtitleSize,
@@ -59,6 +60,8 @@ export const SubtitleModals: React.FC = ({
}) => {
// Track which specific online subtitle is currently loaded
const [selectedOnlineSubtitleId, setSelectedOnlineSubtitleId] = React.useState(null);
+ // Track which subtitle is currently being loaded
+ const [loadingSubtitleId, setLoadingSubtitleId] = React.useState(null);
React.useEffect(() => {
if (showSubtitleModal && !isLoadingSubtitleList && availableSubtitles.length === 0) {
@@ -77,11 +80,26 @@ export const SubtitleModals: React.FC = ({
setShowSubtitleModal(false);
};
- const handleLoadWyzieSubtitle = (subtitle: WyzieSubtitle) => {
+ const handleLoadStremioSubtitle = (subtitle: Subtitle) => {
+ console.log('[SubtitleModals] Starting to load subtitle:', subtitle.id);
+ setLoadingSubtitleId(subtitle.id);
setSelectedOnlineSubtitleId(subtitle.id);
- loadWyzieSubtitle(subtitle);
+ loadStremioSubtitle(subtitle);
};
+ // Clear loading state when subtitle loading is complete
+ React.useEffect(() => {
+ console.log('[SubtitleModals] isLoadingSubtitles changed:', isLoadingSubtitles);
+ if (!isLoadingSubtitles) {
+ console.log('[SubtitleModals] Clearing loadingSubtitleId');
+ // Force clear loading state with a small delay to ensure proper re-render
+ setTimeout(() => {
+ setLoadingSubtitleId(null);
+ console.log('[SubtitleModals] loadingSubtitleId cleared');
+ }, 50);
+ }
+ }, [isLoadingSubtitles]);
+
// Main subtitle menu
const renderSubtitleMenu = () => {
if (!showSubtitleModal) return null;
@@ -396,10 +414,10 @@ export const SubtitleModals: React.FC = ({
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
}}
onPress={() => {
- handleLoadWyzieSubtitle(sub);
+ handleLoadStremioSubtitle(sub);
}}
activeOpacity={0.7}
- disabled={isLoadingSubtitles}
+ disabled={loadingSubtitleId === sub.id}
>
@@ -409,16 +427,16 @@ export const SubtitleModals: React.FC = ({
fontWeight: '500',
marginBottom: 4,
}}>
- {sub.display}
+ {formatLanguage(sub.lang)}
- {formatLanguage(sub.language)}
+ {sub.addonName || 'Stremio Addon'}
- {isLoadingSubtitles ? (
+ {loadingSubtitleId === sub.id ? (
) : isSelected ? (
@@ -539,4 +557,4 @@ export const SubtitleModals: React.FC = ({
);
};
-export default SubtitleModals;
\ No newline at end of file
+export default SubtitleModals;
\ No newline at end of file
diff --git a/src/components/player/utils/playerTypes.ts b/src/components/player/utils/playerTypes.ts
index 3f2c5d82..1ee195a6 100644
--- a/src/components/player/utils/playerTypes.ts
+++ b/src/components/player/utils/playerTypes.ts
@@ -71,18 +71,4 @@ export interface SubtitleCue {
start: number;
end: number;
text: string;
-}
-
-// Add interface for Wyzie subtitle API response
-export interface WyzieSubtitle {
- id: string;
- url: string;
- flagUrl: string;
- format: string;
- encoding: string;
- media: string;
- display: string;
- language: string;
- isHearingImpaired: boolean;
- source: string;
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/hooks/useTraktAutosync.ts b/src/hooks/useTraktAutosync.ts
index 8c14ea90..013a6d07 100644
--- a/src/hooks/useTraktAutosync.ts
+++ b/src/hooks/useTraktAutosync.ts
@@ -190,7 +190,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
currentTime
);
- logger.log(`[TraktAutosync] Synced progress ${progressPercent.toFixed(1)}%: ${contentData.title}`);
+ // Progress sync logging removed
}
} catch (error) {
logger.error('[TraktAutosync] Error syncing progress:', error);
@@ -201,7 +201,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
const handlePlaybackEnd = useCallback(async (currentTime: number, duration: number, reason: 'ended' | 'unmount' = 'ended') => {
const now = Date.now();
- logger.log(`[TraktAutosync] handlePlaybackEnd called: reason=${reason}, time=${currentTime}, duration=${duration}, authenticated=${isAuthenticated}, enabled=${autosyncSettings.enabled}, started=${hasStartedWatching.current}, stopped=${hasStopped.current}, complete=${isSessionComplete.current}, session=${sessionKey.current}, unmountCount=${unmountCount.current}`);
+ // Removed excessive logging for handlePlaybackEnd calls
if (!isAuthenticated || !autosyncSettings.enabled) {
logger.log(`[TraktAutosync] Skipping handlePlaybackEnd: authenticated=${isAuthenticated}, enabled=${autosyncSettings.enabled}`);
@@ -227,7 +227,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
hasStopped.current = false;
isSignificantUpdate = true;
} else {
- logger.log(`[TraktAutosync] Already stopped this session, skipping duplicate call (reason: ${reason})`);
+ // Already stopped this session, skipping duplicate call
return;
}
}
@@ -247,7 +247,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
try {
let progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
- logger.log(`[TraktAutosync] Initial progress calculation: ${progressPercent.toFixed(1)}%`);
+ // Initial progress calculation logging removed
// For unmount calls, always use the highest available progress
// Check current progress, last synced progress, and local storage progress
@@ -278,28 +278,26 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
}
if (maxProgress !== progressPercent) {
- logger.log(`[TraktAutosync] Using highest available progress for unmount: ${maxProgress.toFixed(1)}% (current: ${progressPercent.toFixed(1)}%, last synced: ${lastSyncProgress.current.toFixed(1)}%)`);
+ // Highest progress logging removed
progressPercent = maxProgress;
} else {
- logger.log(`[TraktAutosync] Current progress is already highest: ${progressPercent.toFixed(1)}%`);
+ // Current progress logging removed
}
}
// If we have valid progress but no started session, force start one first
if (!hasStartedWatching.current && progressPercent > 1) {
- logger.log(`[TraktAutosync] Force starting session for progress: ${progressPercent.toFixed(1)}%`);
const contentData = buildContentData();
const success = await startWatching(contentData, progressPercent);
if (success) {
hasStartedWatching.current = true;
- logger.log(`[TraktAutosync] Force started watching: ${contentData.title}`);
}
}
// Only stop if we have meaningful progress (>= 0.5%) or it's a natural video end
// Lower threshold for unmount calls to catch more edge cases
if (reason === 'unmount' && progressPercent < 0.5) {
- logger.log(`[TraktAutosync] Skipping unmount stop for ${options.title} - too early (${progressPercent.toFixed(1)}%)`);
+ // Early unmount stop logging removed
return;
}
diff --git a/src/hooks/useTraktIntegration.ts b/src/hooks/useTraktIntegration.ts
index 1e818a0e..ccd62647 100644
--- a/src/hooks/useTraktIntegration.ts
+++ b/src/hooks/useTraktIntegration.ts
@@ -243,7 +243,7 @@ export function useTraktIntegration() {
// Get playback progress from Trakt
const getTraktPlaybackProgress = useCallback(async (type?: 'movies' | 'shows'): Promise => {
- logger.log(`[useTraktIntegration] getTraktPlaybackProgress called - isAuthenticated: ${isAuthenticated}, type: ${type || 'all'}`);
+ // getTraktPlaybackProgress call logging removed
if (!isAuthenticated) {
logger.log('[useTraktIntegration] getTraktPlaybackProgress: Not authenticated');
@@ -251,9 +251,9 @@ export function useTraktIntegration() {
}
try {
- logger.log('[useTraktIntegration] Calling traktService.getPlaybackProgress...');
+ // traktService.getPlaybackProgress call logging removed
const result = await traktService.getPlaybackProgress(type);
- logger.log(`[useTraktIntegration] traktService.getPlaybackProgress returned ${result.length} items`);
+ // Playback progress logging removed
return result;
} catch (error) {
logger.error('[useTraktIntegration] Error getting playback progress:', error);
@@ -335,7 +335,7 @@ export function useTraktIntegration() {
traktService.getWatchedMovies()
]);
- logger.log(`[useTraktIntegration] Retrieved ${traktProgress.length} progress items, ${watchedMovies.length} watched movies`);
+ // Progress retrieval logging removed
// Batch process all updates to reduce storage notifications
const updatePromises: Promise[] = [];
@@ -406,7 +406,7 @@ export function useTraktIntegration() {
// Execute all updates in parallel
await Promise.all(updatePromises);
- logger.log(`[useTraktIntegration] Successfully merged ${updatePromises.length} items from Trakt`);
+ // Trakt merge logging removed
return true;
} catch (error) {
logger.error('[useTraktIntegration] Error fetching and merging Trakt progress:', error);
@@ -431,9 +431,7 @@ export function useTraktIntegration() {
if (isAuthenticated) {
// Fetch Trakt progress and merge with local
fetchAndMergeTraktProgress().then((success) => {
- if (success) {
- logger.log('[useTraktIntegration] Trakt progress merged successfully');
- }
+ // Trakt progress merge success logging removed
});
}
}, [isAuthenticated, fetchAndMergeTraktProgress]);
@@ -503,4 +501,4 @@ export function useTraktIntegration() {
fetchAndMergeTraktProgress,
forceSyncTraktProgress // For manual testing
};
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/hooks/useWatchProgress.ts b/src/hooks/useWatchProgress.ts
index 63427bc3..7e0f5a05 100644
--- a/src/hooks/useWatchProgress.ts
+++ b/src/hooks/useWatchProgress.ts
@@ -114,7 +114,7 @@ export const useWatchProgress = (
const mostRecentProgress = sortedProgresses[0];
const progress = mostRecentProgress.progress;
- logger.log(`[useWatchProgress] Using most recent progress for ${mostRecentProgress.episodeId}, updated at ${new Date(progress.lastUpdated).toLocaleString()}`);
+ // Removed excessive logging for most recent progress
setWatchProgress({
...progress,
@@ -204,4 +204,4 @@ export const useWatchProgress = (
getPlayButtonText,
loadWatchProgress
};
-};
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx
index 2048dd59..7e4bef87 100644
--- a/src/screens/HomeScreen.tsx
+++ b/src/screens/HomeScreen.tsx
@@ -85,7 +85,8 @@ type HomeScreenListItem =
| { type: 'continueWatching'; key: string }
| { type: 'catalog'; catalog: CatalogContent; key: string }
| { type: 'placeholder'; key: string }
- | { type: 'welcome'; key: string };
+ | { type: 'welcome'; key: string }
+ | { type: 'loadMore'; key: string };
// Sample categories (real app would get these from API)
const SAMPLE_CATEGORIES: Category[] = [
@@ -122,6 +123,7 @@ const HomeScreen = () => {
const [loadedCatalogCount, setLoadedCatalogCount] = useState(0);
const [hasAddons, setHasAddons] = useState(null);
const totalCatalogsRef = useRef(0);
+ const [visibleCatalogCount, setVisibleCatalogCount] = useState(8); // Moderate number of visible catalogs
const {
featuredContent,
@@ -131,7 +133,7 @@ const HomeScreen = () => {
refreshFeatured
} = useFeaturedContent();
- // Progressive catalog loading function
+ // Progressive catalog loading function with performance optimizations
const loadCatalogsProgressively = useCallback(async () => {
setCatalogsLoading(true);
setCatalogs([]);
@@ -155,6 +157,24 @@ const HomeScreen = () => {
const catalogPromises: Promise[] = [];
let catalogIndex = 0;
+ // Limit concurrent catalog loading to prevent overwhelming the system
+ const MAX_CONCURRENT_CATALOGS = 5;
+ let activeCatalogLoads = 0;
+ const catalogQueue: (() => Promise)[] = [];
+
+ const processCatalogQueue = async () => {
+ while (catalogQueue.length > 0 && activeCatalogLoads < MAX_CONCURRENT_CATALOGS) {
+ const catalogLoader = catalogQueue.shift();
+ if (catalogLoader) {
+ activeCatalogLoads++;
+ catalogLoader().finally(() => {
+ activeCatalogLoads--;
+ processCatalogQueue(); // Process next in queue
+ });
+ }
+ }
+ };
+
for (const addon of addons) {
if (addon.catalogs) {
for (const catalog of addon.catalogs) {
@@ -167,29 +187,29 @@ const HomeScreen = () => {
const currentIndex = catalogIndex;
catalogPlaceholders.push(null); // Reserve position
- const catalogPromise = (async () => {
+ const catalogLoader = async () => {
try {
const manifest = addonManifests.find((a: any) => a.id === addon.id);
if (!manifest) return;
const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
if (metas && metas.length > 0) {
- const items = metas.map((meta: any) => ({
+ // Limit items per catalog to reduce memory usage
+ const limitedMetas = metas.slice(0, 20); // Moderate limit for better content variety
+
+ const items = limitedMetas.map((meta: any) => ({
id: meta.id,
type: meta.type,
name: meta.name,
poster: meta.poster,
posterShape: meta.posterShape,
- banner: meta.background,
- logo: meta.logo,
+ // Remove banner and logo to reduce memory usage
imdbRating: meta.imdbRating,
year: meta.year,
genres: meta.genres,
description: meta.description,
runtime: meta.runtime,
released: meta.released,
- trailerStreams: meta.trailerStreams,
- videos: meta.videos,
directors: meta.director,
creators: meta.creator,
certification: meta.certification
@@ -221,9 +241,9 @@ const HomeScreen = () => {
} finally {
setLoadedCatalogCount(prev => prev + 1);
}
- })();
+ };
- catalogPromises.push(catalogPromise);
+ catalogQueue.push(catalogLoader);
catalogIndex++;
}
}
@@ -235,11 +255,28 @@ const HomeScreen = () => {
// Initialize catalogs array with proper length
setCatalogs(new Array(catalogIndex).fill(null));
- // Start all catalog loading promises but don't wait for them
- // They will update the state progressively as they complete
- await Promise.allSettled(catalogPromises);
+ // Start processing the catalog queue
+ processCatalogQueue();
- // Only set catalogsLoading to false after all promises have settled
+ // Wait for all catalogs to load with a timeout
+ const checkAllLoaded = () => {
+ return new Promise((resolve) => {
+ const interval = setInterval(() => {
+ if (loadedCatalogCount >= catalogIndex || catalogQueue.length === 0) {
+ clearInterval(interval);
+ resolve();
+ }
+ }, 100);
+
+ // Timeout after 30 seconds
+ setTimeout(() => {
+ clearInterval(interval);
+ resolve();
+ }, 30000);
+ });
+ };
+
+ await checkAllLoaded();
setCatalogsLoading(false);
} catch (error) {
console.error('[HomeScreen] Error in progressive catalog loading:', error);
@@ -322,48 +359,67 @@ const HomeScreen = () => {
if (refreshTimeoutRef.current) {
clearTimeout(refreshTimeoutRef.current);
}
+
+ // Clear image cache when component unmounts to free memory
+ try {
+ ExpoImage.clearMemoryCache();
+ } catch (error) {
+ console.warn('Failed to clear image cache:', error);
+ }
};
}, [currentTheme.colors.darkBackground]);
- // Optimized preload images function with better memory management
+ // Periodic memory cleanup when many catalogs are loaded
+ useEffect(() => {
+ if (catalogs.filter(c => c).length > 15) {
+ const cleanup = setTimeout(() => {
+ try {
+ ExpoImage.clearMemoryCache();
+ } catch (error) {
+ console.warn('Failed to clear image cache:', error);
+ }
+ }, 60000); // Clean every 60 seconds when many catalogs are loaded
+
+ return () => clearTimeout(cleanup);
+ }
+ }, [catalogs]);
+
+ // Balanced preload images function
const preloadImages = useCallback(async (content: StreamingContent[]) => {
if (!content.length) return;
try {
- // Significantly reduced concurrent prefetching to prevent heating
- const BATCH_SIZE = 2; // Reduced from 3 to 2
- const MAX_IMAGES = 5; // Reduced from 10 to 5
+ // Moderate prefetching for better performance balance
+ const MAX_IMAGES = 6; // Preload 6 most important images
- // Only preload the most important images (poster and banner, skip logo)
- const allImages = content.slice(0, MAX_IMAGES)
- .map(item => [item.poster, item.banner])
- .flat()
+ // Only preload poster images (skip banner and logo entirely)
+ const posterImages = content.slice(0, MAX_IMAGES)
+ .map(item => item.poster)
.filter(Boolean) as string[];
- // Process in smaller batches with longer delays
- for (let i = 0; i < allImages.length; i += BATCH_SIZE) {
- const batch = allImages.slice(i, i + BATCH_SIZE);
+ // Process in batches of 2 with moderate delays
+ for (let i = 0; i < posterImages.length; i += 2) {
+ const batch = posterImages.slice(i, i + 2);
- try {
- await Promise.all(
- batch.map(async (imageUrl) => {
- try {
- // Use our cache service instead of direct prefetch
- await imageCacheService.getCachedImageUrl(imageUrl);
- // Increased delay between prefetches to reduce CPU load
- await new Promise(resolve => setTimeout(resolve, 100));
- } catch (error) {
- // Silently handle individual prefetch errors
- }
- })
- );
-
- // Longer delay between batches to allow GC and reduce heating
- if (i + BATCH_SIZE < allImages.length) {
- await new Promise(resolve => setTimeout(resolve, 200));
+ await Promise.all(batch.map(async (imageUrl) => {
+ try {
+ // Use our cache service with timeout
+ const timeoutPromise = new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Timeout')), 2000)
+ );
+
+ await Promise.race([
+ imageCacheService.getCachedImageUrl(imageUrl),
+ timeoutPromise
+ ]);
+ } catch (error) {
+ // Skip failed images and continue
}
- } catch (error) {
- // Continue with next batch if current batch fails
+ }));
+
+ // Moderate delay between batches
+ if (i + 2 < posterImages.length) {
+ await new Promise(resolve => setTimeout(resolve, 200));
}
}
} catch (error) {
@@ -490,7 +546,10 @@ const HomeScreen = () => {
data.push({ type: 'thisWeek', key: 'thisWeek' });
data.push({ type: 'continueWatching', key: 'continueWatching' });
- catalogs.forEach((catalog, index) => {
+ // Only show a limited number of catalogs initially for performance
+ const catalogsToShow = catalogs.slice(0, visibleCatalogCount);
+
+ catalogsToShow.forEach((catalog, index) => {
if (catalog) {
data.push({ type: 'catalog', catalog, key: `${catalog.addon}-${catalog.id}-${index}` });
} else {
@@ -499,8 +558,17 @@ const HomeScreen = () => {
}
});
+ // Add a "Load More" button if there are more catalogs to show
+ if (catalogs.length > visibleCatalogCount && catalogs.filter(c => c).length > visibleCatalogCount) {
+ data.push({ type: 'loadMore', key: 'load-more' } as any);
+ }
+
return data;
- }, [hasAddons, showHeroSection, catalogs]);
+ }, [hasAddons, showHeroSection, catalogs, visibleCatalogCount]);
+
+ const handleLoadMoreCatalogs = useCallback(() => {
+ setVisibleCatalogCount(prev => Math.min(prev + 5, catalogs.length));
+ }, [catalogs.length]);
const renderListItem = useCallback(({ item }: { item: HomeScreenListItem }) => {
switch (item.type) {
@@ -535,7 +603,7 @@ const HomeScreen = () => {
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.placeholderPosters}>
- {[...Array(5)].map((_, posterIndex) => (
+ {[...Array(3)].map((_, posterIndex) => (
{
);
+ case 'loadMore':
+ return (
+
+
+
+
+
+ Load More Catalogs
+
+
+
+
+ );
case 'welcome':
return ;
default:
@@ -556,7 +640,8 @@ const HomeScreen = () => {
featuredContent,
isSaved,
handleSaveToLibrary,
- currentTheme.colors
+ currentTheme.colors,
+ handleLoadMoreCatalogs
]);
const ListFooterComponent = useMemo(() => (
@@ -608,9 +693,9 @@ const HomeScreen = () => {
]}
showsVerticalScrollIndicator={false}
ListFooterComponent={ListFooterComponent}
- initialNumToRender={5}
- maxToRenderPerBatch={5}
- windowSize={11}
+ initialNumToRender={4}
+ maxToRenderPerBatch={3}
+ windowSize={7}
removeClippedSubviews={Platform.OS === 'android'}
onEndReachedThreshold={0.5}
updateCellsBatchingPeriod={50}
@@ -618,11 +703,8 @@ const HomeScreen = () => {
minIndexForVisible: 0,
autoscrollToTopThreshold: 10
}}
- getItemLayout={(data, index) => ({
- length: index === 0 ? 400 : 280, // Approximate heights for different item types
- offset: index === 0 ? 0 : 400 + (index - 1) * 280,
- index,
- })}
+ disableIntervalMomentum={true}
+ scrollEventThrottle={16}
/>
);
@@ -757,6 +839,27 @@ const styles = StyleSheet.create({
fontWeight: '600',
marginLeft: 8,
},
+ loadMoreContainer: {
+ padding: 16,
+ alignItems: 'center',
+ },
+ loadMoreButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingVertical: 12,
+ borderRadius: 25,
+ elevation: 2,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ },
+ loadMoreText: {
+ marginLeft: 8,
+ fontSize: 14,
+ fontWeight: '600',
+ },
loadingContainer: {
flex: 1,
justifyContent: 'center',
@@ -1131,4 +1234,4 @@ const styles = StyleSheet.create({
},
});
-export default React.memo(HomeScreen);
\ No newline at end of file
+export default React.memo(HomeScreen);
\ No newline at end of file
diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx
index 1d01a7c8..a181971f 100644
--- a/src/screens/StreamsScreen.tsx
+++ b/src/screens/StreamsScreen.tsx
@@ -839,32 +839,9 @@ export const StreamsScreen = () => {
const streamName = stream.name || stream.title || 'Unnamed Stream';
const streamProvider = stream.addonId || stream.addonName || stream.name;
- // Debug logging for stream provider identification
- console.log('[StreamsScreen] Stream Provider Debug:');
- console.log(' stream.addonId:', stream.addonId);
- console.log(' stream.addonName:', stream.addonName);
- console.log(' stream.name:', stream.name);
- console.log(' final streamProvider:', streamProvider);
- console.log(' stream.url:', stream.url);
+ // Stream provider debug logging removed
- // Enhanced logging for NetMirror streams
- if (streamProvider && (streamProvider.toLowerCase().includes('netmirror') || stream.url?.includes('nm-cdn'))) {
- console.log('\n[StreamsScreen] š¬ NETMIRROR STREAM DETAILS:');
- console.log(' šŗ Stream Name:', streamName);
- console.log(' š Final Extracted URL:', stream.url);
- console.log(' š Headers:', JSON.stringify(stream.headers, null, 2));
- console.log(' šÆ Quality:', stream.title?.match(/(\d+)p/)?.[1] || 'Unknown');
- console.log(' š± Platform:', Platform.OS);
- console.log(' āļø Stream Object:', JSON.stringify({
- name: stream.name,
- title: stream.title,
- url: stream.url,
- headers: stream.headers,
- addonId: stream.addonId,
- addonName: stream.addonName
- }, null, 2));
- console.log('\n');
- }
+ // NetMirror stream logging removed
// Navigate to player immediately without waiting for orientation lock
// This prevents delay in player opening
diff --git a/src/services/storageService.ts b/src/services/storageService.ts
index 9ef38bc8..75c296c6 100644
--- a/src/services/storageService.ts
+++ b/src/services/storageService.ts
@@ -350,9 +350,7 @@ class StorageService {
};
await this.setWatchProgress(id, type, newProgress, episodeId);
- const timeSource = exactTime ? 'exact' : 'calculated';
- const durationSource = await this.getContentDuration(id, type, episodeId) ? 'stored' : 'estimated';
- logger.log(`[StorageService] Created progress from Trakt: ${(currentTime/60).toFixed(1)}min (${timeSource}) of ${(duration/60).toFixed(0)}min (${durationSource})`);
+ // Progress creation logging removed
} else {
// Local progress exists - merge intelligently
const localProgressPercent = (localProgress.currentTime / localProgress.duration) * 100;
@@ -416,11 +414,7 @@ class StorageService {
};
await this.setWatchProgress(id, type, updatedProgress, episodeId);
- // Only log significant changes
- if (progressDiff > 10 || traktProgress === 100) {
- const timeSource = exactTime ? 'exact' : 'calculated';
- logger.log(`[StorageService] Updated progress: ${(currentTime/60).toFixed(1)}min (${timeSource}) = ${traktProgress}%`);
- }
+ // Progress update logging removed
}
} catch (error) {
logger.error('Error merging with Trakt progress:', error);
@@ -428,4 +422,4 @@ class StorageService {
}
}
-export const storageService = StorageService.getInstance();
\ No newline at end of file
+export const storageService = StorageService.getInstance();
\ No newline at end of file
diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts
index 075614f2..3feb5f9b 100644
--- a/src/services/stremioService.ts
+++ b/src/services/stremioService.ts
@@ -1121,8 +1121,10 @@ class StremioService {
// For series episodes, use the videoId directly which includes series ID + episode info
let url = '';
if (type === 'series' && videoId) {
- // For series, the format should be /subtitles/series/tt12345:1:2.json
- url = `${baseUrl}/subtitles/${type}/${videoId}.json`;
+ // For series, extract the IMDB ID and episode info from videoId (series:tt12345:1:2)
+ // and construct the proper URL format: /subtitles/series/tt12345:1:2.json
+ const episodeInfo = videoId.replace('series:', '');
+ url = `${baseUrl}/subtitles/series/${episodeInfo}.json`;
} else {
// For movies, the format is /subtitles/movie/tt12345.json
url = `${baseUrl}/subtitles/${type}/${id}.json`;
diff --git a/src/services/traktService.ts b/src/services/traktService.ts
index cbd3c32f..3bce8074 100644
--- a/src/services/traktService.ts
+++ b/src/services/traktService.ts
@@ -598,14 +598,7 @@ export class TraktService {
const response = await fetch(`${TRAKT_API_URL}${endpoint}`, options);
- // Debug log API responses for scrobble endpoints
- if (endpoint.includes('/scrobble/')) {
- logger.log(`[TraktService] DEBUG API Response for ${endpoint}:`, {
- status: response.status,
- statusText: response.statusText,
- headers: Object.fromEntries(response.headers.entries())
- });
- }
+ // Debug logging removed to reduce terminal noise
// Handle rate limiting with exponential backoff
if (response.status === 429) {
@@ -693,7 +686,7 @@ export class TraktService {
// Debug log successful scrobble responses
if (endpoint.includes('/scrobble/')) {
- logger.log(`[TraktService] DEBUG API Success for ${endpoint}:`, responseData);
+ // API success logging removed
}
return responseData;
@@ -1150,7 +1143,7 @@ export class TraktService {
progress: Math.round(progress * 100) / 100 // Round to 2 decimal places
};
- logger.log('[TraktService] DEBUG movie payload:', JSON.stringify(payload, null, 2));
+ // Movie payload logging removed
return payload;
} else if (contentData.type === 'episode') {
if (!contentData.season || !contentData.episode || !contentData.showTitle || !contentData.showYear) {
@@ -1192,7 +1185,7 @@ export class TraktService {
payload.episode.ids.imdb = cleanEpisodeImdbId;
}
- logger.log('[TraktService] DEBUG episode payload:', JSON.stringify(payload, null, 2));
+ // Episode payload logging removed
return payload;
}
@@ -1287,17 +1280,7 @@ export class TraktService {
return true;
}
- // Debug log the content data being sent
- logger.log(`[TraktService] DEBUG scrobbleStart payload:`, {
- type: contentData.type,
- title: contentData.title,
- year: contentData.year,
- imdbId: contentData.imdbId,
- season: contentData.season,
- episode: contentData.episode,
- showTitle: contentData.showTitle,
- progress: progress
- });
+ // Debug log removed to reduce terminal noise
// Only start if not already watching this content
if (this.currentlyWatching.has(watchingKey)) {
@@ -1485,23 +1468,23 @@ export class TraktService {
public async debugPlaybackProgress(): Promise {
try {
if (!await this.isAuthenticated()) {
- logger.log('[TraktService] DEBUG: Not authenticated');
+ // Debug logging removed
return;
}
const progress = await this.getPlaybackProgress();
- logger.log(`[TraktService] DEBUG: Found ${progress.length} items in Trakt playback progress:`);
+ // Progress logging removed
progress.forEach((item, index) => {
if (item.type === 'movie' && item.movie) {
- logger.log(`[TraktService] DEBUG ${index + 1}: Movie "${item.movie.title}" (${item.movie.year}) - ${item.progress.toFixed(1)}% - Paused: ${item.paused_at}`);
+ // Movie progress logging removed
} else if (item.type === 'episode' && item.episode && item.show) {
- logger.log(`[TraktService] DEBUG ${index + 1}: Episode "${item.show.title}" S${item.episode.season}E${item.episode.number} - ${item.progress.toFixed(1)}% - Paused: ${item.paused_at}`);
+ // Episode progress logging removed
}
});
if (progress.length === 0) {
- logger.log('[TraktService] DEBUG: No items found in Trakt playback progress');
+ // No progress logging removed
}
} catch (error) {
logger.error('[TraktService] DEBUG: Error fetching playback progress:', error);
diff --git a/src/utils/.logger.ts.swp b/src/utils/.logger.ts.swp
new file mode 100644
index 00000000..9c23798e
Binary files /dev/null and b/src/utils/.logger.ts.swp differ
diff --git a/src/utils/httpInterceptor.ts b/src/utils/httpInterceptor.ts
index 1933adf5..e7a2143b 100644
--- a/src/utils/httpInterceptor.ts
+++ b/src/utils/httpInterceptor.ts
@@ -5,13 +5,7 @@ export const logHttpRequest = async (url: string, options: RequestInit = {}): Pr
const method = options.method || 'GET';
const headers = options.headers || {};
- // Log HTTP request
- console.log('\nš [AndroidVideoPlayer] HTTP REQUEST:');
- console.log('š URL:', url);
- console.log('š§ Method:', method);
- console.log('š Headers:', JSON.stringify(headers, null, 2));
- console.log('ā° Request Time:', new Date().toISOString());
- console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
+ // HTTP request logging removed
const startTime = Date.now();
@@ -21,33 +15,14 @@ export const logHttpRequest = async (url: string, options: RequestInit = {}): Pr
const endTime = Date.now();
const duration = endTime - startTime;
- // Log HTTP response success
- console.log('\nā
[AndroidVideoPlayer] HTTP RESPONSE SUCCESS:');
- console.log('š URL:', url);
- console.log('š Status:', `${response.status} ${response.statusText}`);
- console.log('š Response Headers:', JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2));
- console.log('ā±ļø Duration:', `${duration}ms`);
- console.log('š¦ Content-Type:', response.headers.get('content-type') || 'Unknown');
- console.log('š Content-Length:', response.headers.get('content-length') || 'Unknown');
- console.log('š CORS:', response.headers.get('access-control-allow-origin') || 'Not specified');
- console.log('ā° Response Time:', new Date().toISOString());
- console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
+ // HTTP response success logging removed
return response;
} catch (error: any) {
const endTime = Date.now();
const duration = endTime - startTime;
- // Log HTTP response error
- console.log('\nā [AndroidVideoPlayer] HTTP RESPONSE ERROR:');
- console.log('š URL:', url);
- console.log('š Status: Network Error');
- console.log('š¬ Error Message:', error.message || 'Unknown error');
- console.log('š Error Type:', error.name || 'Unknown');
- console.log('ā±ļø Duration:', `${duration}ms`);
- console.log('š Full Error:', JSON.stringify(error, null, 2));
- console.log('ā° Error Time:', new Date().toISOString());
- console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
+ // HTTP response error logging removed
throw error;
}