Merge branch 'tapframe:main' into main

This commit is contained in:
milicevicivan 2026-02-19 21:47:25 +01:00 committed by GitHub
commit 6c6f90de51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 230 additions and 72 deletions

View file

@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<img src="assets/nuviotext.png" alt="Nuvio" width="300" /> <img src="https://github.com/tapframe/NuvioTV/blob/main/assets/brand/app_logo_wordmark.png" alt="Nuvio" width="300" />
<br /> <br />
<br /> <br />

View file

@ -95,8 +95,8 @@ android {
applicationId 'com.nuvio.app' applicationId 'com.nuvio.app'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 35 versionCode 36
versionName "1.3.7" versionName "1.4.0"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
} }
@ -118,7 +118,7 @@ android {
def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4] def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
applicationVariants.all { variant -> applicationVariants.all { variant ->
variant.outputs.each { output -> variant.outputs.each { output ->
def baseVersionCode = 35 // Current versionCode 35 from defaultConfig def baseVersionCode = 36 // Current versionCode 36 from defaultConfig
def abiName = output.getFilter(com.android.build.OutputFile.ABI) def abiName = output.getFilter(com.android.build.OutputFile.ABI)
def versionCode = baseVersionCode * 100 // Base multiplier def versionCode = baseVersionCode * 100 // Base multiplier

View file

@ -3,5 +3,5 @@
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string> <string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string> <string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
<string name="expo_system_ui_user_interface_style" translatable="false">dark</string> <string name="expo_system_ui_user_interface_style" translatable="false">dark</string>
<string name="expo_runtime_version">1.3.7</string> <string name="expo_runtime_version">1.4.0</string>
</resources> </resources>

View file

@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Nuvio", "name": "Nuvio",
"slug": "nuvio", "slug": "nuvio",
"version": "1.3.7", "version": "1.4.0",
"orientation": "default", "orientation": "default",
"backgroundColor": "#020404", "backgroundColor": "#020404",
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png", "icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
@ -17,7 +17,7 @@
"ios": { "ios": {
"supportsTablet": true, "supportsTablet": true,
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png", "icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
"buildNumber": "35", "buildNumber": "36",
"infoPlist": { "infoPlist": {
"NSAppTransportSecurity": { "NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true "NSAllowsArbitraryLoads": true
@ -52,7 +52,7 @@
"android.permission.WRITE_SETTINGS" "android.permission.WRITE_SETTINGS"
], ],
"package": "com.nuvio.app", "package": "com.nuvio.app",
"versionCode": 35, "versionCode": 36,
"architectures": [ "architectures": [
"arm64-v8a", "arm64-v8a",
"armeabi-v7a", "armeabi-v7a",
@ -105,6 +105,6 @@
"fallbackToCacheTimeout": 30000, "fallbackToCacheTimeout": 30000,
"url": "https://ota.nuvioapp.space/api/manifest" "url": "https://ota.nuvioapp.space/api/manifest"
}, },
"runtimeVersion": "1.3.7" "runtimeVersion": "1.4.0"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 968 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 133 KiB

View file

@ -5,13 +5,22 @@
}, },
"build": { "build": {
"development": { "development": {
"env": {
"SENTRY_DISABLE_AUTO_UPLOAD": "true"
},
"developmentClient": true, "developmentClient": true,
"distribution": "internal" "distribution": "internal"
}, },
"preview": { "preview": {
"env": {
"SENTRY_DISABLE_AUTO_UPLOAD": "true"
},
"distribution": "internal" "distribution": "internal"
}, },
"production": { "production": {
"env": {
"SENTRY_DISABLE_AUTO_UPLOAD": "true"
},
"autoIncrement": true, "autoIncrement": true,
"extends": "apk", "extends": "apk",
"android": { "android": {
@ -21,12 +30,18 @@
} }
}, },
"release": { "release": {
"env": {
"SENTRY_DISABLE_AUTO_UPLOAD": "true"
},
"distribution": "store", "distribution": "store",
"android": { "android": {
"buildType": "app-bundle" "buildType": "app-bundle"
} }
}, },
"apk": { "apk": {
"env": {
"SENTRY_DISABLE_AUTO_UPLOAD": "true"
},
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"gradleCommand": ":app:assembleRelease" "gradleCommand": ":app:assembleRelease"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 125 KiB

View file

@ -39,7 +39,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>35</string> <string>36</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12.0</string> <string>12.0</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>

View file

@ -6,7 +6,7 @@
"start": "expo start", "start": "expo start",
"android": "expo run:android", "android": "expo run:android",
"ios": "expo run:ios", "ios": "expo run:ios",
"build": "export NODE_ENV=production && cd android && ./gradlew assembleRelease", "build": "export NODE_ENV=production && export SENTRY_DISABLE_AUTO_UPLOAD=true && cd android && ./gradlew assembleRelease",
"postinstall": "patch-package" "postinstall": "patch-package"
}, },
"dependencies": { "dependencies": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 153 KiB

View file

@ -734,7 +734,81 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
let contentResult = null; let contentResult = null;
let lastError = null; let lastError = null;
// Try with original ID first // Check if user prefers external meta addons
const preferExternal = settings.preferExternalMetaAddonDetail;
if (preferExternal) {
// Try external meta addons first
try {
console.log('🔍 [useMetadata] Trying external meta addons first');
const [content, castData] = await Promise.allSettled([
withRetry(async () => {
// Get all installed addons
const allAddons = await stremioService.getInstalledAddonsAsync();
// Find catalog addon index
const catalogAddonIndex = allAddons.findIndex(addon => addon.id === addonId);
// Filter for meta addons that are BEFORE catalog addon in priority
const externalMetaAddons = allAddons
.slice(0, catalogAddonIndex >= 0 ? catalogAddonIndex : allAddons.length)
.filter(addon => {
if (!addon.resources || !Array.isArray(addon.resources)) return false;
return addon.resources.some(resource => {
if (typeof resource === 'string') return resource === 'meta';
return (resource as any).name === 'meta';
});
});
// Try each external meta addon in priority order
for (const addon of externalMetaAddons) {
try {
const result = await withTimeout(
stremioService.getMetaDetails(type, actualId, addon.id),
API_TIMEOUT
);
if (result) {
console.log('🔍 [useMetadata] Got metadata from external addon:', addon.name);
if (actualId.startsWith('tt')) {
setImdbId(actualId);
}
return result;
}
} catch (error) {
console.log('🔍 [useMetadata] External addon failed:', addon.name, error);
continue;
}
}
// If no external addon worked, fall back to catalog addon
console.log('🔍 [useMetadata] No external meta addon worked, falling back to catalog addon');
const result = await withTimeout(
catalogService.getEnhancedContentDetails(type, actualId, addonId),
API_TIMEOUT
);
if (actualId.startsWith('tt')) {
setImdbId(actualId);
}
return result;
}),
loadCast()
]);
contentResult = content;
if (content.status === 'fulfilled' && content.value) {
console.log('🔍 [useMetadata] Successfully got metadata with external meta addon priority');
} else {
console.log('🔍 [useMetadata] External meta addon priority failed, will try fallback');
lastError = (content as any)?.reason;
}
} catch (error) {
console.log('🔍 [useMetadata] External meta addon attempt failed:', { error: error instanceof Error ? error.message : String(error) });
lastError = error;
}
} else {
// Original behavior: try with original ID first
try { try {
console.log('🔍 [useMetadata] Attempting metadata fetch with original ID:', { type, actualId, addonId }); console.log('🔍 [useMetadata] Attempting metadata fetch with original ID:', { type, actualId, addonId });
const [content, castData] = await Promise.allSettled([ const [content, castData] = await Promise.allSettled([
@ -773,6 +847,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
console.log('🔍 [useMetadata] Original ID attempt failed:', { error: error instanceof Error ? error.message : String(error) }); console.log('🔍 [useMetadata] Original ID attempt failed:', { error: error instanceof Error ? error.message : String(error) });
lastError = error; lastError = error;
} }
}
// If original TMDB ID failed and enrichment is disabled, try ID conversion as fallback // If original TMDB ID failed and enrichment is disabled, try ID conversion as fallback
if (!contentResult || (contentResult.status === 'fulfilled' && !contentResult.value) || contentResult.status === 'rejected') { if (!contentResult || (contentResult.status === 'fulfilled' && !contentResult.value) || contentResult.status === 'rejected') {
@ -1831,10 +1906,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
seasonNum = parts.pop() || ''; seasonNum = parts.pop() || '';
showIdStr = parts.join(':'); showIdStr = parts.join(':');
} else if (parts.length === 2) { } else if (parts.length === 2) {
// Edge case: maybe just id:episode? unlikely but safe fallback // For IDs like mal:57658:1, this is showId:episode (no season)
episodeNum = parts[1];
seasonNum = '1'; // Default
showIdStr = parts[0]; showIdStr = parts[0];
episodeNum = parts[1];
seasonNum = ''; // No season for this format
} }
if (__DEV__) console.log(`🔍 [loadEpisodeStreams] Parsed ID: show=${showIdStr}, s=${seasonNum}, e=${episodeNum}`); if (__DEV__) console.log(`🔍 [loadEpisodeStreams] Parsed ID: show=${showIdStr}, s=${seasonNum}, e=${episodeNum}`);
@ -1912,6 +1987,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// This handles cases where 'tt' is used for a unique episode ID directly // This handles cases where 'tt' is used for a unique episode ID directly
if (!seasonNum && !episodeNum) { if (!seasonNum && !episodeNum) {
stremioEpisodeId = episodeId; stremioEpisodeId = episodeId;
} else if (!seasonNum) {
// No season (e.g., mal:57658:1) - use id:episode format
stremioEpisodeId = `${id}:${episodeNum}`;
} else { } else {
stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`; stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
} }
@ -1923,6 +2001,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (!seasonNum && !episodeNum) { if (!seasonNum && !episodeNum) {
// Remove 'series:' prefix if present to be safe, though parsing logic above usually handles it // Remove 'series:' prefix if present to be safe, though parsing logic above usually handles it
stremioEpisodeId = episodeId.replace(/^series:/, ''); stremioEpisodeId = episodeId.replace(/^series:/, '');
} else if (!seasonNum) {
// No season (e.g., mal:57658:1) - use id:episode format
stremioEpisodeId = `${id}:${episodeNum}`;
} else { } else {
stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`; stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
} }

View file

@ -115,6 +115,8 @@ export interface AppSettings {
preferredAudioLanguage: string; // Preferred language for audio tracks (ISO 639-1 code) preferredAudioLanguage: string; // Preferred language for audio tracks (ISO 639-1 code)
subtitleSourcePreference: 'internal' | 'external' | 'any'; // Prefer internal (embedded), external (addon), or any subtitleSourcePreference: 'internal' | 'external' | 'any'; // Prefer internal (embedded), external (addon), or any
enableSubtitleAutoSelect: boolean; // Auto-select subtitles based on preferences enableSubtitleAutoSelect: boolean; // Auto-select subtitles based on preferences
// External metadata addon preference
preferExternalMetaAddonDetail: boolean; // Prefer metadata from external meta addons on detail page
} }
export const DEFAULT_SETTINGS: AppSettings = { export const DEFAULT_SETTINGS: AppSettings = {
@ -203,6 +205,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
preferredAudioLanguage: 'en', // Default to English audio preferredAudioLanguage: 'en', // Default to English audio
subtitleSourcePreference: 'internal', // Prefer internal/embedded subtitles first subtitleSourcePreference: 'internal', // Prefer internal/embedded subtitles first
enableSubtitleAutoSelect: true, // Auto-select subtitles by default enableSubtitleAutoSelect: true, // Auto-select subtitles by default
// External metadata addon preference
preferExternalMetaAddonDetail: false, // Disabled by default
}; };
const SETTINGS_STORAGE_KEY = 'app_settings'; const SETTINGS_STORAGE_KEY = 'app_settings';

View file

@ -983,6 +983,8 @@
"select_catalogs": "Select Catalogs", "select_catalogs": "Select Catalogs",
"all_catalogs": "All catalogs", "all_catalogs": "All catalogs",
"selected": "selected", "selected": "selected",
"prefer_external_meta": "Prefer External Meta Addon",
"prefer_external_meta_desc": "Use external metadata on detail page",
"hero_layout": "Hero Layout", "hero_layout": "Hero Layout",
"layout_legacy": "Legacy", "layout_legacy": "Legacy",
"layout_carousel": "Carousel", "layout_carousel": "Carousel",

View file

@ -1425,3 +1425,4 @@
} }
} }
} }
}

View file

@ -625,7 +625,6 @@
"enter_custom_key": "Voer je eigen TMDb API key in en sla op.", "enter_custom_key": "Voer je eigen TMDb API key in en sla op.",
"key_verified": "API key geverifieerd en succesvol opgeslagen." "key_verified": "API key geverifieerd en succesvol opgeslagen."
}, },
{
"settings": { "settings": {
"language": "Taal", "language": "Taal",
"select_language": "Selecteer taal", "select_language": "Selecteer taal",
@ -650,7 +649,7 @@
"slovenian": "Sloveens", "slovenian": "Sloveens",
"macedonian": "Macedonisch", "macedonian": "Macedonisch",
"russian": "Russisch", "russian": "Russisch",
"filipino": "Filipijns" "filipino": "Filipijns",
"dutch_nl": "Nederlands (Nederland)", "dutch_nl": "Nederlands (Nederland)",
"romanian": "Roemeens", "romanian": "Roemeens",
"albanian": "Albanees", "albanian": "Albanees",
@ -1029,7 +1028,6 @@
"no_upcoming_found": "Geen aankomende afleveringen gevonden", "no_upcoming_found": "Geen aankomende afleveringen gevonden",
"add_series_desc": "Voeg series toe aan je bibliotheek om ze hier te zien" "add_series_desc": "Voeg series toe aan je bibliotheek om ze hier te zien"
}, },
{
"mdblist": { "mdblist": {
"title": "Beoordelingsbronnen", "title": "Beoordelingsbronnen",
"status_disabled": "MDBList uitgeschakeld", "status_disabled": "MDBList uitgeschakeld",

View file

@ -983,6 +983,8 @@
"select_catalogs": "Wybierz katalogi", "select_catalogs": "Wybierz katalogi",
"all_catalogs": "Wszystkie katalogi", "all_catalogs": "Wszystkie katalogi",
"selected": "wybrane", "selected": "wybrane",
"prefer_external_meta": "Preferuj zewnętrzny dodatek meta",
"prefer_external_meta_desc": "Używaj zewnętrznych metadanych na stronie szczegółów",
"hero_layout": "Układ sekcji Hero", "hero_layout": "Układ sekcji Hero",
"layout_legacy": "Klasyczny", "layout_legacy": "Klasyczny",
"layout_carousel": "Karuzela", "layout_carousel": "Karuzela",

View file

@ -625,7 +625,6 @@
"enter_custom_key": "Te rugăm să introduci și să salvezi cheia personalizată.", "enter_custom_key": "Te rugăm să introduci și să salvezi cheia personalizată.",
"key_verified": "Cheia API a fost verificată și salvată cu succes." "key_verified": "Cheia API a fost verificată și salvată cu succes."
}, },
{
"settings": { "settings": {
"language": "Limbă", "language": "Limbă",
"select_language": "Selectează limba", "select_language": "Selectează limba",
@ -1029,7 +1028,6 @@
"no_upcoming_found": "Niciun episod viitor găsit", "no_upcoming_found": "Niciun episod viitor găsit",
"add_series_desc": "Adaugă seriale în bibliotecă pentru a vedea episoadele lor viitoare aici" "add_series_desc": "Adaugă seriale în bibliotecă pentru a vedea episoadele lor viitoare aici"
}, },
{
"mdblist": { "mdblist": {
"title": "Surse de evaluare", "title": "Surse de evaluare",
"status_disabled": "MDBList dezactivat", "status_disabled": "MDBList dezactivat",

View file

@ -625,7 +625,6 @@
"enter_custom_key": "Ju lutem jepni dhe ruani çelësin tuaj personal.", "enter_custom_key": "Ju lutem jepni dhe ruani çelësin tuaj personal.",
"key_verified": "Çelësi API u verifikua dhe u ruajt me sukses." "key_verified": "Çelësi API u verifikua dhe u ruajt me sukses."
}, },
{
"settings": { "settings": {
"language": "Gjuha", "language": "Gjuha",
"select_language": "Zgjidh Gjuhën", "select_language": "Zgjidh Gjuhën",
@ -973,7 +972,6 @@
"alert_disconnect_title": "Shkëput Torbox", "alert_disconnect_title": "Shkëput Torbox",
"alert_disconnect_msg": "Jeni të sigurt? Kjo do të fshijë çelësin API të ruajtur." "alert_disconnect_msg": "Jeni të sigurt? Kjo do të fshijë çelësin API të ruajtur."
}, },
{
"home_screen": { "home_screen": {
"title": "Cilësimet e Ekranit Kryesor", "title": "Cilësimet e Ekranit Kryesor",
"changes_applied": "Ndryshimet u aplikuan", "changes_applied": "Ndryshimet u aplikuan",

View file

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; 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 { mmkvStorage } from '../services/mmkvStorage';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
@ -11,7 +11,7 @@ import { useToast } from '../contexts/ToastContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__'; const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__';
const AUTH_BG_GRADIENT = ['#07090F', '#0D1020', '#140B24']; const AUTH_BG_GRADIENT = ['#07090F', '#0D1020', '#140B24'] as const;
const normalizeAuthErrorMessage = (input: string): string => { const normalizeAuthErrorMessage = (input: string): string => {
const raw = (input || '').trim(); const raw = (input || '').trim();
@ -425,6 +425,16 @@ const AuthScreen: React.FC = () => {
</View> </View>
</View> </View>
{mode === 'signin' && (
<TouchableOpacity
onPress={() => Linking.openURL('https://nuvioapp.space/account/reset-password')}
activeOpacity={0.75}
style={styles.forgotPasswordButton}
>
<Text style={[styles.forgotPasswordText, { color: currentTheme.colors.textMuted }]}>Forgot password?</Text>
</TouchableOpacity>
)}
{/* Confirm Password (signup only) */} {/* Confirm Password (signup only) */}
{mode === 'signup' && ( {mode === 'signup' && (
<View style={styles.inputContainer}> <View style={styles.inputContainer}>
@ -744,6 +754,15 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
fontWeight: '500', fontWeight: '500',
}, },
forgotPasswordButton: {
alignSelf: 'flex-end',
marginTop: -6,
marginBottom: 12,
},
forgotPasswordText: {
fontSize: 13,
fontWeight: '600',
},
}); });
export default AuthScreen; export default AuthScreen;

View file

@ -344,9 +344,22 @@ const HomeScreenSettings: React.FC = () => {
colors={colors} colors={colors}
renderControl={ChevronRight} renderControl={ChevronRight}
onPress={() => navigation.navigate('HeroCatalogs')} onPress={() => navigation.navigate('HeroCatalogs')}
isLast={true}
/> />
)} )}
<SettingItem
title={t("home_screen.prefer_external_meta")}
description={t("home_screen.prefer_external_meta_desc")}
icon="cloud-download"
isDarkMode={isDarkMode}
colors={colors}
renderControl={() => (
<CustomSwitch
value={settings.preferExternalMetaAddonDetail}
onValueChange={(value) => handleUpdateSetting('preferExternalMetaAddonDetail', value)}
/>
)}
isLast={true}
/>
</SettingsCard> </SettingsCard>
{settings.showHeroSection && ( {settings.showHeroSection && (

View file

@ -825,7 +825,11 @@ class SupabaseSyncService {
} }
private normalizeUrl(url: string): string { 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 { private toBigIntNumber(value: unknown): number {
@ -1063,14 +1067,37 @@ class SupabaseSyncService {
.map((url) => this.normalizeUrl(url)) .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 || []) { for (const row of rows || []) {
if (!row.url) continue; if (!row.url) continue;
const normalized = this.normalizeUrl(row.url); const normalized = this.normalizeUrl(row.url);
if (installedUrls.has(normalized)) continue; if (installedUrls.has(normalized)) continue;
try { 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); await stremioService.installAddon(row.url);
installedUrls.add(normalized); installedUrls.add(normalized);
if (manifest?.id) installedAddonIds.add(manifest.id);
} catch (error) { } catch (error) {
logger.warn('[SupabaseSyncService] Failed to install synced addon:', row.url, error); logger.warn('[SupabaseSyncService] Failed to install synced addon:', row.url, error);
} }

View file

@ -1,7 +1,7 @@
// Single source of truth for the app version displayed in Settings // Single source of truth for the app version displayed in Settings
// Update this when bumping app version // Update this when bumping app version
export const APP_VERSION = '1.3.7'; export const APP_VERSION = '1.4.0';
export function getDisplayedAppVersion(): string { export function getDisplayedAppVersion(): string {
return APP_VERSION; return APP_VERSION;