diff --git a/android/.kotlin/sessions/kotlin-compiler-17947464936783636493.salive b/android/.kotlin/sessions/kotlin-compiler-17947464936783636493.salive
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/screens/AuthScreen.tsx b/src/screens/AuthScreen.tsx
index 9bd9ef84..4033429f 100644
--- a/src/screens/AuthScreen.tsx
+++ b/src/screens/AuthScreen.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { View, TextInput, Text, TouchableOpacity, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Animated, Easing, Keyboard, StatusBar, useWindowDimensions } from 'react-native';
+import { View, TextInput, Text, TouchableOpacity, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Animated, Easing, Keyboard, StatusBar, useWindowDimensions, Linking } from 'react-native';
import { mmkvStorage } from '../services/mmkvStorage';
import { LinearGradient } from 'expo-linear-gradient';
import { MaterialIcons } from '@expo/vector-icons';
@@ -425,6 +425,16 @@ const AuthScreen: React.FC = () => {
+ {mode === 'signin' && (
+ Linking.openURL('https://nuvioapp.space/account/reset-password')}
+ activeOpacity={0.75}
+ style={styles.forgotPasswordButton}
+ >
+ Forgot password?
+
+ )}
+
{/* Confirm Password (signup only) */}
{mode === 'signup' && (
@@ -744,6 +754,15 @@ const styles = StyleSheet.create({
fontSize: 14,
fontWeight: '500',
},
+ forgotPasswordButton: {
+ alignSelf: 'flex-end',
+ marginTop: -6,
+ marginBottom: 12,
+ },
+ forgotPasswordText: {
+ fontSize: 13,
+ fontWeight: '600',
+ },
});
export default AuthScreen;
diff --git a/src/services/supabaseSyncService.ts b/src/services/supabaseSyncService.ts
index 2134edbc..6c4265bb 100644
--- a/src/services/supabaseSyncService.ts
+++ b/src/services/supabaseSyncService.ts
@@ -825,7 +825,11 @@ class SupabaseSyncService {
}
private normalizeUrl(url: string): string {
- return url.trim().toLowerCase();
+ let u = url.trim().toLowerCase();
+
+ u = u.replace(/\/manifest\.json\/?$/i, '');
+ u = u.replace(/\/+$/, '');
+ return u;
}
private toBigIntNumber(value: unknown): number {
@@ -1063,14 +1067,37 @@ class SupabaseSyncService {
.map((url) => this.normalizeUrl(url))
);
+ // Build a set of currently-installed addon manifest IDs so we can also
+ // skip by ID (prevents duplicate installations of stream-providing addons
+ // that the URL check alone might miss due to URL format differences).
+ const installedAddonIds = new Set(
+ installed.map((addon) => addon.id).filter(Boolean)
+ );
+
for (const row of rows || []) {
if (!row.url) continue;
const normalized = this.normalizeUrl(row.url);
if (installedUrls.has(normalized)) continue;
try {
+ // Pre-check: fetch manifest to see if this addon ID is already installed.
+ // This prevents creating duplicate installations for stream-providing
+ // addons whose URLs differ only by format (e.g. with/without manifest.json).
+ let manifest: Manifest | null = null;
+ try {
+ manifest = await stremioService.getManifest(row.url);
+ } catch {
+ // If manifest fetch fails, fall through to installAddon which will also fail and be caught below.
+ }
+ if (manifest?.id && installedAddonIds.has(manifest.id)) {
+ // Addon already installed under a different URL variant — skip.
+ logger.log(`[SupabaseSyncService] pullAddonsToLocal: skipping duplicate addon id=${manifest.id} url=${row.url}`);
+ installedUrls.add(normalized);
+ continue;
+ }
await stremioService.installAddon(row.url);
installedUrls.add(normalized);
+ if (manifest?.id) installedAddonIds.add(manifest.id);
} catch (error) {
logger.warn('[SupabaseSyncService] Failed to install synced addon:', row.url, error);
}