mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-27 11:23:02 +00:00
improvements on trakt
This commit is contained in:
parent
2feba6f6eb
commit
a42ce3bdfa
4 changed files with 238 additions and 77 deletions
|
|
@ -7,9 +7,11 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
AppState,
|
AppState,
|
||||||
AppStateStatus
|
AppStateStatus,
|
||||||
|
Alert,
|
||||||
|
ActivityIndicator
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import Animated, { FadeIn } from 'react-native-reanimated';
|
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
|
|
@ -19,6 +21,7 @@ import { Image as ExpoImage } from 'expo-image';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { storageService } from '../../services/storageService';
|
import { storageService } from '../../services/storageService';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
import * as Haptics from 'expo-haptics';
|
||||||
|
|
||||||
// Define interface for continue watching items
|
// Define interface for continue watching items
|
||||||
interface ContinueWatchingItem extends StreamingContent {
|
interface ContinueWatchingItem extends StreamingContent {
|
||||||
|
|
@ -76,6 +79,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const appState = useRef(AppState.currentState);
|
const appState = useRef(AppState.currentState);
|
||||||
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
|
||||||
|
const longPressTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Use a state to track if a background refresh is in progress
|
// Use a state to track if a background refresh is in progress
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
@ -245,6 +250,9 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
if (refreshTimerRef.current) {
|
if (refreshTimerRef.current) {
|
||||||
clearTimeout(refreshTimerRef.current);
|
clearTimeout(refreshTimerRef.current);
|
||||||
}
|
}
|
||||||
|
if (longPressTimeoutRef.current) {
|
||||||
|
clearTimeout(longPressTimeoutRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Fallback: poll for updates every 30 seconds
|
// Fallback: poll for updates every 30 seconds
|
||||||
|
|
@ -255,6 +263,9 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
if (refreshTimerRef.current) {
|
if (refreshTimerRef.current) {
|
||||||
clearTimeout(refreshTimerRef.current);
|
clearTimeout(refreshTimerRef.current);
|
||||||
}
|
}
|
||||||
|
if (longPressTimeoutRef.current) {
|
||||||
|
clearTimeout(longPressTimeoutRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [loadContinueWatching, handleAppStateChange]);
|
}, [loadContinueWatching, handleAppStateChange]);
|
||||||
|
|
@ -277,6 +288,60 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
navigation.navigate('Metadata', { id, type });
|
navigation.navigate('Metadata', { id, type });
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
|
// Handle long press to delete
|
||||||
|
const handleLongPress = useCallback((item: ContinueWatchingItem) => {
|
||||||
|
try {
|
||||||
|
// Trigger haptic feedback
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore haptic errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show confirmation alert
|
||||||
|
Alert.alert(
|
||||||
|
"Remove from Continue Watching",
|
||||||
|
`Remove "${item.name}" from your continue watching list?`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: "Cancel",
|
||||||
|
style: "cancel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Remove",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: async () => {
|
||||||
|
setDeletingItemId(item.id);
|
||||||
|
try {
|
||||||
|
// Trigger haptic feedback for confirmation
|
||||||
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
|
|
||||||
|
// Remove the watch progress
|
||||||
|
await storageService.removeWatchProgress(
|
||||||
|
item.id,
|
||||||
|
item.type,
|
||||||
|
item.type === 'series' && item.season && item.episode
|
||||||
|
? `${item.season}:${item.episode}`
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the list by filtering out the deleted item
|
||||||
|
setContinueWatchingItems(prev =>
|
||||||
|
prev.filter(i => i.id !== item.id ||
|
||||||
|
(i.type === 'series' && item.type === 'series' &&
|
||||||
|
(i.season !== item.season || i.episode !== item.episode))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to remove watch progress:', error);
|
||||||
|
} finally {
|
||||||
|
setDeletingItemId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// If no continue watching items, don't render anything
|
// If no continue watching items, don't render anything
|
||||||
if (continueWatchingItems.length === 0) {
|
if (continueWatchingItems.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -302,6 +367,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
}]}
|
}]}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
onPress={() => handleContentPress(item.id, item.type)}
|
onPress={() => handleContentPress(item.id, item.type)}
|
||||||
|
onLongPress={() => handleLongPress(item)}
|
||||||
|
delayLongPress={800}
|
||||||
>
|
>
|
||||||
{/* Poster Image */}
|
{/* Poster Image */}
|
||||||
<View style={styles.posterContainer}>
|
<View style={styles.posterContainer}>
|
||||||
|
|
@ -315,6 +382,17 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
placeholderContentFit="cover"
|
placeholderContentFit="cover"
|
||||||
recyclingKey={item.id}
|
recyclingKey={item.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Delete Indicator Overlay */}
|
||||||
|
{deletingItemId === item.id && (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.duration(200)}
|
||||||
|
exiting={FadeOut.duration(200)}
|
||||||
|
style={styles.deletingOverlay}
|
||||||
|
>
|
||||||
|
<ActivityIndicator size="large" color="#FFFFFF" />
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Content Details */}
|
{/* Content Details */}
|
||||||
|
|
@ -442,6 +520,7 @@ const styles = StyleSheet.create({
|
||||||
posterContainer: {
|
posterContainer: {
|
||||||
width: 80,
|
width: 80,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
},
|
},
|
||||||
continueWatchingPoster: {
|
continueWatchingPoster: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
@ -449,6 +528,18 @@ const styles = StyleSheet.create({
|
||||||
borderTopLeftRadius: 12,
|
borderTopLeftRadius: 12,
|
||||||
borderBottomLeftRadius: 12,
|
borderBottomLeftRadius: 12,
|
||||||
},
|
},
|
||||||
|
deletingOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderTopLeftRadius: 12,
|
||||||
|
borderBottomLeftRadius: 12,
|
||||||
|
},
|
||||||
contentDetails: {
|
contentDetails: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
|
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
|
||||||
const isMounted = useRef(true);
|
const isMounted = useRef(true);
|
||||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
||||||
|
|
||||||
const hideControls = () => {
|
const hideControls = () => {
|
||||||
Animated.timing(fadeAnim, {
|
Animated.timing(fadeAnim, {
|
||||||
|
|
@ -557,12 +558,24 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = async () => {
|
||||||
logger.log('[AndroidVideoPlayer] Close button pressed - syncing to Trakt before closing');
|
logger.log('[AndroidVideoPlayer] Close button pressed - syncing to Trakt before closing');
|
||||||
logger.log(`[AndroidVideoPlayer] Current progress: ${currentTime}/${duration} (${duration > 0 ? ((currentTime / duration) * 100).toFixed(1) : 0}%)`);
|
|
||||||
|
// Set syncing state to prevent multiple close attempts
|
||||||
|
setIsSyncingBeforeClose(true);
|
||||||
|
|
||||||
|
// Make sure we have the most accurate current time
|
||||||
|
const actualCurrentTime = currentTime;
|
||||||
|
const progressPercent = duration > 0 ? (actualCurrentTime / duration) * 100 : 0;
|
||||||
|
|
||||||
|
logger.log(`[AndroidVideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Force one last progress update (scrobble/pause) with the exact time
|
||||||
|
await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true);
|
||||||
|
|
||||||
// Sync progress to Trakt before closing
|
// Sync progress to Trakt before closing
|
||||||
traktAutosync.handlePlaybackEnd(currentTime, duration, 'unmount');
|
await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount');
|
||||||
|
|
||||||
// Start exit animation
|
// Start exit animation
|
||||||
Animated.parallel([
|
Animated.parallel([
|
||||||
|
|
@ -578,7 +591,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}),
|
}),
|
||||||
]).start();
|
]).start();
|
||||||
|
|
||||||
// Small delay to allow animation to start, then unlock orientation and navigate
|
// Longer delay to ensure Trakt sync completes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ScreenOrientation.unlockAsync().then(() => {
|
ScreenOrientation.unlockAsync().then(() => {
|
||||||
disableImmersiveMode();
|
disableImmersiveMode();
|
||||||
|
|
@ -588,7 +601,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
disableImmersiveMode();
|
disableImmersiveMode();
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 500); // Increased from 100ms to 500ms
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[AndroidVideoPlayer] Error syncing to Trakt before closing:', error);
|
||||||
|
// Navigate anyway even if sync fails
|
||||||
|
disableImmersiveMode();
|
||||||
|
navigation.goBack();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResume = async () => {
|
const handleResume = async () => {
|
||||||
|
|
@ -631,9 +650,22 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
setIsBuffering(data.isBuffering);
|
setIsBuffering(data.isBuffering);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEnd = () => {
|
const onEnd = async () => {
|
||||||
// Sync final progress to Trakt
|
// Make sure we report 100% progress to Trakt
|
||||||
traktAutosync.handlePlaybackEnd(currentTime, duration, 'ended');
|
const finalTime = duration;
|
||||||
|
setCurrentTime(finalTime);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Force one last progress update (scrobble/pause) just in case
|
||||||
|
await traktAutosync.handleProgressUpdate(finalTime, duration, true);
|
||||||
|
logger.log('[AndroidVideoPlayer] Sent final progress update on video end');
|
||||||
|
|
||||||
|
// Now send the stop call
|
||||||
|
await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended');
|
||||||
|
logger.log('[AndroidVideoPlayer] Completed video end sync to Trakt');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[AndroidVideoPlayer] Error syncing to Trakt on video end:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAudioTrack = (trackId: number) => {
|
const selectAudioTrack = (trackId: number) => {
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
|
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
|
||||||
const isMounted = useRef(true);
|
const isMounted = useRef(true);
|
||||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
||||||
|
|
||||||
const hideControls = () => {
|
const hideControls = () => {
|
||||||
Animated.timing(fadeAnim, {
|
Animated.timing(fadeAnim, {
|
||||||
|
|
@ -575,12 +576,24 @@ const VideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = async () => {
|
||||||
logger.log('[VideoPlayer] Close button pressed - syncing to Trakt before closing');
|
logger.log('[VideoPlayer] Close button pressed - syncing to Trakt before closing');
|
||||||
logger.log(`[VideoPlayer] Current progress: ${currentTime}/${duration} (${duration > 0 ? ((currentTime / duration) * 100).toFixed(1) : 0}%)`);
|
|
||||||
|
// Set syncing state to prevent multiple close attempts
|
||||||
|
setIsSyncingBeforeClose(true);
|
||||||
|
|
||||||
|
// Make sure we have the most accurate current time
|
||||||
|
const actualCurrentTime = currentTime;
|
||||||
|
const progressPercent = duration > 0 ? (actualCurrentTime / duration) * 100 : 0;
|
||||||
|
|
||||||
|
logger.log(`[VideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Force one last progress update (scrobble/pause) with the exact time
|
||||||
|
await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true);
|
||||||
|
|
||||||
// Sync progress to Trakt before closing
|
// Sync progress to Trakt before closing
|
||||||
traktAutosync.handlePlaybackEnd(currentTime, duration, 'unmount');
|
await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount');
|
||||||
|
|
||||||
// Start exit animation
|
// Start exit animation
|
||||||
Animated.parallel([
|
Animated.parallel([
|
||||||
|
|
@ -596,7 +609,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
}),
|
}),
|
||||||
]).start();
|
]).start();
|
||||||
|
|
||||||
// Small delay to allow animation to start, then unlock orientation and navigate
|
// Longer delay to ensure Trakt sync completes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ScreenOrientation.unlockAsync().then(() => {
|
ScreenOrientation.unlockAsync().then(() => {
|
||||||
disableImmersiveMode();
|
disableImmersiveMode();
|
||||||
|
|
@ -606,7 +619,13 @@ const VideoPlayer: React.FC = () => {
|
||||||
disableImmersiveMode();
|
disableImmersiveMode();
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 500); // Increased from 100ms to 500ms
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[VideoPlayer] Error syncing to Trakt before closing:', error);
|
||||||
|
// Navigate anyway even if sync fails
|
||||||
|
disableImmersiveMode();
|
||||||
|
navigation.goBack();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResume = async () => {
|
const handleResume = async () => {
|
||||||
|
|
@ -649,9 +668,22 @@ const VideoPlayer: React.FC = () => {
|
||||||
setIsBuffering(event.isBuffering);
|
setIsBuffering(event.isBuffering);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEnd = () => {
|
const onEnd = async () => {
|
||||||
// Sync final progress to Trakt
|
// Make sure we report 100% progress to Trakt
|
||||||
traktAutosync.handlePlaybackEnd(currentTime, duration, 'ended');
|
const finalTime = duration;
|
||||||
|
setCurrentTime(finalTime);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Force one last progress update (scrobble/pause) just in case
|
||||||
|
await traktAutosync.handleProgressUpdate(finalTime, duration, true);
|
||||||
|
logger.log('[VideoPlayer] Sent final progress update on video end');
|
||||||
|
|
||||||
|
// Now send the stop call
|
||||||
|
await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended');
|
||||||
|
logger.log('[VideoPlayer] Completed video end sync to Trakt');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[VideoPlayer] Error syncing to Trakt on video end:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAudioTrack = (trackId: number) => {
|
const selectAudioTrack = (trackId: number) => {
|
||||||
|
|
|
||||||
|
|
@ -296,13 +296,19 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only stop if we have meaningful progress (>= 1%) or it's a natural video end
|
// Only stop if we have meaningful progress (>= 0.5%) or it's a natural video end
|
||||||
// Skip unmount calls with very low progress unless video actually ended
|
// Lower threshold for unmount calls to catch more edge cases
|
||||||
if (reason === 'unmount' && progressPercent < 1) {
|
if (reason === 'unmount' && progressPercent < 0.5) {
|
||||||
logger.log(`[TraktAutosync] Skipping unmount stop for ${options.title} - too early (${progressPercent.toFixed(1)}%)`);
|
logger.log(`[TraktAutosync] Skipping unmount stop for ${options.title} - too early (${progressPercent.toFixed(1)}%)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For natural end events, always set progress to at least 90%
|
||||||
|
if (reason === 'ended' && progressPercent < 90) {
|
||||||
|
logger.log(`[TraktAutosync] Natural end detected but progress is low (${progressPercent.toFixed(1)}%), boosting to 90%`);
|
||||||
|
progressPercent = 90;
|
||||||
|
}
|
||||||
|
|
||||||
// Mark stop attempt and update timestamp
|
// Mark stop attempt and update timestamp
|
||||||
lastStopCall.current = now;
|
lastStopCall.current = now;
|
||||||
hasStopped.current = true;
|
hasStopped.current = true;
|
||||||
|
|
@ -349,7 +355,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
// Reset stop flag on error so we can try again
|
// Reset stop flag on error so we can try again
|
||||||
hasStopped.current = false;
|
hasStopped.current = false;
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, autosyncSettings.enabled, stopWatching, buildContentData, options]);
|
}, [isAuthenticated, autosyncSettings.enabled, stopWatching, startWatching, buildContentData, options]);
|
||||||
|
|
||||||
// Reset state (useful when switching content)
|
// Reset state (useful when switching content)
|
||||||
const resetState = useCallback(() => {
|
const resetState = useCallback(() => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue