mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +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,
|
||||
Dimensions,
|
||||
AppState,
|
||||
AppStateStatus
|
||||
AppStateStatus,
|
||||
Alert,
|
||||
ActivityIndicator
|
||||
} 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 { NavigationProp } from '@react-navigation/native';
|
||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
|
|
@ -19,6 +21,7 @@ import { Image as ExpoImage } from 'expo-image';
|
|||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { storageService } from '../../services/storageService';
|
||||
import { logger } from '../../utils/logger';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
|
||||
// Define interface for continue watching items
|
||||
interface ContinueWatchingItem extends StreamingContent {
|
||||
|
|
@ -76,6 +79,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
const [loading, setLoading] = useState(true);
|
||||
const appState = useRef(AppState.currentState);
|
||||
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
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
|
@ -245,6 +250,9 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current);
|
||||
}
|
||||
if (longPressTimeoutRef.current) {
|
||||
clearTimeout(longPressTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Fallback: poll for updates every 30 seconds
|
||||
|
|
@ -255,6 +263,9 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current);
|
||||
}
|
||||
if (longPressTimeoutRef.current) {
|
||||
clearTimeout(longPressTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [loadContinueWatching, handleAppStateChange]);
|
||||
|
|
@ -277,6 +288,60 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
navigation.navigate('Metadata', { id, type });
|
||||
}, [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 (continueWatchingItems.length === 0) {
|
||||
return null;
|
||||
|
|
@ -302,6 +367,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
}]}
|
||||
activeOpacity={0.8}
|
||||
onPress={() => handleContentPress(item.id, item.type)}
|
||||
onLongPress={() => handleLongPress(item)}
|
||||
delayLongPress={800}
|
||||
>
|
||||
{/* Poster Image */}
|
||||
<View style={styles.posterContainer}>
|
||||
|
|
@ -315,6 +382,17 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
placeholderContentFit="cover"
|
||||
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>
|
||||
|
||||
{/* Content Details */}
|
||||
|
|
@ -442,6 +520,7 @@ const styles = StyleSheet.create({
|
|||
posterContainer: {
|
||||
width: 80,
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
},
|
||||
continueWatchingPoster: {
|
||||
width: '100%',
|
||||
|
|
@ -449,6 +528,18 @@ const styles = StyleSheet.create({
|
|||
borderTopLeftRadius: 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: {
|
||||
flex: 1,
|
||||
padding: 12,
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
|
||||
const isMounted = useRef(true);
|
||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
||||
|
||||
const hideControls = () => {
|
||||
Animated.timing(fadeAnim, {
|
||||
|
|
@ -557,38 +558,56 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
const handleClose = async () => {
|
||||
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}%)`);
|
||||
|
||||
// Sync progress to Trakt before closing
|
||||
traktAutosync.handlePlaybackEnd(currentTime, duration, 'unmount');
|
||||
// Set syncing state to prevent multiple close attempts
|
||||
setIsSyncingBeforeClose(true);
|
||||
|
||||
// Start exit animation
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(openingFadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
|
||||
// Small delay to allow animation to start, then unlock orientation and navigate
|
||||
setTimeout(() => {
|
||||
ScreenOrientation.unlockAsync().then(() => {
|
||||
disableImmersiveMode();
|
||||
navigation.goBack();
|
||||
}).catch(() => {
|
||||
// Fallback: navigate even if orientation unlock fails
|
||||
disableImmersiveMode();
|
||||
navigation.goBack();
|
||||
});
|
||||
}, 100);
|
||||
// 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
|
||||
await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount');
|
||||
|
||||
// Start exit animation
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(openingFadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
|
||||
// Longer delay to ensure Trakt sync completes
|
||||
setTimeout(() => {
|
||||
ScreenOrientation.unlockAsync().then(() => {
|
||||
disableImmersiveMode();
|
||||
navigation.goBack();
|
||||
}).catch(() => {
|
||||
// Fallback: navigate even if orientation unlock fails
|
||||
disableImmersiveMode();
|
||||
navigation.goBack();
|
||||
});
|
||||
}, 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 () => {
|
||||
|
|
@ -631,9 +650,22 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
setIsBuffering(data.isBuffering);
|
||||
};
|
||||
|
||||
const onEnd = () => {
|
||||
// Sync final progress to Trakt
|
||||
traktAutosync.handlePlaybackEnd(currentTime, duration, 'ended');
|
||||
const onEnd = async () => {
|
||||
// Make sure we report 100% progress to Trakt
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ const VideoPlayer: React.FC = () => {
|
|||
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
|
||||
const isMounted = useRef(true);
|
||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
||||
|
||||
const hideControls = () => {
|
||||
Animated.timing(fadeAnim, {
|
||||
|
|
@ -575,38 +576,56 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
const handleClose = async () => {
|
||||
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}%)`);
|
||||
|
||||
// Sync progress to Trakt before closing
|
||||
traktAutosync.handlePlaybackEnd(currentTime, duration, 'unmount');
|
||||
// Set syncing state to prevent multiple close attempts
|
||||
setIsSyncingBeforeClose(true);
|
||||
|
||||
// Start exit animation
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(openingFadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
|
||||
// Small delay to allow animation to start, then unlock orientation and navigate
|
||||
setTimeout(() => {
|
||||
ScreenOrientation.unlockAsync().then(() => {
|
||||
disableImmersiveMode();
|
||||
navigation.goBack();
|
||||
}).catch(() => {
|
||||
// Fallback: navigate even if orientation unlock fails
|
||||
disableImmersiveMode();
|
||||
navigation.goBack();
|
||||
});
|
||||
}, 100);
|
||||
// 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
|
||||
await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount');
|
||||
|
||||
// Start exit animation
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(openingFadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
|
||||
// Longer delay to ensure Trakt sync completes
|
||||
setTimeout(() => {
|
||||
ScreenOrientation.unlockAsync().then(() => {
|
||||
disableImmersiveMode();
|
||||
navigation.goBack();
|
||||
}).catch(() => {
|
||||
// Fallback: navigate even if orientation unlock fails
|
||||
disableImmersiveMode();
|
||||
navigation.goBack();
|
||||
});
|
||||
}, 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 () => {
|
||||
|
|
@ -649,9 +668,22 @@ const VideoPlayer: React.FC = () => {
|
|||
setIsBuffering(event.isBuffering);
|
||||
};
|
||||
|
||||
const onEnd = () => {
|
||||
// Sync final progress to Trakt
|
||||
traktAutosync.handlePlaybackEnd(currentTime, duration, 'ended');
|
||||
const onEnd = async () => {
|
||||
// Make sure we report 100% progress to Trakt
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -259,13 +259,13 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
|||
maxProgress = lastSyncProgress.current;
|
||||
}
|
||||
|
||||
// Also check local storage for the highest recorded progress
|
||||
try {
|
||||
const savedProgress = await storageService.getWatchProgress(
|
||||
options.id,
|
||||
options.type,
|
||||
options.episodeId
|
||||
);
|
||||
// Also check local storage for the highest recorded progress
|
||||
try {
|
||||
const savedProgress = await storageService.getWatchProgress(
|
||||
options.id,
|
||||
options.type,
|
||||
options.episodeId
|
||||
);
|
||||
|
||||
if (savedProgress && savedProgress.duration > 0) {
|
||||
const savedProgressPercent = (savedProgress.currentTime / savedProgress.duration) * 100;
|
||||
|
|
@ -296,13 +296,19 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
|||
}
|
||||
}
|
||||
|
||||
// Only stop if we have meaningful progress (>= 1%) or it's a natural video end
|
||||
// Skip unmount calls with very low progress unless video actually ended
|
||||
if (reason === 'unmount' && progressPercent < 1) {
|
||||
// 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)}%)`);
|
||||
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
|
||||
lastStopCall.current = now;
|
||||
hasStopped.current = true;
|
||||
|
|
@ -349,7 +355,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
|||
// Reset stop flag on error so we can try again
|
||||
hasStopped.current = false;
|
||||
}
|
||||
}, [isAuthenticated, autosyncSettings.enabled, stopWatching, buildContentData, options]);
|
||||
}, [isAuthenticated, autosyncSettings.enabled, stopWatching, startWatching, buildContentData, options]);
|
||||
|
||||
// Reset state (useful when switching content)
|
||||
const resetState = useCallback(() => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue