mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-21 10:03:56 +00:00
feat: returning with edit profile modal
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "اَلْعَرَبِيَّةُ",
|
||||
"home": {
|
||||
"featured": "مميّز",
|
||||
"trending": "شائع",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "беларуская мова",
|
||||
"home": {
|
||||
"featured": "Рэкамэндаванае",
|
||||
"trending": "Актуальнае",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Català",
|
||||
"app": {
|
||||
"successfully_signed_in": "Has entrat correctament"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Dansk",
|
||||
"home": {
|
||||
"featured": "Anbefalet",
|
||||
"trending": "Trender",
|
||||
|
||||
277
src/locales/de/translation.json
Normal file
277
src/locales/de/translation.json
Normal file
@@ -0,0 +1,277 @@
|
||||
{
|
||||
"language_name": "Deutsch",
|
||||
"app": {
|
||||
"successfully_signed_in": "Erfolgreich angemeldet"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Empfohlen",
|
||||
"trending": "Beliebt",
|
||||
"surprise_me": "Überrasche mich",
|
||||
"no_results": "Keine Ergebnisse gefunden"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Downloads",
|
||||
"settings": "Einstellungen",
|
||||
"my_library": "Meine Bibliothek",
|
||||
"downloading_metadata": "{{title}} (Metadaten werden heruntergeladen…)",
|
||||
"paused": "{{title}} (Pausiert)",
|
||||
"downloading": "{{title}} ({{percentage}} - Wird heruntergeladen…)",
|
||||
"filter": "Bibliothek filtern",
|
||||
"home": "Home",
|
||||
"queued": "{{title}} (In Warteschlange)",
|
||||
"game_has_no_executable": "Spiel hat keine ausführbare Datei gewählt",
|
||||
"sign_in": "Anmelden"
|
||||
},
|
||||
"header": {
|
||||
"search": "Spiele suchen",
|
||||
"home": "Home",
|
||||
"catalogue": "Katalog",
|
||||
"downloads": "Downloads",
|
||||
"search_results": "Suchergebnisse",
|
||||
"settings": "Einstellungen",
|
||||
"version_available_install": "Version {{version}} verfügbar. Klicke hier, um neuzustarten und sie zu installieren.",
|
||||
"version_available_download": "Version {{version}} verfügbar. Klicke hier, um sie herunterzuladen."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Keine aktive Downloads",
|
||||
"downloading_metadata": "Metadaten von {{title}} werden heruntergeladen…",
|
||||
"downloading": "{{title}} wird heruntergeladen… ({{percentage}} abgeschlossen) - Abschluss {{eta}} - {{speed}}",
|
||||
"calculating_eta": "{{title}} wird heruntergeladen… ({{percentage}} abgeschlossen) - Verbleibende Zeit wird berechnet…",
|
||||
"checking_files": "Prüfe Dateien von {{title}}… ({{percentage}} abgeschlossen)"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Nächste Seite",
|
||||
"previous_page": "Vorherige Seite"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Download-Optionen öffnen",
|
||||
"download_options_zero": "Keine Download-Optionen",
|
||||
"download_options_one": "{{count}} Download-Option",
|
||||
"download_options_other": "{{count}} Download-Optionen",
|
||||
"updated_at": "Aktualisiert {{updated_at}}",
|
||||
"install": "Installieren",
|
||||
"resume": "Fortfahren",
|
||||
"pause": "Pausieren",
|
||||
"cancel": "Abbrechen",
|
||||
"remove": "Entfernen",
|
||||
"space_left_on_disk": "{{space}} auf Festplatte verfügbar",
|
||||
"eta": "Abschluss {{eta}}",
|
||||
"calculating_eta": "Verbleibende Zeit wird berechnet…",
|
||||
"downloading_metadata": "Metadaten werden heruntergeladen…",
|
||||
"filter": "Repacks filtern",
|
||||
"requirements": "Systemanforderungen",
|
||||
"minimum": "Minimum",
|
||||
"recommended": "Empfohlen",
|
||||
"paused": "Pausiert",
|
||||
"release_date": "Veröffentlicht am {{date}}",
|
||||
"publisher": "Veröffentlicht von {{publisher}}",
|
||||
"hours": "Stunden",
|
||||
"minutes": "Minuten",
|
||||
"amount_hours": "{{amount}} Stunden",
|
||||
"amount_minutes": "{{amount}} Minuten",
|
||||
"accuracy": "{{accuracy}}% Genauigkeit",
|
||||
"add_to_library": "Zu Bibliothek hinzufügen",
|
||||
"remove_from_library": "Von Bibliothek entfernen",
|
||||
"no_downloads": "Keine Downloads verfügbar",
|
||||
"play_time": "{{amount}} lang gespielt",
|
||||
"last_time_played": "Zuletzt gespielt {{period}}",
|
||||
"not_played_yet": "{{title}} wurde noch nicht gespielt",
|
||||
"next_suggestion": "Nächste Empfehlung",
|
||||
"play": "Spielen",
|
||||
"deleting": "Installer wird gelöscht…",
|
||||
"close": "Schließen",
|
||||
"playing_now": "Spielt jetzt",
|
||||
"change": "Ändern",
|
||||
"repacks_modal_description": "Wähle das Repack, das du herunterladen möchtest",
|
||||
"select_folder_hint": "Um das Standardverzeichnis zu ändern, gehe zu den <0>Einstellungen</0>",
|
||||
"download_now": "Jetzt herunterladen",
|
||||
"no_shop_details": "Shop-Details konnten nicht abgerufen werden.",
|
||||
"download_options": "Download-Optionen",
|
||||
"download_path": "Download-Verzeichnis",
|
||||
"previous_screenshot": "Vorheriger Screenshot",
|
||||
"next_screenshot": "Nächster Screenshot",
|
||||
"screenshot": "Screenshot {{number}}",
|
||||
"open_screenshot": "Screenshot {{number}} öffnen",
|
||||
"download_settings": "Download-Einstellungen",
|
||||
"downloader": "Downloader",
|
||||
"select_executable": "Auswählen",
|
||||
"no_executable_selected": "Keine ausführbare Datei gewählt",
|
||||
"open_folder": "Verzeichnis öffnen",
|
||||
"open_download_location": "Heruntergeladene Dateien anzeigen",
|
||||
"create_shortcut": "Desktop-Verknüpfung erstellen",
|
||||
"remove_files": "Dateien entfernen",
|
||||
"remove_from_library_title": "Bist du dir sicher?",
|
||||
"remove_from_library_description": "Dies wird {{game}} aus deiner Bibliothek entfernen",
|
||||
"options": "Optionen",
|
||||
"executable_section_title": "Ausführbare Datei",
|
||||
"executable_section_description": "Pfad der Datei, die bei Klick auf \"Play\" ausgeführt wird",
|
||||
"downloads_secion_title": "Downloads",
|
||||
"downloads_section_description": "Sieh dir Updates oder andere Versionen dieses Spiels an",
|
||||
"danger_zone_section_title": "Gefahrenzone",
|
||||
"danger_zone_section_description": "Entferne dieses Spiel aus deiner Bibliothek oder die von Hydra heruntergeladenen Dateien",
|
||||
"download_in_progress": "Download erfolgt",
|
||||
"download_paused": "Download ist pausiert",
|
||||
"last_downloaded_option": "Letzte Download-Option",
|
||||
"create_shortcut_success": "Verknüpfung erfolgreich erstellt",
|
||||
"create_shortcut_error": "Fehler bei Erstellung von Verknüpfung"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Hydra aktivieren",
|
||||
"installation_id": "Installations ID:",
|
||||
"enter_activation_code": "Aktivierungscode eingeben",
|
||||
"message": "Wenn du nicht weißt wo du fragen musst, solltest du dies nicht haben.",
|
||||
"activate": "Aktivieren",
|
||||
"loading": "Lädt…"
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "Fortfahren",
|
||||
"pause": "Pause",
|
||||
"eta": "Abschluss {{eta}}",
|
||||
"paused": "Pausiert",
|
||||
"verifying": "Verifiziere…",
|
||||
"completed": "Abgeschlossen",
|
||||
"removed": "Nicht heruntergeladen",
|
||||
"cancel": "Abbrechen",
|
||||
"filter": "Heruntergeladene Spiele filtern",
|
||||
"remove": "Entfernen",
|
||||
"downloading_metadata": "Metadaten werden heruntergeladen…",
|
||||
"deleting": "Installer wird entfernt…",
|
||||
"delete": "Installer entfernen",
|
||||
"delete_modal_title": "Bist du dir sicher?",
|
||||
"delete_modal_description": "Dies wird alle Installationsdateien von deinem Computer entfernen",
|
||||
"install": "Installieren",
|
||||
"download_in_progress": "Läuft",
|
||||
"queued_downloads": "Downloads in Warteschlange",
|
||||
"downloads_completed": "Abgeschlossen",
|
||||
"queued": "In Warteschlange",
|
||||
"no_downloads_title": "Welch Leere",
|
||||
"no_downloads_description": "Du hast mit Hydra noch nichts heruntergeladen, aber es ist nie zu spät anzufangen.",
|
||||
"checking_files": "Dateien werden überprüft…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Download-Pfad",
|
||||
"change": "Aktualisieren",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"enable_download_notifications": "Wenn ein Download abgeschlossen wird",
|
||||
"enable_repack_list_notifications": "Wenn ein neues Repack hinzugefügt wird",
|
||||
"real_debrid_api_token_label": "Real-Debrid API Token",
|
||||
"quit_app_instead_hiding": "Hydra verlassen statt minimieren beim Schließen",
|
||||
"launch_with_system": "Hydra bei Systemstart starten",
|
||||
"general": "Allgemein",
|
||||
"behavior": "Verhalten",
|
||||
"download_sources": "Download-Quellen",
|
||||
"language": "Sprache",
|
||||
"real_debrid_api_token": "API Token",
|
||||
"enable_real_debrid": "Real-Debrid aktivieren",
|
||||
"real_debrid_description": "Real-Debrid ist ein unrestriktiver Downloader, der es dir ermöglicht Dateien sofort und mit deiner maximalen Internetgeschwindigkeit herunterzuladen.",
|
||||
"real_debrid_invalid_token": "API token nicht gültig",
|
||||
"real_debrid_api_token_hint": "<0>Hier</0> kannst du dir deinen API Token holen",
|
||||
"real_debrid_free_account_error": "Das Konto \"{{username}}\" ist ein gratis account. Bitte abonniere Real-Debrid",
|
||||
"real_debrid_linked_message": "Konto \"{{username}}\" verknüpft",
|
||||
"save_changes": "Änderungen speichern",
|
||||
"changes_saved": "Änderungen erfolgreich gespeichert",
|
||||
"download_sources_description": "Hydra wird die Download-Links von diesen Quellen abrufen. Die Quell-URL muss ein direkter Link zu einer .json Datei, welche die Download-Links enthält, sein.",
|
||||
"validate_download_source": "Validieren",
|
||||
"remove_download_source": "Entfernen",
|
||||
"add_download_source": "Quelle hinzufügen",
|
||||
"download_count_zero": "Keine Download-Option",
|
||||
"download_count_one": "{{countFormatted}} Download-Option",
|
||||
"download_count_other": "{{countFormatted}} Download-Optionen",
|
||||
"download_source_url": "Download Quell-URL",
|
||||
"add_download_source_description": "Füge die URL, welche die .json Datei enthält, ein",
|
||||
"download_source_up_to_date": "Auf aktuellem Stand",
|
||||
"download_source_errored": "Fehlgeschlagen",
|
||||
"sync_download_sources": "Quellen synchronisieren",
|
||||
"removed_download_source": "Download-Quelle entfernt",
|
||||
"added_download_source": "Download-Quelle hinzugefügt",
|
||||
"download_sources_synced": "Alle Download-Quellen sind synchronisiert",
|
||||
"insert_valid_json_url": "Füge eine gültige JSON URL ein",
|
||||
"found_download_option_zero": "Keine Download-Option gefunden",
|
||||
"found_download_option_one": "{{countFormatted}} Download-Option gefunden",
|
||||
"found_download_option_other": "{{countFormatted}} Download-Optionen gefunden",
|
||||
"import": "Importieren"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download abgeschlossen",
|
||||
"game_ready_to_install": "{{title}} ist bereit zur Installation",
|
||||
"repack_list_updated": "Repack-Liste aktualisiert",
|
||||
"repack_count_one": "{{count}} Repack hinzugefügt",
|
||||
"repack_count_other": "{{count}} Repacks hinzugefügt",
|
||||
"new_update_available": "Version {{version}} verfügbar",
|
||||
"restart_to_install_update": "Um das Update zu installieren, starte Hydra neu"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Hydra öffnen",
|
||||
"quit": "Schließen"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "Keine Downloads verfügbar"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Programme nicht installiert",
|
||||
"description": "Ausführbare Dateien für Wine oder Lutris wurden auf deinem System nicht gefunden",
|
||||
"instructions": "Überprüfe die korrekte Installation dieser für deine Linux-Distro, damit das Spiel normal laufen kann"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Knopf schließen"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Sichtbarkeit des Passworts umschalten"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} Stunden",
|
||||
"amount_minutes": "{{amount}} Minuten",
|
||||
"last_time_played": "Zuletzt gespielt {{period}}",
|
||||
"activity": "Letzte Aktivität",
|
||||
"library": "Bibliothek",
|
||||
"total_play_time": "Gesamtspielzeit: {{amount}}",
|
||||
"no_recent_activity_title": "Hmmm… hier ist nichts",
|
||||
"no_recent_activity_description": "Du hast in letzter Zeit keine Spiele gespielt. Es wird Zeit das zu ändern!",
|
||||
"display_name": "Anzeigename",
|
||||
"saving": "Speichert",
|
||||
"save": "Speichern",
|
||||
"edit_profile": "Profil Bearbeiten",
|
||||
"saved_successfully": "Erfolgreich gespeichert",
|
||||
"try_again": "Bitte versuche es erneut",
|
||||
"sign_out_modal_title": "Bist du dir sicher?",
|
||||
"cancel": "Abbrechen",
|
||||
"successfully_signed_out": "Erfolgreich abgemeldet",
|
||||
"sign_out": "Abmelden",
|
||||
"playing_for": "Spielt {{amount}} lang",
|
||||
"sign_out_modal_text": "Deine Bibliothek ist mit deinem aktuellen Konto verknüpft. Wenn du dich abmeldest, wird deine Bibliothek nicht mehr sichtbar sein und jeglicher Fortschritt wird nicht gespeichert. Abmelden fortführen?",
|
||||
"add_friends": "Freunde hinzufügen",
|
||||
"add": "Hinzufügen",
|
||||
"friend_code": "Freundescode",
|
||||
"see_profile": "Profil anzeigen",
|
||||
"sending": "Sendet",
|
||||
"friend_request_sent": "Freundschaftsanfrage versendet",
|
||||
"friends": "Freunde",
|
||||
"friends_list": "Freundesliste",
|
||||
"user_not_found": "Nutzer nicht gefunden",
|
||||
"block_user": "Nutzer blockieren",
|
||||
"add_friend": "Freund hinzufügen",
|
||||
"request_sent": "Anfrage versendet",
|
||||
"request_received": "Anfrage erhalten",
|
||||
"accept_request": "Anfrage annehmen",
|
||||
"ignore_request": "Anfrage ignorieren",
|
||||
"cancel_request": "Anfrage zurückziehen",
|
||||
"undo_friendship": "Freundschaft kündigen",
|
||||
"request_accepted": "Anfrage akzeptiert",
|
||||
"user_blocked_successfully": "Nutzer erfolgreich blockiert",
|
||||
"user_block_modal_text": "{{displayName}} wird dadurch blockiert",
|
||||
"settings": "Einstellungen",
|
||||
"public": "Öffentlich",
|
||||
"private": "Privat",
|
||||
"friends_only": "Nur Freunde",
|
||||
"privacy": "Privatsphäre",
|
||||
"blocked_users": "Blockierte Nutzer",
|
||||
"unblock": "Freigeben",
|
||||
"no_friends_added": "Du hast noch keine Freunde hinzugefügt",
|
||||
"pending": "Ausstehend",
|
||||
"no_pending_invites": "Du hast keine ausstehenden Einladungen",
|
||||
"no_blocked_users": "Du hast keine blockierten Nutzer",
|
||||
"friend_code_copied": "Freundescode kopiert",
|
||||
"undo_friendship_modal_text": "Freundschaft mit {{displayName}} wird dadurch gekündigt"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "English",
|
||||
"app": {
|
||||
"successfully_signed_in": "Successfully signed in"
|
||||
},
|
||||
@@ -20,7 +21,8 @@
|
||||
"home": "Home",
|
||||
"queued": "{{title}} (Queued)",
|
||||
"game_has_no_executable": "Game has no executable selected",
|
||||
"sign_in": "Sign in"
|
||||
"sign_in": "Sign in",
|
||||
"friends": "Friends"
|
||||
},
|
||||
"header": {
|
||||
"search": "Search games",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Español",
|
||||
"app": {
|
||||
"successfully_signed_in": "Sesión iniciada correctamente"
|
||||
},
|
||||
@@ -270,6 +271,7 @@
|
||||
"pending": "Pendiente",
|
||||
"no_pending_invites": "No tienes invitaciones pendientes",
|
||||
"no_blocked_users": "No has bloqueado a ningún usuario",
|
||||
"friend_code_copied": "Código de amigo copiado"
|
||||
"friend_code_copied": "Código de amigo copiado",
|
||||
"undo_friendship_modal_text": "Esto deshará tu amistad con {{displayName}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "فارسی",
|
||||
"home": {
|
||||
"featured": "پیشنهادی",
|
||||
"trending": "پرطرفدار",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Français",
|
||||
"home": {
|
||||
"featured": "En vedette",
|
||||
"trending": "Tendance",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Magyar",
|
||||
"home": {
|
||||
"featured": "Featured",
|
||||
"trending": "Népszerű",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Bahasa Indonesia",
|
||||
"app": {
|
||||
"successfully_signed_in": "Berhasil masuk"
|
||||
},
|
||||
|
||||
@@ -1,21 +1,49 @@
|
||||
export { default as en } from "./en/translation.json";
|
||||
export { default as pt } from "./pt/translation.json";
|
||||
export { default as es } from "./es/translation.json";
|
||||
export { default as nl } from "./nl/translation.json";
|
||||
export { default as fr } from "./fr/translation.json";
|
||||
export { default as hu } from "./hu/translation.json";
|
||||
export { default as it } from "./it/translation.json";
|
||||
export { default as pl } from "./pl/translation.json";
|
||||
export { default as ru } from "./ru/translation.json";
|
||||
export { default as tr } from "./tr/translation.json";
|
||||
export { default as be } from "./be/translation.json";
|
||||
export { default as uk } from "./uk/translation.json";
|
||||
export { default as zh } from "./zh/translation.json";
|
||||
export { default as id } from "./id/translation.json";
|
||||
export { default as ko } from "./ko/translation.json";
|
||||
export { default as da } from "./da/translation.json";
|
||||
export { default as ar } from "./ar/translation.json";
|
||||
export { default as fa } from "./fa/translation.json";
|
||||
export { default as ro } from "./ro/translation.json";
|
||||
export { default as ca } from "./ca/translation.json";
|
||||
export { default as kk } from "./kk/translation.json";
|
||||
import en from "./en/translation.json";
|
||||
import ptPT from "./pt-PT/translation.json";
|
||||
import ptBR from "./pt-BR/translation.json";
|
||||
import es from "./es/translation.json";
|
||||
import nl from "./nl/translation.json";
|
||||
import fr from "./fr/translation.json";
|
||||
import hu from "./hu/translation.json";
|
||||
import it from "./it/translation.json";
|
||||
import de from "./de/translation.json";
|
||||
import pl from "./pl/translation.json";
|
||||
import ru from "./ru/translation.json";
|
||||
import tr from "./tr/translation.json";
|
||||
import be from "./be/translation.json";
|
||||
import uk from "./uk/translation.json";
|
||||
import zh from "./zh/translation.json";
|
||||
import id from "./id/translation.json";
|
||||
import ko from "./ko/translation.json";
|
||||
import da from "./da/translation.json";
|
||||
import ar from "./ar/translation.json";
|
||||
import fa from "./fa/translation.json";
|
||||
import ro from "./ro/translation.json";
|
||||
import ca from "./ca/translation.json";
|
||||
import kk from "./kk/translation.json";
|
||||
|
||||
export default {
|
||||
"pt-BR": ptBR,
|
||||
"pt-PT": ptPT,
|
||||
en,
|
||||
de,
|
||||
es,
|
||||
nl,
|
||||
fr,
|
||||
hu,
|
||||
it,
|
||||
pl,
|
||||
ru,
|
||||
tr,
|
||||
be,
|
||||
uk,
|
||||
zh,
|
||||
id,
|
||||
ko,
|
||||
da,
|
||||
ar,
|
||||
fa,
|
||||
ro,
|
||||
ca,
|
||||
kk,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Italiano",
|
||||
"home": {
|
||||
"featured": "In primo piano",
|
||||
"trending": "Di tendenza",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "қазақ тілі",
|
||||
"app": {
|
||||
"successfully_signed_in": "Сәтті кіру"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "한국어",
|
||||
"home": {
|
||||
"featured": "추천",
|
||||
"trending": "인기",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Nederlands",
|
||||
"home": {
|
||||
"featured": "Uitgelicht",
|
||||
"trending": "Trending",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Polski",
|
||||
"home": {
|
||||
"featured": "Wyróżnione",
|
||||
"trending": "Trendujące",
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"language_name": "Português (Brasil)",
|
||||
"app": {
|
||||
"successfully_signed_in": "Autenticado com sucesso"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Destaques",
|
||||
"trending": "Populares",
|
||||
"hot": "🔥 Populares agora",
|
||||
"weekly": "📅 Mais baixados da semana",
|
||||
"surprise_me": "Surpreenda-me",
|
||||
"no_results": "Nenhum resultado encontrado"
|
||||
},
|
||||
@@ -20,7 +23,8 @@
|
||||
"home": "Início",
|
||||
"queued": "{{title}} (Na fila)",
|
||||
"game_has_no_executable": "Jogo não possui executável selecionado",
|
||||
"sign_in": "Login"
|
||||
"sign_in": "Login",
|
||||
"friends": "Amigos"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar jogos",
|
||||
@@ -166,7 +170,7 @@
|
||||
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
||||
"save_changes": "Salvar mudanças",
|
||||
"changes_saved": "Ajustes salvos com sucesso",
|
||||
"download_sources_description": "Hydra vai buscar links de download em todas as fonte habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
|
||||
"download_sources_description": "Hydra vai buscar links de download em todas as fontes habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
|
||||
"validate_download_source": "Validar",
|
||||
"remove_download_source": "Remover",
|
||||
"add_download_source": "Adicionar fonte",
|
||||
@@ -239,7 +243,7 @@
|
||||
"cancel": "Cancelar",
|
||||
"successfully_signed_out": "Deslogado com sucesso",
|
||||
"sign_out": "Sair da conta",
|
||||
"sign_out_modal_title": "Tem certeza?",
|
||||
"sign_out_modal_title": "Deseja mesmo sair?",
|
||||
"playing_for": "Jogando por {{amount}}",
|
||||
"sign_out_modal_text": "Sua biblioteca de jogos está associada com a sua conta atual. Ao sair, sua biblioteca não aparecerá mais no Hydra e qualquer progresso não será salvo. Deseja continuar?",
|
||||
"add_friends": "Adicionar Amigos",
|
||||
281
src/locales/pt-PT/translation.json
Normal file
281
src/locales/pt-PT/translation.json
Normal file
@@ -0,0 +1,281 @@
|
||||
{
|
||||
"language_name": "Português (Portugal)",
|
||||
"app": {
|
||||
"successfully_signed_in": "Sessão iniciada com sucesso"
|
||||
},
|
||||
"home": {
|
||||
"featured": "Destaques",
|
||||
"trending": "Populares",
|
||||
"surprise_me": "Surpreende-me",
|
||||
"no_results": "Nenhum resultado encontrado"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Transferências",
|
||||
"settings": "Definições",
|
||||
"my_library": "Biblioteca",
|
||||
"downloading_metadata": "{{title}} (A transferir metadados…)",
|
||||
"paused": "{{title}} (Pausado)",
|
||||
"downloading": "{{title}} ({{percentage}} - A transferir…)",
|
||||
"filter": "Procurar",
|
||||
"home": "Início",
|
||||
"queued": "{{title}} (Na fila)",
|
||||
"game_has_no_executable": "Jogo não tem executável selecionado",
|
||||
"sign_in": "Iniciar sessão"
|
||||
},
|
||||
"header": {
|
||||
"search": "Procurar jogos",
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Transferências",
|
||||
"search_results": "Resultados da pesquisa",
|
||||
"settings": "Definições",
|
||||
"home": "Início",
|
||||
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.",
|
||||
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "Sem transferências em andamento",
|
||||
"downloading_metadata": "A transferir metadados de {{title}}…",
|
||||
"downloading": "A transferir {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}",
|
||||
"calculating_eta": "A transferir {{title}}… ({{percentage}} concluído) - A calcular tempo restante…",
|
||||
"checking_files": "A verificar ficheiros de {{title}}…"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "Ver opções de transferência",
|
||||
"download_options_zero": "Sem opções de transferência",
|
||||
"download_options_one": "{{count}} opção de transferência",
|
||||
"download_options_other": "{{count}} opções de transferência",
|
||||
"updated_at": "Atualizado a {{updated_at}}",
|
||||
"resume": "Retomar",
|
||||
"pause": "Pausar",
|
||||
"cancel": "Cancelar",
|
||||
"remove": "Remover",
|
||||
"space_left_on_disk": "{{space}} livres no disco",
|
||||
"eta": "Conclusão {{eta}}",
|
||||
"calculating_eta": "A calcular tempo restante…",
|
||||
"downloading_metadata": "A transferir metadados…",
|
||||
"filter": "Filtrar repacks",
|
||||
"requirements": "Requisitos do sistema",
|
||||
"minimum": "Mínimos",
|
||||
"recommended": "Recomendados",
|
||||
"paused": "Pausado",
|
||||
"release_date": "Lançado em {{date}}",
|
||||
"publisher": "Publicado por {{publisher}}",
|
||||
"hours": "horas",
|
||||
"minutes": "minutos",
|
||||
"amount_hours": "{{amount}} horas",
|
||||
"amount_minutes": "{{amount}} minutos",
|
||||
"accuracy": "{{accuracy}}% de precisão",
|
||||
"add_to_library": "Adicionar à biblioteca",
|
||||
"remove_from_library": "Remover da biblioteca",
|
||||
"no_downloads": "Nenhuma transferência disponível",
|
||||
"play_time": "Jogou por {{amount}}",
|
||||
"next_suggestion": "Próxima sugestão",
|
||||
"install": "Instalar",
|
||||
"last_time_played": "Última sessão {{period}}",
|
||||
"play": "Jogar",
|
||||
"not_played_yet": "Ainda não jogou {{title}}",
|
||||
"close": "Fechar",
|
||||
"deleting": "A eliminar instalador…",
|
||||
"playing_now": "A jogar agora",
|
||||
"change": "Explorar",
|
||||
"repacks_modal_description": "Escolha o repack do jogo que deseja transferir",
|
||||
"select_folder_hint": "Para trocar o diretório padrão, aceda à <0>Tela de Definições</0>",
|
||||
"download_now": "Iniciar transferência",
|
||||
"no_shop_details": "Não foi possível obter os detalhes da loja.",
|
||||
"download_options": "Opções de transferência",
|
||||
"download_path": "Diretório de transferência",
|
||||
"previous_screenshot": "Captura de ecrã anterior",
|
||||
"next_screenshot": "Próxima captura de ecrã",
|
||||
"screenshot": "Captura de ecrã {{number}}",
|
||||
"open_screenshot": "Ver captura de ecrã {{number}}",
|
||||
"download_settings": "Definições de transferência",
|
||||
"downloader": "Downloader",
|
||||
"select_executable": "Explorar",
|
||||
"no_executable_selected": "Nenhum executável selecionado",
|
||||
"open_folder": "Abrir pasta",
|
||||
"open_download_location": "Ver ficheiros transferidos",
|
||||
"create_shortcut": "Criar atalho no ambiente de trabalho",
|
||||
"remove_files": "Remover ficheiros",
|
||||
"options": "Gerir",
|
||||
"remove_from_library_description": "Isto irá remover {{game}} da sua biblioteca",
|
||||
"remove_from_library_title": "Tem a certeza?",
|
||||
"executable_section_title": "Executável",
|
||||
"executable_section_description": "O caminho do ficheiro que será executado ao clicar em \"Jogar\"",
|
||||
"downloads_secion_title": "Transferências",
|
||||
"downloads_section_description": "Confira atualizações ou versões diferentes para este mesmo título",
|
||||
"danger_zone_section_title": "Zona de perigo",
|
||||
"danger_zone_section_description": "Remova o jogo da sua biblioteca ou os ficheiros que foram transferidos pelo Hydra",
|
||||
"download_in_progress": "Transferência em andamento",
|
||||
"download_paused": "Transferência pausada",
|
||||
"last_downloaded_option": "Última opção transferida",
|
||||
"create_shortcut_success": "Atalho criado com sucesso",
|
||||
"create_shortcut_error": "Erro ao criar atalho"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
"installation_id": "ID da instalação:",
|
||||
"enter_activation_code": "Insira o seu código de ativação",
|
||||
"message": "Se não sabe onde conseguir o código, talvez não devesse estar aqui.",
|
||||
"activate": "Ativar",
|
||||
"loading": "A carregar…"
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "Retomar",
|
||||
"pause": "Pausar",
|
||||
"eta": "Conclusão {{eta}}",
|
||||
"paused": "Pausado",
|
||||
"verifying": "A verificar…",
|
||||
"completed": "Concluído",
|
||||
"removed": "Cancelado",
|
||||
"cancel": "Cancelar",
|
||||
"filter": "Filtrar jogos transferidos",
|
||||
"remove": "Remover",
|
||||
"downloading_metadata": "A transferir metadados…",
|
||||
"delete": "Remover instalador",
|
||||
"delete_modal_description": "Isto removerá todos os ficheiros de instalação do seu computador",
|
||||
"delete_modal_title": "Tem a certeza?",
|
||||
"deleting": "A eliminar instalador…",
|
||||
"install": "Instalar",
|
||||
"download_in_progress": "A transferir agora",
|
||||
"queued_downloads": "Na fila",
|
||||
"downloads_completed": "Concluído",
|
||||
"queued": "Na fila",
|
||||
"no_downloads_title": "Nada por aqui…",
|
||||
"no_downloads_description": "Ainda não transferiu nada pelo Hydra, mas nunca é tarde para começar.",
|
||||
"checking_files": "A verificar ficheiros…"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Diretório das transferências",
|
||||
"change": "Explorar...",
|
||||
"notifications": "Notificações",
|
||||
"enable_download_notifications": "Quando uma transferência for concluída",
|
||||
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
||||
"real_debrid_api_token_label": "Token de API do Real-Debrid",
|
||||
"quit_app_instead_hiding": "Encerrar o Hydra em vez de apenas minimizá-lo ao fechar.",
|
||||
"launch_with_system": "Iniciar o Hydra com o sistema",
|
||||
"general": "Geral",
|
||||
"behavior": "Comportamento",
|
||||
"download_sources": "Fontes de transferência",
|
||||
"language": "Idioma",
|
||||
"real_debrid_api_token": "Token de API",
|
||||
"enable_real_debrid": "Ativar Real-Debrid",
|
||||
"real_debrid_api_token_hint": "Pode obter o seu token de API <0>aqui</0>",
|
||||
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite transferir ficheiros instantaneamente e com a melhor velocidade da sua Internet.",
|
||||
"real_debrid_invalid_token": "Token de API inválido",
|
||||
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, subscreva o Real-Debrid",
|
||||
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
||||
"save_changes": "Guardar alterações",
|
||||
"changes_saved": "Definições guardadas com sucesso",
|
||||
"download_sources_description": "O Hydra vai procurar links de transferência em todas as fontes ativadas. A URL da página de detalhes da loja não é guardada no seu dispositivo. Utilizamos um sistema de metadados criado pela comunidade para fornecer suporte a mais fontes de transferência de jogos.",
|
||||
"enable_source": "Ativar",
|
||||
"disable_source": "Desativar",
|
||||
"validate_download_source": "Validar",
|
||||
"remove_download_source": "Remover",
|
||||
"add_download_source": "Adicionar fonte",
|
||||
"download_count_zero": "Sem transferências na lista",
|
||||
"download_count_one": "{{countFormatted}} transferência na lista",
|
||||
"download_count_other": "{{countFormatted}} transferências na lista",
|
||||
"download_options_zero": "Sem transferências disponíveis",
|
||||
"download_options_one": "{{countFormatted}} transferência disponível",
|
||||
"download_options_other": "{{countFormatted}} transferências disponíveis",
|
||||
"download_source_url": "URL da fonte",
|
||||
"add_download_source_description": "Insira o URL contendo o arquivo .json",
|
||||
"download_source_up_to_date": "Sincronizada",
|
||||
"download_source_errored": "Falhou",
|
||||
"sync_download_sources": "Sincronizar",
|
||||
"removed_download_source": "Fonte removida",
|
||||
"added_download_source": "Fonte adicionada",
|
||||
"download_sources_synced": "As fontes foram sincronizadas",
|
||||
"insert_valid_json_url": "Insira o URL de um JSON válido",
|
||||
"found_download_option_zero": "Nenhuma opção de transferência encontrada",
|
||||
"found_download_option_one": "{{countFormatted}} opção de transferência encontrada",
|
||||
"found_download_option_other": "{{countFormatted}} opções de transferências encontradas",
|
||||
"import": "Importar"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Transferência concluída",
|
||||
"game_ready_to_install": "{{title}} está pronto para ser descarregado",
|
||||
"repack_list_updated": "Lista de repacks atualizada",
|
||||
"repack_count_one": "{{count}} novo repack",
|
||||
"repack_count_other": "{{count}} novos repacks",
|
||||
"new_update_available": "Versão {{version}} disponível",
|
||||
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "Abrir Hydra",
|
||||
"quit": "Fechar"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "Sem transferências disponíveis"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "Programas não instalados",
|
||||
"description": "Os executáveis do Wine ou Lutris não foram encontrados em seu sistema.",
|
||||
"instructions": "Verifique a forma correta de instalar algum deles na sua distro Linux, garantindo assim a execução normal do jogo."
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "Próxima página",
|
||||
"previous_page": "Página anterior"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Botão de fechar"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "Alternar visibilidade da palavra-passe"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} horas",
|
||||
"amount_minutes": "{{amount}} minutos",
|
||||
"last_time_played": "Última sessão {{period}}",
|
||||
"activity": "Atividades recentes",
|
||||
"library": "Biblioteca",
|
||||
"total_play_time": "Tempo total de jogo: {{amount}}",
|
||||
"no_recent_activity_title": "Hmmm… nada por aqui",
|
||||
"no_recent_activity_description": "Parece que não jogaste nada recentemente. Que tal começar agora?",
|
||||
"display_name": "Nome de exibição",
|
||||
"saving": "a guardar…",
|
||||
"save": "Guardar",
|
||||
"edit_profile": "Editar perfil",
|
||||
"saved_successfully": "Guardado com sucesso",
|
||||
"try_again": "Por favor, tenta novamente",
|
||||
"cancel": "Cancelar",
|
||||
"successfully_signed_out": "Terminado com sucesso",
|
||||
"sign_out": "Terminar sessão",
|
||||
"sign_out_modal_title": "Tens a certeza?",
|
||||
"playing_for": "A jogar há {{amount}}",
|
||||
"sign_out_modal_text": "A tua biblioteca de jogos está associada com a tua conta atual. Ao sair, a tua biblioteca não aparecerá mais no Hydra e qualquer progresso não será guardado. Desejas continuar?",
|
||||
"add_friends": "Adicionar Amigos",
|
||||
"friend_code": "Código de amigo",
|
||||
"see_profile": "Ver perfil",
|
||||
"friend_request_sent": "Pedido de amizade enviado",
|
||||
"friends": "Amigos",
|
||||
"add": "Adicionar",
|
||||
"sending": "A enviar",
|
||||
"friends_list": "Lista de amigos",
|
||||
"user_not_found": "Utilizador não encontrado",
|
||||
"block_user": "Bloquear",
|
||||
"add_friend": "Adicionar amigo",
|
||||
"request_sent": "Pedido enviado",
|
||||
"request_received": "Pedido recebido",
|
||||
"accept_request": "Aceitar pedido",
|
||||
"ignore_request": "Ignorar pedido",
|
||||
"cancel_request": "Cancelar pedido",
|
||||
"undo_friendship": "Desfazer amizade",
|
||||
"request_accepted": "Pedido de amizade aceito",
|
||||
"user_blocked_successfully": "Utilizador bloqueado com sucesso",
|
||||
"user_block_modal_text": "Bloquear {{displayName}}",
|
||||
"settings": "Definições",
|
||||
"privacy": "Privacidade",
|
||||
"private": "Privado",
|
||||
"friends_only": "Apenas amigos",
|
||||
"public": "Público",
|
||||
"blocked_users": "Utilizadores bloqueados",
|
||||
"unblock": "Desbloquear",
|
||||
"no_friends_added": "Ainda não adicionaste amigos",
|
||||
"pending": "Pendentes",
|
||||
"no_pending_invites": "Não tens convites de amizade pendentes",
|
||||
"no_blocked_users": "Não tens nenhum utilizador bloqueado",
|
||||
"friend_code_copied": "Código de amigo copiado"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Română",
|
||||
"home": {
|
||||
"featured": "Recomandate",
|
||||
"trending": "Populare",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Русский",
|
||||
"app": {
|
||||
"successfully_signed_in": "Успешный вход"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Türkçe",
|
||||
"home": {
|
||||
"featured": "Öne çıkan",
|
||||
"trending": "Popüler",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "Українська",
|
||||
"app": {
|
||||
"successfully_signed_in": "Успішний вхід в систему"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"language_name": "中文",
|
||||
"app": {
|
||||
"successfully_signed_in": "已成功登录"
|
||||
},
|
||||
|
||||
@@ -1,35 +1,42 @@
|
||||
import type { CatalogueEntry, GameShop } from "@types";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { RepacksManager, requestSteam250 } from "@main/services";
|
||||
import { formatName, steamUrlBuilder } from "@shared";
|
||||
import { HydraApi, RepacksManager } from "@main/services";
|
||||
import { CatalogueCategory, formatName, steamUrlBuilder } from "@shared";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
|
||||
const resultSize = 12;
|
||||
const getCatalogue = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
category: CatalogueCategory
|
||||
) => {
|
||||
const params = new URLSearchParams({
|
||||
take: "12",
|
||||
skip: "0",
|
||||
});
|
||||
|
||||
const getCatalogue = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const trendingGames = await requestSteam250("/90day");
|
||||
const results: CatalogueEntry[] = [];
|
||||
const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>(
|
||||
`/games/${category}?${params.toString()}`
|
||||
);
|
||||
|
||||
for (let i = 0; i < resultSize; i++) {
|
||||
if (!trendingGames[i]) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
return Promise.all(
|
||||
response.map(async (game) => {
|
||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
const { title, objectID } = trendingGames[i]!;
|
||||
const repacks = RepacksManager.search({ query: formatName(title) });
|
||||
const repacks = RepacksManager.search({
|
||||
query: formatName(steamGame.name),
|
||||
});
|
||||
|
||||
const catalogueEntry = {
|
||||
objectID,
|
||||
title,
|
||||
shop: "steam" as GameShop,
|
||||
cover: steamUrlBuilder.library(objectID),
|
||||
};
|
||||
|
||||
results.push({ ...catalogueEntry, repacks });
|
||||
}
|
||||
|
||||
return results;
|
||||
return {
|
||||
title: steamGame.name,
|
||||
shop: game.shop,
|
||||
repacks,
|
||||
cover: steamUrlBuilder.library(game.objectId),
|
||||
objectID: game.objectId,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
registerEvent("getCatalogue", getCatalogue);
|
||||
|
||||
23
src/main/events/catalogue/get-game-stats.ts
Normal file
23
src/main/events/catalogue/get-game-stats.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameStats } from "@types";
|
||||
|
||||
const getGameStats = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => {
|
||||
const params = new URLSearchParams({
|
||||
objectId,
|
||||
shop,
|
||||
});
|
||||
|
||||
const response = await HydraApi.get<GameStats>(
|
||||
`/games/stats?${params.toString()}`
|
||||
);
|
||||
return response;
|
||||
};
|
||||
|
||||
registerEvent("getGameStats", getGameStats);
|
||||
@@ -8,6 +8,7 @@ import "./catalogue/get-how-long-to-beat";
|
||||
import "./catalogue/get-random-game";
|
||||
import "./catalogue/search-games";
|
||||
import "./catalogue/search-game-repacks";
|
||||
import "./catalogue/get-game-stats";
|
||||
import "./hardware/get-disk-free-space";
|
||||
import "./library/add-game-to-library";
|
||||
import "./library/create-game-shortcut";
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { HydraApi, logger } from "@main/services";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import type { UserProfile } from "@types";
|
||||
import { getUserFriends } from "./get-user-friends";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
const getSteamGame = async (objectId: string) => {
|
||||
try {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
return {
|
||||
title: steamGame.name,
|
||||
iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon),
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error("Failed to get Steam game", err);
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getUser = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
userId: string
|
||||
@@ -20,36 +37,51 @@ const getUser = async (
|
||||
if (!profile) return null;
|
||||
|
||||
const recentGames = await Promise.all(
|
||||
profile.recentGames.map(async (game) => {
|
||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
profile.recentGames
|
||||
.map(async (game) => {
|
||||
const steamGame = await getSteamGame(game.objectId);
|
||||
|
||||
return {
|
||||
...game,
|
||||
title: steamGame.name,
|
||||
iconUrl: steamUrlBuilder.icon(game.objectId, steamGame.clientIcon),
|
||||
};
|
||||
})
|
||||
return {
|
||||
...game,
|
||||
...steamGame,
|
||||
};
|
||||
})
|
||||
.filter((game) => game)
|
||||
);
|
||||
|
||||
// const libraryGames = await Promise.all(
|
||||
// profile.libraryGames.map(async (game) => {
|
||||
// return getSteamUserGame(game);
|
||||
// })
|
||||
// );
|
||||
const libraryGames = await Promise.all(
|
||||
profile.libraryGames
|
||||
.map(async (game) => {
|
||||
const steamGame = await getSteamGame(game.objectId);
|
||||
|
||||
// const currentGame = await getGameRunning(profile.currentGame);
|
||||
return {
|
||||
...game,
|
||||
...steamGame,
|
||||
};
|
||||
})
|
||||
.filter((game) => game)
|
||||
);
|
||||
|
||||
if (profile.currentGame) {
|
||||
const steamGame = await getSteamGame(profile.currentGame.objectId);
|
||||
|
||||
if (steamGame) {
|
||||
profile.currentGame = {
|
||||
...profile.currentGame,
|
||||
...steamGame,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...profile,
|
||||
// libraryGames,
|
||||
libraryGames,
|
||||
recentGames,
|
||||
friends: friends.friends,
|
||||
totalFriends: friends.totalFriends,
|
||||
// currentGame,
|
||||
};
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ import url from "node:url";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { logger, PythonInstance, WindowManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import * as resources from "@locales";
|
||||
import resources from "@locales";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { knexClient, migrationConfig } from "./knex-client";
|
||||
|
||||
|
||||
@@ -3,6 +3,15 @@ import { HydraApi } from "../hydra-api";
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
export const createGame = async (game: Game) => {
|
||||
console.log({ objectId: game.objectID, shop: game.shop });
|
||||
|
||||
HydraApi.post("/games/download", {
|
||||
objectId: game.objectID,
|
||||
shop: game.shop,
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
HydraApi.post(`/profile/games`, {
|
||||
objectId: game.objectID,
|
||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
FriendRequestAction,
|
||||
UpdateProfileRequest,
|
||||
} from "@types";
|
||||
import type { CatalogueCategory } from "@shared";
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
/* Torrenting */
|
||||
@@ -34,7 +35,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
|
||||
/* Catalogue */
|
||||
searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),
|
||||
getCatalogue: () => ipcRenderer.invoke("getCatalogue"),
|
||||
getCatalogue: (category: CatalogueCategory) =>
|
||||
ipcRenderer.invoke("getCatalogue", category),
|
||||
getGameShopDetails: (objectID: string, shop: GameShop, language: string) =>
|
||||
ipcRenderer.invoke("getGameShopDetails", objectID, shop, language),
|
||||
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
|
||||
@@ -44,6 +46,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("getGames", take, prevCursor),
|
||||
searchGameRepacks: (query: string) =>
|
||||
ipcRenderer.invoke("searchGameRepacks", query),
|
||||
getGameStats: (objectId: string, shop: GameShop) =>
|
||||
ipcRenderer.invoke("getGameStats", objectId, shop),
|
||||
|
||||
/* User preferences */
|
||||
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const actions = style({
|
||||
display: "flex",
|
||||
alignSelf: "flex-end",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const descriptionText = style({
|
||||
fontSize: "16px",
|
||||
lineHeight: "24px",
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Button } from "../button/button";
|
||||
import { Modal, type ModalProps } from "../modal/modal";
|
||||
|
||||
import * as styles from "./confirmation-modal.css";
|
||||
|
||||
export interface ConfirmationModalProps extends ModalProps {
|
||||
confirmButtonLabel: string;
|
||||
cancelButtonLabel: string;
|
||||
descriptionText: string;
|
||||
}
|
||||
|
||||
export function ConfirmationModal({
|
||||
confirmButtonLabel,
|
||||
cancelButtonLabel,
|
||||
descriptionText,
|
||||
...props
|
||||
}: ConfirmationModalProps) {
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
||||
<p className={styles.descriptionText}>{descriptionText}</p>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Button theme="danger">{cancelButtonLabel}</Button>
|
||||
<Button>{confirmButtonLabel}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import { DownloadIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
||||
import type { CatalogueEntry } from "@types";
|
||||
import {
|
||||
DownloadIcon,
|
||||
FileDirectoryIcon,
|
||||
PeopleIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import type { CatalogueEntry, GameStats } from "@types";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
|
||||
import * as styles from "./game-card.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "../badge/badge";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
export interface GameCardProps
|
||||
extends React.DetailedHTMLProps<
|
||||
@@ -22,12 +27,35 @@ const shopIcon = {
|
||||
export function GameCard({ game, ...props }: GameCardProps) {
|
||||
const { t } = useTranslation("game_card");
|
||||
|
||||
const [stats, setStats] = useState<GameStats | null>(null);
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const uniqueRepackers = Array.from(
|
||||
new Set(game.repacks.map(({ repacker }) => repacker))
|
||||
);
|
||||
|
||||
const handleHover = useCallback(() => {
|
||||
if (!stats) {
|
||||
window.electron.getGameStats(game.objectID, game.shop).then((stats) => {
|
||||
setStats(stats);
|
||||
});
|
||||
}
|
||||
}, [game, stats]);
|
||||
|
||||
const numberFormatter = useMemo(() => {
|
||||
return new Intl.NumberFormat(i18n.language, {
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
}, [i18n.language]);
|
||||
|
||||
return (
|
||||
<button {...props} type="button" className={styles.card}>
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={styles.card}
|
||||
onMouseEnter={handleHover}
|
||||
>
|
||||
<div className={styles.backdrop}>
|
||||
<img src={game.cover} alt={game.title} className={styles.cover} />
|
||||
|
||||
@@ -48,19 +76,20 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||
) : (
|
||||
<p className={styles.noDownloadsLabel}>{t("no_downloads")}</p>
|
||||
)}
|
||||
|
||||
<div className={styles.specifics}>
|
||||
<div className={styles.specificsItem}>
|
||||
<DownloadIcon />
|
||||
<span>{game.repacks.length}</span>
|
||||
<span>
|
||||
{stats ? numberFormatter.format(stats.downloadCount) : "…"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{game.repacks.length > 0 && (
|
||||
<div className={styles.specificsItem}>
|
||||
<FileDirectoryIcon />
|
||||
<span>{game.repacks.at(0)?.fileSize}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.specificsItem}>
|
||||
<PeopleIcon />
|
||||
<span>
|
||||
{stats ? numberFormatter.format(stats?.playerCount) : "…"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from "./link/link";
|
||||
export * from "./select-field/select-field";
|
||||
export * from "./toast/toast";
|
||||
export * from "./badge/badge";
|
||||
export * from "./confirmation-modal/confirmation-modal";
|
||||
|
||||
@@ -34,7 +34,7 @@ export const profileButtonContent = style({
|
||||
export const profileAvatar = style({
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
borderRadius: "50%",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
@@ -52,17 +52,6 @@ export const profileButtonInformation = style({
|
||||
minWidth: 0,
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -85,11 +74,11 @@ export const friendsButton = style({
|
||||
position: "relative",
|
||||
transition: "all ease 0.3s",
|
||||
":hover": {
|
||||
backgroundColor: "#DADBE1",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const friendsButtonLabel = style({
|
||||
export const friendsButtonBadge = style({
|
||||
backgroundColor: vars.color.success,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
|
||||
@@ -2,10 +2,9 @@ import { useNavigate } from "react-router-dom";
|
||||
import { PeopleIcon, PersonIcon } from "@primer/octicons-react";
|
||||
import * as styles from "./sidebar-profile.css";
|
||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import { FriendRequest } from "@types";
|
||||
|
||||
export function SidebarProfile() {
|
||||
const navigate = useNavigate();
|
||||
@@ -14,17 +13,13 @@ export function SidebarProfile() {
|
||||
|
||||
const { userDetails, friendRequests, showFriendsModal } = useUserDetails();
|
||||
|
||||
const [receivedRequests, setReceivedRequests] = useState<FriendRequest[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setReceivedRequests(
|
||||
friendRequests.filter((request) => request.type === "RECEIVED")
|
||||
);
|
||||
const receivedRequests = useMemo(() => {
|
||||
return friendRequests.filter((request) => request.type === "RECEIVED");
|
||||
}, [friendRequests]);
|
||||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
const handleProfileClick = () => {
|
||||
if (userDetails === null) {
|
||||
window.electron.openAuthWindow();
|
||||
return;
|
||||
@@ -33,12 +28,35 @@ export function SidebarProfile() {
|
||||
navigate(`/profile/${userDetails!.id}`);
|
||||
};
|
||||
|
||||
const friendsButton = useMemo(() => {
|
||||
if (!userDetails) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.friendsButton}
|
||||
onClick={() =>
|
||||
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
||||
}
|
||||
title={t("friends")}
|
||||
>
|
||||
{receivedRequests.length > 0 && (
|
||||
<small className={styles.friendsButtonBadge}>
|
||||
{receivedRequests.length > 99 ? "99+" : receivedRequests.length}
|
||||
</small>
|
||||
)}
|
||||
|
||||
<PeopleIcon size={16} />
|
||||
</button>
|
||||
);
|
||||
}, [userDetails, t, receivedRequests, showFriendsModal]);
|
||||
|
||||
return (
|
||||
<div className={styles.profileContainer}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.profileButton}
|
||||
onClick={handleButtonClick}
|
||||
onClick={handleProfileClick}
|
||||
>
|
||||
<div className={styles.profileButtonContent}>
|
||||
<div className={styles.profileAvatar}>
|
||||
@@ -76,17 +94,7 @@ export function SidebarProfile() {
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.friendsButton}
|
||||
onClick={() =>
|
||||
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
||||
}
|
||||
>
|
||||
<small className={styles.friendsButtonLabel}>10</small>
|
||||
|
||||
<PeopleIcon size={16} />
|
||||
</button>
|
||||
{friendsButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ export const sidebar = recipe({
|
||||
pointerEvents: "none",
|
||||
},
|
||||
},
|
||||
darwin: {
|
||||
true: {
|
||||
paddingTop: `${SPACING_UNIT * 6}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -153,12 +153,14 @@ export function Sidebar() {
|
||||
<>
|
||||
<aside
|
||||
ref={sidebarRef}
|
||||
className={styles.sidebar({ resizing: isResizing })}
|
||||
className={styles.sidebar({
|
||||
resizing: isResizing,
|
||||
darwin: window.electron.platform === "darwin",
|
||||
})}
|
||||
style={{
|
||||
width: sidebarWidth,
|
||||
minWidth: sidebarWidth,
|
||||
maxWidth: sidebarWidth,
|
||||
paddingTop: 8 * 6,
|
||||
}}
|
||||
>
|
||||
<SidebarProfile />
|
||||
@@ -180,8 +182,6 @@ export function Sidebar() {
|
||||
>
|
||||
{render(isDownloading)}
|
||||
<span>{t(nameKey)}</span>
|
||||
|
||||
<ChevronDownIcon />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -9,3 +9,5 @@ export const DOWNLOADER_NAME = {
|
||||
[Downloader.PixelDrain]: "PixelDrain",
|
||||
[Downloader.Qiwi]: "Qiwi",
|
||||
};
|
||||
|
||||
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface UserProfileContext {
|
||||
heroBackground: string;
|
||||
/* Indicates if the current user is viewing their own profile */
|
||||
isMe: boolean;
|
||||
|
||||
getUserProfile: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
|
||||
@@ -20,6 +22,7 @@ export const userProfileContext = createContext<UserProfileContext>({
|
||||
userProfile: null,
|
||||
heroBackground: DEFAULT_USER_PROFILE_BACKGROUND,
|
||||
isMe: false,
|
||||
getUserProfile: async () => {},
|
||||
});
|
||||
|
||||
const { Provider } = userProfileContext;
|
||||
@@ -47,7 +50,7 @@ export function UserProfileContextProvider({
|
||||
format: "hex",
|
||||
});
|
||||
|
||||
return `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8, 0.7)})`;
|
||||
return `linear-gradient(135deg, ${darkenColor(output as string, 0.5)}, ${darkenColor(output as string, 0.6, 0.5)})`;
|
||||
};
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
@@ -73,6 +76,9 @@ export function UserProfileContextProvider({
|
||||
}, [navigate, showErrorToast, userId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setUserProfile(null);
|
||||
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
|
||||
|
||||
getUserProfile();
|
||||
}, [getUserProfile]);
|
||||
|
||||
@@ -82,6 +88,7 @@ export function UserProfileContextProvider({
|
||||
userProfile,
|
||||
heroBackground,
|
||||
isMe: userDetails?.id === userProfile?.id,
|
||||
getUserProfile,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
import type { CatalogueCategory } from "@shared";
|
||||
import type {
|
||||
AppUpdaterEvent,
|
||||
CatalogueEntry,
|
||||
@@ -19,6 +20,7 @@ import type {
|
||||
UserFriends,
|
||||
UserBlocks,
|
||||
UpdateProfileRequest,
|
||||
GameStats,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
@@ -40,7 +42,7 @@ declare global {
|
||||
|
||||
/* Catalogue */
|
||||
searchGames: (query: string) => Promise<CatalogueEntry[]>;
|
||||
getCatalogue: () => Promise<CatalogueEntry[]>;
|
||||
getCatalogue: (category: CatalogueCategory) => Promise<CatalogueEntry[]>;
|
||||
getGameShopDetails: (
|
||||
objectID: string,
|
||||
shop: GameShop,
|
||||
@@ -57,6 +59,7 @@ declare global {
|
||||
prevCursor?: number
|
||||
) => Promise<{ results: CatalogueEntry[]; cursor: number }>;
|
||||
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
|
||||
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
|
||||
|
||||
/* Library */
|
||||
addGameToLibrary: (
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
|
||||
import { store } from "./store";
|
||||
|
||||
import * as resources from "@locales";
|
||||
import resources from "@locales";
|
||||
|
||||
Sentry.init({});
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import { useDate, useDownload } from "@renderer/hooks";
|
||||
import { Link } from "@renderer/components";
|
||||
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
|
||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
|
||||
export function HeroPanelPlaytime() {
|
||||
const [lastTimePlayed, setLastTimePlayed] = useState("");
|
||||
@@ -36,7 +35,7 @@ export function HeroPanelPlaytime() {
|
||||
});
|
||||
}, [i18n.language]);
|
||||
|
||||
const formatPlayTime = () => {
|
||||
const formattedPlayTime = useMemo(() => {
|
||||
const milliseconds = game?.playTimeInMilliseconds || 0;
|
||||
const seconds = milliseconds / 1000;
|
||||
const minutes = seconds / 60;
|
||||
@@ -49,7 +48,7 @@ export function HeroPanelPlaytime() {
|
||||
|
||||
const hours = minutes / 60;
|
||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||
};
|
||||
}, [game?.playTimeInMilliseconds, numberFormatter, t]);
|
||||
|
||||
if (!game) return null;
|
||||
|
||||
@@ -96,7 +95,7 @@ export function HeroPanelPlaytime() {
|
||||
<>
|
||||
<p>
|
||||
{t("play_time", {
|
||||
amount: formatPlayTime(),
|
||||
amount: formattedPlayTime,
|
||||
})}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -60,3 +60,11 @@ export const noResults = style({
|
||||
gap: "16px",
|
||||
gridColumn: "1 / -1",
|
||||
});
|
||||
|
||||
export const buttonsList = style({
|
||||
display: "flex",
|
||||
listStyle: "none",
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as styles from "./home.css";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import Lottie from "lottie-react";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { CatalogueCategory } from "@shared";
|
||||
|
||||
export function Home() {
|
||||
const { t } = useTranslation("home");
|
||||
@@ -21,15 +22,25 @@ export function Home() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||
|
||||
const [catalogue, setCatalogue] = useState<CatalogueEntry[]>([]);
|
||||
const [currentCatalogueCategory, setCurrentCatalogueCategory] = useState(
|
||||
CatalogueCategory.Hot
|
||||
);
|
||||
|
||||
const getCatalogue = useCallback(() => {
|
||||
const [catalogue, setCatalogue] = useState<
|
||||
Record<CatalogueCategory, CatalogueEntry[]>
|
||||
>({
|
||||
[CatalogueCategory.Hot]: [],
|
||||
[CatalogueCategory.Weekly]: [],
|
||||
});
|
||||
|
||||
const getCatalogue = useCallback((category: CatalogueCategory) => {
|
||||
setCurrentCatalogueCategory(category);
|
||||
setIsLoading(true);
|
||||
|
||||
window.electron
|
||||
.getCatalogue()
|
||||
.getCatalogue(category)
|
||||
.then((catalogue) => {
|
||||
setCatalogue(catalogue);
|
||||
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
@@ -58,11 +69,13 @@ export function Home() {
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
getCatalogue();
|
||||
getCatalogue(CatalogueCategory.Hot);
|
||||
|
||||
getRandomGame();
|
||||
}, [getCatalogue, getRandomGame]);
|
||||
|
||||
const categories = Object.values(CatalogueCategory);
|
||||
|
||||
return (
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<section className={styles.content}>
|
||||
@@ -71,7 +84,22 @@ export function Home() {
|
||||
<Hero />
|
||||
|
||||
<section className={styles.homeHeader}>
|
||||
<h2>{t("trending")}</h2>
|
||||
<ul className={styles.buttonsList}>
|
||||
{categories.map((category) => (
|
||||
<li key={category}>
|
||||
<Button
|
||||
theme={
|
||||
category === currentCatalogueCategory
|
||||
? "primary"
|
||||
: "outline"
|
||||
}
|
||||
onClick={() => getCatalogue(category)}
|
||||
>
|
||||
{t(category)}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
onClick={handleRandomizerClick}
|
||||
@@ -89,12 +117,14 @@ export function Home() {
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
<h2>{t(currentCatalogueCategory)}</h2>
|
||||
|
||||
<section className={styles.cards}>
|
||||
{isLoading
|
||||
? Array.from({ length: 12 }).map((_, index) => (
|
||||
<Skeleton key={index} className={styles.cardSkeleton} />
|
||||
))
|
||||
: catalogue.map((result) => (
|
||||
: catalogue[currentCatalogueCategory].map((result) => (
|
||||
<GameCard
|
||||
key={result.objectID}
|
||||
game={result}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { vars, SPACING_UNIT } from "../../theme.css";
|
||||
import { vars, SPACING_UNIT } from "../../../theme.css";
|
||||
import { globalStyle, style } from "@vanilla-extract/css";
|
||||
|
||||
export const gameCover = style({
|
||||
transition: "all ease 0.2s",
|
||||
boxShadow: "0 8px 10px -2px rgba(0, 0, 0, 0.5)",
|
||||
width: "100%",
|
||||
":before": {
|
||||
content: "",
|
||||
top: "0",
|
||||
@@ -60,14 +61,66 @@ export const friend = style({
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const friendAvatar = style({
|
||||
width: "50px",
|
||||
height: "50px",
|
||||
borderRadius: "4px",
|
||||
});
|
||||
|
||||
export const friendName = style({
|
||||
color: vars.color.muted,
|
||||
fontWeight: "bold",
|
||||
fontSize: vars.size.body,
|
||||
});
|
||||
|
||||
export const rightContent = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
flexDirection: "column",
|
||||
"@media": {
|
||||
"(min-width: 768px)": {
|
||||
width: "100%",
|
||||
maxWidth: "200px",
|
||||
},
|
||||
"(min-width: 1024px)": {
|
||||
maxWidth: "300px",
|
||||
width: "100%",
|
||||
},
|
||||
"(min-width: 1280px)": {
|
||||
width: "100%",
|
||||
maxWidth: "400px",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const listItem = style({
|
||||
display: "flex",
|
||||
cursor: "pointer",
|
||||
transition: "all ease 0.1s",
|
||||
color: vars.color.muted,
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
alignItems: "center",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
textDecoration: "none",
|
||||
},
|
||||
});
|
||||
|
||||
export const gamesGrid = style({
|
||||
listStyle: "none",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
display: "grid",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
"@media": {
|
||||
"(min-width: 768px)": {
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
},
|
||||
"(min-width: 1250px)": {
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
},
|
||||
"(min-width: 1600px)": {
|
||||
gridTemplateColumns: "repeat(8, 1fr)",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,19 +1,27 @@
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { useContext, useEffect, useMemo } from "react";
|
||||
import { ProfileHero } from "./profile-hero/profile-hero";
|
||||
import { useCallback, useContext, useEffect, useMemo } from "react";
|
||||
import { ProfileHero } from "../profile-hero/profile-hero";
|
||||
import { useAppDispatch } from "@renderer/hooks";
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./profile-content.css";
|
||||
import { ClockIcon, PeopleIcon } from "@primer/octicons-react";
|
||||
import { ClockIcon } from "@primer/octicons-react";
|
||||
import { Link } from "@renderer/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserGame } from "@types";
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function ProfileContent() {
|
||||
const { userProfile } = useContext(userProfileContext);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { i18n, t } = useTranslation("user_profile");
|
||||
|
||||
useEffect(() => {
|
||||
if (userProfile) {
|
||||
dispatch(setHeaderTitle(userProfile.displayName));
|
||||
@@ -21,8 +29,42 @@ export function ProfileContent() {
|
||||
}, [userProfile, dispatch]);
|
||||
|
||||
const truncatedGamesList = useMemo(() => {
|
||||
if (!userProfile) return [];
|
||||
return userProfile?.libraryGames.slice(0, 12);
|
||||
}, [userProfile?.libraryGames]);
|
||||
}, [userProfile]);
|
||||
|
||||
const numberFormatter = useMemo(() => {
|
||||
return new Intl.NumberFormat(i18n.language, {
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
}, [i18n.language]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const formatPlayTime = useCallback(
|
||||
(game: UserGame) => {
|
||||
const seconds = game?.playTimeInSeconds || 0;
|
||||
const minutes = seconds / 60;
|
||||
|
||||
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
|
||||
return t("amount_minutes", {
|
||||
amount: minutes.toFixed(0),
|
||||
});
|
||||
}
|
||||
|
||||
const hours = minutes / 60;
|
||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||
},
|
||||
[numberFormatter, t]
|
||||
);
|
||||
|
||||
const buildUserGameDetailsPath = (game: UserGame) =>
|
||||
buildGameDetailsPath({
|
||||
...game,
|
||||
objectID: game.objectId,
|
||||
});
|
||||
|
||||
if (!userProfile) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -35,23 +77,14 @@ export function ProfileContent() {
|
||||
padding: `${SPACING_UNIT * 3}px`,
|
||||
}}
|
||||
>
|
||||
<div style={{}}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2>Library</h2>
|
||||
<h2>{t("library")}</h2>
|
||||
|
||||
<h3>{userProfile?.libraryGames.length}</h3>
|
||||
<h3>{numberFormatter.format(userProfile.libraryGames.length)}</h3>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
style={{
|
||||
listStyle: "none",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(6, 1fr)",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<ul className={styles.gamesGrid}>
|
||||
{truncatedGamesList.map((game) => (
|
||||
<li
|
||||
key={game.objectId}
|
||||
@@ -64,13 +97,20 @@ export function ProfileContent() {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={{ cursor: "pointer" }}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
className={styles.gameCover}
|
||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||
>
|
||||
<img
|
||||
src={steamUrlBuilder.cover(game.objectId)}
|
||||
alt={game.title}
|
||||
style={{ width: "100%" }}
|
||||
style={{
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
@@ -78,14 +118,7 @@ export function ProfileContent() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
minWidth: 350,
|
||||
display: "flex",
|
||||
gap: SPACING_UNIT * 2,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div className={styles.rightContent}>
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2>Played recently</h2>
|
||||
@@ -94,14 +127,10 @@ export function ProfileContent() {
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
{userProfile?.recentGames.map((game) => (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
<li key={`${game.shop}-${game.objectId}`}>
|
||||
<Link
|
||||
to={buildUserGameDetailsPath(game)}
|
||||
className={styles.listItem}
|
||||
>
|
||||
<img
|
||||
src={game.iconUrl}
|
||||
@@ -113,21 +142,27 @@ export function ProfileContent() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: "bold" }}>{game.title}</span>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<ClockIcon />
|
||||
<span>{game.playTimeInSeconds}</span>
|
||||
<small>{formatPlayTime(game)}</small>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -136,30 +171,31 @@ export function ProfileContent() {
|
||||
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2>Friends</h2>
|
||||
|
||||
<h2>{t("friends")}</h2>
|
||||
<span>{userProfile?.totalFriends}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
{userProfile?.friends.map((friend) => (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
style={{ cursor: "pointer" }}
|
||||
className={styles.friend}
|
||||
<li key={friend.id}>
|
||||
<Link
|
||||
to={`/profile/${friend.id}`}
|
||||
className={styles.listItem}
|
||||
>
|
||||
<img
|
||||
src={friend.profileImageUrl}
|
||||
alt={friend.displayName}
|
||||
style={{ width: "100%" }}
|
||||
className={styles.friendAvatar}
|
||||
style={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
<span className={styles.friendName}>
|
||||
{friend.displayName}
|
||||
</span>
|
||||
</button>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -6,11 +6,11 @@ export const profileContentBox = style({
|
||||
flexDirection: "column",
|
||||
});
|
||||
|
||||
export const profileAvatarContainer = style({
|
||||
export const profileAvatarButton = style({
|
||||
width: "96px",
|
||||
minWidth: "96px",
|
||||
height: "96px",
|
||||
borderRadius: "50%",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
@@ -18,7 +18,11 @@ export const profileAvatarContainer = style({
|
||||
overflow: "hidden",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||
zIndex: 1,
|
||||
cursor: "pointer",
|
||||
transition: "all ease 0.3s",
|
||||
":hover": {
|
||||
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.7)",
|
||||
},
|
||||
});
|
||||
|
||||
export const profileAvatar = style({
|
||||
@@ -44,3 +48,24 @@ export const profileDisplayName = style({
|
||||
textOverflow: "ellipsis",
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const heroPanel = style({
|
||||
width: "100%",
|
||||
height: "72px",
|
||||
minHeight: "72px",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "space-between",
|
||||
backdropFilter: `blur(10px)`,
|
||||
borderTop: `solid 1px rgba(255, 255, 255, 0.1)`,
|
||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.3)",
|
||||
});
|
||||
|
||||
export const userInformation = style({
|
||||
display: "flex",
|
||||
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
@@ -1,101 +1,164 @@
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./profile-hero.css";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { useContext, useMemo, useState } from "react";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import {
|
||||
CheckCircleFillIcon,
|
||||
PencilIcon,
|
||||
PersonIcon,
|
||||
SignOutIcon,
|
||||
XCircleFillIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { Button, Link } from "@renderer/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDate } from "@renderer/hooks";
|
||||
import { useDate, useToast, useUserDetails } from "@renderer/hooks";
|
||||
import { addSeconds } from "date-fns";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import type { FriendRequestAction } from "@types";
|
||||
import { UserProfileSettingsModal } from "../user-profile-settings-modal";
|
||||
|
||||
type FriendAction =
|
||||
| FriendRequestAction
|
||||
| ("BLOCK" | "UNDO_FRIENDSHIP" | "SEND");
|
||||
|
||||
export function ProfileHero() {
|
||||
const { userProfile, heroBackground, isMe } = useContext(userProfileContext);
|
||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||
|
||||
const context = useContext(userProfileContext);
|
||||
const {
|
||||
signOut,
|
||||
updateFriendRequestState,
|
||||
sendFriendRequest,
|
||||
undoFriendship,
|
||||
blockUser,
|
||||
} = useUserDetails();
|
||||
|
||||
const { isMe, heroBackground, getUserProfile } = context;
|
||||
|
||||
const userProfile = context.userProfile!;
|
||||
const { currentGame } = userProfile;
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { formatDistance } = useDate();
|
||||
|
||||
if (!userProfile) return null;
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const { currentGame } = userProfile;
|
||||
const navigate = useNavigate();
|
||||
|
||||
console.log(userProfile);
|
||||
const handleSignOut = async () => {
|
||||
await signOut();
|
||||
|
||||
showSuccessToast(t("successfully_signed_out"));
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const handleFriendAction = (userId: string, action: FriendAction) => {
|
||||
try {
|
||||
if (action === "UNDO_FRIENDSHIP") {
|
||||
undoFriendship(userId).then(getUserProfile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "BLOCK") {
|
||||
blockUser(userId).then(() => {
|
||||
showSuccessToast(t("user_blocked_successfully"));
|
||||
navigate(-1);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "SEND") {
|
||||
sendFriendRequest(userProfile.id).then(getUserProfile);
|
||||
return;
|
||||
}
|
||||
|
||||
updateFriendRequestState(userId, action).then(getUserProfile);
|
||||
} catch (err) {
|
||||
showErrorToast(t("try_again"));
|
||||
}
|
||||
};
|
||||
|
||||
const profileActions = useMemo(() => {
|
||||
if (isMe) {
|
||||
return (
|
||||
<>
|
||||
<Button theme="outline">{t("settings")}</Button>
|
||||
<Button theme="outline" onClick={() => setShowEditProfileModal(true)}>
|
||||
<PencilIcon />
|
||||
{t("edit_profile")}
|
||||
</Button>
|
||||
|
||||
<Button theme="danger">{t("sign_out")}</Button>
|
||||
<Button theme="danger" onClick={handleSignOut}>
|
||||
<SignOutIcon />
|
||||
{t("sign_out")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// if (userProfile.relation == null) {
|
||||
// return (
|
||||
// <>
|
||||
// <Button
|
||||
// theme="outline"
|
||||
// onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
||||
// >
|
||||
// {t("add_friend")}
|
||||
// </Button>
|
||||
if (userProfile.relation == null) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
||||
>
|
||||
{t("add_friend")}
|
||||
</Button>
|
||||
|
||||
// <Button theme="danger" onClick={() => setShowUserBlockModal(true)}>
|
||||
// {t("block_user")}
|
||||
// </Button>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
<Button
|
||||
theme="danger"
|
||||
onClick={() => handleFriendAction(userProfile.id, "BLOCK")}
|
||||
>
|
||||
{t("block_user")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// if (userProfile.relation.status === "ACCEPTED") {
|
||||
// return (
|
||||
// <>
|
||||
// <Button
|
||||
// theme="outline"
|
||||
// // className={styles.cancelRequestButton}
|
||||
// // onClick={() => setShowUndoFriendshipModal(true)}
|
||||
// >
|
||||
// <XCircleFillIcon size={28} /> {t("undo_friendship")}
|
||||
// </Button>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
if (userProfile.relation.status === "ACCEPTED") {
|
||||
return (
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP")}
|
||||
>
|
||||
<XCircleFillIcon />
|
||||
{t("undo_friendship")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// if (userProfile.relation.BId === userProfile.id) {
|
||||
// return (
|
||||
// <Button
|
||||
// theme="outline"
|
||||
// // className={styles.cancelRequestButton}
|
||||
// // onClick={() =>
|
||||
// // handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
||||
// // }
|
||||
// >
|
||||
// <XCircleFillIcon size={28} /> {t("cancel_request")}
|
||||
// </Button>
|
||||
// );
|
||||
// }
|
||||
if (userProfile.relation.BId === userProfile.id) {
|
||||
return (
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() =>
|
||||
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
||||
}
|
||||
>
|
||||
<XCircleFillIcon size={28} /> {t("cancel_request")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
theme="outline"
|
||||
// onClick={() =>
|
||||
// handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||
// }
|
||||
onClick={() =>
|
||||
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||
}
|
||||
>
|
||||
<CheckCircleFillIcon size={28} /> {t("accept_request")}
|
||||
</Button>
|
||||
<Button
|
||||
theme="outline"
|
||||
// onClick={() =>
|
||||
// handleFriendAction(userProfile.relation!.AId, "REFUSED")
|
||||
// }
|
||||
onClick={() =>
|
||||
handleFriendAction(userProfile.relation!.AId, "REFUSED")
|
||||
}
|
||||
>
|
||||
<XCircleFillIcon size={28} /> {t("ignore_request")}
|
||||
</Button>
|
||||
@@ -105,19 +168,27 @@ export function ProfileHero() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <ConfirmationModal
|
||||
visible
|
||||
title={t("sign_out_modal_title")}
|
||||
descriptionText={t("sign_out_modal_text")}
|
||||
confirmButtonLabel={t("sign_out")}
|
||||
cancelButtonLabel={t("cancel")}
|
||||
/> */}
|
||||
|
||||
<UserProfileSettingsModal
|
||||
visible={showEditProfileModal}
|
||||
userProfile={userProfile}
|
||||
updateUserProfile={getUserProfile}
|
||||
onClose={() => setShowEditProfileModal(false)}
|
||||
/>
|
||||
|
||||
<section
|
||||
className={styles.profileContentBox}
|
||||
style={{ background: heroBackground }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<div className={styles.profileAvatarContainer}>
|
||||
<div className={styles.userInformation}>
|
||||
<button type="button" className={styles.profileAvatarButton}>
|
||||
{userProfile.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
@@ -127,12 +198,13 @@ export function ProfileHero() {
|
||||
) : (
|
||||
<PersonIcon size={72} />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className={styles.profileInformation}>
|
||||
<h2 className={styles.profileDisplayName}>
|
||||
{userProfile.displayName}
|
||||
</h2>
|
||||
|
||||
{currentGame && (
|
||||
<div
|
||||
style={{
|
||||
@@ -149,14 +221,23 @@ export function ProfileHero() {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Link to={buildGameDetailsPath(currentGame)}>
|
||||
<Link
|
||||
to={buildGameDetailsPath({
|
||||
...currentGame,
|
||||
objectID: currentGame.objectId,
|
||||
})}
|
||||
>
|
||||
{currentGame.title}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<small>
|
||||
{t("playing_for", {
|
||||
amount: formatDistance(
|
||||
currentGame.sessionDurationInSeconds,
|
||||
addSeconds(
|
||||
new Date(),
|
||||
-currentGame.sessionDurationInSeconds
|
||||
),
|
||||
new Date()
|
||||
),
|
||||
})}
|
||||
@@ -166,20 +247,7 @@ export function ProfileHero() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "72px",
|
||||
minHeight: "72px",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "space-between",
|
||||
backdropFilter: `blur(10px)`,
|
||||
borderTop: `solid 1px rgba(255, 255, 255, 0.1)`,
|
||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
>
|
||||
<div className={styles.heroPanel}>
|
||||
<div></div>
|
||||
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||
{profileActions}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const wrapper = style({
|
||||
@@ -7,297 +7,3 @@ export const wrapper = style({
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
});
|
||||
|
||||
export const profileContentBox = style({
|
||||
display: "flex",
|
||||
cursor: "pointer",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
alignItems: "center",
|
||||
borderRadius: "4px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
width: "100%",
|
||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.7)",
|
||||
transition: "all ease 0.3s",
|
||||
});
|
||||
|
||||
export const profileAvatarContainer = style({
|
||||
width: "96px",
|
||||
minWidth: "96px",
|
||||
height: "96px",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const friendAvatarContainer = style({
|
||||
width: "35px",
|
||||
minWidth: "35px",
|
||||
height: "35px",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
overflow: "hidden",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||
});
|
||||
|
||||
export const friendListDisplayName = style({
|
||||
fontWeight: "bold",
|
||||
fontSize: vars.size.body,
|
||||
textAlign: "left",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
});
|
||||
|
||||
export const profileAvatarEditContainer = style({
|
||||
alignSelf: "center",
|
||||
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({
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: "50%",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const profileAvatarEditOverlay = style({
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "#00000055",
|
||||
color: vars.color.muted,
|
||||
zIndex: 1,
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
export const profileInformation = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
alignItems: "flex-start",
|
||||
color: "#c0c1c7",
|
||||
zIndex: 1,
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const profileDisplayName = style({
|
||||
fontWeight: "bold",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const profileContent = style({
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
flexDirection: "row",
|
||||
gap: `${SPACING_UNIT * 4}px`,
|
||||
});
|
||||
|
||||
export const profileGameSection = style({
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const friendsSection = style({
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const friendsSectionHeader = style({
|
||||
fontSize: vars.size.body,
|
||||
color: vars.color.body,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
":hover": {
|
||||
color: vars.color.muted,
|
||||
},
|
||||
});
|
||||
|
||||
export const contentSidebar = style({
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
"@media": {
|
||||
"(min-width: 768px)": {
|
||||
width: "100%",
|
||||
maxWidth: "150px",
|
||||
},
|
||||
"(min-width: 1024px)": {
|
||||
maxWidth: "250px",
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const feedGameIcon = style({
|
||||
height: "100%",
|
||||
});
|
||||
|
||||
export const libraryGameIcon = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: "4px",
|
||||
});
|
||||
|
||||
export const friendProfileIcon = style({
|
||||
height: "100%",
|
||||
});
|
||||
|
||||
export const feedItem = style({
|
||||
color: vars.color.body,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
height: "72px",
|
||||
transition: "all ease 0.2s",
|
||||
cursor: "pointer",
|
||||
zIndex: "1",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const gameListItem = style({
|
||||
color: vars.color.body,
|
||||
transition: "all ease 0.2s",
|
||||
cursor: "pointer",
|
||||
zIndex: "1",
|
||||
overflow: "hidden",
|
||||
padding: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const friendListContainer = style({
|
||||
color: vars.color.body,
|
||||
width: "100%",
|
||||
height: "54px",
|
||||
padding: `0 ${SPACING_UNIT}px`,
|
||||
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||
transition: "all ease 0.2s",
|
||||
position: "relative",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const gameInformation = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
});
|
||||
|
||||
export const profileHeaderSkeleton = style({
|
||||
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 signOutModalContent = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const signOutModalButtonsContainer = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "end",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
paddingTop: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const profileBackground = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
objectFit: "cover",
|
||||
left: "0",
|
||||
top: "0",
|
||||
borderRadius: "4px",
|
||||
});
|
||||
|
||||
export const cancelRequestButton = style({
|
||||
cursor: "pointer",
|
||||
color: vars.color.body,
|
||||
":hover": {
|
||||
color: vars.color.danger,
|
||||
},
|
||||
});
|
||||
|
||||
export const acceptRequestButton = style({
|
||||
cursor: "pointer",
|
||||
color: vars.color.success,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { ProfileSkeleton } from "./profile-skeleton";
|
||||
import { ProfileContent } from "./profile-content";
|
||||
import { ProfileContent } from "./profile-content/profile-content";
|
||||
import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./profile.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserBlockModalProps {
|
||||
visible: boolean;
|
||||
displayName: string;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const UserBlockModal = ({
|
||||
visible,
|
||||
displayName,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: UserBlockModalProps) => {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("sign_out_modal_title")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.signOutModalContent}>
|
||||
<p>{t("user_block_modal_text", { displayName })}</p>
|
||||
<div className={styles.signOutModalButtonsContainer}>
|
||||
<Button onClick={onConfirm} theme="danger">
|
||||
{t("block_user")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose} theme="primary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./profile.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserConfirmUndoFriendshipModalProps {
|
||||
visible: boolean;
|
||||
displayName: string;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function UserConfirmUndoFriendshipModal({
|
||||
visible,
|
||||
displayName,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: UserConfirmUndoFriendshipModalProps) {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("sign_out_modal_title")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.signOutModalContent}>
|
||||
<p>{t("undo_friendship_modal_text", { displayName })}</p>
|
||||
<div className={styles.signOutModalButtonsContainer}>
|
||||
<Button onClick={onConfirm} theme="danger">
|
||||
{t("undo_friendship")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose} theme="primary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export const UserEditProfile = ({
|
||||
filters: [
|
||||
{
|
||||
name: "Image",
|
||||
extensions: ["jpg", "jpeg", "png", "webp"],
|
||||
extensions: ["jpg", "jpeg", "png"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -44,7 +44,12 @@ export const UserProfileSettingsModal = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal visible={visible} title={t("settings")} onClose={onClose}>
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("settings")}
|
||||
onClose={onClose}
|
||||
clickOutsideToClose={false}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./profile.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserSignOutModalProps {
|
||||
visible: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const UserSignOutModal = ({
|
||||
visible,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: UserSignOutModalProps) => {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("sign_out_modal_title")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.signOutModalContent}>
|
||||
<p>{t("sign_out_modal_text")}</p>
|
||||
<div className={styles.signOutModalButtonsContainer}>
|
||||
<Button onClick={onConfirm} theme="danger">
|
||||
{t("sign_out")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose} theme="primary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import ISO6391 from "iso-639-1";
|
||||
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
@@ -8,11 +6,9 @@ import {
|
||||
SelectField,
|
||||
} from "@renderer/components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAppSelector } from "@renderer/hooks";
|
||||
|
||||
import { changeLanguage } from "i18next";
|
||||
import * as languageResources from "@locales";
|
||||
import languageResources from "@locales";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { settingsContext } from "@renderer/context";
|
||||
|
||||
@@ -50,9 +46,9 @@ export function SettingsGeneral() {
|
||||
|
||||
setLanguageOptions(
|
||||
orderBy(
|
||||
Object.keys(languageResources).map((language) => {
|
||||
Object.entries(languageResources).map(([language, value]) => {
|
||||
return {
|
||||
nativeName: ISO6391.getNativeName(language),
|
||||
nativeName: value.language_name,
|
||||
option: language,
|
||||
};
|
||||
}),
|
||||
@@ -93,8 +89,6 @@ export function SettingsGeneral() {
|
||||
|
||||
function updateFormWithUserPreferences() {
|
||||
if (userPreferences) {
|
||||
const parsedLanguage = userPreferences.language.split("-")[0];
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
downloadsPath: userPreferences.downloadsPath ?? defaultDownloadsPath,
|
||||
@@ -102,7 +96,7 @@ export function SettingsGeneral() {
|
||||
userPreferences.downloadNotificationsEnabled,
|
||||
repackUpdatesNotificationsEnabled:
|
||||
userPreferences.repackUpdatesNotificationsEnabled,
|
||||
language: parsedLanguage,
|
||||
language: userPreferences.language,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,106 +1,27 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
export const container = style({
|
||||
padding: "24px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
alignItems: "flex-start",
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
backgroundColor: vars.color.background,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: `${SPACING_UNIT * 3}px`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
|
||||
borderRadius: "4px",
|
||||
boxShadow: "0px 0px 15px 0px #000000",
|
||||
borderRadius: "8px",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: "1",
|
||||
});
|
||||
|
||||
export const sidebar = style({
|
||||
width: "200px",
|
||||
export const settingsCategories = style({
|
||||
display: "flex",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
borderRadius: "4px",
|
||||
backgroundColor: vars.color.background,
|
||||
minHeight: "500px",
|
||||
flexDirection: "column",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT}px`,
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
|
||||
});
|
||||
|
||||
export const menuGroup = style({
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
});
|
||||
|
||||
export const menu = style({
|
||||
listStyle: "none",
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const menuItem = recipe({
|
||||
base: {
|
||||
transition: "all ease 0.1s",
|
||||
cursor: "pointer",
|
||||
textWrap: "nowrap",
|
||||
display: "flex",
|
||||
color: vars.color.muted,
|
||||
borderRadius: "4px",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
active: {
|
||||
true: {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
},
|
||||
muted: {
|
||||
true: {
|
||||
opacity: vars.opacity.disabled,
|
||||
":hover": {
|
||||
opacity: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const menuItemButton = style({
|
||||
color: "inherit",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
cursor: "pointer",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
padding: `9px ${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const menuItemButtonLabel = style({
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const categoryTitle = style({
|
||||
color: "#ff",
|
||||
fontWeight: "bold",
|
||||
fontSize: "18px",
|
||||
paddingBottom: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Button } from "@renderer/components";
|
||||
|
||||
import * as styles from "./settings.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SettingsRealDebrid } from "./settings-real-debrid";
|
||||
@@ -13,10 +15,12 @@ import {
|
||||
export function Settings() {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const categories = {
|
||||
[t("account")]: [t("my_profile"), t("friends")],
|
||||
Hydra: [t("general"), t("behavior"), t("download_sources"), "Real-Debrid"],
|
||||
};
|
||||
const categories = [
|
||||
t("general"),
|
||||
t("behavior"),
|
||||
t("download_sources"),
|
||||
"Real-Debrid",
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsContextProvider>
|
||||
@@ -40,34 +44,21 @@ export function Settings() {
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<aside className={styles.sidebar}>
|
||||
{Object.entries(categories).map(([category, items]) => (
|
||||
<div key={category} className={styles.menuGroup}>
|
||||
<span className={styles.categoryTitle}>{category}</span>
|
||||
|
||||
<ul className={styles.menu}>
|
||||
{items.map((item, index) => (
|
||||
<li
|
||||
key={`item-${index}`}
|
||||
className={styles.menuItem({
|
||||
active: currentCategoryIndex === index,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={() => setCurrentCategoryIndex(index)}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</aside>
|
||||
|
||||
<div className={styles.content}>
|
||||
<section className={styles.settingsCategories}>
|
||||
{categories.map((category, index) => (
|
||||
<Button
|
||||
key={category}
|
||||
theme={
|
||||
currentCategoryIndex === index ? "primary" : "outline"
|
||||
}
|
||||
onClick={() => setCurrentCategoryIndex(index)}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<h2>{categories[currentCategoryIndex]}</h2>
|
||||
{renderCategory()}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,11 @@ export enum DownloadSourceStatus {
|
||||
Errored,
|
||||
}
|
||||
|
||||
export enum CatalogueCategory {
|
||||
Hot = "hot",
|
||||
Weekly = "weekly",
|
||||
}
|
||||
|
||||
export class UserNotLoggedInError extends Error {
|
||||
constructor() {
|
||||
super("user not logged in");
|
||||
|
||||
@@ -309,7 +309,8 @@ export interface UserRelation {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserProfileCurrentGame extends GameRunning {
|
||||
export interface UserProfileCurrentGame extends Omit<GameRunning, "objectID"> {
|
||||
objectId: string;
|
||||
sessionDurationInSeconds: number;
|
||||
}
|
||||
|
||||
@@ -345,3 +346,8 @@ export interface DownloadSource {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface GameStats {
|
||||
downloadCount: number;
|
||||
playerCount: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user