diff --git a/.fvmrc b/.fvmrc
index fc9e690..afd2043 100644
--- a/.fvmrc
+++ b/.fvmrc
@@ -1,3 +1,3 @@
{
- "flutter": "3.44.1"
+ "flutter": "3.44.2"
}
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..f6fd92e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+
+version: 2
+updates:
+ - package-ecosystem: "pub"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2058f2b..a88cf85 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,3 @@
{
- "dart.flutterSdkPath": ".fvm/versions/3.44.1"
+ "dart.flutterSdkPath": ".fvm/versions/3.44.2"
}
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index 55c7681..b55cba1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -25,8 +25,7 @@
- Install dependencies with `fvm flutter pub get` before finalizing code changes.
- Run targeted checks first, then broader ones as needed.
- Useful commands:
-- `fvm flutter test integration_test`
-- `dart format lib test integration_test`
+- `dart format lib test`
- `fvm flutter analyze`
## Repo Notes
diff --git a/ios/ParenW/Info.plist b/ios/ParenW/Info.plist
index f4c7497..2176381 100644
--- a/ios/ParenW/Info.plist
+++ b/ios/ParenW/Info.plist
@@ -5,7 +5,7 @@
CFBundleShortVersionString
1.11.5
CFBundleVersion
- 645
+ 667
NSExtension
NSExtensionPointIdentifier
diff --git a/lib/components/adaptive_overlay.dart b/lib/components/adaptive_overlay.dart
index 85f8dcf..b9ab7a2 100644
--- a/lib/components/adaptive_overlay.dart
+++ b/lib/components/adaptive_overlay.dart
@@ -67,9 +67,13 @@ class AdaptiveSheetLayout extends StatelessWidget {
StupidSimpleSheetRoute adaptiveSheetRoute({
required Widget child,
bool originateAboveBottomViewInset = false,
+ bool barrierDismissible = true,
+ bool draggable = true,
}) {
return StupidSimpleSheetRoute(
originateAboveBottomViewInset: originateAboveBottomViewInset,
+ barrierDismissible: barrierDismissible,
+ draggable: draggable,
child: AdaptiveSheetLayout(child: child),
);
}
diff --git a/lib/components/home_header.dart b/lib/components/home_header.dart
index 65226bc..ecc42cc 100644
--- a/lib/components/home_header.dart
+++ b/lib/components/home_header.dart
@@ -41,7 +41,7 @@ class HomeHeader extends StatelessWidget implements PreferredSizeWidget {
width: 52,
child: isDesktop
? buildInfoIconButton(context, colorScheme)
- : index != 2
+ : index != 3
? buildNavigateIconButtonForward(colorScheme)
: buildInfoIconButton(context, colorScheme),
),
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 6f6b98b..43b597d 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -146,5 +146,36 @@
"toLabel": "Nach",
"more": "Mehr",
"deleteEntryTitle": "Eintrag löschen",
- "deleteEntryContent": "Möchten Sie \"{description}\" wirklich löschen?"
+ "deleteEntryContent": "Möchten Sie \"{description}\" wirklich löschen?",
+ "apply": "Anwenden",
+ "submit": "Absenden",
+ "quiz": "Quiz",
+ "quizSettings": "Quiz-Einstellungen",
+ "quizRoundsPerGame": "Runden pro Spiel: {count}",
+ "quizChoices": "Auswahl",
+ "quizInput": "Eingabe",
+ "quizEstimateInCurrency": "Schätzen in {currency}",
+ "quizYourEstimate": "Deine Schätzung",
+ "quizGoodCalibration": "Gut kalibriert",
+ "quizKeepCalibrating": "Weiter kalibrieren",
+ "quizCorrectAmount": "Richtig: {amount}",
+ "quizYourGuess": "Deine Schätzung: {amount}",
+ "quizYouWereOff": "Du lagst {percent}% daneben",
+ "quizShowSummary": "Zusammenfassung anzeigen",
+ "quizNextRound": "Nächste Runde",
+ "quizSummary": "Quiz-Zusammenfassung",
+ "quizScore": "Punktzahl",
+ "quizBestStreak": "Beste Serie",
+ "quizWithinTenPercent": "Innerhalb von 10%",
+ "quizAverageMiss": "Durchschnittliche Abweichung",
+ "quizSummaryCorrect": "Richtig {amount} · {percent}% daneben",
+ "quizPlayAgain": "Nochmal spielen",
+ "quizScenarioBottleOfWater": "Flasche Wasser",
+ "quizScenarioCoffee": "Kaffee",
+ "quizScenarioStreetSnack": "Snack am Straßenstand",
+ "quizScenarioLunch": "Mittagessen",
+ "quizScenarioTaxiRide": "Taxifahrt",
+ "quizScenarioMuseumTicket": "Museumsticket",
+ "quizScenarioGroceries": "Lebensmittel",
+ "quizScenarioHotelNight": "Hotelnacht"
}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index ffbe12e..028e151 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -149,5 +149,36 @@
"toLabel": "To",
"more": "More",
"deleteEntryTitle": "Delete Entry",
- "deleteEntryContent": "Are you sure you want to delete \"{description}\"?"
+ "deleteEntryContent": "Are you sure you want to delete \"{description}\"?",
+ "apply": "Apply",
+ "submit": "Submit",
+ "quiz": "Quiz",
+ "quizSettings": "Quiz settings",
+ "quizRoundsPerGame": "Rounds per game: {count}",
+ "quizChoices": "Choices",
+ "quizInput": "Input",
+ "quizEstimateInCurrency": "Estimate it in {currency}",
+ "quizYourEstimate": "Your estimate",
+ "quizGoodCalibration": "Good calibration",
+ "quizKeepCalibrating": "Keep calibrating",
+ "quizCorrectAmount": "Correct: {amount}",
+ "quizYourGuess": "Your guess: {amount}",
+ "quizYouWereOff": "You were {percent}% off",
+ "quizShowSummary": "Show summary",
+ "quizNextRound": "Next round",
+ "quizSummary": "Quiz summary",
+ "quizScore": "Score",
+ "quizBestStreak": "Best streak",
+ "quizWithinTenPercent": "Within 10%",
+ "quizAverageMiss": "Average miss",
+ "quizSummaryCorrect": "Correct {amount} · {percent}% off",
+ "quizPlayAgain": "Play again",
+ "quizScenarioBottleOfWater": "Bottle of water",
+ "quizScenarioCoffee": "Coffee",
+ "quizScenarioStreetSnack": "Street snack",
+ "quizScenarioLunch": "Lunch",
+ "quizScenarioTaxiRide": "Taxi ride",
+ "quizScenarioMuseumTicket": "Museum ticket",
+ "quizScenarioGroceries": "Groceries",
+ "quizScenarioHotelNight": "Hotel night"
}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index c2c1d66..91a277e 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -149,5 +149,36 @@
"toLabel": "Vers",
"more": "Plus",
"deleteEntryTitle": "Supprimer l'entrée",
- "deleteEntryContent": "Voulez-vous vraiment supprimer \"{description}\" ?"
+ "deleteEntryContent": "Voulez-vous vraiment supprimer \"{description}\" ?",
+ "apply": "Appliquer",
+ "submit": "Envoyer",
+ "quiz": "Quiz",
+ "quizSettings": "Paramètres du quiz",
+ "quizRoundsPerGame": "Manches par partie : {count}",
+ "quizChoices": "Choix",
+ "quizInput": "Saisie",
+ "quizEstimateInCurrency": "Estimez en {currency}",
+ "quizYourEstimate": "Votre estimation",
+ "quizGoodCalibration": "Bonne calibration",
+ "quizKeepCalibrating": "Continuez à calibrer",
+ "quizCorrectAmount": "Correct : {amount}",
+ "quizYourGuess": "Votre estimation : {amount}",
+ "quizYouWereOff": "Vous aviez {percent} % d'écart",
+ "quizShowSummary": "Afficher le résumé",
+ "quizNextRound": "Manche suivante",
+ "quizSummary": "Résumé du quiz",
+ "quizScore": "Score",
+ "quizBestStreak": "Meilleure série",
+ "quizWithinTenPercent": "À moins de 10 %",
+ "quizAverageMiss": "Écart moyen",
+ "quizSummaryCorrect": "Correct {amount} · écart {percent} %",
+ "quizPlayAgain": "Rejouer",
+ "quizScenarioBottleOfWater": "Bouteille d'eau",
+ "quizScenarioCoffee": "Café",
+ "quizScenarioStreetSnack": "Snack de rue",
+ "quizScenarioLunch": "Déjeuner",
+ "quizScenarioTaxiRide": "Course en taxi",
+ "quizScenarioMuseumTicket": "Billet de musée",
+ "quizScenarioGroceries": "Courses",
+ "quizScenarioHotelNight": "Nuit d'hôtel"
}
diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb
index 15665a1..4f4cf5f 100644
--- a/lib/l10n/app_hi.arb
+++ b/lib/l10n/app_hi.arb
@@ -149,5 +149,36 @@
"toLabel": "में",
"more": "अधिक",
"deleteEntryTitle": "प्रविष्टि हटाएं",
- "deleteEntryContent": "क्या आप वाकई \"{description}\" को हटाना चाहते हैं?"
+ "deleteEntryContent": "क्या आप वाकई \"{description}\" को हटाना चाहते हैं?",
+ "apply": "लागू करें",
+ "submit": "जमा करें",
+ "quiz": "क्विज़",
+ "quizSettings": "क्विज़ सेटिंग्स",
+ "quizRoundsPerGame": "प्रति गेम राउंड: {count}",
+ "quizChoices": "विकल्प",
+ "quizInput": "इनपुट",
+ "quizEstimateInCurrency": "{currency} में अनुमान लगाएं",
+ "quizYourEstimate": "आपका अनुमान",
+ "quizGoodCalibration": "अच्छा अनुमान",
+ "quizKeepCalibrating": "अभ्यास जारी रखें",
+ "quizCorrectAmount": "सही: {amount}",
+ "quizYourGuess": "आपका अनुमान: {amount}",
+ "quizYouWereOff": "आप {percent}% चूके",
+ "quizShowSummary": "सारांश दिखाएं",
+ "quizNextRound": "अगला राउंड",
+ "quizSummary": "क्विज़ सारांश",
+ "quizScore": "स्कोर",
+ "quizBestStreak": "सबसे अच्छी स्ट्रीक",
+ "quizWithinTenPercent": "10% के भीतर",
+ "quizAverageMiss": "औसत चूक",
+ "quizSummaryCorrect": "सही {amount} · {percent}% चूक",
+ "quizPlayAgain": "फिर खेलें",
+ "quizScenarioBottleOfWater": "पानी की बोतल",
+ "quizScenarioCoffee": "कॉफी",
+ "quizScenarioStreetSnack": "स्ट्रीट स्नैक",
+ "quizScenarioLunch": "दोपहर का भोजन",
+ "quizScenarioTaxiRide": "टैक्सी यात्रा",
+ "quizScenarioMuseumTicket": "संग्रहालय टिकट",
+ "quizScenarioGroceries": "किराने का सामान",
+ "quizScenarioHotelNight": "होटल की रात"
}
diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb
index 42facd7..de18123 100644
--- a/lib/l10n/app_id.arb
+++ b/lib/l10n/app_id.arb
@@ -149,5 +149,36 @@
"toLabel": "Ke",
"more": "Lainnya",
"deleteEntryTitle": "Hapus Entri",
- "deleteEntryContent": "Apakah Anda yakin ingin menghapus \"{description}\"?"
+ "deleteEntryContent": "Apakah Anda yakin ingin menghapus \"{description}\"?",
+ "apply": "Terapkan",
+ "submit": "Kirim",
+ "quiz": "Kuis",
+ "quizSettings": "Pengaturan kuis",
+ "quizRoundsPerGame": "Ronde per game: {count}",
+ "quizChoices": "Pilihan",
+ "quizInput": "Input",
+ "quizEstimateInCurrency": "Perkirakan dalam {currency}",
+ "quizYourEstimate": "Perkiraanmu",
+ "quizGoodCalibration": "Kalibrasi bagus",
+ "quizKeepCalibrating": "Terus kalibrasi",
+ "quizCorrectAmount": "Benar: {amount}",
+ "quizYourGuess": "Tebakanmu: {amount}",
+ "quizYouWereOff": "Kamu meleset {percent}%",
+ "quizShowSummary": "Tampilkan ringkasan",
+ "quizNextRound": "Ronde berikutnya",
+ "quizSummary": "Ringkasan kuis",
+ "quizScore": "Skor",
+ "quizBestStreak": "Rentetan terbaik",
+ "quizWithinTenPercent": "Dalam 10%",
+ "quizAverageMiss": "Rata-rata meleset",
+ "quizSummaryCorrect": "Benar {amount} · meleset {percent}%",
+ "quizPlayAgain": "Main lagi",
+ "quizScenarioBottleOfWater": "Sebotol air",
+ "quizScenarioCoffee": "Kopi",
+ "quizScenarioStreetSnack": "Camilan kaki lima",
+ "quizScenarioLunch": "Makan siang",
+ "quizScenarioTaxiRide": "Naik taksi",
+ "quizScenarioMuseumTicket": "Tiket museum",
+ "quizScenarioGroceries": "Belanja bahan makanan",
+ "quizScenarioHotelNight": "Semalam di hotel"
}
diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb
index 27dff22..11995d4 100644
--- a/lib/l10n/app_ja.arb
+++ b/lib/l10n/app_ja.arb
@@ -146,5 +146,36 @@
"toLabel": "変換先",
"more": "その他",
"deleteEntryTitle": "エントリを削除",
- "deleteEntryContent": "「{description}」を本当に削除しますか?"
+ "deleteEntryContent": "「{description}」を本当に削除しますか?",
+ "apply": "適用",
+ "submit": "送信",
+ "quiz": "クイズ",
+ "quizSettings": "クイズ設定",
+ "quizRoundsPerGame": "1ゲームのラウンド数: {count}",
+ "quizChoices": "選択肢",
+ "quizInput": "入力",
+ "quizEstimateInCurrency": "{currency}で見積もる",
+ "quizYourEstimate": "あなたの見積もり",
+ "quizGoodCalibration": "感覚は良好",
+ "quizKeepCalibrating": "もう少し調整",
+ "quizCorrectAmount": "正解: {amount}",
+ "quizYourGuess": "あなたの予想: {amount}",
+ "quizYouWereOff": "{percent}%ずれていました",
+ "quizShowSummary": "結果を表示",
+ "quizNextRound": "次のラウンド",
+ "quizSummary": "クイズ結果",
+ "quizScore": "スコア",
+ "quizBestStreak": "最高連続正解",
+ "quizWithinTenPercent": "10%以内",
+ "quizAverageMiss": "平均誤差",
+ "quizSummaryCorrect": "正解 {amount} · {percent}%ずれ",
+ "quizPlayAgain": "もう一度",
+ "quizScenarioBottleOfWater": "水のボトル",
+ "quizScenarioCoffee": "コーヒー",
+ "quizScenarioStreetSnack": "屋台の軽食",
+ "quizScenarioLunch": "ランチ",
+ "quizScenarioTaxiRide": "タクシー移動",
+ "quizScenarioMuseumTicket": "美術館チケット",
+ "quizScenarioGroceries": "食料品",
+ "quizScenarioHotelNight": "ホテル1泊"
}
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index e43be7d..4be0630 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -1001,6 +1001,192 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Are you sure you want to delete \"{description}\"?'**
String deleteEntryContent(Object description);
+
+ /// No description provided for @apply.
+ ///
+ /// In en, this message translates to:
+ /// **'Apply'**
+ String get apply;
+
+ /// No description provided for @submit.
+ ///
+ /// In en, this message translates to:
+ /// **'Submit'**
+ String get submit;
+
+ /// No description provided for @quiz.
+ ///
+ /// In en, this message translates to:
+ /// **'Quiz'**
+ String get quiz;
+
+ /// No description provided for @quizSettings.
+ ///
+ /// In en, this message translates to:
+ /// **'Quiz settings'**
+ String get quizSettings;
+
+ /// No description provided for @quizRoundsPerGame.
+ ///
+ /// In en, this message translates to:
+ /// **'Rounds per game: {count}'**
+ String quizRoundsPerGame(Object count);
+
+ /// No description provided for @quizChoices.
+ ///
+ /// In en, this message translates to:
+ /// **'Choices'**
+ String get quizChoices;
+
+ /// No description provided for @quizInput.
+ ///
+ /// In en, this message translates to:
+ /// **'Input'**
+ String get quizInput;
+
+ /// No description provided for @quizEstimateInCurrency.
+ ///
+ /// In en, this message translates to:
+ /// **'Estimate it in {currency}'**
+ String quizEstimateInCurrency(Object currency);
+
+ /// No description provided for @quizYourEstimate.
+ ///
+ /// In en, this message translates to:
+ /// **'Your estimate'**
+ String get quizYourEstimate;
+
+ /// No description provided for @quizGoodCalibration.
+ ///
+ /// In en, this message translates to:
+ /// **'Good calibration'**
+ String get quizGoodCalibration;
+
+ /// No description provided for @quizKeepCalibrating.
+ ///
+ /// In en, this message translates to:
+ /// **'Keep calibrating'**
+ String get quizKeepCalibrating;
+
+ /// No description provided for @quizCorrectAmount.
+ ///
+ /// In en, this message translates to:
+ /// **'Correct: {amount}'**
+ String quizCorrectAmount(Object amount);
+
+ /// No description provided for @quizYourGuess.
+ ///
+ /// In en, this message translates to:
+ /// **'Your guess: {amount}'**
+ String quizYourGuess(Object amount);
+
+ /// No description provided for @quizYouWereOff.
+ ///
+ /// In en, this message translates to:
+ /// **'You were {percent}% off'**
+ String quizYouWereOff(Object percent);
+
+ /// No description provided for @quizShowSummary.
+ ///
+ /// In en, this message translates to:
+ /// **'Show summary'**
+ String get quizShowSummary;
+
+ /// No description provided for @quizNextRound.
+ ///
+ /// In en, this message translates to:
+ /// **'Next round'**
+ String get quizNextRound;
+
+ /// No description provided for @quizSummary.
+ ///
+ /// In en, this message translates to:
+ /// **'Quiz summary'**
+ String get quizSummary;
+
+ /// No description provided for @quizScore.
+ ///
+ /// In en, this message translates to:
+ /// **'Score'**
+ String get quizScore;
+
+ /// No description provided for @quizBestStreak.
+ ///
+ /// In en, this message translates to:
+ /// **'Best streak'**
+ String get quizBestStreak;
+
+ /// No description provided for @quizWithinTenPercent.
+ ///
+ /// In en, this message translates to:
+ /// **'Within 10%'**
+ String get quizWithinTenPercent;
+
+ /// No description provided for @quizAverageMiss.
+ ///
+ /// In en, this message translates to:
+ /// **'Average miss'**
+ String get quizAverageMiss;
+
+ /// No description provided for @quizSummaryCorrect.
+ ///
+ /// In en, this message translates to:
+ /// **'Correct {amount} · {percent}% off'**
+ String quizSummaryCorrect(Object amount, Object percent);
+
+ /// No description provided for @quizPlayAgain.
+ ///
+ /// In en, this message translates to:
+ /// **'Play again'**
+ String get quizPlayAgain;
+
+ /// No description provided for @quizScenarioBottleOfWater.
+ ///
+ /// In en, this message translates to:
+ /// **'Bottle of water'**
+ String get quizScenarioBottleOfWater;
+
+ /// No description provided for @quizScenarioCoffee.
+ ///
+ /// In en, this message translates to:
+ /// **'Coffee'**
+ String get quizScenarioCoffee;
+
+ /// No description provided for @quizScenarioStreetSnack.
+ ///
+ /// In en, this message translates to:
+ /// **'Street snack'**
+ String get quizScenarioStreetSnack;
+
+ /// No description provided for @quizScenarioLunch.
+ ///
+ /// In en, this message translates to:
+ /// **'Lunch'**
+ String get quizScenarioLunch;
+
+ /// No description provided for @quizScenarioTaxiRide.
+ ///
+ /// In en, this message translates to:
+ /// **'Taxi ride'**
+ String get quizScenarioTaxiRide;
+
+ /// No description provided for @quizScenarioMuseumTicket.
+ ///
+ /// In en, this message translates to:
+ /// **'Museum ticket'**
+ String get quizScenarioMuseumTicket;
+
+ /// No description provided for @quizScenarioGroceries.
+ ///
+ /// In en, this message translates to:
+ /// **'Groceries'**
+ String get quizScenarioGroceries;
+
+ /// No description provided for @quizScenarioHotelNight.
+ ///
+ /// In en, this message translates to:
+ /// **'Hotel night'**
+ String get quizScenarioHotelNight;
}
class _AppLocalizationsDelegate
diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart
index 7935eb4..78ee0be 100644
--- a/lib/l10n/app_localizations_de.dart
+++ b/lib/l10n/app_localizations_de.dart
@@ -513,4 +513,109 @@ class AppLocalizationsDe extends AppLocalizations {
String deleteEntryContent(Object description) {
return 'Möchten Sie \"$description\" wirklich löschen?';
}
+
+ @override
+ String get apply => 'Anwenden';
+
+ @override
+ String get submit => 'Absenden';
+
+ @override
+ String get quiz => 'Quiz';
+
+ @override
+ String get quizSettings => 'Quiz-Einstellungen';
+
+ @override
+ String quizRoundsPerGame(Object count) {
+ return 'Runden pro Spiel: $count';
+ }
+
+ @override
+ String get quizChoices => 'Auswahl';
+
+ @override
+ String get quizInput => 'Eingabe';
+
+ @override
+ String quizEstimateInCurrency(Object currency) {
+ return 'Schätzen in $currency';
+ }
+
+ @override
+ String get quizYourEstimate => 'Deine Schätzung';
+
+ @override
+ String get quizGoodCalibration => 'Gut kalibriert';
+
+ @override
+ String get quizKeepCalibrating => 'Weiter kalibrieren';
+
+ @override
+ String quizCorrectAmount(Object amount) {
+ return 'Richtig: $amount';
+ }
+
+ @override
+ String quizYourGuess(Object amount) {
+ return 'Deine Schätzung: $amount';
+ }
+
+ @override
+ String quizYouWereOff(Object percent) {
+ return 'Du lagst $percent% daneben';
+ }
+
+ @override
+ String get quizShowSummary => 'Zusammenfassung anzeigen';
+
+ @override
+ String get quizNextRound => 'Nächste Runde';
+
+ @override
+ String get quizSummary => 'Quiz-Zusammenfassung';
+
+ @override
+ String get quizScore => 'Punktzahl';
+
+ @override
+ String get quizBestStreak => 'Beste Serie';
+
+ @override
+ String get quizWithinTenPercent => 'Innerhalb von 10%';
+
+ @override
+ String get quizAverageMiss => 'Durchschnittliche Abweichung';
+
+ @override
+ String quizSummaryCorrect(Object amount, Object percent) {
+ return 'Richtig $amount · $percent% daneben';
+ }
+
+ @override
+ String get quizPlayAgain => 'Nochmal spielen';
+
+ @override
+ String get quizScenarioBottleOfWater => 'Flasche Wasser';
+
+ @override
+ String get quizScenarioCoffee => 'Kaffee';
+
+ @override
+ String get quizScenarioStreetSnack => 'Snack am Straßenstand';
+
+ @override
+ String get quizScenarioLunch => 'Mittagessen';
+
+ @override
+ String get quizScenarioTaxiRide => 'Taxifahrt';
+
+ @override
+ String get quizScenarioMuseumTicket => 'Museumsticket';
+
+ @override
+ String get quizScenarioGroceries => 'Lebensmittel';
+
+ @override
+ String get quizScenarioHotelNight => 'Hotelnacht';
}
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 281a093..d6bcfa8 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -513,4 +513,109 @@ class AppLocalizationsEn extends AppLocalizations {
String deleteEntryContent(Object description) {
return 'Are you sure you want to delete \"$description\"?';
}
+
+ @override
+ String get apply => 'Apply';
+
+ @override
+ String get submit => 'Submit';
+
+ @override
+ String get quiz => 'Quiz';
+
+ @override
+ String get quizSettings => 'Quiz settings';
+
+ @override
+ String quizRoundsPerGame(Object count) {
+ return 'Rounds per game: $count';
+ }
+
+ @override
+ String get quizChoices => 'Choices';
+
+ @override
+ String get quizInput => 'Input';
+
+ @override
+ String quizEstimateInCurrency(Object currency) {
+ return 'Estimate it in $currency';
+ }
+
+ @override
+ String get quizYourEstimate => 'Your estimate';
+
+ @override
+ String get quizGoodCalibration => 'Good calibration';
+
+ @override
+ String get quizKeepCalibrating => 'Keep calibrating';
+
+ @override
+ String quizCorrectAmount(Object amount) {
+ return 'Correct: $amount';
+ }
+
+ @override
+ String quizYourGuess(Object amount) {
+ return 'Your guess: $amount';
+ }
+
+ @override
+ String quizYouWereOff(Object percent) {
+ return 'You were $percent% off';
+ }
+
+ @override
+ String get quizShowSummary => 'Show summary';
+
+ @override
+ String get quizNextRound => 'Next round';
+
+ @override
+ String get quizSummary => 'Quiz summary';
+
+ @override
+ String get quizScore => 'Score';
+
+ @override
+ String get quizBestStreak => 'Best streak';
+
+ @override
+ String get quizWithinTenPercent => 'Within 10%';
+
+ @override
+ String get quizAverageMiss => 'Average miss';
+
+ @override
+ String quizSummaryCorrect(Object amount, Object percent) {
+ return 'Correct $amount · $percent% off';
+ }
+
+ @override
+ String get quizPlayAgain => 'Play again';
+
+ @override
+ String get quizScenarioBottleOfWater => 'Bottle of water';
+
+ @override
+ String get quizScenarioCoffee => 'Coffee';
+
+ @override
+ String get quizScenarioStreetSnack => 'Street snack';
+
+ @override
+ String get quizScenarioLunch => 'Lunch';
+
+ @override
+ String get quizScenarioTaxiRide => 'Taxi ride';
+
+ @override
+ String get quizScenarioMuseumTicket => 'Museum ticket';
+
+ @override
+ String get quizScenarioGroceries => 'Groceries';
+
+ @override
+ String get quizScenarioHotelNight => 'Hotel night';
}
diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart
index 9cc6d91..b4cdfa8 100644
--- a/lib/l10n/app_localizations_fr.dart
+++ b/lib/l10n/app_localizations_fr.dart
@@ -514,4 +514,109 @@ class AppLocalizationsFr extends AppLocalizations {
String deleteEntryContent(Object description) {
return 'Voulez-vous vraiment supprimer \"$description\" ?';
}
+
+ @override
+ String get apply => 'Appliquer';
+
+ @override
+ String get submit => 'Envoyer';
+
+ @override
+ String get quiz => 'Quiz';
+
+ @override
+ String get quizSettings => 'Paramètres du quiz';
+
+ @override
+ String quizRoundsPerGame(Object count) {
+ return 'Manches par partie : $count';
+ }
+
+ @override
+ String get quizChoices => 'Choix';
+
+ @override
+ String get quizInput => 'Saisie';
+
+ @override
+ String quizEstimateInCurrency(Object currency) {
+ return 'Estimez en $currency';
+ }
+
+ @override
+ String get quizYourEstimate => 'Votre estimation';
+
+ @override
+ String get quizGoodCalibration => 'Bonne calibration';
+
+ @override
+ String get quizKeepCalibrating => 'Continuez à calibrer';
+
+ @override
+ String quizCorrectAmount(Object amount) {
+ return 'Correct : $amount';
+ }
+
+ @override
+ String quizYourGuess(Object amount) {
+ return 'Votre estimation : $amount';
+ }
+
+ @override
+ String quizYouWereOff(Object percent) {
+ return 'Vous aviez $percent % d\'écart';
+ }
+
+ @override
+ String get quizShowSummary => 'Afficher le résumé';
+
+ @override
+ String get quizNextRound => 'Manche suivante';
+
+ @override
+ String get quizSummary => 'Résumé du quiz';
+
+ @override
+ String get quizScore => 'Score';
+
+ @override
+ String get quizBestStreak => 'Meilleure série';
+
+ @override
+ String get quizWithinTenPercent => 'À moins de 10 %';
+
+ @override
+ String get quizAverageMiss => 'Écart moyen';
+
+ @override
+ String quizSummaryCorrect(Object amount, Object percent) {
+ return 'Correct $amount · écart $percent %';
+ }
+
+ @override
+ String get quizPlayAgain => 'Rejouer';
+
+ @override
+ String get quizScenarioBottleOfWater => 'Bouteille d\'eau';
+
+ @override
+ String get quizScenarioCoffee => 'Café';
+
+ @override
+ String get quizScenarioStreetSnack => 'Snack de rue';
+
+ @override
+ String get quizScenarioLunch => 'Déjeuner';
+
+ @override
+ String get quizScenarioTaxiRide => 'Course en taxi';
+
+ @override
+ String get quizScenarioMuseumTicket => 'Billet de musée';
+
+ @override
+ String get quizScenarioGroceries => 'Courses';
+
+ @override
+ String get quizScenarioHotelNight => 'Nuit d\'hôtel';
}
diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart
index c1ba33b..65a2c3a 100644
--- a/lib/l10n/app_localizations_hi.dart
+++ b/lib/l10n/app_localizations_hi.dart
@@ -513,4 +513,109 @@ class AppLocalizationsHi extends AppLocalizations {
String deleteEntryContent(Object description) {
return 'क्या आप वाकई \"$description\" को हटाना चाहते हैं?';
}
+
+ @override
+ String get apply => 'लागू करें';
+
+ @override
+ String get submit => 'जमा करें';
+
+ @override
+ String get quiz => 'क्विज़';
+
+ @override
+ String get quizSettings => 'क्विज़ सेटिंग्स';
+
+ @override
+ String quizRoundsPerGame(Object count) {
+ return 'प्रति गेम राउंड: $count';
+ }
+
+ @override
+ String get quizChoices => 'विकल्प';
+
+ @override
+ String get quizInput => 'इनपुट';
+
+ @override
+ String quizEstimateInCurrency(Object currency) {
+ return '$currency में अनुमान लगाएं';
+ }
+
+ @override
+ String get quizYourEstimate => 'आपका अनुमान';
+
+ @override
+ String get quizGoodCalibration => 'अच्छा अनुमान';
+
+ @override
+ String get quizKeepCalibrating => 'अभ्यास जारी रखें';
+
+ @override
+ String quizCorrectAmount(Object amount) {
+ return 'सही: $amount';
+ }
+
+ @override
+ String quizYourGuess(Object amount) {
+ return 'आपका अनुमान: $amount';
+ }
+
+ @override
+ String quizYouWereOff(Object percent) {
+ return 'आप $percent% चूके';
+ }
+
+ @override
+ String get quizShowSummary => 'सारांश दिखाएं';
+
+ @override
+ String get quizNextRound => 'अगला राउंड';
+
+ @override
+ String get quizSummary => 'क्विज़ सारांश';
+
+ @override
+ String get quizScore => 'स्कोर';
+
+ @override
+ String get quizBestStreak => 'सबसे अच्छी स्ट्रीक';
+
+ @override
+ String get quizWithinTenPercent => '10% के भीतर';
+
+ @override
+ String get quizAverageMiss => 'औसत चूक';
+
+ @override
+ String quizSummaryCorrect(Object amount, Object percent) {
+ return 'सही $amount · $percent% चूक';
+ }
+
+ @override
+ String get quizPlayAgain => 'फिर खेलें';
+
+ @override
+ String get quizScenarioBottleOfWater => 'पानी की बोतल';
+
+ @override
+ String get quizScenarioCoffee => 'कॉफी';
+
+ @override
+ String get quizScenarioStreetSnack => 'स्ट्रीट स्नैक';
+
+ @override
+ String get quizScenarioLunch => 'दोपहर का भोजन';
+
+ @override
+ String get quizScenarioTaxiRide => 'टैक्सी यात्रा';
+
+ @override
+ String get quizScenarioMuseumTicket => 'संग्रहालय टिकट';
+
+ @override
+ String get quizScenarioGroceries => 'किराने का सामान';
+
+ @override
+ String get quizScenarioHotelNight => 'होटल की रात';
}
diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart
index 70c0ed5..45f92bb 100644
--- a/lib/l10n/app_localizations_id.dart
+++ b/lib/l10n/app_localizations_id.dart
@@ -513,4 +513,109 @@ class AppLocalizationsId extends AppLocalizations {
String deleteEntryContent(Object description) {
return 'Apakah Anda yakin ingin menghapus \"$description\"?';
}
+
+ @override
+ String get apply => 'Terapkan';
+
+ @override
+ String get submit => 'Kirim';
+
+ @override
+ String get quiz => 'Kuis';
+
+ @override
+ String get quizSettings => 'Pengaturan kuis';
+
+ @override
+ String quizRoundsPerGame(Object count) {
+ return 'Ronde per game: $count';
+ }
+
+ @override
+ String get quizChoices => 'Pilihan';
+
+ @override
+ String get quizInput => 'Input';
+
+ @override
+ String quizEstimateInCurrency(Object currency) {
+ return 'Perkirakan dalam $currency';
+ }
+
+ @override
+ String get quizYourEstimate => 'Perkiraanmu';
+
+ @override
+ String get quizGoodCalibration => 'Kalibrasi bagus';
+
+ @override
+ String get quizKeepCalibrating => 'Terus kalibrasi';
+
+ @override
+ String quizCorrectAmount(Object amount) {
+ return 'Benar: $amount';
+ }
+
+ @override
+ String quizYourGuess(Object amount) {
+ return 'Tebakanmu: $amount';
+ }
+
+ @override
+ String quizYouWereOff(Object percent) {
+ return 'Kamu meleset $percent%';
+ }
+
+ @override
+ String get quizShowSummary => 'Tampilkan ringkasan';
+
+ @override
+ String get quizNextRound => 'Ronde berikutnya';
+
+ @override
+ String get quizSummary => 'Ringkasan kuis';
+
+ @override
+ String get quizScore => 'Skor';
+
+ @override
+ String get quizBestStreak => 'Rentetan terbaik';
+
+ @override
+ String get quizWithinTenPercent => 'Dalam 10%';
+
+ @override
+ String get quizAverageMiss => 'Rata-rata meleset';
+
+ @override
+ String quizSummaryCorrect(Object amount, Object percent) {
+ return 'Benar $amount · meleset $percent%';
+ }
+
+ @override
+ String get quizPlayAgain => 'Main lagi';
+
+ @override
+ String get quizScenarioBottleOfWater => 'Sebotol air';
+
+ @override
+ String get quizScenarioCoffee => 'Kopi';
+
+ @override
+ String get quizScenarioStreetSnack => 'Camilan kaki lima';
+
+ @override
+ String get quizScenarioLunch => 'Makan siang';
+
+ @override
+ String get quizScenarioTaxiRide => 'Naik taksi';
+
+ @override
+ String get quizScenarioMuseumTicket => 'Tiket museum';
+
+ @override
+ String get quizScenarioGroceries => 'Belanja bahan makanan';
+
+ @override
+ String get quizScenarioHotelNight => 'Semalam di hotel';
}
diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart
index 99867c3..8ff00ef 100644
--- a/lib/l10n/app_localizations_ja.dart
+++ b/lib/l10n/app_localizations_ja.dart
@@ -508,4 +508,109 @@ class AppLocalizationsJa extends AppLocalizations {
String deleteEntryContent(Object description) {
return '「$description」を本当に削除しますか?';
}
+
+ @override
+ String get apply => '適用';
+
+ @override
+ String get submit => '送信';
+
+ @override
+ String get quiz => 'クイズ';
+
+ @override
+ String get quizSettings => 'クイズ設定';
+
+ @override
+ String quizRoundsPerGame(Object count) {
+ return '1ゲームのラウンド数: $count';
+ }
+
+ @override
+ String get quizChoices => '選択肢';
+
+ @override
+ String get quizInput => '入力';
+
+ @override
+ String quizEstimateInCurrency(Object currency) {
+ return '$currencyで見積もる';
+ }
+
+ @override
+ String get quizYourEstimate => 'あなたの見積もり';
+
+ @override
+ String get quizGoodCalibration => '感覚は良好';
+
+ @override
+ String get quizKeepCalibrating => 'もう少し調整';
+
+ @override
+ String quizCorrectAmount(Object amount) {
+ return '正解: $amount';
+ }
+
+ @override
+ String quizYourGuess(Object amount) {
+ return 'あなたの予想: $amount';
+ }
+
+ @override
+ String quizYouWereOff(Object percent) {
+ return '$percent%ずれていました';
+ }
+
+ @override
+ String get quizShowSummary => '結果を表示';
+
+ @override
+ String get quizNextRound => '次のラウンド';
+
+ @override
+ String get quizSummary => 'クイズ結果';
+
+ @override
+ String get quizScore => 'スコア';
+
+ @override
+ String get quizBestStreak => '最高連続正解';
+
+ @override
+ String get quizWithinTenPercent => '10%以内';
+
+ @override
+ String get quizAverageMiss => '平均誤差';
+
+ @override
+ String quizSummaryCorrect(Object amount, Object percent) {
+ return '正解 $amount · $percent%ずれ';
+ }
+
+ @override
+ String get quizPlayAgain => 'もう一度';
+
+ @override
+ String get quizScenarioBottleOfWater => '水のボトル';
+
+ @override
+ String get quizScenarioCoffee => 'コーヒー';
+
+ @override
+ String get quizScenarioStreetSnack => '屋台の軽食';
+
+ @override
+ String get quizScenarioLunch => 'ランチ';
+
+ @override
+ String get quizScenarioTaxiRide => 'タクシー移動';
+
+ @override
+ String get quizScenarioMuseumTicket => '美術館チケット';
+
+ @override
+ String get quizScenarioGroceries => '食料品';
+
+ @override
+ String get quizScenarioHotelNight => 'ホテル1泊';
}
diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart
index e738699..089ee05 100644
--- a/lib/l10n/app_localizations_tr.dart
+++ b/lib/l10n/app_localizations_tr.dart
@@ -512,4 +512,109 @@ class AppLocalizationsTr extends AppLocalizations {
String deleteEntryContent(Object description) {
return '\"$description\" kaydını silmek istediğinizden emin misiniz?';
}
+
+ @override
+ String get apply => 'Uygula';
+
+ @override
+ String get submit => 'Gönder';
+
+ @override
+ String get quiz => 'Test';
+
+ @override
+ String get quizSettings => 'Test ayarları';
+
+ @override
+ String quizRoundsPerGame(Object count) {
+ return 'Oyun başına tur: $count';
+ }
+
+ @override
+ String get quizChoices => 'Seçenekler';
+
+ @override
+ String get quizInput => 'Giriş';
+
+ @override
+ String quizEstimateInCurrency(Object currency) {
+ return '$currency olarak tahmin et';
+ }
+
+ @override
+ String get quizYourEstimate => 'Tahminin';
+
+ @override
+ String get quizGoodCalibration => 'İyi kalibrasyon';
+
+ @override
+ String get quizKeepCalibrating => 'Kalibre etmeye devam';
+
+ @override
+ String quizCorrectAmount(Object amount) {
+ return 'Doğru: $amount';
+ }
+
+ @override
+ String quizYourGuess(Object amount) {
+ return 'Senin tahminin: $amount';
+ }
+
+ @override
+ String quizYouWereOff(Object percent) {
+ return '%$percent saptın';
+ }
+
+ @override
+ String get quizShowSummary => 'Özeti göster';
+
+ @override
+ String get quizNextRound => 'Sonraki tur';
+
+ @override
+ String get quizSummary => 'Test özeti';
+
+ @override
+ String get quizScore => 'Puan';
+
+ @override
+ String get quizBestStreak => 'En iyi seri';
+
+ @override
+ String get quizWithinTenPercent => '%10 içinde';
+
+ @override
+ String get quizAverageMiss => 'Ortalama sapma';
+
+ @override
+ String quizSummaryCorrect(Object amount, Object percent) {
+ return 'Doğru $amount · %$percent sapma';
+ }
+
+ @override
+ String get quizPlayAgain => 'Tekrar oyna';
+
+ @override
+ String get quizScenarioBottleOfWater => 'Bir şişe su';
+
+ @override
+ String get quizScenarioCoffee => 'Kahve';
+
+ @override
+ String get quizScenarioStreetSnack => 'Sokak atıştırmalığı';
+
+ @override
+ String get quizScenarioLunch => 'Öğle yemeği';
+
+ @override
+ String get quizScenarioTaxiRide => 'Taksi yolculuğu';
+
+ @override
+ String get quizScenarioMuseumTicket => 'Müze bileti';
+
+ @override
+ String get quizScenarioGroceries => 'Market alışverişi';
+
+ @override
+ String get quizScenarioHotelNight => 'Otel gecesi';
}
diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart
index 7b2844f..5f10932 100644
--- a/lib/l10n/app_localizations_zh.dart
+++ b/lib/l10n/app_localizations_zh.dart
@@ -506,4 +506,109 @@ class AppLocalizationsZh extends AppLocalizations {
String deleteEntryContent(Object description) {
return '确定要删除“$description”吗?';
}
+
+ @override
+ String get apply => '应用';
+
+ @override
+ String get submit => '提交';
+
+ @override
+ String get quiz => '测验';
+
+ @override
+ String get quizSettings => '测验设置';
+
+ @override
+ String quizRoundsPerGame(Object count) {
+ return '每局轮数:$count';
+ }
+
+ @override
+ String get quizChoices => '选项';
+
+ @override
+ String get quizInput => '输入';
+
+ @override
+ String quizEstimateInCurrency(Object currency) {
+ return '估算为 $currency';
+ }
+
+ @override
+ String get quizYourEstimate => '你的估算';
+
+ @override
+ String get quizGoodCalibration => '校准不错';
+
+ @override
+ String get quizKeepCalibrating => '继续校准';
+
+ @override
+ String quizCorrectAmount(Object amount) {
+ return '正确:$amount';
+ }
+
+ @override
+ String quizYourGuess(Object amount) {
+ return '你的猜测:$amount';
+ }
+
+ @override
+ String quizYouWereOff(Object percent) {
+ return '你偏差了 $percent%';
+ }
+
+ @override
+ String get quizShowSummary => '显示总结';
+
+ @override
+ String get quizNextRound => '下一轮';
+
+ @override
+ String get quizSummary => '测验总结';
+
+ @override
+ String get quizScore => '得分';
+
+ @override
+ String get quizBestStreak => '最佳连击';
+
+ @override
+ String get quizWithinTenPercent => '10%以内';
+
+ @override
+ String get quizAverageMiss => '平均偏差';
+
+ @override
+ String quizSummaryCorrect(Object amount, Object percent) {
+ return '正确 $amount · 偏差 $percent%';
+ }
+
+ @override
+ String get quizPlayAgain => '再玩一次';
+
+ @override
+ String get quizScenarioBottleOfWater => '一瓶水';
+
+ @override
+ String get quizScenarioCoffee => '咖啡';
+
+ @override
+ String get quizScenarioStreetSnack => '街头小吃';
+
+ @override
+ String get quizScenarioLunch => '午餐';
+
+ @override
+ String get quizScenarioTaxiRide => '出租车行程';
+
+ @override
+ String get quizScenarioMuseumTicket => '博物馆门票';
+
+ @override
+ String get quizScenarioGroceries => '日用品';
+
+ @override
+ String get quizScenarioHotelNight => '酒店一晚';
}
diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb
index aa325f7..4bd4d44 100644
--- a/lib/l10n/app_tr.arb
+++ b/lib/l10n/app_tr.arb
@@ -149,5 +149,36 @@
"toLabel": "Şuna",
"more": "Daha fazla",
"deleteEntryTitle": "Kaydı Sil",
- "deleteEntryContent": "\"{description}\" kaydını silmek istediğinizden emin misiniz?"
+ "deleteEntryContent": "\"{description}\" kaydını silmek istediğinizden emin misiniz?",
+ "apply": "Uygula",
+ "submit": "Gönder",
+ "quiz": "Test",
+ "quizSettings": "Test ayarları",
+ "quizRoundsPerGame": "Oyun başına tur: {count}",
+ "quizChoices": "Seçenekler",
+ "quizInput": "Giriş",
+ "quizEstimateInCurrency": "{currency} olarak tahmin et",
+ "quizYourEstimate": "Tahminin",
+ "quizGoodCalibration": "İyi kalibrasyon",
+ "quizKeepCalibrating": "Kalibre etmeye devam",
+ "quizCorrectAmount": "Doğru: {amount}",
+ "quizYourGuess": "Senin tahminin: {amount}",
+ "quizYouWereOff": "%{percent} saptın",
+ "quizShowSummary": "Özeti göster",
+ "quizNextRound": "Sonraki tur",
+ "quizSummary": "Test özeti",
+ "quizScore": "Puan",
+ "quizBestStreak": "En iyi seri",
+ "quizWithinTenPercent": "%10 içinde",
+ "quizAverageMiss": "Ortalama sapma",
+ "quizSummaryCorrect": "Doğru {amount} · %{percent} sapma",
+ "quizPlayAgain": "Tekrar oyna",
+ "quizScenarioBottleOfWater": "Bir şişe su",
+ "quizScenarioCoffee": "Kahve",
+ "quizScenarioStreetSnack": "Sokak atıştırmalığı",
+ "quizScenarioLunch": "Öğle yemeği",
+ "quizScenarioTaxiRide": "Taksi yolculuğu",
+ "quizScenarioMuseumTicket": "Müze bileti",
+ "quizScenarioGroceries": "Market alışverişi",
+ "quizScenarioHotelNight": "Otel gecesi"
}
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index f7e4a48..75087be 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -149,5 +149,36 @@
"toLabel": "到",
"more": "更多",
"deleteEntryTitle": "删除记录",
- "deleteEntryContent": "确定要删除“{description}”吗?"
+ "deleteEntryContent": "确定要删除“{description}”吗?",
+ "apply": "应用",
+ "submit": "提交",
+ "quiz": "测验",
+ "quizSettings": "测验设置",
+ "quizRoundsPerGame": "每局轮数:{count}",
+ "quizChoices": "选项",
+ "quizInput": "输入",
+ "quizEstimateInCurrency": "估算为 {currency}",
+ "quizYourEstimate": "你的估算",
+ "quizGoodCalibration": "校准不错",
+ "quizKeepCalibrating": "继续校准",
+ "quizCorrectAmount": "正确:{amount}",
+ "quizYourGuess": "你的猜测:{amount}",
+ "quizYouWereOff": "你偏差了 {percent}%",
+ "quizShowSummary": "显示总结",
+ "quizNextRound": "下一轮",
+ "quizSummary": "测验总结",
+ "quizScore": "得分",
+ "quizBestStreak": "最佳连击",
+ "quizWithinTenPercent": "10%以内",
+ "quizAverageMiss": "平均偏差",
+ "quizSummaryCorrect": "正确 {amount} · 偏差 {percent}%",
+ "quizPlayAgain": "再玩一次",
+ "quizScenarioBottleOfWater": "一瓶水",
+ "quizScenarioCoffee": "咖啡",
+ "quizScenarioStreetSnack": "街头小吃",
+ "quizScenarioLunch": "午餐",
+ "quizScenarioTaxiRide": "出租车行程",
+ "quizScenarioMuseumTicket": "博物馆门票",
+ "quizScenarioGroceries": "日用品",
+ "quizScenarioHotelNight": "酒店一晚"
}
diff --git a/lib/screens/home.dart b/lib/screens/home.dart
index 03c9e04..1918636 100644
--- a/lib/screens/home.dart
+++ b/lib/screens/home.dart
@@ -17,6 +17,7 @@ import 'package:paren/providers/constants.dart';
import 'package:paren/providers/extensions.dart';
import 'package:paren/providers/paren.dart';
import 'package:paren/screens/home/conversion.dart';
+import 'package:paren/screens/home/currency_quiz.dart';
import 'package:paren/screens/home/customization.dart';
import 'package:paren/screens/home/sheets.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -167,6 +168,11 @@ class _HomeState extends State {
color: context.theme.colorScheme.outlineVariant,
),
Expanded(child: Conversion()),
+ Container(
+ width: 1,
+ color: context.theme.colorScheme.outlineVariant,
+ ),
+ Expanded(child: CurrencyQuizScreen()),
// add border
Container(
width: 1,
@@ -185,7 +191,12 @@ class _HomeState extends State {
onPageChanged: (value) {
paren.currentPage.value = value;
},
- children: [Sheets(), Conversion(), Customization()],
+ children: [
+ Sheets(),
+ Conversion(),
+ CurrencyQuizScreen(),
+ Customization(),
+ ],
);
}),
),
@@ -231,6 +242,7 @@ class _HomeState extends State {
icon: Icon(Icons.calculate_outlined),
label: l10n.calculation,
),
+ GlassBottomBarTab(icon: Icon(Icons.quiz_outlined), label: l10n.quiz),
GlassBottomBarTab(icon: Icon(Icons.settings), label: l10n.settings),
],
extraButton: GlassBottomBarExtraButton(
diff --git a/lib/screens/home/currency_quiz.dart b/lib/screens/home/currency_quiz.dart
new file mode 100644
index 0000000..956f901
--- /dev/null
+++ b/lib/screens/home/currency_quiz.dart
@@ -0,0 +1,1080 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:get/get.dart';
+import 'package:intl/intl.dart';
+import 'package:paren/classes/currency.dart';
+import 'package:paren/components/adaptive_overlay.dart';
+import 'package:paren/l10n/app_localizations_extension.dart';
+import 'package:paren/providers/extensions.dart';
+import 'package:paren/providers/paren.dart';
+
+enum QuizMode { multipleChoice, freeInput }
+
+enum _QuizPhase { answering, result, summary }
+
+enum QuizScenario {
+ bottleOfWater,
+ coffee,
+ streetSnack,
+ lunch,
+ taxiRide,
+ museumTicket,
+ groceries,
+ hotelNight,
+}
+
+class QuizRound {
+ final Currency sourceCurrency;
+ final Currency homeCurrency;
+ final double sourceAmount;
+ final double correctAmount;
+ final QuizScenario scenario;
+ final List options;
+
+ const QuizRound({
+ required this.sourceCurrency,
+ required this.homeCurrency,
+ required this.sourceAmount,
+ required this.correctAmount,
+ required this.scenario,
+ required this.options,
+ });
+}
+
+class QuizAnswer {
+ final QuizRound round;
+ final double guess;
+ final double deviation;
+ final int points;
+ final bool streakHit;
+
+ const QuizAnswer({
+ required this.round,
+ required this.guess,
+ required this.deviation,
+ required this.points,
+ required this.streakHit,
+ });
+}
+
+class CurrencyQuizScreen extends StatefulWidget {
+ const CurrencyQuizScreen({super.key});
+
+ @override
+ State createState() => _CurrencyQuizScreenState();
+}
+
+class _CurrencyQuizScreenState extends State {
+ final Paren paren = Get.find();
+ final random = Random();
+ final inputController = TextEditingController();
+
+ final mode = QuizMode.multipleChoice.obs;
+ final phase = _QuizPhase.answering.obs;
+ final roundCount = 10.obs;
+ final roundIndex = 1.obs;
+ final score = 0.obs;
+ final streak = 0.obs;
+ final bestStreak = 0.obs;
+ final selectedAnswer = Rxn();
+ final currentRound = Rxn();
+ final lastAnswer = Rxn();
+ final answers = [].obs;
+
+ final scenarios = const [
+ (scenario: QuizScenario.bottleOfWater, eurMin: 1.0, eurMax: 3.0),
+ (scenario: QuizScenario.coffee, eurMin: 2.5, eurMax: 6.0),
+ (scenario: QuizScenario.streetSnack, eurMin: 4.0, eurMax: 10.0),
+ (scenario: QuizScenario.lunch, eurMin: 10.0, eurMax: 22.0),
+ (scenario: QuizScenario.taxiRide, eurMin: 12.0, eurMax: 45.0),
+ (scenario: QuizScenario.museumTicket, eurMin: 8.0, eurMax: 24.0),
+ (scenario: QuizScenario.groceries, eurMin: 22.0, eurMax: 75.0),
+ (scenario: QuizScenario.hotelNight, eurMin: 70.0, eurMax: 190.0),
+ ];
+
+ Worker? workerFrom;
+ Worker? workerTo;
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ startGame();
+ });
+
+ workerFrom = ever(paren.fromCurrency, (_) {
+ startGame();
+ });
+ workerTo = ever(paren.toCurrency, (_) {
+ startGame();
+ });
+ }
+
+ @override
+ void dispose() {
+ inputController.dispose();
+ workerFrom?.dispose();
+ workerTo?.dispose();
+ super.dispose();
+ }
+
+ void startGame() {
+ answers.clear();
+ roundIndex.value = 1;
+ score.value = 0;
+ streak.value = 0;
+ bestStreak.value = 0;
+ _nextRound();
+ }
+
+ void _nextRound() {
+ selectedAnswer.value = null;
+ lastAnswer.value = null;
+ inputController.clear();
+ currentRound.value = _buildRound();
+ phase.value = _QuizPhase.answering;
+ }
+
+ QuizRound _buildRound() {
+ var homeCurrency = paren.currencyById(paren.toCurrency.value);
+ var sourceCurrency = paren.currencyById(paren.fromCurrency.value);
+ var scenario = scenarios[random.nextInt(scenarios.length)];
+ var eurValue =
+ scenario.eurMin +
+ random.nextDouble() * (scenario.eurMax - scenario.eurMin);
+ var sourceAmount = paren.convertValue(
+ eurValue,
+ fromId: 'eur',
+ toId: sourceCurrency.id,
+ );
+ sourceAmount = _roundReadableAmount(sourceAmount, sourceCurrency.id);
+ var correctAmount = paren.convertValue(
+ sourceAmount,
+ fromId: sourceCurrency.id,
+ toId: homeCurrency.id,
+ );
+ var options = _buildOptions(correctAmount, homeCurrency.id);
+
+ return QuizRound(
+ sourceCurrency: sourceCurrency,
+ homeCurrency: homeCurrency,
+ sourceAmount: sourceAmount,
+ correctAmount: correctAmount,
+ scenario: scenario.scenario,
+ options: options,
+ );
+ }
+
+ List _buildOptions(double correctAmount, String currencyId) {
+ var factors = [1.0, 0.9, 1.3, 0.3 + random.nextDouble() * 1.4];
+ if (random.nextBool()) {
+ factors[1] = 1.1;
+ }
+ if (random.nextBool()) {
+ factors[2] = 0.7;
+ }
+ factors.shuffle(random);
+
+ var options = factors
+ .map(
+ (factor) => _roundReadableAmount(correctAmount * factor, currencyId),
+ )
+ .toList();
+ var correctOption = _roundReadableAmount(correctAmount, currencyId);
+ var correctIndex = options.indexWhere((option) => option == correctOption);
+ if (correctIndex == -1) {
+ options[random.nextInt(options.length)] = correctOption;
+ }
+
+ var cycleCount = 0;
+ while (options.toSet().length < options.length && cycleCount++ < 10) {
+ var duplicateIndex = options.indexWhere(
+ (option) => options.where((other) => other == option).length > 1,
+ );
+ options[duplicateIndex] = _roundReadableAmount(
+ correctAmount * (0.55 + random.nextDouble() * 1.2),
+ currencyId,
+ );
+ }
+ return options;
+ }
+
+ double _roundReadableAmount(double value, String currencyId) {
+ if (value <= 0) {
+ return 0;
+ }
+ if (_zeroDecimalCurrencies.contains(currencyId.toLowerCase())) {
+ if (value >= 10000) {
+ return (value / 1000).round() * 1000;
+ }
+ if (value >= 1000) {
+ return (value / 100).round() * 100;
+ }
+ return value.roundToDouble();
+ }
+ if (value >= 1000) {
+ return (value / 10).round() * 10;
+ }
+ if (value >= 100) {
+ return value.roundToDouble();
+ }
+ if (value >= 10) {
+ return (value * 2).round() / 2;
+ }
+ return (value * 10).round() / 10;
+ }
+
+ Future submitMultipleChoice(BuildContext context, double guess) async {
+ if (phase.value != _QuizPhase.answering) {
+ return;
+ }
+ selectedAnswer.value = guess;
+ var answer = _recordAnswer(
+ guess,
+ exactChoice: guess == _displayCorrectAmount,
+ );
+ if (answer != null) {
+ await _showResultSheet(context, answer);
+ }
+ }
+
+ Future submitFreeInput(BuildContext context) async {
+ if (phase.value != _QuizPhase.answering) {
+ return;
+ }
+ var normalized = inputController.text.replaceAll(',', '.').trim();
+ var guess = double.tryParse(normalized);
+ if (guess == null || guess <= 0) {
+ HapticFeedback.lightImpact();
+ return;
+ }
+ var answer = _recordAnswer(guess);
+ if (answer != null) {
+ await _showResultSheet(context, answer);
+ }
+ }
+
+ QuizAnswer? _recordAnswer(double guess, {bool? exactChoice}) {
+ var round = currentRound.value;
+ if (round == null) {
+ return null;
+ }
+ var deviation = (guess - round.correctAmount).abs() / round.correctAmount;
+ var streakHit = exactChoice ?? deviation <= 0.1;
+ var roundPoints = exactChoice == false
+ ? 0
+ : max(0, (100 * (1 - min(deviation, 1))).round());
+
+ if (streakHit) {
+ streak.value += 1;
+ bestStreak.value = max(bestStreak.value, streak.value);
+ roundPoints += min(streak.value * 5, 50);
+ HapticFeedback.mediumImpact();
+ } else {
+ streak.value = 0;
+ HapticFeedback.heavyImpact();
+ }
+
+ score.value += roundPoints;
+ var answer = QuizAnswer(
+ round: round,
+ guess: guess,
+ deviation: deviation,
+ points: roundPoints,
+ streakHit: streakHit,
+ );
+ lastAnswer.value = answer;
+ answers.add(answer);
+ phase.value = _QuizPhase.result;
+ return answer;
+ }
+
+ Future _showResultSheet(BuildContext context, QuizAnswer answer) async {
+ var shouldContinue = await Navigator.of(context).push(
+ adaptiveSheetRoute(
+ barrierDismissible: false,
+ draggable: false,
+ child: ResultSheet(
+ answer: answer,
+ formatAmount: _formatAmount,
+ isFinalRound: roundIndex.value >= roundCount.value,
+ ),
+ ),
+ );
+ if (!mounted || shouldContinue != true) {
+ return;
+ }
+ continueGame();
+ }
+
+ void continueGame() {
+ if (roundIndex.value >= roundCount.value) {
+ phase.value = _QuizPhase.summary;
+ return;
+ }
+ roundIndex.value += 1;
+ _nextRound();
+ }
+
+ double get _displayCorrectAmount {
+ var round = currentRound.value;
+ if (round == null) {
+ return 0;
+ }
+ return _roundReadableAmount(round.correctAmount, round.homeCurrency.id);
+ }
+
+ NumberFormat _formatter(String currencyId) {
+ return NumberFormat.simpleCurrency(
+ name: currencyId.toUpperCase(),
+ locale: Get.locale?.toLanguageTag(),
+ );
+ }
+
+ String _formatAmount(double amount, String currencyId) {
+ return _formatter(currencyId).format(amount);
+ }
+
+ Future _showSettings() async {
+ var draftCount = roundCount.value.obs;
+ await Get.dialog(
+ AlertDialog(
+ title: Text(context.l10n.quizSettings),
+ content: Obx(
+ () => Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(context.l10n.quizRoundsPerGame(draftCount.value)),
+ Slider(
+ min: 5,
+ max: 20,
+ divisions: 3,
+ value: draftCount.value.toDouble(),
+ label: draftCount.value.toString(),
+ onChanged: (value) {
+ draftCount.value = value.round();
+ },
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(onPressed: Get.back, child: Text(context.l10n.cancel)),
+ FilledButton(
+ onPressed: () {
+ roundCount.value = draftCount.value;
+ Get.back();
+ startGame();
+ },
+ child: Text(context.l10n.apply),
+ ),
+ ],
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Obx(() {
+ var round = currentRound.value;
+
+ if (round == null) {
+ return const Center(child: CircularProgressIndicator());
+ }
+
+ return AnimatedSwitcher(
+ duration: 250.milliseconds,
+ child: switch (phase.value) {
+ _QuizPhase.summary => QuizSummaryScreen(
+ key: const ValueKey('summary'),
+ score: score.value,
+ bestStreak: bestStreak.value,
+ answers: answers,
+ formatAmount: _formatAmount,
+ onRestart: startGame,
+ ),
+ _ => _buildGame(context, round),
+ },
+ );
+ });
+ }
+
+ Widget _buildGame(BuildContext context, QuizRound round) {
+ return GestureDetector(
+ onHorizontalDragEnd: (details) {
+ if (phase.value == _QuizPhase.result &&
+ (details.primaryVelocity ?? 0) < -200) {
+ continueGame();
+ }
+ },
+ child: ListView(
+ key: const ValueKey('game'),
+ padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
+ children: [
+ _QuizTopBar(
+ mode: mode.value,
+ roundIndex: roundIndex.value,
+ roundCount: roundCount.value,
+ score: score.value,
+ streak: streak.value,
+ onModeChanged: (value) {
+ if (phase.value == _QuizPhase.answering) {
+ mode.value = value;
+ inputController.clear();
+ }
+ },
+ onSettings: _showSettings,
+ ),
+ 14.h,
+ QuizQuestionCard(
+ round: round,
+ sourceAmount: _formatAmount(
+ round.sourceAmount,
+ round.sourceCurrency.id,
+ ),
+ ),
+ 16.h,
+ if (mode.value == QuizMode.multipleChoice)
+ MultipleChoiceAnswerGrid(
+ options: round.options,
+ selectedAnswer: selectedAnswer.value,
+ correctAnswer: _displayCorrectAmount,
+ showResult: phase.value == _QuizPhase.result,
+ currencyId: round.homeCurrency.id,
+ formatAmount: _formatAmount,
+ onSelected: (guess) => submitMultipleChoice(context, guess),
+ )
+ else
+ FreeInputAnswerField(
+ controller: inputController,
+ enabled: phase.value == _QuizPhase.answering,
+ currencyId: round.homeCurrency.id,
+ onSubmit: () => submitFreeInput(context),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _QuizTopBar extends StatelessWidget {
+ final QuizMode mode;
+ final int roundIndex;
+ final int roundCount;
+ final int score;
+ final int streak;
+ final ValueChanged onModeChanged;
+ final VoidCallback onSettings;
+
+ const _QuizTopBar({
+ required this.mode,
+ required this.roundIndex,
+ required this.roundCount,
+ required this.score,
+ required this.streak,
+ required this.onModeChanged,
+ required this.onSettings,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ var colorScheme = context.theme.colorScheme;
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ _MetricChip(
+ icon: Icons.flag,
+ label: '$roundIndex/$roundCount',
+ ),
+ _MetricChip(icon: Icons.stars, label: '$score'),
+ _MetricChip(
+ icon: Icons.local_fire_department,
+ label: '$streak',
+ ),
+ ],
+ ),
+ ),
+ IconButton.filledTonal(
+ onPressed: onSettings,
+ icon: const Icon(Icons.tune),
+ tooltip: context.l10n.quizSettings,
+ ),
+ ],
+ ),
+ 12.h,
+ SegmentedButton(
+ segments: [
+ ButtonSegment(
+ value: QuizMode.multipleChoice,
+ icon: const Icon(Icons.grid_view),
+ label: Text(context.l10n.quizChoices),
+ ),
+ ButtonSegment(
+ value: QuizMode.freeInput,
+ icon: const Icon(Icons.edit),
+ label: Text(context.l10n.quizInput),
+ ),
+ ],
+ selected: {mode},
+ style: SegmentedButton.styleFrom(
+ selectedBackgroundColor: colorScheme.primaryContainer,
+ ),
+ onSelectionChanged: (values) {
+ onModeChanged(values.first);
+ },
+ ),
+ ],
+ );
+ }
+}
+
+class _MetricChip extends StatelessWidget {
+ final IconData icon;
+ final String label;
+
+ const _MetricChip({required this.icon, required this.label});
+
+ @override
+ Widget build(BuildContext context) {
+ var colorScheme = context.theme.colorScheme;
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
+ decoration: BoxDecoration(
+ color: colorScheme.secondaryContainer.withValues(alpha: 0.45),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(icon, size: 16, color: colorScheme.secondary),
+ 6.w,
+ Text(label, style: const TextStyle(fontWeight: FontWeight.w700)),
+ ],
+ ),
+ );
+ }
+}
+
+class QuizQuestionCard extends StatelessWidget {
+ final QuizRound round;
+ final String sourceAmount;
+
+ const QuizQuestionCard({
+ super.key,
+ required this.round,
+ required this.sourceAmount,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ var colorScheme = context.theme.colorScheme;
+ return Container(
+ padding: const EdgeInsets.all(18),
+ decoration: BoxDecoration(
+ color: colorScheme.primaryContainer.withValues(alpha: 0.32),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: colorScheme.primary.withValues(alpha: 0.2)),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ quizScenarioLabel(context, round.scenario),
+ style: TextStyle(
+ color: colorScheme.secondary,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ 10.h,
+ FittedBox(
+ alignment: Alignment.centerLeft,
+ fit: BoxFit.scaleDown,
+ child: Text(
+ '$sourceAmount (${round.sourceCurrency.id.toUpperCase()}) ➜ ?',
+ style: Theme.of(context).textTheme.headlineMedium?.copyWith(
+ fontWeight: FontWeight.w800,
+ letterSpacing: 0,
+ ),
+ ),
+ ),
+ 8.h,
+ Text(
+ context.l10n.quizEstimateInCurrency(
+ round.homeCurrency.id.toUpperCase(),
+ ),
+ style: TextStyle(color: colorScheme.onSurfaceVariant),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class MultipleChoiceAnswerGrid extends StatelessWidget {
+ final List options;
+ final double? selectedAnswer;
+ final double correctAnswer;
+ final bool showResult;
+ final String currencyId;
+ final String Function(double amount, String currencyId) formatAmount;
+ final ValueChanged onSelected;
+
+ const MultipleChoiceAnswerGrid({
+ super.key,
+ required this.options,
+ required this.selectedAnswer,
+ required this.correctAnswer,
+ required this.showResult,
+ required this.currencyId,
+ required this.formatAmount,
+ required this.onSelected,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ var isWide = constraints.maxWidth >= 560;
+ return GridView.builder(
+ itemCount: options.length,
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: isWide ? 2 : 1,
+ childAspectRatio: isWide ? 3.4 : 5.2,
+ crossAxisSpacing: 10,
+ mainAxisSpacing: 10,
+ ),
+ itemBuilder: (context, index) {
+ var option = options[index];
+ var isCorrect = option == correctAnswer;
+ var isSelected = selectedAnswer == option;
+ return _AnswerCard(
+ label: formatAmount(option, currencyId),
+ isCorrect: isCorrect,
+ isSelected: isSelected,
+ showResult: showResult,
+ onTap: () => onSelected(option),
+ );
+ },
+ );
+ },
+ );
+ }
+}
+
+class _AnswerCard extends StatelessWidget {
+ final String label;
+ final bool isCorrect;
+ final bool isSelected;
+ final bool showResult;
+ final VoidCallback onTap;
+
+ const _AnswerCard({
+ required this.label,
+ required this.isCorrect,
+ required this.isSelected,
+ required this.showResult,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ var colorScheme = context.theme.colorScheme;
+ var color = colorScheme.surface;
+ var borderColor = colorScheme.outlineVariant;
+ var icon = Icons.circle_outlined;
+
+ if (showResult && isCorrect) {
+ color = Colors.green.withValues(alpha: 0.18);
+ borderColor = Colors.green;
+ icon = Icons.check_circle;
+ } else if (showResult && isSelected) {
+ color = colorScheme.errorContainer;
+ borderColor = colorScheme.error;
+ icon = Icons.cancel;
+ }
+
+ return AnimatedContainer(
+ duration: 220.milliseconds,
+ curve: Curves.easeOut,
+ decoration: BoxDecoration(
+ color: color,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: borderColor, width: showResult ? 1.8 : 1),
+ ),
+ child: InkWell(
+ borderRadius: BorderRadius.circular(8),
+ onTap: showResult ? null : onTap,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Row(
+ children: [
+ Icon(icon, color: borderColor),
+ 12.w,
+ Expanded(
+ child: FittedBox(
+ alignment: Alignment.centerLeft,
+ fit: BoxFit.scaleDown,
+ child: Text(
+ label,
+ style: Theme.of(context).textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w800,
+ letterSpacing: 0,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class FreeInputAnswerField extends StatelessWidget {
+ final TextEditingController controller;
+ final bool enabled;
+ final String currencyId;
+ final VoidCallback onSubmit;
+
+ const FreeInputAnswerField({
+ super.key,
+ required this.controller,
+ required this.enabled,
+ required this.currencyId,
+ required this.onSubmit,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: controller,
+ enabled: enabled,
+ autofocus: true,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
+ inputFormatters: [
+ FilteringTextInputFormatter.allow(RegExp(r'[0-9\.,]')),
+ ],
+ textInputAction: TextInputAction.done,
+ decoration: InputDecoration(
+ prefixIcon: const Icon(Icons.payments_outlined),
+ suffixText: currencyId.toUpperCase(),
+ labelText: context.l10n.quizYourEstimate,
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ onSubmitted: (_) => onSubmit(),
+ ),
+ ),
+ 10.w,
+ FilledButton.icon(
+ onPressed: enabled ? onSubmit : null,
+ icon: const Icon(Icons.check),
+ label: Text(context.l10n.submit),
+ ),
+ ],
+ );
+ }
+}
+
+class ResultSheet extends StatelessWidget {
+ final QuizAnswer answer;
+ final bool isFinalRound;
+ final String Function(double amount, String currencyId) formatAmount;
+
+ const ResultSheet({
+ super.key,
+ required this.answer,
+ required this.formatAmount,
+ required this.isFinalRound,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return _ResultPanel(
+ answer: answer,
+ formatAmount: formatAmount,
+ onContinue: () => Navigator.of(context).pop(true),
+ isFinalRound: isFinalRound,
+ );
+ }
+}
+
+class _ResultPanel extends StatelessWidget {
+ final QuizAnswer answer;
+ final bool isFinalRound;
+ final String Function(double amount, String currencyId) formatAmount;
+ final VoidCallback onContinue;
+
+ const _ResultPanel({
+ required this.answer,
+ required this.formatAmount,
+ required this.onContinue,
+ required this.isFinalRound,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ var colorScheme = context.theme.colorScheme;
+ var closeness = (1 - answer.deviation).clamp(0.0, 1.0);
+ var homeId = answer.round.homeCurrency.id;
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: colorScheme.outlineVariant),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Row(
+ children: [
+ Icon(
+ answer.streakHit ? Icons.emoji_events : Icons.insights,
+ color: answer.streakHit
+ ? Colors.amber.shade700
+ : colorScheme.primary,
+ ),
+ 10.w,
+ Expanded(
+ child: Text(
+ answer.streakHit
+ ? context.l10n.quizGoodCalibration
+ : context.l10n.quizKeepCalibrating,
+ style: Theme.of(context).textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w800,
+ ),
+ ),
+ ),
+ Text('+${answer.points}'),
+ ],
+ ),
+ 12.h,
+ Text(
+ context.l10n.quizCorrectAmount(
+ formatAmount(answer.round.correctAmount, homeId),
+ ),
+ ),
+ Text(context.l10n.quizYourGuess(formatAmount(answer.guess, homeId))),
+ 12.h,
+ LinearProgressIndicator(
+ value: closeness,
+ minHeight: 10,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ 8.h,
+ Text(
+ context.l10n.quizYouWereOff((answer.deviation * 100).round()),
+ textAlign: TextAlign.center,
+ style: TextStyle(color: colorScheme.onSurfaceVariant),
+ ),
+ 14.h,
+ FilledButton.icon(
+ onPressed: onContinue,
+ icon: Icon(isFinalRound ? Icons.leaderboard : Icons.arrow_forward),
+ label: Text(
+ isFinalRound
+ ? context.l10n.quizShowSummary
+ : context.l10n.quizNextRound,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class QuizSummaryScreen extends StatelessWidget {
+ final int score;
+ final int bestStreak;
+ final List answers;
+ final String Function(double amount, String currencyId) formatAmount;
+ final VoidCallback onRestart;
+
+ const QuizSummaryScreen({
+ super.key,
+ required this.score,
+ required this.bestStreak,
+ required this.answers,
+ required this.formatAmount,
+ required this.onRestart,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ var colorScheme = context.theme.colorScheme;
+ var averageDeviation = answers.isEmpty
+ ? 0
+ : answers
+ .map((answer) => answer.deviation)
+ .reduce((left, right) => left + right) /
+ answers.length;
+ var accurateRounds = answers
+ .where((answer) => answer.deviation <= 0.1)
+ .length;
+
+ return ListView(
+ key: const ValueKey('summary'),
+ padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
+ children: [
+ Container(
+ padding: const EdgeInsets.all(18),
+ decoration: BoxDecoration(
+ color: colorScheme.primaryContainer.withValues(alpha: 0.32),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ context.l10n.quizSummary,
+ style: Theme.of(context).textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.w800,
+ letterSpacing: 0,
+ ),
+ ),
+ 14.h,
+ Wrap(
+ spacing: 10,
+ runSpacing: 10,
+ children: [
+ _SummaryTile(label: context.l10n.quizScore, value: '$score'),
+ _SummaryTile(
+ label: context.l10n.quizBestStreak,
+ value: '$bestStreak',
+ ),
+ _SummaryTile(
+ label: context.l10n.quizWithinTenPercent,
+ value: '$accurateRounds/${answers.length}',
+ ),
+ _SummaryTile(
+ label: context.l10n.quizAverageMiss,
+ value: '${(averageDeviation * 100).round()}%',
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ 16.h,
+ ...answers.map(
+ (answer) => ListTile(
+ contentPadding: EdgeInsets.zero,
+ leading: CircleAvatar(
+ backgroundColor: answer.streakHit
+ ? Colors.green.withValues(alpha: 0.18)
+ : colorScheme.errorContainer,
+ child: Icon(
+ answer.streakHit ? Icons.check : Icons.close,
+ color: answer.streakHit ? Colors.green : colorScheme.error,
+ ),
+ ),
+ title: Text(
+ '${formatAmount(answer.round.sourceAmount, answer.round.sourceCurrency.id)} ${answer.round.sourceCurrency.id.toUpperCase()}',
+ ),
+ subtitle: Text(
+ context.l10n.quizSummaryCorrect(
+ formatAmount(
+ answer.round.correctAmount,
+ answer.round.homeCurrency.id,
+ ),
+ (answer.deviation * 100).round(),
+ ),
+ ),
+ trailing: Text('+${answer.points}'),
+ ),
+ ),
+ 12.h,
+ FilledButton.icon(
+ onPressed: onRestart,
+ icon: const Icon(Icons.refresh),
+ label: Text(context.l10n.quizPlayAgain),
+ ),
+ ],
+ );
+ }
+}
+
+String quizScenarioLabel(BuildContext context, QuizScenario scenario) {
+ return switch (scenario) {
+ QuizScenario.bottleOfWater => context.l10n.quizScenarioBottleOfWater,
+ QuizScenario.coffee => context.l10n.quizScenarioCoffee,
+ QuizScenario.streetSnack => context.l10n.quizScenarioStreetSnack,
+ QuizScenario.lunch => context.l10n.quizScenarioLunch,
+ QuizScenario.taxiRide => context.l10n.quizScenarioTaxiRide,
+ QuizScenario.museumTicket => context.l10n.quizScenarioMuseumTicket,
+ QuizScenario.groceries => context.l10n.quizScenarioGroceries,
+ QuizScenario.hotelNight => context.l10n.quizScenarioHotelNight,
+ };
+}
+
+class _SummaryTile extends StatelessWidget {
+ final String label;
+ final String value;
+
+ const _SummaryTile({required this.label, required this.value});
+
+ @override
+ Widget build(BuildContext context) {
+ var colorScheme = context.theme.colorScheme;
+ return Container(
+ width: 138,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: colorScheme.surface.withValues(alpha: 0.74),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: colorScheme.outlineVariant),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(label, style: TextStyle(color: colorScheme.onSurfaceVariant)),
+ 6.h,
+ Text(
+ value,
+ style: Theme.of(
+ context,
+ ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w800),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+const _zeroDecimalCurrencies = {
+ 'bif',
+ 'clp',
+ 'djf',
+ 'gnf',
+ 'isk',
+ 'jpy',
+ 'kmf',
+ 'krw',
+ 'pyg',
+ 'rwf',
+ 'ugx',
+ 'vnd',
+ 'vuv',
+ 'xaf',
+ 'xof',
+ 'xpf',
+};
diff --git a/pubspec.lock b/pubspec.lock
index 054e3eb..7e08769 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -482,10 +482,10 @@ packages:
dependency: "direct main"
description:
name: liquid_glass_widgets
- sha256: "935c01b978378a130072967f6d8f7827421f2220df1a35e58383178ba492c793"
+ sha256: "2794bd0080a27c0f78a8c7ee70a0be1700057f3c9578794e6891e94157f166ac"
url: "https://pub.dev"
source: hosted
- version: "0.15.6"
+ version: "0.16.0"
logging:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index bdb69d4..4208098 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -31,7 +31,7 @@ dependencies:
flutter_colorpicker: 1.1.0
stupid_simple_sheet: 0.9.1+1
json_annotation: 4.12.0
- liquid_glass_widgets: ^0.15.6
+ liquid_glass_widgets: ^0.16.0
# image_picker: 1.1.2
dev_dependencies:
diff --git a/release-android.sh b/release-android.sh
index 5913e8f..a64e7ba 100755
--- a/release-android.sh
+++ b/release-android.sh
@@ -2,7 +2,9 @@
git pull
git_count=$(git rev-list --count main)
+echo "==> flutter build aab --release --build-number='$git_count'"
flutter build aab --release --build-number="$git_count"
cd android || exit
+echo "==> bundler exec fastlane release"
bundler exec fastlane release
cd .. || exit
\ No newline at end of file
diff --git a/release-emre.sh b/release-emre.sh
index f07e841..9e6fc65 100755
--- a/release-emre.sh
+++ b/release-emre.sh
@@ -1,43 +1,45 @@
#!/bin/bash
git pull
git_count=$(git rev-list --count HEAD)
-echo "Rolling out +$git_count ..."
+echo "==> Rolling out +$git_count ..."
-echo "Do you want to release all [0], ios [1], android [2]?"
-echo "Enter number: "
+echo "==> Do you want to release all [0], ios [1], android [2]?"
+echo "==> Enter number: "
read selection
if [[ ! $selection =~ ^[0-9]+$ ]] ; then
- echo "This was not a number. Retry."
+ echo "==> This was not a number. Retry."
exit
fi
+echo "==> flutter doctor --verbose"
flutter doctor --verbose
+echo "==> flutter clean && flutter pub get"
flutter clean && flutter pub get
if [ $selection = "0" ] ; then
- echo "Pushing all"
+ echo "==> Pushing all"
echo ""
- echo "Pushing ios"
+ echo "==> Pushing ios"
./release-ios.sh
echo ""
- echo "Pushing android"
+ echo "==> Pushing android"
./release-android.sh
echo ""
- echo "Finished"
+ echo "==> Done"
fi
if [ $selection = "1" ] ; then
- echo "Pushing ios"
+ echo "==> Pushing ios"
./release-ios.sh
echo ""
- echo "Finished"
+ echo "==> Done"
fi
if [ $selection = "2" ] ; then
- echo "Pushing android"
+ echo "==> Pushing android"
./release-android.sh
echo ""
- echo "Finished"
+ echo "==> Done"
fi
\ No newline at end of file
diff --git a/release-github-ipa.sh b/release-github-ipa.sh
new file mode 100755
index 0000000..4c258f3
--- /dev/null
+++ b/release-github-ipa.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+git_count=$(git rev-list --count main)
+
+REPO="yurtemre7/paren"
+
+if [[ ! -f pubspec.yaml ]]; then
+ echo "Error: run this script from your Flutter project root." >&2
+ exit 1
+fi
+
+echo "==> flutter build ipa --release --build-number='$git_count'"
+flutter build ipa --release --build-number="$git_count"
+
+IPA_PATH="$(find build/ios/ipa -maxdepth 1 -type f -name '*.ipa' -print0 | xargs -r -0 ls -t | head -n 1)"
+
+if [[ -z "$IPA_PATH" || ! -f "$IPA_PATH" ]]; then
+ echo "Error: no IPA found in build/ios/ipa." >&2
+ exit 1
+fi
+
+TAG="$(gh release view -R "$REPO" --json tagName --jq .tagName)"
+
+if [[ -z "$TAG" ]]; then
+ echo "Error: could not find the latest release in $REPO." >&2
+ exit 1
+fi
+
+echo "==> Uploading $(basename "$IPA_PATH") to release $TAG"
+gh release upload "$TAG" "$IPA_PATH" -R "$REPO" --clobber
+
+echo "==> Done"
\ No newline at end of file
diff --git a/release-ios.sh b/release-ios.sh
index 16f156f..244201a 100755
--- a/release-ios.sh
+++ b/release-ios.sh
@@ -2,10 +2,13 @@
git pull
git_count=$(git rev-list --count main)
+echo "==> flutter build ios --release --build-number='$git_count'"
flutter build ios --release --build-number="$git_count"
cd ios || exit
+echo "==> bundler exec fastlane release"
bundler exec fastlane release
cd .. || exit
+./release-github-ipa.sh
git add ./ios/
git commit -m "chore: update ios version"
git push