feat: add complete multilanguage support for german and english
This commit is contained in:
parent
d4be30cf74
commit
e5c61447a3
25 changed files with 500 additions and 168 deletions
|
|
@ -33,6 +33,7 @@
|
||||||
"registerHaveAccount": "Bereits registriert? ",
|
"registerHaveAccount": "Bereits registriert? ",
|
||||||
"registerLoginButton": "LOGIN",
|
"registerLoginButton": "LOGIN",
|
||||||
|
|
||||||
|
"hubCycleComplete": "Zyklus beendet! Beende ihn in den Stats.",
|
||||||
"hubNoActiveCycle": "Kein aktiver Zyklus",
|
"hubNoActiveCycle": "Kein aktiver Zyklus",
|
||||||
"hubCreateCycle": "Neuen Zyklus starten",
|
"hubCreateCycle": "Neuen Zyklus starten",
|
||||||
"hubCycleLabel": "Zyklus",
|
"hubCycleLabel": "Zyklus",
|
||||||
|
|
@ -153,6 +154,26 @@
|
||||||
"emomConfirm": "BESTÄTIGEN & BEENDEN",
|
"emomConfirm": "BESTÄTIGEN & BEENDEN",
|
||||||
"emomRepsPerRound": "Wiederholungen pro Runde",
|
"emomRepsPerRound": "Wiederholungen pro Runde",
|
||||||
|
|
||||||
|
"questDailyBounties": "TÄGLICHE KOPFGELDER",
|
||||||
|
"questViewAll": "ALLE ANZEIGEN >",
|
||||||
|
"questClaim": "EINSAMMELN",
|
||||||
|
"questRewardCollected": "Belohnung: {xp} XP erhalten!",
|
||||||
|
"@questRewardCollected": {
|
||||||
|
"placeholders": {
|
||||||
|
"xp": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"guideNotFound": "Keine Schriftrolle für diese Technik gefunden.",
|
||||||
|
"guideExecution": "AUSFÜHRUNG",
|
||||||
|
"guideMistakes": "HÄUFIGE FEHLER",
|
||||||
|
"guideAttributes": "BETROFFENE ATTRIBUTE",
|
||||||
|
"guideDiffNovice": "ANFÄNGER",
|
||||||
|
"guideDiffAdept": "FORTGESCHRITTEN",
|
||||||
|
"guideDiffMaster": "MEISTER",
|
||||||
|
|
||||||
"questTabDailies": "TÄGLICH",
|
"questTabDailies": "TÄGLICH",
|
||||||
"questTabJourney": "REISE",
|
"questTabJourney": "REISE",
|
||||||
"questEmptyDailies": "Keine täglichen Quests.\nKomm morgen wieder!",
|
"questEmptyDailies": "Keine täglichen Quests.\nKomm morgen wieder!",
|
||||||
|
|
@ -203,6 +224,32 @@
|
||||||
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
|
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
|
||||||
"confirmButton": "BESTÄTIGEN",
|
"confirmButton": "BESTÄTIGEN",
|
||||||
|
|
||||||
|
"battleTitle": "Kampf",
|
||||||
|
"battleNoExercises": "Keine Übungen konfiguriert",
|
||||||
|
"battleWeekDay": "Woche {week} - Tag {day}",
|
||||||
|
"@battleWeekDay": {
|
||||||
|
"placeholders": {
|
||||||
|
"week": {"type": "int"},
|
||||||
|
"day": {"type": "int"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bodyweight": "Körpergewicht",
|
||||||
|
"battleBossDefeated": "BOSS BESIEGT!",
|
||||||
|
"timerComplete": "Zeit abgelaufen!",
|
||||||
|
"battleRepsPerRound": "{reps} Reps pro Runde",
|
||||||
|
"@battleRepsPerRound": {
|
||||||
|
"placeholders": {
|
||||||
|
"reps": {"type": "int"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"battleWeightKg": "GEWICHT: {weight} kg",
|
||||||
|
"@battleWeightKg": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {"type": "double", "format": "decimalPattern"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"commonConfirm": "BESTÄTIGEN",
|
||||||
|
|
||||||
"guidePullupTitle": "Weighted Pull-Up",
|
"guidePullupTitle": "Weighted Pull-Up",
|
||||||
"guidePullupLore": "Den Körper gegen die Schwerkraft zu ziehen, ist der ultimative Beweis für Oberkörperkraft.",
|
"guidePullupLore": "Den Körper gegen die Schwerkraft zu ziehen, ist der ultimative Beweis für Oberkörperkraft.",
|
||||||
"guidePullupSteps": "Greife die Stange etwas weiter als schulterbreit (Obergriff)|Aktiviere den Core und ziehe die Schulterblätter nach unten/hinten|Ziehe dich hoch, bis das Kinn über der Stange ist|Lasse dich kontrolliert wieder ab",
|
"guidePullupSteps": "Greife die Stange etwas weiter als schulterbreit (Obergriff)|Aktiviere den Core und ziehe die Schulterblätter nach unten/hinten|Ziehe dich hoch, bis das Kinn über der Stange ist|Lasse dich kontrolliert wieder ab",
|
||||||
|
|
@ -384,6 +431,44 @@
|
||||||
"lobbyStatusActive": "Raid startet...",
|
"lobbyStatusActive": "Raid startet...",
|
||||||
"lobbyStatusEntering": "Betrete das Schlachtfeld...",
|
"lobbyStatusEntering": "Betrete das Schlachtfeld...",
|
||||||
|
|
||||||
|
"unknownMember": "Unbekannt",
|
||||||
|
"leaderboardTitle": "HALL OF FAME",
|
||||||
|
"leaderboardHero": "Held #{id}",
|
||||||
|
"@leaderboardHero": {
|
||||||
|
"placeholders": {
|
||||||
|
"id": {"type": "String"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"leaderboardSubtitle": "Stufe {level} • {xp} XP",
|
||||||
|
"@leaderboardSubtitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"level": {"type": "int"},
|
||||||
|
"xp": {"type": "int"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exportEmailSubject": "SLRPG Daten Export",
|
||||||
|
"exportEmailBody": "Deine SLRPG Trainingsdaten (DSGVO Export)",
|
||||||
|
"deleteConfirmationWord": "LÖSCHEN",
|
||||||
|
|
||||||
|
"timerPause": "PAUSE",
|
||||||
|
"timerReset": "RESET",
|
||||||
|
"timerSkip": "SKIP",
|
||||||
|
"timerRestart": "NEUSTART",
|
||||||
|
"genderMale": "Männlich",
|
||||||
|
"genderFemale": "Weiblich",
|
||||||
|
|
||||||
|
"timerIgnite": "STARTEN",
|
||||||
|
"timerResume": "FORTSETZEN",
|
||||||
|
"timerReady": "BEREIT?",
|
||||||
|
"timerPaused": "PAUSIERT",
|
||||||
|
"timerRound": "RUNDE {current} / {total}",
|
||||||
|
"@timerRound": {
|
||||||
|
"placeholders": {
|
||||||
|
"current": {"type": "int"},
|
||||||
|
"total": {"type": "int"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"connectivityError": "Keine Internetverbindung verfügbar.",
|
"connectivityError": "Keine Internetverbindung verfügbar.",
|
||||||
"connectivityMultiplayerError": "Für Multiplayer wird eine Internetverbindung benötigt.",
|
"connectivityMultiplayerError": "Für Multiplayer wird eine Internetverbindung benötigt.",
|
||||||
|
|
||||||
|
|
@ -393,5 +478,60 @@
|
||||||
"errorNotFound": "Daten konnten nicht gefunden werden.",
|
"errorNotFound": "Daten konnten nicht gefunden werden.",
|
||||||
"errorEntryNotUnique": "Dieser Eintrag ist bereits vergeben.",
|
"errorEntryNotUnique": "Dieser Eintrag ist bereits vergeben.",
|
||||||
"errorAuthenticationFailed": "E-Mail oder Passwort falsch.",
|
"errorAuthenticationFailed": "E-Mail oder Passwort falsch.",
|
||||||
"errorIllegalRequest": "Ungültige Anfrage."
|
"errorIllegalRequest": "Ungültige Anfrage.",
|
||||||
|
|
||||||
|
"commonLevel": "Stufe",
|
||||||
|
"commonRequired": "Erforderlich",
|
||||||
|
"commonUpdate": "AKTUALISIEREN",
|
||||||
|
"commonEdit": "BEARBEITEN",
|
||||||
|
"commonLogout": "ABMELDEN",
|
||||||
|
"commonSave": "SPEICHERN",
|
||||||
|
"commonCancel": "ABBRECHEN",
|
||||||
|
"commonConfirm": "BESTÄTIGEN",
|
||||||
|
|
||||||
|
"profileEditTitle": "Profil bearbeiten",
|
||||||
|
"profileEditAppearance": "Aussehen bearbeiten",
|
||||||
|
"profileSelectScenery": "Hintergrund wählen",
|
||||||
|
"profileBodyweightUpdated": "Körpergewicht aktualisiert",
|
||||||
|
"profileChangePassword": "Passwort ändern",
|
||||||
|
"profileOldPassword": "Altes Passwort",
|
||||||
|
"profileNewPassword": "Neues Passwort",
|
||||||
|
"profileConfirmNewPassword": "Bestätigen",
|
||||||
|
"profilePassMismatch": "Stimmt nicht überein",
|
||||||
|
"profilePassMinChars": "Min 8 Zeichen",
|
||||||
|
"profilePassChangedSuccess": "Passwort erfolgreich geändert",
|
||||||
|
"profilePhysicalStats": "Physische Werte",
|
||||||
|
"profileCurrentBodyweight": "Aktuelles Gewicht",
|
||||||
|
"profileTrainingFocus": "Trainingsfokus",
|
||||||
|
"profileAccessoryTemplate": "Assistenz-Template",
|
||||||
|
"profileAccountSecurity": "Kontosicherheit",
|
||||||
|
"profileDangerZone": "Gefahrenzone",
|
||||||
|
"profileResetProgress": "Fortschritt zurücksetzen",
|
||||||
|
"profileResetProgressSubtitle": "Setzt Level, XP und Historie zurück",
|
||||||
|
"profileResetConfirmTitle": "Fortschritt löschen?",
|
||||||
|
"profileResetConfirmBody": "Dies löscht alle Workouts und setzt Level auf 1. Nicht widerruflich.",
|
||||||
|
"profileDeleteAccount": "Account löschen",
|
||||||
|
"profileDeleteAccountSubtitle": "Löscht Account und Daten dauerhaft",
|
||||||
|
"profileDeleteConfirmTitle": "Account löschen?",
|
||||||
|
"profileDeleteConfirmBody": "Bist du sicher? Alle Daten gehen verloren.",
|
||||||
|
|
||||||
|
"templateStrengthOnly": "Nur Stärke",
|
||||||
|
"templateStrengthOnlyDesc": "Hauptübungen + FSL. Pur & Schnell.",
|
||||||
|
"templateHypertrophy": "Hypertrophie Support",
|
||||||
|
"templateHypertrophyDesc": "Bodybuilding-Assistenz für Muskelpanzer.",
|
||||||
|
"templateConditioning": "Der Motor (Ausdauer)",
|
||||||
|
"templateConditioningDesc": "15 min Kettlebell Intervalle.",
|
||||||
|
"templateActiveJourneys": "AKTIVE REISEN",
|
||||||
|
"templatePullupJourney": "Quest: Der erste Klimmzug",
|
||||||
|
"templatePullupJourneyDesc": "Spezifische Progression für das eigene Körpergewicht.",
|
||||||
|
|
||||||
|
"setupFailed": "Setup fehlgeschlagen: {error}",
|
||||||
|
"@setupFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "Object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setupEmailExists": "E-Mail existiert bereits. Bitte einloggen."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
"registerHaveAccount": "Already have an account? ",
|
"registerHaveAccount": "Already have an account? ",
|
||||||
"registerLoginButton": "LOGIN",
|
"registerLoginButton": "LOGIN",
|
||||||
|
|
||||||
|
"hubCycleComplete": "Cycle complete! Finish it in stats.",
|
||||||
"hubNoActiveCycle": "No active cycle",
|
"hubNoActiveCycle": "No active cycle",
|
||||||
"hubCreateCycle": "Create New Cycle",
|
"hubCreateCycle": "Create New Cycle",
|
||||||
"hubCycleLabel": "Cycle",
|
"hubCycleLabel": "Cycle",
|
||||||
|
|
@ -153,6 +154,26 @@
|
||||||
"emomConfirm": "CONFIRM & FINISH",
|
"emomConfirm": "CONFIRM & FINISH",
|
||||||
"emomRepsPerRound": "Reps per Round",
|
"emomRepsPerRound": "Reps per Round",
|
||||||
|
|
||||||
|
"questDailyBounties": "DAILY BOUNTIES",
|
||||||
|
"questViewAll": "VIEW ALL >",
|
||||||
|
"questClaim": "CLAIM",
|
||||||
|
"questRewardCollected": "Reward collected: {xp} XP!",
|
||||||
|
"@questRewardCollected": {
|
||||||
|
"placeholders": {
|
||||||
|
"xp": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"guideNotFound": "No ancient scroll found for this technique.",
|
||||||
|
"guideExecution": "EXECUTION",
|
||||||
|
"guideMistakes": "COMMON MISTAKES",
|
||||||
|
"guideAttributes": "ATTRIBUTES AFFECTED",
|
||||||
|
"guideDiffNovice": "NOVICE",
|
||||||
|
"guideDiffAdept": "ADEPT",
|
||||||
|
"guideDiffMaster": "MASTER",
|
||||||
|
|
||||||
"questTabDailies": "DAILIES",
|
"questTabDailies": "DAILIES",
|
||||||
"questTabJourney": "JOURNEY",
|
"questTabJourney": "JOURNEY",
|
||||||
"questEmptyDailies": "No daily quests available.\nCome back tomorrow!",
|
"questEmptyDailies": "No daily quests available.\nCome back tomorrow!",
|
||||||
|
|
@ -203,6 +224,32 @@
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"confirmButton": "CONFIRM",
|
"confirmButton": "CONFIRM",
|
||||||
|
|
||||||
|
"battleTitle": "Battle",
|
||||||
|
"battleNoExercises": "No exercises configured",
|
||||||
|
"battleWeekDay": "Week {week} - Day {day}",
|
||||||
|
"@battleWeekDay": {
|
||||||
|
"placeholders": {
|
||||||
|
"week": {"type": "int"},
|
||||||
|
"day": {"type": "int"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bodyweight": "Bodyweight",
|
||||||
|
"battleBossDefeated": "BOSS DEFEATED!",
|
||||||
|
"timerComplete": "Time Complete!",
|
||||||
|
"battleRepsPerRound": "{reps} Reps per Round",
|
||||||
|
"@battleRepsPerRound": {
|
||||||
|
"placeholders": {
|
||||||
|
"reps": {"type": "int"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"battleWeightKg": "WEIGHT: {weight} kg",
|
||||||
|
"@battleWeightKg": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {"type": "double", "format": "decimalPattern"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"commonConfirm": "CONFIRM",
|
||||||
|
|
||||||
"guidePullupTitle": "Weighted Pull-Up",
|
"guidePullupTitle": "Weighted Pull-Up",
|
||||||
"guidePullupLore": "Pulling your body against gravity is the ultimate proof of upper body strength.",
|
"guidePullupLore": "Pulling your body against gravity is the ultimate proof of upper body strength.",
|
||||||
"guidePullupSteps": "Grip the bar slightly wider than shoulder width (overhand)|Engage core and pull shoulder blades down/back|Pull yourself up until chin is over the bar|Lower yourself with control",
|
"guidePullupSteps": "Grip the bar slightly wider than shoulder width (overhand)|Engage core and pull shoulder blades down/back|Pull yourself up until chin is over the bar|Lower yourself with control",
|
||||||
|
|
@ -398,6 +445,44 @@
|
||||||
"lobbyStatusActive": "Raid is starting...",
|
"lobbyStatusActive": "Raid is starting...",
|
||||||
"lobbyStatusEntering": "Entering Battle...",
|
"lobbyStatusEntering": "Entering Battle...",
|
||||||
|
|
||||||
|
"unknownMember": "Unknown",
|
||||||
|
"leaderboardTitle": "HALL OF FAME",
|
||||||
|
"leaderboardHero": "Hero #{id}",
|
||||||
|
"@leaderboardHero": {
|
||||||
|
"placeholders": {
|
||||||
|
"id": {"type": "String"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"leaderboardSubtitle": "Level {level} • {xp} XP",
|
||||||
|
"@leaderboardSubtitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"level": {"type": "int"},
|
||||||
|
"xp": {"type": "int"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exportEmailSubject": "SLRPG Data Export",
|
||||||
|
"exportEmailBody": "Your SLRPG training data (GDPR export)",
|
||||||
|
"deleteConfirmationWord": "DELETE",
|
||||||
|
|
||||||
|
"timerPause": "PAUSE",
|
||||||
|
"timerReset": "RESET",
|
||||||
|
"timerSkip": "SKIP",
|
||||||
|
"timerRestart": "RESTART",
|
||||||
|
"genderMale": "Male",
|
||||||
|
"genderFemale": "Female",
|
||||||
|
|
||||||
|
"timerIgnite": "IGNITE ENGINE",
|
||||||
|
"timerResume": "RESUME",
|
||||||
|
"timerReady": "READY?",
|
||||||
|
"timerPaused": "PAUSED",
|
||||||
|
"timerRound": "ROUND {current} / {total}",
|
||||||
|
"@timerRound": {
|
||||||
|
"placeholders": {
|
||||||
|
"current": {"type": "int"},
|
||||||
|
"total": {"type": "int"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"connectivityError": "No internet connection available.",
|
"connectivityError": "No internet connection available.",
|
||||||
"connectivityMultiplayerError": "Active internet connection required for multiplayer.",
|
"connectivityMultiplayerError": "Active internet connection required for multiplayer.",
|
||||||
|
|
||||||
|
|
@ -407,5 +492,60 @@
|
||||||
"errorNotFound": "Data not found.",
|
"errorNotFound": "Data not found.",
|
||||||
"errorEntryNotUnique": "Entry already exists.",
|
"errorEntryNotUnique": "Entry already exists.",
|
||||||
"errorAuthenticationFailed": "E-Mail or Passwort wrong.",
|
"errorAuthenticationFailed": "E-Mail or Passwort wrong.",
|
||||||
"errorIllegalRequest": "Illegal Request."
|
"errorIllegalRequest": "Illegal Request.",
|
||||||
|
|
||||||
|
"commonLevel": "Lvl",
|
||||||
|
"commonRequired": "Required",
|
||||||
|
"commonUpdate": "UPDATE",
|
||||||
|
"commonEdit": "EDIT",
|
||||||
|
"commonLogout": "LOGOUT",
|
||||||
|
"commonSave": "SAVE",
|
||||||
|
"commonCancel": "CANCEL",
|
||||||
|
"commonConfirm": "CONFIRM",
|
||||||
|
|
||||||
|
"profileEditTitle": "Edit Profile",
|
||||||
|
"profileEditAppearance": "Edit Appearance",
|
||||||
|
"profileSelectScenery": "Select Scenery",
|
||||||
|
"profileBodyweightUpdated": "Bodyweight updated",
|
||||||
|
"profileChangePassword": "Change Password",
|
||||||
|
"profileOldPassword": "Old Password",
|
||||||
|
"profileNewPassword": "New Password",
|
||||||
|
"profileConfirmNewPassword": "Confirm New",
|
||||||
|
"profilePassMismatch": "Mismatch",
|
||||||
|
"profilePassMinChars": "Min 8 chars",
|
||||||
|
"profilePassChangedSuccess": "Password changed successfully",
|
||||||
|
"profilePhysicalStats": "Physical Stats",
|
||||||
|
"profileCurrentBodyweight": "Current Bodyweight",
|
||||||
|
"profileTrainingFocus": "Training Focus",
|
||||||
|
"profileAccessoryTemplate": "Accessory Template",
|
||||||
|
"profileAccountSecurity": "Account Security",
|
||||||
|
"profileDangerZone": "Danger Zone",
|
||||||
|
"profileResetProgress": "Reset Progress",
|
||||||
|
"profileResetProgressSubtitle": "Resets Level, XP and Training History",
|
||||||
|
"profileResetConfirmTitle": "Reset Progress?",
|
||||||
|
"profileResetConfirmBody": "This will delete all your workouts and reset your Level to 1. This cannot be undone.",
|
||||||
|
"profileDeleteAccount": "Delete Account",
|
||||||
|
"profileDeleteAccountSubtitle": "Permanently delete your account and data",
|
||||||
|
"profileDeleteConfirmTitle": "Delete Account?",
|
||||||
|
"profileDeleteConfirmBody": "Are you sure you want to delete your account? All data will be lost forever.",
|
||||||
|
|
||||||
|
"templateStrengthOnly": "Strength Only",
|
||||||
|
"templateStrengthOnlyDesc": "Main Lifts + FSL. Pure & Fast.",
|
||||||
|
"templateHypertrophy": "Hypertrophy Support",
|
||||||
|
"templateHypertrophyDesc": "Bodybuilding accessories to build muscle armor.",
|
||||||
|
"templateConditioning": "The Engine (Conditioning)",
|
||||||
|
"templateConditioningDesc": "15 min Kettlebell intervals to boost stamina.",
|
||||||
|
"templateActiveJourneys": "ACTIVE JOURNEYS",
|
||||||
|
"templatePullupJourney": "Quest: The First Pull-Up",
|
||||||
|
"templatePullupJourneyDesc": "Specific progression to master your bodyweight.",
|
||||||
|
|
||||||
|
"setupFailed": "Setup failed: {error}",
|
||||||
|
"@setupFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "Object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setupEmailExists": "Email already exists. Please login or use another email."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,26 +57,27 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_errorMessage = _parseErrorMessage(e.toString());
|
_errorMessage = _parseErrorMessage(e.toString(), l10n);
|
||||||
});
|
});
|
||||||
ErrorHandler.showErrorSnackBar(context, e);
|
ErrorHandler.showErrorSnackBar(context, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _parseErrorMessage(String error) {
|
String _parseErrorMessage(String error, AppLocalizations l10n) {
|
||||||
if (error.contains('400')) {
|
if (error.contains('400')) {
|
||||||
return 'Invalid email or password';
|
return l10n.loginErrorInvalid;
|
||||||
} else if (error.contains('SocketException') ||
|
} else if (error.contains('SocketException') ||
|
||||||
error.contains('Connection refused') ||
|
error.contains('Connection refused') ||
|
||||||
error.contains('Network is unreachable')) {
|
error.contains('Network is unreachable')) {
|
||||||
return 'Could not connect to server.\nPlease check your internet connection.';
|
return l10n.loginErrorConnection;
|
||||||
} else if (error.contains('timeout')) {
|
} else if (error.contains('timeout')) {
|
||||||
return 'Connection timeout.\nPlease try again.';
|
return l10n.loginErrorTimeout;
|
||||||
}
|
}
|
||||||
return 'Login failed. Please try again.';
|
return l10n.loginErrorGeneric;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _hasChanges = false);
|
setState(() => _hasChanges = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Bodyweight updated')),
|
SnackBar(content: Text(AppLocalizations.of(context)!.profileBodyweightUpdated)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -66,6 +66,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showChangePasswordDialog() {
|
void _showChangePasswordDialog() {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final oldPassCtrl = TextEditingController();
|
final oldPassCtrl = TextEditingController();
|
||||||
final newPassCtrl = TextEditingController();
|
final newPassCtrl = TextEditingController();
|
||||||
final confirmPassCtrl = TextEditingController();
|
final confirmPassCtrl = TextEditingController();
|
||||||
|
|
@ -74,7 +75,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('Change Password'),
|
title: Text(l10n.profileChangePassword),
|
||||||
content: Form(
|
content: Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -83,22 +84,22 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: oldPassCtrl,
|
controller: oldPassCtrl,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: const InputDecoration(labelText: 'Old Password'),
|
decoration: InputDecoration(labelText: l10n.profileOldPassword),
|
||||||
validator: (v) => v?.isEmpty == true ? 'Required' : null,
|
validator: (v) => v?.isEmpty == true ? l10n.commonRequired : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: newPassCtrl,
|
controller: newPassCtrl,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: const InputDecoration(labelText: 'New Password'),
|
decoration: InputDecoration(labelText: l10n.profileNewPassword),
|
||||||
validator: (v) => (v?.length ?? 0) < 8 ? 'Min 8 chars' : null,
|
validator: (v) => (v?.length ?? 0) < 8 ? l10n.profilePassMinChars : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: confirmPassCtrl,
|
controller: confirmPassCtrl,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: const InputDecoration(labelText: 'Confirm New'),
|
decoration: InputDecoration(labelText: l10n.profileConfirmNewPassword),
|
||||||
validator: (v) => v != newPassCtrl.text ? 'Mismatch' : null,
|
validator: (v) => v != newPassCtrl.text ? l10n.profilePassMismatch : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -106,7 +107,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('CANCEL'),
|
child: Text(l10n.commonCancel),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|
@ -120,8 +121,8 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
);
|
);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('Password changed successfully')),
|
content: Text(l10n.profilePassChangedSuccess)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -137,7 +138,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('UPDATE'),
|
child: Text(l10n.commonUpdate),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -149,6 +150,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
final currentConfig = _user?.avatarConfig != null
|
final currentConfig = _user?.avatarConfig != null
|
||||||
? AvatarConfig.fromJson(_user!.avatarConfig!)
|
? AvatarConfig.fromJson(_user!.avatarConfig!)
|
||||||
: const AvatarConfig();
|
: const AvatarConfig();
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -158,7 +160,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text('Select Scenery',
|
child: Text(l10n.profileSelectScenery,
|
||||||
style: Theme.of(context).textTheme.titleLarge),
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|
@ -225,7 +227,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
const Icon(Icons.lock, color: Colors.white54),
|
const Icon(Icons.lock, color: Colors.white54),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Lvl ${item.unlockLevel}',
|
'${l10n.commonLevel} ${item.unlockLevel}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -279,6 +281,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
|
|
||||||
void _confirmDangerAction(
|
void _confirmDangerAction(
|
||||||
String title, String content, VoidCallback onConfirm) {
|
String title, String content, VoidCallback onConfirm) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
|
|
@ -287,7 +290,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('CANCEL'),
|
child: Text(l10n.commonCancel),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style:
|
style:
|
||||||
|
|
@ -296,7 +299,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
onConfirm();
|
onConfirm();
|
||||||
},
|
},
|
||||||
child: const Text('CONFIRM'),
|
child: Text(l10n.commonConfirm),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -307,6 +310,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
final currentConfig = _user?.avatarConfig != null
|
final currentConfig = _user?.avatarConfig != null
|
||||||
? AvatarConfig.fromJson(_user!.avatarConfig!)
|
? AvatarConfig.fromJson(_user!.avatarConfig!)
|
||||||
: const AvatarConfig();
|
: const AvatarConfig();
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -315,7 +319,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
backgroundColor: AppTheme.backgroundColor,
|
backgroundColor: AppTheme.backgroundColor,
|
||||||
builder: (context) => Scaffold(
|
builder: (context) => Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Edit Appearance'),
|
title: Text(l10n.profileEditAppearance),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
|
|
@ -325,7 +329,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context, _tempAvatarConfig);
|
Navigator.pop(context, _tempAvatarConfig);
|
||||||
},
|
},
|
||||||
child: const Text('SAVE',
|
child: Text(l10n.commonSave,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppTheme.primaryColor)),
|
color: AppTheme.primaryColor)),
|
||||||
|
|
@ -409,7 +413,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Edit Profile'),
|
title: Text(l10n.profileEditTitle),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => context.go('/hub'),
|
onPressed: () => context.go('/hub'),
|
||||||
|
|
@ -418,7 +422,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
if (_hasChanges)
|
if (_hasChanges)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _isLoading ? null : _saveBodyweight,
|
onPressed: _isLoading ? null : _saveBodyweight,
|
||||||
child: const Text('SAVE',
|
child: Text(l10n.commonSave,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppTheme.primaryColor,
|
color: AppTheme.primaryColor,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold)),
|
||||||
|
|
@ -458,11 +462,11 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: _showBackgroundSelector,
|
onPressed: _showBackgroundSelector,
|
||||||
icon: const Icon(Icons.landscape),
|
icon: const Icon(Icons.landscape),
|
||||||
label: const Text('CHANGE SCENERY'),
|
label: Text(l10n.profileSelectScenery.toUpperCase()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Text('Physical Stats',
|
Text(l10n.profilePhysicalStats,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleLarge
|
.titleLarge
|
||||||
|
|
@ -474,7 +478,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('Current Bodyweight',
|
Text(l10n.profileCurrentBodyweight,
|
||||||
style: Theme.of(context).textTheme.bodyMedium),
|
style: Theme.of(context).textTheme.bodyMedium),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -509,7 +513,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Text('Training Focus',
|
Text(l10n.profileTrainingFocus,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleLarge
|
.titleLarge
|
||||||
|
|
@ -521,7 +525,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('Accessory Template',
|
Text(l10n.profileAccessoryTemplate,
|
||||||
style: Theme.of(context).textTheme.bodyMedium),
|
style: Theme.of(context).textTheme.bodyMedium),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildTemplateSelector(),
|
_buildTemplateSelector(),
|
||||||
|
|
@ -529,7 +533,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('Account Security',
|
Text(l10n.profileAccountSecurity,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleLarge
|
.titleLarge
|
||||||
|
|
@ -537,7 +541,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.lock_outline),
|
leading: const Icon(Icons.lock_outline),
|
||||||
title: const Text('Change Password'),
|
title: Text(l10n.profileChangePassword),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: _showChangePasswordDialog,
|
onTap: _showChangePasswordDialog,
|
||||||
),
|
),
|
||||||
|
|
@ -549,7 +553,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text('Danger Zone',
|
Text(l10n.profileDangerZone,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleLarge
|
.titleLarge
|
||||||
|
|
@ -567,13 +571,13 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.refresh,
|
leading: const Icon(Icons.refresh,
|
||||||
color: AppTheme.errorColor),
|
color: AppTheme.errorColor),
|
||||||
title: const Text('Reset Progress',
|
title: Text(l10n.profileResetProgress,
|
||||||
style: TextStyle(color: AppTheme.errorColor)),
|
style: TextStyle(color: AppTheme.errorColor)),
|
||||||
subtitle: const Text(
|
subtitle: Text(
|
||||||
'Resets Level, XP and Training History'),
|
l10n.profileResetProgressSubtitle),
|
||||||
onTap: () => _confirmDangerAction(
|
onTap: () => _confirmDangerAction(
|
||||||
'Reset Progress?',
|
l10n.profileResetConfirmTitle,
|
||||||
'This will delete all your workouts and reset your Level to 1. This cannot be undone.',
|
l10n.profileResetConfirmBody,
|
||||||
() async {
|
() async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
await userRepo.resetProgress();
|
await userRepo.resetProgress();
|
||||||
|
|
@ -588,13 +592,13 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete_forever,
|
leading: const Icon(Icons.delete_forever,
|
||||||
color: AppTheme.errorColor),
|
color: AppTheme.errorColor),
|
||||||
title: const Text('Delete Account',
|
title: Text(l10n.profileDeleteAccount,
|
||||||
style: TextStyle(color: AppTheme.errorColor)),
|
style: TextStyle(color: AppTheme.errorColor)),
|
||||||
subtitle: const Text(
|
subtitle: Text(
|
||||||
'Permanently delete your account and data'),
|
l10n.profileDeleteAccountSubtitle),
|
||||||
onTap: () => _confirmDangerAction(
|
onTap: () => _confirmDangerAction(
|
||||||
'Delete Account?',
|
l10n.profileDeleteConfirmTitle,
|
||||||
'Are you sure you want to delete your account? All data will be lost forever.',
|
l10n.profileDeleteConfirmBody,
|
||||||
() async {
|
() async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -621,7 +625,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
if (mounted) context.go('/login');
|
if (mounted) context.go('/login');
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.logout),
|
icon: const Icon(Icons.logout),
|
||||||
label: const Text('LOGOUT'),
|
label: Text(l10n.commonLogout),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
),
|
),
|
||||||
|
|
@ -637,6 +641,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
|
|
||||||
Widget _buildTemplateSelector() {
|
Widget _buildTemplateSelector() {
|
||||||
final current = _getTemplateFromSettings(_user?.inventorySettings ?? {});
|
final current = _getTemplateFromSettings(_user?.inventorySettings ?? {});
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -644,31 +649,31 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
_RadioTile<AccessoryTemplate>(
|
_RadioTile<AccessoryTemplate>(
|
||||||
value: AccessoryTemplate.none,
|
value: AccessoryTemplate.none,
|
||||||
groupValue: current,
|
groupValue: current,
|
||||||
title: 'Strength Only',
|
title: l10n.templateStrengthOnly,
|
||||||
subtitle: 'Main Lifts + FSL. Pure & Fast.',
|
subtitle: l10n.templateStrengthOnlyDesc,
|
||||||
onChanged: (val) => _updateTemplate(val!),
|
onChanged: (val) => _updateTemplate(val!),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
_RadioTile<AccessoryTemplate>(
|
_RadioTile<AccessoryTemplate>(
|
||||||
value: AccessoryTemplate.hypertrophy,
|
value: AccessoryTemplate.hypertrophy,
|
||||||
groupValue: current,
|
groupValue: current,
|
||||||
title: 'Hypertrophy Support',
|
title: l10n.templateHypertrophy,
|
||||||
subtitle: 'Bodybuilding accessories to build muscle armor.',
|
subtitle: l10n.templateHypertrophyDesc,
|
||||||
onChanged: (val) => _updateTemplate(val!),
|
onChanged: (val) => _updateTemplate(val!),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
_RadioTile<AccessoryTemplate>(
|
_RadioTile<AccessoryTemplate>(
|
||||||
value: AccessoryTemplate.conditioning,
|
value: AccessoryTemplate.conditioning,
|
||||||
groupValue: current,
|
groupValue: current,
|
||||||
title: 'The Engine (Conditioning)',
|
title: l10n.templateConditioning,
|
||||||
subtitle: '15 min Kettlebell intervals to boost stamina.',
|
subtitle: l10n.templateConditioningDesc,
|
||||||
onChanged: (val) => _updateTemplate(val!),
|
onChanged: (val) => _updateTemplate(val!),
|
||||||
),
|
),
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(16, 24, 16, 8),
|
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'ACTIVE JOURNEYS',
|
l10n.templateActiveJourneys,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -678,8 +683,8 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
_RadioTile<AccessoryTemplate>(
|
_RadioTile<AccessoryTemplate>(
|
||||||
value: AccessoryTemplate.journeyPullup,
|
value: AccessoryTemplate.journeyPullup,
|
||||||
groupValue: current,
|
groupValue: current,
|
||||||
title: 'Quest: The First Pull-Up',
|
title: l10n.templatePullupJourney,
|
||||||
subtitle: 'Specific progression to master your bodyweight.',
|
subtitle: l10n.templatePullupJourneyDesc,
|
||||||
onChanged: (val) => _updateTemplate(val!),
|
onChanged: (val) => _updateTemplate(val!),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
if (!found) {
|
if (!found) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('Cycle complete! Finish it in stats.')),
|
content: Text(AppLocalizations.of(context)!.hubCycleComplete)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -224,8 +224,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (sets >= 20)
|
if (sets >= 20)
|
||||||
const Text('⚠️ HARDCORE MODE',
|
Text(l10n.missionBriefingHardcore,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: AppTheme.errorColor,
|
color: AppTheme.errorColor,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold)),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:slrpg_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
|
|
||||||
class StartRaidButton extends StatefulWidget {
|
class StartRaidButton extends StatefulWidget {
|
||||||
|
|
@ -82,7 +83,7 @@ class _StartRaidButtonState extends State<StartRaidButton>
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
'START RAID',
|
AppLocalizations.of(context)!.lobbyStartRaid,
|
||||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ class QuestLogScreen extends ConsumerWidget {
|
||||||
length: 2,
|
length: 2,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Quest Log'),
|
title: Text(l10n.historyTitle),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => context.go('/hub'),
|
onPressed: () => context.go('/hub'),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:slrpg_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../domain/entities/avatar_config.dart';
|
import '../../domain/entities/avatar_config.dart';
|
||||||
import 'avatar_renderer.dart';
|
import 'avatar_renderer.dart';
|
||||||
|
|
@ -38,6 +39,7 @@ class _AvatarEditorState extends State<AvatarEditor> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
|
|
@ -52,9 +54,9 @@ class _AvatarEditorState extends State<AvatarEditor> {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: SegmentedButton<String>(
|
child: SegmentedButton<String>(
|
||||||
segments: const [
|
segments: [
|
||||||
ButtonSegment(value: 'male', label: Text('Male')),
|
ButtonSegment(value: 'male', label: Text(l10n.genderMale)),
|
||||||
ButtonSegment(value: 'female', label: Text('Female')),
|
ButtonSegment(value: 'female', label: Text(l10n.genderFemale)),
|
||||||
],
|
],
|
||||||
selected: {_gender},
|
selected: {_gender},
|
||||||
onSelectionChanged: (Set<String> newSelection) {
|
onSelectionChanged: (Set<String> newSelection) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:slrpg_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../gamification/data/repositories/quest_repository.dart';
|
import '../../../gamification/data/repositories/quest_repository.dart';
|
||||||
import '../../../../shared/data/local/app_database.dart';
|
import '../../../../shared/data/local/app_database.dart';
|
||||||
|
|
@ -11,6 +12,7 @@ class QuestBoardWidget extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final questRepo = ref.watch(questRepositoryProvider);
|
final questRepo = ref.watch(questRepositoryProvider);
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
return StreamBuilder<List<QuestCollection>>(
|
return StreamBuilder<List<QuestCollection>>(
|
||||||
stream: questRepo.watchQuests(),
|
stream: questRepo.watchQuests(),
|
||||||
|
|
@ -39,7 +41,7 @@ class QuestBoardWidget extends ConsumerWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'DAILY BOUNTIES',
|
l10n.questDailyBounties,
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
color: AppTheme.secondaryColor,
|
color: AppTheme.secondaryColor,
|
||||||
letterSpacing: 1.5,
|
letterSpacing: 1.5,
|
||||||
|
|
@ -47,8 +49,9 @@ class QuestBoardWidget extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => context.go('/quests'),
|
onTap: () => context.go('/quests'),
|
||||||
child: const Text('VIEW ALL >',
|
child: Text(l10n.questViewAll,
|
||||||
style: TextStyle(fontSize: 10, color: Colors.grey)),
|
style:
|
||||||
|
const TextStyle(fontSize: 10, color: Colors.grey)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:slrpg_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../shared/data/local/app_database.dart';
|
import '../../../../shared/data/local/app_database.dart';
|
||||||
import '../../data/repositories/quest_repository.dart';
|
import '../../data/repositories/quest_repository.dart';
|
||||||
|
|
@ -26,13 +27,14 @@ class _QuestItemState extends ConsumerState<QuestItem> {
|
||||||
await questRepo.claimQuest(widget.quest.id);
|
await questRepo.claimQuest(widget.quest.id);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Row(
|
content: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.check_circle, color: AppTheme.successColor),
|
const Icon(Icons.check_circle, color: AppTheme.successColor),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('Reward collected: ${widget.quest.rewardXP} XP!'),
|
Text(l10n.questRewardCollected(widget.quest.rewardXP)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.black87,
|
backgroundColor: Colors.black87,
|
||||||
|
|
@ -164,7 +166,8 @@ class _QuestItemState extends ConsumerState<QuestItem> {
|
||||||
height: 12,
|
height: 12,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2, color: Colors.white))
|
strokeWidth: 2, color: Colors.white))
|
||||||
: const Text('CLAIM', style: TextStyle(fontSize: 12)),
|
: Text(AppLocalizations.of(context)!.questClaim,
|
||||||
|
style: const TextStyle(fontSize: 12)),
|
||||||
)
|
)
|
||||||
else if (widget.quest.rewardItem != null && !isClaimed)
|
else if (widget.quest.rewardItem != null && !isClaimed)
|
||||||
const Icon(Icons.inventory_2,
|
const Icon(Icons.inventory_2,
|
||||||
|
|
|
||||||
|
|
@ -214,8 +214,27 @@ class _ExerciseDetailRow extends StatelessWidget {
|
||||||
|
|
||||||
const _ExerciseDetailRow({required this.exercise});
|
const _ExerciseDetailRow({required this.exercise});
|
||||||
|
|
||||||
|
String _getLocalizedName(BuildContext context, String rawName) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
switch (rawName) {
|
||||||
|
case 'Back Squat':
|
||||||
|
return l10n.exerciseSquat;
|
||||||
|
case 'Weighted Pull-up':
|
||||||
|
return l10n.exercisePullup;
|
||||||
|
case 'Pendlay Row':
|
||||||
|
return l10n.exerciseRow;
|
||||||
|
case 'Weighted Dip':
|
||||||
|
return l10n.exerciseDip;
|
||||||
|
case 'Bench Press':
|
||||||
|
return l10n.exerciseBench;
|
||||||
|
default:
|
||||||
|
return rawName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -230,7 +249,7 @@ class _ExerciseDetailRow extends StatelessWidget {
|
||||||
margin: const EdgeInsets.only(right: 8),
|
margin: const EdgeInsets.only(right: 8),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
exercise.exerciseName,
|
_getLocalizedName(context, exercise.exerciseName),
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
fontWeight: FontWeight.bold, color: AppTheme.textSecondary),
|
fontWeight: FontWeight.bold, color: AppTheme.textSecondary),
|
||||||
),
|
),
|
||||||
|
|
@ -256,7 +275,7 @@ class _ExerciseDetailRow extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${set.targetWeightTotal} kg',
|
'${set.targetWeightTotal} ${l10n.unitKg}',
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Error saving: $e'),
|
content: Text(l10n.inventorySaveError(e.toString())),
|
||||||
backgroundColor: AppTheme.errorColor),
|
backgroundColor: AppTheme.errorColor),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +238,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
||||||
min: 10,
|
min: 10,
|
||||||
max: 25,
|
max: 25,
|
||||||
divisions: 6,
|
divisions: 6,
|
||||||
label: '$_barWeight kg',
|
label: '$_barWeight ${l10n.unitKg}',
|
||||||
activeColor: AppTheme.primaryColor,
|
activeColor: AppTheme.primaryColor,
|
||||||
onChanged: (val) => setState(() {
|
onChanged: (val) => setState(() {
|
||||||
_barWeight = val;
|
_barWeight = val;
|
||||||
|
|
@ -246,7 +246,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('${_barWeight.toStringAsFixed(1)} kg',
|
Text('${_barWeight.toStringAsFixed(1)} ${l10n.unitKg}',
|
||||||
style:
|
style:
|
||||||
const TextStyle(fontWeight: FontWeight.bold)),
|
const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
],
|
],
|
||||||
|
|
@ -398,6 +398,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBandChip(String color, int resistance, bool isSelected) {
|
Widget _buildBandChip(String color, int resistance, bool isSelected) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -430,7 +431,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
||||||
if (isSelected) const SizedBox(width: 4),
|
if (isSelected) const SizedBox(width: 4),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'$color (~${resistance}kg)',
|
'$color (~$resistance${l10n.unitKg})',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? _getBandColor(color)
|
? _getBandColor(color)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:slrpg_app/l10n/app_localizations.dart';
|
||||||
import 'package:slrpg_app/src/features/multiplayer/domain/entities/leaderboard_entry.dart';
|
import 'package:slrpg_app/src/features/multiplayer/domain/entities/leaderboard_entry.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../shared/data/repositories/user_repository.dart';
|
import '../../../../shared/data/repositories/user_repository.dart';
|
||||||
|
|
@ -20,10 +21,11 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final leaderboardAsync = ref.watch(leaderboardProvider);
|
final leaderboardAsync = ref.watch(leaderboardProvider);
|
||||||
final currentUserAsync = ref.watch(userRepositoryProvider).getLocalUser();
|
final currentUserAsync = ref.watch(userRepositoryProvider).getLocalUser();
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('HALL OF FAME'),
|
title: Text(l10n.leaderboardTitle),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => context.go('/hub'),
|
onPressed: () => context.go('/hub'),
|
||||||
|
|
@ -54,7 +56,7 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
|
||||||
leading: _buildRankBadge(entry.rank),
|
leading: _buildRankBadge(entry.rank),
|
||||||
title: Text(
|
title: Text(
|
||||||
entry.name.isEmpty
|
entry.name.isEmpty
|
||||||
? 'Hero #${entry.id.substring(0, 5)}'
|
? l10n.leaderboardHero(entry.id.substring(0, 5))
|
||||||
: entry.name,
|
: entry.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight:
|
fontWeight:
|
||||||
|
|
@ -62,7 +64,8 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
|
||||||
color: isMe ? AppTheme.primaryColor : Colors.white,
|
color: isMe ? AppTheme.primaryColor : Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text('Level ${entry.level} • ${entry.xp} XP'),
|
subtitle: Text(
|
||||||
|
l10n.leaderboardSubtitle(entry.level, entry.xp)),
|
||||||
trailing: _buildAvatarPreview(entry),
|
trailing: _buildAvatarPreview(entry),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -71,7 +74,8 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (err, stack) => Center(child: Text('Error: $err')),
|
error: (err, stack) =>
|
||||||
|
Center(child: Text(l10n.setupFailed(err.toString()))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ class _LobbyScreenState extends ConsumerState<LobbyScreen> {
|
||||||
extra: {'week': next['week'], 'day': next['day']});
|
extra: {'week': next['week'], 'day': next['day']});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return const Center(child: Text('Entering Battle...'));
|
return Center(child: Text(l10n.lobbyStatusEntering));
|
||||||
}
|
}
|
||||||
return Center(child: Text(l10n.lobbyStatusActive));
|
return Center(child: Text(l10n.lobbyStatusActive));
|
||||||
},
|
},
|
||||||
|
|
@ -243,6 +243,7 @@ class _MemberCard extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
|
@ -253,7 +254,7 @@ class _MemberCard extends StatelessWidget {
|
||||||
? AvatarRenderer(config: member.avatar!, size: 40)
|
? AvatarRenderer(config: member.avatar!, size: 40)
|
||||||
: const CircleAvatar(child: Icon(Icons.person)),
|
: const CircleAvatar(child: Icon(Icons.person)),
|
||||||
),
|
),
|
||||||
title: Text(member.username ?? 'Unknown'),
|
title: Text(member.username ?? l10n.unknownMember),
|
||||||
trailing: member.isReady
|
trailing: member.isReady
|
||||||
? const Icon(Icons.check_circle, color: Colors.green)
|
? const Icon(Icons.check_circle, color: Colors.green)
|
||||||
: const Icon(Icons.circle_outlined, color: Colors.grey),
|
: const Icon(Icons.circle_outlined, color: Colors.grey),
|
||||||
|
|
|
||||||
|
|
@ -112,9 +112,10 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Setup failed: $e'),
|
content: Text(l10n.setupFailed(e.toString())),
|
||||||
backgroundColor: AppTheme.errorColor,
|
backgroundColor: AppTheme.errorColor,
|
||||||
duration: const Duration(seconds: 5),
|
duration: const Duration(seconds: 5),
|
||||||
),
|
),
|
||||||
|
|
@ -196,7 +197,7 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
|
||||||
prefixIcon: Icon(Icons.lock),
|
prefixIcon: Icon(Icons.lock),
|
||||||
),
|
),
|
||||||
validator: (v) =>
|
validator: (v) =>
|
||||||
(v?.length ?? 0) < 8 ? 'Min 8 characters' : null,
|
(v?.length ?? 0) < 8 ? l10n.profilePassMinChars : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ class _BodyweightInputScreenState extends ConsumerState<BodyweightInputScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
_useKg ? 'kg' : 'lbs',
|
_useKg ? l10n.unitKg : l10n.unitLbs,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -179,10 +179,11 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
|
||||||
log('Stack trace: $stackTrace');
|
log('Stack trace: $stackTrace');
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
String message = 'Setup failed: ${e.toString()}';
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
String message = l10n.setupFailed(e.toString());
|
||||||
if (e.toString().toLowerCase().contains('unique') ||
|
if (e.toString().toLowerCase().contains('unique') ||
|
||||||
e.toString().toLowerCase().contains('email')) {
|
e.toString().toLowerCase().contains('email')) {
|
||||||
message = 'Email already exists. Please login or use another email.';
|
message = l10n.setupEmailExists;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
@ -261,14 +262,14 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
|
||||||
min: 10,
|
min: 10,
|
||||||
max: 25,
|
max: 25,
|
||||||
divisions: 3,
|
divisions: 3,
|
||||||
label: '${_barWeight.toStringAsFixed(0)} kg',
|
label: '${_barWeight.toStringAsFixed(0)} ${l10n.unitKg}',
|
||||||
activeColor: AppTheme.primaryColor,
|
activeColor: AppTheme.primaryColor,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() => _barWeight = value);
|
setState(() => _barWeight = value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${_barWeight.toStringAsFixed(0)} kg',
|
'${_barWeight.toStringAsFixed(0)} ${l10n.unitKg}',
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
color: AppTheme.primaryColor,
|
color: AppTheme.primaryColor,
|
||||||
),
|
),
|
||||||
|
|
@ -459,7 +460,7 @@ class _InventorySetupScreenState extends ConsumerState<InventorySetupScreen> {
|
||||||
if (isSelected) const SizedBox(width: 4),
|
if (isSelected) const SizedBox(width: 4),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'$color (~${resistance}kg)',
|
'$color (~$resistance${AppLocalizations.of(context)!.unitKg})',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isSelected ? Colors.white : AppTheme.textSecondary,
|
color: isSelected ? Colors.white : AppTheme.textSecondary,
|
||||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
_ExerciseCard(
|
_ExerciseCard(
|
||||||
title: l10n.strengthLegs,
|
title: l10n.strengthLegs,
|
||||||
exerciseName: 'Back Squat',
|
exerciseName: l10n.exerciseSquat,
|
||||||
icon: Icons.accessibility_new,
|
icon: Icons.accessibility_new,
|
||||||
weightController: _squatWeightController,
|
weightController: _squatWeightController,
|
||||||
repsController: _squatRepsController,
|
repsController: _squatRepsController,
|
||||||
|
|
@ -231,8 +231,8 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_AdaptiveExerciseCard(
|
_AdaptiveExerciseCard(
|
||||||
slotTitle: l10n.strengthPull,
|
slotTitle: l10n.strengthPull,
|
||||||
primaryName: 'Weighted Pull-up',
|
primaryName: l10n.exercisePullup,
|
||||||
secondaryName: 'Pendlay Row',
|
secondaryName: l10n.exerciseRow,
|
||||||
icon: Icons.north,
|
icon: Icons.north,
|
||||||
isCapable: _canDoPullup,
|
isCapable: _canDoPullup,
|
||||||
onToggleCapability: (val) {
|
onToggleCapability: (val) {
|
||||||
|
|
@ -254,12 +254,10 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
||||||
repsController: _pullRepsController,
|
repsController: _pullRepsController,
|
||||||
weightLabel: _canDoPullup
|
weightLabel: _canDoPullup
|
||||||
? (_isAssistedPull
|
? (_isAssistedPull
|
||||||
? 'Band Assistance (kg)'
|
? l10n.bandAssistanceLabel
|
||||||
: 'Added Weight (kg)')
|
: l10n.addWeightLabel)
|
||||||
: 'Row Weight (kg)',
|
: l10n.rowWeightLabel,
|
||||||
// weightLabel:
|
repsLabel: _canDoPullup ? l10n.repsLabel : l10n.reps5rmLabel,
|
||||||
// _canDoPullup ? 'Add. Weight (kg)' : 'Row Weight (kg)',
|
|
||||||
repsLabel: _canDoPullup ? 'Reps' : '5RM Reps (usually 5)',
|
|
||||||
showResults: true,
|
showResults: true,
|
||||||
result1RM: _calculated1RMs['pullup'] ?? 0,
|
result1RM: _calculated1RMs['pullup'] ?? 0,
|
||||||
resultTM: _calculatedTMs['pullup'] ?? 0,
|
resultTM: _calculatedTMs['pullup'] ?? 0,
|
||||||
|
|
@ -268,8 +266,8 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_AdaptiveExerciseCard(
|
_AdaptiveExerciseCard(
|
||||||
slotTitle: l10n.strengthPush,
|
slotTitle: l10n.strengthPush,
|
||||||
primaryName: 'Weighted Dip',
|
primaryName: l10n.exerciseDip,
|
||||||
secondaryName: 'Bench Press',
|
secondaryName: l10n.exerciseBench,
|
||||||
icon: Icons.south,
|
icon: Icons.south,
|
||||||
isCapable: _canDoDip,
|
isCapable: _canDoDip,
|
||||||
onToggleCapability: (val) {
|
onToggleCapability: (val) {
|
||||||
|
|
@ -292,11 +290,10 @@ class _StrengthTestScreenState extends ConsumerState<StrengthTestScreen> {
|
||||||
repsController: _pushRepsController,
|
repsController: _pushRepsController,
|
||||||
weightLabel: _canDoDip
|
weightLabel: _canDoDip
|
||||||
? (_isAssistedDip
|
? (_isAssistedDip
|
||||||
? 'Band Assistance (kg)'
|
? l10n.bandAssistanceLabel
|
||||||
: 'Added Weight (kg)')
|
: l10n.addWeightLabel)
|
||||||
: 'Weight (kg)',
|
: l10n.weightLabel,
|
||||||
// weightLabel: _canDoDip ? 'Add. Weight (kg)' : 'Weight (kg)',
|
repsLabel: l10n.repsLabel,
|
||||||
repsLabel: 'Reps',
|
|
||||||
showWeightInput: true,
|
showWeightInput: true,
|
||||||
showResults: true,
|
showResults: true,
|
||||||
result1RM: _calculated1RMs['dip'] ?? 0,
|
result1RM: _calculated1RMs['dip'] ?? 0,
|
||||||
|
|
@ -416,7 +413,7 @@ class _ExerciseCard extends StatelessWidget {
|
||||||
: l10n.weightLabel,
|
: l10n.weightLabel,
|
||||||
isDense: true),
|
isDense: true),
|
||||||
onChanged: (_) => onChanged(),
|
onChanged: (_) => onChanged(),
|
||||||
validator: (v) => v!.isEmpty ? 'Required' : null,
|
validator: (v) => v!.isEmpty ? l10n.commonRequired : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
@ -428,7 +425,7 @@ class _ExerciseCard extends StatelessWidget {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: l10n.repsLabel, isDense: true),
|
labelText: l10n.repsLabel, isDense: true),
|
||||||
onChanged: (_) => onChanged(),
|
onChanged: (_) => onChanged(),
|
||||||
validator: (v) => v!.isEmpty ? 'Required' : null,
|
validator: (v) => v!.isEmpty ? l10n.commonRequired : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -555,7 +552,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
|
||||||
if (!isCapable) ...[
|
if (!isCapable) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Adjusted: ${"Wendler 5/3/1"}',
|
l10n.adjustedWendler,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppTheme.secondaryColor,
|
color: AppTheme.secondaryColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -578,7 +575,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: weightLabel, isDense: true),
|
labelText: weightLabel, isDense: true),
|
||||||
onChanged: (_) => onChanged(),
|
onChanged: (_) => onChanged(),
|
||||||
validator: (v) => v!.isEmpty ? 'Required' : null,
|
validator: (v) => v!.isEmpty ? l10n.commonRequired : null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|
@ -592,7 +589,7 @@ class _AdaptiveExerciseCard extends StatelessWidget {
|
||||||
decoration:
|
decoration:
|
||||||
InputDecoration(labelText: repsLabel, isDense: true),
|
InputDecoration(labelText: repsLabel, isDense: true),
|
||||||
onChanged: (_) => onChanged(),
|
onChanged: (_) => onChanged(),
|
||||||
validator: (v) => v!.isEmpty ? 'Required' : null,
|
validator: (v) => v!.isEmpty ? l10n.commonRequired : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -626,7 +623,7 @@ class _ResultBox extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(l10n.est1rm),
|
Text(l10n.est1rm),
|
||||||
Text('${rm.toStringAsFixed(1)} kg',
|
Text('${rm.toStringAsFixed(1)} ${l10n.unitKg}',
|
||||||
style: Theme.of(context).textTheme.bodyLarge),
|
style: Theme.of(context).textTheme.bodyLarge),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -635,7 +632,7 @@ class _ResultBox extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(l10n.trainingMaxLabel),
|
Text(l10n.trainingMaxLabel),
|
||||||
Text('${tm.toStringAsFixed(1)} kg',
|
Text('${tm.toStringAsFixed(1)} ${l10n.unitKg}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppTheme.primaryColor,
|
color: AppTheme.primaryColor,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold)),
|
||||||
|
|
|
||||||
|
|
@ -176,8 +176,8 @@ class _PrivacyPolicyScreenState extends ConsumerState<PrivacyPolicyScreen> {
|
||||||
|
|
||||||
await Share.shareXFiles(
|
await Share.shareXFiles(
|
||||||
[XFile(file.path)],
|
[XFile(file.path)],
|
||||||
subject: 'SLRPG Data Export',
|
subject: l10n.exportEmailSubject,
|
||||||
text: 'Your SLRPG training data (GDPR export)',
|
text: l10n.exportEmailBody,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -207,7 +207,7 @@ class _PrivacyPolicyScreenState extends ConsumerState<PrivacyPolicyScreen> {
|
||||||
Future<void> _showDeleteAccountDialog() async {
|
Future<void> _showDeleteAccountDialog() async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final confirmationController = TextEditingController();
|
final confirmationController = TextEditingController();
|
||||||
final expectedText = l10n.localeName == 'de' ? 'LÖSCHEN' : 'DELETE';
|
final expectedText = l10n.deleteConfirmationWord;
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
|
||||||
|
|
@ -208,15 +208,15 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
||||||
String getLabel(String id) {
|
String getLabel(String id) {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'squat':
|
case 'squat':
|
||||||
return 'Squat';
|
return l10n.exerciseSquat;
|
||||||
case 'pullup':
|
case 'pullup':
|
||||||
return 'Pull-up';
|
return l10n.exercisePullup;
|
||||||
case 'row':
|
case 'row':
|
||||||
return 'Row';
|
return l10n.exerciseRow;
|
||||||
case 'dip':
|
case 'dip':
|
||||||
return 'Dip';
|
return l10n.exerciseDip;
|
||||||
case 'bench':
|
case 'bench':
|
||||||
return 'Bench';
|
return l10n.exerciseBench;
|
||||||
default:
|
default:
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
@ -358,13 +358,13 @@ class _CurrentCycleCard extends StatelessWidget {
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_StatRow(
|
_StatRow(
|
||||||
label: l10n.exerciseSquat,
|
label: l10n.exerciseSquat,
|
||||||
value: '${tms['squat'].toStringAsFixed(2)} kg'),
|
value: '${tms['squat'].toStringAsFixed(2)} ${l10n.unitKg}'),
|
||||||
_StatRow(
|
_StatRow(
|
||||||
label: getLabel(pullVariant),
|
label: getLabel(pullVariant),
|
||||||
value: '${tms['pullup'].toStringAsFixed(2)} kg'),
|
value: '${tms['pullup'].toStringAsFixed(2)} ${l10n.unitKg}'),
|
||||||
_StatRow(
|
_StatRow(
|
||||||
label: getLabel(pushVariant),
|
label: getLabel(pushVariant),
|
||||||
value: '${tms['dip'].toStringAsFixed(2)} kg'),
|
value: '${tms['dip'].toStringAsFixed(2)} ${l10n.unitKg}'),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|
@ -471,6 +471,7 @@ class _DiffRow extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final diff = newVal - oldVal;
|
final diff = newVal - oldVal;
|
||||||
final isPositive = diff > 0;
|
final isPositive = diff > 0;
|
||||||
|
|
||||||
|
|
@ -491,8 +492,9 @@ class _DiffRow extends StatelessWidget {
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppTheme.successColor, fontWeight: FontWeight.bold))
|
color: AppTheme.successColor, fontWeight: FontWeight.bold))
|
||||||
else
|
else
|
||||||
const Text('STALLED',
|
Text(l10n.statsStalled,
|
||||||
style: TextStyle(color: AppTheme.secondaryColor, fontSize: 12)),
|
style: const TextStyle(
|
||||||
|
color: AppTheme.secondaryColor, fontSize: 12)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ class ExerciseGuideSheet extends StatelessWidget {
|
||||||
if (guide == null) {
|
if (guide == null) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
child: const Text('No ancient scroll found for this technique.',
|
child: Text(l10n.guideNotFound,
|
||||||
textAlign: TextAlign.center, style: TextStyle(color: Colors.grey)),
|
textAlign: TextAlign.center, style: const TextStyle(color: Colors.grey)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ class ExerciseGuideSheet extends StatelessWidget {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildDifficultyBadge(guide.difficulty),
|
_buildDifficultyBadge(context, guide.difficulty),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
@ -80,18 +80,18 @@ class ExerciseGuideSheet extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
_buildSectionTitle(context, 'EXECUTION'),
|
_buildSectionTitle(context, l10n.guideExecution),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
...guide.steps
|
...guide.steps
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
.map((entry) => _buildStep(entry.key + 1, entry.value)),
|
.map((entry) => _buildStep(entry.key + 1, entry.value)),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
_buildSectionTitle(context, 'COMMON MISTAKES'),
|
_buildSectionTitle(context, l10n.guideMistakes),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
...guide.commonMistakes.map((m) => _buildMistake(m)),
|
...guide.commonMistakes.map((m) => _buildMistake(m)),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
_buildSectionTitle(context, 'ATTRIBUTES AFFECTED'),
|
_buildSectionTitle(context, l10n.guideAttributes),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
|
|
@ -118,17 +118,23 @@ class ExerciseGuideSheet extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDifficultyBadge(String diff) {
|
Widget _buildDifficultyBadge(BuildContext context, String diff) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
Color color;
|
Color color;
|
||||||
|
String text = diff;
|
||||||
|
|
||||||
switch (diff) {
|
switch (diff) {
|
||||||
case 'Novice':
|
case 'Novice':
|
||||||
color = Colors.green;
|
color = Colors.green;
|
||||||
|
text = l10n.guideDiffNovice;
|
||||||
break;
|
break;
|
||||||
case 'Adept':
|
case 'Adept':
|
||||||
color = Colors.orange;
|
color = Colors.orange;
|
||||||
|
text = l10n.guideDiffAdept;
|
||||||
break;
|
break;
|
||||||
case 'Master':
|
case 'Master':
|
||||||
color = AppTheme.errorColor;
|
color = AppTheme.errorColor;
|
||||||
|
text = l10n.guideDiffMaster;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
color = Colors.grey;
|
color = Colors.grey;
|
||||||
|
|
@ -143,7 +149,7 @@ class ExerciseGuideSheet extends StatelessWidget {
|
||||||
border: Border.all(color: color.withValues(alpha: 0.5)),
|
border: Border.all(color: color.withValues(alpha: 0.5)),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
diff.toUpperCase(),
|
text.toUpperCase(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: color, fontSize: 12, fontWeight: FontWeight.bold),
|
color: color, fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -502,15 +502,15 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
|
|
||||||
if (state.error != null) {
|
if (state.error != null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Battle')),
|
appBar: AppBar(title: Text(l10n.battleTitle)),
|
||||||
body: Center(child: Text('Error: ${state.error}')),
|
body: Center(child: Text(l10n.setupFailed(state.error.toString()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.exercises.isEmpty) {
|
if (state.exercises.isEmpty) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Battle')),
|
appBar: AppBar(title: Text(l10n.battleTitle)),
|
||||||
body: const Center(child: Text('No exercises configured')),
|
body: Center(child: Text(l10n.battleNoExercises)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -737,7 +737,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
color: AppTheme.primaryColor),
|
color: AppTheme.primaryColor),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
'${currentSet.targetWeightTotal} kg',
|
'${currentSet.targetWeightTotal} ${l10n.unitKg}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
|
|
@ -842,16 +842,16 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
size: 48, color: AppTheme.successColor),
|
size: 48, color: AppTheme.successColor),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Time Complete!',
|
l10n.timerComplete,
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
Text(
|
||||||
'How many reps did you complete?',
|
l10n.amrapResultBody,
|
||||||
style: TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -894,9 +894,9 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
backgroundColor: AppTheme.successColor,
|
backgroundColor: AppTheme.successColor,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'CONFIRM',
|
l10n.commonConfirm,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
|
@ -1165,7 +1165,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
|
|
||||||
AppBar _buildAppBar(AppLocalizations l10n) {
|
AppBar _buildAppBar(AppLocalizations l10n) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
title: Text('Week ${widget.week} - Day ${widget.day}'),
|
title: Text(l10n.battleWeekDay(widget.week, widget.day)),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () => _showAbandonDialog(l10n),
|
onPressed: () => _showAbandonDialog(l10n),
|
||||||
|
|
@ -1322,7 +1322,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'${nextSet.repsTarget} x ${nextSet.targetWeightTotal > 0 ? "${nextSet.targetWeightTotal} kg" : "Bodyweight"}',
|
'${nextSet.repsTarget} x ${nextSet.targetWeightTotal > 0 ? "${nextSet.targetWeightTotal} ${l10n.unitKg}" : l10n.bodyweight}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
|
|
@ -1394,6 +1394,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
int completedHP,
|
int completedHP,
|
||||||
int totalHP,
|
int totalHP,
|
||||||
) {
|
) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final state = ref.watch(battleControllerProvider);
|
final state = ref.watch(battleControllerProvider);
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
|
@ -1435,7 +1436,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${currentSet.repsTarget} Reps per Round',
|
l10n.battleRepsPerRound(currentSet.repsTarget),
|
||||||
style: const TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -1505,7 +1506,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'WEIGHT: ${currentSet.targetWeightTotal} kg',
|
l10n.battleWeightKg(currentSet.targetWeightTotal),
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
color: AppTheme.primaryColor,
|
color: AppTheme.primaryColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:slrpg_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
|
|
||||||
|
|
@ -127,6 +128,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final progress = 1.0 - (_secondsRemaining / widget.intervalSeconds);
|
final progress = 1.0 - (_secondsRemaining / widget.intervalSeconds);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
|
@ -139,7 +141,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
|
||||||
border: Border.all(color: AppTheme.primaryColor),
|
border: Border.all(color: AppTheme.primaryColor),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'ROUND ${widget.currentSet} / ${widget.totalSets}',
|
l10n.timerRound(widget.currentSet, widget.totalSets),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppTheme.primaryColor,
|
color: AppTheme.primaryColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -192,7 +194,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
|
||||||
widget.currentSet == 1 &&
|
widget.currentSet == 1 &&
|
||||||
_secondsRemaining == widget.intervalSeconds)
|
_secondsRemaining == widget.intervalSeconds)
|
||||||
Text(
|
Text(
|
||||||
'READY?',
|
l10n.timerReady,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.labelLarge
|
.labelLarge
|
||||||
|
|
@ -200,7 +202,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
|
||||||
)
|
)
|
||||||
else if (!_isRunning)
|
else if (!_isRunning)
|
||||||
Text(
|
Text(
|
||||||
'PAUSED',
|
l10n.timerPaused,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.labelLarge
|
.labelLarge
|
||||||
|
|
@ -222,8 +224,8 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
|
||||||
icon: const Icon(Icons.play_arrow),
|
icon: const Icon(Icons.play_arrow),
|
||||||
label: Text(widget.currentSet == 1 &&
|
label: Text(widget.currentSet == 1 &&
|
||||||
_secondsRemaining == widget.intervalSeconds
|
_secondsRemaining == widget.intervalSeconds
|
||||||
? 'IGNITE ENGINE'
|
? l10n.timerIgnite
|
||||||
: 'RESUME'),
|
: l10n.timerResume),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppTheme.successColor,
|
backgroundColor: AppTheme.successColor,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
|
@ -237,7 +239,7 @@ class _EmomTimerWidgetState extends State<EmomTimerWidget>
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: _pauseTimer,
|
onPressed: _pauseTimer,
|
||||||
icon: const Icon(Icons.pause),
|
icon: const Icon(Icons.pause),
|
||||||
label: const Text('PAUSE'),
|
label: Text(l10n.timerPause),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: AppTheme.errorColor,
|
foregroundColor: AppTheme.errorColor,
|
||||||
side: const BorderSide(color: AppTheme.errorColor),
|
side: const BorderSide(color: AppTheme.errorColor),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:slrpg_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
|
|
||||||
class TimerWidget extends StatefulWidget {
|
class TimerWidget extends StatefulWidget {
|
||||||
|
|
@ -96,6 +97,7 @@ class _TimerWidgetState extends State<TimerWidget> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final progress = widget.durationSeconds > 0
|
final progress = widget.durationSeconds > 0
|
||||||
? _secondsRemaining / widget.durationSeconds
|
? _secondsRemaining / widget.durationSeconds
|
||||||
: 0.0;
|
: 0.0;
|
||||||
|
|
@ -129,11 +131,11 @@ class _TimerWidgetState extends State<TimerWidget> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_isCompleted)
|
if (_isCompleted)
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
'COMPLETE!',
|
l10n.timerComplete,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: AppTheme.successColor,
|
color: AppTheme.successColor,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -153,7 +155,7 @@ class _TimerWidgetState extends State<TimerWidget> {
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: _isRunning ? _pause : _start,
|
onPressed: _isRunning ? _pause : _start,
|
||||||
icon: Icon(_isRunning ? Icons.pause : Icons.play_arrow),
|
icon: Icon(_isRunning ? Icons.pause : Icons.play_arrow),
|
||||||
label: Text(_isRunning ? 'PAUSE' : 'START'),
|
label: Text(_isRunning ? l10n.timerPause : 'START'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
_isRunning ? Colors.orange : AppTheme.primaryColor,
|
_isRunning ? Colors.orange : AppTheme.primaryColor,
|
||||||
|
|
@ -168,7 +170,7 @@ class _TimerWidgetState extends State<TimerWidget> {
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: _reset,
|
onPressed: _reset,
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
label: const Text('RESET'),
|
label: Text(l10n.timerReset),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: AppTheme.primaryColor,
|
foregroundColor: AppTheme.primaryColor,
|
||||||
side: const BorderSide(color: AppTheme.primaryColor),
|
side: const BorderSide(color: AppTheme.primaryColor),
|
||||||
|
|
@ -182,7 +184,7 @@ class _TimerWidgetState extends State<TimerWidget> {
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: _skip,
|
onPressed: _skip,
|
||||||
icon: const Icon(Icons.skip_next),
|
icon: const Icon(Icons.skip_next),
|
||||||
label: const Text('SKIP'),
|
label: Text(l10n.timerSkip),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: Colors.grey,
|
foregroundColor: Colors.grey,
|
||||||
side: const BorderSide(color: Colors.grey),
|
side: const BorderSide(color: Colors.grey),
|
||||||
|
|
@ -196,7 +198,7 @@ class _TimerWidgetState extends State<TimerWidget> {
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: _reset,
|
onPressed: _reset,
|
||||||
icon: const Icon(Icons.replay),
|
icon: const Icon(Icons.replay),
|
||||||
label: const Text('RESTART'),
|
label: Text(l10n.timerRestart),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppTheme.primaryColor,
|
backgroundColor: AppTheme.primaryColor,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ class WorkoutContent extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
_InfoBox(
|
_InfoBox(
|
||||||
label: l10n.battleWeight,
|
label: l10n.battleWeight,
|
||||||
value: '${currentSet.targetWeightTotal} kg',
|
value: '${currentSet.targetWeightTotal} ${l10n.unitKg}',
|
||||||
),
|
),
|
||||||
_InfoBox(
|
_InfoBox(
|
||||||
label: l10n.battleReps,
|
label: l10n.battleReps,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue