feat: initialize core components and multi-language support structures
Gitea Auto Deploy / Deploy-Container (push) Successful in 38s
Gitea Auto Deploy / Deploy-Container (push) Successful in 38s
This commit is contained in:
+50
-3
@@ -153,10 +153,28 @@
|
|||||||
"importFreeSRDataSuccess": "FreeSR-Daten erfolgreich importiert",
|
"importFreeSRDataSuccess": "FreeSR-Daten erfolgreich importiert",
|
||||||
"importDatabaseSuccess": "Datenbank erfolgreich importiert",
|
"importDatabaseSuccess": "Datenbank erfolgreich importiert",
|
||||||
"getData": "Daten abrufen",
|
"getData": "Daten abrufen",
|
||||||
|
"fetchingData": "Daten werden abgerufen...",
|
||||||
"import": "Importieren",
|
"import": "Importieren",
|
||||||
"freeSRImport": "FreeSR-Import",
|
"freeSRImport": "FreeSR-Import",
|
||||||
"onlySupportFreeSRJsonFile": "Unterstützt nur FreeSR-JSON-Dateien",
|
"onlySupportFreeSRJsonFile": "Unterstützt nur FreeSR-JSON-Dateien",
|
||||||
"pickAFile": "Datei auswählen",
|
"pickAFile": "Datei auswählen",
|
||||||
|
"changeFile": "Datei ändern",
|
||||||
|
"avatars": "Charaktere",
|
||||||
|
"noFreeSRDataLoaded": "Keine FreeSR-Daten geladen",
|
||||||
|
"dropFreeSRJsonHint": "Ziehe deine FreeSR.json-Datei hierher oder klicke, um sie auszuwählen.",
|
||||||
|
"step1": "SCHRITT 1",
|
||||||
|
"step2": "SCHRITT 2",
|
||||||
|
"step3": "SCHRITT 3",
|
||||||
|
"exportJsonFromFreeSR": "JSON aus FreeSR exportieren",
|
||||||
|
"dropOrSelectFile": "Datei ablegen oder auswählen",
|
||||||
|
"syncCharacterBuilds": "Charakter-Builds synchronisieren",
|
||||||
|
"enterUidPlaceholder": "9-stellige UID eingeben...",
|
||||||
|
"publicCharacters": "Öffentliche Charaktere",
|
||||||
|
"fetchFromEnka": "Von Enka.Network abrufen",
|
||||||
|
"fetchEnkaHint": "Gib oben deine Honkai: Star Rail UID ein, um deine öffentlich ausgestellten Charaktere zu synchronisieren.",
|
||||||
|
"inGameProfile": "Im Spiel: Trailblazer-Profil",
|
||||||
|
"displayCharactersPublicly": "Charaktere öffentlich anzeigen",
|
||||||
|
"enterUidAndSync": "UID eingeben & synchronisieren",
|
||||||
"lightConeSetting": "Lichtkegel-Einstellung",
|
"lightConeSetting": "Lichtkegel-Einstellung",
|
||||||
"relicMaker": "Relikt-Ersteller",
|
"relicMaker": "Relikt-Ersteller",
|
||||||
"pleaseSelectAllOptions": "Bitte wähle alle Optionen",
|
"pleaseSelectAllOptions": "Bitte wähle alle Optionen",
|
||||||
@@ -219,10 +237,40 @@
|
|||||||
"searchStage": "Phase suchen...",
|
"searchStage": "Phase suchen...",
|
||||||
"noStageFound": "Keine Phase gefunden",
|
"noStageFound": "Keine Phase gefunden",
|
||||||
"searchMonster": "Monster suchen...",
|
"searchMonster": "Monster suchen...",
|
||||||
"changeRelic": "Relikt wechseln",
|
"HEAD": "Kopf",
|
||||||
|
"HAND": "Handschuhe",
|
||||||
|
"BODY": "Körper",
|
||||||
|
"FOOT": "Füße",
|
||||||
|
"NECK": "Planarsphäre",
|
||||||
|
"OBJECT": "Verbindungskette",
|
||||||
|
"changeRelic": "Relikt ändern",
|
||||||
"deleteRelic": "Relikt löschen",
|
"deleteRelic": "Relikt löschen",
|
||||||
"deleteRelicConfirm": "Möchtest du das Relikt auf diesem Platz wirklich löschen?",
|
"deleteRelicConfirm": "Möchtest du das Relikt auf diesem Platz wirklich löschen?",
|
||||||
"setEffects": "Effekte einstellen",
|
"confirmDelete": "Löschen bestätigen",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"relicDeletedSuccessfully": "Relikt erfolgreich gelöscht",
|
||||||
|
"profileCreatedSuccessfully": "Profil erfolgreich erstellt",
|
||||||
|
"profileUpdatedSuccessfully": "Profil erfolgreich aktualisiert",
|
||||||
|
"profileDeletedSuccessfully": "Profil erfolgreich gelöscht",
|
||||||
|
"profileSwitched": "Profil gewechselt zu",
|
||||||
|
"deleteProfileConfirm": "Möchtest du das Profil wirklich löschen",
|
||||||
|
"deleteProfile": "Profil löschen",
|
||||||
|
"copyText": "Text kopieren",
|
||||||
|
"copyImage": "Bild kopieren",
|
||||||
|
"processing": "Verarbeite...",
|
||||||
|
"saving": "Speichere...",
|
||||||
|
"buildInfoCopied": "Build-Infos in Zwischenablage kopiert!",
|
||||||
|
"copyFailed": "Kopieren der Build-Infos fehlgeschlagen!",
|
||||||
|
"imageCopiedToClipboard": "Bild in Zwischenablage kopiert!",
|
||||||
|
"clipboardWriteFailed": "Direktes Schreiben in Zwischenablage fehlgeschlagen!",
|
||||||
|
"imageSavedSuccessfully": "Bild erfolgreich gespeichert!",
|
||||||
|
"failedToGenerateImage": "Bildgenerierung fehlgeschlagen!",
|
||||||
|
"errorGeneratingShowcaseCard": "Fehler beim Erstellen der Showcase-Karte!",
|
||||||
|
"feature": "Feature",
|
||||||
|
"fix": "Behebung",
|
||||||
|
"improvement": "Verbesserung",
|
||||||
|
"version": "Version",
|
||||||
|
"setEffects": "Set-Effekte einstellen",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"normal": "Standardangriff",
|
"normal": "Standardangriff",
|
||||||
"bpskill": "Fähigkeit",
|
"bpskill": "Fähigkeit",
|
||||||
@@ -258,7 +306,6 @@
|
|||||||
"upRoll": "Aufwertung hoch",
|
"upRoll": "Aufwertung hoch",
|
||||||
"downRoll": "Aufwertung runter",
|
"downRoll": "Aufwertung runter",
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
"avatars": "Avatare",
|
|
||||||
"quickView": "Schnellansicht",
|
"quickView": "Schnellansicht",
|
||||||
"extraSetting": "Extra-Einstellungen",
|
"extraSetting": "Extra-Einstellungen",
|
||||||
"disableCensorship": "Zensur deaktivieren",
|
"disableCensorship": "Zensur deaktivieren",
|
||||||
|
|||||||
+48
-2
@@ -154,10 +154,28 @@
|
|||||||
"importFreeSRDataSuccess": "Import FreeSR data success",
|
"importFreeSRDataSuccess": "Import FreeSR data success",
|
||||||
"importDatabaseSuccess": "Import database success",
|
"importDatabaseSuccess": "Import database success",
|
||||||
"getData": "Get Data",
|
"getData": "Get Data",
|
||||||
|
"fetchingData": "Fetching...",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"freeSRImport": "FreeSR Import",
|
"freeSRImport": "FreeSR Import",
|
||||||
"onlySupportFreeSRJsonFile": "Only support FreeSR json file",
|
"onlySupportFreeSRJsonFile": "Only support FreeSR json file",
|
||||||
"pickAFile": "Pick a file",
|
"pickAFile": "Pick a file",
|
||||||
|
"changeFile": "Change File",
|
||||||
|
"avatars": "Avatars",
|
||||||
|
"noFreeSRDataLoaded": "No FreeSR Data Loaded",
|
||||||
|
"dropFreeSRJsonHint": "Drop your FreeSR.json file here or click anywhere to select from your device.",
|
||||||
|
"step1": "STEP 1",
|
||||||
|
"step2": "STEP 2",
|
||||||
|
"step3": "STEP 3",
|
||||||
|
"exportJsonFromFreeSR": "Export JSON from FreeSR",
|
||||||
|
"dropOrSelectFile": "Drop or select file",
|
||||||
|
"syncCharacterBuilds": "Sync character builds",
|
||||||
|
"enterUidPlaceholder": "Enter 9-digit UID...",
|
||||||
|
"publicCharacters": "Public Characters",
|
||||||
|
"fetchFromEnka": "Fetch from Enka.Network",
|
||||||
|
"fetchEnkaHint": "Enter your Honkai: Star Rail UID above to sync your public showcase characters.",
|
||||||
|
"inGameProfile": "In-game Trailblazer Profile",
|
||||||
|
"displayCharactersPublicly": "Display characters publicly",
|
||||||
|
"enterUidAndSync": "Enter UID & Sync",
|
||||||
"lightConeSetting": "LightCone Setting",
|
"lightConeSetting": "LightCone Setting",
|
||||||
"relicMaker": "Relic Maker",
|
"relicMaker": "Relic Maker",
|
||||||
"pleaseSelectAllOptions": "Please select all options",
|
"pleaseSelectAllOptions": "Please select all options",
|
||||||
@@ -219,10 +237,39 @@
|
|||||||
"addNewWave": "Add New Wave",
|
"addNewWave": "Add New Wave",
|
||||||
"searchStage": "Search stage...",
|
"searchStage": "Search stage...",
|
||||||
"noStageFound": "No stage found",
|
"noStageFound": "No stage found",
|
||||||
"searchMonster": "Search monster...",
|
"HEAD": "Head",
|
||||||
|
"HAND": "Hands",
|
||||||
|
"BODY": "Body",
|
||||||
|
"FOOT": "Feet",
|
||||||
|
"NECK": "Planar Sphere",
|
||||||
|
"OBJECT": "Link Rope",
|
||||||
"changeRelic": "Change relic",
|
"changeRelic": "Change relic",
|
||||||
"deleteRelic": "Delete relic",
|
"deleteRelic": "Delete relic",
|
||||||
"deleteRelicConfirm": "Are you sure you want to delete relic in slot",
|
"deleteRelicConfirm": "Are you sure you want to delete relic in slot",
|
||||||
|
"confirmDelete": "Confirm Delete",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"relicDeletedSuccessfully": "Relic deleted successfully",
|
||||||
|
"profileCreatedSuccessfully": "Profile created successfully",
|
||||||
|
"profileUpdatedSuccessfully": "Profile updated successfully",
|
||||||
|
"profileDeletedSuccessfully": "Profile deleted successfully",
|
||||||
|
"profileSwitched": "Profile switched to",
|
||||||
|
"deleteProfileConfirm": "Are you sure you want to delete profile",
|
||||||
|
"deleteProfile": "Delete Profile",
|
||||||
|
"copyText": "Copy Text",
|
||||||
|
"copyImage": "Copy Image",
|
||||||
|
"processing": "Processing...",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"buildInfoCopied": "Build info copied to clipboard!",
|
||||||
|
"copyFailed": "Failed to copy build info!",
|
||||||
|
"imageCopiedToClipboard": "Image copied to clipboard!",
|
||||||
|
"clipboardWriteFailed": "Browser clipboard write failed!",
|
||||||
|
"imageSavedSuccessfully": "Image saved successfully!",
|
||||||
|
"failedToGenerateImage": "Failed to generate image!",
|
||||||
|
"errorGeneratingShowcaseCard": "Error generating showcase card!",
|
||||||
|
"feature": "Feature",
|
||||||
|
"fix": "Fix",
|
||||||
|
"improvement": "Improvement",
|
||||||
|
"version": "Version",
|
||||||
"setEffects": "Set Effects",
|
"setEffects": "Set Effects",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"normal": "Basic ATK",
|
"normal": "Basic ATK",
|
||||||
@@ -259,7 +306,6 @@
|
|||||||
"upRoll": "Up Roll",
|
"upRoll": "Up Roll",
|
||||||
"downRoll": "Down Roll",
|
"downRoll": "Down Roll",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"avatars": "Avatars",
|
|
||||||
"quickView": "Quick View",
|
"quickView": "Quick View",
|
||||||
"extraSetting": "Extra Settings",
|
"extraSetting": "Extra Settings",
|
||||||
"disableCensorship": "Disable Censorship",
|
"disableCensorship": "Disable Censorship",
|
||||||
|
|||||||
+49
-2
@@ -153,10 +153,28 @@
|
|||||||
"importFreeSRDataSuccess": "Datos de FreeSR importados con éxito",
|
"importFreeSRDataSuccess": "Datos de FreeSR importados con éxito",
|
||||||
"importDatabaseSuccess": "Base de datos importada con éxito",
|
"importDatabaseSuccess": "Base de datos importada con éxito",
|
||||||
"getData": "Obtener Datos",
|
"getData": "Obtener Datos",
|
||||||
|
"fetchingData": "Obteniendo datos...",
|
||||||
"import": "Importar",
|
"import": "Importar",
|
||||||
"freeSRImport": "Importar FreeSR",
|
"freeSRImport": "Importar FreeSR",
|
||||||
"onlySupportFreeSRJsonFile": "Solo soporta archivos JSON de FreeSR",
|
"onlySupportFreeSRJsonFile": "Solo soporta archivos JSON de FreeSR",
|
||||||
"pickAFile": "Elegir un archivo",
|
"pickAFile": "Elegir un archivo",
|
||||||
|
"changeFile": "Cambiar archivo",
|
||||||
|
"avatars": "Personajes",
|
||||||
|
"noFreeSRDataLoaded": "No se han cargado datos de FreeSR",
|
||||||
|
"dropFreeSRJsonHint": "Arrastra tu archivo FreeSR.json aquí o haz clic para seleccionarlo.",
|
||||||
|
"step1": "PASO 1",
|
||||||
|
"step2": "PASO 2",
|
||||||
|
"step3": "PASO 3",
|
||||||
|
"exportJsonFromFreeSR": "Exportar JSON de FreeSR",
|
||||||
|
"dropOrSelectFile": "Soltar o seleccionar archivo",
|
||||||
|
"syncCharacterBuilds": "Sincronizar builds de personajes",
|
||||||
|
"enterUidPlaceholder": "Ingresar UID de 9 dígitos...",
|
||||||
|
"publicCharacters": "Personajes públicos",
|
||||||
|
"fetchFromEnka": "Obtener de Enka.Network",
|
||||||
|
"fetchEnkaHint": "Ingresa tu UID de Honkai: Star Rail arriba para sincronizar tus personajes públicos.",
|
||||||
|
"inGameProfile": "Perfil de Trazacaminos en el juego",
|
||||||
|
"displayCharactersPublicly": "Mostrar personajes públicamente",
|
||||||
|
"enterUidAndSync": "Ingresar UID y sincronizar",
|
||||||
"lightConeSetting": "Ajuste de Cono de Luz",
|
"lightConeSetting": "Ajuste de Cono de Luz",
|
||||||
"relicMaker": "Creador de Artefactos",
|
"relicMaker": "Creador de Artefactos",
|
||||||
"pleaseSelectAllOptions": "Por favor selecciona todas las opciones",
|
"pleaseSelectAllOptions": "Por favor selecciona todas las opciones",
|
||||||
@@ -219,10 +237,40 @@
|
|||||||
"searchStage": "Buscar etapa...",
|
"searchStage": "Buscar etapa...",
|
||||||
"noStageFound": "No se encontró etapa",
|
"noStageFound": "No se encontró etapa",
|
||||||
"searchMonster": "Buscar monstruo...",
|
"searchMonster": "Buscar monstruo...",
|
||||||
|
"HEAD": "Cabeza",
|
||||||
|
"HAND": "Manos",
|
||||||
|
"BODY": "Cuerpo",
|
||||||
|
"FOOT": "Pies",
|
||||||
|
"NECK": "Esfera planar",
|
||||||
|
"OBJECT": "Cuerda de unión",
|
||||||
"changeRelic": "Cambiar artefacto",
|
"changeRelic": "Cambiar artefacto",
|
||||||
"deleteRelic": "Borrar artefacto",
|
"deleteRelic": "Borrar artefacto",
|
||||||
"deleteRelicConfirm": "¿Seguro que quieres borrar el artefacto en este espacio?",
|
"deleteRelicConfirm": "¿Seguro que quieres borrar el artefacto en este espacio?",
|
||||||
"setEffects": "Configurar Efectos",
|
"confirmDelete": "Confirmar eliminación",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"relicDeletedSuccessfully": "Artefacto eliminado con éxito",
|
||||||
|
"profileCreatedSuccessfully": "Perfil creado con éxito",
|
||||||
|
"profileUpdatedSuccessfully": "Perfil actualizado con éxito",
|
||||||
|
"profileDeletedSuccessfully": "Perfil eliminado con éxito",
|
||||||
|
"profileSwitched": "Perfil cambiado a",
|
||||||
|
"deleteProfileConfirm": "¿Seguro que quieres borrar el perfil",
|
||||||
|
"deleteProfile": "Borrar perfil",
|
||||||
|
"copyText": "Copiar texto",
|
||||||
|
"copyImage": "Copiar imagen",
|
||||||
|
"processing": "Procesando...",
|
||||||
|
"saving": "Guardando...",
|
||||||
|
"buildInfoCopied": "¡Información de la build copiada al portapapeles!",
|
||||||
|
"copyFailed": "¡Error al copiar la información de la build!",
|
||||||
|
"imageCopiedToClipboard": "¡Imagen copiada al portapapeles!",
|
||||||
|
"clipboardWriteFailed": "¡Error al escribir en el portapapeles!",
|
||||||
|
"imageSavedSuccessfully": "¡Imagen guardada con éxito!",
|
||||||
|
"failedToGenerateImage": "¡Error al generar la imagen!",
|
||||||
|
"errorGeneratingShowcaseCard": "¡Error al generar la tarjeta showcase!",
|
||||||
|
"feature": "Función",
|
||||||
|
"fix": "Corrección",
|
||||||
|
"improvement": "Mejora",
|
||||||
|
"version": "Versión",
|
||||||
|
"setEffects": "Efectos de conjunto",
|
||||||
"details": "Detalles",
|
"details": "Detalles",
|
||||||
"normal": "Ataque Básico",
|
"normal": "Ataque Básico",
|
||||||
"bpskill": "Habilidad",
|
"bpskill": "Habilidad",
|
||||||
@@ -258,7 +306,6 @@
|
|||||||
"upRoll": "Subir Mejora",
|
"upRoll": "Subir Mejora",
|
||||||
"downRoll": "Bajar Mejora",
|
"downRoll": "Bajar Mejora",
|
||||||
"actions": "Acciones",
|
"actions": "Acciones",
|
||||||
"avatars": "Avatares",
|
|
||||||
"quickView": "Vista Rápida",
|
"quickView": "Vista Rápida",
|
||||||
"extraSetting": "Ajustes Extra",
|
"extraSetting": "Ajustes Extra",
|
||||||
"disableCensorship": "Desactivar Censura",
|
"disableCensorship": "Desactivar Censura",
|
||||||
|
|||||||
+50
-3
@@ -153,10 +153,28 @@
|
|||||||
"importFreeSRDataSuccess": "Importation FreeSR réussie",
|
"importFreeSRDataSuccess": "Importation FreeSR réussie",
|
||||||
"importDatabaseSuccess": "Importation de base de données réussie",
|
"importDatabaseSuccess": "Importation de base de données réussie",
|
||||||
"getData": "Obtenir des données",
|
"getData": "Obtenir des données",
|
||||||
|
"fetchingData": "Récupération...",
|
||||||
"import": "Importer",
|
"import": "Importer",
|
||||||
"freeSRImport": "Importation FreeSR",
|
"freeSRImport": "Importation FreeSR",
|
||||||
"onlySupportFreeSRJsonFile": "Supporte uniquement les fichiers JSON FreeSR",
|
"onlySupportFreeSRJsonFile": "Supporte uniquement les fichiers JSON FreeSR",
|
||||||
"pickAFile": "Choisir un fichier",
|
"pickAFile": "Choisir un fichier",
|
||||||
|
"changeFile": "Changer de fichier",
|
||||||
|
"avatars": "Personnages",
|
||||||
|
"noFreeSRDataLoaded": "Aucune donnée FreeSR chargée",
|
||||||
|
"dropFreeSRJsonHint": "Déposez votre fichier FreeSR.json ici ou cliquez pour le sélectionner.",
|
||||||
|
"step1": "ÉTAPE 1",
|
||||||
|
"step2": "ÉTAPE 2",
|
||||||
|
"step3": "ÉTAPE 3",
|
||||||
|
"exportJsonFromFreeSR": "Exporter le JSON depuis FreeSR",
|
||||||
|
"dropOrSelectFile": "Déposer ou sélectionner le fichier",
|
||||||
|
"syncCharacterBuilds": "Synchroniser les builds des personnages",
|
||||||
|
"enterUidPlaceholder": "Entrez l'UID à 9 chiffres...",
|
||||||
|
"publicCharacters": "Personnages publics",
|
||||||
|
"fetchFromEnka": "Récupérer depuis Enka.Network",
|
||||||
|
"fetchEnkaHint": "Entrez votre UID Honkai: Star Rail ci-dessus pour synchroniser vos personnages exposés.",
|
||||||
|
"inGameProfile": "Profil de Pionnier en jeu",
|
||||||
|
"displayCharactersPublicly": "Afficher les personnages publiquement",
|
||||||
|
"enterUidAndSync": "Entrez l'UID et synchronisez",
|
||||||
"lightConeSetting": "Paramètre de Cône de Lumière",
|
"lightConeSetting": "Paramètre de Cône de Lumière",
|
||||||
"relicMaker": "Créateur de Reliques",
|
"relicMaker": "Créateur de Reliques",
|
||||||
"pleaseSelectAllOptions": "Veuillez sélectionner toutes les options",
|
"pleaseSelectAllOptions": "Veuillez sélectionner toutes les options",
|
||||||
@@ -218,11 +236,41 @@
|
|||||||
"addNewWave": "Ajouter une nouvelle vague",
|
"addNewWave": "Ajouter une nouvelle vague",
|
||||||
"searchStage": "Chercher un niveau...",
|
"searchStage": "Chercher un niveau...",
|
||||||
"noStageFound": "Aucun niveau trouvé",
|
"noStageFound": "Aucun niveau trouvé",
|
||||||
"searchMonster": "Chercher un monstre...",
|
"searchMonster": "Rechercher un monstre...",
|
||||||
|
"HEAD": "Tête",
|
||||||
|
"HAND": "Mains",
|
||||||
|
"BODY": "Corps",
|
||||||
|
"FOOT": "Pieds",
|
||||||
|
"NECK": "Sphère planaire",
|
||||||
|
"OBJECT": "Corde de liaison",
|
||||||
"changeRelic": "Changer de relique",
|
"changeRelic": "Changer de relique",
|
||||||
"deleteRelic": "Supprimer la relique",
|
"deleteRelic": "Supprimer la relique",
|
||||||
"deleteRelicConfirm": "Êtes-vous sûr de vouloir supprimer la relique de cet emplacement ?",
|
"deleteRelicConfirm": "Êtes-vous sûr de vouloir supprimer la relique de cet emplacement ?",
|
||||||
"setEffects": "Définir les effets",
|
"confirmDelete": "Confirmer la suppression",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"relicDeletedSuccessfully": "Relique supprimée avec succès",
|
||||||
|
"profileCreatedSuccessfully": "Profil créé avec succès",
|
||||||
|
"profileUpdatedSuccessfully": "Profil mis à jour avec succès",
|
||||||
|
"profileDeletedSuccessfully": "Profil supprimé avec succès",
|
||||||
|
"profileSwitched": "Profil changé pour",
|
||||||
|
"deleteProfileConfirm": "Êtes-vous sûr de vouloir supprimer le profil",
|
||||||
|
"deleteProfile": "Supprimer le profil",
|
||||||
|
"copyText": "Copier le texte",
|
||||||
|
"copyImage": "Copier l'image",
|
||||||
|
"processing": "Traitement...",
|
||||||
|
"saving": "Sauvegarde...",
|
||||||
|
"buildInfoCopied": "Informations du build copiées dans le presse-papier !",
|
||||||
|
"copyFailed": "Échec de la copie des informations du build !",
|
||||||
|
"imageCopiedToClipboard": "Image copiée dans le presse-papier !",
|
||||||
|
"clipboardWriteFailed": "Échec de l'écriture dans le presse-papier !",
|
||||||
|
"imageSavedSuccessfully": "Image enregistrée avec succès !",
|
||||||
|
"failedToGenerateImage": "Échec de la génération de l'image !",
|
||||||
|
"errorGeneratingShowcaseCard": "Erreur lors de la génération de la carte showcase !",
|
||||||
|
"feature": "Fonctionnalité",
|
||||||
|
"fix": "Correction",
|
||||||
|
"improvement": "Amélioration",
|
||||||
|
"version": "Version",
|
||||||
|
"setEffects": "Effets de panoplie",
|
||||||
"details": "Détails",
|
"details": "Détails",
|
||||||
"normal": "Attaque Normale",
|
"normal": "Attaque Normale",
|
||||||
"bpskill": "Compétence",
|
"bpskill": "Compétence",
|
||||||
@@ -258,7 +306,6 @@
|
|||||||
"upRoll": "Augmenter l'amélioration",
|
"upRoll": "Augmenter l'amélioration",
|
||||||
"downRoll": "Diminuer l'amélioration",
|
"downRoll": "Diminuer l'amélioration",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"avatars": "Avatars",
|
|
||||||
"quickView": "Aperçu rapide",
|
"quickView": "Aperçu rapide",
|
||||||
"extraSetting": "Paramètres supplémentaires",
|
"extraSetting": "Paramètres supplémentaires",
|
||||||
"disableCensorship": "Désactiver la censure",
|
"disableCensorship": "Désactiver la censure",
|
||||||
|
|||||||
+50
-3
@@ -153,10 +153,28 @@
|
|||||||
"importFreeSRDataSuccess": "Impor data FreeSR berhasil",
|
"importFreeSRDataSuccess": "Impor data FreeSR berhasil",
|
||||||
"importDatabaseSuccess": "Impor database berhasil",
|
"importDatabaseSuccess": "Impor database berhasil",
|
||||||
"getData": "Dapatkan Data",
|
"getData": "Dapatkan Data",
|
||||||
|
"fetchingData": "Mengambil data...",
|
||||||
"import": "Impor",
|
"import": "Impor",
|
||||||
"freeSRImport": "Impor FreeSR",
|
"freeSRImport": "Impor FreeSR",
|
||||||
"onlySupportFreeSRJsonFile": "Hanya mendukung file JSON FreeSR",
|
"onlySupportFreeSRJsonFile": "Hanya mendukung file JSON FreeSR",
|
||||||
"pickAFile": "Pilih sebuah file",
|
"pickAFile": "Pilih sebuah file",
|
||||||
|
"changeFile": "Ganti file",
|
||||||
|
"avatars": "Karakter",
|
||||||
|
"noFreeSRDataLoaded": "Tidak ada data FreeSR yang dimuat",
|
||||||
|
"dropFreeSRJsonHint": "Tarik file FreeSR.json Anda ke sini atau klik untuk memilih file.",
|
||||||
|
"step1": "LANGKAH 1",
|
||||||
|
"step2": "LANGKAH 2",
|
||||||
|
"step3": "LANGKAH 3",
|
||||||
|
"exportJsonFromFreeSR": "Ekspor JSON dari FreeSR",
|
||||||
|
"dropOrSelectFile": "Tarik atau pilih file",
|
||||||
|
"syncCharacterBuilds": "Sinkronkan build karakter",
|
||||||
|
"enterUidPlaceholder": "Masukkan 9 digit UID...",
|
||||||
|
"publicCharacters": "Karakter Publik",
|
||||||
|
"fetchFromEnka": "Ambil dari Enka.Network",
|
||||||
|
"fetchEnkaHint": "Masukkan UID Honkai: Star Rail Anda di atas untuk menyinkronkan karakter publik.",
|
||||||
|
"inGameProfile": "Profil Trailblazer dalam game",
|
||||||
|
"displayCharactersPublicly": "Tampilkan karakter secara publik",
|
||||||
|
"enterUidAndSync": "Masukkan UID & Sinkronkan",
|
||||||
"lightConeSetting": "Pengaturan Light Cone",
|
"lightConeSetting": "Pengaturan Light Cone",
|
||||||
"relicMaker": "Pembuat Relic",
|
"relicMaker": "Pembuat Relic",
|
||||||
"pleaseSelectAllOptions": "Silakan pilih semua opsi",
|
"pleaseSelectAllOptions": "Silakan pilih semua opsi",
|
||||||
@@ -219,10 +237,40 @@
|
|||||||
"searchStage": "Cari tahap...",
|
"searchStage": "Cari tahap...",
|
||||||
"noStageFound": "Tahap tidak ditemukan",
|
"noStageFound": "Tahap tidak ditemukan",
|
||||||
"searchMonster": "Cari monster...",
|
"searchMonster": "Cari monster...",
|
||||||
"changeRelic": "Ganti relic",
|
"HEAD": "Kepala",
|
||||||
|
"HAND": "Tangan",
|
||||||
|
"BODY": "Badan",
|
||||||
|
"FOOT": "Kaki",
|
||||||
|
"NECK": "Bola Planar",
|
||||||
|
"OBJECT": "Tali Penghubung",
|
||||||
|
"changeRelic": "Ubah relic",
|
||||||
"deleteRelic": "Hapus relic",
|
"deleteRelic": "Hapus relic",
|
||||||
"deleteRelicConfirm": "Yakin ingin menghapus relic di slot ini?",
|
"deleteRelicConfirm": "Yakin ingin menghapus relic di slot ini?",
|
||||||
"setEffects": "Atur Efek",
|
"confirmDelete": "Konfirmasi Hapus",
|
||||||
|
"cancel": "Batal",
|
||||||
|
"relicDeletedSuccessfully": "Relic berhasil dihapus",
|
||||||
|
"profileCreatedSuccessfully": "Profil berhasil dibuat",
|
||||||
|
"profileUpdatedSuccessfully": "Profil berhasil diperbarui",
|
||||||
|
"profileDeletedSuccessfully": "Profil berhasil dihapus",
|
||||||
|
"profileSwitched": "Profil beralih ke",
|
||||||
|
"deleteProfileConfirm": "Yakin ingin menghapus profil",
|
||||||
|
"deleteProfile": "Hapus profil",
|
||||||
|
"copyText": "Salin Teks",
|
||||||
|
"copyImage": "Salin Gambar",
|
||||||
|
"processing": "Memproses...",
|
||||||
|
"saving": "Menyimpan...",
|
||||||
|
"buildInfoCopied": "Info build berhasil disalin ke papan klip!",
|
||||||
|
"copyFailed": "Gagal menyalin info build!",
|
||||||
|
"imageCopiedToClipboard": "Gambar berhasil disalin ke papan klip!",
|
||||||
|
"clipboardWriteFailed": "Gagal menulis ke papan klip browser!",
|
||||||
|
"imageSavedSuccessfully": "Gambar berhasil disimpan!",
|
||||||
|
"failedToGenerateImage": "Gagal membuat gambar!",
|
||||||
|
"errorGeneratingShowcaseCard": "Gagal membuat kartu showcase!",
|
||||||
|
"feature": "Fitur",
|
||||||
|
"fix": "Perbaikan",
|
||||||
|
"improvement": "Peningkatan",
|
||||||
|
"version": "Versi",
|
||||||
|
"setEffects": "Efek Set",
|
||||||
"details": "Detail",
|
"details": "Detail",
|
||||||
"normal": "Basic ATK",
|
"normal": "Basic ATK",
|
||||||
"bpskill": "Skill",
|
"bpskill": "Skill",
|
||||||
@@ -258,7 +306,6 @@
|
|||||||
"upRoll": "Up Roll",
|
"upRoll": "Up Roll",
|
||||||
"downRoll": "Down Roll",
|
"downRoll": "Down Roll",
|
||||||
"actions": "Aksi",
|
"actions": "Aksi",
|
||||||
"avatars": "Avatar",
|
|
||||||
"quickView": "Tampilan Cepat",
|
"quickView": "Tampilan Cepat",
|
||||||
"extraSetting": "Pengaturan Ekstra",
|
"extraSetting": "Pengaturan Ekstra",
|
||||||
"disableCensorship": "Nonaktifkan Sensor",
|
"disableCensorship": "Nonaktifkan Sensor",
|
||||||
|
|||||||
+50
-3
@@ -153,10 +153,28 @@
|
|||||||
"importFreeSRDataSuccess": "FreeSR データのインポート成功",
|
"importFreeSRDataSuccess": "FreeSR データのインポート成功",
|
||||||
"importDatabaseSuccess": "データベースのインポート成功",
|
"importDatabaseSuccess": "データベースのインポート成功",
|
||||||
"getData": "データ取得",
|
"getData": "データ取得",
|
||||||
|
"fetchingData": "データを取得中...",
|
||||||
"import": "インポート",
|
"import": "インポート",
|
||||||
"freeSRImport": "FreeSR インポート",
|
"freeSRImport": "FreeSR インポート",
|
||||||
"onlySupportFreeSRJsonFile": "FreeSR JSON ファイルのみ対応",
|
"onlySupportFreeSRJsonFile": "FreeSR JSON ファイルのみ対応",
|
||||||
"pickAFile": "ファイル選択",
|
"pickAFile": "ファイル選択",
|
||||||
|
"changeFile": "ファイルを変更",
|
||||||
|
"avatars": "キャラクター",
|
||||||
|
"noFreeSRDataLoaded": "FreeSR データが読み込まれていません",
|
||||||
|
"dropFreeSRJsonHint": "FreeSR.json ファイルをここにドラッグ&ドロップするか、クリックして選択してください。",
|
||||||
|
"step1": "ステップ 1",
|
||||||
|
"step2": "ステップ 2",
|
||||||
|
"step3": "ステップ 3",
|
||||||
|
"exportJsonFromFreeSR": "FreeSR から JSON をエクスポート",
|
||||||
|
"dropOrSelectFile": "ファイルをドロップまたは選択",
|
||||||
|
"syncCharacterBuilds": "キャラのビルドを同期",
|
||||||
|
"enterUidPlaceholder": "9桁のUIDを入力...",
|
||||||
|
"publicCharacters": "公開キャラクター",
|
||||||
|
"fetchFromEnka": "Enka.Network から取得",
|
||||||
|
"fetchEnkaHint": "上に崩壊:スターレイルのUIDを入力して、公開ショーケースのキャラクターを同期します。",
|
||||||
|
"inGameProfile": "ゲーム内の開拓者プロフィール",
|
||||||
|
"displayCharactersPublicly": "キャラクターを公開設定にする",
|
||||||
|
"enterUidAndSync": "UIDを入力して同期",
|
||||||
"lightConeSetting": "ライトコーン設定",
|
"lightConeSetting": "ライトコーン設定",
|
||||||
"relicMaker": "遺物製作",
|
"relicMaker": "遺物製作",
|
||||||
"pleaseSelectAllOptions": "すべてのオプションを選択してください",
|
"pleaseSelectAllOptions": "すべてのオプションを選択してください",
|
||||||
@@ -218,10 +236,40 @@
|
|||||||
"searchStage": "ステージを検索...",
|
"searchStage": "ステージを検索...",
|
||||||
"noStageFound": "ステージが見つかりません",
|
"noStageFound": "ステージが見つかりません",
|
||||||
"searchMonster": "敵を検索...",
|
"searchMonster": "敵を検索...",
|
||||||
|
"HEAD": "頭部",
|
||||||
|
"HAND": "手部",
|
||||||
|
"BODY": "胴体",
|
||||||
|
"FOOT": "脚部",
|
||||||
|
"NECK": "次元界オーブ",
|
||||||
|
"OBJECT": "連結縄",
|
||||||
"changeRelic": "遺物を変更",
|
"changeRelic": "遺物を変更",
|
||||||
"deleteRelic": "遺物を削除",
|
"deleteRelic": "遺物を削除",
|
||||||
"deleteRelicConfirm": "この遺物を削除してもよろしいですか?",
|
"deleteRelicConfirm": "このスロットの遺物を削除してもよろしいですか?",
|
||||||
"setEffects": "効果を設定",
|
"confirmDelete": "削除の確認",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"relicDeletedSuccessfully": "遺物を削除しました",
|
||||||
|
"profileCreatedSuccessfully": "プロファイルを作成しました",
|
||||||
|
"profileUpdatedSuccessfully": "プロファイルを更新しました",
|
||||||
|
"profileDeletedSuccessfully": "プロファイルを削除しました",
|
||||||
|
"profileSwitched": "プロファイルを切り替えました",
|
||||||
|
"deleteProfileConfirm": "このプロファイルを削除してもよろしいですか",
|
||||||
|
"deleteProfile": "プロファイルを削除",
|
||||||
|
"copyText": "テキストをコピー",
|
||||||
|
"copyImage": "画像をコピー",
|
||||||
|
"processing": "処理中...",
|
||||||
|
"saving": "保存中...",
|
||||||
|
"buildInfoCopied": "ビルド情報をクリップボードにコピーしました!",
|
||||||
|
"copyFailed": "ビルド情報のコピーに失敗しました!",
|
||||||
|
"imageCopiedToClipboard": "画像をクリップボードにコピーしました!",
|
||||||
|
"clipboardWriteFailed": "クリップボードへの書き込みに失敗しました!",
|
||||||
|
"imageSavedSuccessfully": "画像を保存しました!",
|
||||||
|
"failedToGenerateImage": "画像の生成に失敗しました!",
|
||||||
|
"errorGeneratingShowcaseCard": "ショーケースカードの生成中にエラーが発生しました!",
|
||||||
|
"feature": "機能",
|
||||||
|
"fix": "修正",
|
||||||
|
"improvement": "改善",
|
||||||
|
"version": "バージョン",
|
||||||
|
"setEffects": "セット効果",
|
||||||
"details": "詳細",
|
"details": "詳細",
|
||||||
"normal": "通常攻撃",
|
"normal": "通常攻撃",
|
||||||
"bpskill": "戦闘スキル",
|
"bpskill": "戦闘スキル",
|
||||||
@@ -257,7 +305,6 @@
|
|||||||
"upRoll": "サブステータスを増やす",
|
"upRoll": "サブステータスを増やす",
|
||||||
"downRoll": "サブステータスを減らす",
|
"downRoll": "サブステータスを減らす",
|
||||||
"actions": "アクション",
|
"actions": "アクション",
|
||||||
"avatars": "アバター",
|
|
||||||
"quickView": "クイックビュー",
|
"quickView": "クイックビュー",
|
||||||
"extraSetting": "追加設定",
|
"extraSetting": "追加設定",
|
||||||
"disableCensorship": "検閲を無効化",
|
"disableCensorship": "検閲を無効化",
|
||||||
|
|||||||
+50
-3
@@ -153,10 +153,28 @@
|
|||||||
"importFreeSRDataSuccess": "FreeSR 데이터 가져오기 성공",
|
"importFreeSRDataSuccess": "FreeSR 데이터 가져오기 성공",
|
||||||
"importDatabaseSuccess": "데이터베이스 가져오기 성공",
|
"importDatabaseSuccess": "데이터베이스 가져오기 성공",
|
||||||
"getData": "데이터 가져오기",
|
"getData": "데이터 가져오기",
|
||||||
|
"fetchingData": "데이터 가져오는 중...",
|
||||||
"import": "가져오기",
|
"import": "가져오기",
|
||||||
"freeSRImport": "FreeSR 가져오기",
|
"freeSRImport": "FreeSR 가져오기",
|
||||||
"onlySupportFreeSRJsonFile": "FreeSR JSON 파일만 지원",
|
"onlySupportFreeSRJsonFile": "FreeSR JSON 파일만 지원",
|
||||||
"pickAFile": "파일 선택",
|
"pickAFile": "파일 선택",
|
||||||
|
"changeFile": "파일 변경",
|
||||||
|
"avatars": "캐릭터",
|
||||||
|
"noFreeSRDataLoaded": "FreeSR 데이터가 로드되지 않음",
|
||||||
|
"dropFreeSRJsonHint": "FreeSR.json 파일을 여기에 끌어다 놓거나 클릭하여 선택하세요.",
|
||||||
|
"step1": "1단계",
|
||||||
|
"step2": "2단계",
|
||||||
|
"step3": "3단계",
|
||||||
|
"exportJsonFromFreeSR": "FreeSR에서 JSON 내보내기",
|
||||||
|
"dropOrSelectFile": "파일을 끌어다 놓거나 선택",
|
||||||
|
"syncCharacterBuilds": "캐릭터 빌드 동기화",
|
||||||
|
"enterUidPlaceholder": "9자리 UID 입력...",
|
||||||
|
"publicCharacters": "공개 캐릭터",
|
||||||
|
"fetchFromEnka": "Enka.Network에서 가져오기",
|
||||||
|
"fetchEnkaHint": "위에 붕괴: 스타레일 UID를 입력하여 공개 쇼케이스 캐릭터를 동기화하세요.",
|
||||||
|
"inGameProfile": "인게임 개척자 프로필",
|
||||||
|
"displayCharactersPublicly": "캐릭터 공개 설정",
|
||||||
|
"enterUidAndSync": "UID 입력 및 동기화",
|
||||||
"lightConeSetting": "라이트콘 설정",
|
"lightConeSetting": "라이트콘 설정",
|
||||||
"relicMaker": "유물 제작",
|
"relicMaker": "유물 제작",
|
||||||
"pleaseSelectAllOptions": "모든 옵션을 선택해주세요",
|
"pleaseSelectAllOptions": "모든 옵션을 선택해주세요",
|
||||||
@@ -217,11 +235,41 @@
|
|||||||
"addNewWave": "새 웨이브 추가",
|
"addNewWave": "새 웨이브 추가",
|
||||||
"searchStage": "스테이지 검색...",
|
"searchStage": "스테이지 검색...",
|
||||||
"noStageFound": "스테이지를 찾을 수 없음",
|
"noStageFound": "스테이지를 찾을 수 없음",
|
||||||
"searchMonster": "적 검색...",
|
"searchMonster": "몬스터 검색...",
|
||||||
|
"HEAD": "머리",
|
||||||
|
"HAND": "손",
|
||||||
|
"BODY": "몸통",
|
||||||
|
"FOOT": "발",
|
||||||
|
"NECK": "차원 구체",
|
||||||
|
"OBJECT": "연결 매듭",
|
||||||
"changeRelic": "유물 변경",
|
"changeRelic": "유물 변경",
|
||||||
"deleteRelic": "유물 삭제",
|
"deleteRelic": "유물 삭제",
|
||||||
"deleteRelicConfirm": "이 슬롯의 유물을 삭제하시겠습니까?",
|
"deleteRelicConfirm": "이 슬롯의 유물을 삭제하시겠습니까?",
|
||||||
"setEffects": "효과 설정",
|
"confirmDelete": "삭제 확인",
|
||||||
|
"cancel": "취소",
|
||||||
|
"relicDeletedSuccessfully": "유물이 성공적으로 삭제되었습니다",
|
||||||
|
"profileCreatedSuccessfully": "프로필이 성공적으로 생성되었습니다",
|
||||||
|
"profileUpdatedSuccessfully": "프로필이 성공적으로 수정되었습니다",
|
||||||
|
"profileDeletedSuccessfully": "프로필이 성공적으로 삭제되었습니다",
|
||||||
|
"profileSwitched": "프로필 전환됨",
|
||||||
|
"deleteProfileConfirm": "이 프로필을 삭제하시겠습니까",
|
||||||
|
"deleteProfile": "프로필 삭제",
|
||||||
|
"copyText": "텍스트 복사",
|
||||||
|
"copyImage": "이미지 복사",
|
||||||
|
"processing": "처리 중...",
|
||||||
|
"saving": "저장 중...",
|
||||||
|
"buildInfoCopied": "빌드 정보가 클립보드에 복사되었습니다!",
|
||||||
|
"copyFailed": "빌드 정보 복사 실패!",
|
||||||
|
"imageCopiedToClipboard": "이미지가 클립보드에 복사되었습니다!",
|
||||||
|
"clipboardWriteFailed": "클립보드 쓰기에 실패했습니다!",
|
||||||
|
"imageSavedSuccessfully": "이미지가 성공적으로 저장되었습니다!",
|
||||||
|
"failedToGenerateImage": "이미지 생성 실패!",
|
||||||
|
"errorGeneratingShowcaseCard": "쇼케이스 카드 생성 중 오류가 발생했습니다!",
|
||||||
|
"feature": "기능",
|
||||||
|
"fix": "수정",
|
||||||
|
"improvement": "개선",
|
||||||
|
"version": "버전",
|
||||||
|
"setEffects": "세트 효과",
|
||||||
"details": "상세",
|
"details": "상세",
|
||||||
"normal": "기본 공격",
|
"normal": "기본 공격",
|
||||||
"bpskill": "스킬",
|
"bpskill": "스킬",
|
||||||
@@ -257,7 +305,6 @@
|
|||||||
"upRoll": "부옵션 추가",
|
"upRoll": "부옵션 추가",
|
||||||
"downRoll": "부옵션 감소",
|
"downRoll": "부옵션 감소",
|
||||||
"actions": "동작",
|
"actions": "동작",
|
||||||
"avatars": "아바타",
|
|
||||||
"quickView": "빠른 조회",
|
"quickView": "빠른 조회",
|
||||||
"extraSetting": "추가 설정",
|
"extraSetting": "추가 설정",
|
||||||
"disableCensorship": "검열 비활성화",
|
"disableCensorship": "검열 비활성화",
|
||||||
|
|||||||
+50
-3
@@ -153,10 +153,28 @@
|
|||||||
"importFreeSRDataSuccess": "Dados FreeSR importados com sucesso",
|
"importFreeSRDataSuccess": "Dados FreeSR importados com sucesso",
|
||||||
"importDatabaseSuccess": "Banco de dados importado com sucesso",
|
"importDatabaseSuccess": "Banco de dados importado com sucesso",
|
||||||
"getData": "Obter Dados",
|
"getData": "Obter Dados",
|
||||||
|
"fetchingData": "Buscando dados...",
|
||||||
"import": "Importar",
|
"import": "Importar",
|
||||||
"freeSRImport": "Importação FreeSR",
|
"freeSRImport": "Importação FreeSR",
|
||||||
"onlySupportFreeSRJsonFile": "Suporta apenas arquivos JSON FreeSR",
|
"onlySupportFreeSRJsonFile": "Suporta apenas arquivos JSON FreeSR",
|
||||||
"pickAFile": "Escolher um arquivo",
|
"pickAFile": "Escolher um arquivo",
|
||||||
|
"changeFile": "Alterar arquivo",
|
||||||
|
"avatars": "Personagens",
|
||||||
|
"noFreeSRDataLoaded": "Nenhum dado do FreeSR carregado",
|
||||||
|
"dropFreeSRJsonHint": "Arraste seu arquivo FreeSR.json aqui ou clique para selecionar.",
|
||||||
|
"step1": "PASSO 1",
|
||||||
|
"step2": "PASSO 2",
|
||||||
|
"step3": "PASSO 3",
|
||||||
|
"exportJsonFromFreeSR": "Exportar JSON do FreeSR",
|
||||||
|
"dropOrSelectFile": "Solte ou selecione o arquivo",
|
||||||
|
"syncCharacterBuilds": "Sincronizar builds de personagens",
|
||||||
|
"enterUidPlaceholder": "Insira o UID de 9 dígitos...",
|
||||||
|
"publicCharacters": "Personagens Públicos",
|
||||||
|
"fetchFromEnka": "Buscar do Enka.Network",
|
||||||
|
"fetchEnkaHint": "Insira seu UID de Honkai: Star Rail acima para sincronizar seus personagens públicos.",
|
||||||
|
"inGameProfile": "Perfil de Desbravador no jogo",
|
||||||
|
"displayCharactersPublicly": "Exibir personagens publicamente",
|
||||||
|
"enterUidAndSync": "Insira UID e sincronize",
|
||||||
"lightConeSetting": "Configuração de Cone de Luz",
|
"lightConeSetting": "Configuração de Cone de Luz",
|
||||||
"relicMaker": "Criador de Relíquias",
|
"relicMaker": "Criador de Relíquias",
|
||||||
"pleaseSelectAllOptions": "Por favor selecione todas as opções",
|
"pleaseSelectAllOptions": "Por favor selecione todas as opções",
|
||||||
@@ -219,10 +237,40 @@
|
|||||||
"searchStage": "Buscar fase...",
|
"searchStage": "Buscar fase...",
|
||||||
"noStageFound": "Fase não encontrada",
|
"noStageFound": "Fase não encontrada",
|
||||||
"searchMonster": "Buscar monstro...",
|
"searchMonster": "Buscar monstro...",
|
||||||
"changeRelic": "Mudar relíquia",
|
"HEAD": "Cabeça",
|
||||||
|
"HAND": "Mãos",
|
||||||
|
"BODY": "Corpo",
|
||||||
|
"FOOT": "Pés",
|
||||||
|
"NECK": "Esfera Planar",
|
||||||
|
"OBJECT": "Corda de Ligação",
|
||||||
|
"changeRelic": "Alterar relíquia",
|
||||||
"deleteRelic": "Deletar relíquia",
|
"deleteRelic": "Deletar relíquia",
|
||||||
"deleteRelicConfirm": "Tem certeza que deseja deletar a relíquia neste slot?",
|
"deleteRelicConfirm": "Tem certeza que deseja deletar a relíquia neste slot?",
|
||||||
"setEffects": "Definir Efeitos",
|
"confirmDelete": "Confirmar exclusão",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"relicDeletedSuccessfully": "Relíquia excluída com sucesso",
|
||||||
|
"profileCreatedSuccessfully": "Perfil criado com sucesso",
|
||||||
|
"profileUpdatedSuccessfully": "Perfil atualizado com sucesso",
|
||||||
|
"profileDeletedSuccessfully": "Perfil excluído com sucesso",
|
||||||
|
"profileSwitched": "Perfil alterado para",
|
||||||
|
"deleteProfileConfirm": "Tem certeza que deseja excluir o perfil",
|
||||||
|
"deleteProfile": "Excluir perfil",
|
||||||
|
"copyText": "Copiar texto",
|
||||||
|
"copyImage": "Copiar imagem",
|
||||||
|
"processing": "Processando...",
|
||||||
|
"saving": "Salvando...",
|
||||||
|
"buildInfoCopied": "Informações da build copiadas para a área de transferência!",
|
||||||
|
"copyFailed": "Falha ao copiar informações da build!",
|
||||||
|
"imageCopiedToClipboard": "Imagem copiada para a área de transferência!",
|
||||||
|
"clipboardWriteFailed": "Falha ao gravar na área de transferência!",
|
||||||
|
"imageSavedSuccessfully": "Imagem salva com sucesso!",
|
||||||
|
"failedToGenerateImage": "Falha ao gerar imagem!",
|
||||||
|
"errorGeneratingShowcaseCard": "Erro ao gerar cartão showcase!",
|
||||||
|
"feature": "Funcionalidade",
|
||||||
|
"fix": "Correção",
|
||||||
|
"improvement": "Melhoria",
|
||||||
|
"version": "Versão",
|
||||||
|
"setEffects": "Efeitos de Conjunto",
|
||||||
"details": "Detalhes",
|
"details": "Detalhes",
|
||||||
"normal": "Ataque Básico",
|
"normal": "Ataque Básico",
|
||||||
"bpskill": "Perícia",
|
"bpskill": "Perícia",
|
||||||
@@ -258,7 +306,6 @@
|
|||||||
"upRoll": "Aumentar Melhoria",
|
"upRoll": "Aumentar Melhoria",
|
||||||
"downRoll": "Diminuir Melhoria",
|
"downRoll": "Diminuir Melhoria",
|
||||||
"actions": "Ações",
|
"actions": "Ações",
|
||||||
"avatars": "Avatares",
|
|
||||||
"quickView": "Visualização Rápida",
|
"quickView": "Visualização Rápida",
|
||||||
"extraSetting": "Configurações Extras",
|
"extraSetting": "Configurações Extras",
|
||||||
"disableCensorship": "Desativar Censura",
|
"disableCensorship": "Desativar Censura",
|
||||||
|
|||||||
+49
-2
@@ -153,10 +153,28 @@
|
|||||||
"importFreeSRDataSuccess": "Успешный импорт из FreeSR",
|
"importFreeSRDataSuccess": "Успешный импорт из FreeSR",
|
||||||
"importDatabaseSuccess": "Успешный импорт базы данных",
|
"importDatabaseSuccess": "Успешный импорт базы данных",
|
||||||
"getData": "Получить данные",
|
"getData": "Получить данные",
|
||||||
|
"fetchingData": "Получение данных...",
|
||||||
"import": "Импорт",
|
"import": "Импорт",
|
||||||
"freeSRImport": "Импорт FreeSR",
|
"freeSRImport": "Импорт FreeSR",
|
||||||
"onlySupportFreeSRJsonFile": "Поддерживаются только JSON от FreeSR",
|
"onlySupportFreeSRJsonFile": "Поддерживаются только JSON от FreeSR",
|
||||||
"pickAFile": "Выберите файл",
|
"pickAFile": "Выберите файл",
|
||||||
|
"changeFile": "Изменить файл",
|
||||||
|
"avatars": "Персонажи",
|
||||||
|
"noFreeSRDataLoaded": "Данные FreeSR не загружены",
|
||||||
|
"dropFreeSRJsonHint": "Перетащите файл FreeSR.json сюда или нажмите, чтобы выбрать.",
|
||||||
|
"step1": "ШАГ 1",
|
||||||
|
"step2": "ШАГ 2",
|
||||||
|
"step3": "ШАГ 3",
|
||||||
|
"exportJsonFromFreeSR": "Экспорт JSON из FreeSR",
|
||||||
|
"dropOrSelectFile": "Перетащите или выберите файл",
|
||||||
|
"syncCharacterBuilds": "Синхронизировать сборки персонажей",
|
||||||
|
"enterUidPlaceholder": "Введите 9-значный UID...",
|
||||||
|
"publicCharacters": "Публичные персонажи",
|
||||||
|
"fetchFromEnka": "Получить из Enka.Network",
|
||||||
|
"fetchEnkaHint": "Введите UID Honkai: Star Rail выше, чтобы синхронизировать персонажей.",
|
||||||
|
"inGameProfile": "Профиль Первопроходца в игре",
|
||||||
|
"displayCharactersPublicly": "Открыть персонажей в витрине",
|
||||||
|
"enterUidAndSync": "Введите UID и синхронизируйте",
|
||||||
"lightConeSetting": "Настройки конуса",
|
"lightConeSetting": "Настройки конуса",
|
||||||
"relicMaker": "Создатель реликвий",
|
"relicMaker": "Создатель реликвий",
|
||||||
"pleaseSelectAllOptions": "Пожалуйста, выберите все параметры",
|
"pleaseSelectAllOptions": "Пожалуйста, выберите все параметры",
|
||||||
@@ -219,10 +237,40 @@
|
|||||||
"searchStage": "Поиск стадии...",
|
"searchStage": "Поиск стадии...",
|
||||||
"noStageFound": "Стадия не найдена",
|
"noStageFound": "Стадия не найдена",
|
||||||
"searchMonster": "Поиск монстра...",
|
"searchMonster": "Поиск монстра...",
|
||||||
|
"HEAD": "Голова",
|
||||||
|
"HAND": "Руки",
|
||||||
|
"BODY": "Тело",
|
||||||
|
"FOOT": "Ноги",
|
||||||
|
"NECK": "Планарная сфера",
|
||||||
|
"OBJECT": "Соединительная вязь",
|
||||||
"changeRelic": "Изменить реликвию",
|
"changeRelic": "Изменить реликвию",
|
||||||
"deleteRelic": "Удалить реликвию",
|
"deleteRelic": "Удалить реликвию",
|
||||||
"deleteRelicConfirm": "Удалить реликвию в этом слоте?",
|
"deleteRelicConfirm": "Удалить реликвию в этом слоте?",
|
||||||
"setEffects": "Настроить эффекты",
|
"confirmDelete": "Подтвердить удаление",
|
||||||
|
"cancel": "Отмена",
|
||||||
|
"relicDeletedSuccessfully": "Реликвия успешно удалена",
|
||||||
|
"profileCreatedSuccessfully": "Профиль успешно создан",
|
||||||
|
"profileUpdatedSuccessfully": "Профиль успешно обновлен",
|
||||||
|
"profileDeletedSuccessfully": "Профиль успешно удален",
|
||||||
|
"profileSwitched": "Профиль изменен на",
|
||||||
|
"deleteProfileConfirm": "Вы уверены, что хотите удалить профиль",
|
||||||
|
"deleteProfile": "Удалить профиль",
|
||||||
|
"copyText": "Копировать текст",
|
||||||
|
"copyImage": "Копировать изображение",
|
||||||
|
"processing": "Обработка...",
|
||||||
|
"saving": "Сохранение...",
|
||||||
|
"buildInfoCopied": "Информация о сборке скопирована в буфер обмена!",
|
||||||
|
"copyFailed": "Не удалось скопировать информацию о сборке!",
|
||||||
|
"imageCopiedToClipboard": "Изображение скопировано в буфер обмена!",
|
||||||
|
"clipboardWriteFailed": "Не удалось записать в буфер обмена!",
|
||||||
|
"imageSavedSuccessfully": "Изображение успешно сохранено!",
|
||||||
|
"failedToGenerateImage": "Не удалось создать изображение!",
|
||||||
|
"errorGeneratingShowcaseCard": "Ошибка генерации карточки showcase!",
|
||||||
|
"feature": "Функция",
|
||||||
|
"fix": "Исправление",
|
||||||
|
"improvement": "Улучшение",
|
||||||
|
"version": "Версия",
|
||||||
|
"setEffects": "Эффекты набора",
|
||||||
"details": "Детали",
|
"details": "Детали",
|
||||||
"normal": "Базовая атака",
|
"normal": "Базовая атака",
|
||||||
"bpskill": "Навык",
|
"bpskill": "Навык",
|
||||||
@@ -258,7 +306,6 @@
|
|||||||
"upRoll": "Повысить улучшение",
|
"upRoll": "Повысить улучшение",
|
||||||
"downRoll": "Понизить улучшение",
|
"downRoll": "Понизить улучшение",
|
||||||
"actions": "Действия",
|
"actions": "Действия",
|
||||||
"avatars": "Персонажи",
|
|
||||||
"quickView": "Быстрый просмотр",
|
"quickView": "Быстрый просмотр",
|
||||||
"extraSetting": "Дополнительные настройки",
|
"extraSetting": "Дополнительные настройки",
|
||||||
"disableCensorship": "Отключить цензуру",
|
"disableCensorship": "Отключить цензуру",
|
||||||
|
|||||||
+49
-2
@@ -153,10 +153,28 @@
|
|||||||
"importFreeSRDataSuccess": "นำเข้าข้อมูล FreeSR สำเร็จ",
|
"importFreeSRDataSuccess": "นำเข้าข้อมูล FreeSR สำเร็จ",
|
||||||
"importDatabaseSuccess": "นำเข้าฐานข้อมูลสำเร็จ",
|
"importDatabaseSuccess": "นำเข้าฐานข้อมูลสำเร็จ",
|
||||||
"getData": "รับข้อมูล",
|
"getData": "รับข้อมูล",
|
||||||
|
"fetchingData": "กำลังดึงข้อมูล...",
|
||||||
"import": "นำเข้า",
|
"import": "นำเข้า",
|
||||||
"freeSRImport": "นำเข้า FreeSR",
|
"freeSRImport": "นำเข้า FreeSR",
|
||||||
"onlySupportFreeSRJsonFile": "รองรับเฉพาะไฟล์ JSON ของ FreeSR",
|
"onlySupportFreeSRJsonFile": "รองรับเฉพาะไฟล์ JSON ของ FreeSR",
|
||||||
"pickAFile": "เลือกไฟล์",
|
"pickAFile": "เลือกไฟล์",
|
||||||
|
"changeFile": "เปลี่ยนไฟล์",
|
||||||
|
"avatars": "ตัวละคร",
|
||||||
|
"noFreeSRDataLoaded": "ไม่ได้โหลดข้อมูล FreeSR",
|
||||||
|
"dropFreeSRJsonHint": "ลากไฟล์ FreeSR.json ของคุณมาที่นี่หรือคลิกเพื่อเลือกไฟล์",
|
||||||
|
"step1": "ขั้นตอนที่ 1",
|
||||||
|
"step2": "ขั้นตอนที่ 2",
|
||||||
|
"step3": "ขั้นตอนที่ 3",
|
||||||
|
"exportJsonFromFreeSR": "ส่งออก JSON จาก FreeSR",
|
||||||
|
"dropOrSelectFile": "ลากหรือเลือกไฟล์",
|
||||||
|
"syncCharacterBuilds": "ซิงค์บิลด์ตัวละคร",
|
||||||
|
"enterUidPlaceholder": "ป้อน UID 9 หลัก...",
|
||||||
|
"publicCharacters": "ตัวละครสาธารณะ",
|
||||||
|
"fetchFromEnka": "ดึงข้อมูลจาก Enka.Network",
|
||||||
|
"fetchEnkaHint": "ป้อน UID Honkai: Star Rail ของคุณด้านบนเพื่อซิงค์ตัวละครที่แสดงแบบสาธารณะ",
|
||||||
|
"inGameProfile": "โปรไฟล์ผู้บุกเบิกในเกม",
|
||||||
|
"displayCharactersPublicly": "แสดงตัวละครแบบสาธารณะ",
|
||||||
|
"enterUidAndSync": "ป้อน UID และซิงค์",
|
||||||
"lightConeSetting": "การตั้งค่า Light Cone",
|
"lightConeSetting": "การตั้งค่า Light Cone",
|
||||||
"relicMaker": "สร้างรีลิกส์",
|
"relicMaker": "สร้างรีลิกส์",
|
||||||
"pleaseSelectAllOptions": "โปรดเลือกตัวเลือกทั้งหมด",
|
"pleaseSelectAllOptions": "โปรดเลือกตัวเลือกทั้งหมด",
|
||||||
@@ -219,10 +237,40 @@
|
|||||||
"searchStage": "ค้นหาด่าน...",
|
"searchStage": "ค้นหาด่าน...",
|
||||||
"noStageFound": "ไม่พบด่าน",
|
"noStageFound": "ไม่พบด่าน",
|
||||||
"searchMonster": "ค้นหามอนสเตอร์...",
|
"searchMonster": "ค้นหามอนสเตอร์...",
|
||||||
|
"HEAD": "ส่วนหัว",
|
||||||
|
"HAND": "ส่วนมือ",
|
||||||
|
"BODY": "ส่วนลำตัว",
|
||||||
|
"FOOT": "ส่วนเท้า",
|
||||||
|
"NECK": "Planar Sphere",
|
||||||
|
"OBJECT": "Link Rope",
|
||||||
"changeRelic": "เปลี่ยนรีลิกส์",
|
"changeRelic": "เปลี่ยนรีลิกส์",
|
||||||
"deleteRelic": "ลบรีลิกส์",
|
"deleteRelic": "ลบรีลิกส์",
|
||||||
"deleteRelicConfirm": "คุณแน่ใจหรือไม่ว่าต้องการลบรีลิกส์ในช่องนี้?",
|
"deleteRelicConfirm": "คุณแน่ใจหรือไม่ว่าต้องการลบรีลิกส์ในช่องนี้?",
|
||||||
"setEffects": "ตั้งค่าเอฟเฟกต์",
|
"confirmDelete": "ยืนยันการลบ",
|
||||||
|
"cancel": "ยกเลิก",
|
||||||
|
"relicDeletedSuccessfully": "ลบรีลิกส์สำเร็จ",
|
||||||
|
"profileCreatedSuccessfully": "สร้างโปรไฟล์สำเร็จ",
|
||||||
|
"profileUpdatedSuccessfully": "อัปเดตโปรไฟล์สำเร็จ",
|
||||||
|
"profileDeletedSuccessfully": "ลบโปรไฟล์สำเร็จ",
|
||||||
|
"profileSwitched": "สลับโปรไฟล์เป็น",
|
||||||
|
"deleteProfileConfirm": "คุณแน่ใจหรือไม่ว่าต้องการลบโปรไฟล์",
|
||||||
|
"deleteProfile": "ลบโปรไฟล์",
|
||||||
|
"copyText": "คัดลอกข้อความ",
|
||||||
|
"copyImage": "คัดลอกรูปภาพ",
|
||||||
|
"processing": "กำลังประมวลผล...",
|
||||||
|
"saving": "กำลังบันทึก...",
|
||||||
|
"buildInfoCopied": "คัดลอกข้อมูลบิลด์ไปยังคลิปบอร์ดแล้ว!",
|
||||||
|
"copyFailed": "คัดลอกข้อมูลบิลด์ไม่สำเร็จ!",
|
||||||
|
"imageCopiedToClipboard": "คัดลอกรูปภาพไปยังคลิปบอร์ดแล้ว!",
|
||||||
|
"clipboardWriteFailed": "ไม่สามารถเขียนลงคลิปบอร์ดได้!",
|
||||||
|
"imageSavedSuccessfully": "บันทึกรูปภาพสำเร็จ!",
|
||||||
|
"failedToGenerateImage": "สร้างรูปภาพไม่สำเร็จ!",
|
||||||
|
"errorGeneratingShowcaseCard": "เกิดข้อผิดพลาดในการสร้างการ์ดโชว์เคส!",
|
||||||
|
"feature": "ฟีเจอร์",
|
||||||
|
"fix": "แก้ไข",
|
||||||
|
"improvement": "ปรับปรุง",
|
||||||
|
"version": "เวอร์ชัน",
|
||||||
|
"setEffects": "เอฟเฟกต์เซ็ต",
|
||||||
"details": "รายละเอียด",
|
"details": "รายละเอียด",
|
||||||
"normal": "โจมตีปกติ",
|
"normal": "โจมตีปกติ",
|
||||||
"bpskill": "สกิลต่อสู้",
|
"bpskill": "สกิลต่อสู้",
|
||||||
@@ -258,7 +306,6 @@
|
|||||||
"upRoll": "เพิ่มอัปเกรด",
|
"upRoll": "เพิ่มอัปเกรด",
|
||||||
"downRoll": "ลดอัปเกรด",
|
"downRoll": "ลดอัปเกรด",
|
||||||
"actions": "แอ็กชัน",
|
"actions": "แอ็กชัน",
|
||||||
"avatars": "อวตาร",
|
|
||||||
"quickView": "ดูแบบด่วน",
|
"quickView": "ดูแบบด่วน",
|
||||||
"extraSetting": "การตั้งค่าเพิ่มเติม",
|
"extraSetting": "การตั้งค่าเพิ่มเติม",
|
||||||
"disableCensorship": "ปิดการเซ็นเซอร์",
|
"disableCensorship": "ปิดการเซ็นเซอร์",
|
||||||
|
|||||||
+50
-3
@@ -153,10 +153,28 @@
|
|||||||
"importFreeSRDataSuccess": "Nhập dữ liệu FreeSR thành công",
|
"importFreeSRDataSuccess": "Nhập dữ liệu FreeSR thành công",
|
||||||
"importDatabaseSuccess": "Nhập cơ sở dữ liệu thành công",
|
"importDatabaseSuccess": "Nhập cơ sở dữ liệu thành công",
|
||||||
"getData": "Lấy dữ liệu",
|
"getData": "Lấy dữ liệu",
|
||||||
|
"fetchingData": "Đang lấy dữ liệu...",
|
||||||
"import": "Nhập",
|
"import": "Nhập",
|
||||||
"freeSRImport": "Nhập FreeSR",
|
"freeSRImport": "Nhập FreeSR",
|
||||||
"onlySupportFreeSRJsonFile": "Chỉ hỗ trợ tệp JSON từ FreeSR",
|
"onlySupportFreeSRJsonFile": "Chỉ hỗ trợ tệp JSON từ FreeSR",
|
||||||
"pickAFile": "Chọn tệp",
|
"pickAFile": "Chọn tệp",
|
||||||
|
"changeFile": "Đổi file",
|
||||||
|
"avatars": "Nhân vật",
|
||||||
|
"noFreeSRDataLoaded": "Chưa có dữ liệu FreeSR",
|
||||||
|
"dropFreeSRJsonHint": "Kéo thả file FreeSR.json vào đây hoặc nhấn để chọn từ thiết bị của bạn.",
|
||||||
|
"step1": "BƯỚC 1",
|
||||||
|
"step2": "BƯỚC 2",
|
||||||
|
"step3": "BƯỚC 3",
|
||||||
|
"exportJsonFromFreeSR": "Xuất JSON từ FreeSR",
|
||||||
|
"dropOrSelectFile": "Kéo thả hoặc chọn file",
|
||||||
|
"syncCharacterBuilds": "Đồng bộ build nhân vật",
|
||||||
|
"enterUidPlaceholder": "Nhập UID 9 chữ số...",
|
||||||
|
"publicCharacters": "Nhân vật công khai",
|
||||||
|
"fetchFromEnka": "Lấy dữ liệu từ Enka.Network",
|
||||||
|
"fetchEnkaHint": "Nhập UID Honkai: Star Rail ở trên để đồng bộ các nhân vật trưng bày công khai.",
|
||||||
|
"inGameProfile": "Hồ sơ Nhà Khai Phá trong game",
|
||||||
|
"displayCharactersPublicly": "Bật hiển thị nhân vật công khai",
|
||||||
|
"enterUidAndSync": "Nhập UID & Đồng bộ",
|
||||||
"lightConeSetting": "Cài đặt Nón Ánh Sáng",
|
"lightConeSetting": "Cài đặt Nón Ánh Sáng",
|
||||||
"relicMaker": "Trình tạo Thánh Di Vật",
|
"relicMaker": "Trình tạo Thánh Di Vật",
|
||||||
"pleaseSelectAllOptions": "Vui lòng chọn tất cả tùy chọn",
|
"pleaseSelectAllOptions": "Vui lòng chọn tất cả tùy chọn",
|
||||||
@@ -219,10 +237,40 @@
|
|||||||
"searchStage": "Tìm màn...",
|
"searchStage": "Tìm màn...",
|
||||||
"noStageFound": "Không tìm thấy màn",
|
"noStageFound": "Không tìm thấy màn",
|
||||||
"searchMonster": "Tìm quái...",
|
"searchMonster": "Tìm quái...",
|
||||||
|
"HEAD": "Đầu",
|
||||||
|
"HAND": "Tay",
|
||||||
|
"BODY": "Thân",
|
||||||
|
"FOOT": "Chân",
|
||||||
|
"NECK": "Quả Cầu Vị Diện",
|
||||||
|
"OBJECT": "Dây Liên Kết",
|
||||||
"changeRelic": "Thay đổi di vật",
|
"changeRelic": "Thay đổi di vật",
|
||||||
"deleteRelic": "Xóa di vật",
|
"deleteRelic": "Xóa di vật",
|
||||||
"deleteRelicConfirm": "Bạn có chắc chắn muốn xóa di vật trong ô này không?",
|
"deleteRelicConfirm": "Bạn có chắc chắn muốn xóa di vật trong ô",
|
||||||
"setEffects": "Thiết lập hiệu ứng",
|
"confirmDelete": "Xác nhận xóa",
|
||||||
|
"cancel": "Hủy",
|
||||||
|
"relicDeletedSuccessfully": "Đã xóa di vật thành công",
|
||||||
|
"profileCreatedSuccessfully": "Đã tạo hồ sơ thành công",
|
||||||
|
"profileUpdatedSuccessfully": "Đã cập nhật hồ sơ thành công",
|
||||||
|
"profileDeletedSuccessfully": "Đã xóa hồ sơ thành công",
|
||||||
|
"profileSwitched": "Đã chuyển sang hồ sơ",
|
||||||
|
"deleteProfileConfirm": "Bạn có chắc chắn muốn xóa hồ sơ",
|
||||||
|
"deleteProfile": "Xóa hồ sơ",
|
||||||
|
"copyText": "Sao chép Text",
|
||||||
|
"copyImage": "Sao chép Ảnh",
|
||||||
|
"processing": "Đang xử lý...",
|
||||||
|
"saving": "Đang lưu...",
|
||||||
|
"buildInfoCopied": "Đã sao chép thông tin build!",
|
||||||
|
"copyFailed": "Lỗi khi sao chép thông tin build!",
|
||||||
|
"imageCopiedToClipboard": "Đã sao chép ảnh vào Clipboard!",
|
||||||
|
"clipboardWriteFailed": "Trình duyệt không cho phép sao chép ảnh trực tiếp!",
|
||||||
|
"imageSavedSuccessfully": "Đã lưu ảnh thành công!",
|
||||||
|
"failedToGenerateImage": "Không thể tạo ảnh!",
|
||||||
|
"errorGeneratingShowcaseCard": "Lỗi khi tạo ảnh showcase!",
|
||||||
|
"feature": "Tính năng",
|
||||||
|
"fix": "Sửa lỗi",
|
||||||
|
"improvement": "Cải tiến",
|
||||||
|
"version": "Phiên bản",
|
||||||
|
"setEffects": "Hiệu ứng bộ",
|
||||||
"details": "Chi tiết",
|
"details": "Chi tiết",
|
||||||
"normal": "Đánh thường",
|
"normal": "Đánh thường",
|
||||||
"bpskill": "Chiến kỹ",
|
"bpskill": "Chiến kỹ",
|
||||||
@@ -257,7 +305,6 @@
|
|||||||
"upRoll": "Tăng dòng",
|
"upRoll": "Tăng dòng",
|
||||||
"downRoll": "Giảm dòng",
|
"downRoll": "Giảm dòng",
|
||||||
"actions": "Hành động",
|
"actions": "Hành động",
|
||||||
"avatars": "Nhân vật",
|
|
||||||
"quickView": "Xem nhanh",
|
"quickView": "Xem nhanh",
|
||||||
"extraSetting": "Cài đặt bổ sung",
|
"extraSetting": "Cài đặt bổ sung",
|
||||||
"disableCensorship": "Tắt kiểm duyệt",
|
"disableCensorship": "Tắt kiểm duyệt",
|
||||||
|
|||||||
+49
-2
@@ -153,10 +153,28 @@
|
|||||||
"importFreeSRDataSuccess": "导入 FreeSR 数据成功",
|
"importFreeSRDataSuccess": "导入 FreeSR 数据成功",
|
||||||
"importDatabaseSuccess": "导入数据库成功",
|
"importDatabaseSuccess": "导入数据库成功",
|
||||||
"getData": "获取数据",
|
"getData": "获取数据",
|
||||||
|
"fetchingData": "正在获取数据...",
|
||||||
"import": "导入",
|
"import": "导入",
|
||||||
"freeSRImport": "导入 FreeSR",
|
"freeSRImport": "导入 FreeSR",
|
||||||
"onlySupportFreeSRJsonFile": "仅支持 FreeSR JSON 文件",
|
"onlySupportFreeSRJsonFile": "仅支持 FreeSR JSON 文件",
|
||||||
"pickAFile": "选择文件",
|
"pickAFile": "选择文件",
|
||||||
|
"changeFile": "更换文件",
|
||||||
|
"avatars": "角色",
|
||||||
|
"noFreeSRDataLoaded": "未加载 FreeSR 数据",
|
||||||
|
"dropFreeSRJsonHint": "将 FreeSR.json 文件拖放到此处,或点击从设备中选择。",
|
||||||
|
"step1": "步骤 1",
|
||||||
|
"step2": "步骤 2",
|
||||||
|
"step3": "步骤 3",
|
||||||
|
"exportJsonFromFreeSR": "从 FreeSR 导出 JSON",
|
||||||
|
"dropOrSelectFile": "拖放或选择文件",
|
||||||
|
"syncCharacterBuilds": "同步角色配置",
|
||||||
|
"enterUidPlaceholder": "输入 9 位 UID...",
|
||||||
|
"publicCharacters": "公开展示角色",
|
||||||
|
"fetchFromEnka": "从 Enka.Network 获取数据",
|
||||||
|
"fetchEnkaHint": "在上方输入崩坏:星穹铁道 UID 即可同步公开展示的角色。",
|
||||||
|
"inGameProfile": "游戏内开拓者个人资料",
|
||||||
|
"displayCharactersPublicly": "将展示角色设为公开",
|
||||||
|
"enterUidAndSync": "输入 UID 并同步",
|
||||||
"lightConeSetting": "光锥设置",
|
"lightConeSetting": "光锥设置",
|
||||||
"relicMaker": "遗器制作",
|
"relicMaker": "遗器制作",
|
||||||
"pleaseSelectAllOptions": "请选择所有选项",
|
"pleaseSelectAllOptions": "请选择所有选项",
|
||||||
@@ -218,10 +236,40 @@
|
|||||||
"searchStage": "搜索关卡...",
|
"searchStage": "搜索关卡...",
|
||||||
"noStageFound": "未找到关卡",
|
"noStageFound": "未找到关卡",
|
||||||
"searchMonster": "搜索怪物...",
|
"searchMonster": "搜索怪物...",
|
||||||
|
"HEAD": "头部",
|
||||||
|
"HAND": "手部",
|
||||||
|
"BODY": "躯干",
|
||||||
|
"FOOT": "脚部",
|
||||||
|
"NECK": "位面球",
|
||||||
|
"OBJECT": "连结绳",
|
||||||
"changeRelic": "更换遗物",
|
"changeRelic": "更换遗物",
|
||||||
"deleteRelic": "删除遗物",
|
"deleteRelic": "删除遗物",
|
||||||
"deleteRelicConfirm": "确定要删除插槽中的遗物吗",
|
"deleteRelicConfirm": "确定要删除插槽中的遗物吗",
|
||||||
"setEffects": "设置效果",
|
"confirmDelete": "确认删除",
|
||||||
|
"cancel": "取消",
|
||||||
|
"relicDeletedSuccessfully": "遗物删除成功",
|
||||||
|
"profileCreatedSuccessfully": "配置创建成功",
|
||||||
|
"profileUpdatedSuccessfully": "配置更新成功",
|
||||||
|
"profileDeletedSuccessfully": "配置删除成功",
|
||||||
|
"profileSwitched": "已切换至配置",
|
||||||
|
"deleteProfileConfirm": "确定要删除此配置吗",
|
||||||
|
"deleteProfile": "删除配置",
|
||||||
|
"copyText": "复制文本",
|
||||||
|
"copyImage": "复制图片",
|
||||||
|
"processing": "处理中...",
|
||||||
|
"saving": "保存中...",
|
||||||
|
"buildInfoCopied": "已复制配装信息到剪贴板!",
|
||||||
|
"copyFailed": "复制配装信息失败!",
|
||||||
|
"imageCopiedToClipboard": "已复制图片到剪贴板!",
|
||||||
|
"clipboardWriteFailed": "浏览器不支持直接写入剪贴板!",
|
||||||
|
"imageSavedSuccessfully": "图片保存成功!",
|
||||||
|
"failedToGenerateImage": "生成图片失败!",
|
||||||
|
"errorGeneratingShowcaseCard": "生成展示卡片出错!",
|
||||||
|
"feature": "新特性",
|
||||||
|
"fix": "修复",
|
||||||
|
"improvement": "改进",
|
||||||
|
"version": "版本",
|
||||||
|
"setEffects": "套装效果",
|
||||||
"details": "详情",
|
"details": "详情",
|
||||||
"normal": "普通攻击",
|
"normal": "普通攻击",
|
||||||
"bpskill": "技能",
|
"bpskill": "技能",
|
||||||
@@ -257,7 +305,6 @@
|
|||||||
"upRoll": "增加副属性",
|
"upRoll": "增加副属性",
|
||||||
"downRoll": "减少副属性",
|
"downRoll": "减少副属性",
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
"avatars": "头像",
|
|
||||||
"quickView": "快速预览",
|
"quickView": "快速预览",
|
||||||
"extraSetting": "额外设置",
|
"extraSetting": "额外设置",
|
||||||
"disableCensorship": "禁用审查",
|
"disableCensorship": "禁用审查",
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ export default function ActionBar() {
|
|||||||
return avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0] ?? null;
|
return avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0] ?? null;
|
||||||
}, [avatarSelected, avatars]);
|
}, [avatarSelected, avatars]);
|
||||||
|
|
||||||
|
const [profileToDelete, setProfileToDelete] = useState<{ id: number; name: string } | null>(null);
|
||||||
|
|
||||||
const listProfile = useMemo(() => {
|
const listProfile = useMemo(() => {
|
||||||
if (!avatarSelected) return [];
|
if (!avatarSelected) return [];
|
||||||
const avatar = avatars[avatarSelected.ID];
|
const avatar = avatars[avatarSelected.ID];
|
||||||
@@ -93,13 +95,13 @@ export default function ActionBar() {
|
|||||||
};
|
};
|
||||||
const newListProfile = [...listProfile, newProfile];
|
const newListProfile = [...listProfile, newProfile];
|
||||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
|
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
|
||||||
toast.success("Profile created successfully")
|
toast.success(transI18n("profileCreatedSuccessfully"))
|
||||||
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
|
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
|
||||||
const newListProfile = listProfile.map((p, idx) =>
|
const newListProfile = listProfile.map((p, idx) =>
|
||||||
idx === profileEdit ? { ...p, profile_name: profileName } : p
|
idx === profileEdit ? { ...p, profile_name: profileName } : p
|
||||||
);
|
);
|
||||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
|
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
|
||||||
toast.success("Profile updated successfully")
|
toast.success(transI18n("profileUpdatedSuccessfully"))
|
||||||
}
|
}
|
||||||
handleCloseModal("update_profile_modal");
|
handleCloseModal("update_profile_modal");
|
||||||
};
|
};
|
||||||
@@ -129,22 +131,33 @@ export default function ActionBar() {
|
|||||||
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
|
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
|
||||||
const selectedProfile = avatars[avatarSelected.ID].profileList?.[profileId];
|
const selectedProfile = avatars[avatarSelected.ID].profileList?.[profileId];
|
||||||
if (selectedProfile) {
|
if (selectedProfile) {
|
||||||
toast.success(`Profile changed to: ${selectedProfile.profile_name}`)
|
toast.success(`${transI18n("profileSwitched")}: ${selectedProfile.profile_name}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
|
const requestDeleteProfile = (profileId: number, e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (!avatarSelected || profileId === 0 || !avatars[avatarSelected.ID]) return;
|
if (!avatarSelected || profileId === 0 || !avatars[avatarSelected.ID]) return;
|
||||||
const targetProfile = avatars[avatarSelected.ID].profileList?.[profileId];
|
const targetProfile = avatars[avatarSelected.ID].profileList?.[profileId];
|
||||||
if (!targetProfile) return;
|
if (!targetProfile) return;
|
||||||
if (window.confirm(`Are you sure you want to delete profile: ${targetProfile.profile_name}?`)) {
|
setProfileToDelete({ id: profileId, name: targetProfile.profile_name });
|
||||||
const currentSelect = avatars[avatarSelected.ID].profileSelect;
|
handleShow("delete_profile_confirm_modal");
|
||||||
const newListProfile = listProfile.filter((_, idx) => idx !== profileId);
|
|
||||||
const nextSelect = currentSelect >= profileId ? Math.max(0, currentSelect - 1) : currentSelect;
|
|
||||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: nextSelect })
|
|
||||||
toast.success(`Profile ${targetProfile.profile_name} deleted successfully`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleConfirmDeleteProfile = () => {
|
||||||
|
if (!avatarSelected || !profileToDelete || !avatars[avatarSelected.ID]) return;
|
||||||
|
const currentSelect = avatars[avatarSelected.ID].profileSelect;
|
||||||
|
const newListProfile = listProfile.filter((_, idx) => idx !== profileToDelete.id);
|
||||||
|
const nextSelect = currentSelect >= profileToDelete.id ? Math.max(0, currentSelect - 1) : currentSelect;
|
||||||
|
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: nextSelect })
|
||||||
|
toast.success(transI18n("profileDeletedSuccessfully"))
|
||||||
|
handleCloseModal("delete_profile_confirm_modal");
|
||||||
|
setProfileToDelete(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelDeleteProfile = () => {
|
||||||
|
handleCloseModal("delete_profile_confirm_modal");
|
||||||
|
setProfileToDelete(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConnectOrSyncPS = async () => {
|
const handleConnectOrSyncPS = async () => {
|
||||||
@@ -193,7 +206,7 @@ export default function ActionBar() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<button className="btn btn-ghost" onClick={() => setIsOpenCreateProfile(false)}>
|
<button className="btn btn-ghost" onClick={() => setIsOpenCreateProfile(false)}>
|
||||||
Cancel
|
{transI18n("cancel")}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-primary" onClick={handleUpdateProfile}>
|
<button className="btn btn-primary" onClick={handleUpdateProfile}>
|
||||||
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
|
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
|
||||||
@@ -320,14 +333,14 @@ export default function ActionBar() {
|
|||||||
setIsOpenCreateProfile(true)
|
setIsOpenCreateProfile(true)
|
||||||
handleShow("update_profile_modal")
|
handleShow("update_profile_modal")
|
||||||
}}
|
}}
|
||||||
title="Edit Profile"
|
title={transI18n("editProfile")}
|
||||||
>
|
>
|
||||||
<Edit2 className="w-3 h-3" />
|
<Edit2 className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-ghost btn-xs btn-circle text-error hover:bg-error/20"
|
className="btn btn-ghost btn-xs btn-circle text-error hover:bg-error/20"
|
||||||
onClick={(e) => handleDeleteProfile(index, e)}
|
onClick={(e) => requestDeleteProfile(index, e)}
|
||||||
title="Delete Profile"
|
title={transI18n("deleteProfile")}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3" />
|
<Trash2 className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -490,6 +503,7 @@ export default function ActionBar() {
|
|||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="btn btn-sm btn-circle btn-ghost text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors"
|
className="btn btn-sm btn-circle btn-ghost text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
@@ -505,6 +519,53 @@ export default function ActionBar() {
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Delete Profile Confirmation Modal */}
|
||||||
|
<dialog id="delete_profile_confirm_modal" className="modal modal-bottom sm:modal-middle">
|
||||||
|
<div className="modal-box max-w-md bg-base-100/95 backdrop-blur-xl border border-white/10 shadow-2xl rounded-2xl p-6 overflow-hidden relative">
|
||||||
|
<div className="pointer-events-none absolute -top-12 left-1/2 -translate-x-1/2 w-32 h-32 bg-rose-500/15 rounded-full blur-2xl" />
|
||||||
|
|
||||||
|
<div className="relative z-10 text-center space-y-4">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-rose-500/15 text-rose-400 border border-rose-500/30 flex items-center justify-center mx-auto shadow-[0_0_20px_rgba(244,63,94,0.2)]">
|
||||||
|
<Trash2 className="w-6 h-6 stroke-[1.75]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<h3 className="font-black text-lg text-base-content tracking-tight">
|
||||||
|
{transI18n("deleteProfile")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs sm:text-sm text-base-content/70 leading-relaxed">
|
||||||
|
{transI18n("deleteProfileConfirm")}{" "}
|
||||||
|
<span className="font-bold text-amber-400 font-mono">
|
||||||
|
"{profileToDelete?.name}"
|
||||||
|
</span>
|
||||||
|
?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCancelDeleteProfile}
|
||||||
|
className="flex-1 h-10 px-4 rounded-xl font-bold text-xs sm:text-sm bg-base-200 hover:bg-base-300 text-base-content/80 hover:text-base-content border border-white/10 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
{transI18n("cancel")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleConfirmDeleteProfile}
|
||||||
|
className="flex-1 h-10 px-4 rounded-xl font-bold text-xs sm:text-sm bg-gradient-to-r from-rose-500 to-red-600 hover:from-rose-400 hover:to-red-500 text-white shadow-md shadow-rose-500/25 hover:shadow-rose-500/40 transition-all flex items-center justify-center gap-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
<span>{transI18n("confirmDelete")}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" className="modal-backdrop">
|
||||||
|
<button onClick={handleCancelDeleteProfile}>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import ParseText from '../parseText';
|
|||||||
import useDetailDataStore from '@/stores/detailDataStore';
|
import useDetailDataStore from '@/stores/detailDataStore';
|
||||||
import { getLocaleName } from '@/helper/getName';
|
import { getLocaleName } from '@/helper/getName';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
|
||||||
export default function CharacterInfoCard({
|
export default function CharacterInfoCard({
|
||||||
character,
|
character,
|
||||||
@@ -35,15 +36,24 @@ export default function CharacterInfoCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group rounded-2xl p-4 border transition-all duration-200 cursor-pointer select-none ${
|
className={`group relative overflow-hidden rounded-2xl p-3 sm:p-4 border transition-all duration-300 ease-out cursor-pointer select-none ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-primary/10 border-primary shadow-lg shadow-primary/20 ring-2 ring-primary/40'
|
? 'bg-primary/10 border-primary/80 shadow-[0_0_24px_rgba(var(--color-primary-rgb,6,182,212),0.25)] ring-1 ring-primary/60 scale-[1.01]'
|
||||||
: 'bg-base-200/60 border-white/5 hover:border-white/20 opacity-80 hover:opacity-100 hover:bg-base-200'
|
: 'bg-base-200/50 hover:bg-base-200/90 border-white/10 hover:border-white/25 hover:shadow-lg hover:-translate-y-1'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onCharacterToggle(character)}
|
onClick={() => onCharacterToggle(character)}
|
||||||
>
|
>
|
||||||
{/* Character Portrait */}
|
{/* Selection Checkmark Badge */}
|
||||||
<div className="relative mb-3 aspect-4/5 rounded-xl overflow-hidden bg-base-300/40 flex items-center justify-center">
|
<div className={`absolute top-2.5 right-2.5 z-20 w-6 h-6 rounded-full flex items-center justify-center transition-all duration-200 ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-primary text-primary-content shadow-md scale-100 ring-2 ring-primary/40'
|
||||||
|
: 'bg-black/40 border border-white/20 text-transparent scale-90 group-hover:scale-100 group-hover:border-white/40'
|
||||||
|
}`}>
|
||||||
|
<Check className={`w-3.5 h-3.5 stroke-[3] ${isSelected ? 'opacity-100' : 'opacity-0'}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Character Portrait Area */}
|
||||||
|
<div className="relative mb-3 aspect-4/5 rounded-xl overflow-hidden bg-radial from-base-300/60 to-base-300/20 flex items-center justify-center">
|
||||||
{avatar && (
|
{avatar && (
|
||||||
<>
|
<>
|
||||||
<Image
|
<Image
|
||||||
@@ -53,61 +63,64 @@ export default function CharacterInfoCard({
|
|||||||
height={512}
|
height={512}
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
className="w-full h-full object-contain transition-transform duration-300 group-hover:scale-105"
|
className="w-full h-full object-contain transition-transform duration-500 ease-out group-hover:scale-108"
|
||||||
/>
|
/>
|
||||||
|
{/* Damage & Path Badges */}
|
||||||
|
<div className="absolute top-2 left-2 flex items-center gap-1.5 z-10">
|
||||||
{damageType?.[avatar.DamageType]?.Icon && (
|
{damageType?.[avatar.DamageType]?.Icon && (
|
||||||
<div className="absolute top-1.5 left-1.5 p-0.5 rounded-full bg-black/60 backdrop-blur-xs border border-white/10">
|
<div className="p-1 rounded-lg bg-black/65 backdrop-blur-md border border-white/15 shadow-sm" title={avatar.DamageType}>
|
||||||
<Image
|
<Image
|
||||||
width={24}
|
width={20}
|
||||||
height={24}
|
height={20}
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={`${process.env.CDN_URL}/${damageType[avatar.DamageType].Icon}`}
|
src={`${process.env.CDN_URL}/${damageType[avatar.DamageType].Icon}`}
|
||||||
className="w-5 h-5 object-contain"
|
className="w-4 h-4 object-contain"
|
||||||
alt={avatar.DamageType.toLowerCase()}
|
alt={avatar.DamageType.toLowerCase()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{baseType?.[avatar.BaseType]?.Icon && (
|
{baseType?.[avatar.BaseType]?.Icon && (
|
||||||
<div className="absolute top-1.5 right-1.5 p-0.5 rounded-full bg-black/60 backdrop-blur-xs border border-white/10">
|
<div className="p-1 rounded-lg bg-black/65 backdrop-blur-md border border-white/15 shadow-sm" title={avatar.BaseType}>
|
||||||
<Image
|
<Image
|
||||||
width={24}
|
width={20}
|
||||||
height={24}
|
height={20}
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={`${process.env.CDN_URL}/${baseType[avatar.BaseType].Icon}`}
|
src={`${process.env.CDN_URL}/${baseType[avatar.BaseType].Icon}`}
|
||||||
className="w-5 h-5 object-contain"
|
className="w-4 h-4 object-contain"
|
||||||
alt={avatar.BaseType.toLowerCase()}
|
alt={avatar.BaseType.toLowerCase()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Character Name and Level Badge */}
|
{/* Character Name & Badges */}
|
||||||
<div className="text-center mb-3">
|
<div className="text-center mb-2.5">
|
||||||
<ParseText
|
<ParseText
|
||||||
className="text-base font-bold line-clamp-1"
|
className="text-sm sm:text-base font-bold line-clamp-1 tracking-tight"
|
||||||
text={avatar ? getLocaleName(locale, avatar.Name) : character.avatar_id.toString()}
|
text={avatar ? getLocaleName(locale, avatar.Name) : character.avatar_id.toString()}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-center gap-2 mt-1">
|
<div className="flex items-center justify-center gap-2 mt-1.5">
|
||||||
<span className="text-xs font-mono font-semibold px-2 py-0.5 rounded-full bg-base-300/80 text-base-content/80">
|
<span className="text-[11px] font-mono font-semibold px-2 py-0.5 rounded-md bg-base-300/80 text-base-content/90 border border-white/5">
|
||||||
Lv.{character.level}
|
Lv.{character.level}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-mono font-bold px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-400 border border-amber-500/30">
|
<span className="text-[11px] font-mono font-bold px-2 py-0.5 rounded-md bg-amber-500/15 text-amber-400 border border-amber-500/30">
|
||||||
E{character.rank}
|
E{character.rank}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Relics Mini-grid */}
|
{/* Relics Grid (2 rows of 3, no overflow) */}
|
||||||
{character.relics.length > 0 && (
|
{character.relics.length > 0 && (
|
||||||
<div className="flex flex-wrap items-center justify-center gap-1.5 mb-3 p-1.5 rounded-xl bg-base-100/50 border border-white/5">
|
<div className="grid grid-cols-3 gap-1.5 sm:gap-2 justify-items-center py-1.5 px-2.5 rounded-xl bg-base-300/30 w-fit mx-auto mb-2.5">
|
||||||
{character.relics.map((relic, index) => (
|
{character.relics.map((relic, index) => (
|
||||||
<div key={index} className="relative group/relic">
|
<div key={index} className="relative group/relic">
|
||||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-base-200 border border-amber-500/40">
|
<div className="w-7 h-7 sm:w-8 sm:h-8 rounded-lg flex items-center justify-center bg-base-100/90 border border-amber-500/30 hover:border-amber-400 transition-colors shadow-xs">
|
||||||
<Image
|
<Image
|
||||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
|
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
|
||||||
alt="Relic"
|
alt="Relic"
|
||||||
@@ -115,10 +128,10 @@ export default function CharacterInfoCard({
|
|||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
width={32}
|
width={32}
|
||||||
height={32}
|
height={32}
|
||||||
className="w-6 h-6 object-contain"
|
className="w-5 h-5 sm:w-6 sm:h-6 object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 bg-black/80 text-[9px] font-mono font-bold text-amber-400 px-1 rounded-full border border-white/10">
|
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2 bg-black/90 text-[8px] font-mono font-bold text-amber-300 px-1 rounded-full border border-white/10 leading-tight">
|
||||||
+{relic.level}
|
+{relic.level}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,29 +139,31 @@ export default function CharacterInfoCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Light Cone */}
|
{/* Light Cone Preview (Seamless strip) */}
|
||||||
{character.lightcone && character.lightcone.item_id !== 0 && (
|
{character.lightcone && character.lightcone.item_id !== 0 && (
|
||||||
<div className="p-2 rounded-xl bg-base-100/50 border border-white/5 text-center">
|
<div className="pt-2 border-t border-white/10 flex items-center gap-2.5">
|
||||||
<div className="h-28 flex items-center justify-center mb-1">
|
<div className="h-12 w-9 shrink-0 flex items-center justify-center rounded bg-base-300/40 overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
|
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
|
||||||
alt="Lightcone"
|
alt="Lightcone"
|
||||||
width={160}
|
width={72}
|
||||||
height={200}
|
height={96}
|
||||||
className="h-full w-auto object-contain rounded-lg"
|
className="h-full w-auto object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs font-bold truncate">
|
<div className="flex-1 min-w-0 text-left">
|
||||||
|
<div className="text-xs font-semibold truncate leading-tight">
|
||||||
<ParseText
|
<ParseText
|
||||||
text={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
|
text={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<span className="text-[11px] font-mono text-base-content/70">Lv.{character.lightcone.level}</span>
|
<span className="text-[10px] font-mono text-base-content/70">Lv.{character.lightcone.level}</span>
|
||||||
<span className="text-[11px] font-mono font-bold text-primary">S{character.lightcone.rank}</span>
|
<span className="text-[10px] font-mono font-bold text-primary">S{character.lightcone.rank}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import useUserDataStore from "@/stores/userDataStore";
|
|||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface RelicCardProps {
|
interface RelicCardProps {
|
||||||
slot: string
|
slot: string
|
||||||
@@ -26,36 +27,23 @@ const getRarityColor = (rarity: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSlotIconAndName = (slot: string) => {
|
const slotIconMap: Record<string, { icon: string; key: string }> = {
|
||||||
const slotMap: Record<string, { icon: string; name: string }> = {
|
'1': { icon: '/relics/HEAD.png', key: 'HEAD' },
|
||||||
'1': { icon: '/relics/HEAD.png', name: 'Head' },
|
'2': { icon: '/relics/HAND.png', key: 'HAND' },
|
||||||
'2': { icon: '/relics/HAND.png', name: 'Hands' },
|
'3': { icon: '/relics/BODY.png', key: 'BODY' },
|
||||||
'3': { icon: '/relics/BODY.png', name: 'Body' },
|
'4': { icon: '/relics/FOOT.png', key: 'FOOT' },
|
||||||
'4': { icon: '/relics/FOOT.png', name: 'Feet' },
|
'5': { icon: '/relics/NECK.png', key: 'NECK' },
|
||||||
'5': { icon: '/relics/NECK.png', name: 'Planar Sphere' },
|
'6': { icon: '/relics/OBJECT.png', key: 'OBJECT' },
|
||||||
'6': { icon: '/relics/OBJECT.png', name: 'Link Rope' },
|
'HEAD': { icon: '/relics/HEAD.png', key: 'HEAD' },
|
||||||
};
|
'HAND': { icon: '/relics/HAND.png', key: 'HAND' },
|
||||||
|
'BODY': { icon: '/relics/BODY.png', key: 'BODY' },
|
||||||
const current = slotMap[slot];
|
'FOOT': { icon: '/relics/FOOT.png', key: 'FOOT' },
|
||||||
if (!current) return null;
|
'NECK': { icon: '/relics/NECK.png', key: 'NECK' },
|
||||||
|
'OBJECT': { icon: '/relics/OBJECT.png', key: 'OBJECT' },
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1.5 justify-center mt-2.5">
|
|
||||||
<Image
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src={current.icon}
|
|
||||||
alt={current.name}
|
|
||||||
width={18}
|
|
||||||
height={18}
|
|
||||||
className="w-4 h-4 object-contain theme-stat-icon opacity-85"
|
|
||||||
/>
|
|
||||||
<span className="text-xs font-bold text-base-content">{current.name}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
|
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
|
||||||
|
const transI18n = useTranslations("DataPage");
|
||||||
const { avatars } = useUserDataStore(
|
const { avatars } = useUserDataStore(
|
||||||
useShallow((state) => ({ avatars: state.avatars }))
|
useShallow((state) => ({ avatars: state.avatars }))
|
||||||
)
|
)
|
||||||
@@ -63,6 +51,9 @@ export default function RelicCard({ slot, avatarId }: RelicCardProps) {
|
|||||||
useShallow((state) => ({ selectedRelicSlot: state.selectedRelicSlot }))
|
useShallow((state) => ({ selectedRelicSlot: state.selectedRelicSlot }))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const slotInfo = slotIconMap[slot];
|
||||||
|
const slotDisplayName = slotInfo ? (transI18n(slotInfo.key) || slotInfo.key) : slot;
|
||||||
|
|
||||||
const relicDetail = useMemo(() => {
|
const relicDetail = useMemo(() => {
|
||||||
const avatar = avatars[avatarId];
|
const avatar = avatars[avatarId];
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
@@ -103,7 +94,20 @@ export default function RelicCard({ slot, avatarId }: RelicCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getSlotIconAndName(slot)}
|
{slotInfo && (
|
||||||
|
<div className="flex items-center gap-1.5 justify-center mt-2.5">
|
||||||
|
<Image
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={slotInfo.icon}
|
||||||
|
alt={slotDisplayName}
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
className="w-4 h-4 object-contain theme-stat-icon opacity-85"
|
||||||
|
/>
|
||||||
|
<span className="text-xs font-bold text-base-content">{slotDisplayName}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
@@ -127,7 +131,20 @@ export default function RelicCard({ slot, avatarId }: RelicCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getSlotIconAndName(slot)}
|
{slotInfo && (
|
||||||
|
<div className="flex items-center gap-1.5 justify-center mt-2.5">
|
||||||
|
<Image
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={slotInfo.icon}
|
||||||
|
alt={slotDisplayName}
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
className="w-4 h-4 object-contain theme-stat-icon opacity-85"
|
||||||
|
/>
|
||||||
|
<span className="text-xs font-bold text-base-content">{slotDisplayName}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import useLocaleStore from '@/stores/localeStore';
|
import useLocaleStore from '@/stores/localeStore';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
|
import { Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function ChangelogBar() {
|
export default function ChangelogBar() {
|
||||||
|
const transI18n = useTranslations("DataPage");
|
||||||
const { changelog } = useLocaleStore(
|
const { changelog } = useLocaleStore(
|
||||||
useShallow((state) => ({ changelog: state.changelog }))
|
useShallow((state) => ({ changelog: state.changelog }))
|
||||||
)
|
)
|
||||||
@@ -36,13 +38,13 @@ export default function ChangelogBar() {
|
|||||||
const getTypeLabel = (type: string) => {
|
const getTypeLabel = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'feature':
|
case 'feature':
|
||||||
return 'Feature';
|
return transI18n("feature") || 'Feature';
|
||||||
case 'fix':
|
case 'fix':
|
||||||
return 'Fix';
|
return transI18n("fix") || 'Fix';
|
||||||
case 'improvement':
|
case 'improvement':
|
||||||
return 'Improvement';
|
return transI18n("improvement") || 'Improvement';
|
||||||
default:
|
default:
|
||||||
return 'Update';
|
return transI18n("update") || 'Update';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@ export default function ChangelogBar() {
|
|||||||
{getTypeLabel(change.type)}
|
{getTypeLabel(change.type)}
|
||||||
</span>
|
</span>
|
||||||
<h3 className="font-extrabold text-sm sm:text-base tracking-tight">
|
<h3 className="font-extrabold text-sm sm:text-base tracking-tight">
|
||||||
Version {change.version}
|
{transI18n("version") || "Version"} {change.version}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -646,8 +646,8 @@ export default function Header() {
|
|||||||
: 'max-w-[92vw] sm:max-w-[88vw] max-h-[90vh]'
|
: 'max-w-[92vw] sm:max-w-[88vw] max-h-[90vh]'
|
||||||
} max-h-[90vh] flex flex-col bg-base-100 text-base-content border border-base-content/10 shadow-2xl rounded-2xl p-0 overflow-hidden`}>
|
} max-h-[90vh] flex flex-col bg-base-100 text-base-content border border-base-content/10 shadow-2xl rounded-2xl p-0 overflow-hidden`}>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="sticky top-0 z-20 flex items-center justify-between px-6 py-4 border-b border-base-content/10 bg-base-100 shrink-0">
|
<div className="sticky top-0 z-20 flex items-center justify-between px-4 py-3 sm:px-6 sm:py-4 border-b border-base-content/10 bg-base-100 shrink-0">
|
||||||
<h3 className="font-bold text-xl tracking-tight bg-clip-text text-transparent bg-linear-to-r from-primary via-accent to-secondary">
|
<h3 className="font-bold text-lg sm:text-xl tracking-tight bg-clip-text text-transparent bg-linear-to-r from-primary via-accent to-secondary">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import CharacterInfoCard from "../card/characterInfoCard";
|
import CharacterInfoCard from "../card/characterInfoCard";
|
||||||
import useEnkaStore from "@/stores/enkaStore";
|
import useEnkaStore from "@/stores/enkaStore";
|
||||||
@@ -13,6 +14,18 @@ import { avatarEnkaDetailSchema, enkaResponseSchema } from "@/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
CheckCheck,
|
||||||
|
RotateCcw,
|
||||||
|
AlertCircle,
|
||||||
|
Sparkles,
|
||||||
|
ArrowRight,
|
||||||
|
Users,
|
||||||
|
Globe,
|
||||||
|
UserCheck,
|
||||||
|
Loader2
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
type AvatarEnkaDetailInput = z.infer<typeof avatarEnkaDetailSchema>;
|
type AvatarEnkaDetailInput = z.infer<typeof avatarEnkaDetailSchema>;
|
||||||
|
|
||||||
@@ -51,7 +64,7 @@ export default function EnkaImport() {
|
|||||||
setEnkaData: state.setEnkaData,
|
setEnkaData: state.setEnkaData,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage");
|
||||||
const { avatars, setAvatar } = useUserDataStore(
|
const { avatars, setAvatar } = useUserDataStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
avatars: state.avatars,
|
avatars: state.avatars,
|
||||||
@@ -62,48 +75,66 @@ export default function EnkaImport() {
|
|||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
setIsOpenImport: state.setIsOpenImport,
|
setIsOpenImport: state.setIsOpenImport,
|
||||||
}))
|
}))
|
||||||
)
|
);
|
||||||
const { mapAvatar } = useDetailDataStore(
|
const { mapAvatar } = useDetailDataStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
mapAvatar: state.mapAvatar,
|
mapAvatar: state.mapAvatar,
|
||||||
}))
|
}))
|
||||||
)
|
);
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [Error, setError] = useState("")
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const validCharacters = enkaData?.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]) ?? []
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
|
const validCharacters = enkaData?.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]) ?? [];
|
||||||
|
|
||||||
const handlerFetchData = async () => {
|
const handlerFetchData = async () => {
|
||||||
if (!uidInput) {
|
const cleanUid = uidInput.trim();
|
||||||
setError(transI18n("pleaseEnterUid"))
|
if (!cleanUid) {
|
||||||
|
setErrorMsg(transI18n("pleaseEnterUid"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true)
|
setIsLoading(true);
|
||||||
|
setErrorMsg("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data: unknown = await SendDataThroughProxy({data: {serverUrl: "https://enka.network/api/hsr/uid/" + uidInput, method: "GET"}})
|
const data: unknown = await SendDataThroughProxy({
|
||||||
const parsed = enkaResponseSchema.safeParse(data)
|
data: {
|
||||||
|
serverUrl: "https://enka.network/api/hsr/uid/" + cleanUid,
|
||||||
|
method: "GET"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const parsed = enkaResponseSchema.safeParse(data);
|
||||||
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
setEnkaData(null)
|
setEnkaData(null);
|
||||||
setSelectedCharacters([])
|
setSelectedCharacters([]);
|
||||||
setError(transI18n("failedToFetchEnkaData"))
|
setErrorMsg(transI18n("failedToFetchEnkaData"));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnkaData(parsed.data as EnkaResponse)
|
setEnkaData(parsed.data as EnkaResponse);
|
||||||
setSelectedCharacters(parsed.data.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]).map(toCharacterInfoCard))
|
const validChars = parsed.data.detailInfo.avatarDetailList
|
||||||
setError("")
|
.filter((character) => mapAvatar?.[character.avatarId])
|
||||||
|
.map(toCharacterInfoCard);
|
||||||
|
setSelectedCharacters(validChars);
|
||||||
|
setErrorMsg("");
|
||||||
} catch {
|
} catch {
|
||||||
setEnkaData(null)
|
setEnkaData(null);
|
||||||
setSelectedCharacters([])
|
setSelectedCharacters([]);
|
||||||
setError(transI18n("failedToFetchEnkaData"))
|
setErrorMsg(transI18n("failedToFetchEnkaData"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handlerFetchData();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
||||||
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
|
if (selectedCharacters.some((selected) => selected.key === character.key)) {
|
||||||
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
|
setSelectedCharacters(selectedCharacters.filter((selected) => selected.key !== character.key));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedCharacters([...selectedCharacters, character]);
|
setSelectedCharacters([...selectedCharacters, character]);
|
||||||
@@ -121,29 +152,32 @@ export default function EnkaImport() {
|
|||||||
|
|
||||||
const handleImport = () => {
|
const handleImport = () => {
|
||||||
if (selectedCharacters.length === 0) {
|
if (selectedCharacters.length === 0) {
|
||||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
setErrorMsg(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!enkaData) {
|
if (!enkaData) {
|
||||||
setError(transI18n("noDataToImport"));
|
setErrorMsg(transI18n("noDataToImport"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError("");
|
setErrorMsg("");
|
||||||
const listAvatars = { ...avatars }
|
const listAvatars = { ...avatars };
|
||||||
const parsed = enkaResponseSchema.safeParse(enkaData)
|
const parsed = enkaResponseSchema.safeParse(enkaData);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
setError(transI18n("failedToFetchEnkaData"));
|
setErrorMsg(transI18n("failedToFetchEnkaData"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterData = parsed.data.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId] && selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatarId))
|
const filterData = parsed.data.detailInfo.avatarDetailList.filter((character) =>
|
||||||
|
mapAvatar?.[character.avatarId] && selectedCharacters.some((selected) => selected.avatar_id === character.avatarId)
|
||||||
|
);
|
||||||
|
|
||||||
filterData.forEach((character) => {
|
filterData.forEach((character) => {
|
||||||
const currentAvatar = listAvatars[character.avatarId.toString()]
|
const currentAvatar = listAvatars[character.avatarId.toString()];
|
||||||
if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
|
if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
|
||||||
const newProfileList = [...(currentAvatar.profileList || [])]
|
const newProfileList = [...(currentAvatar.profileList || [])];
|
||||||
const newProfile = converterOneEnkaDataToAvatarStore(character, newProfileList.length)
|
const newProfile = converterOneEnkaDataToAvatarStore(character, newProfileList.length);
|
||||||
if (newProfile) {
|
if (newProfile) {
|
||||||
newProfileList.push(newProfile)
|
newProfileList.push(newProfile);
|
||||||
}
|
}
|
||||||
const newAvatar = {
|
const newAvatar = {
|
||||||
...currentAvatar,
|
...currentAvatar,
|
||||||
@@ -158,83 +192,233 @@ export default function EnkaImport() {
|
|||||||
},
|
},
|
||||||
profileList: newProfileList,
|
profileList: newProfileList,
|
||||||
profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
|
profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
|
||||||
|
};
|
||||||
|
setAvatar(newAvatar);
|
||||||
}
|
}
|
||||||
setAvatar(newAvatar)
|
});
|
||||||
}
|
|
||||||
})
|
setIsOpenImport(false);
|
||||||
setIsOpenImport(false)
|
toast.success(transI18n("importEnkaDataSuccess"));
|
||||||
toast.success(transI18n("importEnkaDataSuccess"))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-6">
|
<div className="w-full space-y-6">
|
||||||
{/* Header */}
|
{/* Top Bar: Title & UID Input */}
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 sm:gap-4">
|
||||||
<h1 className="text-base-content text-xl sm:text-2xl font-bold">HSR UID</h1>
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="px-2 py-0.5 text-[10px] font-bold uppercase tracking-widest rounded-md bg-emerald-500/15 text-emerald-400 border border-emerald-500/25">
|
||||||
|
Enka.Network Live
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-base-content/50 font-medium">Showcase Fetcher</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl sm:text-2xl font-black tracking-tight text-base-content flex items-center gap-2">
|
||||||
|
HSR UID Showcase
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-3 items-center">
|
{/* UID Search Input Form */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2.5 w-full md:w-auto">
|
||||||
|
<div className="relative flex-1 md:w-72">
|
||||||
|
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-base-content/50 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={uidInput}
|
value={uidInput}
|
||||||
onChange={(e) => setUidInput(e.target.value)}
|
onChange={(e) => setUidInput(e.target.value)}
|
||||||
className="input input-bordered w-full max-w-xs rounded-xl bg-base-200 border-base-300 text-sm focus:border-primary"
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Enter UID"
|
className="w-full h-10 pl-10 pr-9 rounded-xl bg-base-200/90 text-base-content border border-base-content/20 text-sm font-mono focus:border-cyan-500 focus:outline-none focus:ring-1 focus:ring-cyan-500 transition-all placeholder:font-sans placeholder:text-xs placeholder:text-base-content/40"
|
||||||
|
placeholder={transI18n("enterUidPlaceholder")}
|
||||||
/>
|
/>
|
||||||
<button onClick={handlerFetchData} className="btn btn-sm btn-success rounded-xl font-bold">
|
{uidInput && (
|
||||||
{transI18n("getData")}
|
<button
|
||||||
</button>
|
type="button"
|
||||||
{selectedCharacters.length > 0 && (
|
onClick={() => setUidInput("")}
|
||||||
<button onClick={handleImport} className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs">
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/40 hover:text-base-content text-xs p-0.5 rounded-full hover:bg-base-300 transition-colors"
|
||||||
{transI18n("import")}
|
>
|
||||||
|
✕
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{Error && (
|
<button
|
||||||
<div className="text-error text-sm font-semibold mt-2">😭 {Error}</div>
|
type="button"
|
||||||
|
onClick={handlerFetchData}
|
||||||
|
disabled={isLoading}
|
||||||
|
className={`h-10 min-h-[40px] px-5 rounded-xl font-bold text-xs sm:text-sm flex items-center justify-center gap-2 select-none shrink-0 transition-all duration-200 shadow-md ${
|
||||||
|
isLoading
|
||||||
|
? 'bg-cyan-600 text-white cursor-wait opacity-90 shadow-cyan-500/30'
|
||||||
|
: 'bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white shadow-cyan-500/25 hover:shadow-cyan-500/40 hover:scale-[1.02] active:scale-[0.98] cursor-pointer'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-white shrink-0" />
|
||||||
|
<span className="text-white font-bold">{transI18n("fetchingData")}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles className="w-4 h-4 text-white shrink-0" />
|
||||||
|
<span className="text-white font-bold">{transI18n("getData")}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message Banner */}
|
||||||
|
{errorMsg && (
|
||||||
|
<div className="flex items-center gap-2 px-4 py-3 rounded-xl bg-rose-500/10 border border-rose-500/30 text-rose-400 text-xs font-semibold animate-in fade-in duration-200">
|
||||||
|
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||||
|
<span>{errorMsg}</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Player Info */}
|
{/* Trailblazer Profile HUD Banner (when loaded) */}
|
||||||
{enkaData && (
|
{enkaData && (
|
||||||
<div className="bg-base-200 p-4 rounded-xl border border-base-300 text-base-content space-y-1">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4 py-3 px-4 rounded-xl bg-base-200/50 border border-white/10">
|
||||||
<div className="text-sm font-semibold">Name: <span className="font-bold text-primary">{enkaData.detailInfo.nickname}</span></div>
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-sm font-semibold">UID: <span className="font-mono">{enkaData.detailInfo.uid}</span></div>
|
<div className="w-10 h-10 rounded-xl bg-primary/10 border border-primary/30 flex items-center justify-center text-primary font-bold text-base shrink-0">
|
||||||
<div className="text-sm font-semibold">Level: <span className="font-bold">{enkaData.detailInfo.level}</span></div>
|
<UserCheck className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="font-bold text-base-content text-sm sm:text-base truncate">
|
||||||
|
{enkaData.detailInfo.nickname}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-mono font-bold px-1.5 py-0.5 rounded bg-primary/15 text-primary border border-primary/25">
|
||||||
|
Lv.{enkaData.detailInfo.level}
|
||||||
|
</span>
|
||||||
|
{enkaData.detailInfo.worldLevel && (
|
||||||
|
<span className="text-[10px] font-mono text-base-content/60 px-1.5 py-0.5 rounded bg-base-300/60">
|
||||||
|
WL {enkaData.detailInfo.worldLevel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] font-mono text-base-content/50 mt-0.5 truncate">
|
||||||
|
UID: {enkaData.detailInfo.uid}
|
||||||
|
{enkaData.detailInfo.signature && (
|
||||||
|
<span className="ml-2 font-sans italic text-base-content/40">
|
||||||
|
“{enkaData.detailInfo.signature}”
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 self-end sm:self-center shrink-0">
|
||||||
|
<span className="text-xs text-base-content/60">{transI18n("publicCharacters")}:</span>
|
||||||
|
<span className="badge badge-primary font-mono font-bold text-xs">
|
||||||
|
{validCharacters.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Selection Info */}
|
{/* Floating Selection & Actions Bar */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 bg-base-200 border border-base-300 p-3.5 rounded-2xl shadow-xs">
|
{enkaData && (
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 py-2.5 px-4 rounded-xl bg-base-200/60 backdrop-blur-md border border-white/10 shadow-xs">
|
||||||
|
{/* Left: Counter and Quick Select */}
|
||||||
|
<div className="flex items-center justify-between sm:justify-start gap-3 flex-wrap">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<Users className="w-4 h-4 text-cyan-500" />
|
||||||
<span className="text-xs sm:text-sm font-bold text-base-content">
|
<span className="text-xs sm:text-sm font-bold text-base-content">
|
||||||
{transI18n("selectedCharacters")}:
|
{transI18n("selectedCharacters")}:
|
||||||
</span>
|
</span>
|
||||||
<span className="badge badge-primary font-mono font-bold text-xs">
|
<span className="px-2 py-0.5 rounded-full bg-cyan-500/20 text-cyan-400 border border-cyan-500/30 font-mono font-bold text-xs">
|
||||||
{selectedCharacters.length}
|
{selectedCharacters.length} / {validCharacters.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
<div className="h-4 w-px bg-white/10 hidden sm:block" />
|
||||||
onClick={clearSelection}
|
|
||||||
className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
|
<div className="flex items-center gap-1.5">
|
||||||
>
|
|
||||||
{transI18n("clearAll")}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={selectAll}
|
onClick={selectAll}
|
||||||
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
|
className="h-8 px-2.5 rounded-lg text-xs font-semibold text-cyan-400 hover:text-cyan-300 bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/25 transition-all flex items-center gap-1 cursor-pointer"
|
||||||
>
|
>
|
||||||
|
<CheckCheck className="w-3.5 h-3.5" />
|
||||||
{transI18n("selectAll")}
|
{transI18n("selectAll")}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearSelection}
|
||||||
|
className="h-8 px-2.5 rounded-lg text-xs font-semibold text-rose-400 hover:text-rose-300 bg-rose-500/10 hover:bg-rose-500/20 border border-rose-500/25 transition-all flex items-center gap-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3.5 h-3.5" />
|
||||||
|
{transI18n("clearAll")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{isLoading && (
|
{/* Right: Import CTA */}
|
||||||
<div className="flex justify-center items-center h-64">
|
<button
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
type="button"
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={selectedCharacters.length === 0}
|
||||||
|
className="h-9 px-5 rounded-xl font-bold text-xs sm:text-sm bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white shadow-md shadow-cyan-500/25 hover:shadow-cyan-500/40 disabled:opacity-40 disabled:pointer-events-none flex items-center justify-center gap-1.5 transition-all w-full sm:w-auto cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="text-white">{transI18n("import")}</span>
|
||||||
|
<span className="font-mono text-xs text-cyan-100">({selectedCharacters.length})</span>
|
||||||
|
<ArrowRight className="w-3.5 h-3.5 text-white" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Loading Shimmer State */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3 sm:gap-4 py-4">
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
|
<div key={i} className="rounded-2xl bg-base-200/30 border border-white/5 animate-pulse p-3.5 space-y-3">
|
||||||
|
<div className="w-full aspect-4/5 rounded-xl bg-base-300/40" />
|
||||||
|
<div className="h-4 bg-base-300/60 rounded w-2/3 mx-auto" />
|
||||||
|
<div className="h-3 bg-base-300/40 rounded w-1/2 mx-auto" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Empty State (when no UID fetched yet) */}
|
||||||
|
{!isLoading && !enkaData && (
|
||||||
|
<div className="relative overflow-hidden py-12 sm:py-16 px-4 sm:px-6 rounded-2xl border border-dashed border-white/15 bg-base-200/20 text-center select-none">
|
||||||
|
{/* Ambient Glow */}
|
||||||
|
<div className="pointer-events-none absolute -top-12 left-1/2 -translate-x-1/2 h-40 w-40 rounded-full bg-emerald-500/10 blur-3xl" />
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-md mx-auto space-y-4">
|
||||||
|
<div className="w-14 h-14 sm:w-16 sm:h-16 rounded-2xl bg-base-300/60 border border-white/10 text-emerald-400 mx-auto flex items-center justify-center shadow-lg">
|
||||||
|
<Globe className="w-7 h-7 sm:w-8 sm:h-8 stroke-[1.5]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<h3 className="text-base sm:text-lg font-bold text-base-content">
|
||||||
|
{transI18n("fetchFromEnka")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-base-content/60 leading-relaxed">
|
||||||
|
{transI18n("fetchEnkaHint")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step hints */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5 sm:gap-3 pt-3 text-left border-t border-white/5">
|
||||||
|
<div className="p-3 rounded-xl bg-base-300/30">
|
||||||
|
<span className="text-[10px] font-bold text-emerald-400 block mb-0.5">{transI18n("step1")}</span>
|
||||||
|
<p className="text-[11px] text-base-content/70 leading-tight">{transI18n("inGameProfile")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-xl bg-base-300/30">
|
||||||
|
<span className="text-[10px] font-bold text-emerald-400 block mb-0.5">{transI18n("step2")}</span>
|
||||||
|
<p className="text-[11px] text-base-content/70 leading-tight">{transI18n("displayCharactersPublicly")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-xl bg-base-300/30">
|
||||||
|
<span className="text-[10px] font-bold text-emerald-400 block mb-0.5">{transI18n("step3")}</span>
|
||||||
|
<p className="text-[11px] text-base-content/70 leading-tight">{transI18n("enterUidAndSync")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Character Grid */}
|
{/* Character Grid */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4 sm:gap-6">
|
{!isLoading && enkaData && (
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3 sm:gap-4">
|
||||||
{validCharacters.map((character) => (
|
{validCharacters.map((character) => (
|
||||||
<CharacterInfoCard
|
<CharacterInfoCard
|
||||||
key={character.avatarId}
|
key={character.avatarId}
|
||||||
@@ -244,6 +428,8 @@ export default function EnkaImport() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+289
-107
@@ -4,7 +4,7 @@ import useFreeSRStore from "@/stores/freesrStore";
|
|||||||
import useModelStore from "@/stores/modelStore";
|
import useModelStore from "@/stores/modelStore";
|
||||||
import useUserDataStore from "@/stores/userDataStore";
|
import useUserDataStore from "@/stores/userDataStore";
|
||||||
import { CharacterInfoCardType } from "@/types";
|
import { CharacterInfoCardType } from "@/types";
|
||||||
import { useState } from "react";
|
import { useState, useRef } from "react";
|
||||||
import CharacterInfoCard from "../card/characterInfoCard";
|
import CharacterInfoCard from "../card/characterInfoCard";
|
||||||
import { freeSrJsonSchema } from "@/zod";
|
import { freeSrJsonSchema } from "@/zod";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -13,12 +13,22 @@ import { useTranslations } from "next-intl";
|
|||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
UploadCloud,
|
||||||
|
FileJson,
|
||||||
|
CheckCheck,
|
||||||
|
RotateCcw,
|
||||||
|
AlertCircle,
|
||||||
|
ArrowRight,
|
||||||
|
Users,
|
||||||
|
FileCheck
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
type FreeSRJsonInput = z.infer<typeof freeSrJsonSchema>;
|
type FreeSRJsonInput = z.infer<typeof freeSrJsonSchema>;
|
||||||
|
|
||||||
const toCharacterInfoCard = (data: FreeSRJsonInput, character: FreeSRJsonInput["avatars"][string]): CharacterInfoCardType => {
|
const toCharacterInfoCard = (data: FreeSRJsonInput, character: FreeSRJsonInput["avatars"][string]): CharacterInfoCardType => {
|
||||||
const lightcone = data.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
|
const lightcone = data.lightcones?.find((lc) => lc.equip_avatar === character.avatar_id);
|
||||||
const relics = data.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
|
const relics = data.relics?.filter((relic) => relic.equip_avatar === character.avatar_id) || [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: character.avatar_id,
|
key: character.avatar_id,
|
||||||
@@ -35,8 +45,8 @@ const toCharacterInfoCard = (data: FreeSRJsonInput, character: FreeSRJsonInput["
|
|||||||
relic_id: relic.relic_id,
|
relic_id: relic.relic_id,
|
||||||
relic_set_id: relic.relic_set_id,
|
relic_set_id: relic.relic_set_id,
|
||||||
})),
|
})),
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function FreeSRImport() {
|
export default function FreeSRImport() {
|
||||||
const { avatars, setAvatar } = useUserDataStore(
|
const { avatars, setAvatar } = useUserDataStore(
|
||||||
@@ -54,9 +64,14 @@ export default function FreeSRImport() {
|
|||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
setIsOpenImport: state.setIsOpenImport,
|
setIsOpenImport: state.setIsOpenImport,
|
||||||
}))
|
}))
|
||||||
)
|
);
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [Error, setError] = useState("")
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
|
const [fileName, setFileName] = useState<string | null>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore(
|
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
freeSRData: state.freeSRData,
|
freeSRData: state.freeSRData,
|
||||||
@@ -64,14 +79,19 @@ export default function FreeSRImport() {
|
|||||||
selectedCharacters: state.selectedCharacters,
|
selectedCharacters: state.selectedCharacters,
|
||||||
setSelectedCharacters: state.setSelectedCharacters,
|
setSelectedCharacters: state.setSelectedCharacters,
|
||||||
}))
|
}))
|
||||||
)
|
);
|
||||||
const transI18n = useTranslations("DataPage")
|
|
||||||
const parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData)
|
const transI18n = useTranslations("DataPage");
|
||||||
const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null
|
const parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData);
|
||||||
|
const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null;
|
||||||
|
|
||||||
|
const availableCharacters = validFreeSRData
|
||||||
|
? Object.values(validFreeSRData.avatars).filter(it => mapAvatar?.[it.avatar_id])
|
||||||
|
: [];
|
||||||
|
|
||||||
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
||||||
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
|
if (selectedCharacters.some((selected) => selected.key === character.key)) {
|
||||||
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
|
setSelectedCharacters(selectedCharacters.filter((selected) => selected.key !== character.key));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedCharacters([...selectedCharacters, character]);
|
setSelectedCharacters([...selectedCharacters, character]);
|
||||||
@@ -82,93 +102,118 @@ export default function FreeSRImport() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectAll = () => {
|
const selectAll = () => {
|
||||||
const parsed = freeSrJsonSchema.safeParse(freeSRData)
|
if (validFreeSRData) {
|
||||||
if (parsed.success) {
|
setSelectedCharacters(availableCharacters.map((char) => toCharacterInfoCard(validFreeSRData, char)));
|
||||||
setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character)));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const processFile = (file: File) => {
|
||||||
setIsLoading(true)
|
setIsLoading(true);
|
||||||
const file = event.target.files?.[0];
|
if (!file.name.endsWith(".json") && file.type !== "application/json") {
|
||||||
if (!file) {
|
setSelectedCharacters([]);
|
||||||
setSelectedCharacters([])
|
setFreeSRData(null);
|
||||||
setFreeSRData(null)
|
setFileName(null);
|
||||||
setError(transI18n("pleaseSelectAFile"))
|
setErrorMsg(transI18n("fileMustBeAValidJsonFile"));
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (!file.name.endsWith(".json") || file.type !== "application/json") {
|
|
||||||
setSelectedCharacters([])
|
|
||||||
setFreeSRData(null)
|
|
||||||
setError(transI18n("fileMustBeAValidJsonFile"))
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.target?.result as string);
|
const raw = JSON.parse(e.target?.result as string);
|
||||||
const parsed = freeSrJsonSchema.safeParse(data)
|
const parsed = freeSrJsonSchema.safeParse(raw);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
setSelectedCharacters([])
|
setSelectedCharacters([]);
|
||||||
setFreeSRData(null)
|
setFreeSRData(null);
|
||||||
setError(transI18n("fileMustBeAValidJsonFile"))
|
setFileName(null);
|
||||||
return
|
setErrorMsg(transI18n("fileMustBeAValidJsonFile"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFreeSRData(parsed.data)
|
setFreeSRData(parsed.data);
|
||||||
setError("")
|
setFileName(file.name);
|
||||||
|
setErrorMsg("");
|
||||||
|
|
||||||
setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character)));
|
const validChars = Object.values(parsed.data.avatars)
|
||||||
|
.filter(it => mapAvatar?.[it.avatar_id])
|
||||||
|
.map((char) => toCharacterInfoCard(parsed.data, char));
|
||||||
|
setSelectedCharacters(validChars);
|
||||||
} catch {
|
} catch {
|
||||||
|
setSelectedCharacters([]);
|
||||||
setSelectedCharacters([])
|
setFreeSRData(null);
|
||||||
setFreeSRData(null)
|
setFileName(null);
|
||||||
setError(transI18n("fileMustBeAValidJsonFile"))
|
setErrorMsg(transI18n("fileMustBeAValidJsonFile"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = () => {
|
reader.onerror = () => {
|
||||||
setSelectedCharacters([])
|
setSelectedCharacters([]);
|
||||||
setFreeSRData(null)
|
setFreeSRData(null);
|
||||||
setError(transI18n("fileMustBeAValidJsonFile"))
|
setFileName(null);
|
||||||
setIsLoading(false)
|
setErrorMsg(transI18n("fileMustBeAValidJsonFile"));
|
||||||
}
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
processFile(file);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
const file = e.dataTransfer.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
processFile(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleImport = () => {
|
const handleImport = () => {
|
||||||
if (selectedCharacters.length === 0) {
|
if (selectedCharacters.length === 0) {
|
||||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
setErrorMsg(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!freeSRData) {
|
if (!freeSRData) {
|
||||||
setError(transI18n("noDataToImport"));
|
setErrorMsg(transI18n("noDataToImport"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError("");
|
setErrorMsg("");
|
||||||
|
|
||||||
const parsed = freeSrJsonSchema.safeParse(freeSRData)
|
const parsed = freeSrJsonSchema.safeParse(freeSRData);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
setError(transI18n("fileMustBeAValidJsonFile"));
|
setErrorMsg(transI18n("fileMustBeAValidJsonFile"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const listAvatars = { ...avatars }
|
const listAvatars = { ...avatars };
|
||||||
const filterData = Object.values(parsed.data.avatars).filter((character) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
|
const filterData = Object.values(parsed.data.avatars).filter((character) =>
|
||||||
|
selectedCharacters.some((selected) => selected.avatar_id === character.avatar_id)
|
||||||
|
);
|
||||||
|
|
||||||
filterData.forEach((character) => {
|
filterData.forEach((character) => {
|
||||||
const currentAvatar = listAvatars[character.avatar_id]
|
const currentAvatar = listAvatars[character.avatar_id];
|
||||||
if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
|
if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
|
||||||
const newProfileList = [...(currentAvatar.profileList || [])]
|
const newProfileList = [...(currentAvatar.profileList || [])];
|
||||||
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newProfileList.length, character.avatar_id)
|
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newProfileList.length, character.avatar_id);
|
||||||
if (newProfile) {
|
if (newProfile) {
|
||||||
newProfileList.push(newProfile)
|
newProfileList.push(newProfile);
|
||||||
}
|
}
|
||||||
const newAvatar = {
|
const newAvatar = {
|
||||||
...currentAvatar,
|
...currentAvatar,
|
||||||
@@ -180,81 +225,218 @@ export default function FreeSRImport() {
|
|||||||
},
|
},
|
||||||
profileList: newProfileList,
|
profileList: newProfileList,
|
||||||
profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
|
profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
|
||||||
|
};
|
||||||
|
setAvatar(newAvatar);
|
||||||
}
|
}
|
||||||
setAvatar(newAvatar)
|
});
|
||||||
}
|
|
||||||
})
|
setIsOpenImport(false);
|
||||||
setIsOpenImport(false)
|
toast.success(transI18n("importFreeSRDataSuccess"));
|
||||||
toast.success(transI18n("importFreeSRDataSuccess"))
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-6">
|
<div className="w-full space-y-6">
|
||||||
{/* Header */}
|
{/* Top Bar: Title & Upload Trigger */}
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 sm:gap-4">
|
||||||
<h1 className="text-base-content text-xl sm:text-2xl font-bold">{transI18n("freeSRImport")}</h1>
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="px-2 py-0.5 text-[10px] font-bold uppercase tracking-widest rounded-md bg-primary/15 text-primary border border-primary/25">
|
||||||
|
FreeSR Protocol
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-base-content/50 font-medium">JSON Importer</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl sm:text-2xl font-black tracking-tight text-base-content flex items-center gap-2">
|
||||||
|
{transI18n("freeSRImport")}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 items-center">
|
{/* File Dropzone / Active File Badge */}
|
||||||
<fieldset className="fieldset">
|
<input
|
||||||
<legend className="fieldset-legend text-xs font-semibold">{transI18n("pickAFile")}</legend>
|
type="file"
|
||||||
<input type="file" accept=".json" className="file-input file-input-sm file-input-bordered rounded-xl" onChange={handlerReadFile} />
|
ref={fileInputRef}
|
||||||
<label className="label text-[11px] text-base-content/60">{transI18n("onlySupportFreeSRJsonFile")}</label>
|
accept=".json,application/json"
|
||||||
</fieldset>
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
|
||||||
{selectedCharacters.length > 0 && (
|
{!validFreeSRData ? (
|
||||||
<button onClick={handleImport} className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs">
|
<div
|
||||||
{transI18n("import")}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
className={`flex items-center gap-3 px-4 py-2.5 rounded-xl border border-dashed cursor-pointer transition-all duration-200 select-none w-full sm:w-auto ${
|
||||||
|
isDragging
|
||||||
|
? 'bg-primary/15 border-primary shadow-[0_0_20px_rgba(var(--color-primary-rgb,6,182,212),0.3)] scale-[1.02]'
|
||||||
|
: 'bg-base-200/50 hover:bg-base-200/80 border-white/20 hover:border-primary/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-primary/10 text-primary flex items-center justify-center shrink-0">
|
||||||
|
<UploadCloud className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-xs font-bold text-base-content flex items-center gap-1.5">
|
||||||
|
{transI18n("pickAFile")}
|
||||||
|
<span className="text-[10px] font-normal text-base-content/60">(.json)</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-base-content/50 truncate max-w-[200px]">
|
||||||
|
{transI18n("onlySupportFreeSRJsonFile")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2.5 w-full sm:w-auto">
|
||||||
|
<div className="flex items-center justify-between sm:justify-start gap-2 h-10 px-3.5 rounded-xl bg-emerald-500/10 border border-emerald-500/25 text-emerald-400 text-xs font-semibold">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<FileCheck className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span className="truncate max-w-[180px] font-mono text-xs">{fileName || "data.json"}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 font-bold shrink-0">
|
||||||
|
{availableCharacters.length} {transI18n("avatars")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className="h-10 px-3.5 rounded-xl font-semibold text-xs bg-base-200 hover:bg-base-300 text-base-content border border-base-content/15 transition-all flex items-center justify-center cursor-pointer"
|
||||||
|
>
|
||||||
|
{transI18n("changeFile")}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{Error && (
|
|
||||||
<div className="text-error text-sm font-semibold mt-2">😭 {Error}</div>
|
{/* Error Message */}
|
||||||
|
{errorMsg && (
|
||||||
|
<div className="flex items-center gap-2 px-4 py-3 rounded-xl bg-rose-500/10 border border-rose-500/30 text-rose-400 text-xs font-semibold animate-in fade-in duration-200">
|
||||||
|
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||||
|
<span>{errorMsg}</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Selection Info */}
|
{/* Floating Selection & Actions Bar */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 bg-base-200 border border-base-300 p-3.5 rounded-2xl shadow-xs">
|
{validFreeSRData && (
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 py-2.5 px-4 rounded-xl bg-base-200/60 backdrop-blur-md border border-white/10 shadow-xs">
|
||||||
|
{/* Left: Counter and Quick Select */}
|
||||||
|
<div className="flex items-center justify-between sm:justify-start gap-3 flex-wrap">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<Users className="w-4 h-4 text-cyan-500" />
|
||||||
<span className="text-xs sm:text-sm font-bold text-base-content">
|
<span className="text-xs sm:text-sm font-bold text-base-content">
|
||||||
{transI18n("selectedCharacters")}:
|
{transI18n("selectedCharacters")}:
|
||||||
</span>
|
</span>
|
||||||
<span className="badge badge-primary font-mono font-bold text-xs">
|
<span className="px-2 py-0.5 rounded-full bg-cyan-500/20 text-cyan-400 border border-cyan-500/30 font-mono font-bold text-xs">
|
||||||
{selectedCharacters.length}
|
{selectedCharacters.length} / {availableCharacters.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
<div className="h-4 w-px bg-white/10 hidden sm:block" />
|
||||||
onClick={clearSelection}
|
|
||||||
className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
|
<div className="flex items-center gap-1.5">
|
||||||
>
|
|
||||||
{transI18n("clearAll")}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={selectAll}
|
onClick={selectAll}
|
||||||
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
|
className="h-8 px-2.5 rounded-lg text-xs font-semibold text-cyan-400 hover:text-cyan-300 bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/25 transition-all flex items-center gap-1 cursor-pointer"
|
||||||
>
|
>
|
||||||
|
<CheckCheck className="w-3.5 h-3.5" />
|
||||||
{transI18n("selectAll")}
|
{transI18n("selectAll")}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearSelection}
|
||||||
|
className="h-8 px-2.5 rounded-lg text-xs font-semibold text-rose-400 hover:text-rose-300 bg-rose-500/10 hover:bg-rose-500/20 border border-rose-500/25 transition-all flex items-center gap-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3.5 h-3.5" />
|
||||||
|
{transI18n("clearAll")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{isLoading && (
|
{/* Right: Import CTA */}
|
||||||
<div className="flex justify-center items-center h-64">
|
<button
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
type="button"
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={selectedCharacters.length === 0}
|
||||||
|
className="h-9 px-5 rounded-xl font-bold text-xs sm:text-sm bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white shadow-md shadow-cyan-500/25 hover:shadow-cyan-500/40 disabled:opacity-40 disabled:pointer-events-none flex items-center justify-center gap-1.5 transition-all w-full sm:w-auto cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="text-white">{transI18n("import")}</span>
|
||||||
|
<span className="font-mono text-xs text-cyan-100">({selectedCharacters.length})</span>
|
||||||
|
<ArrowRight className="w-3.5 h-3.5 text-white" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Loading Shimmer State */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3 sm:gap-4 py-4">
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
|
<div key={i} className="rounded-2xl bg-base-200/30 border border-white/5 animate-pulse p-3.5 space-y-3">
|
||||||
|
<div className="w-full aspect-4/5 rounded-xl bg-base-300/40" />
|
||||||
|
<div className="h-4 bg-base-300/60 rounded w-2/3 mx-auto" />
|
||||||
|
<div className="h-3 bg-base-300/40 rounded w-1/2 mx-auto" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Empty State (when no valid data is loaded) */}
|
||||||
|
{!isLoading && !validFreeSRData && (
|
||||||
|
<div
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
className="relative overflow-hidden py-12 sm:py-16 px-4 sm:px-6 rounded-2xl border border-dashed border-white/15 bg-base-200/20 hover:bg-base-200/40 transition-all duration-300 text-center cursor-pointer group select-none"
|
||||||
|
>
|
||||||
|
{/* Ambient Glow */}
|
||||||
|
<div className="pointer-events-none absolute -top-12 left-1/2 -translate-x-1/2 h-40 w-40 rounded-full bg-primary/10 blur-3xl group-hover:scale-150 transition-transform duration-700" />
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-md mx-auto space-y-4">
|
||||||
|
<div className="w-14 h-14 sm:w-16 sm:h-16 rounded-2xl bg-base-300/60 border border-white/10 text-primary mx-auto flex items-center justify-center shadow-lg group-hover:scale-110 group-hover:border-primary/40 transition-all duration-300">
|
||||||
|
<FileJson className="w-7 h-7 sm:w-8 sm:h-8 stroke-[1.5]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<h3 className="text-base sm:text-lg font-bold text-base-content">
|
||||||
|
{transI18n("noFreeSRDataLoaded")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-base-content/60 leading-relaxed">
|
||||||
|
{transI18n("dropFreeSRJsonHint")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step hints */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5 sm:gap-3 pt-3 text-left border-t border-white/5">
|
||||||
|
<div className="p-3 rounded-xl bg-base-300/30">
|
||||||
|
<span className="text-[10px] font-bold text-primary block mb-0.5">{transI18n("step1")}</span>
|
||||||
|
<p className="text-[11px] text-base-content/70 leading-tight">{transI18n("exportJsonFromFreeSR")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-xl bg-base-300/30">
|
||||||
|
<span className="text-[10px] font-bold text-primary block mb-0.5">{transI18n("step2")}</span>
|
||||||
|
<p className="text-[11px] text-base-content/70 leading-tight">{transI18n("dropOrSelectFile")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-xl bg-base-300/30">
|
||||||
|
<span className="text-[10px] font-bold text-primary block mb-0.5">{transI18n("step3")}</span>
|
||||||
|
<p className="text-[11px] text-base-content/70 leading-tight">{transI18n("syncCharacterBuilds")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Character Grid */}
|
{/* Character Grid */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4 sm:gap-6">
|
{!isLoading && validFreeSRData && (
|
||||||
{validFreeSRData && Object.values(validFreeSRData.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => {
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3 sm:gap-4">
|
||||||
return (
|
{availableCharacters.map((character) => (
|
||||||
<CharacterInfoCard
|
<CharacterInfoCard
|
||||||
key={character.avatar_id}
|
key={character.avatar_id}
|
||||||
character={toCharacterInfoCard(validFreeSRData, character)}
|
character={toCharacterInfoCard(validFreeSRData, character)}
|
||||||
selectedCharacters={selectedCharacters}
|
selectedCharacters={selectedCharacters}
|
||||||
onCharacterToggle={handleCharacterToggle}
|
onCharacterToggle={handleCharacterToggle}
|
||||||
/>
|
/>
|
||||||
)
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -401,7 +401,8 @@ export default function QuickView() {
|
|||||||
subAffix,
|
subAffix,
|
||||||
relicEffects,
|
relicEffects,
|
||||||
mapRelicSet,
|
mapRelicSet,
|
||||||
avatarSkillTree
|
avatarSkillTree,
|
||||||
|
transI18n
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
"use client";
|
"use client";
|
||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
import RelicMaker from "../relicBar";
|
import RelicMaker from "../relicBar";
|
||||||
import useUserDataStore from "@/stores/userDataStore";
|
import useUserDataStore from "@/stores/userDataStore";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -15,6 +16,8 @@ import useDetailDataStore from "@/stores/detailDataStore";
|
|||||||
import { getLocaleName } from '@/helper';
|
import { getLocaleName } from '@/helper';
|
||||||
import useLocaleStore from "@/stores/localeStore";
|
import useLocaleStore from "@/stores/localeStore";
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import { Trash2 } from "lucide-react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export default function RelicsInfo() {
|
export default function RelicsInfo() {
|
||||||
const { avatars, setAvatar } = useUserDataStore(
|
const { avatars, setAvatar } = useUserDataStore(
|
||||||
@@ -78,6 +81,8 @@ export default function RelicsInfo() {
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [relicSlotToDelete, setRelicSlotToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleShow = (modalId: string) => {
|
const handleShow = (modalId: string) => {
|
||||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||||
if (modal) {
|
if (modal) {
|
||||||
@@ -125,6 +130,25 @@ export default function RelicsInfo() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const requestDeleteRelic = (slot: string) => {
|
||||||
|
setRelicSlotToDelete(slot);
|
||||||
|
handleShow("delete_relic_confirm_modal");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (relicSlotToDelete) {
|
||||||
|
handlerDeleteRelic(relicSlotToDelete);
|
||||||
|
toast.success(transI18n("relicDeletedSuccessfully"));
|
||||||
|
handleCloseModal("delete_relic_confirm_modal");
|
||||||
|
setRelicSlotToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelDelete = () => {
|
||||||
|
handleCloseModal("delete_relic_confirm_modal");
|
||||||
|
setRelicSlotToDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handlerChangeRelic = (slot: string) => {
|
const handlerChangeRelic = (slot: string) => {
|
||||||
const relic = getRelic(slot)
|
const relic = getRelic(slot)
|
||||||
setSelectedRelicSlot(slot)
|
setSelectedRelicSlot(slot)
|
||||||
@@ -217,6 +241,7 @@ export default function RelicsInfo() {
|
|||||||
for (const item of modalConfigs) {
|
for (const item of modalConfigs) {
|
||||||
handleCloseModal(item?.id || "")
|
handleCloseModal(item?.id || "")
|
||||||
}
|
}
|
||||||
|
handleCloseModal("delete_relic_confirm_modal")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -250,22 +275,14 @@ export default function RelicsInfo() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 6-Slot Grid completely free of box-in-box */}
|
{/* 6-Slot Grid completely free of box-in-box */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-6 py-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||||
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
|
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => {
|
onClick={() => setSelectedRelicSlot(item)}
|
||||||
if (item === selectedRelicSlot) {
|
className={`group relative rounded-xl transition-all duration-200 cursor-pointer ${selectedRelicSlot === item
|
||||||
setSelectedRelicSlot("")
|
? "ring-2 ring-primary ring-offset-2 ring-offset-base-100 shadow-md"
|
||||||
} else {
|
: "hover:ring-1 hover:ring-base-content/20"
|
||||||
setSelectedRelicSlot(item)
|
|
||||||
}
|
|
||||||
handlerChangeRelic(item)
|
|
||||||
}}
|
|
||||||
className={`relative group flex flex-col items-center justify-center p-2 rounded-2xl transition-all duration-200 cursor-pointer ${
|
|
||||||
item === selectedRelicSlot
|
|
||||||
? "bg-primary/5 rounded-2xl ring-1.5 ring-primary/40"
|
|
||||||
: "hover:bg-base-content/5"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<RelicCard
|
<RelicCard
|
||||||
@@ -276,6 +293,7 @@ export default function RelicsInfo() {
|
|||||||
{/* Micro Actions */}
|
{/* Micro Actions */}
|
||||||
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
|
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handlerChangeRelic(item)
|
handlerChangeRelic(item)
|
||||||
@@ -290,11 +308,10 @@ export default function RelicsInfo() {
|
|||||||
|
|
||||||
{getRelic(item) && (
|
{getRelic(item) && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
|
requestDeleteRelic(item)
|
||||||
handlerDeleteRelic(item)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className="btn btn-xs btn-circle btn-error shadow-xs"
|
className="btn btn-xs btn-circle btn-error shadow-xs"
|
||||||
title={transI18n("deleteRelic")}
|
title={transI18n("deleteRelic")}
|
||||||
@@ -312,6 +329,7 @@ export default function RelicsInfo() {
|
|||||||
{/* Bottom Actions Bar */}
|
{/* Bottom Actions Bar */}
|
||||||
<div className="grid grid-cols-2 gap-3 pt-2">
|
<div className="grid grid-cols-2 gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
disabled={!selectedRelicSlot}
|
disabled={!selectedRelicSlot}
|
||||||
onClick={() => handlerChangeRelic(selectedRelicSlot)}
|
onClick={() => handlerChangeRelic(selectedRelicSlot)}
|
||||||
className="btn btn-primary btn-sm rounded-xl font-bold shadow-xs"
|
className="btn btn-primary btn-sm rounded-xl font-bold shadow-xs"
|
||||||
@@ -319,14 +337,15 @@ export default function RelicsInfo() {
|
|||||||
{transI18n("changeRelic")}
|
{transI18n("changeRelic")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled={!selectedRelicSlot}
|
type="button"
|
||||||
|
disabled={!selectedRelicSlot || !getRelic(selectedRelicSlot)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
|
if (selectedRelicSlot) {
|
||||||
handlerDeleteRelic(selectedRelicSlot)
|
requestDeleteRelic(selectedRelicSlot)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="btn btn-ghost btn-sm text-error hover:bg-error/20 rounded-xl"
|
className="btn btn-ghost btn-sm text-error hover:bg-error/20 rounded-xl disabled:opacity-40 disabled:pointer-events-none"
|
||||||
>
|
>
|
||||||
{transI18n("deleteRelic")}
|
{transI18n("deleteRelic")}
|
||||||
</button>
|
</button>
|
||||||
@@ -413,6 +432,7 @@ export default function RelicsInfo() {
|
|||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="btn btn-sm btn-circle btn-ghost text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors"
|
className="btn btn-sm btn-circle btn-ghost text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
@@ -429,6 +449,82 @@ export default function RelicsInfo() {
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Delete Relic Confirmation Modal */}
|
||||||
|
<dialog id="delete_relic_confirm_modal" className="modal modal-bottom sm:modal-middle">
|
||||||
|
<div className="modal-box max-w-md bg-base-100/95 backdrop-blur-xl border border-white/10 shadow-2xl rounded-2xl p-6 overflow-hidden relative">
|
||||||
|
{/* Ambient Glow */}
|
||||||
|
<div className="pointer-events-none absolute -top-12 left-1/2 -translate-x-1/2 w-32 h-32 bg-rose-500/15 rounded-full blur-2xl" />
|
||||||
|
|
||||||
|
<div className="relative z-10 text-center space-y-4">
|
||||||
|
{/* Rose Icon Badge */}
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-rose-500/15 text-rose-400 border border-rose-500/30 flex items-center justify-center mx-auto shadow-[0_0_20px_rgba(244,63,94,0.2)]">
|
||||||
|
<Trash2 className="w-6 h-6 stroke-[1.75]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title & Description */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<h3 className="font-black text-lg text-base-content tracking-tight">
|
||||||
|
{transI18n("deleteRelic")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs sm:text-sm text-base-content/70 leading-relaxed">
|
||||||
|
{transI18n("deleteRelicConfirm")}{" "}
|
||||||
|
<span className="font-bold text-amber-400 capitalize font-mono">
|
||||||
|
{relicSlotToDelete ? (transI18n({ "1": "HEAD", "2": "HAND", "3": "BODY", "4": "FOOT", "5": "NECK", "6": "OBJECT" }[relicSlotToDelete] || relicSlotToDelete) || relicSlotToDelete) : ""}
|
||||||
|
</span>
|
||||||
|
?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Relic Preview Card (if relic exists) */}
|
||||||
|
{relicSlotToDelete && getRelic(relicSlotToDelete) && (
|
||||||
|
<div className="flex items-center gap-3 p-3 rounded-xl bg-base-200/70 border border-white/10 text-left">
|
||||||
|
<div className="w-10 h-10 rounded-lg flex items-center justify-center bg-base-300/60 border border-amber-500/30 shrink-0">
|
||||||
|
<Image
|
||||||
|
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${getRelic(relicSlotToDelete)?.relic_set_id}_${relicSlotToDelete}.png`}
|
||||||
|
alt="Relic"
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
width={36}
|
||||||
|
height={36}
|
||||||
|
className="w-7 h-7 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-xs font-bold text-base-content truncate">
|
||||||
|
{getLocaleName(locale, mapRelicSet[getRelic(relicSlotToDelete)?.relic_set_id || ""]?.Name)}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-base-content/60 font-mono">
|
||||||
|
+{getRelic(relicSlotToDelete)?.level} • <span>{transI18n({ "1": "HEAD", "2": "HAND", "3": "BODY", "4": "FOOT", "5": "NECK", "6": "OBJECT" }[relicSlotToDelete] || relicSlotToDelete)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex items-center gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCancelDelete}
|
||||||
|
className="flex-1 h-10 px-4 rounded-xl font-bold text-xs sm:text-sm bg-base-200 hover:bg-base-300 text-base-content/80 hover:text-base-content border border-white/10 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
{transI18n("cancel")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleConfirmDelete}
|
||||||
|
className="flex-1 h-10 px-4 rounded-xl font-bold text-xs sm:text-sm bg-gradient-to-r from-rose-500 to-red-600 hover:from-rose-400 hover:to-red-500 text-white shadow-md shadow-rose-500/25 hover:shadow-rose-500/40 transition-all flex items-center justify-center gap-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
<span>{transI18n("confirmDelete")}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" className="modal-backdrop">
|
||||||
|
<button onClick={handleCancelDelete}>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function ShowCaseInfo() {
|
|||||||
|
|
||||||
const handleSaveImage = useCallback(async () => {
|
const handleSaveImage = useCallback(async () => {
|
||||||
if (cardRef.current === null || !avatarSelected) {
|
if (cardRef.current === null || !avatarSelected) {
|
||||||
toast.error("Avatar showcase not found!");
|
toast.error(transI18n("noAvatarSelected") || "Avatar showcase not found!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isSaving) return;
|
if (isSaving) return;
|
||||||
@@ -78,7 +78,7 @@ export default function ShowCaseInfo() {
|
|||||||
|
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
toast.error("Failed to generate image!");
|
toast.error(transI18n("failedToGenerateImage"));
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -89,10 +89,10 @@ export default function ShowCaseInfo() {
|
|||||||
link.click();
|
link.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
toast.success(locale === "vi" ? "Đã lưu ảnh thành công!" : "Image saved successfully!");
|
toast.success(transI18n("imageSavedSuccessfully"));
|
||||||
}, "image/png");
|
}, "image/png");
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Error generating showcase card!");
|
toast.error(transI18n("errorGeneratingShowcaseCard"));
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}, [avatarSelected, locale, transI18n, isSaving]);
|
}, [avatarSelected, locale, transI18n, isSaving]);
|
||||||
@@ -638,16 +638,16 @@ export default function ShowCaseInfo() {
|
|||||||
const textToCopy = lines.join("\n");
|
const textToCopy = lines.join("\n");
|
||||||
navigator.clipboard.writeText(textToCopy)
|
navigator.clipboard.writeText(textToCopy)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(locale === "vi" ? "Đã sao chép thông tin build!" : "Build info copied to clipboard!");
|
toast.success(transI18n("buildInfoCopied"));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(locale === "vi" ? "Lỗi khi sao chép!" : "Failed to copy build info!");
|
toast.error(transI18n("copyFailed"));
|
||||||
});
|
});
|
||||||
}, [avatarSelected, avatarData, avatarProfile, mapLightCone, locale, transI18n, characterStats, relicEffects, mapRelicSet, relicStats]);
|
}, [avatarSelected, avatarData, avatarProfile, mapLightCone, locale, transI18n, characterStats, relicEffects, mapRelicSet, relicStats]);
|
||||||
|
|
||||||
const handleCopyImage = useCallback(async () => {
|
const handleCopyImage = useCallback(async () => {
|
||||||
if (cardRef.current === null || !avatarSelected) {
|
if (cardRef.current === null || !avatarSelected) {
|
||||||
toast.error("Avatar showcase not found!");
|
toast.error(transI18n("noAvatarSelected") || "Avatar showcase not found!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isCopyingImage) return;
|
if (isCopyingImage) return;
|
||||||
@@ -677,25 +677,25 @@ export default function ShowCaseInfo() {
|
|||||||
|
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
toast.error("Failed to create image blob");
|
toast.error(transI18n("failedToGenerateImage"));
|
||||||
setIsCopyingImage(false);
|
setIsCopyingImage(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigator.clipboard.write([
|
navigator.clipboard.write([
|
||||||
new ClipboardItem({ "image/png": blob })
|
new ClipboardItem({ "image/png": blob })
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
toast.success(locale === "vi" ? "Đã sao chép ảnh vào Clipboard!" : "Image copied to clipboard!");
|
toast.success(transI18n("imageCopiedToClipboard"));
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
toast.error(locale === "vi" ? "Trình duyệt không cho phép sao chép ảnh trực tiếp!" : "Clipboard write failed!");
|
toast.error(transI18n("clipboardWriteFailed"));
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
setIsCopyingImage(false);
|
setIsCopyingImage(false);
|
||||||
});
|
});
|
||||||
}, "image/png");
|
}, "image/png");
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Error generating showcase image!");
|
toast.error(transI18n("errorGeneratingShowcaseCard"));
|
||||||
setIsCopyingImage(false);
|
setIsCopyingImage(false);
|
||||||
}
|
}
|
||||||
}, [avatarSelected, locale, isCopyingImage]);
|
}, [avatarSelected, isCopyingImage, transI18n]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-4">
|
<div className="w-full space-y-4">
|
||||||
@@ -712,23 +712,23 @@ export default function ShowCaseInfo() {
|
|||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-ghost border border-base-content/20 rounded-xl font-bold shadow-xs active-press gap-1.5 hover:bg-base-200"
|
className="btn btn-sm btn-ghost border border-base-content/20 rounded-xl font-bold shadow-xs active-press gap-1.5 hover:bg-base-200"
|
||||||
onClick={handleCopyBuildText}
|
onClick={handleCopyBuildText}
|
||||||
title="Copy build summary as text"
|
title={transI18n("copyText")}
|
||||||
>
|
>
|
||||||
<Copy className="w-4 h-4" />
|
<Copy className="w-4 h-4" />
|
||||||
<span>{locale === "vi" ? "Sao chép Text" : "Copy Text"}</span>
|
<span>{transI18n("copyText")}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled={isCopyingImage}
|
disabled={isCopyingImage}
|
||||||
className="btn btn-sm btn-ghost border border-base-content/20 rounded-xl font-bold shadow-xs active-press gap-1.5 hover:bg-base-200"
|
className="btn btn-sm btn-ghost border border-base-content/20 rounded-xl font-bold shadow-xs active-press gap-1.5 hover:bg-base-200"
|
||||||
onClick={handleCopyImage}
|
onClick={handleCopyImage}
|
||||||
title="Copy card image to clipboard"
|
title={transI18n("copyImage")}
|
||||||
>
|
>
|
||||||
{isCopyingImage ? (
|
{isCopyingImage ? (
|
||||||
<span className="loading loading-spinner loading-xs"></span>
|
<span className="loading loading-spinner loading-xs"></span>
|
||||||
) : (
|
) : (
|
||||||
<ImageIcon className="w-4 h-4" />
|
<ImageIcon className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
<span>{isCopyingImage ? (locale === "vi" ? "Đang xử lý..." : "Processing...") : (locale === "vi" ? "Sao chép Ảnh" : "Copy Image")}</span>
|
<span>{isCopyingImage ? transI18n("processing") : transI18n("copyImage")}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
@@ -740,7 +740,7 @@ export default function ShowCaseInfo() {
|
|||||||
) : (
|
) : (
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
<span>{isSaving ? (locale === "vi" ? "Đang lưu..." : "Saving...") : transI18n("save")}</span>
|
<span>{isSaving ? transI18n("saving") : transI18n("save")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user