trakt update

This commit is contained in:
tapframe 2025-09-15 17:40:50 +05:30
parent 22a118c383
commit 3b460ec63f
9 changed files with 127 additions and 57 deletions

View file

@ -16,7 +16,7 @@
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true"> <application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/> <meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/> <meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ERROR_RECOVERY_ONLY"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="30000"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="30000"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified"> <activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified">

View file

@ -3,4 +3,5 @@
<color name="iconBackground">#020404</color> <color name="iconBackground">#020404</color>
<color name="colorPrimary">#023c69</color> <color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#020404</color> <color name="colorPrimaryDark">#020404</color>
<color name="activityBackground">#020404</color>
</resources> </resources>

View file

@ -5,6 +5,7 @@
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item> <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#020404</item> <item name="android:statusBarColor">#020404</item>
<item name="android:windowBackground">@color/activityBackground</item>
</style> </style>
<style name="ResetEditText" parent="@android:style/Widget.EditText"> <style name="ResetEditText" parent="@android:style/Widget.EditText">
<item name="android:padding">0dp</item> <item name="android:padding">0dp</item>

View file

@ -4,6 +4,7 @@
"slug": "nuvio", "slug": "nuvio",
"version": "0.6.0-beta.11", "version": "0.6.0-beta.11",
"orientation": "default", "orientation": "default",
"backgroundColor": "#020404",
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png", "icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
"userInterfaceStyle": "dark", "userInterfaceStyle": "dark",
"scheme": "stremioexpo", "scheme": "stremioexpo",

View file

@ -3,29 +3,42 @@ import { View, StyleSheet, Animated, Easing } from 'react-native';
import TraktIcon from '../../../assets/rating-icons/trakt.svg'; import TraktIcon from '../../../assets/rating-icons/trakt.svg';
export const TraktLoadingSpinner = () => { export const TraktLoadingSpinner = () => {
const spinValue = useRef(new Animated.Value(0)).current; const pulseValue = useRef(new Animated.Value(0)).current;
useEffect(() => { useEffect(() => {
const spin = Animated.loop( const pulse = Animated.loop(
Animated.timing(spinValue, { Animated.sequence([
toValue: 1, Animated.timing(pulseValue, {
duration: 1500, toValue: 1,
easing: Easing.linear, duration: 900,
useNativeDriver: true, easing: Easing.inOut(Easing.ease),
}) useNativeDriver: true,
}),
Animated.timing(pulseValue, {
toValue: 0,
duration: 900,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
})
])
); );
spin.start(); pulse.start();
return () => spin.stop(); return () => pulse.stop();
}, [spinValue]); }, [pulseValue]);
const rotation = spinValue.interpolate({ const opacity = pulseValue.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: ['0deg', '360deg'], outputRange: [0.65, 1],
});
const scale = pulseValue.interpolate({
inputRange: [0, 1],
outputRange: [0.95, 1.05],
}); });
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Animated.View style={{ transform: [{ rotate: rotation }] }}> <Animated.View style={{ opacity, transform: [{ scale }] }}>
<TraktIcon width={80} height={80} /> <TraktIcon width={80} height={80} />
</Animated.View> </Animated.View>
</View> </View>

View file

@ -326,6 +326,17 @@ const TraktSettingsScreen: React.FC = () => {
]}> ]}>
Sync Settings Sync Settings
</Text> </Text>
<View style={[
styles.infoBox,
{ backgroundColor: isDarkMode ? currentTheme.colors.elevation1 : '#F5F7FB', borderColor: isDarkMode ? 'rgba(255,255,255,0.06)' : '#E3E8F0' }
]}>
<Text style={[
styles.infoText,
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
When connected to Trakt, Continue Watching is sourced from Trakt. Account sync for watch progress is disabled to avoid conflicts.
</Text>
</View>
<View style={styles.settingItem}> <View style={styles.settingItem}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
@ -564,6 +575,16 @@ const styles = StyleSheet.create({
settingDescription: { settingDescription: {
fontSize: 14, fontSize: 14,
}, },
infoBox: {
padding: 12,
borderRadius: 8,
borderWidth: 1,
marginBottom: 16,
},
infoText: {
fontSize: 13,
lineHeight: 18,
},
}); });
export default TraktSettingsScreen; export default TraktSettingsScreen;

View file

@ -7,6 +7,7 @@ import { catalogService, StreamingContent } from './catalogService';
// import localScraperService from './localScraperService'; // import localScraperService from './localScraperService';
import { settingsEmitter } from '../hooks/useSettings'; import { settingsEmitter } from '../hooks/useSettings';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { traktService } from './traktService';
type WatchProgressRow = { type WatchProgressRow = {
user_id: string; user_id: string;
@ -81,6 +82,7 @@ class SyncService {
const user = await accountService.getCurrentUser(); const user = await accountService.getCurrentUser();
if (!user) return; if (!user) return;
const userId = user.id; const userId = user.id;
const traktActive = await traktService.isAuthenticated();
const addChannel = (table: string, handler: (payload: any) => void) => { const addChannel = (table: string, handler: (payload: any) => void) => {
const channel = supabase const channel = supabase
@ -91,44 +93,49 @@ class SyncService {
logger.log(`[Sync] Realtime subscribed: ${table}`); logger.log(`[Sync] Realtime subscribed: ${table}`);
}; };
// Watch progress: apply granular updates (ignore self-caused pushes via suppressPush) // Watch progress realtime is disabled when Trakt is active
addChannel('watch_progress', async (payload) => { if (!traktActive) {
try { // Watch progress: apply granular updates (ignore self-caused pushes via suppressPush)
const row = (payload.new || payload.old); addChannel('watch_progress', async (payload) => {
if (!row) return; try {
const type = row.media_type as string; const row = (payload.new || payload.old);
const id = row.media_id as string; if (!row) return;
const episodeId = (payload.eventType === 'DELETE') ? (row.episode_id || '') : (row.episode_id || ''); const type = row.media_type as string;
this.suppressPush = true; const id = row.media_id as string;
const deletedAt = (row as any).deleted_at; const episodeId = (payload.eventType === 'DELETE') ? (row.episode_id || '') : (row.episode_id || '');
if (payload.eventType === 'DELETE' || deletedAt) { this.suppressPush = true;
await storageService.removeWatchProgress(id, type, episodeId || undefined); const deletedAt = (row as any).deleted_at;
// Record tombstone with remote timestamp if available if (payload.eventType === 'DELETE' || deletedAt) {
try { await storageService.removeWatchProgress(id, type, episodeId || undefined);
const remoteUpdated = (row as any).updated_at ? new Date((row as any).updated_at).getTime() : Date.now(); // Record tombstone with remote timestamp if available
await storageService.addWatchProgressTombstone(id, type, episodeId || undefined, remoteUpdated); try {
} catch {} const remoteUpdated = (row as any).updated_at ? new Date((row as any).updated_at).getTime() : Date.now();
} else { await storageService.addWatchProgressTombstone(id, type, episodeId || undefined, remoteUpdated);
await storageService.setWatchProgress( } catch {}
id, } else {
type, await storageService.setWatchProgress(
{ id,
currentTime: row.current_time_seconds || 0, type,
duration: row.duration_seconds || 0, {
lastUpdated: row.last_updated_ms || Date.now(), currentTime: row.current_time_seconds || 0,
traktSynced: row.trakt_synced ?? undefined, duration: row.duration_seconds || 0,
traktLastSynced: row.trakt_last_synced_ms ?? undefined, lastUpdated: row.last_updated_ms || Date.now(),
traktProgress: row.trakt_progress_percent ?? undefined, traktSynced: row.trakt_synced ?? undefined,
}, traktLastSynced: row.trakt_last_synced_ms ?? undefined,
// Ensure we pass through the full remote episode_id as-is; empty string becomes undefined traktProgress: row.trakt_progress_percent ?? undefined,
(row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined },
); // Ensure we pass through the full remote episode_id as-is; empty string becomes undefined
(row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined
);
}
} catch {}
finally {
this.suppressPush = false;
} }
} catch {} });
finally { } else {
this.suppressPush = false; logger.log('[Sync] Trakt active → skipping watch_progress realtime subscription');
} }
});
const debouncedPull = (payload?: any) => { const debouncedPull = (payload?: any) => {
if (payload?.table) logger.log(`[Sync][rt] change on ${payload.table} → debounced fullPull`); if (payload?.table) logger.log(`[Sync][rt] change on ${payload.table} → debounced fullPull`);
@ -352,9 +359,10 @@ class SyncService {
const user = await accountService.getCurrentUser(); const user = await accountService.getCurrentUser();
if (!user) return; if (!user) return;
const userId = user.id; const userId = user.id;
const traktActive = await traktService.isAuthenticated();
await Promise.allSettled([ await Promise.allSettled([
(async () => { (!traktActive ? (async () => {
logger.log('[Sync] pull watch_progress'); logger.log('[Sync] pull watch_progress');
const { data: wp } = await supabase const { data: wp } = await supabase
.from('watch_progress') .from('watch_progress')
@ -397,7 +405,7 @@ class SyncService {
} }
} catch {} } catch {}
} }
})(), })() : Promise.resolve()),
(async () => { (async () => {
logger.log('[Sync] pull user_settings'); logger.log('[Sync] pull user_settings');
const { data: us } = await supabase const { data: us } = await supabase
@ -673,6 +681,13 @@ class SyncService {
async pushWatchProgress(): Promise<void> { async pushWatchProgress(): Promise<void> {
const user = await accountService.getCurrentUser(); const user = await accountService.getCurrentUser();
if (!user) return; if (!user) return;
// When Trakt is authenticated, disable account push for continue watching
try {
if (await traktService.isAuthenticated()) {
logger.log('[Sync] Trakt active → skipping push watch_progress');
return;
}
} catch {}
const userId = user.id; const userId = user.id;
const unsynced = await storageService.getUnsyncedProgress(); const unsynced = await storageService.getUnsyncedProgress();
logger.log(`[Sync] push watch_progress rows=${unsynced.length}`); logger.log(`[Sync] push watch_progress rows=${unsynced.length}`);
@ -746,6 +761,13 @@ class SyncService {
private async softDeleteWatchProgress(type: string, id: string, episodeId?: string): Promise<void> { private async softDeleteWatchProgress(type: string, id: string, episodeId?: string): Promise<void> {
const user = await accountService.getCurrentUser(); const user = await accountService.getCurrentUser();
if (!user) return; if (!user) return;
// When Trakt is authenticated, do not propagate deletes to account server for watch progress
try {
if (await traktService.isAuthenticated()) {
logger.log('[Sync] Trakt active → skipping softDelete watch_progress');
return;
}
} catch {}
try { try {
const { error } = await supabase const { error } = await supabase
.from('watch_progress') .from('watch_progress')

3
src/types/svg.d.ts vendored
View file

@ -1,5 +1,6 @@
import * as React from 'react';
declare module '*.svg' { declare module '*.svg' {
import { SvgProps } from 'react-native-svg'; import { SvgProps } from 'react-native-svg';
const content: React.FC<SvgProps>; const content: React.FC<SvgProps>;
export default content; export default content;
} }

View file

@ -5,6 +5,16 @@
"jsx": "react-jsx", "jsx": "react-jsx",
"esModuleInterop": true, "esModuleInterop": true,
"target": "es2017", "target": "es2017",
"downlevelIteration": true "downlevelIteration": true,
} "typeRoots": [
"./node_modules/@types",
"./src/types"
]
},
"include": [
"src/**/*",
"App.tsx",
"index.ts",
"assets/**/*.svg"
]
} }