diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 5461cde..97e74e3 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -1346,7 +1346,7 @@ const VideoPlayer: React.FC = () => { if (type !== 'series' || !nextEpisode || duration <= 0) { if (showNextEpisodeButton) { // Hide button with animation - Animated.parallel([ +fi Animated.parallel([ Animated.timing(nextEpisodeButtonOpacity, { toValue: 0, duration: 200, diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 71786a0..2357653 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; +import { syncService } from '../services/SyncService'; import AsyncStorage from '@react-native-async-storage/async-storage'; // Simple event emitter for settings changes @@ -122,12 +123,40 @@ export const useSettings = () => { const loadSettings = async () => { try { const scope = (await AsyncStorage.getItem('@user:current')) || 'local'; - const storedSettings = await AsyncStorage.getItem(`@user:${scope}:${SETTINGS_STORAGE_KEY}`); - if (storedSettings) { - const parsedSettings = JSON.parse(storedSettings); - // Merge with defaults to ensure all properties exist - setSettings({ ...DEFAULT_SETTINGS, ...parsedSettings }); + const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`; + const [scopedJson, legacyJson] = await Promise.all([ + AsyncStorage.getItem(scopedKey), + AsyncStorage.getItem(SETTINGS_STORAGE_KEY), + ]); + const parsedScoped = scopedJson ? JSON.parse(scopedJson) : null; + const parsedLegacy = legacyJson ? JSON.parse(legacyJson) : null; + + let merged = parsedScoped || parsedLegacy; + + // Fallback: scan any existing user-scoped settings if current scope not set yet + if (!merged) { + try { + const allKeys = await AsyncStorage.getAllKeys(); + const candidateKeys = (allKeys || []).filter(k => k.endsWith(`:${SETTINGS_STORAGE_KEY}`)); + if (candidateKeys.length > 0) { + const pairs = await AsyncStorage.multiGet(candidateKeys); + for (const [, value] of pairs) { + if (value) { + try { + const candidate = JSON.parse(value); + if (candidate && typeof candidate === 'object') { + merged = candidate; + break; + } + } catch {} + } + } + } + } catch {} } + + if (merged) setSettings({ ...DEFAULT_SETTINGS, ...merged }); + else setSettings(DEFAULT_SETTINGS); } catch (error) { console.error('Failed to load settings:', error); // Fallback to default settings on error @@ -143,7 +172,14 @@ export const useSettings = () => { const newSettings = { ...settings, [key]: value }; try { const scope = (await AsyncStorage.getItem('@user:current')) || 'local'; - await AsyncStorage.setItem(`@user:${scope}:${SETTINGS_STORAGE_KEY}`, JSON.stringify(newSettings)); + const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`; + // Write to both scoped key (multi-user aware) and legacy key for backward compatibility + await Promise.all([ + AsyncStorage.setItem(scopedKey, JSON.stringify(newSettings)), + AsyncStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings)), + ]); + // Ensure a current scope exists to avoid future loads missing the chosen scope + await AsyncStorage.setItem('@user:current', scope); setSettings(newSettings); console.log(`Setting updated: ${key}`, value); @@ -152,6 +188,9 @@ export const useSettings = () => { console.log('Emitting settings change event'); settingsEmitter.emit(); } + + // If authenticated, push settings to server to prevent overwrite on next pull + try { syncService.pushSettings(); } catch {} } catch (error) { console.error('Failed to save settings:', error); } diff --git a/src/services/SyncService.ts b/src/services/SyncService.ts index 6ff452e..b36de86 100644 --- a/src/services/SyncService.ts +++ b/src/services/SyncService.ts @@ -612,7 +612,7 @@ class SyncService { try { map.set('org.stremio.opensubtitlesv3', await stremioService.getManifest('https://opensubtitles-v3.strem.io/manifest.json')); } catch {} (stremioService as any).installedAddons = map; - const order = (addons as any[]).map(a => a.addon_id); + let order = (addons as any[]).map(a => a.addon_id); const ensureFront = (arr: string[], id: string) => { const idx = arr.indexOf(id); if (idx === -1) arr.unshift(id); @@ -620,11 +620,42 @@ class SyncService { }; ensureFront(order, 'com.linvo.cinemeta'); ensureFront(order, 'org.stremio.opensubtitlesv3'); - // Keep order strictly from server after preinstalled - // Do not append missing local-only ids to avoid resurrecting removed addons + // Prefer local order if it exists; otherwise use remote + try { + const userScope = `@user:${userId}:stremio-addon-order`; + const [localScopedOrder, localLegacyOrder, localGuestOrder] = await Promise.all([ + AsyncStorage.getItem(userScope), + AsyncStorage.getItem('stremio-addon-order'), + AsyncStorage.getItem('@user:local:stremio-addon-order'), + ]); + const localOrderRaw = localScopedOrder || localLegacyOrder || localGuestOrder; + if (localOrderRaw) { + const localOrder = JSON.parse(localOrderRaw) as string[]; + // Filter to only installed ids + const localFiltered = localOrder.filter(id => map.has(id)); + if (localFiltered.length > 0) { + order = localFiltered; + } + } + } catch {} + (stremioService as any).addonOrder = order; await (stremioService as any).saveInstalledAddons(); await (stremioService as any).saveAddonOrder(); + // Push merged order to server to preserve across devices + try { + const rows = order.map((addonId: string, idx: number) => ({ + user_id: userId, + addon_id: addonId, + position: idx, + })); + const { error } = await supabase + .from('installed_addons') + .upsert(rows, { onConflict: 'user_id,addon_id' }); + if (error) logger.warn('[SyncService] push merged addon order error', error); + } catch (e) { + logger.warn('[SyncService] push merged addon order exception', e); + } } async pushWatchProgress(): Promise { diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index 9582498..6be9069 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -301,40 +301,23 @@ class StremioService { } } - // Load addon order if exists - const storedOrder = await AsyncStorage.getItem(`@user:${scope}:${this.ADDON_ORDER_KEY}`); + // Load addon order if exists (scoped first, then legacy, then @user:local for migration safety) + let storedOrder = await AsyncStorage.getItem(`@user:${scope}:${this.ADDON_ORDER_KEY}`); + if (!storedOrder) storedOrder = await AsyncStorage.getItem(this.ADDON_ORDER_KEY); + if (!storedOrder) storedOrder = await AsyncStorage.getItem(`@user:local:${this.ADDON_ORDER_KEY}`); if (storedOrder) { this.addonOrder = JSON.parse(storedOrder); // Filter out any ids that aren't in installedAddons this.addonOrder = this.addonOrder.filter(id => this.installedAddons.has(id)); } - // Ensure Cinemeta is first in the order - if (!this.addonOrder.includes(cinemetaId)) { - this.addonOrder.unshift(cinemetaId); - } else { - // Move Cinemeta to the front if it's not already there - const cinemetaIndex = this.addonOrder.indexOf(cinemetaId); - if (cinemetaIndex > 0) { - this.addonOrder.splice(cinemetaIndex, 1); - this.addonOrder.unshift(cinemetaId); - } + // Ensure required pre-installed addons are present without forcing their position + if (!this.addonOrder.includes(cinemetaId) && this.installedAddons.has(cinemetaId)) { + this.addonOrder.push(cinemetaId); + } + if (!this.addonOrder.includes(opensubsId) && this.installedAddons.has(opensubsId)) { + this.addonOrder.push(opensubsId); } - - // Ensure OpenSubtitles v3 is present right after Cinemeta (if not already ordered) - const ensureOpensubsPosition = () => { - const idx = this.addonOrder.indexOf(opensubsId); - const cinIdx = this.addonOrder.indexOf(cinemetaId); - if (idx === -1) { - // Insert after Cinemeta - this.addonOrder.splice(cinIdx + 1, 0, opensubsId); - } else if (idx <= cinIdx) { - // Move it to right after Cinemeta - this.addonOrder.splice(idx, 1); - this.addonOrder.splice(cinIdx + 1, 0, opensubsId); - } - }; - ensureOpensubsPosition(); // Add any missing addons to the order const installedIds = Array.from(this.installedAddons.keys()); @@ -399,7 +382,11 @@ class StremioService { private async saveAddonOrder(): Promise { try { const scope = (await AsyncStorage.getItem('@user:current')) || 'local'; - await AsyncStorage.setItem(`@user:${scope}:${this.ADDON_ORDER_KEY}`, JSON.stringify(this.addonOrder)); + // Write to both scoped and legacy keys for compatibility + await Promise.all([ + AsyncStorage.setItem(`@user:${scope}:${this.ADDON_ORDER_KEY}`, JSON.stringify(this.addonOrder)), + AsyncStorage.setItem(this.ADDON_ORDER_KEY, JSON.stringify(this.addonOrder)), + ]); } catch (error) { logger.error('Failed to save addon order:', error); } @@ -446,6 +433,7 @@ class StremioService { await this.saveInstalledAddons(); await this.saveAddonOrder(); + try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {} // Emit an event that an addon was added addonEmitter.emit(ADDON_EVENTS.ADDON_ADDED, manifest.id); } else { @@ -466,6 +454,7 @@ class StremioService { this.addonOrder = this.addonOrder.filter(addonId => addonId !== id); this.saveInstalledAddons(); this.saveAddonOrder(); + try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {} // Emit an event that an addon was removed addonEmitter.emit(ADDON_EVENTS.ADDON_REMOVED, id); } @@ -1238,6 +1227,8 @@ class StremioService { [this.addonOrder[index - 1], this.addonOrder[index]] = [this.addonOrder[index], this.addonOrder[index - 1]]; this.saveAddonOrder(); + // Immediately push to server to avoid resets on restart + try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {} // Emit an event that the order has changed addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED); return true; @@ -1252,6 +1243,8 @@ class StremioService { [this.addonOrder[index], this.addonOrder[index + 1]] = [this.addonOrder[index + 1], this.addonOrder[index]]; this.saveAddonOrder(); + // Immediately push to server to avoid resets on restart + try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {} // Emit an event that the order has changed addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED); return true;