mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 15:01:59 +00:00
Sync behaviour improvments
This commit is contained in:
parent
62defd4773
commit
abfccd0e36
3 changed files with 57 additions and 14 deletions
|
|
@ -10,6 +10,7 @@ type AccountContextValue = {
|
||||||
signIn: (email: string, password: string) => Promise<string | null>;
|
signIn: (email: string, password: string) => Promise<string | null>;
|
||||||
signUp: (email: string, password: string) => Promise<string | null>;
|
signUp: (email: string, password: string) => Promise<string | null>;
|
||||||
signOut: () => Promise<void>;
|
signOut: () => Promise<void>;
|
||||||
|
refreshCurrentUser: () => Promise<void>;
|
||||||
updateProfile: (partial: { avatarUrl?: string; displayName?: string }) => Promise<string | null>;
|
updateProfile: (partial: { avatarUrl?: string; displayName?: string }) => Promise<string | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -47,16 +48,21 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
||||||
|
|
||||||
// Auth state listener
|
// Auth state listener
|
||||||
const { data: subscription } = supabase.auth.onAuthStateChange(async (_event, session) => {
|
const { data: subscription } = supabase.auth.onAuthStateChange(async (_event, session) => {
|
||||||
const fullUser = session?.user ? await accountService.getCurrentUser() : null;
|
setLoading(true);
|
||||||
setUser(fullUser);
|
try {
|
||||||
if (fullUser) {
|
const fullUser = session?.user ? await accountService.getCurrentUser() : null;
|
||||||
await syncService.migrateLocalScopeToUser();
|
setUser(fullUser);
|
||||||
await syncService.subscribeRealtime();
|
if (fullUser) {
|
||||||
// Pull first to hydrate local state, then push to avoid wiping server with empty local
|
await syncService.migrateLocalScopeToUser();
|
||||||
await syncService.fullPull();
|
await syncService.subscribeRealtime();
|
||||||
await syncService.fullPush();
|
// Pull first to hydrate local state, then push to avoid wiping server with empty local
|
||||||
} else {
|
await syncService.fullPull();
|
||||||
syncService.unsubscribeRealtime();
|
await syncService.fullPush();
|
||||||
|
} else {
|
||||||
|
syncService.unsubscribeRealtime();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -81,6 +87,15 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
||||||
await accountService.signOut();
|
await accountService.signOut();
|
||||||
setUser(null);
|
setUser(null);
|
||||||
},
|
},
|
||||||
|
refreshCurrentUser: async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const u = await accountService.getCurrentUser();
|
||||||
|
setUser(u);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
updateProfile: async (partial) => {
|
updateProfile: async (partial) => {
|
||||||
const err = await accountService.updateProfile(partial);
|
const err = await accountService.updateProfile(partial);
|
||||||
if (!err) {
|
if (!err) {
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext();
|
const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext();
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { user, signOut, loading: accountLoading } = useAccount();
|
const { user, signOut, loading: accountLoading, refreshCurrentUser } = useAccount();
|
||||||
|
|
||||||
// Tablet-specific state
|
// Tablet-specific state
|
||||||
const [selectedCategory, setSelectedCategory] = useState('account');
|
const [selectedCategory, setSelectedCategory] = useState('account');
|
||||||
|
|
@ -248,10 +248,12 @@ const SettingsScreen: React.FC = () => {
|
||||||
if (__DEV__) console.log('SettingsScreen focused, refreshing auth status. Current state:', { isAuthenticated, userProfile: userProfile?.username });
|
if (__DEV__) console.log('SettingsScreen focused, refreshing auth status. Current state:', { isAuthenticated, userProfile: userProfile?.username });
|
||||||
}
|
}
|
||||||
refreshAuthStatus();
|
refreshAuthStatus();
|
||||||
|
// Also refresh account user in case we returned from auth flow
|
||||||
|
refreshCurrentUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [navigation, isAuthenticated, userProfile, refreshAuthStatus]);
|
}, [navigation, isAuthenticated, userProfile, refreshAuthStatus, refreshCurrentUser]);
|
||||||
|
|
||||||
// States for dynamic content
|
// States for dynamic content
|
||||||
const [addonCount, setAddonCount] = useState<number>(0);
|
const [addonCount, setAddonCount] = useState<number>(0);
|
||||||
|
|
|
||||||
|
|
@ -693,6 +693,8 @@ class SyncService {
|
||||||
(stremioService as any).addonOrder = order;
|
(stremioService as any).addonOrder = order;
|
||||||
await (stremioService as any).saveInstalledAddons();
|
await (stremioService as any).saveInstalledAddons();
|
||||||
await (stremioService as any).saveAddonOrder();
|
await (stremioService as any).saveAddonOrder();
|
||||||
|
// Mark addons initialized for this user to prevent destructive merges on first push
|
||||||
|
try { await AsyncStorage.setItem(`@user:${userId}:addons_initialized`, 'true'); } catch {}
|
||||||
// Push merged order to server to preserve across devices
|
// Push merged order to server to preserve across devices
|
||||||
try {
|
try {
|
||||||
const rows = order.map((addonId: string, idx: number) => ({
|
const rows = order.map((addonId: string, idx: number) => ({
|
||||||
|
|
@ -848,9 +850,27 @@ 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 addons = await stremioService.getInstalledAddonsAsync();
|
let addons = await stremioService.getInstalledAddonsAsync();
|
||||||
logger.log(`[Sync] push installed_addons count=${addons.length}`);
|
logger.log(`[Sync] push installed_addons count=${addons.length}`);
|
||||||
const order = (stremioService as any).addonOrder as string[];
|
let order = (stremioService as any).addonOrder as string[];
|
||||||
|
|
||||||
|
// Safety: if this is a first-time push and local addons are fewer than remote, pull before pushing
|
||||||
|
try {
|
||||||
|
const initialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true';
|
||||||
|
const { data: remoteBefore } = await supabase
|
||||||
|
.from('installed_addons')
|
||||||
|
.select('addon_id')
|
||||||
|
.eq('user_id', userId);
|
||||||
|
const remoteCount = (remoteBefore || []).length;
|
||||||
|
if (!initialized && remoteCount > addons.length) {
|
||||||
|
logger.log('[Sync] addons not initialized and local smaller than remote → pulling before push');
|
||||||
|
await this.pullAddonsSnapshot(userId);
|
||||||
|
// refresh local state after pull
|
||||||
|
addons = await stremioService.getInstalledAddonsAsync();
|
||||||
|
order = (stremioService as any).addonOrder as string[];
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
const rows = addons.map((a: any) => ({
|
const rows = addons.map((a: any) => ({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
addon_id: a.id,
|
addon_id: a.id,
|
||||||
|
|
@ -863,12 +883,17 @@ class SyncService {
|
||||||
manifest_data: a,
|
manifest_data: a,
|
||||||
}));
|
}));
|
||||||
// Delete remote addons that no longer exist locally (excluding pre-installed to be safe)
|
// Delete remote addons that no longer exist locally (excluding pre-installed to be safe)
|
||||||
|
// Guard: do not perform deletions on first-time merge when remote has more addons
|
||||||
try {
|
try {
|
||||||
const { data: remote, error: rErr } = await supabase
|
const { data: remote, error: rErr } = await supabase
|
||||||
.from('installed_addons')
|
.from('installed_addons')
|
||||||
.select('addon_id')
|
.select('addon_id')
|
||||||
.eq('user_id', userId);
|
.eq('user_id', userId);
|
||||||
if (!rErr && remote) {
|
if (!rErr && remote) {
|
||||||
|
const initialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true';
|
||||||
|
if (!initialized && (remote as any[]).length > addons.length) {
|
||||||
|
logger.log('[Sync] skipping deletions during first-time addon merge');
|
||||||
|
} else {
|
||||||
const localIds = new Set(addons.map((a: any) => a.id));
|
const localIds = new Set(addons.map((a: any) => a.id));
|
||||||
const toDeletePromises = (remote as any[])
|
const toDeletePromises = (remote as any[])
|
||||||
.map(r => r.addon_id as string)
|
.map(r => r.addon_id as string)
|
||||||
|
|
@ -894,6 +919,7 @@ class SyncService {
|
||||||
.in('addon_id', toDelete);
|
.in('addon_id', toDelete);
|
||||||
if (del.error && __DEV__) console.warn('[SyncService] delete addons error', del.error);
|
if (del.error && __DEV__) console.warn('[SyncService] delete addons error', del.error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (__DEV__) console.warn('[SyncService] deletion sync for addons failed', e);
|
if (__DEV__) console.warn('[SyncService] deletion sync for addons failed', e);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue