mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
Merge pull request #599 from hydralauncher/feature/user-profile
feat: user profile page
This commit is contained in:
commit
571947cd23
@ -53,6 +53,7 @@
|
|||||||
"electron-log": "^5.1.4",
|
"electron-log": "^5.1.4",
|
||||||
"electron-updater": "^6.1.8",
|
"electron-updater": "^6.1.8",
|
||||||
"fetch-cookie": "^3.0.1",
|
"fetch-cookie": "^3.0.1",
|
||||||
|
"file-type": "^19.0.0",
|
||||||
"flexsearch": "^0.7.43",
|
"flexsearch": "^0.7.43",
|
||||||
"i18next": "^23.11.2",
|
"i18next": "^23.11.2",
|
||||||
"i18next-browser-languagedetector": "^7.2.1",
|
"i18next-browser-languagedetector": "^7.2.1",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "مميّز",
|
"featured": "مميّز",
|
||||||
"recently_added": "مضاف مؤخراً",
|
|
||||||
"trending": "شائع",
|
"trending": "شائع",
|
||||||
"surprise_me": "فاجئني",
|
"surprise_me": "فاجئني",
|
||||||
"no_results": "لم يتم العثور على نتائج"
|
"no_results": "لم يتم العثور على نتائج"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (متوقف)",
|
"paused": "{{title}} (متوقف)",
|
||||||
"downloading": "{{title}} ({{percentage}} - جارٍ التنزيل...)",
|
"downloading": "{{title}} ({{percentage}} - جارٍ التنزيل...)",
|
||||||
"filter": "بحث في المكتبة",
|
"filter": "بحث في المكتبة",
|
||||||
"follow_us": "تابعنا",
|
"home": "الرئيسية"
|
||||||
"home": "الرئيسية",
|
|
||||||
"discord": "انضم إلى الـDiscord الخاص بنا",
|
|
||||||
"telegram": "انضم إلى قناة Telegram الخاصة بنا",
|
|
||||||
"x": "تابعنا على X",
|
|
||||||
"github": "ساهم في مشروعنا على GitHub"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "ابحث عن الألعاب",
|
"search": "ابحث عن الألعاب",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Рэкамэндаванае",
|
"featured": "Рэкамэндаванае",
|
||||||
"recently_added": "Нядаўна дададзенае",
|
|
||||||
"trending": "Актуальнае",
|
"trending": "Актуальнае",
|
||||||
"surprise_me": "Здзіві мяне",
|
"surprise_me": "Здзіві мяне",
|
||||||
"no_results": "Няма вынікаў"
|
"no_results": "Няма вынікаў"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (Спынена)",
|
"paused": "{{title}} (Спынена)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
|
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
|
||||||
"filter": "Фільтар бібліятэкі",
|
"filter": "Фільтар бібліятэкі",
|
||||||
"follow_us": "Падпісвайцеся на нас",
|
"home": "Галоўная"
|
||||||
"home": "Галоўная",
|
|
||||||
"discord": "Далучайцеся да Discord",
|
|
||||||
"telegram": "Далучайцеся да Telegram",
|
|
||||||
"x": "Падпісвайцеся на X",
|
|
||||||
"github": "Зрабіць свой унёсак на GitHub"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Пошук",
|
"search": "Пошук",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Anbefalet",
|
"featured": "Anbefalet",
|
||||||
"recently_added": "Nyligt tilføjet",
|
|
||||||
"trending": "Trender",
|
"trending": "Trender",
|
||||||
"surprise_me": "Overrask mig",
|
"surprise_me": "Overrask mig",
|
||||||
"no_results": "Ingen resultater fundet"
|
"no_results": "Ingen resultater fundet"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (Paused)",
|
"paused": "{{title}} (Paused)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||||
"filter": "Filtrer bibliotek",
|
"filter": "Filtrer bibliotek",
|
||||||
"follow_us": "Følg os",
|
"home": "Hjem"
|
||||||
"home": "Hjem",
|
|
||||||
"discord": "Tilslut dig vores Discord",
|
|
||||||
"telegram": "Tilslut dig vores Telegram",
|
|
||||||
"x": "Følg på X",
|
|
||||||
"github": "Bidrag på GitHub"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Søg spil",
|
"search": "Søg spil",
|
||||||
|
@ -228,11 +228,21 @@
|
|||||||
"user_profile": {
|
"user_profile": {
|
||||||
"amount_hours": "{{amount}} hours",
|
"amount_hours": "{{amount}} hours",
|
||||||
"amount_minutes": "{{amount}} minutes",
|
"amount_minutes": "{{amount}} minutes",
|
||||||
"play_time": "Played for {{amount}}",
|
|
||||||
"last_time_played": "Last played {{period}}",
|
"last_time_played": "Last played {{period}}",
|
||||||
"sign_out": "Sign out",
|
"sign_out": "Sign out",
|
||||||
"activity": "Recent Activity",
|
"activity": "Recent activity",
|
||||||
"library": "Library",
|
"library": "Library",
|
||||||
"total_play_time": "Total playtime: {{amount}}"
|
"total_play_time": "Total playtime: {{amount}}",
|
||||||
|
"no_recent_activity_title": "Hmmm… nothing here",
|
||||||
|
"no_recent_activity_description": "You haven't played any games recently. It's time to change that!",
|
||||||
|
"display_name": "Display name",
|
||||||
|
"saving": "Saving",
|
||||||
|
"save": "Save",
|
||||||
|
"edit_profile": "Edit Profile",
|
||||||
|
"saved_successfully": "Saved successfully",
|
||||||
|
"try_again": "Please, try again",
|
||||||
|
"signout_modal_title": "Are you sure?",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"signout": "Sign Out"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "پیشنهادی",
|
"featured": "پیشنهادی",
|
||||||
"recently_added": "تازه اضافه شده",
|
|
||||||
"trending": "پرطرفدار",
|
"trending": "پرطرفدار",
|
||||||
"surprise_me": "سوپرایزم کن",
|
"surprise_me": "سوپرایزم کن",
|
||||||
"no_results": "اتمامای پیدا نشد"
|
"no_results": "اتمامای پیدا نشد"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (متوقف شده)",
|
"paused": "{{title}} (متوقف شده)",
|
||||||
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
|
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
|
||||||
"filter": "فیلتر کردن کتابخانه",
|
"filter": "فیلتر کردن کتابخانه",
|
||||||
"follow_us": "دنبال کردن ما",
|
"home": "خانه"
|
||||||
"home": "خانه",
|
|
||||||
"discord": "عضویت در دیسکورد ما",
|
|
||||||
"telegram": "عضویت در تلگرام ما",
|
|
||||||
"x": "دنبال کرد در ایکس",
|
|
||||||
"github": "مشارکت در گیتهاب"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "جستجوی بازیها",
|
"search": "جستجوی بازیها",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "En vedette",
|
"featured": "En vedette",
|
||||||
"recently_added": "Récemment ajouté",
|
|
||||||
"trending": "Tendance",
|
"trending": "Tendance",
|
||||||
"surprise_me": "Surprenez-moi",
|
"surprise_me": "Surprenez-moi",
|
||||||
"no_results": "Aucun résultat trouvé"
|
"no_results": "Aucun résultat trouvé"
|
||||||
@ -15,8 +14,7 @@
|
|||||||
"paused": "{{title}} (En pause)",
|
"paused": "{{title}} (En pause)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
|
||||||
"filter": "Filtrer la bibliothèque",
|
"filter": "Filtrer la bibliothèque",
|
||||||
"home": "Page d’accueil",
|
"home": "Page d’accueil"
|
||||||
"follow_us": "Suivez-nous"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Recherche",
|
"search": "Recherche",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Featured",
|
"featured": "Featured",
|
||||||
"recently_added": "Nemrég hozzáadott",
|
|
||||||
"trending": "Népszerű",
|
"trending": "Népszerű",
|
||||||
"surprise_me": "Lepj meg",
|
"surprise_me": "Lepj meg",
|
||||||
"no_results": "Nem található"
|
"no_results": "Nem található"
|
||||||
@ -15,7 +14,6 @@
|
|||||||
"paused": "{{title}} (Szünet)",
|
"paused": "{{title}} (Szünet)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
|
||||||
"filter": "Könyvtár szűrése",
|
"filter": "Könyvtár szűrése",
|
||||||
"follow_us": "Kövess minket",
|
|
||||||
"home": "Főoldal"
|
"home": "Főoldal"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Unggulan",
|
"featured": "Unggulan",
|
||||||
"recently_added": "Terbaru",
|
|
||||||
"trending": "Trending",
|
"trending": "Trending",
|
||||||
"surprise_me": "Kejutkan Saya",
|
"surprise_me": "Kejutkan Saya",
|
||||||
"no_results": "Tidak ada hasil"
|
"no_results": "Tidak ada hasil"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (Terhenti)",
|
"paused": "{{title}} (Terhenti)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Mengunduh…)",
|
"downloading": "{{title}} ({{percentage}} - Mengunduh…)",
|
||||||
"filter": "Filter koleksi",
|
"filter": "Filter koleksi",
|
||||||
"follow_us": "Ikuti kami",
|
"home": "Beranda"
|
||||||
"home": "Beranda",
|
|
||||||
"discord": "Gabung Discord kami",
|
|
||||||
"telegram": "Gabung Telegram kami",
|
|
||||||
"x": "Ikuti akun X kami",
|
|
||||||
"github": "Kontribusi di GitHub"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Pencarian",
|
"search": "Pencarian",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "In primo piano",
|
"featured": "In primo piano",
|
||||||
"recently_added": "Aggiunti di recente",
|
|
||||||
"trending": "Di tendenza",
|
"trending": "Di tendenza",
|
||||||
"surprise_me": "Sorprendimi",
|
"surprise_me": "Sorprendimi",
|
||||||
"no_results": "Nessun risultato trovato"
|
"no_results": "Nessun risultato trovato"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (In pausa)",
|
"paused": "{{title}} (In pausa)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Download…)",
|
"downloading": "{{title}} ({{percentage}} - Download…)",
|
||||||
"filter": "Filtra libreria",
|
"filter": "Filtra libreria",
|
||||||
"follow_us": "Seguici",
|
"home": "Home"
|
||||||
"home": "Home",
|
|
||||||
"discord": "Unisciti al nostro Discord",
|
|
||||||
"telegram": "Unisciti al nostro Telegram",
|
|
||||||
"x": "Segui su X",
|
|
||||||
"github": "Contribuisci su GitHub"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Cerca",
|
"search": "Cerca",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "추천",
|
"featured": "추천",
|
||||||
"recently_added": "최근 추가됨",
|
|
||||||
"trending": "인기",
|
"trending": "인기",
|
||||||
"surprise_me": "무작위 추천",
|
"surprise_me": "무작위 추천",
|
||||||
"no_results": "결과 없음"
|
"no_results": "결과 없음"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (일시 정지됨)",
|
"paused": "{{title}} (일시 정지됨)",
|
||||||
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
|
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
|
||||||
"filter": "라이브러리 정렬",
|
"filter": "라이브러리 정렬",
|
||||||
"follow_us": "공식 SNS",
|
"home": "홈"
|
||||||
"home": "홈",
|
|
||||||
"discord": "공식 디스코드",
|
|
||||||
"telegram": "공식 텔레그램",
|
|
||||||
"x": "공식 X (구 트위터)",
|
|
||||||
"github": "GitHub에서 기여하기"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "게임 검색하기",
|
"search": "게임 검색하기",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Uitgelicht",
|
"featured": "Uitgelicht",
|
||||||
"recently_added": "Recent Toegevoegd",
|
|
||||||
"trending": "Trending",
|
"trending": "Trending",
|
||||||
"surprise_me": "Verrasing",
|
"surprise_me": "Verrasing",
|
||||||
"no_results": "Geen resultaten gevonden"
|
"no_results": "Geen resultaten gevonden"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (Gepauzeerd)",
|
"paused": "{{title}} (Gepauzeerd)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||||
"filter": "Filter Bibliotheek",
|
"filter": "Filter Bibliotheek",
|
||||||
"follow_us": "volg ons",
|
"home": "Home"
|
||||||
"home": "Home",
|
|
||||||
"discord": "Volg onze Discord",
|
|
||||||
"telegram": "Volg onze Telegram",
|
|
||||||
"x": "Volg ons op X",
|
|
||||||
"github": "Contribute op GitHub"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Zoek spellen",
|
"search": "Zoek spellen",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Wyróżnione",
|
"featured": "Wyróżnione",
|
||||||
"recently_added": "Ostatnio dodane",
|
|
||||||
"trending": "Trendujące",
|
"trending": "Trendujące",
|
||||||
"surprise_me": "Zaskocz mnie",
|
"surprise_me": "Zaskocz mnie",
|
||||||
"no_results": "Nie znaleziono wyników"
|
"no_results": "Nie znaleziono wyników"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (Zatrzymano)",
|
"paused": "{{title}} (Zatrzymano)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
|
||||||
"filter": "Filtruj biblioteke",
|
"filter": "Filtruj biblioteke",
|
||||||
"follow_us": "Śledź nas",
|
"home": "Główna"
|
||||||
"home": "Główna",
|
|
||||||
"discord": "Dołącz nasz Discord",
|
|
||||||
"telegram": "Dołącz nasz Telegram",
|
|
||||||
"x": "Śledź na X",
|
|
||||||
"github": "Przyczyń się na GitHub"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Szukaj",
|
"search": "Szukaj",
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
"downloading": "{{title}} ({{percentage}} - Baixando…)",
|
"downloading": "{{title}} ({{percentage}} - Baixando…)",
|
||||||
"filter": "Filtrar biblioteca",
|
"filter": "Filtrar biblioteca",
|
||||||
"home": "Início",
|
"home": "Início",
|
||||||
"follow_us": "Acompanhe-nos",
|
|
||||||
"queued": "{{title}} (Na fila)",
|
"queued": "{{title}} (Na fila)",
|
||||||
"game_has_no_executable": "Jogo não possui executável selecionado"
|
"game_has_no_executable": "Jogo não possui executável selecionado"
|
||||||
},
|
},
|
||||||
@ -229,11 +228,21 @@
|
|||||||
"user_profile": {
|
"user_profile": {
|
||||||
"amount_hours": "{{amount}} horas",
|
"amount_hours": "{{amount}} horas",
|
||||||
"amount_minutes": "{{amount}} minutos",
|
"amount_minutes": "{{amount}} minutos",
|
||||||
"play_time": "Jogado por {{amount}}",
|
|
||||||
"last_time_played": "Jogou {{period}}",
|
"last_time_played": "Jogou {{period}}",
|
||||||
"sign_out": "Sair da conta",
|
"sign_out": "Sair da conta",
|
||||||
"activity": "Atividade Recente",
|
"activity": "Atividade recente",
|
||||||
"library": "Biblioteca",
|
"library": "Biblioteca",
|
||||||
"total_play_time": "Tempo total de jogo: {{amount}}"
|
"total_play_time": "Tempo total de jogo: {{amount}}",
|
||||||
|
"no_recent_activity_title": "Hmmm… nada por aqui",
|
||||||
|
"no_recent_activity_description": "Parece que você não jogou nada recentemente. Que tal começar agora?",
|
||||||
|
"display_name": "Nome de exibição",
|
||||||
|
"saving": "Salvando…",
|
||||||
|
"save": "Salvar",
|
||||||
|
"edit_profile": "Editar Perfil",
|
||||||
|
"saved_successfully": "Salvo com sucesso",
|
||||||
|
"try_again": "Por favor, tente novamente",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"signout": "Sair da conta",
|
||||||
|
"signout_modal_title": "Tem certeza?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Öne çıkan",
|
"featured": "Öne çıkan",
|
||||||
"recently_added": "Son eklenen",
|
|
||||||
"trending": "Popüler",
|
"trending": "Popüler",
|
||||||
"surprise_me": "Şaşırt beni",
|
"surprise_me": "Şaşırt beni",
|
||||||
"no_results": "Sonuç bulunamadı"
|
"no_results": "Sonuç bulunamadı"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (Duraklatıldı)",
|
"paused": "{{title}} (Duraklatıldı)",
|
||||||
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
|
||||||
"filter": "Kütüphaneyi filtrele",
|
"filter": "Kütüphaneyi filtrele",
|
||||||
"follow_us": "Bizi takip et",
|
"home": "Ana menü"
|
||||||
"home": "Ana menü",
|
|
||||||
"discord": "Discord'umuza katıl",
|
|
||||||
"telegram": "Telegram'umuza katıl",
|
|
||||||
"x": "X'te bizi takip et",
|
|
||||||
"github": "GitHub'da bize katkı yap"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Ara",
|
"search": "Ara",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Рекомендоване",
|
"featured": "Рекомендоване",
|
||||||
"recently_added": "Нове",
|
|
||||||
"trending": "У тренді",
|
"trending": "У тренді",
|
||||||
"surprise_me": "Здивуй мене",
|
"surprise_me": "Здивуй мене",
|
||||||
"no_results": "Результатів не знайдено"
|
"no_results": "Результатів не знайдено"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (Призупинено)",
|
"paused": "{{title}} (Призупинено)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Завантаження…)",
|
"downloading": "{{title}} ({{percentage}} - Завантаження…)",
|
||||||
"filter": "Фільтр бібліотеки",
|
"filter": "Фільтр бібліотеки",
|
||||||
"follow_us": "Підписуйтесь на нас",
|
"home": "Головна"
|
||||||
"home": "Головна",
|
|
||||||
"discord": "Приєднуйтесь до Discord",
|
|
||||||
"telegram": "Приєднуйтесь до Telegram",
|
|
||||||
"x": "Підписуйтесь на X",
|
|
||||||
"github": "Зробіть свій внесок на GitHub"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Пошук",
|
"search": "Пошук",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "特色推荐",
|
"featured": "特色推荐",
|
||||||
"recently_added": "最近添加",
|
|
||||||
"trending": "最近热门",
|
"trending": "最近热门",
|
||||||
"surprise_me": "向我推荐",
|
"surprise_me": "向我推荐",
|
||||||
"no_results": "没有找到结果"
|
"no_results": "没有找到结果"
|
||||||
@ -15,12 +14,7 @@
|
|||||||
"paused": "{{title}} (已暂停)",
|
"paused": "{{title}} (已暂停)",
|
||||||
"downloading": "{{title}} ({{percentage}} - 正在下载…)",
|
"downloading": "{{title}} ({{percentage}} - 正在下载…)",
|
||||||
"filter": "筛选游戏库",
|
"filter": "筛选游戏库",
|
||||||
"follow_us": "关注我们",
|
"home": "主页"
|
||||||
"home": "主页",
|
|
||||||
"discord": "加入我们的Discord",
|
|
||||||
"telegram": "加入我们的Telegram",
|
|
||||||
"x": "在X上关注我们",
|
|
||||||
"github": "在GitHub上贡献"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
|
@ -22,6 +22,7 @@ import "./library/open-game-installer-path";
|
|||||||
import "./library/update-executable-path";
|
import "./library/update-executable-path";
|
||||||
import "./library/remove-game";
|
import "./library/remove-game";
|
||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
|
import "./misc/is-user-logged-in";
|
||||||
import "./misc/open-external";
|
import "./misc/open-external";
|
||||||
import "./misc/show-open-dialog";
|
import "./misc/show-open-dialog";
|
||||||
import "./torrenting/cancel-game-download";
|
import "./torrenting/cancel-game-download";
|
||||||
@ -42,6 +43,7 @@ import "./download-sources/sync-download-sources";
|
|||||||
import "./auth/signout";
|
import "./auth/signout";
|
||||||
import "./user/get-user";
|
import "./user/get-user";
|
||||||
import "./profile/get-me";
|
import "./profile/get-me";
|
||||||
|
import "./profile/update-profile";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
ipcMain.handle("getVersion", () => app.getVersion());
|
ipcMain.handle("getVersion", () => app.getVersion());
|
||||||
|
8
src/main/events/misc/is-user-logged-in.ts
Normal file
8
src/main/events/misc/is-user-logged-in.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services/hydra-api";
|
||||||
|
|
||||||
|
const isUserLoggedIn = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
return HydraApi.isLoggedIn();
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("isUserLoggedIn", isUserLoggedIn);
|
63
src/main/events/profile/update-profile.ts
Normal file
63
src/main/events/profile/update-profile.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services/hydra-api";
|
||||||
|
import axios from "axios";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileTypeFromFile } from "file-type";
|
||||||
|
import { UserProfile } from "@types";
|
||||||
|
|
||||||
|
const patchUserProfile = async (
|
||||||
|
displayName: string,
|
||||||
|
profileImageUrl?: string
|
||||||
|
) => {
|
||||||
|
if (profileImageUrl) {
|
||||||
|
return HydraApi.patch("/profile", {
|
||||||
|
displayName,
|
||||||
|
profileImageUrl,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return HydraApi.patch("/profile", {
|
||||||
|
displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProfile = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
displayName: string,
|
||||||
|
newProfileImagePath: string | null
|
||||||
|
): Promise<UserProfile> => {
|
||||||
|
console.log(newProfileImagePath);
|
||||||
|
|
||||||
|
if (!newProfileImagePath) {
|
||||||
|
return (await patchUserProfile(displayName)).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(newProfileImagePath);
|
||||||
|
const fileBuffer = fs.readFileSync(newProfileImagePath);
|
||||||
|
const fileSizeInBytes = stats.size;
|
||||||
|
|
||||||
|
const profileImageUrl = await HydraApi.post(`/presigned-urls/profile-image`, {
|
||||||
|
imageExt: path.extname(newProfileImagePath).slice(1),
|
||||||
|
imageLength: fileSizeInBytes,
|
||||||
|
})
|
||||||
|
.then(async (preSignedResponse) => {
|
||||||
|
const { presignedUrl, profileImageUrl } = preSignedResponse.data;
|
||||||
|
|
||||||
|
const mimeType = await fileTypeFromFile(newProfileImagePath);
|
||||||
|
|
||||||
|
await axios.put(presignedUrl, fileBuffer, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": mimeType?.mime,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return profileImageUrl;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (await patchUserProfile(displayName, profileImageUrl)).data;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("updateProfile", updateProfile);
|
@ -2,6 +2,7 @@ import { app, BrowserWindow, net, protocol } from "electron";
|
|||||||
import updater from "electron-updater";
|
import updater from "electron-updater";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import url from "node:url";
|
||||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||||
import { DownloadManager, logger, WindowManager } from "@main/services";
|
import { DownloadManager, logger, WindowManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { dataSource } from "@main/data-source";
|
||||||
@ -51,9 +52,10 @@ if (process.defaultApp) {
|
|||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
electronApp.setAppUserModelId("site.hydralauncher.hydra");
|
electronApp.setAppUserModelId("site.hydralauncher.hydra");
|
||||||
|
|
||||||
protocol.handle("hydra", (request) =>
|
protocol.handle("local", (request) => {
|
||||||
net.fetch("file://" + request.url.slice("hydra://".length))
|
const filePath = request.url.slice("local://".length);
|
||||||
);
|
return net.fetch(url.pathToFileURL(filePath).toString());
|
||||||
|
});
|
||||||
|
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
await dataSource.runMigrations();
|
await dataSource.runMigrations();
|
||||||
|
@ -11,6 +11,8 @@ export class HydraApi {
|
|||||||
|
|
||||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5;
|
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5;
|
||||||
|
|
||||||
|
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
||||||
|
|
||||||
private static userAuth = {
|
private static userAuth = {
|
||||||
authToken: "",
|
authToken: "",
|
||||||
refreshToken: "",
|
refreshToken: "",
|
||||||
@ -32,7 +34,9 @@ export class HydraApi {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const tokenExpirationTimestamp =
|
const tokenExpirationTimestamp =
|
||||||
now.getTime() + expiresIn - this.EXPIRATION_OFFSET_IN_MS;
|
now.getTime() +
|
||||||
|
this.secondsToMilliseconds(expiresIn) -
|
||||||
|
this.EXPIRATION_OFFSET_IN_MS;
|
||||||
|
|
||||||
this.userAuth = {
|
this.userAuth = {
|
||||||
authToken: accessToken,
|
authToken: accessToken,
|
||||||
@ -119,7 +123,9 @@ export class HydraApi {
|
|||||||
const { accessToken, expiresIn } = response.data;
|
const { accessToken, expiresIn } = response.data;
|
||||||
|
|
||||||
const tokenExpirationTimestamp =
|
const tokenExpirationTimestamp =
|
||||||
now.getTime() + expiresIn - this.EXPIRATION_OFFSET_IN_MS;
|
now.getTime() +
|
||||||
|
this.secondsToMilliseconds(expiresIn) -
|
||||||
|
this.EXPIRATION_OFFSET_IN_MS;
|
||||||
|
|
||||||
this.userAuth.authToken = accessToken;
|
this.userAuth.authToken = accessToken;
|
||||||
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
||||||
|
@ -112,6 +112,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
getVersion: () => ipcRenderer.invoke("getVersion"),
|
getVersion: () => ipcRenderer.invoke("getVersion"),
|
||||||
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
|
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
|
||||||
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
|
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
|
||||||
|
isUserLoggedIn: () => ipcRenderer.invoke("isUserLoggedIn"),
|
||||||
showOpenDialog: (options: Electron.OpenDialogOptions) =>
|
showOpenDialog: (options: Electron.OpenDialogOptions) =>
|
||||||
ipcRenderer.invoke("showOpenDialog", options),
|
ipcRenderer.invoke("showOpenDialog", options),
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
@ -134,6 +135,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => ipcRenderer.invoke("getMe"),
|
getMe: () => ipcRenderer.invoke("getMe"),
|
||||||
|
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
||||||
|
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
||||||
|
|
||||||
/* User */
|
/* User */
|
||||||
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<title>Hydra</title>
|
<title>Hydra</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://cdn.losbroxas.org https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' data: https://cdn.losbroxas.org https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://*.s3.amazonaws.com https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' local: data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body style="background-color: #1c1c1c">
|
<body style="background-color: #1c1c1c">
|
||||||
|
@ -19,6 +19,8 @@ import {
|
|||||||
setUserPreferences,
|
setUserPreferences,
|
||||||
toggleDraggingDisabled,
|
toggleDraggingDisabled,
|
||||||
closeToast,
|
closeToast,
|
||||||
|
setUserDetails,
|
||||||
|
setProfileBackground,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
@ -31,7 +33,8 @@ export function App() {
|
|||||||
|
|
||||||
const { clearDownload, setLastPacket } = useDownload();
|
const { clearDownload, setLastPacket } = useDownload();
|
||||||
|
|
||||||
const { updateUser, clearUser } = useUserDetails();
|
const { fetchUserDetails, updateUserDetails, clearUserDetails } =
|
||||||
|
useUserDetails();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -73,26 +76,44 @@ export function App() {
|
|||||||
}, [clearDownload, setLastPacket, updateLibrary]);
|
}, [clearDownload, setLastPacket, updateLibrary]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateUser();
|
const cachedUserDetails = window.localStorage.getItem("userDetails");
|
||||||
}, [updateUser]);
|
|
||||||
|
if (cachedUserDetails) {
|
||||||
|
const { profileBackground, ...userDetails } =
|
||||||
|
JSON.parse(cachedUserDetails);
|
||||||
|
|
||||||
|
dispatch(setUserDetails(userDetails));
|
||||||
|
dispatch(setProfileBackground(profileBackground));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.electron.isUserLoggedIn().then((isLoggedIn) => {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
fetchUserDetails().then((response) => {
|
||||||
|
if (response) setUserDetails(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [dispatch, fetchUserDetails]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listeners = [
|
const listeners = [
|
||||||
window.electron.onSignIn(() => {
|
window.electron.onSignIn(() => {
|
||||||
updateUser();
|
fetchUserDetails().then((response) => {
|
||||||
|
if (response) updateUserDetails(response);
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
window.electron.onLibraryBatchComplete(() => {
|
window.electron.onLibraryBatchComplete(() => {
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
}),
|
}),
|
||||||
window.electron.onSignOut(() => {
|
window.electron.onSignOut(() => {
|
||||||
clearUser();
|
clearUserDetails();
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
listeners.forEach((unsubscribe) => unsubscribe());
|
listeners.forEach((unsubscribe) => unsubscribe());
|
||||||
};
|
};
|
||||||
}, [updateUser, updateLibrary, clearUser]);
|
}, [fetchUserDetails, updateUserDetails, clearUserDetails]);
|
||||||
|
|
||||||
const handleSearch = useCallback(
|
const handleSearch = useCallback(
|
||||||
(query: string) => {
|
(query: string) => {
|
||||||
|
@ -39,7 +39,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
|||||||
|
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
if (location.pathname.startsWith("/game")) return headerTitle;
|
if (location.pathname.startsWith("/game")) return headerTitle;
|
||||||
if (location.pathname.startsWith("/profile")) return headerTitle;
|
if (location.pathname.startsWith("/user")) return headerTitle;
|
||||||
if (location.pathname.startsWith("/search")) return t("search_results");
|
if (location.pathname.startsWith("/search")) return t("search_results");
|
||||||
|
|
||||||
return t(pathTitle[location.pathname]);
|
return t(pathTitle[location.pathname]);
|
||||||
|
58
src/renderer/src/components/sidebar/sidebar-profile.css.ts
Normal file
58
src/renderer/src/components/sidebar/sidebar-profile.css.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
|
export const profileButton = style({
|
||||||
|
display: "flex",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all ease 0.1s",
|
||||||
|
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||||
|
color: vars.color.muted,
|
||||||
|
borderBottom: `solid 1px ${vars.color.border}`,
|
||||||
|
boxShadow: "0px 0px 15px 0px #000000",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileButtonContent = style({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||||
|
height: "40px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileAvatar = style({
|
||||||
|
width: "35px",
|
||||||
|
height: "35px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
position: "relative",
|
||||||
|
objectFit: "cover",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileButtonInformation = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const statusBadge = style({
|
||||||
|
width: "9px",
|
||||||
|
height: "9px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: vars.color.danger,
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "-2px",
|
||||||
|
right: "-3px",
|
||||||
|
zIndex: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileButtonTitle = style({
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: vars.size.body,
|
||||||
|
});
|
@ -1,6 +1,7 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { PersonIcon } from "@primer/octicons-react";
|
import { PersonIcon } from "@primer/octicons-react";
|
||||||
import * as styles from "./sidebar.css";
|
import * as styles from "./sidebar-profile.css";
|
||||||
|
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
@ -9,12 +10,13 @@ export function SidebarProfile() {
|
|||||||
|
|
||||||
const { userDetails, profileBackground } = useUserDetails();
|
const { userDetails, profileBackground } = useUserDetails();
|
||||||
|
|
||||||
const handleClickProfile = () => {
|
const handleButtonClick = () => {
|
||||||
navigate(`/user/${userDetails!.id}`);
|
if (userDetails === null) {
|
||||||
};
|
window.electron.openExternal("https://auth.hydra.losbroxas.org");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleClickLogin = () => {
|
navigate(`/user/${userDetails!.id}`);
|
||||||
window.electron.openExternal("https://auth.hydra.losbroxas.org");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const profileButtonBackground = useMemo(() => {
|
const profileButtonBackground = useMemo(() => {
|
||||||
@ -22,36 +24,16 @@ export function SidebarProfile() {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}, [profileBackground]);
|
}, [profileBackground]);
|
||||||
|
|
||||||
if (userDetails == null) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.profileButton}
|
|
||||||
onClick={handleClickLogin}
|
|
||||||
>
|
|
||||||
<div className={styles.profileAvatar}>
|
|
||||||
<PersonIcon />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.profileButtonInformation}>
|
|
||||||
<p style={{ fontWeight: "bold" }}>Fazer login</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={styles.profileButton}
|
||||||
className={styles.profileButton}
|
style={{ background: profileButtonBackground }}
|
||||||
style={{ background: profileButtonBackground }}
|
onClick={handleButtonClick}
|
||||||
onClick={handleClickProfile}
|
>
|
||||||
>
|
<div className={styles.profileButtonContent}>
|
||||||
<div className={styles.profileAvatar}>
|
<div className={styles.profileAvatar}>
|
||||||
{userDetails.profileImageUrl ? (
|
{userDetails?.profileImageUrl ? (
|
||||||
<img
|
<img
|
||||||
className={styles.profileAvatar}
|
className={styles.profileAvatar}
|
||||||
src={userDetails.profileImageUrl}
|
src={userDetails.profileImageUrl}
|
||||||
@ -63,9 +45,11 @@ export function SidebarProfile() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.profileButtonInformation}>
|
<div className={styles.profileButtonInformation}>
|
||||||
<p style={{ fontWeight: "bold" }}>{userDetails.displayName}</p>
|
<p className={styles.profileButtonTitle}>
|
||||||
|
{userDetails ? userDetails.displayName : "Sign in"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -125,48 +125,3 @@ export const section = style({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
paddingBottom: `${SPACING_UNIT}px`,
|
paddingBottom: `${SPACING_UNIT}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profileButton = style({
|
|
||||||
display: "flex",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all ease 0.1s",
|
|
||||||
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
|
||||||
color: vars.color.muted,
|
|
||||||
borderBottom: `solid 1px ${vars.color.border}`,
|
|
||||||
boxShadow: "0px 0px 15px 0px #000000",
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const profileAvatar = style({
|
|
||||||
width: "30px",
|
|
||||||
height: "30px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
position: "relative",
|
|
||||||
objectFit: "cover",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const profileButtonInformation = style({
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const statusBadge = style({
|
|
||||||
width: "9px",
|
|
||||||
height: "9px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: vars.color.danger,
|
|
||||||
position: "absolute",
|
|
||||||
bottom: "-2px",
|
|
||||||
right: "-3px",
|
|
||||||
zIndex: "1",
|
|
||||||
});
|
|
||||||
|
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
@ -97,6 +97,7 @@ declare global {
|
|||||||
|
|
||||||
/* Misc */
|
/* Misc */
|
||||||
openExternal: (src: string) => Promise<void>;
|
openExternal: (src: string) => Promise<void>;
|
||||||
|
isUserLoggedIn: () => Promise<boolean>;
|
||||||
getVersion: () => Promise<string>;
|
getVersion: () => Promise<string>;
|
||||||
ping: () => string;
|
ping: () => string;
|
||||||
getDefaultDownloadsPath: () => Promise<string>;
|
getDefaultDownloadsPath: () => Promise<string>;
|
||||||
@ -122,6 +123,10 @@ declare global {
|
|||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => Promise<UserProfile | null>;
|
getMe: () => Promise<UserProfile | null>;
|
||||||
|
updateProfile: (
|
||||||
|
displayName: string,
|
||||||
|
newProfileImagePath: string | null
|
||||||
|
) => Promise<UserProfile>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -15,18 +15,14 @@ export const userDetailsSlice = createSlice({
|
|||||||
name: "user-details",
|
name: "user-details",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setUserDetails: (state, action: PayloadAction<UserDetails>) => {
|
setUserDetails: (state, action: PayloadAction<UserDetails | null>) => {
|
||||||
state.userDetails = action.payload;
|
state.userDetails = action.payload;
|
||||||
},
|
},
|
||||||
setProfileBackground: (state, action: PayloadAction<string>) => {
|
setProfileBackground: (state, action: PayloadAction<string | null>) => {
|
||||||
state.profileBackground = action.payload;
|
state.profileBackground = action.payload;
|
||||||
},
|
},
|
||||||
clearUserDetails: (state) => {
|
|
||||||
state.userDetails = null;
|
|
||||||
state.profileBackground = null;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setUserDetails, setProfileBackground, clearUserDetails } =
|
export const { setUserDetails, setProfileBackground } =
|
||||||
userDetailsSlice.actions;
|
userDetailsSlice.actions;
|
||||||
|
@ -2,12 +2,9 @@ import { useCallback } from "react";
|
|||||||
import { average } from "color.js";
|
import { average } from "color.js";
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from "./redux";
|
import { useAppDispatch, useAppSelector } from "./redux";
|
||||||
import {
|
import { setProfileBackground, setUserDetails } from "@renderer/features";
|
||||||
clearUserDetails,
|
|
||||||
setProfileBackground,
|
|
||||||
setUserDetails,
|
|
||||||
} from "@renderer/features";
|
|
||||||
import { darkenColor } from "@renderer/helpers";
|
import { darkenColor } from "@renderer/helpers";
|
||||||
|
import { UserDetails } from "@types";
|
||||||
|
|
||||||
export function useUserDetails() {
|
export function useUserDetails() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -16,42 +13,69 @@ export function useUserDetails() {
|
|||||||
(state) => state.userDetails
|
(state) => state.userDetails
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearUser = useCallback(async () => {
|
const clearUserDetails = useCallback(async () => {
|
||||||
dispatch(clearUserDetails());
|
dispatch(setUserDetails(null));
|
||||||
|
dispatch(setProfileBackground(null));
|
||||||
|
|
||||||
|
window.localStorage.removeItem("userDetails");
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const signOut = useCallback(async () => {
|
const signOut = useCallback(async () => {
|
||||||
clearUser();
|
clearUserDetails();
|
||||||
|
|
||||||
return window.electron.signOut();
|
return window.electron.signOut();
|
||||||
}, [clearUser]);
|
}, [clearUserDetails]);
|
||||||
|
|
||||||
const updateUser = useCallback(async () => {
|
const updateUserDetails = useCallback(
|
||||||
return window.electron.getMe().then(async (userDetails) => {
|
async (userDetails: UserDetails) => {
|
||||||
if (userDetails) {
|
dispatch(setUserDetails(userDetails));
|
||||||
dispatch(setUserDetails(userDetails));
|
|
||||||
|
|
||||||
if (userDetails.profileImageUrl) {
|
if (userDetails.profileImageUrl) {
|
||||||
const output = await average(userDetails.profileImageUrl, {
|
const output = await average(userDetails.profileImageUrl, {
|
||||||
amount: 1,
|
amount: 1,
|
||||||
format: "hex",
|
format: "hex",
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(
|
const profileBackground = `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8)})`;
|
||||||
setProfileBackground(
|
|
||||||
`linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.7)})`
|
dispatch(setProfileBackground(profileBackground));
|
||||||
)
|
|
||||||
);
|
window.localStorage.setItem(
|
||||||
}
|
"userDetails",
|
||||||
|
JSON.stringify({ ...userDetails, profileBackground })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(setProfileBackground(null));
|
||||||
|
|
||||||
|
window.localStorage.setItem("userDetails", JSON.stringify(userDetails));
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}, [dispatch]);
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchUserDetails = useCallback(async () => {
|
||||||
|
return window.electron.getMe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const patchUser = useCallback(
|
||||||
|
async (displayName: string, imageProfileUrl: string | null) => {
|
||||||
|
const response = await window.electron.updateProfile(
|
||||||
|
displayName,
|
||||||
|
imageProfileUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
return updateUserDetails(response);
|
||||||
|
},
|
||||||
|
[updateUserDetails]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userDetails,
|
userDetails,
|
||||||
updateUser,
|
fetchUserDetails,
|
||||||
signOut,
|
signOut,
|
||||||
clearUser,
|
clearUserDetails,
|
||||||
|
updateUserDetails,
|
||||||
|
patchUser,
|
||||||
profileBackground,
|
profileBackground,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
import { SPACING_UNIT } from "../../theme.css";
|
import { SPACING_UNIT } from "../../theme.css";
|
||||||
|
|
||||||
@ -17,11 +16,9 @@ export const content = style({
|
|||||||
flex: "1",
|
flex: "1",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const cards = recipe({
|
export const cards = style({
|
||||||
base: {
|
display: "grid",
|
||||||
display: "grid",
|
gridTemplateColumns: "repeat(2, 1fr)",
|
||||||
gridTemplateColumns: "repeat(2, 1fr)",
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
transition: "all ease 0.2s",
|
||||||
transition: "all ease 0.2s",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
@ -3,26 +3,35 @@ import cn from "classnames";
|
|||||||
|
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./user.css";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { useDate, useUserDetails } from "@renderer/hooks";
|
import { useDate, useUserDetails } from "@renderer/hooks";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { PersonIcon } from "@primer/octicons-react";
|
import { PersonIcon, TelescopeIcon } from "@primer/octicons-react";
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
|
import { UserEditProfileModal } from "./user-edit-modal";
|
||||||
|
import { UserSignOutModal } from "./user-signout-modal";
|
||||||
|
|
||||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
||||||
export interface ProfileContentProps {
|
export interface ProfileContentProps {
|
||||||
userProfile: UserProfile;
|
userProfile: UserProfile;
|
||||||
|
updateUserProfile: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserContent({ userProfile }: ProfileContentProps) {
|
export function UserContent({
|
||||||
|
userProfile,
|
||||||
|
updateUserProfile,
|
||||||
|
}: ProfileContentProps) {
|
||||||
const { t, i18n } = useTranslation("user_profile");
|
const { t, i18n } = useTranslation("user_profile");
|
||||||
|
|
||||||
const { userDetails, profileBackground, signOut } = useUserDetails();
|
const { userDetails, profileBackground, signOut } = useUserDetails();
|
||||||
|
|
||||||
|
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||||
|
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const numberFormatter = useMemo(() => {
|
const numberFormatter = useMemo(() => {
|
||||||
@ -54,8 +63,12 @@ export function UserContent({ userProfile }: ProfileContentProps) {
|
|||||||
navigate(buildGameDetailsPath(game));
|
navigate(buildGameDetailsPath(game));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSignout = async () => {
|
const handleEditProfile = () => {
|
||||||
await signOut();
|
setShowEditProfileModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmSignout = async () => {
|
||||||
|
signOut();
|
||||||
navigate("/");
|
navigate("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,10 +82,24 @@ export function UserContent({ userProfile }: ProfileContentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<UserEditProfileModal
|
||||||
|
visible={showEditProfileModal}
|
||||||
|
onClose={() => setShowEditProfileModal(false)}
|
||||||
|
updateUserProfile={updateUserProfile}
|
||||||
|
userProfile={userProfile}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UserSignOutModal
|
||||||
|
visible={showSignOutModal}
|
||||||
|
onClose={() => setShowSignOutModal(false)}
|
||||||
|
onConfirm={handleConfirmSignout}
|
||||||
|
/>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className={styles.profileContentBox}
|
className={styles.profileContentBox}
|
||||||
style={{
|
style={{
|
||||||
background: profileContentBoxBackground,
|
background: profileContentBoxBackground,
|
||||||
|
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.profileAvatarContainer}>
|
<div className={styles.profileAvatarContainer}>
|
||||||
@ -93,27 +120,53 @@ export function UserContent({ userProfile }: ProfileContentProps) {
|
|||||||
|
|
||||||
{isMe && (
|
{isMe && (
|
||||||
<div style={{ flex: 1, display: "flex", justifyContent: "end" }}>
|
<div style={{ flex: 1, display: "flex", justifyContent: "end" }}>
|
||||||
<Button theme="danger" onClick={handleSignout}>
|
<div
|
||||||
{t("sign_out")}
|
style={{
|
||||||
</Button>
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Button theme="outline" onClick={handleEditProfile}>
|
||||||
|
Editar perfil
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
theme="danger"
|
||||||
|
onClick={() => setShowSignOutModal(true)}
|
||||||
|
>
|
||||||
|
{t("sign_out")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className={styles.profileContent}>
|
<div className={styles.profileContent}>
|
||||||
<div className={styles.profileGameSection}>
|
<div className={styles.profileGameSection}>
|
||||||
<div>
|
<h2>{t("activity")}</h2>
|
||||||
<h2>{t("activity")}</h2>
|
|
||||||
</div>
|
{!userProfile.recentGames.length ? (
|
||||||
<div
|
<div className={styles.noDownloads}>
|
||||||
style={{
|
<div className={styles.telescopeIcon}>
|
||||||
display: "flex",
|
<TelescopeIcon size={24} />
|
||||||
flexDirection: "column",
|
</div>
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
<h2>{t("no_recent_activity_title")}</h2>
|
||||||
}}
|
<p style={{ fontFamily: "Fira Sans" }}>
|
||||||
>
|
{t("no_recent_activity_description")}
|
||||||
{userProfile.recentGames.map((game) => {
|
</p>
|
||||||
return (
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{userProfile.recentGames.map((game) => (
|
||||||
<button
|
<button
|
||||||
key={game.objectID}
|
key={game.objectID}
|
||||||
className={cn(styles.feedItem, styles.profileContentBox)}
|
className={cn(styles.feedItem, styles.profileContentBox)}
|
||||||
@ -139,9 +192,9 @@ export function UserContent({ userProfile }: ProfileContentProps) {
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
))}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn(styles.contentSidebar, styles.profileGameSection)}>
|
<div className={cn(styles.contentSidebar, styles.profileGameSection)}>
|
||||||
@ -170,33 +223,28 @@ export function UserContent({ userProfile }: ProfileContentProps) {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "auto auto auto",
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{userProfile.libraryGames.map((game) => {
|
{userProfile.libraryGames.map((game) => (
|
||||||
return (
|
<button
|
||||||
<button
|
key={game.objectID}
|
||||||
key={game.objectID}
|
className={cn(styles.gameListItem, styles.profileContentBox)}
|
||||||
className={cn(styles.gameListItem, styles.profileContentBox)}
|
onClick={() => handleGameClick(game)}
|
||||||
style={{
|
title={game.title}
|
||||||
padding: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
>
|
||||||
}}
|
{game.iconUrl ? (
|
||||||
onClick={() => handleGameClick(game)}
|
<img
|
||||||
title={game.title}
|
className={styles.libraryGameIcon}
|
||||||
>
|
src={game.iconUrl}
|
||||||
{game.iconUrl ? (
|
alt={game.title}
|
||||||
<img
|
/>
|
||||||
className={styles.libraryGameIcon}
|
) : (
|
||||||
src={game.iconUrl}
|
<SteamLogo className={styles.libraryGameIcon} />
|
||||||
alt={game.title}
|
)}
|
||||||
/>
|
</button>
|
||||||
) : (
|
))}
|
||||||
<SteamLogo className={styles.libraryGameIcon} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
147
src/renderer/src/pages/user/user-edit-modal.tsx
Normal file
147
src/renderer/src/pages/user/user-edit-modal.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { Button, Modal, TextField } from "@renderer/components";
|
||||||
|
import { UserProfile } from "@types";
|
||||||
|
import * as styles from "./user.css";
|
||||||
|
import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export interface UserEditProfileModalProps {
|
||||||
|
userProfile: UserProfile;
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
updateUserProfile: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserEditProfileModal = ({
|
||||||
|
userProfile,
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
updateUserProfile,
|
||||||
|
}: UserEditProfileModalProps) => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
const [displayName, setDisplayName] = useState("");
|
||||||
|
const [newImagePath, setNewImagePath] = useState<string | null>(null);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
const { patchUser } = useUserDetails();
|
||||||
|
|
||||||
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisplayName(userProfile.displayName);
|
||||||
|
}, [userProfile.displayName]);
|
||||||
|
|
||||||
|
const handleChangeProfileAvatar = async () => {
|
||||||
|
const { filePaths } = await window.electron.showOpenDialog({
|
||||||
|
properties: ["openFile"],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "Image",
|
||||||
|
extensions: ["jpg", "jpeg", "png", "gif", "webp", "bmp"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePaths && filePaths.length > 0) {
|
||||||
|
const path = filePaths[0];
|
||||||
|
|
||||||
|
setNewImagePath(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveProfile: React.FormEventHandler<HTMLFormElement> = async (
|
||||||
|
event
|
||||||
|
) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
patchUser(displayName, newImagePath)
|
||||||
|
.then(async () => {
|
||||||
|
await updateUserProfile();
|
||||||
|
showSuccessToast(t("saved_successfully"));
|
||||||
|
cleanFormAndClose();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showErrorToast(t("try_again"));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsSaving(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetModal = () => {
|
||||||
|
setDisplayName(userProfile.displayName);
|
||||||
|
setNewImagePath(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanFormAndClose = () => {
|
||||||
|
resetModal();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const avatarUrl = useMemo(() => {
|
||||||
|
if (newImagePath) return `local:${newImagePath}`;
|
||||||
|
if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
|
||||||
|
return null;
|
||||||
|
}, [newImagePath, userProfile.profileImageUrl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("edit_profile")}
|
||||||
|
onClose={cleanFormAndClose}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSaveProfile}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
|
width: "350px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.profileAvatarEditContainer}
|
||||||
|
onClick={handleChangeProfileAvatar}
|
||||||
|
>
|
||||||
|
{avatarUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.profileAvatar}
|
||||||
|
alt={userProfile.displayName}
|
||||||
|
src={avatarUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={96} />
|
||||||
|
)}
|
||||||
|
<div className={styles.editProfileImageBadge}>
|
||||||
|
<DeviceCameraIcon size={16} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={t("display_name")}
|
||||||
|
value={displayName}
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
containerProps={{ style: { width: "100%" } }}
|
||||||
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={isSaving}
|
||||||
|
style={{ alignSelf: "end" }}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{isSaving ? t("saving") : t("save")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
37
src/renderer/src/pages/user/user-signout-modal.tsx
Normal file
37
src/renderer/src/pages/user/user-signout-modal.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Button, Modal } from "@renderer/components";
|
||||||
|
import * as styles from "./user.css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export interface UserEditProfileModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserSignOutModal = ({
|
||||||
|
visible,
|
||||||
|
onConfirm,
|
||||||
|
onClose,
|
||||||
|
}: UserEditProfileModalProps) => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("signout_modal_title")}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className={styles.signOutModalButtonsContainer}>
|
||||||
|
<Button onClick={onConfirm} theme="outline">
|
||||||
|
{t("signout")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={onClose} theme="primary">
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,13 +1,40 @@
|
|||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
|
import cn from "classnames";
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./user.css";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const UserSkeleton = () => {
|
export const UserSkeleton = () => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Skeleton className={styles.profileHeaderSkeleton} />
|
<Skeleton className={styles.profileHeaderSkeleton} />
|
||||||
<div className={styles.profileContent}>
|
<div className={styles.profileContent}>
|
||||||
<Skeleton height={140} style={{ flex: 1 }} />
|
<div className={styles.profileGameSection}>
|
||||||
<Skeleton width={300} className={styles.contentSidebar} />
|
<h2>{t("activity")}</h2>
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<Skeleton
|
||||||
|
key={index}
|
||||||
|
height={72}
|
||||||
|
style={{ flex: "1", width: "100%" }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cn(styles.contentSidebar, styles.profileGameSection)}>
|
||||||
|
<h2>{t("library")}</h2>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 8 }).map((_, index) => (
|
||||||
|
<Skeleton key={index} style={{ aspectRatio: "1" }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,7 @@ import { style } from "@vanilla-extract/css";
|
|||||||
export const wrapper = style({
|
export const wrapper = style({
|
||||||
padding: "24px",
|
padding: "24px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
@ -12,12 +13,10 @@ export const wrapper = style({
|
|||||||
export const profileContentBox = style({
|
export const profileContentBox = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 2}px`,
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
border: `solid 1px ${vars.color.border}`,
|
border: `solid 1px ${vars.color.border}`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
overflow: "hidden",
|
|
||||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.7)",
|
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.7)",
|
||||||
transition: "all ease 0.3s",
|
transition: "all ease 0.3s",
|
||||||
});
|
});
|
||||||
@ -36,10 +35,38 @@ export const profileAvatarContainer = style({
|
|||||||
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const profileAvatarEditContainer = style({
|
||||||
|
width: "128px",
|
||||||
|
height: "128px",
|
||||||
|
display: "flex",
|
||||||
|
borderRadius: "50%",
|
||||||
|
color: vars.color.body,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
position: "relative",
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||||
|
cursor: "pointer",
|
||||||
|
});
|
||||||
|
|
||||||
export const profileAvatar = style({
|
export const profileAvatar = style({
|
||||||
width: "96px",
|
height: "100%",
|
||||||
height: "96px",
|
width: "100%",
|
||||||
|
borderRadius: "50%",
|
||||||
|
overflow: "hidden",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
|
animationPlayState: "paused",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileAvatarEditOverlay = style({
|
||||||
|
position: "absolute",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "#00000055",
|
||||||
|
color: vars.color.muted,
|
||||||
|
zIndex: 1,
|
||||||
|
cursor: "pointer",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profileInformation = style({
|
export const profileInformation = style({
|
||||||
@ -51,12 +78,14 @@ export const profileInformation = style({
|
|||||||
|
|
||||||
export const profileContent = style({
|
export const profileContent = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
height: "100%",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: `${SPACING_UNIT * 4}px`,
|
gap: `${SPACING_UNIT * 4}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profileGameSection = style({
|
export const profileGameSection = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
@ -73,28 +102,17 @@ export const contentSidebar = style({
|
|||||||
maxWidth: "250px",
|
maxWidth: "250px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
},
|
},
|
||||||
"(min-width: 1280px)": {
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: "350px",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const feedGameIcon = style({
|
export const feedGameIcon = style({
|
||||||
height: "100%",
|
height: "100%",
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
position: "relative",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const libraryGameIcon = style({
|
export const libraryGameIcon = style({
|
||||||
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
position: "relative",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const feedItem = style({
|
export const feedItem = style({
|
||||||
@ -114,13 +132,11 @@ export const feedItem = style({
|
|||||||
|
|
||||||
export const gameListItem = style({
|
export const gameListItem = style({
|
||||||
color: vars.color.body,
|
color: vars.color.body,
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
aspectRatio: "1",
|
|
||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
zIndex: "1",
|
zIndex: "1",
|
||||||
|
overflow: "hidden",
|
||||||
|
padding: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||||
":hover": {
|
":hover": {
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
},
|
},
|
||||||
@ -134,5 +150,49 @@ export const gameInformation = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const profileHeaderSkeleton = style({
|
export const profileHeaderSkeleton = style({
|
||||||
height: "200px",
|
height: "144px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const editProfileImageBadge = style({
|
||||||
|
width: "28px",
|
||||||
|
height: "28px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: vars.color.background,
|
||||||
|
backgroundColor: vars.color.muted,
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "0px",
|
||||||
|
right: "0px",
|
||||||
|
zIndex: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const telescopeIcon = style({
|
||||||
|
width: "60px",
|
||||||
|
height: "60px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginBottom: `${SPACING_UNIT * 2}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const noDownloads = style({
|
||||||
|
display: "flex",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const signOutModalButtonsContainer = style({
|
||||||
|
display: "flex",
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "end",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { UserProfile } from "@types";
|
import { UserProfile } from "@types";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { useAppDispatch } from "@renderer/hooks";
|
import { useAppDispatch } from "@renderer/hooks";
|
||||||
@ -15,8 +15,8 @@ export const User = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
const getUserProfile = useCallback(() => {
|
||||||
window.electron.getUser(userId!).then((userProfile) => {
|
return window.electron.getUser(userId!).then((userProfile) => {
|
||||||
if (userProfile) {
|
if (userProfile) {
|
||||||
dispatch(setHeaderTitle(userProfile.displayName));
|
dispatch(setHeaderTitle(userProfile.displayName));
|
||||||
setUserProfile(userProfile);
|
setUserProfile(userProfile);
|
||||||
@ -24,11 +24,20 @@ export const User = () => {
|
|||||||
});
|
});
|
||||||
}, [dispatch, userId]);
|
}, [dispatch, userId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getUserProfile();
|
||||||
|
}, [getUserProfile]);
|
||||||
|
|
||||||
|
console.log(userProfile);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{userProfile ? (
|
{userProfile ? (
|
||||||
<UserContent userProfile={userProfile} />
|
<UserContent
|
||||||
|
userProfile={userProfile}
|
||||||
|
updateUserProfile={getUserProfile}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UserSkeleton />
|
<UserSkeleton />
|
||||||
)}
|
)}
|
||||||
|
@ -3305,6 +3305,15 @@ file-type@^18.7.0:
|
|||||||
strtok3 "^7.0.0"
|
strtok3 "^7.0.0"
|
||||||
token-types "^5.0.1"
|
token-types "^5.0.1"
|
||||||
|
|
||||||
|
file-type@^19.0.0:
|
||||||
|
version "19.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/file-type/-/file-type-19.0.0.tgz#62a6cadc43f73ba38c53e1a174943a75fdafafa9"
|
||||||
|
integrity sha512-s7cxa7/leUWLiXO78DVVfBVse+milos9FitauDLG1pI7lNaJ2+5lzPnr2N24ym+84HVwJL6hVuGfgVE+ALvU8Q==
|
||||||
|
dependencies:
|
||||||
|
readable-web-to-node-stream "^3.0.2"
|
||||||
|
strtok3 "^7.0.0"
|
||||||
|
token-types "^5.0.1"
|
||||||
|
|
||||||
file-uri-to-path@1.0.0:
|
file-uri-to-path@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||||
|
Loading…
Reference in New Issue
Block a user