mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 09:35:42 +00:00
added donor screen
This commit is contained in:
parent
4f6a150592
commit
36b2375db0
19 changed files with 601 additions and 123 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -105,3 +105,4 @@ LibTorrent/
|
|||
iTorrent/
|
||||
simkl-docss
|
||||
downloader.md
|
||||
server
|
||||
27
Transaction_All.csv
Normal file
27
Transaction_All.csv
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
DateTime (UTC),"From","Message","Item","Received","Given","Currency","TransactionType","TransactionId","Reference","SalesTax","SalesTaxPercentage","SalesTaxIncludesShipping","BuyerCountry","BuyerStateOrProvince","BuyerEmail","PaymentProvider","DiscordUsername"
|
||||
"06/24/2025 22:48","Nunie","This is from Taz just showing some love for your work","Ko-fi Support","10.00","0","USD","Tip","d23c58ab-8165-472f-9af5-af76b99fe196","T-Q5Q61GZKB0","","","","","","nunie661@gmail.com","PayPal",""
|
||||
"07/03/2025 16:17","wildkazoos","Hope this helps get the testflight going! Good luck!","Ko-fi Support","5.00","0","USD","Tip","cb61f5a7-7cbe-41b2-9456-4ec3a7fdcc5a","T-A0A21HG9NY","","","","","","kazoos.wildcat.7y@icloud.com","PayPal","wildkazoos#0"
|
||||
"07/06/2025 20:01","Nunie","Love what your doing to the community keep it up and looking forward to android tv","Ko-fi Support","10.00","0","USD","Tip","7a1dd5fd-6c42-428d-a150-600ceecd0bb9","T-G2G71HMJW5","","","","","","nunie661@gmail.com","PayPal",""
|
||||
"09/11/2025 14:03","ak061","Great app","Ko-fi Support","10.00","0","USD","Tip","c4b10e74-9c3c-49ab-b32e-9b002c34ea58","T-C0C31L3IB8","","","","","","promoemails0501@gmail.com","PayPal",""
|
||||
"09/16/2025 15:32","Gesti","","Ko-fi Support","10.00","0","USD","Tip","eb47a9a4-87ce-48bb-b78d-e2013e220b03","T-H2H61LCN1Y","","","","","","gestii97@gmail.com","PayPal",""
|
||||
"09/18/2025 23:17","MK","Keep up the good work! Much love and support!","Ko-fi Support","50.00","0","USD","Tip","0d6d63e8-312b-48e1-ad45-a9f6bf375fda","T-G2G01LH03X","","","","","","mk90@windowslive.com","PayPal","mk90.#0"
|
||||
"09/21/2025 02:43","Din","","Ko-fi Support","10.00","0","USD","Tip","751dd75f-1088-46c8-9abe-777117c105e7","T-Z8Z61LL2OW","","","","","","greksoft@ymail.com","PayPal","dinny.#0"
|
||||
"09/23/2025 15:28","TheAdamsFamily","","Ko-fi Support","5.00","0","USD","Tip","fcfee7b8-cace-4190-bf9e-6cd776a58e02","T-F1A51LPUXP","","","","","","adam.gee@live.co.uk","PayPal","Not connected"
|
||||
"10/15/2025 00:54","Capillary9835","","Ko-fi Support","5.00","0","USD","Tip","815ce820-4294-413e-9b4c-6999cf2e6de3","T-E1E41MU2KD","","","","","","07yq1ws7@duck.com","PayPal","capillary9835#0"
|
||||
"10/19/2025 14:07","oma ","thank you for the app you've made it's absolutely amazing and the fact it's free <3","Ko-fi Support","10.00","0","USD","Tip","bfa6404c-4ac3-431c-bcb1-164d049b7c8c","T-K3K11N2ANR","","","","","","omar@amgu.com","PayPal",""
|
||||
"10/30/2025 03:23","Rob 🐶","i like to support all stremio app devs, thank you!","Ko-fi Support","5.00","0","USD","Tip","29887b4c-80f6-471a-9649-b33766579a3f","T-G2G01NLYW5","","","","","","robert.koteski@hotmail.com","PayPal",""
|
||||
"11/11/2025 16:54","Ko-fi Supporter","Keep going...","Ko-fi Support","10.00","0","USD","Tip","1a96aad4-e3b5-4c83-bb08-ff797038ea09","T-F1F71OA1C7","","","","","","deepcity88@duck.com","PayPal",""
|
||||
"11/14/2025 07:43","EvanderMegaton","I want to support every developer who does something related to my favorite hobby. Thank you! There is only one problem: there is no Android TV version, and therefore, $5 is for everything you have done so far, and $5 is my investment, stimulation for the TV version of the app :)","Ko-fi Support","10.00","0","USD","Tip","c8623031-36b5-4288-8171-0bc4fbf9ee54","T-C0C21OF41Y","","","","","","velimir.saban@gmail.com","PayPal",""
|
||||
"11/19/2025 17:23","Jx","","Ko-fi Support","5.00","0","USD","Tip","fca4c0e4-cfaa-4488-8f40-84f1cd97ec8e","T-P5P11ORGGH","","","","","","xavier.bento@gmail.com","PayPal","jxb0532_89330#0"
|
||||
"12/09/2025 20:09","Marcojaco","Thanks ","Ko-fi Support","5.00","0","USD","Tip","e4fec6ba-e4db-4d7a-b580-bd8941acf52f","T-U6U71PZLCX","","","","","","marcojaco32@gmail.com","PayPal",""
|
||||
"12/15/2025 22:07","Razz","Best movies app I've seen to date!! Amazing work and long may it continue! Appreciated 👍🏻","Ko-fi Support","7.00","0","USD","Tip","b4a59c25-3eda-4219-b8ea-f0eea8048bf6","T-O4O51QCM3T","","","","","","russellcausier@gmail.com","PayPal","razzthekid82#0"
|
||||
"12/16/2025 18:58","Disc ~ george.epub","","Ko-fi Support","20.00","0","USD","Tip","870c51de-c26f-4e4c-80ae-28202325f861","T-P5P11QECFI","","","","","","georgegaines7@gmail.com","PayPal","Not connected"
|
||||
"12/24/2025 13:45","LexTutor","","Ko-fi Support","10.00","0","USD","Tip","043da538-8f03-4126-83d8-5148ecacf25b","T-U7U31QVQ69","","","","","","arditmemollathe@gmail.com","PayPal","ardit6991#0"
|
||||
"12/25/2025 13:22","Ko-fi Supporter","","Ko-fi Support","5.00","0","USD","Tip","700b1d65-3137-4984-8fc0-86921dccd008","T-F1F11QY1G7","","","","","","watsonzach12@gmail.com","PayPal",""
|
||||
"12/25/2025 22:58","Ko-fi Supporter","","Ko-fi Support","5.00","0","USD","Tip","c5935dfc-2606-47e4-a3de-e84c20ba816c","T-U7U11QZ4NK","","","","","","jason@jasonboshears.com","PayPal","Not connected"
|
||||
"12/29/2025 09:11","Grape Juice","","Ko-fi Support","5.00","0","USD","Tip","b04ac2de-282b-40a8-8f8e-561008b1b624","T-L3L31R7L7U","","","","","","grapejuice897@gmail.com","PayPal","grapedjuice#0"
|
||||
"12/30/2025 01:26","Orlando","","Ko-fi Support","5.00","0","USD","Tip","a8e12e23-2e8a-4aae-b88c-5b84a91e0525","T-M4M71R9DFY","","","","","","orlandodewalt5@gmail.com","PayPal","Not connected"
|
||||
"12/30/2025 13:22","Elyasa' Sidek","","Ko-fi Support","10.00","0","USD","Tip","6e07f94c-c7ba-42e4-8e6c-c50e7879b6aa","T-S6S11RAFG2","","","","","","elyasasidek@gmail.com","PayPal","elyasa0201#0"
|
||||
"01/14/2026 14:58","Tedz ","Thank you Bro","Ko-fi Support","5.00","0","USD","Tip","a7f04654-8f37-4f74-98af-c97b4978c20a","T-R6R61SA5CK","","","","","","long_and_wide@yahoo.com","PayPal",""
|
||||
"01/16/2026 21:34","Ko-fi Supporter","Appreciate you","Ko-fi Support","15.00","0","USD","Tip","236ecb15-ce1e-4a74-bb71-96a34b7994a7","T-M4M41SF6NB","","","","","","maingikenny@gmail.com","PayPal","Not connected"
|
||||
"01/18/2026 11:16","Sovilor","","Ko-fi Support","5.00","0","USD","Tip","31d645b5-3098-465a-94f5-40f83d73adb7","T-S6S51SIYXB","","","","","","sovilor@gmail.com","PayPal","Not connected"
|
||||
|
1
assets/lottie/ranking/bronze.json
Normal file
1
assets/lottie/ranking/bronze.json
Normal file
File diff suppressed because one or more lines are too long
1
assets/lottie/ranking/gold.json
Normal file
1
assets/lottie/ranking/gold.json
Normal file
File diff suppressed because one or more lines are too long
1
assets/lottie/ranking/silver.json
Normal file
1
assets/lottie/ranking/silver.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -820,6 +820,7 @@
|
|||
"special_mentions": "ذكر خاص",
|
||||
"tab_contributors": "المساهمون",
|
||||
"tab_special": "ذكر خاص",
|
||||
"tab_donors": "المانحون",
|
||||
"manager_role": "مدير المجتمع",
|
||||
"manager_desc": "يدير مجتمعات Discord و Reddit الخاصة بـ Nuvio",
|
||||
"sponsor_role": "راعي السيرفر",
|
||||
|
|
@ -833,6 +834,11 @@
|
|||
"gratitude_desc": "كل سطر برمجي، بلاغ عن خطأ، واقتراح يساعد في جعل Nuvio أفضل للجميع",
|
||||
"special_thanks_title": "شكر خاص",
|
||||
"special_thanks_desc": "هؤلاء الأشخاص الرائعون يساعدون في الحفاظ على مجتمع Nuvio وتشغيل السيرفرات",
|
||||
"donors_desc": "شكراً لإيمانك بما نقوم ببناؤه. دعمك يحافظ على Nuvio مجاناً وفي تحسن مستمر.",
|
||||
"latest_donations": "الأحدث",
|
||||
"leaderboard": "الترتيب",
|
||||
"loading_donors": "جاري تحميل المانحين…",
|
||||
"no_donors": "لا يوجد مانحون حتى الآن",
|
||||
"error_rate_limit": "تم تجاوز حد معدل GitHub API. يرجى المحاولة لاحقاً أو التمرير للتحديث.",
|
||||
"error_failed": "فشل تحميل المساهمين. يرجى التحقق من اتصالك بالإنترنت.",
|
||||
"retry": "حاول مرة أخرى",
|
||||
|
|
|
|||
|
|
@ -833,6 +833,12 @@
|
|||
"gratitude_desc": "Jede Zeile Code hilft",
|
||||
"special_thanks_title": "Besonderer Dank",
|
||||
"special_thanks_desc": "Diese erstaunlichen Menschen helfen",
|
||||
"donors_desc": "Danke, dass Sie an das glauben, was wir aufbauen. Ihre Unterstützung hält Nuvio kostenlos und ständig verbessert.",
|
||||
"tab_donors": "Spender",
|
||||
"latest_donations": "Aktuell",
|
||||
"leaderboard": "Bestenliste",
|
||||
"loading_donors": "Spender werden geladen…",
|
||||
"no_donors": "Noch keine Spender",
|
||||
"error_rate_limit": "GitHub API Rate Limit überschritten.",
|
||||
"error_failed": "Fehler beim Laden der Mitwirkenden.",
|
||||
"retry": "Erneut versuchen",
|
||||
|
|
|
|||
|
|
@ -820,6 +820,7 @@
|
|||
"special_mentions": "Special Mentions",
|
||||
"tab_contributors": "Contributors",
|
||||
"tab_special": "Special Mentions",
|
||||
"tab_donors": "Donors",
|
||||
"manager_role": "Community Manager",
|
||||
"manager_desc": "Manages the Discord & Reddit communities for Nuvio",
|
||||
"sponsor_role": "Server Sponsor",
|
||||
|
|
@ -833,6 +834,11 @@
|
|||
"gratitude_desc": "Each line of code, bug report, and suggestion helps make Nuvio better for everyone",
|
||||
"special_thanks_title": "Special Thanks",
|
||||
"special_thanks_desc": "These amazing people help keep the Nuvio community running and the servers online",
|
||||
"donors_desc": "Thank you for believing in what we're building. Your support keeps Nuvio free and constantly improving.",
|
||||
"latest_donations": "Latest",
|
||||
"leaderboard": "Leaderboard",
|
||||
"loading_donors": "Loading donors…",
|
||||
"no_donors": "No donors yet",
|
||||
"error_rate_limit": "GitHub API rate limit exceeded. Please try again later or pull to refresh.",
|
||||
"error_failed": "Failed to load contributors. Please check your internet connection.",
|
||||
"retry": "Try Again",
|
||||
|
|
|
|||
|
|
@ -820,6 +820,7 @@
|
|||
"special_mentions": "Menciones especiales",
|
||||
"tab_contributors": "Colaboradores",
|
||||
"tab_special": "Menciones especiales",
|
||||
"tab_donors": "Donantes",
|
||||
"manager_role": "Community Manager",
|
||||
"manager_desc": "Gestiona las comunidades de Discord y Reddit para Nuvio",
|
||||
"sponsor_role": "Patrocinador del servidor",
|
||||
|
|
@ -833,6 +834,11 @@
|
|||
"gratitude_desc": "Cada línea de código, informe de fallo y sugerencia ayuda a mejorar Nuvio para todos",
|
||||
"special_thanks_title": "Agradecimientos especiales",
|
||||
"special_thanks_desc": "Estas personas increíbles ayudan a mantener la comunidad de Nuvio en marcha y los servidores online",
|
||||
"donors_desc": "Gracias por creer en lo que estamos construyendo. Tu apoyo mantiene Nuvio gratis y en constante mejora.",
|
||||
"latest_donations": "Recientes",
|
||||
"leaderboard": "Clasificación",
|
||||
"loading_donors": "Cargando donantes…",
|
||||
"no_donors": "Sin donantes aún",
|
||||
"error_rate_limit": "Se superó el límite de la API de GitHub. Inténtalo de nuevo más tarde o desliza para actualizar.",
|
||||
"error_failed": "Error al cargar los colaboradores. Comprueba tu conexión a internet.",
|
||||
"retry": "Reintentar",
|
||||
|
|
|
|||
|
|
@ -820,6 +820,7 @@
|
|||
"special_mentions": "Mentions spéciales",
|
||||
"tab_contributors": "Contributeurs",
|
||||
"tab_special": "Mentions spéciales",
|
||||
"tab_donors": "Donateurs",
|
||||
"manager_role": "Responsable de communauté",
|
||||
"manager_desc": "Gère les communautés Discord et Reddit pour Nuvio",
|
||||
"sponsor_role": "Sponsor serveur",
|
||||
|
|
@ -833,6 +834,11 @@
|
|||
"gratitude_desc": "Chaque ligne de code, rapport de bug et suggestion aide à rendre Nuvio meilleur pour tous",
|
||||
"special_thanks_title": "Remerciements spéciaux",
|
||||
"special_thanks_desc": "Ces personnes formidables aident à faire fonctionner la communauté Nuvio et à maintenir les serveurs en ligne",
|
||||
"donors_desc": "Merci de croire en ce que nous construisons. Votre soutien garde Nuvio gratuit et en constant progrès.",
|
||||
"latest_donations": "Récents",
|
||||
"leaderboard": "Classement",
|
||||
"loading_donors": "Chargement des donateurs…",
|
||||
"no_donors": "Pas encore de donateurs",
|
||||
"error_rate_limit": "Limite de débit de l'API GitHub dépassée. Veuillez réessayer plus tard ou faire glisser pour actualiser.",
|
||||
"error_failed": "Échec du chargement des contributeurs. Veuillez vérifier votre connexion Internet.",
|
||||
"retry": "Réessayer",
|
||||
|
|
|
|||
|
|
@ -820,6 +820,7 @@
|
|||
"special_mentions": "Posebna priznanja",
|
||||
"tab_contributors": "Doprinositelji",
|
||||
"tab_special": "Posebna priznanja",
|
||||
"tab_donors": "Donatori",
|
||||
"manager_role": "Community Manager",
|
||||
"manager_desc": "Upravlja Discord i Reddit zajednicama za Nuvio",
|
||||
"sponsor_role": "Sponzor poslužitelja",
|
||||
|
|
@ -833,6 +834,11 @@
|
|||
"gratitude_desc": "Svaka linija koda, prijava greške i prijedlog pomaže učiniti Nuvio boljim za sve",
|
||||
"special_thanks_title": "Posebna zahvala",
|
||||
"special_thanks_desc": "Ovi nevjerojatni ljudi pomažu održavati Nuvio zajednicu aktivnom i poslužitelje online",
|
||||
"donors_desc": "Hvala što vjerujete u ono što gradimo. Vaša podrška čini Nuvio besplatnim i neprestano poboljšanim.",
|
||||
"latest_donations": "Nedavno",
|
||||
"leaderboard": "Ljestvica",
|
||||
"loading_donors": "Učitavanje donatora…",
|
||||
"no_donors": "Još nema donatora",
|
||||
"error_rate_limit": "Prekoračeno ograničenje GitHub API-ja. Molimo pokušajte ponovo kasnije ili povucite za osvježavanje.",
|
||||
"error_failed": "Učitavanje doprinositelja nije uspjelo. Molimo provjerite svoju internetsku vezu.",
|
||||
"retry": "Pokušaj ponovo",
|
||||
|
|
|
|||
|
|
@ -820,6 +820,7 @@
|
|||
"special_mentions": "Menzioni speciali",
|
||||
"tab_contributors": "Collaboratori",
|
||||
"tab_special": "Menzioni speciali",
|
||||
"tab_donors": "Donatori",
|
||||
"manager_role": "Community Manager",
|
||||
"manager_desc": "Gestisce le community Discord e Reddit per Nuvio",
|
||||
"sponsor_role": "Sponsor del Server",
|
||||
|
|
@ -833,6 +834,11 @@
|
|||
"gratitude_desc": "Ogni riga di codice, segnalazione di bug e suggerimento aiuta a rendere Nuvio migliore per tutti",
|
||||
"special_thanks_title": "Ringraziamenti speciali",
|
||||
"special_thanks_desc": "Queste persone fantastiche aiutano a mantenere attiva la community di Nuvio e i server online",
|
||||
"donors_desc": "Grazie per credere in quello che stiamo costruendo. Il vostro supporto mantiene Nuvio gratuito e in continuo miglioramento.",
|
||||
"latest_donations": "Recenti",
|
||||
"leaderboard": "Classifica",
|
||||
"loading_donors": "Caricamento donatori…",
|
||||
"no_donors": "Nessun donatore ancora",
|
||||
"error_rate_limit": "Limite di frequenza API GitHub superato. Riprova più tardi o trascina per aggiornare.",
|
||||
"error_failed": "Impossibile caricare i collaboratori. Controlla la tua connessione internet.",
|
||||
"retry": "Riprova",
|
||||
|
|
|
|||
|
|
@ -834,6 +834,7 @@
|
|||
"special_mentions": "Menções Especiais",
|
||||
"tab_contributors": "Contribuidores",
|
||||
"tab_special": "Menções Especiais",
|
||||
"tab_donors": "Doadores",
|
||||
"manager_role": "Gerente da Comunidade",
|
||||
"manager_desc": "Gerencia as comunidades do Discord e Reddit",
|
||||
"sponsor_role": "Patrocinador do Servidor",
|
||||
|
|
@ -847,6 +848,11 @@
|
|||
"gratitude_desc": "Cada linha de código, relatório de bug e sugestão ajuda a tornar o Nuvio melhor para todos",
|
||||
"special_thanks_title": "Agradecimentos Especiais",
|
||||
"special_thanks_desc": "Essas pessoas incríveis ajudam a manter a comunidade Nuvio funcionando e os servidores online",
|
||||
"donors_desc": "Obrigado por acreditar no que estamos construindo. Seu apoio mantém o Nuvio gratuito e continuamente melhorando.",
|
||||
"latest_donations": "Recentes",
|
||||
"leaderboard": "Placar",
|
||||
"loading_donors": "Carregando doadores…",
|
||||
"no_donors": "Sem doadores ainda",
|
||||
"error_rate_limit": "Limite de taxa da API do GitHub excedido. Tente novamente mais tarde.",
|
||||
"error_failed": "Falha ao carregar colaboradores. Verifique sua conexão com a internet.",
|
||||
"retry": "Tentar Novamente",
|
||||
|
|
|
|||
|
|
@ -834,6 +834,7 @@
|
|||
"special_mentions": "Menções Especiais",
|
||||
"tab_contributors": "Contribuidores",
|
||||
"tab_special": "Menções Especiais",
|
||||
"tab_donors": "Doadores",
|
||||
"manager_role": "Gestor da Comunidade",
|
||||
"manager_desc": "Gere as comunidades do Discord e Reddit",
|
||||
"sponsor_role": "Patrocinador do Servidor",
|
||||
|
|
@ -847,6 +848,11 @@
|
|||
"gratitude_desc": "Cada linha de código, relatório de bug e sugestão ajuda a tornar o Nuvio melhor para todos",
|
||||
"special_thanks_title": "Agradecimentos Especiais",
|
||||
"special_thanks_desc": "Essas pessoas incríveis ajudam a manter a comunidade Nuvio a funcionar e os servidores online",
|
||||
"donors_desc": "Obrigado por acreditar no que estamos a construir. O seu apoio mantém o Nuvio gratuito e continuamente a melhorar.",
|
||||
"latest_donations": "Recentes",
|
||||
"leaderboard": "Placar",
|
||||
"loading_donors": "A carregar doadores…",
|
||||
"no_donors": "Sem doadores ainda",
|
||||
"error_rate_limit": "Limite de taxa da API do GitHub excedido. Tenta novamente mais tarde.",
|
||||
"error_failed": "Falha ao carregar colaboradores. Verifica a tua conexão com a internet.",
|
||||
"retry": "Tentar Novamente",
|
||||
|
|
|
|||
|
|
@ -20,11 +20,13 @@ import { useNavigation } from '@react-navigation/native';
|
|||
import { NavigationProp } from '@react-navigation/native';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { Feather, FontAwesome5 } from '@expo/vector-icons';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { fetchContributors, GitHubContributor } from '../services/githubReleaseService';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { Donation, getDonationsWithCache } from '../services/donationsService';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
const isTablet = width >= 768;
|
||||
|
|
@ -77,7 +79,10 @@ const getSpecialMentionsConfig = (t: any) => [
|
|||
},
|
||||
];
|
||||
|
||||
type TabType = 'contributors' | 'special';
|
||||
type TabType = 'contributors' | 'special' | 'donors';
|
||||
|
||||
type DonorsTabType = 'latest' | 'leaderboard';
|
||||
|
||||
|
||||
interface ContributorCardProps {
|
||||
contributor: GitHubContributor;
|
||||
|
|
@ -237,6 +242,65 @@ const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, curren
|
|||
);
|
||||
};
|
||||
|
||||
interface DonorCardProps {
|
||||
donor: Donation;
|
||||
currentTheme: any;
|
||||
isTablet: boolean;
|
||||
}
|
||||
|
||||
const DonorCard: React.FC<DonorCardProps> = ({ donor, currentTheme, isTablet }) => {
|
||||
const formatDonationDate = (dateString?: string) => {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) return dateString;
|
||||
|
||||
return date.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.contributorCard,
|
||||
{ backgroundColor: currentTheme.colors.elevation1 },
|
||||
isTablet && styles.tabletContributorCard
|
||||
]}
|
||||
>
|
||||
<View style={styles.donorAvatar}>
|
||||
<Text style={[styles.donorAvatarText, { color: currentTheme.colors.white }]}>$</Text>
|
||||
</View>
|
||||
<View style={styles.contributorInfo}>
|
||||
<Text style={[
|
||||
styles.username,
|
||||
{ color: currentTheme.colors.highEmphasis },
|
||||
isTablet && styles.tabletUsername
|
||||
]}>
|
||||
{donor.name}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.donorAmount,
|
||||
{ color: currentTheme.colors.mediumEmphasis },
|
||||
isTablet && styles.tabletContributions
|
||||
]}>
|
||||
{donor.amount.toFixed(2)} {donor.currency} · {formatDonationDate(donor.date)}
|
||||
</Text>
|
||||
{donor.message ? (
|
||||
<Text style={[styles.donorMessage, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{donor.message}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const ContributorsScreen: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
|
|
@ -245,13 +309,105 @@ const ContributorsScreen: React.FC = () => {
|
|||
|
||||
const SPECIAL_MENTIONS_CONFIG = getSpecialMentionsConfig(t);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<TabType>('contributors');
|
||||
const [activeTab, setActiveTab] = useState<TabType>('donors');
|
||||
const [contributors, setContributors] = useState<GitHubContributor[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [specialMentions, setSpecialMentions] = useState<SpecialMention[]>([]);
|
||||
const [specialMentionsLoading, setSpecialMentionsLoading] = useState(true);
|
||||
const [donations, setDonations] = useState<Donation[]>([]);
|
||||
const [donationsLoading, setDonationsLoading] = useState(false);
|
||||
const [donationsRefreshing, setDonationsRefreshing] = useState(false);
|
||||
const [donationsError, setDonationsError] = useState<string | null>(null);
|
||||
const [donorsTab, setDonorsTab] = useState<DonorsTabType>('leaderboard');
|
||||
|
||||
const getDonationTs = useCallback((dateValue?: string) => {
|
||||
if (!dateValue) return 0;
|
||||
const ts = Date.parse(dateValue);
|
||||
return Number.isFinite(ts) ? ts : 0;
|
||||
}, []);
|
||||
|
||||
const formatDonationDate = useCallback((dateString?: string) => {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) return dateString;
|
||||
|
||||
// Use locale-aware formatting
|
||||
return date.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const latestDonations = React.useMemo(() => {
|
||||
return donations
|
||||
.slice()
|
||||
.sort((a, b) => getDonationTs(b.date) - getDonationTs(a.date));
|
||||
}, [donations, getDonationTs]);
|
||||
|
||||
const leaderboardDonations = React.useMemo(() => {
|
||||
const map = new Map<string, { name: string; currency: string; total: number; count: number; lastDate: string }>();
|
||||
for (const d of donations) {
|
||||
const name = (d.name || 'Supporter').trim() || 'Supporter';
|
||||
const currency = d.currency || 'USD';
|
||||
const key = `${name}__${currency}`;
|
||||
const existing = map.get(key);
|
||||
const amount = Number.isFinite(d.amount) ? d.amount : 0;
|
||||
const ts = getDonationTs(d.date);
|
||||
if (!existing) {
|
||||
map.set(key, {
|
||||
name,
|
||||
currency,
|
||||
total: amount,
|
||||
count: 1,
|
||||
lastDate: d.date,
|
||||
});
|
||||
} else {
|
||||
const existingTs = getDonationTs(existing.lastDate);
|
||||
map.set(key, {
|
||||
...existing,
|
||||
total: existing.total + amount,
|
||||
count: existing.count + 1,
|
||||
lastDate: ts > existingTs ? d.date : existing.lastDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = Array.from(map.values()).sort((a, b) => b.total - a.total);
|
||||
|
||||
let lastTotal: number | null = null;
|
||||
let lastRank = 0;
|
||||
|
||||
return sorted.map((entry, index) => {
|
||||
const rank = lastTotal !== null && entry.total === lastTotal ? lastRank : index + 1;
|
||||
lastTotal = entry.total;
|
||||
lastRank = rank;
|
||||
return {
|
||||
...entry,
|
||||
rank,
|
||||
};
|
||||
});
|
||||
}, [donations, getDonationTs]);
|
||||
|
||||
const getInitials = useCallback((name: string) => {
|
||||
const parts = name.trim().split(/\s+/).filter(Boolean);
|
||||
if (parts.length === 0) return 'U';
|
||||
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||||
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
|
||||
}, []);
|
||||
|
||||
const getRankAnimation = useCallback((rank: number) => {
|
||||
if (rank === 1) return require('../../assets/lottie/ranking/gold.json');
|
||||
if (rank === 2) return require('../../assets/lottie/ranking/silver.json');
|
||||
if (rank === 3) return require('../../assets/lottie/ranking/bronze.json');
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
// Fetch Discord user data for special mentions
|
||||
const loadSpecialMentions = useCallback(async () => {
|
||||
|
|
@ -309,6 +465,33 @@ const ContributorsScreen: React.FC = () => {
|
|||
}
|
||||
}, [activeTab, specialMentions.length, loadSpecialMentions]);
|
||||
|
||||
const loadDonations = useCallback(async (isRefresh = false) => {
|
||||
try {
|
||||
setDonationsError(null);
|
||||
if (isRefresh) {
|
||||
setDonationsRefreshing(true);
|
||||
} else {
|
||||
setDonationsLoading(true);
|
||||
}
|
||||
const data = await getDonationsWithCache(isRefresh);
|
||||
setDonations(Array.isArray(data) ? data : []);
|
||||
} catch (e) {
|
||||
if (__DEV__) console.error('Error loading donations:', e);
|
||||
setDonationsError('Failed to load donors.');
|
||||
setDonations([]);
|
||||
} finally {
|
||||
setDonationsLoading(false);
|
||||
setDonationsRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'donors') {
|
||||
// Force refresh on tab open so new donations appear quickly
|
||||
loadDonations(true);
|
||||
}
|
||||
}, [activeTab, loadDonations]);
|
||||
|
||||
const loadContributors = useCallback(async (isRefresh = false) => {
|
||||
try {
|
||||
if (isRefresh) {
|
||||
|
|
@ -486,6 +669,23 @@ const ContributorsScreen: React.FC = () => {
|
|||
{ backgroundColor: currentTheme.colors.elevation1 },
|
||||
isTablet && styles.tabletTabSwitcher
|
||||
]}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.tab,
|
||||
activeTab === 'donors' && { backgroundColor: currentTheme.colors.primary },
|
||||
isTablet && styles.tabletTab
|
||||
]}
|
||||
onPress={() => setActiveTab('donors')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[
|
||||
styles.tabText,
|
||||
{ color: activeTab === 'donors' ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis },
|
||||
isTablet && styles.tabletTabText
|
||||
]}>
|
||||
{t('contributors.tab_donors')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.tab,
|
||||
|
|
@ -524,7 +724,168 @@ const ContributorsScreen: React.FC = () => {
|
|||
|
||||
<View style={styles.content}>
|
||||
<View style={[styles.contentContainer, isTablet && styles.tabletContentContainer]}>
|
||||
{activeTab === 'contributors' ? (
|
||||
{activeTab === 'donors' ? (
|
||||
// Donors Tab
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={[
|
||||
styles.listContent,
|
||||
isTablet && styles.tabletListContent
|
||||
]}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={donationsRefreshing}
|
||||
onRefresh={() => loadDonations(true)}
|
||||
tintColor={currentTheme.colors.primary}
|
||||
colors={[currentTheme.colors.primary]}
|
||||
/>
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[
|
||||
styles.gratitudeCard,
|
||||
{ backgroundColor: currentTheme.colors.elevation1 },
|
||||
isTablet && styles.tabletGratitudeCard
|
||||
]}>
|
||||
<View style={styles.gratitudeContent}>
|
||||
<Feather name="gift" size={isTablet ? 32 : 24} color={currentTheme.colors.primary} />
|
||||
<Text style={[
|
||||
styles.gratitudeText,
|
||||
{ color: currentTheme.colors.highEmphasis },
|
||||
isTablet && styles.tabletGratitudeText
|
||||
]}>
|
||||
{t('contributors.tab_donors')}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.gratitudeSubtext,
|
||||
{ color: currentTheme.colors.mediumEmphasis },
|
||||
isTablet && styles.tabletGratitudeSubtext
|
||||
]}>
|
||||
{t('contributors.donors_desc')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Donors sub-tabs */}
|
||||
<View style={[
|
||||
styles.subTabSwitcher,
|
||||
{ backgroundColor: currentTheme.colors.elevation1 }
|
||||
]}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.subTab,
|
||||
donorsTab === 'leaderboard' && { backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
onPress={() => setDonorsTab('leaderboard')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[
|
||||
styles.subTabText,
|
||||
{ color: donorsTab === 'leaderboard' ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis }
|
||||
]}>
|
||||
{t('contributors.leaderboard')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.subTab,
|
||||
donorsTab === 'latest' && { backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
onPress={() => setDonorsTab('latest')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[
|
||||
styles.subTabText,
|
||||
{ color: donorsTab === 'latest' ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis }
|
||||
]}>
|
||||
{t('contributors.latest_donations')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{donationsLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: currentTheme.colors.mediumEmphasis }]}>{t('contributors.loading_donors')}</Text>
|
||||
</View>
|
||||
) : donationsError ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Feather name="alert-circle" size={48} color={currentTheme.colors.mediumEmphasis} />
|
||||
<Text style={[styles.errorText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{donationsError}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={() => loadDonations(true)}
|
||||
>
|
||||
<Text style={[styles.retryText, { color: currentTheme.colors.white }]}>{t('common.retry')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : donations.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Feather name="gift" size={48} color={currentTheme.colors.mediumEmphasis} />
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.mediumEmphasis }]}>{t('contributors.no_donors')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
donorsTab === 'latest' ? (
|
||||
latestDonations.map((donor, index) => (
|
||||
<DonorCard
|
||||
key={`${donor.name}-${donor.date}-${index}`}
|
||||
donor={donor}
|
||||
currentTheme={currentTheme}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
leaderboardDonations.map((entry, index) => (
|
||||
<View
|
||||
key={`${entry.name}-${entry.currency}-${index}`}
|
||||
style={[
|
||||
styles.leaderboardCard,
|
||||
{ backgroundColor: currentTheme.colors.elevation1 },
|
||||
isTablet && styles.tabletContributorCard
|
||||
]}
|
||||
>
|
||||
<View style={styles.leaderboardAvatar}>
|
||||
{getRankAnimation(entry.rank) ? (
|
||||
<View style={styles.leaderboardBadge}>
|
||||
<Text style={[styles.leaderboardRankText, { color: currentTheme.colors.white }]}>{entry.rank}</Text>
|
||||
<LottieView
|
||||
source={getRankAnimation(entry.rank)}
|
||||
autoPlay
|
||||
loop={false}
|
||||
style={styles.leaderboardLottie}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[styles.leaderboardRankText, { color: currentTheme.colors.white }]}>{entry.rank}</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.contributorInfo}>
|
||||
<Text style={[
|
||||
styles.username,
|
||||
{ color: currentTheme.colors.highEmphasis },
|
||||
isTablet && styles.tabletUsername
|
||||
]}>
|
||||
{entry.name}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.donorAmount,
|
||||
{ color: currentTheme.colors.mediumEmphasis },
|
||||
isTablet && styles.tabletContributions
|
||||
]}>
|
||||
{entry.total.toFixed(2)} {entry.currency} · {entry.count} {entry.count === 1 ? 'donation' : 'donations'}
|
||||
</Text>
|
||||
<Text style={[styles.donorMessage, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Rank #{entry.rank} · Last: {formatDonationDate(entry.lastDate)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)
|
||||
)}
|
||||
</ScrollView>
|
||||
) : activeTab === 'contributors' ? (
|
||||
// Contributors Tab
|
||||
<>
|
||||
{error ? (
|
||||
|
|
@ -606,7 +967,7 @@ const ContributorsScreen: React.FC = () => {
|
|||
</ScrollView>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
) : activeTab === 'special' ? (
|
||||
// Special Mentions Tab
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
|
|
@ -650,7 +1011,7 @@ const ContributorsScreen: React.FC = () => {
|
|||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -931,6 +1292,81 @@ const styles = StyleSheet.create({
|
|||
tabletTabText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
subTabSwitcher: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 16,
|
||||
padding: 4,
|
||||
borderRadius: 12,
|
||||
},
|
||||
subTab: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
subTabText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
leaderboardCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.06)',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 6,
|
||||
elevation: 4,
|
||||
},
|
||||
leaderboardAvatar: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
marginRight: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
leaderboardBadge: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
leaderboardLottie: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
leaderboardRankText: {
|
||||
position: 'absolute',
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
},
|
||||
donorAvatar: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
marginRight: 16,
|
||||
backgroundColor: DISCORD_BRAND_COLOR,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
donorAvatarText: {
|
||||
fontSize: 22,
|
||||
fontWeight: '800',
|
||||
},
|
||||
donorAmount: {
|
||||
fontSize: 14,
|
||||
marginBottom: 4,
|
||||
},
|
||||
donorMessage: {
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
|
||||
export default ContributorsScreen;
|
||||
|
|
|
|||
|
|
@ -807,6 +807,13 @@ const SettingsScreen: React.FC = () => {
|
|||
|
||||
{/* About */}
|
||||
<SettingsCard title={t('settings.about').toUpperCase()}>
|
||||
<SettingItem
|
||||
title={t('settings.items.contributors')}
|
||||
description={t('settings.items.view_contributors')}
|
||||
icon="users"
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('Contributors')}
|
||||
/>
|
||||
<SettingItem
|
||||
title={t('settings.about_nuvio')}
|
||||
description={getDisplayedAppVersion()}
|
||||
|
|
|
|||
|
|
@ -211,15 +211,7 @@ export const AboutSettingsContent: React.FC<AboutSettingsContentProps> = ({
|
|||
onPress={handleVersionTap}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title={t('settings.items.contributors')}
|
||||
description={t('settings.items.view_contributors')}
|
||||
icon="users"
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('Contributors')}
|
||||
isLast={!developerModeEnabled}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
|
||||
{developerModeEnabled && (
|
||||
<SettingItem
|
||||
title={t('settings.developer_mode.title', 'Developer Mode')}
|
||||
|
|
|
|||
53
src/services/donationsService.ts
Normal file
53
src/services/donationsService.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { mmkvStorage } from './mmkvStorage';
|
||||
|
||||
export type Donation = {
|
||||
name: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
date: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const DONATIONS_API_URL = process.env.EXPO_PUBLIC_DONATIONS_API_URL || '';
|
||||
|
||||
export async function fetchDonations(): Promise<Donation[]> {
|
||||
if (!DONATIONS_API_URL) return [];
|
||||
const res = await fetch(`${DONATIONS_API_URL.replace(/\/$/, '')}/api/donations?limit=200`);
|
||||
if (!res.ok) throw new Error(`Donations API failed: ${res.status}`);
|
||||
const json = await res.json();
|
||||
const donations = json?.donations;
|
||||
if (!Array.isArray(donations)) return [];
|
||||
return donations;
|
||||
}
|
||||
|
||||
export async function getDonationsWithCache(forceRefresh = false): Promise<Donation[]> {
|
||||
const CACHE_KEY = 'donations_cache_v1';
|
||||
const TS_KEY = 'donations_cache_ts_v1';
|
||||
const TTL_MS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
if (!forceRefresh) {
|
||||
try {
|
||||
const cached = await mmkvStorage.getItem(CACHE_KEY);
|
||||
const ts = await mmkvStorage.getItem(TS_KEY);
|
||||
if (cached && ts) {
|
||||
const age = Date.now() - parseInt(ts, 10);
|
||||
if (Number.isFinite(age) && age < TTL_MS) {
|
||||
const parsed = JSON.parse(cached);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore cache failures
|
||||
}
|
||||
}
|
||||
|
||||
const donations = await fetchDonations();
|
||||
try {
|
||||
await mmkvStorage.setItem(CACHE_KEY, JSON.stringify(donations));
|
||||
await mmkvStorage.setItem(TS_KEY, Date.now().toString());
|
||||
} catch {
|
||||
// ignore cache failures
|
||||
}
|
||||
|
||||
return donations;
|
||||
}
|
||||
|
|
@ -11,13 +11,10 @@ export class TrailerService {
|
|||
private static readonly ENV_LOCAL_BASE = process.env.EXPO_PUBLIC_TRAILER_LOCAL_BASE || 'http://46.62.173.157:3001';
|
||||
private static readonly ENV_LOCAL_TRAILER_PATH = process.env.EXPO_PUBLIC_TRAILER_LOCAL_TRAILER_PATH || '/trailer';
|
||||
private static readonly ENV_LOCAL_SEARCH_PATH = process.env.EXPO_PUBLIC_TRAILER_LOCAL_SEARCH_PATH || '/search-trailer';
|
||||
private static readonly ENV_XPRIME_URL = process.env.EXPO_PUBLIC_XPRIME_URL || 'https://db.xprime.tv/trailers';
|
||||
|
||||
private static readonly XPRIME_URL = TrailerService.ENV_XPRIME_URL;
|
||||
private static readonly LOCAL_SERVER_URL = `${TrailerService.ENV_LOCAL_BASE}${TrailerService.ENV_LOCAL_TRAILER_PATH}`;
|
||||
private static readonly AUTO_SEARCH_URL = `${TrailerService.ENV_LOCAL_BASE}${TrailerService.ENV_LOCAL_SEARCH_PATH}`;
|
||||
private static readonly TIMEOUT = 20000; // 20 seconds
|
||||
private static readonly USE_LOCAL_SERVER = true; // Toggle between local and XPrime
|
||||
|
||||
/**
|
||||
* Fetches trailer URL for a given title and year
|
||||
|
|
@ -28,20 +25,8 @@ export class TrailerService {
|
|||
* @returns Promise<string | null> - The trailer URL or null if not found
|
||||
*/
|
||||
static async getTrailerUrl(title: string, year: number, tmdbId?: string, type?: 'movie' | 'tv'): Promise<string | null> {
|
||||
logger.info('TrailerService', `getTrailerUrl requested: title="${title}", year=${year}, tmdbId=${tmdbId || 'n/a'}, type=${type || 'n/a'}, useLocal=${this.USE_LOCAL_SERVER}`);
|
||||
if (this.USE_LOCAL_SERVER) {
|
||||
// Try local server first, fallback to XPrime if it fails
|
||||
const localResult = await this.getTrailerFromLocalServer(title, year, tmdbId, type);
|
||||
if (localResult) {
|
||||
// logger.info('TrailerService', 'Returning trailer URL from local server');
|
||||
return localResult;
|
||||
}
|
||||
|
||||
logger.info('TrailerService', `Local server failed, falling back to XPrime for: ${title} (${year})`);
|
||||
return this.getTrailerFromXPrime(title, year);
|
||||
} else {
|
||||
return this.getTrailerFromXPrime(title, year);
|
||||
}
|
||||
logger.info('TrailerService', `getTrailerUrl requested: title="${title}", year=${year}, tmdbId=${tmdbId || 'n/a'}, type=${type || 'n/a'}`);
|
||||
return this.getTrailerFromLocalServer(title, year, tmdbId, type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -150,68 +135,10 @@ export class TrailerService {
|
|||
url: url
|
||||
});
|
||||
}
|
||||
return null; // Return null to trigger XPrime fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches trailer from XPrime API (original method)
|
||||
* @param title - The movie/series title
|
||||
* @param year - The release year
|
||||
* @returns Promise<string | null> - The trailer URL or null if not found
|
||||
*/
|
||||
private static async getTrailerFromXPrime(title: string, year: number): Promise<string | null> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT);
|
||||
|
||||
const url = `${this.XPRIME_URL}?title=${encodeURIComponent(title)}&year=${year}`;
|
||||
|
||||
logger.info('TrailerService', `Fetching trailer from XPrime for: ${title} (${year})`);
|
||||
logger.info('TrailerService', `XPrime request URL: ${url}`);
|
||||
logger.info('TrailerService', `XPrime timeout set to ${this.TIMEOUT}ms`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'text/plain',
|
||||
'User-Agent': 'Nuvio/1.0',
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
logger.info('TrailerService', `XPrime response: status=${response.status} ok=${response.ok}`);
|
||||
if (!response.ok) {
|
||||
logger.warn('TrailerService', `XPrime failed: ${response.status} ${response.statusText}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const trailerUrl = await response.text();
|
||||
logger.info('TrailerService', `XPrime raw URL length: ${trailerUrl ? trailerUrl.length : 0}`);
|
||||
|
||||
if (!trailerUrl || !this.isValidTrailerUrl(trailerUrl.trim())) {
|
||||
logger.warn('TrailerService', `Invalid trailer URL from XPrime: ${trailerUrl}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cleanUrl = trailerUrl.trim();
|
||||
logger.info('TrailerService', `Successfully fetched trailer from XPrime: ${cleanUrl}`);
|
||||
|
||||
return cleanUrl;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
logger.warn('TrailerService', `XPrime request timed out after ${this.TIMEOUT}ms`);
|
||||
} else {
|
||||
const msg = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
||||
logger.error('TrailerService', `Error fetching from XPrime: ${msg}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates if the provided string is a valid trailer URL
|
||||
* @param url - The URL to validate
|
||||
|
|
@ -389,42 +316,39 @@ export class TrailerService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Switch between local server and XPrime API
|
||||
* @param useLocal - true for local server, false for XPrime
|
||||
* Switch between local server (deprecated - always uses local server now)
|
||||
* @param useLocal - true for local server (always true now)
|
||||
*/
|
||||
static setUseLocalServer(useLocal: boolean): void {
|
||||
(this as any).USE_LOCAL_SERVER = useLocal;
|
||||
logger.info('TrailerService', `Switched to ${useLocal ? 'local server' : 'XPrime API'}`);
|
||||
if (!useLocal) {
|
||||
logger.warn('TrailerService', 'XPrime API is no longer supported. Always using local server.');
|
||||
}
|
||||
logger.info('TrailerService', 'Using local server');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current server status
|
||||
* @returns object with server information
|
||||
*/
|
||||
static getServerStatus(): { usingLocal: boolean; localUrl: string; xprimeUrl: string; fallbackEnabled: boolean } {
|
||||
static getServerStatus(): { usingLocal: boolean; localUrl: string } {
|
||||
return {
|
||||
usingLocal: this.USE_LOCAL_SERVER,
|
||||
usingLocal: true,
|
||||
localUrl: this.LOCAL_SERVER_URL,
|
||||
xprimeUrl: this.XPRIME_URL,
|
||||
fallbackEnabled: true // Always enabled now
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test both servers and return their status
|
||||
* Test local server and return its status
|
||||
* @returns Promise with server status information
|
||||
*/
|
||||
static async testServers(): Promise<{
|
||||
localServer: { status: 'online' | 'offline'; responseTime?: number };
|
||||
xprimeServer: { status: 'online' | 'offline'; responseTime?: number };
|
||||
}> {
|
||||
logger.info('TrailerService', 'Testing servers (local and XPrime)');
|
||||
logger.info('TrailerService', 'Testing local server');
|
||||
const results: {
|
||||
localServer: { status: 'online' | 'offline'; responseTime?: number };
|
||||
xprimeServer: { status: 'online' | 'offline'; responseTime?: number };
|
||||
} = {
|
||||
localServer: { status: 'offline' },
|
||||
xprimeServer: { status: 'offline' }
|
||||
localServer: { status: 'offline' }
|
||||
};
|
||||
|
||||
// Test local server
|
||||
|
|
@ -446,26 +370,7 @@ export class TrailerService {
|
|||
logger.warn('TrailerService', `Local server test failed: ${msg}`);
|
||||
}
|
||||
|
||||
// Test XPrime server
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const response = await fetch(`${this.XPRIME_URL}?title=test&year=2023`, {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(5000) // 5 second timeout
|
||||
});
|
||||
if (response.ok || response.status === 404) { // 404 is ok, means server is running
|
||||
results.xprimeServer = {
|
||||
status: 'online',
|
||||
responseTime: Date.now() - startTime
|
||||
};
|
||||
logger.info('TrailerService', `XPrime server online. Response time: ${results.xprimeServer.responseTime}ms`);
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
||||
logger.warn('TrailerService', `XPrime server test failed: ${msg}`);
|
||||
}
|
||||
|
||||
logger.info('TrailerService', `Server test results -> local: ${results.localServer.status}, xprime: ${results.xprimeServer.status}`);
|
||||
logger.info('TrailerService', `Server test results -> local: ${results.localServer.status}`);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue