diff --git a/.gitignore b/.gitignore index d3452d081..bfbb07022 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,9 @@ lib/i18n/generated/ # Widget Preview related .widget_preview/ + +# Unused platforms +macos/* +linux/* +windows/* +web/* diff --git a/android/app/src/main/res/values-hi/widget_plurals.xml b/android/app/src/main/res/values-hi/widget_plurals.xml index 20f3be92f..cc198aa36 100644 --- a/android/app/src/main/res/values-hi/widget_plurals.xml +++ b/android/app/src/main/res/values-hi/widget_plurals.xml @@ -4,8 +4,20 @@ %d दिनों के लिए HRT पर %d दिनों के लिए HRT पर + + HRT on %d week + HRT on %d week + %d महीनों के लिए HRT पर %d महीनों के लिए HRT पर + + HRT पर %d वर्ष + HRT पर %d वर्ष + + + %d प्रवेश लॉग इन + %d प्रवेश लॉग इन + diff --git a/android/app/src/main/res/values-hi/widget_strings.xml b/android/app/src/main/res/values-hi/widget_strings.xml new file mode 100644 index 000000000..453a446e8 --- /dev/null +++ b/android/app/src/main/res/values-hi/widget_strings.xml @@ -0,0 +1,8 @@ + + + HRT पर समय + जब तक आप एचआरटी और आपके कुल सेवन पर रहे हैं, तब तक दिखाएं + अपना पहला सेवन लॉग इन करने के लिए मोना खोलें + 8 महीने के लिए HRT पर + 16 सेवन लॉग इन + diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index a59f7475a..ed1cc185a 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index 03812c49e..475254424 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index e715b4e56..9ddc57d71 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png new file mode 100644 index 000000000..c35a190d6 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/fr-FR/images/phoneScreenshots/1.png b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/1.png new file mode 100644 index 000000000..4a26acc26 Binary files /dev/null and b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/fr-FR/images/phoneScreenshots/2.png b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/2.png new file mode 100644 index 000000000..1f00ee571 Binary files /dev/null and b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/fr-FR/images/phoneScreenshots/3.png b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/3.png new file mode 100644 index 000000000..2882acca7 Binary files /dev/null and b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/fr-FR/images/phoneScreenshots/4.png b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/4.png new file mode 100644 index 000000000..0e2aa10e2 Binary files /dev/null and b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/hi-IN/full_description.txt b/fastlane/metadata/android/hi-IN/full_description.txt new file mode 100644 index 000000000..c187a8c00 --- /dev/null +++ b/fastlane/metadata/android/hi-IN/full_description.txt @@ -0,0 +1,22 @@ +मोना के साथ अपने हार्मोनल स्वास्थ्य का नियंत्रण करें, पूरी तरह से स्थानीय, ओपन सोर्स ऐप आपको अपने हार्मोन यात्रा को ट्रैक करने, समझने और प्रबंधित करने में मदद करने के लिए डिज़ाइन किया गया है। + +क्यों मोना अलग है: + +- पूरी तरह से निजी: आपके सभी डेटा आपके डिवाइस पर रहते हैं। सर्वर पर कुछ भी नहीं भेजा जाता है। +- ओपन सोर्स: ऐप पूरी तरह से खुला स्रोत है, जो समुदाय से अधिकतम पारदर्शिता और योगदान की अनुमति देता है। +- जीवन के लिए नि: शुल्क: कोई सदस्यता, कोई प्रीमियम सुविधाएँ नहीं। मोना हर किसी के लिए स्वतंत्र है। + +दवा अनुस्मारक: + +- अपनी दवा रेजिमेंट जोड़ें +प्रत्येक आगामी खुराक (मात्रा, तारीख, पक्ष, मिस्ड खुराक) के लिए विस्तृत जानकारी प्राप्त करें +- अधिसूचना प्राप्त करें ताकि आप कभी भी खुराक नहीं छोड़ सकें + +अपनी दवा के स्टॉक को कुशलतापूर्वक प्रबंधित करें: + +- स्टॉक में प्रत्येक दवा का ट्रैक रखें +- प्रत्येक सेवन के साथ क्वांटिटी स्वचालित रूप से अपडेट की जाती है + +हार्मोन का स्तर ग्राफ: + +- एस्ट्राडियोल मोनोथेरेपी पर उपयोगकर्ताओं के लिए, समय के साथ नकली हार्मोन स्तर को दृश्यित करें diff --git a/fastlane/metadata/android/hi-IN/short_description.txt b/fastlane/metadata/android/hi-IN/short_description.txt new file mode 100644 index 000000000..5bca09a5c --- /dev/null +++ b/fastlane/metadata/android/hi-IN/short_description.txt @@ -0,0 +1 @@ +आपका ट्रांस हेल्थ साथी diff --git a/fastlane/metadata/android/hi-IN/title.txt b/fastlane/metadata/android/hi-IN/title.txt new file mode 100644 index 000000000..286eee73b --- /dev/null +++ b/fastlane/metadata/android/hi-IN/title.txt @@ -0,0 +1 @@ +मोना - HRT जर्नल diff --git a/fastlane/metadata/android/hu-HU/title.txt b/fastlane/metadata/android/hu-HU/title.txt new file mode 100644 index 000000000..ccde3b94c --- /dev/null +++ b/fastlane/metadata/android/hu-HU/title.txt @@ -0,0 +1 @@ +Mona - HRT napló diff --git a/fastlane/metadata/ios/hi/description.txt b/fastlane/metadata/ios/hi/description.txt new file mode 100644 index 000000000..c187a8c00 --- /dev/null +++ b/fastlane/metadata/ios/hi/description.txt @@ -0,0 +1,22 @@ +मोना के साथ अपने हार्मोनल स्वास्थ्य का नियंत्रण करें, पूरी तरह से स्थानीय, ओपन सोर्स ऐप आपको अपने हार्मोन यात्रा को ट्रैक करने, समझने और प्रबंधित करने में मदद करने के लिए डिज़ाइन किया गया है। + +क्यों मोना अलग है: + +- पूरी तरह से निजी: आपके सभी डेटा आपके डिवाइस पर रहते हैं। सर्वर पर कुछ भी नहीं भेजा जाता है। +- ओपन सोर्स: ऐप पूरी तरह से खुला स्रोत है, जो समुदाय से अधिकतम पारदर्शिता और योगदान की अनुमति देता है। +- जीवन के लिए नि: शुल्क: कोई सदस्यता, कोई प्रीमियम सुविधाएँ नहीं। मोना हर किसी के लिए स्वतंत्र है। + +दवा अनुस्मारक: + +- अपनी दवा रेजिमेंट जोड़ें +प्रत्येक आगामी खुराक (मात्रा, तारीख, पक्ष, मिस्ड खुराक) के लिए विस्तृत जानकारी प्राप्त करें +- अधिसूचना प्राप्त करें ताकि आप कभी भी खुराक नहीं छोड़ सकें + +अपनी दवा के स्टॉक को कुशलतापूर्वक प्रबंधित करें: + +- स्टॉक में प्रत्येक दवा का ट्रैक रखें +- प्रत्येक सेवन के साथ क्वांटिटी स्वचालित रूप से अपडेट की जाती है + +हार्मोन का स्तर ग्राफ: + +- एस्ट्राडियोल मोनोथेरेपी पर उपयोगकर्ताओं के लिए, समय के साथ नकली हार्मोन स्तर को दृश्यित करें diff --git a/fastlane/metadata/ios/hi/keywords.txt b/fastlane/metadata/ios/hi/keywords.txt new file mode 100644 index 000000000..f6f95f0f6 --- /dev/null +++ b/fastlane/metadata/ios/hi/keywords.txt @@ -0,0 +1 @@ +मोना, ट्रांस, ट्रांसजेंडर, हार्मोन, हार्मोनल, ट्रैकर, लॉग, hrt, पत्रिका, मेमो, एस्ट्राडियोल, सिम्युलेटर diff --git a/fastlane/metadata/ios/hi/name.txt b/fastlane/metadata/ios/hi/name.txt new file mode 100644 index 000000000..286eee73b --- /dev/null +++ b/fastlane/metadata/ios/hi/name.txt @@ -0,0 +1 @@ +मोना - HRT जर्नल diff --git a/fastlane/metadata/ios/hi/promotional_text.txt b/fastlane/metadata/ios/hi/promotional_text.txt new file mode 100644 index 000000000..b505ed0a4 --- /dev/null +++ b/fastlane/metadata/ios/hi/promotional_text.txt @@ -0,0 +1 @@ +अपने हार्मोन ट्रैक करें। अपने meds प्रबंधित करें। सभी निजी, खुला और मुक्त।। diff --git a/fastlane/metadata/ios/hi/subtitle.txt b/fastlane/metadata/ios/hi/subtitle.txt new file mode 100644 index 000000000..5bca09a5c --- /dev/null +++ b/fastlane/metadata/ios/hi/subtitle.txt @@ -0,0 +1 @@ +आपका ट्रांस हेल्थ साथी diff --git a/fastlane/metadata/ios/hu/keywords.txt b/fastlane/metadata/ios/hu/keywords.txt new file mode 100644 index 000000000..26e61c02a --- /dev/null +++ b/fastlane/metadata/ios/hu/keywords.txt @@ -0,0 +1 @@ +mona, transz, tansznemű, hormon, hormonális, követő, napló, hrt, napló, memó, estradiol, szimulátor diff --git a/fastlane/metadata/ios/hu/name.txt b/fastlane/metadata/ios/hu/name.txt new file mode 100644 index 000000000..ccde3b94c --- /dev/null +++ b/fastlane/metadata/ios/hu/name.txt @@ -0,0 +1 @@ +Mona - HRT napló diff --git a/fastlane/screenshot_captions/en.json b/fastlane/screenshot_captions/en.json new file mode 100644 index 000000000..631faff28 --- /dev/null +++ b/fastlane/screenshot_captions/en.json @@ -0,0 +1,6 @@ +{ + "1": "Never miss a dose", + "2": "Monitor your levels and blood tests", + "3": "See your history at a glance", + "4": "Never run out" +} \ No newline at end of file diff --git a/fastlane/screenshot_captions/fr.json b/fastlane/screenshot_captions/fr.json new file mode 100644 index 000000000..32a584990 --- /dev/null +++ b/fastlane/screenshot_captions/fr.json @@ -0,0 +1,6 @@ +{ + "1": "Ne ratez jamais une prise", + "2": "Suivez vos taux et prises de sang", + "3": "Votre historique à portée de main", + "4": "Ne manquez jamais de stock" +} \ No newline at end of file diff --git a/fastlane/screenshot_locales.json b/fastlane/screenshot_locales.json new file mode 100644 index 000000000..eed3b74e9 --- /dev/null +++ b/fastlane/screenshot_locales.json @@ -0,0 +1,4 @@ +[ + { "app": "en", "play": "en-US", "appstore": "en-US" }, + { "app": "fr", "play": "fr-FR", "appstore": "fr-FR" } +] diff --git a/fastlane/screenshots/en-US/1.png b/fastlane/screenshots/en-US/1.png new file mode 100644 index 000000000..6880df98a Binary files /dev/null and b/fastlane/screenshots/en-US/1.png differ diff --git a/fastlane/screenshots/en-US/2.png b/fastlane/screenshots/en-US/2.png new file mode 100644 index 000000000..74151da6d Binary files /dev/null and b/fastlane/screenshots/en-US/2.png differ diff --git a/fastlane/screenshots/en-US/3.png b/fastlane/screenshots/en-US/3.png new file mode 100644 index 000000000..271532df7 Binary files /dev/null and b/fastlane/screenshots/en-US/3.png differ diff --git a/fastlane/screenshots/en-US/4.png b/fastlane/screenshots/en-US/4.png new file mode 100644 index 000000000..adfea1357 Binary files /dev/null and b/fastlane/screenshots/en-US/4.png differ diff --git a/fastlane/screenshots/fr-FR/1.png b/fastlane/screenshots/fr-FR/1.png new file mode 100644 index 000000000..075a8facf Binary files /dev/null and b/fastlane/screenshots/fr-FR/1.png differ diff --git a/fastlane/screenshots/fr-FR/2.png b/fastlane/screenshots/fr-FR/2.png new file mode 100644 index 000000000..c36e37050 Binary files /dev/null and b/fastlane/screenshots/fr-FR/2.png differ diff --git a/fastlane/screenshots/fr-FR/3.png b/fastlane/screenshots/fr-FR/3.png new file mode 100644 index 000000000..b157a2c37 Binary files /dev/null and b/fastlane/screenshots/fr-FR/3.png differ diff --git a/fastlane/screenshots/fr-FR/4.png b/fastlane/screenshots/fr-FR/4.png new file mode 100644 index 000000000..cd7aae480 Binary files /dev/null and b/fastlane/screenshots/fr-FR/4.png differ diff --git a/lib/app.dart b/lib/app.dart index 86bec1e53..05bff68e3 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:mona/controllers/notification_planner.dart'; import 'package:mona/controllers/notification_scheduler.dart'; +import 'package:mona/controllers/slot_finder.dart'; +import 'package:mona/controllers/slots_builder.dart'; import 'package:mona/data/providers/medication_intake_provider.dart'; import 'package:mona/data/providers/medication_schedule_provider.dart'; import 'package:mona/i18n/build_context_extensions.dart'; @@ -14,9 +16,13 @@ import 'package:mona/services/notification_service.dart'; import 'package:mona/services/preferences_service.dart'; import 'package:mona/theme/app_theme_controller.dart'; import 'package:mona/theme/material_ui_theme.dart'; +import 'package:mona/ui/views/home/take_medication_page.dart'; import 'package:provider/provider.dart'; import 'ui/views/main_page.dart'; +const bool _forceDefaultColors = bool.fromEnvironment( + 'SCREENSHOT_DEFAULT_COLORS'); // avoid dynamic colors when generating store screenshots + class MonaApp extends StatefulWidget { const MonaApp({super.key}); @@ -25,7 +31,9 @@ class MonaApp extends StatefulWidget { } class _MonaAppState extends State with WidgetsBindingObserver { + final GlobalKey _navigatorKey = GlobalKey(); String? _lastTimeZone; + NotificationPayload? _pendingLaunchTap; late MedicationScheduleProvider _medicationScheduleProvider; late MedicationIntakeProvider _medicationIntakeProvider; late PreferencesService _preferencesService; @@ -41,7 +49,7 @@ class _MonaAppState extends State with WidgetsBindingObserver { _lastTimeZone = clock.now().timeZoneOffset.toString(); WidgetsBinding.instance.addPostFrameCallback((_) async { - await NotificationService().initialize(); + await NotificationService().initialize(onTap: _handleNotificationTap); if (!mounted) return; _medicationScheduleProvider = context.read(); _medicationIntakeProvider = context.read(); @@ -62,6 +70,11 @@ class _MonaAppState extends State with WidgetsBindingObserver { _medicationIntakeProvider.addListener(_regenerateHomeWidget); _localeProvider.addListener(_regenerateHomeWidget); _regenerateHomeWidget(); + + _medicationScheduleProvider.addListener(_routeLaunchTap); + _medicationIntakeProvider.addListener(_routeLaunchTap); + _pendingLaunchTap = await NotificationService().getAppLaunchTap(); + _routeLaunchTap(); }); } @@ -74,6 +87,8 @@ class _MonaAppState extends State with WidgetsBindingObserver { _preferencesService.removeListener(_regenerateNotifications); _medicationIntakeProvider.removeListener(_regenerateHomeWidget); _localeProvider.removeListener(_regenerateHomeWidget); + _medicationScheduleProvider.removeListener(_routeLaunchTap); + _medicationIntakeProvider.removeListener(_routeLaunchTap); } super.dispose(); } @@ -94,6 +109,42 @@ class _MonaAppState extends State with WidgetsBindingObserver { ); } + void _routeLaunchTap() { + final tap = _pendingLaunchTap; + if (tap == null || !_initialized || !mounted) return; + if (_medicationScheduleProvider.isLoading || + _medicationIntakeProvider.isLoading) { + return; + } + + _pendingLaunchTap = null; + _handleNotificationTap(tap); + } + + void _handleNotificationTap(NotificationPayload payload) { + if (!_initialized || !mounted) return; + + final slots = SlotsBuilder( + _medicationIntakeProvider, + _medicationScheduleProvider, + ).intakeSlots(); + final target = findSlot(payload.scheduleId, payload.scheduledTime, slots); + + if (target != null) { + _navigatorKey.currentState?.push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => + TakeMedicationPage(target.schedule, scheduledTime: target.time), + ), + ); + } + + final notificationId = payload.notificationId; + if (notificationId != null) NotificationService().cancel(notificationId); + if (payload.isRepeating) _regenerateNotifications(); + } + void _checkTimezoneChange() { final currentTimezone = clock.now().timeZoneOffset.toString(); if (_lastTimeZone != currentTimezone) { @@ -117,10 +168,11 @@ class _MonaAppState extends State with WidgetsBindingObserver { return DynamicColorBuilder( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { final themes = context.read().buildThemeData( - systemLight: lightDynamic, - systemDark: darkDynamic, + systemLight: _forceDefaultColors ? null : lightDynamic, + systemDark: _forceDefaultColors ? null : darkDynamic, ); return MaterialApp( + navigatorKey: _navigatorKey, title: 'Mona', locale: context.watch().locale, supportedLocales: context.watch().supportedLocales, diff --git a/lib/controllers/medication_intake_manager.dart b/lib/controllers/medication_intake_manager.dart index e0d871c55..761f30977 100644 --- a/lib/controllers/medication_intake_manager.dart +++ b/lib/controllers/medication_intake_manager.dart @@ -3,7 +3,9 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:mona/controllers/supply_item_manager.dart'; +import 'package:mona/data/model/administration_route.dart'; import 'package:mona/data/model/generic_supply_item.dart'; +import 'package:mona/data/model/injection_type.dart'; import 'package:mona/data/model/medication_schedule.dart'; import 'package:mona/data/model/medication_supply_item.dart'; import 'package:mona/data/model/placement.dart'; @@ -33,6 +35,7 @@ class MedicationIntakeManager { String? notes, Decimal? wastedAmount, // in mL List placements = const [], + InjectionType? injectionType, }) async { if (!takenDateTime.isUtc) { throw ArgumentError('takenDateTime must be in UTC'); @@ -57,6 +60,7 @@ class MedicationIntakeManager { deadSpace: deadSpace, placements: placements, dosingBasis: schedule.dosingBasis, + injectionType: injectionType, )); final itemManager = SupplyItemManager(_supplyItemProvider); @@ -111,6 +115,7 @@ class MedicationIntakeManager { List genericItems = const [], String? notes, List placements = const [], + InjectionType? injectionType, }) async { if (!takenDateTime.isUtc) { throw ArgumentError('takenDateTime must be in UTC'); @@ -173,6 +178,7 @@ class MedicationIntakeManager { genericSupplyItemIds: genericItems.map((item) => item.id).toList(), notes: notes, placements: placements, + injectionType: injectionType, )); } @@ -205,6 +211,18 @@ class MedicationIntakeManager { Placement? suggestNextPlacement({required int scheduleId}) => getOrderedPlacements(scheduleId: scheduleId).firstOrNull; + InjectionType? suggestInjectionType({required int scheduleId}) { + final lastIntake = + _medicationIntakeProvider.getLastTakenIntakeForSchedule(scheduleId); + + if (lastIntake == null || + lastIntake.administrationRoute != AdministrationRoute.injection) { + return null; + } + + return lastIntake.injectionType; + } + MedicationSupplyItem? suggestMedicationItem({ required MedicationSchedule schedule, }) { diff --git a/lib/controllers/notification_scheduler.dart b/lib/controllers/notification_scheduler.dart index 78d3215e4..0df655499 100644 --- a/lib/controllers/notification_scheduler.dart +++ b/lib/controllers/notification_scheduler.dart @@ -54,6 +54,7 @@ class NotificationScheduler { final dateFormat = DateFormat.MMMMd(localeName); return NotificationService().scheduleNotification( id: _notificationIdFor(plan.schedule.id, plan.dateTime), + scheduleId: plan.schedule.id, title: t.notificationMedicationReminderTitle( scheduleName: plan.schedule.name), body: t.notificationMedicationReminderBodyDate( @@ -70,6 +71,7 @@ class NotificationScheduler { final timeFormat = DateFormat.Hm(localeName); return NotificationService().scheduleDailyNotification( id: _notificationIdFor(plan.schedule.id, plan.firstFire), + scheduleId: plan.schedule.id, title: t.notificationMedicationReminderTitle( scheduleName: plan.schedule.name), body: t.notificationMedicationReminderBodyTime( @@ -86,6 +88,7 @@ class NotificationScheduler { final weekdayFormat = DateFormat.EEEE(localeName); return NotificationService().scheduleWeeklyNotification( id: _notificationIdFor(plan.schedule.id, plan.firstFire), + scheduleId: plan.schedule.id, title: t.notificationMedicationReminderTitle( scheduleName: plan.schedule.name), body: t.notificationMedicationReminderBodyWeekday( diff --git a/lib/controllers/slot_finder.dart b/lib/controllers/slot_finder.dart new file mode 100644 index 000000000..3cdd69cf3 --- /dev/null +++ b/lib/controllers/slot_finder.dart @@ -0,0 +1,22 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:mona/data/model/intake_slot.dart'; + +IntakeSlot? findSlot( + int scheduleId, + DateTime scheduledTime, + List slots, +) { + final targetSlots = + slots.where((slot) => slot.schedule.id == scheduleId).toList(); + if (targetSlots.isEmpty) return null; + + final slot = targetSlots.length == 1 + ? targetSlots.single + : targetSlots.firstWhereOrNull( + (slot) => slot.time == TimeOfDay.fromDateTime(scheduledTime)); + if (slot == null) return null; + if (slot.intake != null) return null; + + return slot; +} diff --git a/lib/data/model/injection_type.dart b/lib/data/model/injection_type.dart new file mode 100644 index 000000000..baf5c92d9 --- /dev/null +++ b/lib/data/model/injection_type.dart @@ -0,0 +1,9 @@ +import 'package:dart_mappable/dart_mappable.dart'; + +part 'injection_type.mapper.dart'; + +@MappableEnum() +enum InjectionType { + intramuscular, + subcutaneous, +} diff --git a/lib/data/model/injection_type.mapper.dart b/lib/data/model/injection_type.mapper.dart new file mode 100644 index 000000000..27e4f3526 --- /dev/null +++ b/lib/data/model/injection_type.mapper.dart @@ -0,0 +1,55 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format off +// ignore_for_file: type=lint +// ignore_for_file: invalid_use_of_protected_member +// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member +// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter + +part of 'injection_type.dart'; + +class InjectionTypeMapper extends EnumMapper { + InjectionTypeMapper._(); + + static InjectionTypeMapper? _instance; + static InjectionTypeMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = InjectionTypeMapper._()); + } + return _instance!; + } + + static InjectionType fromValue(dynamic value) { + ensureInitialized(); + return MapperContainer.globals.fromValue(value); + } + + @override + InjectionType decode(dynamic value) { + switch (value) { + case r'intramuscular': + return InjectionType.intramuscular; + case r'subcutaneous': + return InjectionType.subcutaneous; + default: + throw MapperException.unknownEnumValue(value); + } + } + + @override + dynamic encode(InjectionType self) { + switch (self) { + case InjectionType.intramuscular: + return r'intramuscular'; + case InjectionType.subcutaneous: + return r'subcutaneous'; + } + } +} + +extension InjectionTypeMapperExtension on InjectionType { + String toValue() { + InjectionTypeMapper.ensureInitialized(); + return MapperContainer.globals.toValue(this) as String; + } +} diff --git a/lib/data/model/medication_intake.dart b/lib/data/model/medication_intake.dart index 47b4229aa..04276278a 100644 --- a/lib/data/model/medication_intake.dart +++ b/lib/data/model/medication_intake.dart @@ -7,6 +7,7 @@ import 'package:mona/data/model/custom_mappers.dart'; import 'package:mona/data/model/date.dart'; import 'package:mona/data/model/dosing_basis.dart'; import 'package:mona/data/model/ester.dart'; +import 'package:mona/data/model/injection_type.dart'; import 'package:mona/data/model/mapping_hooks.dart'; import 'package:mona/data/model/molecule.dart'; import 'package:mona/data/model/placement.dart'; @@ -44,6 +45,7 @@ class MedicationIntake with MedicationIntakeMappable { @MappableField(hook: JsonStringHook()) final List placements; final DosingBasis dosingBasis; + final InjectionType? injectionType; MedicationIntake({ int? id, @@ -62,6 +64,7 @@ class MedicationIntake with MedicationIntakeMappable { this.notes, this.placements = const [], required this.dosingBasis, + this.injectionType, }) : id = id ?? clock.now().millisecondsSinceEpoch { if (takenDateTime != null && !takenDateTime!.isUtc) { throw ArgumentError('takenDateTime must be UTC'); diff --git a/lib/data/model/medication_intake.mapper.dart b/lib/data/model/medication_intake.mapper.dart index 116097286..2bf34fdf1 100644 --- a/lib/data/model/medication_intake.mapper.dart +++ b/lib/data/model/medication_intake.mapper.dart @@ -24,6 +24,7 @@ class MedicationIntakeMapper extends ClassMapperBase { EsterMapper.ensureInitialized(); PlacementMapper.ensureInitialized(); DosingBasisMapper.ensureInitialized(); + InjectionTypeMapper.ensureInitialized(); } return _instance!; } @@ -131,6 +132,12 @@ class MedicationIntakeMapper extends ClassMapperBase { 'dosingBasis', _$dosingBasis, ); + static InjectionType? _$injectionType(MedicationIntake v) => v.injectionType; + static const Field _f$injectionType = Field( + 'injectionType', + _$injectionType, + opt: true, + ); @override final MappableFields fields = const { @@ -150,6 +157,7 @@ class MedicationIntakeMapper extends ClassMapperBase { #notes: _f$notes, #placements: _f$placements, #dosingBasis: _f$dosingBasis, + #injectionType: _f$injectionType, }; static MedicationIntake _instantiate(DecodingData data) { @@ -170,6 +178,7 @@ class MedicationIntakeMapper extends ClassMapperBase { notes: data.dec(_f$notes), placements: data.dec(_f$placements), dosingBasis: data.dec(_f$dosingBasis), + injectionType: data.dec(_f$injectionType), ); } @@ -256,6 +265,7 @@ abstract class MedicationIntakeCopyWith<$R, $In extends MedicationIntake, $Out> String? notes, List? placements, DosingBasis? dosingBasis, + InjectionType? injectionType, }); MedicationIntakeCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t, @@ -305,6 +315,7 @@ class _MedicationIntakeCopyWithImpl<$R, $Out> Object? notes = $none, List? placements, DosingBasis? dosingBasis, + Object? injectionType = $none, }) => $apply( FieldCopyWithData({ @@ -327,6 +338,7 @@ class _MedicationIntakeCopyWithImpl<$R, $Out> if (notes != $none) #notes: notes, if (placements != null) #placements: placements, if (dosingBasis != null) #dosingBasis: dosingBasis, + if (injectionType != $none) #injectionType: injectionType, }), ); @override @@ -356,6 +368,7 @@ class _MedicationIntakeCopyWithImpl<$R, $Out> notes: data.get(#notes, or: $value.notes), placements: data.get(#placements, or: $value.placements), dosingBasis: data.get(#dosingBasis, or: $value.dosingBasis), + injectionType: data.get(#injectionType, or: $value.injectionType), ); @override diff --git a/lib/i18n/en-GB.i18next.json b/lib/i18n/en-GB.i18next.json index ae2d7b139..3687c76e6 100644 --- a/lib/i18n/en-GB.i18next.json +++ b/lib/i18n/en-GB.i18next.json @@ -74,7 +74,7 @@ "reportBugDescription": "Open an issue on GitHub", "translateApp": "Translate the app", "translateAppDescription": "Help translate Mona on Weblate", - "languageMissing": "Is your language missing?", + "missingTranslation": "Is a translation missing?", "donate": "Donate", "donateDescription": "Support Mona on Ko-fi", "backupSaved": "Backup saved", @@ -159,6 +159,9 @@ "intakeRecorded": "Intake recorded", "needleDeadSpace": "Needle dead space", "notes": "Notes", + "injectionType": "Injection type", + "intramuscular": "Intramuscular", + "subcutaneous": "Subcutaneous", "microliters": "μL", "milliliters": "mL", "empty_levels": "Add a blood test or log an estradiol injection to get started", diff --git a/lib/i18n/en.i18next.json b/lib/i18n/en.i18next.json index 9e0e3a06b..eacccbfff 100644 --- a/lib/i18n/en.i18next.json +++ b/lib/i18n/en.i18next.json @@ -74,7 +74,7 @@ "reportBugDescription": "Open an issue on GitHub", "translateApp": "Translate the app", "translateAppDescription": "Help translate Mona on Weblate", - "languageMissing": "Is your language missing?", + "missingTranslation": "Is a translation missing?", "donate": "Donate", "donateDescription": "Support Mona on Ko-fi", "backupSaved": "Backup saved", @@ -159,6 +159,9 @@ "intakeRecorded": "Intake recorded", "needleDeadSpace": "Needle dead space", "notes": "Notes", + "injectionType": "Injection type", + "intramuscular": "Intramuscular", + "subcutaneous": "Subcutaneous", "microliters": "μL", "milliliters": "mL", "empty_levels": "Add a blood test or log an estradiol injection to get started", diff --git a/lib/i18n/fr.i18next.json b/lib/i18n/fr.i18next.json index c57dacccc..428d29fc8 100644 --- a/lib/i18n/fr.i18next.json +++ b/lib/i18n/fr.i18next.json @@ -79,7 +79,7 @@ "reportBugDescription": "Ouvrir une issue sur GitHub", "translateApp": "Traduire l'application", "translateAppDescription": "Aider à traduire Mona sur Weblate", - "languageMissing": "Il manque votre langue ?", + "missingTranslation": "Il manque une traduction ?", "donate": "Faire un don", "donateDescription": "Soutenir Mona sur Ko-fi", "backupSaved": "Sauvegarde enregistrée", @@ -169,6 +169,9 @@ "intakeRecorded": "Prise enregistrée", "needleDeadSpace": "Espace mort de l'aiguille", "notes": "Notes", + "injectionType": "Type d'injection", + "intramuscular": "Intramusculaire", + "subcutaneous": "Sous-cutanée", "microliters": "μL", "milliliters": "mL", "empty_levels": "Ajoutez une prise de sang ou enregistrez une injection d'estradiol pour commencer", diff --git a/lib/i18n/helpers/injection_type_l10n.dart b/lib/i18n/helpers/injection_type_l10n.dart new file mode 100644 index 000000000..dbb445c53 --- /dev/null +++ b/lib/i18n/helpers/injection_type_l10n.dart @@ -0,0 +1,9 @@ +import 'package:mona/data/model/injection_type.dart'; +import 'package:mona/i18n/translations.g.dart'; + +extension InjectionTypeL10n on InjectionType { + String get localizedName => switch (this) { + InjectionType.intramuscular => t.intramuscular, + InjectionType.subcutaneous => t.subcutaneous, + }; +} diff --git a/lib/i18n/hi.i18next.json b/lib/i18n/hi.i18next.json index e7e993e5f..d95ae3523 100644 --- a/lib/i18n/hi.i18next.json +++ b/lib/i18n/hi.i18next.json @@ -67,7 +67,7 @@ "autoUpdate": "Apne aap update", "autoUpdateDescription": "Apne aap check jab naye updates launch ho jayenge", "checkForUpdates": "Updates dekhe", - "checkForUpdatesDescription": "Naye version ke liye manually check kare\\n yeh aapko internet se connect karega\\(Koi data nahi milega)", + "checkForUpdatesDescription": "Naye version ke liye manually check kare\\n yeh aapko internet se connect karega\\n(Koi data nahi milega)", "appVersion": "मोना संस्करण {{version}}", "backupSaved": "बैकअप बचाया", "exportFailed": "निर्यात करने में विफल: {{error}}", @@ -119,5 +119,199 @@ "onHrtForDays_one": "{{count}} दिनों के लिए HRT पर", "onHrtForDays_other": "{{count}} दिनों के लिए HRT पर", "onHrtForMonths_one": "{{count}} महीनों के लिए HRT पर", - "onHrtForMonths_other": "{{count}} महीनों के लिए HRT पर" -} \ No newline at end of file + "onHrtForMonths_other": "{{count}} महीनों के लिए HRT पर", + "onHrtForWeeks_one": "HRT on {{count}} week", + "onHrtForWeeks_other": "HRT on {{count}} week", + "onHrtForYears_one": "HRT पर {{count}} वर्ष", + "onHrtForYears_other": "HRT पर {{count}} वर्ष", + "intakesLoggedCount_one": "{{count}} प्रवेश लॉग इन", + "intakesLoggedCount_other": "{{count}} प्रवेश लॉग इन", + "HrtCounter": "HRT पर समय", + "HrtCounterDescription": "जब तक आप एचआरटी और आपके कुल सेवन पर रहे हैं, तब तक दिखाएं", + "hrtWidgetPlaceholder": "अपना पहला सेवन लॉग इन करने के लिए मोना खोलें", + "hrtWidgetPreviewSample": "8 महीने के लिए HRT पर", + "hrtWidgetPreviewIntakeSample": "16 सेवन लॉग इन", + "startOfDayDescription": "समय पहले {{time}} पिछले दिन की ओर गिनती", + "chooseSchedule": "एक अनुसूची चुनें", + "addSchedulesFirst": "पहले शेड्यूल जोड़ें।।", + "editIntake": "सेवन संपादित करें", + "date": "तारीख", + "amount": "राशि", + "takenAmount": "ली गई राशि", + "wastedAmount": "अपशिष्ट राशि", + "none": "कोई नहीं", + "startOfDay": "दिन की शुरुआत", + "supplyItem": "आपूर्ति आइटम", + "chooseItem": "एक आइटम चुनें", + "noItemsToAdd": "उपलब्ध नहीं है", + "injectionSide": "इंजेक्शन साइड", + "deleteIntake": "इस सेवन को हटा दें?", + "takeMedication": "{{scheduleName}}", + "takeIntake": "सेवन करें", + "intakeRecorded": "दर्ज करना", + "needleDeadSpace": "सुई मृत अंतरिक्ष", + "notes": "नोट", + "microliters": "μL", + "milliliters": "एमएल", + "empty_levels": "एक रक्त परीक्षण जोड़ें या शुरू करने के लिए एक एस्ट्राडियोल इंजेक्शन लॉग इन करें", + "bloodTestsTitle": "रक्त परीक्षण", + "estradiolLevelsTitle": "एस्ट्राडियोल स्तर", + "week": "डब्ल्यू", + "twoWeeks": "2 डब्ल्यू", + "threeMonths": "3 एम", + "sixMonths": "6 एम", + "month": "एम", + "year": "वाई", + "empty_blood_tests": "रक्त परीक्षण यहाँ दिखाई देगा। ऐड बटन का उपयोग करके शुरू करें!", + "addBloodTest": "रक्त परीक्षण जोड़ें", + "editBloodTest": "रक्त परीक्षण", + "newBloodTest": "न्यू ब्लड टेस्ट", + "deleteBloodTest": "इस रक्त परीक्षण को हटा दें?", + "estradiolLevelLabel": "एस्ट्राडियोल स्तर", + "testosteroneLevelLabel": "टेस्टोस्टेरोन का स्तर", + "bloodTestDateLabel": "परीक्षा तिथि", + "chartNowConcentration": "अब {{value}}", + "chartBloodTestLevelTooltip": "{{date}}: {{level}}", + "chartLevelTooltip": "{{date}}: {{level}}", + "empty_supplies": "कोई आपूर्ति नहीं। शुरू करने के लिए एक आइटम जोड़ें।।", + "newItem": "नया आइटम", + "adminRoute": "प्रशासन मार्ग", + "totalAmount": "कुल राशि", + "concentration": "एकाग्रता", + "dosePerUnitLabel": "प्रति खुराक {{unit}}", + "editItem": "आइटम जोड़ें", + "usedAmount": "प्रयुक्त राशि", + "deleteItem": "{{name}}?", + "remaining_one": "{{count}} {{unit}} शेष", + "remaining_other": "{{count}} {{unit}} शेष", + "allItemsFilter": "सब", + "medicationItemsFilter": "दवा", + "genericItems": "उपभोग्य", + "medicationItemType": "दवा", + "genericItemType": "उपभोग्य", + "supplyType": "प्रकार", + "syringe": "सिरिंज", + "wipe": "वाइप्स", + "needle": "सुई", + "gloves": "दस्ताने", + "bandage": "बंधन", + "syringeRemaining_one": "{{count}} syringes शेष", + "syringeRemaining_other": "{{count}} syringes शेष", + "wipeRemaining_one": "{{count}} शेष पोंछे", + "wipeRemaining_other": "{{count}} शेष पोंछे", + "needleRemaining_one": "{{count}} सुई शेष", + "needleRemaining_other": "{{count}} सुई शेष", + "glovesRemaining_one": "{{count}} दस्ताने शेष", + "glovesRemaining_other": "{{count}} दस्ताने शेष", + "bandageRemaining_one": "{{count}} बैंडेज शेष", + "bandageRemaining_other": "{{count}} बैंडेज शेष", + "add": "जोड़ें", + "save": "सहेजें", + "cancel": "रद्द करना", + "next": "अगला", + "delete": "डिलीट", + "deleteElement": "इस मद को हटाएं?", + "irreversibleAction": "यह कार्रवाई नहीं हो सकती है।।", + "name": "नाम", + "molecule": "मोलेकुल", + "ester": "एस्टर", + "estradiol": "एस्ट्राडियोल", + "progesterone": "प्रोजेस्टेरोन", + "testosterone": "टेस्टोस्टेरोन", + "nandrolone": "नांद्र", + "dihydrotestosterone": "Dihydrotestosterone", + "spironolactone": "Spironolactone", + "cyproteroneAcetate": "Cyproterone एसीटेट", + "leuprorelinAcetate": "ल्यूप्रोरेलिन एसीटेट", + "bicalutamide": "Bicalutamide", + "decapeptyl": "Decapeptyl", + "raloxifene": "रालॉक्सीफेन", + "tamoxifen": "टैमोक्सीफेन", + "finasteride": "फिनस्टराइड", + "dutasteride": "Dutasteride", + "minoxidil": "मिनोक्सिडिल", + "pioglitazone": "Pioglitazone", + "enanthate": "Enanthate", + "valerate": "वैलेरेट", + "cypionate": "साइटमैप", + "undecylate": "Undecylate", + "benzoate": "बेंजोएट", + "cypionateSuspension": "Cypionate निलंबन", + "medicationEstradiolEnanthate": "Estradiol enanthate", + "medicationEstradiolValerate": "Estradiol valerate", + "medicationEstradiolCypionate": "Estradiol cypionate", + "medicationEstradiolUndecylate": "Estradiol undecylate", + "medicationEstradiolBenzoate": "Estradiol बेंजोएट", + "medicationEstradiolCypionateSuspension": "Estradiol cypionate निलंबन", + "medicationTestosteroneEnanthate": "टेस्टोस्टेरोन enanthate", + "medicationTestosteroneValerate": "टेस्टोस्टेरोन valerate", + "medicationTestosteroneCypionate": "टेस्टोस्टेरोन cypionate", + "medicationTestosteroneUndecylate": "टेस्टोस्टेरोन undecylate", + "medicationTestosteroneBenzoate": "टेस्टोस्टेरोन benzoate", + "medicationTestosteroneCypionateSuspension": "टेस्टोस्टेरोन साइपीओनेट निलंबन", + "injection": "इंजेक्शन", + "oral": "मौखिक", + "sublingual": "बहुभाषी", + "patch": "पैच", + "gel": "जेल", + "implant": "प्रत्यारोपण", + "suppository": "सपोसिटरी", + "transdermalSpray": "ट्रांसडर्मल स्प्रे", + "transdermalDrops": "ट्रांसडर्मल ड्रॉप", + "deliveryForm": "फॉर्म", + "deliveryFormPump": "पम्प", + "deliveryFormSachet": "साचेत", + "deliveryFormGram": "ट्यूब", + "unitMilligram": "मिलीग्राम", + "unitMicrogramPerDay": "μg / दिन", + "unitPgPerMl": "pg/ml", + "unitPmolPerL": "pmol", + "unitNgPerDl": "ng/dl", + "unitNmolPerL": "nmol/L", + "unitNgPerMl": "एनजी / एमएल", + "administrationRouteUnitMl_one": "एमएल", + "administrationRouteUnitMl_other": "एमएल", + "administrationRouteUnitPill_one": "गोलियाँ", + "administrationRouteUnitPill_other": "गोलियाँ", + "administrationRouteUnitPatch_one": "पैच", + "administrationRouteUnitPatch_other": "पैच", + "administrationRouteUnitPump_one": "पंप", + "administrationRouteUnitPump_other": "पंप", + "administrationRouteUnitSachet_one": "पाउच", + "administrationRouteUnitSachet_other": "पाउच", + "administrationRouteUnitGram_one": "ग्राम", + "administrationRouteUnitGram_other": "ग्राम", + "administrationRouteUnitImplant_one": "प्रत्यारोपण", + "administrationRouteUnitImplant_other": "प्रत्यारोपण", + "administrationRouteUnitSuppository_one": "सट्टा", + "administrationRouteUnitSuppository_other": "सट्टा", + "administrationRouteUnitSpray_one": "स्प्रे", + "administrationRouteUnitSpray_other": "स्प्रे", + "injectionSideLeft": "बाएं", + "injectionSideRight": "अधिकार", + "placementLeft": "बाएं तरफ", + "placementRight": "सही पक्ष", + "placementLeftThigh": "बाएं जांघ", + "placementRightThigh": "सही जांघ", + "placementLeftArm": "बाएं हाथ", + "placementRightArm": "दाहिने हाथ", + "placementLeftButtock": "बाएं बटॉक", + "placementRightButtock": "राइट बटॉक", + "placementLeftAbdomen": "बाएं पेट", + "placementRightAbdomen": "दाहिने पेट", + "applicationSites": "अनुप्रयोग साइट", + "applicationSitesDescription": "उन साइटों को प्रबंधित करें जो आप के बीच घूमते हैं", + "applicationSitesInstructions": "उन साइटों को प्रबंधित करें जो आप के बीच घूमते हैं। साइट्स को आपके सेवन इतिहास के आधार पर सुझाव दिया जाता है। फिर से व्यवस्थित करने के लिए लंबे प्रेस।।", + "addApplicationSite": "साइट जोड़ें", + "customSiteLabel": "कस्टम साइट नाम", + "noApplicationSitesYet": "अभी तक कोई साइट नहीं", + "addSiteToGetStarted": "शुरू करने के लिए नीचे एक साइट जोड़ें।।", + "placementSuggestionPerScheduleTitle": "प्रति अनुसूची सुझाव", + "placementSuggestionPerScheduleDescription": "इस कार्यक्रम के इतिहास पर अगले साइट सुझाव को बेस करें।।", + "requiredField": "आवश्यक फ़ील्ड", + "mustBePositiveNumber": "एक सकारात्मक संख्या होना चाहिए", + "mustBeBetween1And28": "1 और 28 के बीच होना चाहिए", + "mustBeAtMost": "{{max}}", + "invalidTotalAmount": "कुल राशि", + "cannotExceedTotalCapacity": "कुल क्षमता से अधिक नहीं" +} diff --git a/lib/i18n/hu.i18next.json b/lib/i18n/hu.i18next.json new file mode 100644 index 000000000..6c5d56c68 --- /dev/null +++ b/lib/i18n/hu.i18next.json @@ -0,0 +1,87 @@ +{ + "appTitle": "Mona", + "nav_home": "Mona", + "nav_intakes": "Bevevések", + "nav_levels": "Szintek", + "nav_supplies": "Készlet", + "takeAnIntake": "Bevevés felírása", + "addAnItem": "Elem hozzáadása", + "empty_home": "Kezdje egy időbeosztás hozzáadásával a Beállításokban", + "allDone": "Kész!", + "noIntakesDue": "Nincs mára felírt adag", + "upcoming": "Következő", + "asNeeded": "Ahogy szükséges", + "taken": "Bevéve", + "daysAgoCount_one": "{{count}} napja", + "daysAgoCount_other": "{{count}} napja", + "yesterday": "tegnap", + "inDaysCount_one": "{{count}} nap múlva", + "inDaysCount_other": "{{count}} nap múlva", + "tomorrow": "holnap", + "lastTaken": "Utolsó bevevés", + "neverTakenYet": "Nincs bevevés", + "scheduleFrequencyDaily": "Naponta", + "scheduleFrequencyDailyDescription": "Minden nap, specifikus időben", + "scheduleFrequencyEveryNDays_one": "Minden nap", + "scheduleFrequencyEveryNDays_other": "{{count}} naponta", + "scheduleFrequencyInterval": "Intervallum", + "scheduleFrequencyIntervalDescription": "Néhány naponta", + "scheduleFrequencyWeekly": "Hetente", + "scheduleFrequencyMonthly": "Havonta", + "scheduleFrequencyMonthlyDescription": "Minden hónap ugyanazon napján", + "scheduleFrequencyOnDayEveryNMonths_one": "Minden hónap {{day}}.-je", + "scheduleFrequencyOnDayEveryNMonths_other": "{{day}}.-án/én, minden {{count}}. -dik hónapban", + "scheduleFrequencyAsNeeded": "Szükség szerint", + "scheduleFrequencyAsNeededDescription": "Nincs rögzített ütemezés", + "newUpdateAvailable": "Új frissítés érhető el!", + "settingsTitle": "Beállítások", + "notifications": "Értesítések", + "schedulesAndNotifications": "Ütemezések és értesítések", + "general": "Általános", + "schedules": "Ütemezések", + "noSchedules": "Nincsen ütemezés", + "schedulesCreated_one": "{{count}} létrehozva", + "schedulesCreated_other": "{{count}} létrehozva", + "language": "Nyelv", + "languageFollowDevice": "Használja az eszköz nyelvét", + "enableNotifications": "Érdesítések bekapcsolása", + "enableNotificationsDescription": "Küldj emlékeztetőket", + "anchorToLastIntake": "Újraszámítás a legútóbbi bevitel alapján", + "scheduleFrequencyWeeklyDescription": "A hét egy napján", + "goToSettings": "Menj a beállításokhoz", + "anchorToLastIntakeDescription": "Ütemezi a következő bevitelt egy teljes intervallum után a legutóbbi bevitel óta", + "notificationsDisabledTitle": "Az értesítések ki vannak kapcsolva", + "clickToOpenSettings": "Kattintson beállítások megbyitásához", + "exactRemindersDisabled": "A pontos emlékeztetők ki vannak kapcsolva", + "remindersDelayed": "Az emlékeztetők kissé késhetnek. Kattintson beállítások megnyitásahoz.", + "medicalSettings": "Egészségügyi beállítások", + "theme": "Téma", + "themeCustomizeColors": "App színek testreszabása", + "customThemeEnabled": "Saját téma", + "themeGenerate": "Frissítés", + "importFailed": "Importálás sikertelen: {{error}}", + "updates": "Frissítések", + "dataManagement": "Adat Kezelés", + "exportDataTitle": "Adatok Exportálása", + "exportDataSubtitle": "Adatok mentése JSON fájlba", + "units": "Mértékegységek", + "updateNoCompatibleApk": "Az ezközöddel kompatibilis frissítés nem található.", + "updateAppUpToDate": "Az app naprakész!", + "updateCheckNetworkError": "A frissítés ellenőrzése sikertelen.", + "updateDialogTitle": "Új frissítés érhető el", + "updateDialogBody": "A(z) {{latest}} verzió elérhetőve vált! (Jelenlegi: {{current}})\\n\\nEgy ezközöddel kompartibilis frissítés készen áll a telepítésre.", + "updateDownloadAndInstall": "Letöltés és Telepítés", + "updateInstallPermissionRequired": "Engedély szükséges a frissítés telepítéséhez.", + "updateDownloadingTitle": "Frissítés Letöltése...", + "updateFailedOpenInstaller": "A telepítő megnyitása sikertelen volt: {{message}}", + "updateDownloadFailed": "Letöltés sikertelen. Kérem ellenőrizze a kapcsolatát.", + "secretSettings": "Titkos beállítások", + "slimeMode": "Slime mód", + "themeVariant": "Variáns", + "themeContrast": "Kontraszt", + "themeContrastStandard": "Átlagos", + "themeContrastMedium": "Közepes", + "themeContrastHigh": "Magas", + "autoUpdate": "Auto-Frissítés", + "autoUpdateDescription": "Frissítes autómatikus ellenőrzése az app megnyitásakor" +} diff --git a/lib/i18n/id.i18next.json b/lib/i18n/id.i18next.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/lib/i18n/id.i18next.json @@ -0,0 +1 @@ +{} diff --git a/lib/i18n/pt-BR.i18next.json b/lib/i18n/pt-BR.i18next.json index b8dfae0c7..f4254b816 100644 --- a/lib/i18n/pt-BR.i18next.json +++ b/lib/i18n/pt-BR.i18next.json @@ -11,18 +11,18 @@ "noIntakesDue": "Nenhuma dose pendente hoje", "upcoming": "Próximos", "taken": "Tomado", - "daysAgoCount_one": "há {{count}} dias", + "daysAgoCount_one": "há {{count}} dia", "daysAgoCount_many": "há {{count}} dias", "daysAgoCount_other": "há {{count}} dias", "yesterday": "ontem", - "inDaysCount_one": "em {{count}} dias", + "inDaysCount_one": "em {{count}} dia", "inDaysCount_many": "em {{count}} dias", "inDaysCount_other": "em {{count}} dias", "tomorrow": "amanhã", "lastTaken": "Última dose", "neverTakenYet": "Ainda não tomado", - "scheduleFrequencyDaily": "Todos os dias", - "scheduleFrequencyEveryNDays_one": "A cada {{count}} dias", + "scheduleFrequencyDaily": "Diariamente", + "scheduleFrequencyEveryNDays_one": "A cada {{count}} dia", "scheduleFrequencyEveryNDays_many": "A cada {{count}} dias", "scheduleFrequencyEveryNDays_other": "A cada {{count}} dias", "scheduleFrequencyInterval": "Intervalo", @@ -243,7 +243,7 @@ "invalidTotalAmount": "Quantidade total inválida", "cannotExceedTotalCapacity": "Não pode exceder a capacidade total", "scheduleFrequencyMonthly": "Mensal", - "scheduleFrequencyOnDayEveryNMonths_one": "Dia {{day}}, a cada {{count}} mês", + "scheduleFrequencyOnDayEveryNMonths_one": "Dia {{day}}, a cada mês", "scheduleFrequencyOnDayEveryNMonths_many": "Dia {{day}}, a cada {{count}} meses", "scheduleFrequencyOnDayEveryNMonths_other": "Dia {{day}}, a cada {{count}} meses", "anchorToLastIntake": "Recalcular com base na última dose", @@ -305,5 +305,48 @@ "addSiteToGetStarted": "Adicione um local abaixo para começar.", "placementSuggestionPerScheduleTitle": "Sugerir por agendamento", "placementSuggestionPerScheduleDescription": "Baseie a sugestão do próximo local apenas no histórico deste agendamento.", - "mustBeBetween1And28": "Deve estar entre 1 e 28" -} \ No newline at end of file + "mustBeBetween1And28": "Deve estar entre 1 e 28", + "asNeeded": "Conforme necessário", + "scheduleFrequencyDailyDescription": "Todos os dias, em horários específicos", + "scheduleFrequencyIntervalDescription": "A cada poucos dias", + "scheduleFrequencyWeeklyDescription": "Alguns dias da semana", + "scheduleFrequencyMonthlyDescription": "Mesmo dia a cada mês", + "scheduleFrequencyAsNeeded": "Quando necessário", + "scheduleFrequencyAsNeededDescription": "Sem horário pré‐estabelecido", + "backupSaved": "Dados salvo", + "reportBug": "Reportar um bug", + "translateAppDescription": "Ajude a traduzir o mona no Weblate", + "donate": "Faça uma doação", + "translateApp": "Traduzir o aplicativo", + "getInvolved": "Contribua", + "reportBugDescription": "Abra uma issue no Github", + "donateDescription": "Apoie Mona no Ko‐fi", + "missingTranslation": "Tradução faltando?", + "slimeMode": "Modo Slime", + "secretSettings": "Configurações ocultas", + "applicationSites": "Locais de aplicação", + "applicationSitesInstructions": "Gerencie os locais de aplicação alternando entre eles. Os locais são sugeridos com base em seu histórico de doses. Pressione e segure para reordenar.", + "mustBeAtMost": "No máximo {{max}}", + "month": "M", + "year": "A", + "estradiolLevelsTitle": "Níveis de Estradiol", + "week": "S", + "twoWeeks": "2 S", + "threeMonths": "3 M", + "sixMonths": "6 M", + "empty_levels": "Adicione um exame de sangue ou registre uma injeção de estradiol para começar", + "deliveryFormPump": "Pump", + "deliveryFormSachet": "Sachê", + "unitMicrogramPerDay": "µg/dia", + "unitNgPerMl": "ng/mL", + "deliveryForm": "Forma", + "administrationRouteUnitSachet_one": "sachê", + "administrationRouteUnitSachet_many": "sachês", + "administrationRouteUnitSachet_other": "sachês", + "administrationRouteUnitGram_one": "grama", + "administrationRouteUnitGram_many": "gramas", + "administrationRouteUnitGram_other": "gramas", + "injectionType": "Tipo de injeção", + "intramuscular": "Intramuscular", + "subcutaneous": "Subcutânea" +} diff --git a/lib/i18n/translations.g.dart b/lib/i18n/translations.g.dart index 524af0e8d..d5a89daec 100644 --- a/lib/i18n/translations.g.dart +++ b/lib/i18n/translations.g.dart @@ -3,8 +3,8 @@ /// Source: lib/i18n/generated /// To regenerate, run: `dart run slang` /// -/// Locales: 24 -/// Strings: 5909 (246 per locale) +/// Locales: 26 +/// Strings: 6199 (238 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import @@ -21,6 +21,8 @@ import 'translations_et.g.dart' as l_et; import 'translations_fr.g.dart' as l_fr; import 'translations_gl.g.dart' as l_gl; import 'translations_hi.g.dart' as l_hi; +import 'translations_hu.g.dart' as l_hu; +import 'translations_id.g.dart' as l_id; import 'translations_is.g.dart' as l_is; import 'translations_it.g.dart' as l_it; import 'translations_ko.g.dart' as l_ko; @@ -54,6 +56,8 @@ enum AppLocale with BaseAppLocale { fr(languageCode: 'fr'), gl(languageCode: 'gl'), hi(languageCode: 'hi'), + hu(languageCode: 'hu'), + id(languageCode: 'id'), icelandic(languageCode: 'is'), it(languageCode: 'it'), ko(languageCode: 'ko'), @@ -147,6 +151,18 @@ enum AppLocale with BaseAppLocale { cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver, ); + case AppLocale.hu: + return l_hu.TranslationsHu( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.id: + return l_id.TranslationsId( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); case AppLocale.icelandic: return l_is.TranslationsIs( overrides: overrides, diff --git a/lib/i18n/translations_en.g.dart b/lib/i18n/translations_en.g.dart index 02e97a067..109f874c8 100644 --- a/lib/i18n/translations_en.g.dart +++ b/lib/i18n/translations_en.g.dart @@ -251,8 +251,8 @@ class Translations with BaseTranslations { /// en: 'Help translate Mona on Weblate' String get translateAppDescription => 'Help translate Mona on Weblate'; - /// en: 'Is your language missing?' - String get languageMissing => 'Is your language missing?'; + /// en: 'Is a translation missing?' + String get missingTranslation => 'Is a translation missing?'; /// en: 'Donate' String get donate => 'Donate'; @@ -491,6 +491,15 @@ class Translations with BaseTranslations { /// en: 'Notes' String get notes => 'Notes'; + /// en: 'Injection type' + String get injectionType => 'Injection type'; + + /// en: 'Intramuscular' + String get intramuscular => 'Intramuscular'; + + /// en: 'Subcutaneous' + String get subcutaneous => 'Subcutaneous'; + /// en: 'μL' String get microliters => 'μL'; @@ -1179,7 +1188,7 @@ extension on Translations { 'reportBugDescription' => 'Open an issue on GitHub', 'translateApp' => 'Translate the app', 'translateAppDescription' => 'Help translate Mona on Weblate', - 'languageMissing' => 'Is your language missing?', + 'missingTranslation' => 'Is a translation missing?', 'donate' => 'Donate', 'donateDescription' => 'Support Mona on Ko-fi', 'backupSaved' => 'Backup saved', @@ -1270,6 +1279,9 @@ extension on Translations { 'intakeRecorded' => 'Intake recorded', 'needleDeadSpace' => 'Needle dead space', 'notes' => 'Notes', + 'injectionType' => 'Injection type', + 'intramuscular' => 'Intramuscular', + 'subcutaneous' => 'Subcutaneous', 'microliters' => 'μL', 'milliliters' => 'mL', 'empty_levels' => diff --git a/lib/i18n/translations_en_GB.g.dart b/lib/i18n/translations_en_GB.g.dart index 19f95f828..9614c3dad 100644 --- a/lib/i18n/translations_en_GB.g.dart +++ b/lib/i18n/translations_en_GB.g.dart @@ -189,7 +189,7 @@ class TranslationsEnGb extends Translations @override String get translateAppDescription => 'Help translate Mona on Weblate'; @override - String get languageMissing => 'Is your language missing?'; + String get missingTranslation => 'Is a translation missing?'; @override String get donate => 'Donate'; @override @@ -354,6 +354,12 @@ class TranslationsEnGb extends Translations @override String get notes => 'Notes'; @override + String get injectionType => 'Injection type'; + @override + String get intramuscular => 'Intramuscular'; + @override + String get subcutaneous => 'Subcutaneous'; + @override String get microliters => 'μL'; @override String get milliliters => 'mL'; @@ -884,7 +890,7 @@ extension on TranslationsEnGb { 'reportBugDescription' => 'Open an issue on GitHub', 'translateApp' => 'Translate the app', 'translateAppDescription' => 'Help translate Mona on Weblate', - 'languageMissing' => 'Is your language missing?', + 'missingTranslation' => 'Is a translation missing?', 'donate' => 'Donate', 'donateDescription' => 'Support Mona on Ko-fi', 'backupSaved' => 'Backup saved', @@ -975,6 +981,9 @@ extension on TranslationsEnGb { 'intakeRecorded' => 'Intake recorded', 'needleDeadSpace' => 'Needle dead space', 'notes' => 'Notes', + 'injectionType' => 'Injection type', + 'intramuscular' => 'Intramuscular', + 'subcutaneous' => 'Subcutaneous', 'microliters' => 'μL', 'milliliters' => 'mL', 'empty_levels' => diff --git a/lib/i18n/translations_fr.g.dart b/lib/i18n/translations_fr.g.dart index ea972c141..9a0b74504 100644 --- a/lib/i18n/translations_fr.g.dart +++ b/lib/i18n/translations_fr.g.dart @@ -193,7 +193,7 @@ class TranslationsFr extends Translations @override String get translateAppDescription => 'Aider à traduire Mona sur Weblate'; @override - String get languageMissing => 'Il manque votre langue ?'; + String get missingTranslation => 'Il manque une traduction ?'; @override String get donate => 'Faire un don'; @override @@ -363,6 +363,12 @@ class TranslationsFr extends Translations @override String get notes => 'Notes'; @override + String get injectionType => 'Type d\'injection'; + @override + String get intramuscular => 'Intramusculaire'; + @override + String get subcutaneous => 'Sous-cutanée'; + @override String get microliters => 'μL'; @override String get milliliters => 'mL'; @@ -923,7 +929,7 @@ extension on TranslationsFr { 'reportBugDescription' => 'Ouvrir une issue sur GitHub', 'translateApp' => 'Traduire l\'application', 'translateAppDescription' => 'Aider à traduire Mona sur Weblate', - 'languageMissing' => 'Il manque votre langue ?', + 'missingTranslation' => 'Il manque une traduction ?', 'donate' => 'Faire un don', 'donateDescription' => 'Soutenir Mona sur Ko-fi', 'backupSaved' => 'Sauvegarde enregistrée', @@ -1019,6 +1025,9 @@ extension on TranslationsFr { 'intakeRecorded' => 'Prise enregistrée', 'needleDeadSpace' => 'Espace mort de l\'aiguille', 'notes' => 'Notes', + 'injectionType' => 'Type d\'injection', + 'intramuscular' => 'Intramusculaire', + 'subcutaneous' => 'Sous-cutanée', 'microliters' => 'μL', 'milliliters' => 'mL', 'empty_levels' => diff --git a/lib/i18n/translations_hi.g.dart b/lib/i18n/translations_hi.g.dart index 3875c5bbd..abf87aa8f 100644 --- a/lib/i18n/translations_hi.g.dart +++ b/lib/i18n/translations_hi.g.dart @@ -174,7 +174,7 @@ class TranslationsHi extends Translations String get checkForUpdates => 'Updates dekhe'; @override String get checkForUpdatesDescription => - 'Naye version ke liye manually check kare\n yeh aapko internet se connect karega\(Koi data nahi milega)'; + 'Naye version ke liye manually check kare\n yeh aapko internet se connect karega\n(Koi data nahi milega)'; @override String appVersion({required Object version}) => 'मोना संस्करण ${version}'; @override @@ -285,6 +285,336 @@ class TranslationsHi extends Translations @override String get empty_intakes => 'ले लिया गया सेवन यहाँ दिखाई देगा'; @override + String get HrtCounter => 'HRT पर समय'; + @override + String get HrtCounterDescription => + 'जब तक आप एचआरटी और आपके कुल सेवन पर रहे हैं, तब तक दिखाएं'; + @override + String get hrtWidgetPlaceholder => + 'अपना पहला सेवन लॉग इन करने के लिए मोना खोलें'; + @override + String get hrtWidgetPreviewSample => '8 महीने के लिए HRT पर'; + @override + String get hrtWidgetPreviewIntakeSample => '16 सेवन लॉग इन'; + @override + String startOfDayDescription({required Object time}) => + 'समय पहले ${time} पिछले दिन की ओर गिनती'; + @override + String get chooseSchedule => 'एक अनुसूची चुनें'; + @override + String get addSchedulesFirst => 'पहले शेड्यूल जोड़ें।।'; + @override + String get editIntake => 'सेवन संपादित करें'; + @override + String get date => 'तारीख'; + @override + String get amount => 'राशि'; + @override + String get takenAmount => 'ली गई राशि'; + @override + String get wastedAmount => 'अपशिष्ट राशि'; + @override + String get none => 'कोई नहीं'; + @override + String get startOfDay => 'दिन की शुरुआत'; + @override + String get supplyItem => 'आपूर्ति आइटम'; + @override + String get chooseItem => 'एक आइटम चुनें'; + @override + String get noItemsToAdd => 'उपलब्ध नहीं है'; + @override + String get injectionSide => 'इंजेक्शन साइड'; + @override + String get deleteIntake => 'इस सेवन को हटा दें?'; + @override + String takeMedication({required Object scheduleName}) => '${scheduleName}'; + @override + String get takeIntake => 'सेवन करें'; + @override + String get intakeRecorded => 'दर्ज करना'; + @override + String get needleDeadSpace => 'सुई मृत अंतरिक्ष'; + @override + String get notes => 'नोट'; + @override + String get microliters => 'μL'; + @override + String get milliliters => 'एमएल'; + @override + String get empty_levels => + 'एक रक्त परीक्षण जोड़ें या शुरू करने के लिए एक एस्ट्राडियोल इंजेक्शन लॉग इन करें'; + @override + String get bloodTestsTitle => 'रक्त परीक्षण'; + @override + String get estradiolLevelsTitle => 'एस्ट्राडियोल स्तर'; + @override + String get week => 'डब्ल्यू'; + @override + String get twoWeeks => '2 डब्ल्यू'; + @override + String get threeMonths => '3 एम'; + @override + String get sixMonths => '6 एम'; + @override + String get month => 'एम'; + @override + String get year => 'वाई'; + @override + String get empty_blood_tests => + 'रक्त परीक्षण यहाँ दिखाई देगा। ऐड बटन का उपयोग करके शुरू करें!'; + @override + String get addBloodTest => 'रक्त परीक्षण जोड़ें'; + @override + String get editBloodTest => 'रक्त परीक्षण'; + @override + String get newBloodTest => 'न्यू ब्लड टेस्ट'; + @override + String get deleteBloodTest => 'इस रक्त परीक्षण को हटा दें?'; + @override + String get estradiolLevelLabel => 'एस्ट्राडियोल स्तर'; + @override + String get testosteroneLevelLabel => 'टेस्टोस्टेरोन का स्तर'; + @override + String get bloodTestDateLabel => 'परीक्षा तिथि'; + @override + String chartNowConcentration({required Object value}) => 'अब ${value}'; + @override + String chartBloodTestLevelTooltip( + {required Object date, required Object level}) => + '${date}: ${level}'; + @override + String chartLevelTooltip({required Object date, required Object level}) => + '${date}: ${level}'; + @override + String get empty_supplies => + 'कोई आपूर्ति नहीं। शुरू करने के लिए एक आइटम जोड़ें।।'; + @override + String get newItem => 'नया आइटम'; + @override + String get adminRoute => 'प्रशासन मार्ग'; + @override + String get totalAmount => 'कुल राशि'; + @override + String get concentration => 'एकाग्रता'; + @override + String dosePerUnitLabel({required Object unit}) => 'प्रति खुराक ${unit}'; + @override + String get editItem => 'आइटम जोड़ें'; + @override + String get usedAmount => 'प्रयुक्त राशि'; + @override + String deleteItem({required Object name}) => '${name}?'; + @override + String get allItemsFilter => 'सब'; + @override + String get medicationItemsFilter => 'दवा'; + @override + String get genericItems => 'उपभोग्य'; + @override + String get medicationItemType => 'दवा'; + @override + String get genericItemType => 'उपभोग्य'; + @override + String get supplyType => 'प्रकार'; + @override + String get syringe => 'सिरिंज'; + @override + String get wipe => 'वाइप्स'; + @override + String get needle => 'सुई'; + @override + String get gloves => 'दस्ताने'; + @override + String get bandage => 'बंधन'; + @override + String get add => 'जोड़ें'; + @override + String get save => 'सहेजें'; + @override + String get cancel => 'रद्द करना'; + @override + String get next => 'अगला'; + @override + String get delete => 'डिलीट'; + @override + String get deleteElement => 'इस मद को हटाएं?'; + @override + String get irreversibleAction => 'यह कार्रवाई नहीं हो सकती है।।'; + @override + String get name => 'नाम'; + @override + String get molecule => 'मोलेकुल'; + @override + String get ester => 'एस्टर'; + @override + String get estradiol => 'एस्ट्राडियोल'; + @override + String get progesterone => 'प्रोजेस्टेरोन'; + @override + String get testosterone => 'टेस्टोस्टेरोन'; + @override + String get nandrolone => 'नांद्र'; + @override + String get dihydrotestosterone => 'Dihydrotestosterone'; + @override + String get spironolactone => 'Spironolactone'; + @override + String get cyproteroneAcetate => 'Cyproterone एसीटेट'; + @override + String get leuprorelinAcetate => 'ल्यूप्रोरेलिन एसीटेट'; + @override + String get bicalutamide => 'Bicalutamide'; + @override + String get decapeptyl => 'Decapeptyl'; + @override + String get raloxifene => 'रालॉक्सीफेन'; + @override + String get tamoxifen => 'टैमोक्सीफेन'; + @override + String get finasteride => 'फिनस्टराइड'; + @override + String get dutasteride => 'Dutasteride'; + @override + String get minoxidil => 'मिनोक्सिडिल'; + @override + String get pioglitazone => 'Pioglitazone'; + @override + String get enanthate => 'Enanthate'; + @override + String get valerate => 'वैलेरेट'; + @override + String get cypionate => 'साइटमैप'; + @override + String get undecylate => 'Undecylate'; + @override + String get benzoate => 'बेंजोएट'; + @override + String get cypionateSuspension => 'Cypionate निलंबन'; + @override + String get medicationEstradiolEnanthate => 'Estradiol enanthate'; + @override + String get medicationEstradiolValerate => 'Estradiol valerate'; + @override + String get medicationEstradiolCypionate => 'Estradiol cypionate'; + @override + String get medicationEstradiolUndecylate => 'Estradiol undecylate'; + @override + String get medicationEstradiolBenzoate => 'Estradiol बेंजोएट'; + @override + String get medicationEstradiolCypionateSuspension => + 'Estradiol cypionate निलंबन'; + @override + String get medicationTestosteroneEnanthate => 'टेस्टोस्टेरोन enanthate'; + @override + String get medicationTestosteroneValerate => 'टेस्टोस्टेरोन valerate'; + @override + String get medicationTestosteroneCypionate => 'टेस्टोस्टेरोन cypionate'; + @override + String get medicationTestosteroneUndecylate => 'टेस्टोस्टेरोन undecylate'; + @override + String get medicationTestosteroneBenzoate => 'टेस्टोस्टेरोन benzoate'; + @override + String get medicationTestosteroneCypionateSuspension => + 'टेस्टोस्टेरोन साइपीओनेट निलंबन'; + @override + String get injection => 'इंजेक्शन'; + @override + String get oral => 'मौखिक'; + @override + String get sublingual => 'बहुभाषी'; + @override + String get patch => 'पैच'; + @override + String get gel => 'जेल'; + @override + String get implant => 'प्रत्यारोपण'; + @override + String get suppository => 'सपोसिटरी'; + @override + String get transdermalSpray => 'ट्रांसडर्मल स्प्रे'; + @override + String get transdermalDrops => 'ट्रांसडर्मल ड्रॉप'; + @override + String get deliveryForm => 'फॉर्म'; + @override + String get deliveryFormPump => 'पम्प'; + @override + String get deliveryFormSachet => 'साचेत'; + @override + String get deliveryFormGram => 'ट्यूब'; + @override + String get unitMilligram => 'मिलीग्राम'; + @override + String get unitMicrogramPerDay => 'μg / दिन'; + @override + String get unitPgPerMl => 'pg/ml'; + @override + String get unitPmolPerL => 'pmol'; + @override + String get unitNgPerDl => 'ng/dl'; + @override + String get unitNmolPerL => 'nmol/L'; + @override + String get unitNgPerMl => 'एनजी / एमएल'; + @override + String get injectionSideLeft => 'बाएं'; + @override + String get injectionSideRight => 'अधिकार'; + @override + String get placementLeft => 'बाएं तरफ'; + @override + String get placementRight => 'सही पक्ष'; + @override + String get placementLeftThigh => 'बाएं जांघ'; + @override + String get placementRightThigh => 'सही जांघ'; + @override + String get placementLeftArm => 'बाएं हाथ'; + @override + String get placementRightArm => 'दाहिने हाथ'; + @override + String get placementLeftButtock => 'बाएं बटॉक'; + @override + String get placementRightButtock => 'राइट बटॉक'; + @override + String get placementLeftAbdomen => 'बाएं पेट'; + @override + String get placementRightAbdomen => 'दाहिने पेट'; + @override + String get applicationSites => 'अनुप्रयोग साइट'; + @override + String get applicationSitesDescription => + 'उन साइटों को प्रबंधित करें जो आप के बीच घूमते हैं'; + @override + String get applicationSitesInstructions => + 'उन साइटों को प्रबंधित करें जो आप के बीच घूमते हैं। साइट्स को आपके सेवन इतिहास के आधार पर सुझाव दिया जाता है। फिर से व्यवस्थित करने के लिए लंबे प्रेस।।'; + @override + String get addApplicationSite => 'साइट जोड़ें'; + @override + String get customSiteLabel => 'कस्टम साइट नाम'; + @override + String get noApplicationSitesYet => 'अभी तक कोई साइट नहीं'; + @override + String get addSiteToGetStarted => 'शुरू करने के लिए नीचे एक साइट जोड़ें।।'; + @override + String get placementSuggestionPerScheduleTitle => 'प्रति अनुसूची सुझाव'; + @override + String get placementSuggestionPerScheduleDescription => + 'इस कार्यक्रम के इतिहास पर अगले साइट सुझाव को बेस करें।।'; + @override + String get requiredField => 'आवश्यक फ़ील्ड'; + @override + String get mustBePositiveNumber => 'एक सकारात्मक संख्या होना चाहिए'; + @override + String get mustBeBetween1And28 => '1 और 28 के बीच होना चाहिए'; + @override + String mustBeAtMost({required Object max}) => '${max}'; + @override + String get invalidTotalAmount => 'कुल राशि'; + @override + String get cannotExceedTotalCapacity => 'कुल क्षमता से अधिक नहीं'; + @override String daysAgoCount({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( count, @@ -334,6 +664,132 @@ class TranslationsHi extends Translations one: '${count} महीनों के लिए HRT पर', other: '${count} महीनों के लिए HRT पर', ); + @override + String onHrtForWeeks({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'HRT on ${count} week', + other: 'HRT on ${count} week', + ); + @override + String onHrtForYears({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'HRT पर ${count} वर्ष', + other: 'HRT पर ${count} वर्ष', + ); + @override + String intakesLoggedCount({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} प्रवेश लॉग इन', + other: '${count} प्रवेश लॉग इन', + ); + @override + String remaining({required num count, required Object unit}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} ${unit} शेष', + other: '${count} ${unit} शेष', + ); + @override + String syringeRemaining({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} syringes शेष', + other: '${count} syringes शेष', + ); + @override + String wipeRemaining({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} शेष पोंछे', + other: '${count} शेष पोंछे', + ); + @override + String needleRemaining({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} सुई शेष', + other: '${count} सुई शेष', + ); + @override + String glovesRemaining({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} दस्ताने शेष', + other: '${count} दस्ताने शेष', + ); + @override + String bandageRemaining({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} बैंडेज शेष', + other: '${count} बैंडेज शेष', + ); + @override + String administrationRouteUnitMl({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'एमएल', + other: 'एमएल', + ); + @override + String administrationRouteUnitPill({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'गोलियाँ', + other: 'गोलियाँ', + ); + @override + String administrationRouteUnitPatch({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'पैच', + other: 'पैच', + ); + @override + String administrationRouteUnitPump({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'पंप', + other: 'पंप', + ); + @override + String administrationRouteUnitSachet({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'पाउच', + other: 'पाउच', + ); + @override + String administrationRouteUnitGram({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'ग्राम', + other: 'ग्राम', + ); + @override + String administrationRouteUnitImplant({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'प्रत्यारोपण', + other: 'प्रत्यारोपण', + ); + @override + String administrationRouteUnitSuppository({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'सट्टा', + other: 'सट्टा', + ); + @override + String administrationRouteUnitSpray({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'स्प्रे', + other: 'स्प्रे', + ); } /// The flat map containing all translations for locale . @@ -406,7 +862,7 @@ extension on TranslationsHi { 'Apne aap check jab naye updates launch ho jayenge', 'checkForUpdates' => 'Updates dekhe', 'checkForUpdatesDescription' => - 'Naye version ke liye manually check kare\n yeh aapko internet se connect karega\(Koi data nahi milega)', + 'Naye version ke liye manually check kare\n yeh aapko internet se connect karega\n(Koi data nahi milega)', 'appVersion' => ({required Object version}) => 'मोना संस्करण ${version}', 'backupSaved' => 'बैकअप बचाया', 'exportFailed' => ({required Object error}) => @@ -469,6 +925,174 @@ extension on TranslationsHi { 'deleteSchedule' => ({required Object name}) => '${name}?', 'addNotification' => 'अधिसूचना जोड़ें', 'empty_intakes' => 'ले लिया गया सेवन यहाँ दिखाई देगा', + 'HrtCounter' => 'HRT पर समय', + 'HrtCounterDescription' => + 'जब तक आप एचआरटी और आपके कुल सेवन पर रहे हैं, तब तक दिखाएं', + 'hrtWidgetPlaceholder' => 'अपना पहला सेवन लॉग इन करने के लिए मोना खोलें', + 'hrtWidgetPreviewSample' => '8 महीने के लिए HRT पर', + 'hrtWidgetPreviewIntakeSample' => '16 सेवन लॉग इन', + 'startOfDayDescription' => ({required Object time}) => + 'समय पहले ${time} पिछले दिन की ओर गिनती', + 'chooseSchedule' => 'एक अनुसूची चुनें', + 'addSchedulesFirst' => 'पहले शेड्यूल जोड़ें।।', + 'editIntake' => 'सेवन संपादित करें', + 'date' => 'तारीख', + 'amount' => 'राशि', + 'takenAmount' => 'ली गई राशि', + 'wastedAmount' => 'अपशिष्ट राशि', + 'none' => 'कोई नहीं', + 'startOfDay' => 'दिन की शुरुआत', + 'supplyItem' => 'आपूर्ति आइटम', + 'chooseItem' => 'एक आइटम चुनें', + 'noItemsToAdd' => 'उपलब्ध नहीं है', + 'injectionSide' => 'इंजेक्शन साइड', + 'deleteIntake' => 'इस सेवन को हटा दें?', + 'takeMedication' => ({required Object scheduleName}) => '${scheduleName}', + 'takeIntake' => 'सेवन करें', + 'intakeRecorded' => 'दर्ज करना', + 'needleDeadSpace' => 'सुई मृत अंतरिक्ष', + 'notes' => 'नोट', + 'microliters' => 'μL', + 'milliliters' => 'एमएल', + 'empty_levels' => + 'एक रक्त परीक्षण जोड़ें या शुरू करने के लिए एक एस्ट्राडियोल इंजेक्शन लॉग इन करें', + 'bloodTestsTitle' => 'रक्त परीक्षण', + 'estradiolLevelsTitle' => 'एस्ट्राडियोल स्तर', + 'week' => 'डब्ल्यू', + 'twoWeeks' => '2 डब्ल्यू', + 'threeMonths' => '3 एम', + 'sixMonths' => '6 एम', + 'month' => 'एम', + 'year' => 'वाई', + 'empty_blood_tests' => + 'रक्त परीक्षण यहाँ दिखाई देगा। ऐड बटन का उपयोग करके शुरू करें!', + 'addBloodTest' => 'रक्त परीक्षण जोड़ें', + 'editBloodTest' => 'रक्त परीक्षण', + 'newBloodTest' => 'न्यू ब्लड टेस्ट', + 'deleteBloodTest' => 'इस रक्त परीक्षण को हटा दें?', + 'estradiolLevelLabel' => 'एस्ट्राडियोल स्तर', + 'testosteroneLevelLabel' => 'टेस्टोस्टेरोन का स्तर', + 'bloodTestDateLabel' => 'परीक्षा तिथि', + 'chartNowConcentration' => ({required Object value}) => 'अब ${value}', + 'chartBloodTestLevelTooltip' => + ({required Object date, required Object level}) => '${date}: ${level}', + 'chartLevelTooltip' => ({required Object date, required Object level}) => + '${date}: ${level}', + 'empty_supplies' => 'कोई आपूर्ति नहीं। शुरू करने के लिए एक आइटम जोड़ें।।', + 'newItem' => 'नया आइटम', + 'adminRoute' => 'प्रशासन मार्ग', + 'totalAmount' => 'कुल राशि', + 'concentration' => 'एकाग्रता', + 'dosePerUnitLabel' => ({required Object unit}) => 'प्रति खुराक ${unit}', + 'editItem' => 'आइटम जोड़ें', + 'usedAmount' => 'प्रयुक्त राशि', + 'deleteItem' => ({required Object name}) => '${name}?', + 'allItemsFilter' => 'सब', + 'medicationItemsFilter' => 'दवा', + 'genericItems' => 'उपभोग्य', + 'medicationItemType' => 'दवा', + 'genericItemType' => 'उपभोग्य', + 'supplyType' => 'प्रकार', + 'syringe' => 'सिरिंज', + 'wipe' => 'वाइप्स', + 'needle' => 'सुई', + 'gloves' => 'दस्ताने', + 'bandage' => 'बंधन', + 'add' => 'जोड़ें', + 'save' => 'सहेजें', + 'cancel' => 'रद्द करना', + 'next' => 'अगला', + 'delete' => 'डिलीट', + 'deleteElement' => 'इस मद को हटाएं?', + 'irreversibleAction' => 'यह कार्रवाई नहीं हो सकती है।।', + 'name' => 'नाम', + 'molecule' => 'मोलेकुल', + 'ester' => 'एस्टर', + 'estradiol' => 'एस्ट्राडियोल', + 'progesterone' => 'प्रोजेस्टेरोन', + 'testosterone' => 'टेस्टोस्टेरोन', + 'nandrolone' => 'नांद्र', + 'dihydrotestosterone' => 'Dihydrotestosterone', + 'spironolactone' => 'Spironolactone', + 'cyproteroneAcetate' => 'Cyproterone एसीटेट', + 'leuprorelinAcetate' => 'ल्यूप्रोरेलिन एसीटेट', + 'bicalutamide' => 'Bicalutamide', + 'decapeptyl' => 'Decapeptyl', + 'raloxifene' => 'रालॉक्सीफेन', + 'tamoxifen' => 'टैमोक्सीफेन', + 'finasteride' => 'फिनस्टराइड', + 'dutasteride' => 'Dutasteride', + 'minoxidil' => 'मिनोक्सिडिल', + 'pioglitazone' => 'Pioglitazone', + 'enanthate' => 'Enanthate', + 'valerate' => 'वैलेरेट', + 'cypionate' => 'साइटमैप', + 'undecylate' => 'Undecylate', + 'benzoate' => 'बेंजोएट', + 'cypionateSuspension' => 'Cypionate निलंबन', + 'medicationEstradiolEnanthate' => 'Estradiol enanthate', + 'medicationEstradiolValerate' => 'Estradiol valerate', + 'medicationEstradiolCypionate' => 'Estradiol cypionate', + 'medicationEstradiolUndecylate' => 'Estradiol undecylate', + 'medicationEstradiolBenzoate' => 'Estradiol बेंजोएट', + 'medicationEstradiolCypionateSuspension' => 'Estradiol cypionate निलंबन', + 'medicationTestosteroneEnanthate' => 'टेस्टोस्टेरोन enanthate', + 'medicationTestosteroneValerate' => 'टेस्टोस्टेरोन valerate', + 'medicationTestosteroneCypionate' => 'टेस्टोस्टेरोन cypionate', + 'medicationTestosteroneUndecylate' => 'टेस्टोस्टेरोन undecylate', + 'medicationTestosteroneBenzoate' => 'टेस्टोस्टेरोन benzoate', + 'medicationTestosteroneCypionateSuspension' => + 'टेस्टोस्टेरोन साइपीओनेट निलंबन', + 'injection' => 'इंजेक्शन', + 'oral' => 'मौखिक', + 'sublingual' => 'बहुभाषी', + 'patch' => 'पैच', + 'gel' => 'जेल', + 'implant' => 'प्रत्यारोपण', + 'suppository' => 'सपोसिटरी', + 'transdermalSpray' => 'ट्रांसडर्मल स्प्रे', + 'transdermalDrops' => 'ट्रांसडर्मल ड्रॉप', + 'deliveryForm' => 'फॉर्म', + 'deliveryFormPump' => 'पम्प', + 'deliveryFormSachet' => 'साचेत', + 'deliveryFormGram' => 'ट्यूब', + 'unitMilligram' => 'मिलीग्राम', + 'unitMicrogramPerDay' => 'μg / दिन', + 'unitPgPerMl' => 'pg/ml', + 'unitPmolPerL' => 'pmol', + 'unitNgPerDl' => 'ng/dl', + 'unitNmolPerL' => 'nmol/L', + 'unitNgPerMl' => 'एनजी / एमएल', + 'injectionSideLeft' => 'बाएं', + 'injectionSideRight' => 'अधिकार', + 'placementLeft' => 'बाएं तरफ', + 'placementRight' => 'सही पक्ष', + 'placementLeftThigh' => 'बाएं जांघ', + 'placementRightThigh' => 'सही जांघ', + 'placementLeftArm' => 'बाएं हाथ', + 'placementRightArm' => 'दाहिने हाथ', + 'placementLeftButtock' => 'बाएं बटॉक', + 'placementRightButtock' => 'राइट बटॉक', + 'placementLeftAbdomen' => 'बाएं पेट', + 'placementRightAbdomen' => 'दाहिने पेट', + 'applicationSites' => 'अनुप्रयोग साइट', + 'applicationSitesDescription' => + 'उन साइटों को प्रबंधित करें जो आप के बीच घूमते हैं', + 'applicationSitesInstructions' => + 'उन साइटों को प्रबंधित करें जो आप के बीच घूमते हैं। साइट्स को आपके सेवन इतिहास के आधार पर सुझाव दिया जाता है। फिर से व्यवस्थित करने के लिए लंबे प्रेस।।', + 'addApplicationSite' => 'साइट जोड़ें', + 'customSiteLabel' => 'कस्टम साइट नाम', + 'noApplicationSitesYet' => 'अभी तक कोई साइट नहीं', + 'addSiteToGetStarted' => 'शुरू करने के लिए नीचे एक साइट जोड़ें।।', + 'placementSuggestionPerScheduleTitle' => 'प्रति अनुसूची सुझाव', + 'placementSuggestionPerScheduleDescription' => + 'इस कार्यक्रम के इतिहास पर अगले साइट सुझाव को बेस करें।।', + 'requiredField' => 'आवश्यक फ़ील्ड', + 'mustBePositiveNumber' => 'एक सकारात्मक संख्या होना चाहिए', + 'mustBeBetween1And28' => '1 और 28 के बीच होना चाहिए', + 'mustBeAtMost' => ({required Object max}) => '${max}', + 'invalidTotalAmount' => 'कुल राशि', + 'cannotExceedTotalCapacity' => 'कुल क्षमता से अधिक नहीं', 'daysAgoCount' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( count, @@ -512,6 +1136,114 @@ extension on TranslationsHi { one: '${count} महीनों के लिए HRT पर', other: '${count} महीनों के लिए HRT पर', ), + 'onHrtForWeeks' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'HRT on ${count} week', + other: 'HRT on ${count} week', + ), + 'onHrtForYears' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'HRT पर ${count} वर्ष', + other: 'HRT पर ${count} वर्ष', + ), + 'intakesLoggedCount' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} प्रवेश लॉग इन', + other: '${count} प्रवेश लॉग इन', + ), + 'remaining' => ({required num count, required Object unit}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} ${unit} शेष', + other: '${count} ${unit} शेष', + ), + 'syringeRemaining' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} syringes शेष', + other: '${count} syringes शेष', + ), + 'wipeRemaining' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} शेष पोंछे', + other: '${count} शेष पोंछे', + ), + 'needleRemaining' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} सुई शेष', + other: '${count} सुई शेष', + ), + 'glovesRemaining' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} दस्ताने शेष', + other: '${count} दस्ताने शेष', + ), + 'bandageRemaining' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: '${count} बैंडेज शेष', + other: '${count} बैंडेज शेष', + ), + 'administrationRouteUnitMl' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'एमएल', + other: 'एमएल', + ), + 'administrationRouteUnitPill' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'गोलियाँ', + other: 'गोलियाँ', + ), + 'administrationRouteUnitPatch' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'पैच', + other: 'पैच', + ), + 'administrationRouteUnitPump' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'पंप', + other: 'पंप', + ), + 'administrationRouteUnitSachet' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'पाउच', + other: 'पाउच', + ), + 'administrationRouteUnitGram' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'ग्राम', + other: 'ग्राम', + ), + 'administrationRouteUnitImplant' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'प्रत्यारोपण', + other: 'प्रत्यारोपण', + ), + 'administrationRouteUnitSuppository' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'सट्टा', + other: 'सट्टा', + ), + 'administrationRouteUnitSpray' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hi'))( + count, + one: 'स्प्रे', + other: 'स्प्रे', + ), _ => null, }; } diff --git a/lib/i18n/translations_hu.g.dart b/lib/i18n/translations_hu.g.dart new file mode 100644 index 000000000..80517159e --- /dev/null +++ b/lib/i18n/translations_hu.g.dart @@ -0,0 +1,138 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.dart'; +import 'translations.g.dart'; + +// Path: +class TranslationsHu extends Translations + with BaseTranslations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + TranslationsHu( + {Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + TranslationMetadata? meta}) + : assert(overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = meta ?? + TranslationMetadata( + locale: AppLocale.hu, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ), + super( + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver) { + super.$meta.setFlatMapFunction( + $meta.getTranslation); // copy base translations to super.$meta + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; + + /// Access flat map + @override + dynamic operator [](String key) => + $meta.getTranslation(key) ?? super.$meta.getTranslation(key); + + late final TranslationsHu _root = this; // ignore: unused_field + + @override + TranslationsHu $copyWith( + {TranslationMetadata? meta}) => + TranslationsHu(meta: meta ?? this.$meta); + + // Translations + @override + String get appTitle => 'Mona'; + @override + String get nav_home => 'Mona'; + @override + String get nav_intakes => 'Bevevések'; + @override + String get nav_levels => 'Szintek'; + @override + String get nav_supplies => 'Készlet'; + @override + String get takeAnIntake => 'Bevevés felírása'; + @override + String get addAnItem => 'Item hozzáadása'; + @override + String get empty_home => + 'Kezdje egy időbeosztás hozzáadásával a Beállításokban'; + @override + String get allDone => 'Kész!'; + @override + String get noIntakesDue => 'Nincs mára felírt adag'; + @override + String get upcoming => 'Következő'; + @override + String get asNeeded => 'Ahogy szükséges'; + @override + String get taken => 'Bevéve'; + @override + String get yesterday => 'tegnap'; + @override + String daysAgoCount({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hu'))( + count, + one: '${count} napja', + other: '${count} napja', + ); + @override + String inDaysCount({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hu'))( + count, + one: '${count} nap múlva', + other: '${count} nap múlva', + ); +} + +/// The flat map containing all translations for locale . +/// Only for edge cases! For simple maps, use the map function of this library. +/// +/// The Dart AOT compiler has issues with very large switch statements, +/// so the map is split into smaller functions (512 entries each). +extension on TranslationsHu { + dynamic _flatMapFunction(String path) { + return switch (path) { + 'appTitle' => 'Mona', + 'nav_home' => 'Mona', + 'nav_intakes' => 'Bevevések', + 'nav_levels' => 'Szintek', + 'nav_supplies' => 'Készlet', + 'takeAnIntake' => 'Bevevés felírása', + 'addAnItem' => 'Item hozzáadása', + 'empty_home' => 'Kezdje egy időbeosztás hozzáadásával a Beállításokban', + 'allDone' => 'Kész!', + 'noIntakesDue' => 'Nincs mára felírt adag', + 'upcoming' => 'Következő', + 'asNeeded' => 'Ahogy szükséges', + 'taken' => 'Bevéve', + 'yesterday' => 'tegnap', + 'daysAgoCount' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hu'))( + count, + one: '${count} napja', + other: '${count} napja', + ), + 'inDaysCount' => ({required num count}) => + (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('hu'))( + count, + one: '${count} nap múlva', + other: '${count} nap múlva', + ), + _ => null, + }; + } +} diff --git a/lib/i18n/translations_id.g.dart b/lib/i18n/translations_id.g.dart new file mode 100644 index 000000000..1f607cc70 --- /dev/null +++ b/lib/i18n/translations_id.g.dart @@ -0,0 +1,67 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.dart'; +import 'translations.g.dart'; + +// Path: +class TranslationsId extends Translations + with BaseTranslations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + TranslationsId( + {Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + TranslationMetadata? meta}) + : assert(overrides == null, + 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = meta ?? + TranslationMetadata( + locale: AppLocale.id, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ), + super( + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver) { + super.$meta.setFlatMapFunction( + $meta.getTranslation); // copy base translations to super.$meta + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override + final TranslationMetadata $meta; + + /// Access flat map + @override + dynamic operator [](String key) => + $meta.getTranslation(key) ?? super.$meta.getTranslation(key); + + late final TranslationsId _root = this; // ignore: unused_field + + @override + TranslationsId $copyWith( + {TranslationMetadata? meta}) => + TranslationsId(meta: meta ?? this.$meta); + + // Translations +} + +/// The flat map containing all translations for locale . +/// Only for edge cases! For simple maps, use the map function of this library. +/// +/// The Dart AOT compiler has issues with very large switch statements, +/// so the map is split into smaller functions (512 entries each). +extension on TranslationsId { + dynamic _flatMapFunction(String path) { + ; + } +} diff --git a/lib/i18n/translations_pl.g.dart b/lib/i18n/translations_pl.g.dart index 8b508bfa3..82b3b06c9 100644 --- a/lib/i18n/translations_pl.g.dart +++ b/lib/i18n/translations_pl.g.dart @@ -569,6 +569,7 @@ class TranslationsPl extends Translations one: '${count} dzień temu', few: '${count} dni temu', many: '${count} dni temu', + other: '${count} dni temu', ); @override String inDaysCount({required num count}) => @@ -577,6 +578,7 @@ class TranslationsPl extends Translations one: 'za ${count} dzień', few: 'za ${count} dni', many: 'za ${count} dni', + other: 'za ${count} dni', ); @override String scheduleFrequencyEveryNDays({required num count}) => @@ -585,6 +587,7 @@ class TranslationsPl extends Translations one: 'Co dzień', few: 'Co ${count} dni', many: 'Co ${count} dni', + other: 'Co ${count} dni', ); @override String scheduleFrequencyOnDayEveryNMonths( @@ -594,6 +597,7 @@ class TranslationsPl extends Translations one: 'Dzień ${day}, co miesiąc', few: 'Dzień ${day}, co ${count} miesiące', many: 'Dzień ${day}, co ${count} miesięcy', + other: 'Dzień ${day}, co ${count} miesiące', ); @override String schedulesCreated({required num count}) => @@ -602,6 +606,7 @@ class TranslationsPl extends Translations one: 'Utworzono: ${count}', few: 'Utworzono: ${count}', many: 'Utworzono: ${count}', + other: 'Utworzono: ${count}', ); @override String onHrtForDays({required num count}) => @@ -610,6 +615,7 @@ class TranslationsPl extends Translations one: 'Na HTZ od 1 dnia', few: 'Na HTZ od ${count} dni', many: 'Na HTZ od ${count} dni', + other: 'Na HTZ od ${count} dni', ); @override String onHrtForWeeks({required num count}) => @@ -618,6 +624,7 @@ class TranslationsPl extends Translations one: 'Na HTZ od 1 tygodnia', few: 'Na HTZ od ${count} tygodni', many: 'Na HTZ od ${count} tygodni', + other: 'Na HTZ od ${count} tygodni', ); @override String onHrtForMonths({required num count}) => @@ -626,6 +633,7 @@ class TranslationsPl extends Translations one: 'Na HTZ od 1 miesiąca', few: 'Na HTZ od ${count} miesięcy', many: 'Na HTZ od ${count} miesięcy', + other: 'Na HTZ od ${count} miesięcy', ); @override String onHrtForYears({required num count}) => @@ -634,6 +642,7 @@ class TranslationsPl extends Translations one: 'Na HTZ od 1 roku', few: 'Na HTZ od ${count} lat', many: 'Na HTZ od ${count} lat', + other: 'Na HTZ od ${count} lat', ); @override String intakesLoggedCount({required num count}) => @@ -642,6 +651,7 @@ class TranslationsPl extends Translations one: 'Odnotowano 1 przyjęcie', few: 'Odnotowano ${count} przyjęcia', many: 'Odnotowano ${count} przyjęć', + other: 'Odnotowano ${count} przyjęcia', ); @override String remaining({required num count, required Object unit}) => @@ -650,6 +660,7 @@ class TranslationsPl extends Translations one: 'Pozostało: ${count} ${unit}', few: 'Pozostało: ${count} ${unit}', many: 'Pozostało: ${count} ${unit}', + other: 'Pozostało: ${count} ${unit}', ); @override String syringeRemaining({required num count}) => @@ -658,6 +669,7 @@ class TranslationsPl extends Translations one: 'Pozostało: 1 strzykawka', few: 'Pozostało: ${count} strzykawki', many: 'Pozostało: ${count} strzykawek', + other: 'Pozostało: ${count} strzykawki', ); @override String wipeRemaining({required num count}) => @@ -666,6 +678,7 @@ class TranslationsPl extends Translations one: 'Pozostało: 1 chusteczka', few: 'Pozostało: ${count} chusteczki', many: 'Pozostało: ${count} chusteczek', + other: 'Pozostało: ${count} chusteczki', ); @override String needleRemaining({required num count}) => @@ -674,6 +687,7 @@ class TranslationsPl extends Translations one: 'Pozostało: 1 igła', few: 'Pozostało: ${count} igły', many: 'Pozostało: ${count} igieł', + other: 'Pozostało: ${count} igły', ); @override String glovesRemaining({required num count}) => @@ -682,6 +696,7 @@ class TranslationsPl extends Translations one: 'Pozostało: 1 rękawiczka', few: 'Pozostało: ${count} rękawiczki', many: 'Pozostało: ${count} rękawiczek', + other: 'Pozostało: ${count} rękawiczki', ); @override String bandageRemaining({required num count}) => @@ -690,6 +705,7 @@ class TranslationsPl extends Translations one: 'Pozostało: 1 bandaż', few: 'Pozostało: ${count} bandaże', many: 'Pozostało: ${count} bandaży', + other: 'Pozostało: ${count} bandaże', ); @override String administrationRouteUnitMl({required num count}) => @@ -698,6 +714,7 @@ class TranslationsPl extends Translations one: 'ml', few: 'ml', many: 'ml', + other: 'ml', ); @override String administrationRouteUnitPill({required num count}) => @@ -706,6 +723,7 @@ class TranslationsPl extends Translations one: 'tabletka', few: 'tabletki', many: 'tabletek', + other: 'tabletki', ); @override String administrationRouteUnitPatch({required num count}) => @@ -714,6 +732,7 @@ class TranslationsPl extends Translations one: 'plaster', few: 'plastry', many: 'plastrów', + other: 'plastry', ); @override String administrationRouteUnitPump({required num count}) => @@ -722,6 +741,7 @@ class TranslationsPl extends Translations one: 'pompka', few: 'pompki', many: 'pompek', + other: 'pompki', ); @override String administrationRouteUnitImplant({required num count}) => @@ -730,6 +750,7 @@ class TranslationsPl extends Translations one: 'implant', few: 'implanty', many: 'implantów', + other: 'implanty', ); @override String administrationRouteUnitSuppository({required num count}) => @@ -738,6 +759,7 @@ class TranslationsPl extends Translations one: 'czopek', few: 'czopki', many: 'czopków', + other: 'czopki', ); @override String administrationRouteUnitSpray({required num count}) => @@ -746,6 +768,7 @@ class TranslationsPl extends Translations one: 'spryśnięcie', few: 'spryśnięcia', many: 'spryśnięć', + other: 'spryśnięcia', ); } @@ -1031,6 +1054,7 @@ extension on TranslationsPl { one: '${count} dzień temu', few: '${count} dni temu', many: '${count} dni temu', + other: '${count} dni temu', ), 'inDaysCount' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1038,6 +1062,7 @@ extension on TranslationsPl { one: 'za ${count} dzień', few: 'za ${count} dni', many: 'za ${count} dni', + other: 'za ${count} dni', ), 'scheduleFrequencyEveryNDays' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1045,6 +1070,7 @@ extension on TranslationsPl { one: 'Co dzień', few: 'Co ${count} dni', many: 'Co ${count} dni', + other: 'Co ${count} dni', ), 'scheduleFrequencyOnDayEveryNMonths' => ( {required num count, required Object day}) => @@ -1053,6 +1079,7 @@ extension on TranslationsPl { one: 'Dzień ${day}, co miesiąc', few: 'Dzień ${day}, co ${count} miesiące', many: 'Dzień ${day}, co ${count} miesięcy', + other: 'Dzień ${day}, co ${count} miesiące', ), 'schedulesCreated' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1060,6 +1087,7 @@ extension on TranslationsPl { one: 'Utworzono: ${count}', few: 'Utworzono: ${count}', many: 'Utworzono: ${count}', + other: 'Utworzono: ${count}', ), 'onHrtForDays' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1067,6 +1095,7 @@ extension on TranslationsPl { one: 'Na HTZ od 1 dnia', few: 'Na HTZ od ${count} dni', many: 'Na HTZ od ${count} dni', + other: 'Na HTZ od ${count} dni', ), 'onHrtForWeeks' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1074,6 +1103,7 @@ extension on TranslationsPl { one: 'Na HTZ od 1 tygodnia', few: 'Na HTZ od ${count} tygodni', many: 'Na HTZ od ${count} tygodni', + other: 'Na HTZ od ${count} tygodni', ), 'onHrtForMonths' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1081,6 +1111,7 @@ extension on TranslationsPl { one: 'Na HTZ od 1 miesiąca', few: 'Na HTZ od ${count} miesięcy', many: 'Na HTZ od ${count} miesięcy', + other: 'Na HTZ od ${count} miesięcy', ), 'onHrtForYears' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1088,6 +1119,7 @@ extension on TranslationsPl { one: 'Na HTZ od 1 roku', few: 'Na HTZ od ${count} lat', many: 'Na HTZ od ${count} lat', + other: 'Na HTZ od ${count} lat', ), 'intakesLoggedCount' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1095,6 +1127,7 @@ extension on TranslationsPl { one: 'Odnotowano 1 przyjęcie', few: 'Odnotowano ${count} przyjęcia', many: 'Odnotowano ${count} przyjęć', + other: 'Odnotowano ${count} przyjęcia', ), 'remaining' => ({required num count, required Object unit}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1102,6 +1135,7 @@ extension on TranslationsPl { one: 'Pozostało: ${count} ${unit}', few: 'Pozostało: ${count} ${unit}', many: 'Pozostało: ${count} ${unit}', + other: 'Pozostało: ${count} ${unit}', ), 'syringeRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1109,6 +1143,7 @@ extension on TranslationsPl { one: 'Pozostało: 1 strzykawka', few: 'Pozostało: ${count} strzykawki', many: 'Pozostało: ${count} strzykawek', + other: 'Pozostało: ${count} strzykawki', ), 'wipeRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1116,6 +1151,7 @@ extension on TranslationsPl { one: 'Pozostało: 1 chusteczka', few: 'Pozostało: ${count} chusteczki', many: 'Pozostało: ${count} chusteczek', + other: 'Pozostało: ${count} chusteczki', ), 'needleRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1123,6 +1159,7 @@ extension on TranslationsPl { one: 'Pozostało: 1 igła', few: 'Pozostało: ${count} igły', many: 'Pozostało: ${count} igieł', + other: 'Pozostało: ${count} igły', ), 'glovesRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1130,6 +1167,7 @@ extension on TranslationsPl { one: 'Pozostało: 1 rękawiczka', few: 'Pozostało: ${count} rękawiczki', many: 'Pozostało: ${count} rękawiczek', + other: 'Pozostało: ${count} rękawiczki', ), 'bandageRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1137,6 +1175,7 @@ extension on TranslationsPl { one: 'Pozostało: 1 bandaż', few: 'Pozostało: ${count} bandaże', many: 'Pozostało: ${count} bandaży', + other: 'Pozostało: ${count} bandaże', ), 'administrationRouteUnitMl' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1144,6 +1183,7 @@ extension on TranslationsPl { one: 'ml', few: 'ml', many: 'ml', + other: 'ml', ), 'administrationRouteUnitPill' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1151,6 +1191,7 @@ extension on TranslationsPl { one: 'tabletka', few: 'tabletki', many: 'tabletek', + other: 'tabletki', ), 'administrationRouteUnitPatch' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1158,6 +1199,7 @@ extension on TranslationsPl { one: 'plaster', few: 'plastry', many: 'plastrów', + other: 'plastry', ), 'administrationRouteUnitPump' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1165,6 +1207,7 @@ extension on TranslationsPl { one: 'pompka', few: 'pompki', many: 'pompek', + other: 'pompki', ), 'administrationRouteUnitImplant' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1172,6 +1215,7 @@ extension on TranslationsPl { one: 'implant', few: 'implanty', many: 'implantów', + other: 'implanty', ), 'administrationRouteUnitSuppository' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1179,6 +1223,7 @@ extension on TranslationsPl { one: 'czopek', few: 'czopki', many: 'czopków', + other: 'czopki', ), 'administrationRouteUnitSpray' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('pl'))( @@ -1186,6 +1231,7 @@ extension on TranslationsPl { one: 'spryśnięcie', few: 'spryśnięcia', many: 'spryśnięć', + other: 'spryśnięcia', ), _ => null, }; diff --git a/lib/i18n/translations_ru.g.dart b/lib/i18n/translations_ru.g.dart index a9c76f733..16e8d0eb8 100644 --- a/lib/i18n/translations_ru.g.dart +++ b/lib/i18n/translations_ru.g.dart @@ -565,6 +565,7 @@ class TranslationsRu extends Translations one: '${count} день назад', few: '${count} дня назад', many: '${count} дней назад', + other: '${count} дня назад', ); @override String inDaysCount({required num count}) => @@ -573,6 +574,7 @@ class TranslationsRu extends Translations one: 'через ${count} день', few: 'через ${count} дня', many: 'через ${count} дней', + other: 'через ${count} дня', ); @override String scheduleFrequencyEveryNDays({required num count}) => @@ -581,6 +583,7 @@ class TranslationsRu extends Translations one: 'Каждые ${count} дней', few: 'Каждые ${count} дней', many: 'Каждые ${count} дней', + other: 'Каждые ${count} дней', ); @override String schedulesCreated({required num count}) => @@ -589,6 +592,7 @@ class TranslationsRu extends Translations one: '${count} создано', few: '${count} создано', many: '${count} создано', + other: '${count} создано', ); @override String remaining({required num count, required Object unit}) => @@ -597,6 +601,7 @@ class TranslationsRu extends Translations one: '${count} ${unit} осталось', few: '${count} ${unit} осталось', many: '${count} ${unit} осталось', + other: '${count} ${unit} осталось', ); @override String syringeRemaining({required num count}) => @@ -605,6 +610,7 @@ class TranslationsRu extends Translations one: '1 шприц осталось', few: '${count} шприца осталось', many: '${count} шприцев осталось', + other: '${count} шприца осталось', ); @override String wipeRemaining({required num count}) => @@ -613,6 +619,7 @@ class TranslationsRu extends Translations one: '1 салфетка осталось', few: '${count} салфетки осталось', many: '${count} салфеток осталось', + other: '${count} салфетки осталось', ); @override String needleRemaining({required num count}) => @@ -621,6 +628,7 @@ class TranslationsRu extends Translations one: '1 игла осталось', few: '${count} иглы осталось', many: '${count} игл осталось', + other: '${count} иглы осталось', ); @override String glovesRemaining({required num count}) => @@ -629,6 +637,7 @@ class TranslationsRu extends Translations one: '1 перчатка осталось', few: '${count} перчатки осталось', many: '${count} перчаток осталось', + other: '${count} перчатки осталось', ); @override String bandageRemaining({required num count}) => @@ -637,6 +646,7 @@ class TranslationsRu extends Translations one: '1 пластырь осталось', few: '${count} пластыря осталось', many: '${count} пластырей осталось', + other: '${count} пластыря осталось', ); @override String administrationRouteUnitMl({required num count}) => @@ -645,6 +655,7 @@ class TranslationsRu extends Translations one: 'мл', few: 'мл', many: 'мл', + other: 'мл', ); @override String administrationRouteUnitPill({required num count}) => @@ -653,6 +664,7 @@ class TranslationsRu extends Translations one: 'таблетка', few: 'таблетки', many: 'таблеток', + other: 'таблетки', ); @override String administrationRouteUnitPatch({required num count}) => @@ -661,6 +673,7 @@ class TranslationsRu extends Translations one: 'пластырь', few: 'пластыря', many: 'пластырей', + other: 'пластыря', ); @override String administrationRouteUnitPump({required num count}) => @@ -669,6 +682,7 @@ class TranslationsRu extends Translations one: 'помпа', few: 'помпы', many: 'помп', + other: 'помпы', ); @override String administrationRouteUnitImplant({required num count}) => @@ -677,6 +691,7 @@ class TranslationsRu extends Translations one: 'имплант', few: 'импланта', many: 'имплантов', + other: 'импланта', ); @override String administrationRouteUnitSuppository({required num count}) => @@ -685,6 +700,7 @@ class TranslationsRu extends Translations one: 'суппозитория', few: 'суппозитории', many: 'суппозиторий', + other: 'суппозитории', ); @override String administrationRouteUnitSpray({required num count}) => @@ -693,6 +709,7 @@ class TranslationsRu extends Translations one: 'брызг', few: 'брызг', many: 'брызгов', + other: 'брызг', ); @override String scheduleFrequencyOnDayEveryNMonths( @@ -702,6 +719,7 @@ class TranslationsRu extends Translations one: 'День ${day}, каждый ${count} месяц', few: 'День ${day}, каждые ${count} месяца', many: 'День ${day}, каждые ${count} месяцев', + other: 'День ${day}, каждые ${count} месяца', ); @override String onHrtForDays({required num count}) => @@ -710,6 +728,7 @@ class TranslationsRu extends Translations one: 'На ЗГТ уже ${count} день', few: 'На ЗГТ уже ${count} дня', many: 'На ЗГТ уже ${count} дней', + other: 'На ЗГТ уже ${count} дня', ); @override String onHrtForWeeks({required num count}) => @@ -718,6 +737,7 @@ class TranslationsRu extends Translations one: 'На ЗГТ уже ${count} неделю', few: 'На ЗГТ уже ${count} недели', many: 'На ЗГТ уже ${count} недель', + other: 'На ЗГТ уже ${count} недели', ); @override String onHrtForMonths({required num count}) => @@ -726,6 +746,7 @@ class TranslationsRu extends Translations one: 'На ЗГТ уже ${count} месяц', few: 'На ЗГТ уже ${count} месяца', many: 'На ЗГТ уже ${count} месяцев', + other: 'На ЗГТ уже ${count} месяца', ); @override String onHrtForYears({required num count}) => @@ -734,6 +755,7 @@ class TranslationsRu extends Translations one: 'На ЗГТ уже ${count} год', few: 'На ЗГТ уже ${count} года', many: 'На ЗГТ уже ${count} лет', + other: 'На ЗГТ уже ${count} года', ); @override String intakesLoggedCount({required num count}) => @@ -742,6 +764,7 @@ class TranslationsRu extends Translations one: 'Записан ${count} приём', few: 'Записано ${count} приёма', many: 'Записано ${count} приёмов', + other: 'Записано ${count} приёма', ); } @@ -1025,6 +1048,7 @@ extension on TranslationsRu { one: '${count} день назад', few: '${count} дня назад', many: '${count} дней назад', + other: '${count} дня назад', ), 'inDaysCount' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1032,6 +1056,7 @@ extension on TranslationsRu { one: 'через ${count} день', few: 'через ${count} дня', many: 'через ${count} дней', + other: 'через ${count} дня', ), 'scheduleFrequencyEveryNDays' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1039,6 +1064,7 @@ extension on TranslationsRu { one: 'Каждые ${count} дней', few: 'Каждые ${count} дней', many: 'Каждые ${count} дней', + other: 'Каждые ${count} дней', ), 'schedulesCreated' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1046,6 +1072,7 @@ extension on TranslationsRu { one: '${count} создано', few: '${count} создано', many: '${count} создано', + other: '${count} создано', ), 'remaining' => ({required num count, required Object unit}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1053,6 +1080,7 @@ extension on TranslationsRu { one: '${count} ${unit} осталось', few: '${count} ${unit} осталось', many: '${count} ${unit} осталось', + other: '${count} ${unit} осталось', ), 'syringeRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1060,6 +1088,7 @@ extension on TranslationsRu { one: '1 шприц осталось', few: '${count} шприца осталось', many: '${count} шприцев осталось', + other: '${count} шприца осталось', ), 'wipeRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1067,6 +1096,7 @@ extension on TranslationsRu { one: '1 салфетка осталось', few: '${count} салфетки осталось', many: '${count} салфеток осталось', + other: '${count} салфетки осталось', ), 'needleRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1074,6 +1104,7 @@ extension on TranslationsRu { one: '1 игла осталось', few: '${count} иглы осталось', many: '${count} игл осталось', + other: '${count} иглы осталось', ), 'glovesRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1081,6 +1112,7 @@ extension on TranslationsRu { one: '1 перчатка осталось', few: '${count} перчатки осталось', many: '${count} перчаток осталось', + other: '${count} перчатки осталось', ), 'bandageRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1088,6 +1120,7 @@ extension on TranslationsRu { one: '1 пластырь осталось', few: '${count} пластыря осталось', many: '${count} пластырей осталось', + other: '${count} пластыря осталось', ), 'administrationRouteUnitMl' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1095,6 +1128,7 @@ extension on TranslationsRu { one: 'мл', few: 'мл', many: 'мл', + other: 'мл', ), 'administrationRouteUnitPill' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1102,6 +1136,7 @@ extension on TranslationsRu { one: 'таблетка', few: 'таблетки', many: 'таблеток', + other: 'таблетки', ), 'administrationRouteUnitPatch' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1109,6 +1144,7 @@ extension on TranslationsRu { one: 'пластырь', few: 'пластыря', many: 'пластырей', + other: 'пластыря', ), 'administrationRouteUnitPump' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1116,6 +1152,7 @@ extension on TranslationsRu { one: 'помпа', few: 'помпы', many: 'помп', + other: 'помпы', ), 'administrationRouteUnitImplant' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1123,6 +1160,7 @@ extension on TranslationsRu { one: 'имплант', few: 'импланта', many: 'имплантов', + other: 'импланта', ), 'administrationRouteUnitSuppository' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1130,6 +1168,7 @@ extension on TranslationsRu { one: 'суппозитория', few: 'суппозитории', many: 'суппозиторий', + other: 'суппозитории', ), 'administrationRouteUnitSpray' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1137,6 +1176,7 @@ extension on TranslationsRu { one: 'брызг', few: 'брызг', many: 'брызгов', + other: 'брызг', ), 'scheduleFrequencyOnDayEveryNMonths' => ( {required num count, required Object day}) => @@ -1145,6 +1185,7 @@ extension on TranslationsRu { one: 'День ${day}, каждый ${count} месяц', few: 'День ${day}, каждые ${count} месяца', many: 'День ${day}, каждые ${count} месяцев', + other: 'День ${day}, каждые ${count} месяца', ), 'onHrtForDays' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1152,6 +1193,7 @@ extension on TranslationsRu { one: 'На ЗГТ уже ${count} день', few: 'На ЗГТ уже ${count} дня', many: 'На ЗГТ уже ${count} дней', + other: 'На ЗГТ уже ${count} дня', ), 'onHrtForWeeks' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1159,6 +1201,7 @@ extension on TranslationsRu { one: 'На ЗГТ уже ${count} неделю', few: 'На ЗГТ уже ${count} недели', many: 'На ЗГТ уже ${count} недель', + other: 'На ЗГТ уже ${count} недели', ), 'onHrtForMonths' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1166,6 +1209,7 @@ extension on TranslationsRu { one: 'На ЗГТ уже ${count} месяц', few: 'На ЗГТ уже ${count} месяца', many: 'На ЗГТ уже ${count} месяцев', + other: 'На ЗГТ уже ${count} месяца', ), 'onHrtForYears' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1173,6 +1217,7 @@ extension on TranslationsRu { one: 'На ЗГТ уже ${count} год', few: 'На ЗГТ уже ${count} года', many: 'На ЗГТ уже ${count} лет', + other: 'На ЗГТ уже ${count} года', ), 'intakesLoggedCount' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('ru'))( @@ -1180,6 +1225,7 @@ extension on TranslationsRu { one: 'Записан ${count} приём', few: 'Записано ${count} приёма', many: 'Записано ${count} приёмов', + other: 'Записано ${count} приёма', ), _ => null, }; diff --git a/lib/i18n/translations_uk.g.dart b/lib/i18n/translations_uk.g.dart index 08809f4fa..aef351520 100644 --- a/lib/i18n/translations_uk.g.dart +++ b/lib/i18n/translations_uk.g.dart @@ -566,6 +566,7 @@ class TranslationsUk extends Translations one: '${count} день тому', few: '${count} дні тому', many: '${count} днів тому', + other: '${count} дні тому', ); @override String inDaysCount({required num count}) => @@ -574,6 +575,7 @@ class TranslationsUk extends Translations one: 'через ${count} день', few: 'через ${count} дні', many: 'через ${count} днів', + other: 'через ${count} дні', ); @override String scheduleFrequencyEveryNDays({required num count}) => @@ -582,6 +584,7 @@ class TranslationsUk extends Translations one: 'Кожні ${count} днів', few: 'Кожні ${count} днів', many: 'Кожні ${count} днів', + other: 'Кожні ${count} днів', ); @override String schedulesCreated({required num count}) => @@ -590,6 +593,7 @@ class TranslationsUk extends Translations one: 'Створено ${count} розклад', few: 'Створено ${count} розклади', many: 'Створено ${count} розкладів', + other: 'Створено ${count} розклади', ); @override String remaining({required num count, required Object unit}) => @@ -598,6 +602,7 @@ class TranslationsUk extends Translations one: '${count} ${unit} залишилось', few: '${count} ${unit} залишилось', many: '${count} ${unit} залишилось', + other: '${count} ${unit} залишилось', ); @override String syringeRemaining({required num count}) => @@ -606,6 +611,7 @@ class TranslationsUk extends Translations one: '1 шприц залишилось', few: '${count} шприца залишилось', many: '${count} шприців залишилось', + other: '${count} шприца залишилось', ); @override String wipeRemaining({required num count}) => @@ -614,6 +620,7 @@ class TranslationsUk extends Translations one: '1 серветка залишилось', few: '${count} серветки залишилось', many: '${count} серветок залишилось', + other: '${count} серветки залишилось', ); @override String needleRemaining({required num count}) => @@ -622,6 +629,7 @@ class TranslationsUk extends Translations one: '1 голка залишилось', few: '${count} голки залишилось', many: '${count} голок залишилось', + other: '${count} голки залишилось', ); @override String glovesRemaining({required num count}) => @@ -630,6 +638,7 @@ class TranslationsUk extends Translations one: '1 рукавичка залишилось', few: '${count} рукавички залишилось', many: '${count} рукавичок залишилось', + other: '${count} рукавички залишилось', ); @override String bandageRemaining({required num count}) => @@ -638,6 +647,7 @@ class TranslationsUk extends Translations one: '1 пластир залишилось', few: '${count} пластирі залишилось', many: '${count} пластирів залишилось', + other: '${count} пластирі залишилось', ); @override String administrationRouteUnitMl({required num count}) => @@ -646,6 +656,7 @@ class TranslationsUk extends Translations one: 'ml', few: 'ml', many: 'ml', + other: 'ml', ); @override String administrationRouteUnitPill({required num count}) => @@ -654,6 +665,7 @@ class TranslationsUk extends Translations one: 'пігулка', few: 'пігулки', many: 'пігулок', + other: 'пігулки', ); @override String administrationRouteUnitPatch({required num count}) => @@ -662,6 +674,7 @@ class TranslationsUk extends Translations one: 'патч', few: 'патчі', many: 'патчів', + other: 'патчі', ); @override String administrationRouteUnitPump({required num count}) => @@ -670,6 +683,7 @@ class TranslationsUk extends Translations one: 'натискання', few: 'натискання', many: 'натискань', + other: 'натискання', ); @override String administrationRouteUnitImplant({required num count}) => @@ -678,6 +692,7 @@ class TranslationsUk extends Translations one: 'імплант', few: 'імпланти', many: 'імплантів', + other: 'імпланти', ); @override String administrationRouteUnitSuppository({required num count}) => @@ -686,6 +701,7 @@ class TranslationsUk extends Translations one: 'супозиторій', few: 'супозиторії', many: 'супозиторіїв', + other: 'супозиторії', ); @override String administrationRouteUnitSpray({required num count}) => @@ -694,6 +710,7 @@ class TranslationsUk extends Translations one: 'спрей', few: 'спреї', many: 'спреїв', + other: 'спреї', ); @override String scheduleFrequencyOnDayEveryNMonths( @@ -703,6 +720,7 @@ class TranslationsUk extends Translations one: 'День ${day}, кожен ${count} місяць', few: 'День ${day}, кожні ${count} місяці', many: 'День ${day}, кожні ${count} місяців', + other: 'День ${day}, кожні ${count} місяці', ); @override String onHrtForDays({required num count}) => @@ -711,6 +729,7 @@ class TranslationsUk extends Translations one: 'На ЗГТ вже ${count} день', few: 'На ЗГТ вже ${count} дні', many: 'На ЗГТ вже ${count} днів', + other: 'На ЗГТ вже ${count} дні', ); @override String onHrtForWeeks({required num count}) => @@ -719,6 +738,7 @@ class TranslationsUk extends Translations one: 'На ЗГТ вже ${count} тиждень', few: 'На ЗГТ вже ${count} тижні', many: 'На ЗГТ вже ${count} тижнів', + other: 'На ЗГТ вже ${count} тижні', ); @override String onHrtForMonths({required num count}) => @@ -727,6 +747,7 @@ class TranslationsUk extends Translations one: 'На ЗГТ вже ${count} місяць', few: 'На ЗГТ вже ${count} місяці', many: 'На ЗГТ вже ${count} місяців', + other: 'На ЗГТ вже ${count} місяці', ); @override String onHrtForYears({required num count}) => @@ -735,6 +756,7 @@ class TranslationsUk extends Translations one: 'На ЗГТ вже ${count} рік', few: 'На ЗГТ вже ${count} роки', many: 'На ЗГТ вже ${count} років', + other: 'На ЗГТ вже ${count} роки', ); @override String intakesLoggedCount({required num count}) => @@ -743,6 +765,7 @@ class TranslationsUk extends Translations one: 'Записано ${count} прийом', few: 'Записано ${count} прийоми', many: 'Записано ${count} прийомів', + other: 'Записано ${count} прийоми', ); } @@ -1025,6 +1048,7 @@ extension on TranslationsUk { one: '${count} день тому', few: '${count} дні тому', many: '${count} днів тому', + other: '${count} дні тому', ), 'inDaysCount' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1032,6 +1056,7 @@ extension on TranslationsUk { one: 'через ${count} день', few: 'через ${count} дні', many: 'через ${count} днів', + other: 'через ${count} дні', ), 'scheduleFrequencyEveryNDays' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1039,6 +1064,7 @@ extension on TranslationsUk { one: 'Кожні ${count} днів', few: 'Кожні ${count} днів', many: 'Кожні ${count} днів', + other: 'Кожні ${count} днів', ), 'schedulesCreated' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1046,6 +1072,7 @@ extension on TranslationsUk { one: 'Створено ${count} розклад', few: 'Створено ${count} розклади', many: 'Створено ${count} розкладів', + other: 'Створено ${count} розклади', ), 'remaining' => ({required num count, required Object unit}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1053,6 +1080,7 @@ extension on TranslationsUk { one: '${count} ${unit} залишилось', few: '${count} ${unit} залишилось', many: '${count} ${unit} залишилось', + other: '${count} ${unit} залишилось', ), 'syringeRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1060,6 +1088,7 @@ extension on TranslationsUk { one: '1 шприц залишилось', few: '${count} шприца залишилось', many: '${count} шприців залишилось', + other: '${count} шприца залишилось', ), 'wipeRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1067,6 +1096,7 @@ extension on TranslationsUk { one: '1 серветка залишилось', few: '${count} серветки залишилось', many: '${count} серветок залишилось', + other: '${count} серветки залишилось', ), 'needleRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1074,6 +1104,7 @@ extension on TranslationsUk { one: '1 голка залишилось', few: '${count} голки залишилось', many: '${count} голок залишилось', + other: '${count} голки залишилось', ), 'glovesRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1081,6 +1112,7 @@ extension on TranslationsUk { one: '1 рукавичка залишилось', few: '${count} рукавички залишилось', many: '${count} рукавичок залишилось', + other: '${count} рукавички залишилось', ), 'bandageRemaining' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1088,6 +1120,7 @@ extension on TranslationsUk { one: '1 пластир залишилось', few: '${count} пластирі залишилось', many: '${count} пластирів залишилось', + other: '${count} пластирі залишилось', ), 'administrationRouteUnitMl' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1095,6 +1128,7 @@ extension on TranslationsUk { one: 'ml', few: 'ml', many: 'ml', + other: 'ml', ), 'administrationRouteUnitPill' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1102,6 +1136,7 @@ extension on TranslationsUk { one: 'пігулка', few: 'пігулки', many: 'пігулок', + other: 'пігулки', ), 'administrationRouteUnitPatch' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1109,6 +1144,7 @@ extension on TranslationsUk { one: 'патч', few: 'патчі', many: 'патчів', + other: 'патчі', ), 'administrationRouteUnitPump' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1116,6 +1152,7 @@ extension on TranslationsUk { one: 'натискання', few: 'натискання', many: 'натискань', + other: 'натискання', ), 'administrationRouteUnitImplant' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1123,6 +1160,7 @@ extension on TranslationsUk { one: 'імплант', few: 'імпланти', many: 'імплантів', + other: 'імпланти', ), 'administrationRouteUnitSuppository' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1130,6 +1168,7 @@ extension on TranslationsUk { one: 'супозиторій', few: 'супозиторії', many: 'супозиторіїв', + other: 'супозиторії', ), 'administrationRouteUnitSpray' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1137,6 +1176,7 @@ extension on TranslationsUk { one: 'спрей', few: 'спреї', many: 'спреїв', + other: 'спреї', ), 'scheduleFrequencyOnDayEveryNMonths' => ( {required num count, required Object day}) => @@ -1145,6 +1185,7 @@ extension on TranslationsUk { one: 'День ${day}, кожен ${count} місяць', few: 'День ${day}, кожні ${count} місяці', many: 'День ${day}, кожні ${count} місяців', + other: 'День ${day}, кожні ${count} місяці', ), 'onHrtForDays' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1152,6 +1193,7 @@ extension on TranslationsUk { one: 'На ЗГТ вже ${count} день', few: 'На ЗГТ вже ${count} дні', many: 'На ЗГТ вже ${count} днів', + other: 'На ЗГТ вже ${count} дні', ), 'onHrtForWeeks' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1159,6 +1201,7 @@ extension on TranslationsUk { one: 'На ЗГТ вже ${count} тиждень', few: 'На ЗГТ вже ${count} тижні', many: 'На ЗГТ вже ${count} тижнів', + other: 'На ЗГТ вже ${count} тижні', ), 'onHrtForMonths' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1166,6 +1209,7 @@ extension on TranslationsUk { one: 'На ЗГТ вже ${count} місяць', few: 'На ЗГТ вже ${count} місяці', many: 'На ЗГТ вже ${count} місяців', + other: 'На ЗГТ вже ${count} місяці', ), 'onHrtForYears' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1173,6 +1217,7 @@ extension on TranslationsUk { one: 'На ЗГТ вже ${count} рік', few: 'На ЗГТ вже ${count} роки', many: 'На ЗГТ вже ${count} років', + other: 'На ЗГТ вже ${count} роки', ), 'intakesLoggedCount' => ({required num count}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('uk'))( @@ -1180,6 +1225,7 @@ extension on TranslationsUk { one: 'Записано ${count} прийом', few: 'Записано ${count} прийоми', many: 'Записано ${count} прийомів', + other: 'Записано ${count} прийоми', ), _ => null, }; diff --git a/lib/services/db/app_database.dart b/lib/services/db/app_database.dart index cadecc9b5..e75b9eda5 100644 --- a/lib/services/db/app_database.dart +++ b/lib/services/db/app_database.dart @@ -15,6 +15,7 @@ import 'package:mona/services/db/upgrade/v19.dart'; import 'package:mona/services/db/upgrade/v2.dart'; import 'package:mona/services/db/upgrade/v20.dart'; import 'package:mona/services/db/upgrade/v21.dart'; +import 'package:mona/services/db/upgrade/v22.dart'; import 'package:mona/services/db/upgrade/v3.dart'; import 'package:mona/services/db/upgrade/v4.dart'; import 'package:mona/services/db/upgrade/v5.dart'; @@ -25,7 +26,7 @@ import 'package:mona/services/db/upgrade/v9.dart'; import 'package:path/path.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -const int currentDatabaseVersion = 21; +const int currentDatabaseVersion = 22; final Map _upgrades = { 2: DbUpgradeV2(), @@ -48,6 +49,7 @@ final Map _upgrades = { 19: DbUpgradeV19(), 20: DbUpgradeV20(), 21: DbUpgradeV21(), + 22: DbUpgradeV22(), }; class AppDatabase { diff --git a/lib/services/db/db_tables.dart b/lib/services/db/db_tables.dart index 067619abc..553185831 100644 --- a/lib/services/db/db_tables.dart +++ b/lib/services/db/db_tables.dart @@ -34,6 +34,7 @@ const String createMedicationIntakesTable = ''' notes TEXT, placements TEXT NOT NULL, dosingBasis TEXT NOT NULL, + injectionType TEXT, FOREIGN KEY (medicationSupplyItemId) REFERENCES supply_items(id) ON DELETE SET NULL, FOREIGN KEY (scheduleId) REFERENCES medication_schedules(id) ON DELETE SET NULL ) diff --git a/lib/services/db/historical_schemas.dart b/lib/services/db/historical_schemas.dart index 25e8a63e3..6883ce9e0 100644 --- a/lib/services/db/historical_schemas.dart +++ b/lib/services/db/historical_schemas.dart @@ -421,6 +421,30 @@ const String _medicationSchedulesV21 = ''' ) '''; +const String _medicationIntakesV22 = ''' + CREATE TABLE medication_intakes( + id INTEGER PRIMARY KEY AUTOINCREMENT, + scheduledTime TEXT, + takenDateTime TEXT, + takenTimeZone TEXT, + takenDose TEXT NOT NULL, + wastedAmount TEXT, + deadSpace TEXT, + scheduleId INTEGER, + molecule TEXT NOT NULL, + administrationRoute TEXT NOT NULL, + ester TEXT, + medicationSupplyItemId INTEGER, + genericSupplyItemIds TEXT NOT NULL, + notes TEXT, + placements TEXT NOT NULL, + dosingBasis TEXT NOT NULL, + injectionType TEXT, + FOREIGN KEY (medicationSupplyItemId) REFERENCES supply_items(id) ON DELETE SET NULL, + FOREIGN KEY (scheduleId) REFERENCES medication_schedules(id) ON DELETE SET NULL + ) + '''; + const Map> _historicalSchemas = { 4: [ _supplyItemsV4, @@ -530,6 +554,12 @@ const Map> _historicalSchemas = { _medicationSchedulesV21, _bloodTestsV20, ], + 22: [ + _supplyItemsV21, + _medicationIntakesV22, + _medicationSchedulesV21, + _bloodTestsV20, + ], }; List historicalSchemaFor(int version) { diff --git a/lib/services/db/upgrade/v22.dart b/lib/services/db/upgrade/v22.dart new file mode 100644 index 000000000..9af39f0e8 --- /dev/null +++ b/lib/services/db/upgrade/v22.dart @@ -0,0 +1,59 @@ +import 'package:mona/services/db/upgrade/db_upgrade.dart'; + +import 'package:sqflite/sqlite_api.dart'; + +class DbUpgradeV22 implements DbUpgrade { + @override + Future upgrade(Database db, int oldVersion, int newVersion) async { + await db.execute(''' + ALTER TABLE medication_intakes + ADD COLUMN injectionType TEXT + '''); + + await db.execute(''' + UPDATE medication_intakes + SET injectionType = 'intramuscular' + WHERE administrationRoute = 'injection' + '''); + + await _repairDoubleWrappedLevels(db); + } + + // repair v18 being ran on already-wrapped entries + Future _repairDoubleWrappedLevels(Database db) async { + const columns = ['estradiolLevels', 'testosteroneLevels']; + final rows = await db.query('blood_tests', columns: ['id', ...columns]); + + for (final row in rows) { + final fixes = {}; + for (final column in columns) { + final value = row[column] as String?; + final unwrapped = _unwrapLevel(value); + if (unwrapped != value) { + fixes[column] = unwrapped; + } + } + if (fixes.isNotEmpty) { + await db.update('blood_tests', fixes, + where: 'id = ?', whereArgs: [row['id']]); + } + } + } + + static String? _unwrapLevel(String? value) { + if (value == null || !value.startsWith('{"value":"{')) return value; + + final innerStart = value.indexOf('{', 1); + var depth = 0; + for (var i = innerStart; i < value.length; i++) { + final char = value[i]; + if (char == '{') { + depth++; + } else if (char == '}') { + depth--; + if (depth == 0) return value.substring(innerStart, i + 1); + } + } + return value; + } +} diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index d1198f157..c5ffac8a1 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -9,6 +9,13 @@ import 'package:mona/util/string_parsing.dart'; import 'package:timezone/data/latest_all.dart' as tzdata; import 'package:timezone/timezone.dart' as tz; +typedef NotificationPayload = ({ + int scheduleId, + DateTime scheduledTime, + bool isRepeating, + int? notificationId, +}); + class NotificationService { static FlutterLocalNotificationsPlugin Function()? createPlugin = () => FlutterLocalNotificationsPlugin(); @@ -34,7 +41,8 @@ class NotificationService { _notificationsPlugin.resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>(); - Future initialize() async { + Future initialize( + {void Function(NotificationPayload tap)? onTap}) async { if (_initialized) return; tzdata.initializeTimeZones(); @@ -52,14 +60,66 @@ class NotificationService { ); await _notificationsPlugin.initialize( - settings: InitializationSettings( - android: androidSettings, - iOS: iosSettings, - )); + settings: InitializationSettings( + android: androidSettings, + iOS: iosSettings, + ), + onDidReceiveNotificationResponse: (response) { + final payload = + _decodePayload(response.payload, notificationId: response.id); + if (payload != null) onTap?.call(payload); + }, + ); _initialized = true; } + Future getAppLaunchTap() async { + final details = + await _notificationsPlugin.getNotificationAppLaunchDetails(); + if (details == null || !details.didNotificationLaunchApp) return null; + + final response = details.notificationResponse; + if (response == null) return null; + + return _decodePayload(response.payload, notificationId: response.id); + } + + static const _scheduleIdKey = 'scheduleId'; + static const _scheduledTimeKey = 'scheduledTime'; + static const _isRepeatingKey = 'isRepeating'; + + String _encodePayload({ + required int scheduleId, + required DateTime scheduledTime, + required bool isRepeating, + }) => + jsonEncode({ + _scheduleIdKey: scheduleId, + _scheduledTimeKey: scheduledTime.toIso8601String(), + if (isRepeating) _isRepeatingKey: true, + }); + + NotificationPayload? _decodePayload(String? raw, {int? notificationId}) { + if (raw == null || raw.isEmpty) return null; + + final decoded = jsonDecode(raw); + if (decoded is! Map) return null; + final scheduleId = decoded[_scheduleIdKey]; + final scheduledTime = + (decoded[_scheduledTimeKey] as String?)?.toDateTimeOrNull; + if (scheduleId is! int || scheduledTime == null) return null; + + return ( + scheduleId: scheduleId, + scheduledTime: scheduledTime, + isRepeating: decoded[_isRepeatingKey] == true, + notificationId: notificationId, + ); + } + + Future cancel(int id) => _notificationsPlugin.cancel(id: id); + NotificationDetails _notificationDetails() { return const NotificationDetails( android: AndroidNotificationDetails( @@ -121,6 +181,7 @@ class NotificationService { int? id, String? title, String? body, + String? payload, }) async { id ??= Random().nextInt(1 << 31); @@ -135,17 +196,20 @@ class NotificationService { title: title, body: body, notificationDetails: _notificationDetails(), + payload: payload, ); } Future scheduleNotification({ required int id, + required int scheduleId, required String title, required String body, required DateTime scheduledTime, }) => _schedule( id: id, + scheduleId: scheduleId, title: title, body: body, scheduledTime: scheduledTime, @@ -153,12 +217,14 @@ class NotificationService { Future scheduleDailyNotification({ required int id, + required int scheduleId, required String title, required String body, required DateTime firstOccurrence, }) => _schedule( id: id, + scheduleId: scheduleId, title: title, body: body, scheduledTime: firstOccurrence, @@ -167,12 +233,14 @@ class NotificationService { Future scheduleWeeklyNotification({ required int id, + required int scheduleId, required String title, required String body, required DateTime firstOccurrence, }) => _schedule( id: id, + scheduleId: scheduleId, title: title, body: body, scheduledTime: firstOccurrence, @@ -181,15 +249,17 @@ class NotificationService { Future _schedule({ required int id, + required int scheduleId, required String title, required String body, required DateTime scheduledTime, DateTimeComponents? matchComponents, }) async { - final payload = jsonEncode({ - 'scheduledTime': scheduledTime.toIso8601String(), - if (matchComponents != null) 'isRepeating': true, - }); + final payload = _encodePayload( + scheduleId: scheduleId, + scheduledTime: scheduledTime, + isRepeating: matchComponents != null, + ); final dateTime = tz.TZDateTime( tz.local, scheduledTime.year, @@ -229,9 +299,9 @@ class NotificationService { return pendingNotifications.where((notification) { final payload = jsonDecode(notification.payload ?? '{}'); - if (payload['isRepeating'] == true) return false; + if (payload[_isRepeatingKey] == true) return false; final scheduledTime = - (payload['scheduledTime'] as String?)?.toDateTimeOrNull; + (payload[_scheduledTimeKey] as String?)?.toDateTimeOrNull; if (scheduledTime == null) return false; return scheduledTime.isBefore(clock.now()); }).toList(); @@ -243,6 +313,7 @@ class NotificationService { await showNotification( title: notification.title, body: notification.body, + payload: notification.payload, ); } } diff --git a/lib/ui/views/home/settings/application_sites_page.dart b/lib/ui/views/home/settings/application_sites_page.dart index 318aefd3f..f61e4ed3f 100644 --- a/lib/ui/views/home/settings/application_sites_page.dart +++ b/lib/ui/views/home/settings/application_sites_page.dart @@ -6,7 +6,7 @@ import 'package:mona/i18n/helpers/placement_l10n.dart'; import 'package:mona/i18n/translations.g.dart'; import 'package:mona/services/preferences_service.dart'; import 'package:mona/ui/constants/dimensions.dart'; -import 'package:mona/ui/widgets/tinted_switch_tile.dart'; +import 'package:mona/ui/widgets/switch_tile.dart'; import 'package:provider/provider.dart'; class ApplicationSitesPage extends StatelessWidget { @@ -60,7 +60,7 @@ class ApplicationSitesPage extends StatelessWidget { return M3ESegmentedColumn( padding: EdgeInsets.zero, children: [ - TintedSwitchTile( + SwitchTile( key: const ValueKey('placementScopeToggle'), title: t.placementSuggestionPerScheduleTitle, subtitle: t.placementSuggestionPerScheduleDescription, diff --git a/lib/ui/views/home/settings/language_page.dart b/lib/ui/views/home/settings/language_page.dart index eaa94f939..47a9e7d2b 100644 --- a/lib/ui/views/home/settings/language_page.dart +++ b/lib/ui/views/home/settings/language_page.dart @@ -26,6 +26,8 @@ class LanguagePage extends StatefulWidget { 'fr': (english: 'French', native: 'Français'), 'gl': (english: 'Galician', native: 'Galego'), 'hi': (english: 'Hindi', native: 'हिन्दी'), + 'hu': (english: 'Hungarian', native: 'Magyar'), + 'id': (english: 'Indonesian', native: 'Bahasa Indonesia'), 'is': (english: 'Icelandic', native: 'Íslenska'), 'it': (english: 'Italian', native: 'Italiano'), 'ko': (english: 'Korean', native: '한국어'), @@ -120,7 +122,7 @@ class _LanguagePageState extends State { children: [ TappableListTile( leading: const Icon(Symbols.translate_rounded), - title: t.languageMissing, + title: t.missingTranslation, subtitle: t.translateAppDescription, onTap: () => launchUrl( Uri.parse(_weblateUrl), diff --git a/lib/ui/views/home/settings/schedules/edit_schedule/edit_schedule_scheduling_page.dart b/lib/ui/views/home/settings/schedules/edit_schedule/edit_schedule_scheduling_page.dart index e826a72f6..57a55c24a 100644 --- a/lib/ui/views/home/settings/schedules/edit_schedule/edit_schedule_scheduling_page.dart +++ b/lib/ui/views/home/settings/schedules/edit_schedule/edit_schedule_scheduling_page.dart @@ -9,8 +9,8 @@ import 'package:mona/ui/widgets/forms/form_spacer.dart'; import 'package:mona/ui/widgets/forms/form_text_field.dart'; import 'package:mona/ui/widgets/forms/model_form.dart'; import 'package:mona/ui/widgets/scheduling_type_picker.dart'; +import 'package:mona/ui/widgets/switch_tile.dart'; import 'package:mona/ui/widgets/time_list_card.dart'; -import 'package:mona/ui/widgets/tinted_switch_tile.dart'; import 'package:mona/ui/widgets/weekday_picker.dart'; import 'package:mona/util/regex_patterns.dart'; import 'package:mona/util/string_parsing.dart'; @@ -234,7 +234,7 @@ class _EditScheduleSchedulingPageState padding: EdgeInsets.zero, margin: EdgeInsets.symmetric(vertical: 8), children: [ - TintedSwitchTile( + SwitchTile( title: t.anchorToLastIntake, subtitle: t.anchorToLastIntakeDescription, value: _anchorToLastIntake, @@ -264,10 +264,11 @@ class _EditScheduleSchedulingPageState onEdit: _editTime, onDelete: _deleteTime, trailingChildren: [ - TintedSwitchTile( + SwitchTile( title: t.enableNotifications, subtitle: t.enableNotificationsDescription, value: _dailyNotify, + tintEnabled: false, onChanged: (value) => setState(() => _dailyNotify = value), ), ], diff --git a/lib/ui/views/home/settings/schedules/new_schedule_scheduling_page.dart b/lib/ui/views/home/settings/schedules/new_schedule_scheduling_page.dart index fb8396336..1c930ee18 100644 --- a/lib/ui/views/home/settings/schedules/new_schedule_scheduling_page.dart +++ b/lib/ui/views/home/settings/schedules/new_schedule_scheduling_page.dart @@ -15,8 +15,8 @@ import 'package:mona/ui/widgets/forms/form_spacer.dart'; import 'package:mona/ui/widgets/forms/form_text_field.dart'; import 'package:mona/ui/widgets/forms/model_form.dart'; import 'package:mona/ui/widgets/scheduling_type_picker.dart'; +import 'package:mona/ui/widgets/switch_tile.dart'; import 'package:mona/ui/widgets/time_list_card.dart'; -import 'package:mona/ui/widgets/tinted_switch_tile.dart'; import 'package:mona/ui/widgets/weekday_picker.dart'; import 'package:mona/util/regex_patterns.dart'; import 'package:mona/util/string_parsing.dart'; @@ -237,7 +237,7 @@ class _NewScheduleSchedulingPageState extends State { padding: EdgeInsets.zero, margin: EdgeInsets.symmetric(vertical: 8), children: [ - TintedSwitchTile( + SwitchTile( title: t.anchorToLastIntake, subtitle: t.anchorToLastIntakeDescription, value: _anchorToLastIntake, @@ -267,10 +267,11 @@ class _NewScheduleSchedulingPageState extends State { onEdit: _editTime, onDelete: _deleteTime, trailingChildren: [ - TintedSwitchTile( + SwitchTile( title: t.enableNotifications, subtitle: t.enableNotificationsDescription, value: _dailyNotify, + tintEnabled: false, onChanged: (value) => setState(() => _dailyNotify = value), ), ], diff --git a/lib/ui/views/home/settings/secret_settings_page.dart b/lib/ui/views/home/settings/secret_settings_page.dart index 178d5cd33..0e1c3f79d 100644 --- a/lib/ui/views/home/settings/secret_settings_page.dart +++ b/lib/ui/views/home/settings/secret_settings_page.dart @@ -3,7 +3,7 @@ import 'package:m3e_core/m3e_core.dart'; import 'package:mona/i18n/translations.g.dart'; import 'package:mona/services/preferences_service.dart'; import 'package:mona/ui/constants/dimensions.dart'; -import 'package:mona/ui/widgets/tinted_switch_tile.dart'; +import 'package:mona/ui/widgets/switch_tile.dart'; import 'package:provider/provider.dart'; class SecretSettingsPage extends StatelessWidget { @@ -22,7 +22,7 @@ class SecretSettingsPage extends StatelessWidget { M3ESegmentedColumn( padding: EdgeInsets.zero, children: [ - TintedSwitchTile( + SwitchTile( title: t.slimeMode, value: preferencesService.slimeModeEnabled, onChanged: (value) => diff --git a/lib/ui/views/home/settings/settings_page.dart b/lib/ui/views/home/settings/settings_page.dart index 7c28df9bb..47f61dc03 100644 --- a/lib/ui/views/home/settings/settings_page.dart +++ b/lib/ui/views/home/settings/settings_page.dart @@ -18,8 +18,8 @@ import 'package:mona/ui/views/home/settings/schedules/schedules_page.dart'; import 'package:mona/ui/views/home/settings/secret_settings_page.dart'; import 'package:mona/ui/views/home/settings/theme_page.dart'; import 'package:mona/ui/views/home/settings/units_page.dart'; +import 'package:mona/ui/widgets/switch_tile.dart'; import 'package:mona/ui/widgets/tappable_list_tile.dart'; -import 'package:mona/ui/widgets/tinted_switch_tile.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; @@ -222,7 +222,7 @@ class _SettingsPageState extends State )); }, ), - TintedSwitchTile( + SwitchTile( title: t.enableNotifications, subtitle: t.enableNotificationsDescription, value: _notificationsEnabled, @@ -327,7 +327,7 @@ class _SettingsPageState extends State ); }, ), - TintedSwitchTile( + SwitchTile( title: t.HrtCounter, subtitle: t.HrtCounterDescription, value: preferencesService.hrtCounterEnabled, @@ -342,7 +342,7 @@ class _SettingsPageState extends State M3ESegmentedColumn( padding: EdgeInsets.zero, children: [ - TintedSwitchTile( + SwitchTile( title: t.autoUpdate, subtitle: t.autoUpdateDescription, value: _autoCheckUpdatesEnabled, diff --git a/lib/ui/views/home/take_medication_page.dart b/lib/ui/views/home/take_medication_page.dart index f7d3dcbb5..ad42bd6d5 100644 --- a/lib/ui/views/home/take_medication_page.dart +++ b/lib/ui/views/home/take_medication_page.dart @@ -6,6 +6,7 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:mona/controllers/medication_intake_manager.dart'; import 'package:mona/data/model/administration_route.dart'; import 'package:mona/data/model/generic_supply_item.dart'; +import 'package:mona/data/model/injection_type.dart'; import 'package:mona/data/model/medication_intake.dart'; import 'package:mona/data/model/medication_schedule.dart'; import 'package:mona/data/model/medication_supply_item.dart'; @@ -22,6 +23,7 @@ import 'package:mona/ui/widgets/forms/form_info_text.dart'; import 'package:mona/ui/widgets/forms/form_spacer.dart'; import 'package:mona/ui/widgets/forms/form_text_field.dart'; import 'package:mona/ui/widgets/forms/model_form.dart'; +import 'package:mona/ui/widgets/injection_type_picker.dart'; import 'package:mona/ui/widgets/intake_supply_picker.dart'; import 'package:mona/ui/widgets/placement_picker.dart'; import 'package:mona/util/regex_patterns.dart'; @@ -55,6 +57,8 @@ class _TakeMedicationPageState extends State { Decimal? _deadSpace; late TextEditingController _notesController; bool _isTaken = false; + InjectionType? _injectionType; + bool _hasInitializedInjectionType = false; String? get _takenDoseError => MedicationIntake.validateDose(_takenDoseController.text); @@ -80,18 +84,20 @@ class _TakeMedicationPageState extends State { MedicationIntakeManager( medicationIntakeProvider, supplyItemProvider, preferencesService) .takeMedication( - takenDose: _takenDose, - scheduledTime: widget.scheduledTime, - takenDateTime: _takenDate.toUtc(), - medicationItem: _selectedSupplyItem is MedicationSupplyItem - ? _selectedSupplyItem as MedicationSupplyItem - : null, - genericItems: _selectedGenerics, - schedule: widget.schedule, - placements: _selectedPlacements, - deadSpace: _deadSpace, - notes: notes, - wastedAmount: _wastedAmount); + takenDose: _takenDose, + scheduledTime: widget.scheduledTime, + takenDateTime: _takenDate.toUtc(), + medicationItem: _selectedSupplyItem is MedicationSupplyItem + ? _selectedSupplyItem as MedicationSupplyItem + : null, + genericItems: _selectedGenerics, + schedule: widget.schedule, + placements: _selectedPlacements, + deadSpace: _deadSpace, + notes: notes, + wastedAmount: _wastedAmount, + injectionType: _injectionType, + ); setState(() { _isTaken = true; @@ -151,6 +157,12 @@ class _TakeMedicationPageState extends State { } } + void _onInjectionTypeChanged(InjectionType type) { + setState(() { + _injectionType = type; + }); + } + void _refresh() => setState(() {}); @override @@ -223,6 +235,14 @@ class _TakeMedicationPageState extends State { .firstWhere((item) => item?.id == selectedId, orElse: () => null); } + if (!isLoading && !_hasInitializedInjectionType) { + final manager = MedicationIntakeManager( + medicationIntakeProvider, supplyItemProvider, preferencesService); + _injectionType = + manager.suggestInjectionType(scheduleId: widget.schedule.id); + _hasInitializedInjectionType = true; + } + return ModelForm( title: t.takeMedication(scheduleName: widget.schedule.name), avatar: widget.schedule.administrationRoute.icon, @@ -254,6 +274,20 @@ class _TakeMedicationPageState extends State { infoText: supplyItem.localizedSupplyAmount(_takenDose), ), FormSpacer(), + if (usesPlacements && _orderedPlacements.isNotEmpty) + PlacementPicker( + options: _orderedPlacements, + selected: _selectedPlacements, + onChanged: _onPlacementChanged, + ), + if (isInjection) + InjectionTypePicker( + value: _injectionType ?? InjectionType.intramuscular, + onChanged: _onInjectionTypeChanged, + ), + if (isInjection || + (usesPlacements && _orderedPlacements.isNotEmpty)) + FormSpacer(), IntakeSupplyPicker( medicationItem: _selectedSupplyItem is MedicationSupplyItem ? _selectedSupplyItem as MedicationSupplyItem @@ -273,14 +307,6 @@ class _TakeMedicationPageState extends State { () => _selectedGenerics = [..._selectedGenerics, generic]), ), FormSpacer(), - if (usesPlacements && _orderedPlacements.isNotEmpty) ...[ - PlacementPicker( - options: _orderedPlacements, - selected: _selectedPlacements, - onChanged: _onPlacementChanged, - ), - FormSpacer(), - ], if (isInjection) ...[ FormTextField( controller: _wastedAmountController, diff --git a/lib/ui/views/intakes/edit_intake_page.dart b/lib/ui/views/intakes/edit_intake_page.dart index e31dd2fd0..9ec9dd28d 100644 --- a/lib/ui/views/intakes/edit_intake_page.dart +++ b/lib/ui/views/intakes/edit_intake_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:mona/controllers/medication_intake_manager.dart'; import 'package:mona/data/model/administration_route.dart'; import 'package:mona/data/model/generic_supply_item.dart'; +import 'package:mona/data/model/injection_type.dart'; import 'package:mona/data/model/medication_intake.dart'; import 'package:mona/data/model/medication_supply_item.dart'; import 'package:mona/data/model/placement.dart'; @@ -21,6 +22,7 @@ import 'package:mona/ui/widgets/forms/form_info_text.dart'; import 'package:mona/ui/widgets/forms/form_spacer.dart'; import 'package:mona/ui/widgets/forms/form_text_field.dart'; import 'package:mona/ui/widgets/forms/model_form.dart'; +import 'package:mona/ui/widgets/injection_type_picker.dart'; import 'package:mona/ui/widgets/intake_supply_picker.dart'; import 'package:mona/ui/widgets/placement_picker.dart'; import 'package:mona/util/regex_patterns.dart'; @@ -51,6 +53,7 @@ class _EditIntakePageState extends State { List _selectedGenerics = []; bool _hasInitializedSupplyItem = false; late TextEditingController _notesController; + InjectionType? _injectionType; String? get _takenDoseError => MedicationIntake.validateDose(_takenDoseController.text); @@ -100,6 +103,7 @@ class _EditIntakePageState extends State { ? _selectedSupplyItem as MedicationSupplyItem : null, genericItems: _selectedGenerics, + injectionType: _injectionType, notes: notes, ); @@ -170,6 +174,12 @@ class _EditIntakePageState extends State { } } + void _onInjectionTypeChanged(InjectionType type) { + setState(() { + _injectionType = type; + }); + } + void _refresh() => setState(() {}); Future confirmDeleteIntake(BuildContext context) { @@ -183,6 +193,7 @@ class _EditIntakePageState extends State { _takenDose = widget.intake.takenDose; _wastedAmount = widget.intake.wastedAmount ?? Decimal.zero; _deadSpace = widget.intake.deadSpace ?? Decimal.zero; + _injectionType = widget.intake.injectionType; _takenDoseController = TextEditingController(text: _takenDose.toString()); _wastedAmountController = TextEditingController(text: _wastedAmount.toString()); @@ -276,6 +287,21 @@ class _EditIntakePageState extends State { infoText: supplyItem.localizedSupplyAmount(_takenDose), ), FormSpacer(), + if (_usesPlacements && preferencesService.placementsList.isNotEmpty) + PlacementPicker( + options: preferencesService.placementsList, + selected: _selectedPlacements, + onChanged: _onPlacementChanged, + ), + if (_isInjection) + InjectionTypePicker( + value: _injectionType ?? InjectionType.intramuscular, + onChanged: _onInjectionTypeChanged, + ), + if (_isInjection || + (_usesPlacements && + preferencesService.placementsList.isNotEmpty)) + FormSpacer(), IntakeSupplyPicker( medicationItem: _selectedSupplyItem is MedicationSupplyItem ? _selectedSupplyItem as MedicationSupplyItem @@ -295,15 +321,6 @@ class _EditIntakePageState extends State { () => _selectedGenerics = [..._selectedGenerics, generic]), ), FormSpacer(), - if (_usesPlacements && - preferencesService.placementsList.isNotEmpty) ...[ - PlacementPicker( - options: preferencesService.placementsList, - selected: _selectedPlacements, - onChanged: _onPlacementChanged, - ), - FormSpacer(), - ], if (_isInjection) ...[ FormTextField( controller: _wastedAmountController, diff --git a/lib/ui/views/levels/main_graph_page/chart_date_buttons.dart b/lib/ui/views/levels/main_graph_page/chart_date_buttons.dart index 8d1309b31..01af3e1d7 100644 --- a/lib/ui/views/levels/main_graph_page/chart_date_buttons.dart +++ b/lib/ui/views/levels/main_graph_page/chart_date_buttons.dart @@ -1,9 +1,9 @@ +import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:m3e_core/m3e_core.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:mona/i18n/build_context_extensions.dart'; -import 'package:mona/ui/views/levels/main_graph_page/chart_range_selector.dart'; class ChartDateButtons extends StatelessWidget { final int index; @@ -62,17 +62,5 @@ class ChartDateButtons extends StatelessWidget { } } - void _resetToToday() { - final duration = LevelDuration.values[index]; - final today = DateTime.now(); - final newStartDate = switch (duration) { - LevelDuration.week => today.subtract(Duration(days: 3)), - LevelDuration.twoWeeks => today.subtract(Duration(days: 7)), - LevelDuration.month => today.subtract(Duration(days: 15)), - LevelDuration.threeMonths => today.subtract(Duration(days: 45)), - LevelDuration.sixMonths => today.subtract(Duration(days: 90)), - LevelDuration.year => today.subtract(Duration(days: 180)), - }; - onStartDateChanged(newStartDate); - } + void _resetToToday() => onStartDateChanged(clock.now()); } diff --git a/lib/ui/views/levels/main_graph_page/chart_page.dart b/lib/ui/views/levels/main_graph_page/chart_page.dart index 23dcefaed..c2472989c 100644 --- a/lib/ui/views/levels/main_graph_page/chart_page.dart +++ b/lib/ui/views/levels/main_graph_page/chart_page.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:mona/data/providers/medication_intake_provider.dart'; @@ -18,13 +19,14 @@ class ChartPage extends StatefulWidget { } class _ChartPageState extends State with MinuteTicker { - double sliderValue = 0; LevelDuration _duration = LevelDuration.twoWeeks; - DateTime startDate = DateTime.now().subtract(const Duration(days: 9)); + DateTime targetDate = clock.now().subtract(const Duration(days: 2)); bool _isPanning = false; @override Widget build(BuildContext context) { + final dayBoundaries = _dayBoundaries(); + return LiquidGlassBottomClamp( child: Scaffold( appBar: AppBar(title: Text(t.estradiolLevelsTitle)), @@ -56,10 +58,10 @@ class _ChartPageState extends State with MinuteTicker { onHorizontalDragUpdate: (details) { final width = constraints.maxWidth; if (width <= 0) return; - final shift = _offset.inMicroseconds * + final shift = _duration.days.inMicroseconds * ((details.primaryDelta ?? 0) / width); setState(() { - startDate = startDate.subtract( + targetDate = targetDate.subtract( Duration(microseconds: shift.round()), ); }); @@ -67,8 +69,8 @@ class _ChartPageState extends State with MinuteTicker { onHorizontalDragEnd: (_) => setState(() => _isPanning = false), child: MainGraph( - startDate: startDate, - endDate: startDate.add(_offset), + startDate: dayBoundaries.$1, + endDate: dayBoundaries.$2, isPanning: _isPanning, ), ); @@ -83,9 +85,9 @@ class _ChartPageState extends State with MinuteTicker { bottom: 24), child: ChartDateButtons( index: _duration.index, - startDate: startDate, - onStartDateChanged: (newStartDate) => setState(() { - startDate = newStartDate; + startDate: targetDate, + onStartDateChanged: (newTargetDate) => setState(() { + targetDate = newTargetDate; }), ), ), @@ -98,20 +100,19 @@ class _ChartPageState extends State with MinuteTicker { ); } - Duration get _offset { - switch (_duration) { - case LevelDuration.week: - return const Duration(days: 7); - case LevelDuration.twoWeeks: - return const Duration(days: 14); - case LevelDuration.month: - return const Duration(days: 30); - case LevelDuration.threeMonths: - return const Duration(days: 90); - case LevelDuration.sixMonths: - return const Duration(days: 180); - case LevelDuration.year: - return const Duration(days: 365); - } + (DateTime, DateTime) _dayBoundaries() { + final (daysBefore, daysAfter) = switch (_duration) { + LevelDuration.week => (3, 3), + LevelDuration.twoWeeks => (7, 6), + LevelDuration.month => (15, 14), + LevelDuration.threeMonths => (45, 44), + LevelDuration.sixMonths => (90, 89), + LevelDuration.year => (182, 182), + }; + + return ( + targetDate.subtract(Duration(days: daysBefore)), + targetDate.add(Duration(days: daysAfter)), + ); } } diff --git a/lib/ui/views/levels/main_graph_page/chart_range_selector.dart b/lib/ui/views/levels/main_graph_page/chart_range_selector.dart index 5fc67d4cd..4961ed47c 100644 --- a/lib/ui/views/levels/main_graph_page/chart_range_selector.dart +++ b/lib/ui/views/levels/main_graph_page/chart_range_selector.dart @@ -35,7 +35,18 @@ class ChartRangeSelector extends StatelessWidget { } } -enum LevelDuration { week, twoWeeks, month, threeMonths, sixMonths, year } +enum LevelDuration { + week(Duration(days: 7)), + twoWeeks(Duration(days: 14)), + month(Duration(days: 30)), + threeMonths(Duration(days: 90)), + sixMonths(Duration(days: 180)), + year(Duration(days: 365)); + + const LevelDuration(this.days); + + final Duration days; +} extension _DurationLabel on LevelDuration { String get label => switch (this) { diff --git a/lib/ui/views/main_tabs.dart b/lib/ui/views/main_tabs.dart index e9d516534..2bf80a827 100644 --- a/lib/ui/views/main_tabs.dart +++ b/lib/ui/views/main_tabs.dart @@ -1,8 +1,9 @@ // main_tabs.dart import 'package:flutter/material.dart'; -import 'package:material_symbols_icons/material_symbols_icons.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:mona/i18n/translations.g.dart'; import 'package:mona/ui/views/levels/blood_tests_page/blood_test_page.dart'; +import 'package:mona/ui/views/levels/blood_tests_page/new_blood_test_page.dart'; import 'package:mona/ui/views/levels/levels_page/levels_page.dart'; import 'home/home_page.dart'; import 'home/settings/settings_page.dart'; @@ -52,6 +53,7 @@ List getMainTabs(BuildContext context) { title: t.nav_levels, page: const LevelsPage(), icon: Symbols.labs_rounded, + navKey: const ValueKey('navTabLevels'), backgroundColor: Theme.of(context).colorScheme.surfaceContainer, buildActions: (context) => [ IconButton( @@ -63,6 +65,18 @@ List getMainTabs(BuildContext context) { }, ), ], + buildFab: (context) => FloatingActionButton( + tooltip: t.addBloodTest, + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (context) => NewBloodTestPage(), + ), + ); + }, + child: const Icon(Symbols.add_rounded), + ), ), MainTabConfig( title: t.nav_supplies, diff --git a/lib/ui/widgets/injection_type_picker.dart b/lib/ui/widgets/injection_type_picker.dart new file mode 100644 index 000000000..0ed43782d --- /dev/null +++ b/lib/ui/widgets/injection_type_picker.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:m3e_core/m3e_core.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:mona/data/model/injection_type.dart'; +import 'package:mona/i18n/helpers/injection_type_l10n.dart'; +import 'package:mona/i18n/translations.g.dart'; +import 'package:mona/ui/widgets/tappable_list_tile.dart'; + +class InjectionTypePicker extends StatelessWidget { + final InjectionType value; + final ValueChanged onChanged; + + const InjectionTypePicker({ + super.key, + required this.value, + required this.onChanged, + }); + + Future _openTypeSheet(BuildContext context) async { + final selected = await showModalBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + builder: (sheetContext) => SafeArea( + child: ListView( + shrinkWrap: true, + children: [ + for (final type in InjectionType.values) + ListTile( + title: Text(type.localizedName), + trailing: + type == value ? const Icon(Symbols.check_rounded) : null, + onTap: () => Navigator.of(sheetContext).pop(type), + ), + ], + ), + ), + ); + + if (selected == null || selected == value) return; + onChanged(selected); + } + + @override + Widget build(BuildContext context) { + return M3ESegmentedColumn( + padding: EdgeInsets.zero, + margin: EdgeInsets.symmetric(vertical: 8), + children: [ + TappableListTile( + title: value.localizedName, + subtitle: t.injectionType, + trailing: const Icon(Symbols.syringe_rounded), + onTap: () => _openTypeSheet(context), + ), + ], + ); + } +} diff --git a/lib/ui/widgets/intake_supply_picker.dart b/lib/ui/widgets/intake_supply_picker.dart index c99865db9..d7422655e 100644 --- a/lib/ui/widgets/intake_supply_picker.dart +++ b/lib/ui/widgets/intake_supply_picker.dart @@ -126,6 +126,11 @@ class IntakeSupplyPicker extends StatelessWidget { title: Text(item.name), subtitle: Text(item.localizedConcentrationAndRemaining), trailing: IconButton( + style: IconButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: Size.zero, + padding: EdgeInsets.zero, + ), icon: const Icon(Symbols.close_rounded), onPressed: onRemoveMedication, ), @@ -140,6 +145,11 @@ class IntakeSupplyPicker extends StatelessWidget { title: Text(generic.name), subtitle: Text(generic.localizedSummary), trailing: IconButton( + style: IconButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: Size.zero, + padding: EdgeInsets.zero, + ), icon: const Icon(Symbols.close_rounded), onPressed: () => onRemoveGenericAt(index), ), diff --git a/lib/ui/widgets/tinted_switch_tile.dart b/lib/ui/widgets/switch_tile.dart similarity index 81% rename from lib/ui/widgets/tinted_switch_tile.dart rename to lib/ui/widgets/switch_tile.dart index 4bb23fa92..09a5a51c9 100644 --- a/lib/ui/widgets/tinted_switch_tile.dart +++ b/lib/ui/widgets/switch_tile.dart @@ -1,18 +1,20 @@ import 'package:flutter/material.dart'; -class TintedSwitchTile extends StatelessWidget { - const TintedSwitchTile({ +class SwitchTile extends StatelessWidget { + const SwitchTile({ super.key, required this.title, required this.value, required this.onChanged, this.subtitle, + this.tintEnabled = true, }); final String title; final String? subtitle; final bool value; final ValueChanged onChanged; + final bool tintEnabled; @override Widget build(BuildContext context) { @@ -22,7 +24,7 @@ class TintedSwitchTile extends StatelessWidget { title: Text(title), subtitle: subtitle == null ? null : Text(subtitle!), value: value, - tileColor: value + tileColor: value && tintEnabled ? Theme.of(context).colorScheme.primaryContainer.withValues( alpha: 0.4, ) diff --git a/pubspec.lock b/pubspec.lock index 4d2c72eca..a5c67ad39 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -432,7 +432,7 @@ packages: source: sdk version: "0.0.0" flutter_driver: - dependency: transitive + dependency: "direct dev" description: flutter source: sdk version: "0.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index e7a9bf5ed..5926b3009 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + flutter_driver: + sdk: flutter integration_test: sdk: flutter patrol: ^4.6.1 diff --git a/scripts/i18n.sh b/scripts/i18n.sh index 8ab1ad32b..2b0d68207 100755 --- a/scripts/i18n.sh +++ b/scripts/i18n.sh @@ -5,8 +5,8 @@ set -euo pipefail cd "$(dirname "$0")/.." -fvm dart run tool/i18next_to_slang.dart +fvm dart run tool/i18n/i18next_to_slang.dart fvm dart run slang -fvm dart run tool/i18next_to_android_widget_res.dart -fvm dart run tool/sanitize_generated.dart +fvm dart run tool/i18n/i18next_to_android_widget_res.dart +fvm dart run tool/i18n/sanitize_generated.dart fvm dart format lib/i18n/ diff --git a/scripts/screenshots.sh b/scripts/screenshots.sh new file mode 100755 index 000000000..157885349 --- /dev/null +++ b/scripts/screenshots.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# Generates in-app store screenshots for each mapped language. +# +# Usage: +# scripts/screenshots.sh [--platform android|ios|all] [--locale ] [--frames-only] +# +# --frames-only to skip captures +# Env overrides: +# SCREENSHOT_ANDROID_DEVICE adb device id (default: first attached device) +# SCREENSHOT_IOS_DEVICE simulator name (default: "iPhone 17 Pro Max") + +set -euo pipefail + +PLATFORM="all" +ONLY_LOCALE="" +FRAMES_ONLY=0 + +while [ $# -gt 0 ]; do + case "$1" in + --platform) PLATFORM="$2"; shift 2 ;; + --locale) ONLY_LOCALE="$2"; shift 2 ;; + --frames-only) FRAMES_ONLY=1; shift ;; + *) echo "Unknown argument: $1" >&2; exit 2 ;; + esac +done + +MAP="fastlane/screenshot_locales.json" +TARGET="tool/screenshots/screenshots.dart" +DRIVER="tool/screenshots/screenshots_driver.dart" + +[ -f "$MAP" ] || { echo "Missing locale map: $MAP" >&2; exit 1; } +jq -e . "$MAP" >/dev/null 2>&1 || { echo "Malformed locale map: $MAP" >&2; exit 1; } + +# Read the app-locale tags, optionally filtered to one. +APP_TAGS=() +while IFS= read -r tag; do + APP_TAGS+=("$tag") +done < <(jq -r '.[].app' "$MAP") + +resolve_ios_udid() { + local name="${SCREENSHOT_IOS_DEVICE:-iPhone 17 Pro Max}" + local udid + udid=$(xcrun simctl list devices available --json \ + | jq -r --arg n "$name" \ + '.devices | to_entries[] | .value[] | select(.name==$n) | .udid' \ + | head -n1) || true + [ -n "$udid" ] || { echo "No available iOS simulator named '$name'." >&2; exit 1; } + xcrun simctl boot "$udid" >/dev/null 2>&1 || true + echo "$udid" +} + +resolve_android_device() { + local dev="${SCREENSHOT_ANDROID_DEVICE:-}" + if [ -z "$dev" ]; then + dev=$(adb devices | awk 'NR>1 && $2=="device"{print $1; exit}') + fi + [ -n "$dev" ] || { echo "No Android device. Start an emulator first." >&2; exit 1; } + echo "$dev" +} + +drive_one() { + local platform="$1" app_tag="$2" device="$3"; shift 3 + local out="build/screenshots/$platform/$app_tag" + rm -rf "$out"; mkdir -p "$out" + echo "==> $platform / $app_tag" + + local ios_udid="" + local android_serial="" + if [ "$platform" = "ios" ]; then + ios_udid="$device" + else + android_serial="$device" + fi + + SCREENSHOT_OUT="$out" SCREENSHOT_IOS_UDID="$ios_udid" \ + SCREENSHOT_ANDROID_SERIAL="$android_serial" fvm flutter drive \ + --driver="$DRIVER" \ + --target="$TARGET" \ + -d "$device" \ + --dart-define=SCREENSHOT_LOCALE="$app_tag" \ + --dart-define=SCREENSHOT_DEFAULT_COLORS=true \ + "$@" + + if [ "$platform" = "ios" ]; then + xcrun simctl status_bar "$device" clear || true + else + adb -s "$device" shell am broadcast \ + -a com.android.systemui.demo -e command exit >/dev/null 2>&1 || true + fi +} + +frame_one() { + local platform="$1" app_tag="$2" out_dir="$3" + local in_dir="build/screenshots/$platform/$app_tag" + [ -d "$in_dir" ] || { echo "No raw screenshots in $in_dir" >&2; return 1; } + local captions="fastlane/screenshot_captions/$app_tag.json" + rm -rf "$out_dir"; mkdir -p "$out_dir" + fvm flutter test tool/screenshots/frame_screenshots.dart \ + --dart-define=FRAME_INPUT_DIR="$in_dir" \ + --dart-define=FRAME_OUTPUT_DIR="$out_dir" \ + --dart-define=FRAME_CAPTIONS="$captions" + echo "-- framed $platform / $app_tag -> $out_dir" +} + +run_platform() { + local platform="$1" + local device="" + local flavor_args=() + if [ "$FRAMES_ONLY" != "1" ]; then + if [ "$platform" = "android" ]; then + device="$(resolve_android_device)" + flavor_args=(--flavor store) + else + device="$(resolve_ios_udid)" + # iOS: default scheme until a `store` scheme exists in Xcode. + flavor_args=() + fi + fi + + for app_tag in ${APP_TAGS[@]+"${APP_TAGS[@]}"}; do + [ -n "$ONLY_LOCALE" ] && [ "$app_tag" != "$ONLY_LOCALE" ] && continue + + local store_tag + if [ "$platform" = "android" ]; then + store_tag=$(jq -r --arg a "$app_tag" '.[] | select(.app==$a) | .play' "$MAP") + else + store_tag=$(jq -r --arg a "$app_tag" '.[] | select(.app==$a) | .appstore' "$MAP") + fi + + if [ "$store_tag" = "null" ] || [ -z "$store_tag" ]; then + echo "-- skip $platform / $app_tag (no store locale)" + continue + fi + + local dest + if [ "$platform" = "android" ]; then + dest="fastlane/metadata/android/$store_tag/images/phoneScreenshots" + else + dest="fastlane/screenshots/$store_tag" + fi + + if [ "$FRAMES_ONLY" != "1" ]; then + drive_one "$platform" "$app_tag" "$device" ${flavor_args[@]+"${flavor_args[@]}"} + fi + frame_one "$platform" "$app_tag" "$dest" + done +} + +case "$PLATFORM" in + android) run_platform android ;; + ios) run_platform ios ;; + all) run_platform android; run_platform ios ;; + *) echo "Unknown platform: $PLATFORM" >&2; exit 2 ;; +esac + +echo "Done." diff --git a/test/controllers/medication_intake_manager_test.dart b/test/controllers/medication_intake_manager_test.dart index aa928f8e0..da0a9216d 100644 --- a/test/controllers/medication_intake_manager_test.dart +++ b/test/controllers/medication_intake_manager_test.dart @@ -8,6 +8,7 @@ import 'package:mona/data/model/administration_route.dart'; import 'package:mona/data/model/dosing_basis.dart'; import 'package:mona/data/model/ester.dart'; import 'package:mona/data/model/generic_supply_item.dart'; +import 'package:mona/data/model/injection_type.dart'; import 'package:mona/data/model/medication_intake.dart'; import 'package:mona/data/model/medication_supply_item.dart'; import 'package:mona/data/model/molecule.dart'; @@ -148,6 +149,7 @@ void main() { schedule: schedule, placements: [aPlacement(preset: PlacementPreset.leftThigh)], notes: notes, + injectionType: InjectionType.subcutaneous, ); }); @@ -212,6 +214,11 @@ void main() { // Assert expect(addedIntake.notes, notes); }); + + test('sets injectionType on the intake from the schedule', () { + // Assert + expect(addedIntake.injectionType, InjectionType.subcutaneous); + }); }); group('no supply items', () { @@ -827,12 +834,14 @@ void main() { medicationSupplyItemId: null, wastedAmount: null, deadSpace: null, + injectionType: InjectionType.subcutaneous, ); final newDose = Decimal.parse('3'); final newWasted = Decimal.parse('0.2'); final newDeadSpace = Decimal.parse('50'); final newTimezone = 'Europe/Paris'; final newNotes = 'edited'; + final newType = InjectionType.intramuscular; when(mockMedicationIntakeProvider.updateIntake(any)) .thenAnswer((inv) async { @@ -850,6 +859,7 @@ void main() { placements: [aCustomPlacement('belly')], medicationItem: null, notes: newNotes, + injectionType: newType, ); // Assert @@ -866,7 +876,8 @@ void main() { [aCustomPlacement('belly')]) .having((i) => i.notes, 'notes', newNotes) .having((i) => i.medicationSupplyItemId, 'medicationSupplyItemId', - isNull), + isNull) + .having((i) => i.injectionType, 'injectionType', newType), ); }); @@ -1349,6 +1360,51 @@ void main() { }); }); + group('suggestInjectionType', () { + test('returns null when the schedule is not an injection', () { + // Arrange + when(mockMedicationIntakeProvider.getTakenIntakesDescForSchedule(1234)) + .thenReturn([ + aMedicationIntake(administrationRoute: AdministrationRoute.oral) + ]); + + // Act + final suggestion = manager.suggestInjectionType(scheduleId: 1234); + + // Assert + expect(suggestion, isNull); + }); + + test('returns null when there are no intakes for a schedule', () { + // Arrange + when(mockMedicationIntakeProvider.getTakenIntakesDescForSchedule(1234)) + .thenReturn([]); + + // Act + final suggestion = manager.suggestInjectionType(scheduleId: 1234); + + // Assert + expect(suggestion, isNull); + }); + + test('returns the injection type of the last intake', () { + // Arrange + when(mockMedicationIntakeProvider.getLastTakenIntakeForSchedule(1234)) + .thenReturn( + aMedicationIntake( + administrationRoute: AdministrationRoute.injection, + injectionType: InjectionType.subcutaneous, + ), + ); + + // Act + final suggestion = manager.suggestInjectionType(scheduleId: 1234); + + // Assert + expect(suggestion, InjectionType.subcutaneous); + }); + }); + group('suggestMedicationItem', () { test('suggests the previous intake vial when it still matches', () { // Arrange diff --git a/test/controllers/notification_scheduler_test.dart b/test/controllers/notification_scheduler_test.dart index 96d82b310..2421de739 100644 --- a/test/controllers/notification_scheduler_test.dart +++ b/test/controllers/notification_scheduler_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:clock/clock.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -272,6 +274,35 @@ void main() { }); }); + group('payload', () { + String capturedScheduledPayload() => verify(plugin.zonedSchedule( + id: anyNamed('id'), + title: anyNamed('title'), + body: anyNamed('body'), + scheduledDate: anyNamed('scheduledDate'), + notificationDetails: anyNamed('notificationDetails'), + androidScheduleMode: anyNamed('androidScheduleMode'), + matchDateTimeComponents: anyNamed('matchDateTimeComponents'), + payload: captureAnyNamed('payload'), + )).captured.single as String; + + test('embeds the schedule id in the scheduled payload', () async { + // Arrange + when(planner.planNotifications(daysAhead: anyNamed('daysAhead'))) + .thenReturn( + [anOccurrencePlan(schedule: aMedicationSchedule(id: 42))]); + final sut = NotificationScheduler(planner, preferences); + + // Act + await sut.regenerateAll('en'); + + // Assert + final payload = + jsonDecode(capturedScheduledPayload()) as Map; + expect(payload['scheduleId'], 42); + }); + }); + group('notification ids', () { List capturedScheduledIds() => verify(plugin.zonedSchedule( id: captureAnyNamed('id'), diff --git a/test/controllers/slot_finder_test.dart b/test/controllers/slot_finder_test.dart new file mode 100644 index 000000000..23ddb515b --- /dev/null +++ b/test/controllers/slot_finder_test.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mona/controllers/slot_finder.dart'; +import 'package:mona/data/model/date.dart'; +import 'package:mona/data/model/intake_slot.dart'; +import 'package:mona/data/model/medication_schedule.dart'; +import 'package:mona/data/model/scheduling_strategy.dart'; + +import '../fixtures.dart'; + +void main() { + DateTime aScheduledTime({TimeOfDay? at}) => + DateTime(2026, 2, 8, at?.hour ?? 0, at?.minute ?? 0); + + IntakeSlot aSlot( + MedicationSchedule schedule, { + TimeOfDay? time, + bool taken = false, + }) => + IntakeSlot( + schedule: schedule, + status: taken ? ScheduleStatus.taken : ScheduleStatus.upcoming, + date: Date.today(), + time: time, + intake: taken ? aMedicationIntake(scheduleId: schedule.id) : null, + ); + + group('findSlot', () { + test('finds the correct target for a non-taken slot', () { + // Arrange + final schedule = aMedicationSchedule(); + final slots = [aSlot(schedule)]; + + // Act + final target = findSlot(schedule.id, aScheduledTime(), slots); + + // Assert + expect(target, slots.single); + }); + + test('returns null when the matching slot is already taken', () { + // Arrange + final schedule = aMedicationSchedule(); + final slots = [aSlot(schedule, taken: true)]; + + // Act + final target = findSlot(schedule.id, aScheduledTime(), slots); + + // Assert + expect(target, isNull); + }); + + test('returns null when no slot matches the schedule id', () { + // Arrange + final tapped = aMedicationSchedule(); + final other = aMedicationSchedule(); + final slots = [aSlot(other)]; + + // Act + final target = findSlot(tapped.id, aScheduledTime(), slots); + + // Assert + expect(target, isNull); + }); + + test('matches the daily slot whose time of day equals the scheduled time', + () { + // Arrange + final schedule = aMedicationSchedule(); + final slots = [ + aSlot(schedule, time: morning), + aSlot(schedule, time: afternoon), + ]; + + // Act + final target = + findSlot(schedule.id, aScheduledTime(at: afternoon), slots); + + // Assert + expect(target, slots.last); + }); + + test('returns null when no daily slot matches the scheduled time', () { + // Arrange + final schedule = aMedicationSchedule(); + final slots = [ + aSlot(schedule, time: morning), + aSlot(schedule, time: afternoon), + ]; + + // Act + final target = findSlot(schedule.id, aScheduledTime(at: evening), slots); + + // Assert + expect(target, isNull); + }); + }); +} diff --git a/test/fixtures.dart b/test/fixtures.dart index 2532f138d..800d0f330 100644 --- a/test/fixtures.dart +++ b/test/fixtures.dart @@ -8,6 +8,7 @@ import 'package:mona/data/model/date.dart'; import 'package:mona/data/model/dosing_basis.dart'; import 'package:mona/data/model/ester.dart'; import 'package:mona/data/model/generic_supply_item.dart'; +import 'package:mona/data/model/injection_type.dart'; import 'package:mona/data/model/medication_intake.dart'; import 'package:mona/data/model/medication_schedule.dart'; import 'package:mona/data/model/medication_supply_item.dart'; @@ -131,6 +132,7 @@ MedicationIntake aMedicationIntake({ Ester? ester, List placements = const [], DosingBasis dosingBasis = DosingBasis.mass, + InjectionType? injectionType, }) { final takenAt = !taken ? null @@ -154,6 +156,7 @@ MedicationIntake aMedicationIntake({ ester: ester, placements: placements, dosingBasis: dosingBasis, + injectionType: injectionType, ); } diff --git a/test/services/notification_service_test.dart b/test/services/notification_service_test.dart index 73c30d16d..7ae90b525 100644 --- a/test/services/notification_service_test.dart +++ b/test/services/notification_service_test.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:clock/clock.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mona/services/notification_service.dart'; @@ -12,6 +13,8 @@ class FakeFlutterLocalNotificationsPlugin implements FlutterLocalNotificationsPlugin { final List> scheduled = []; final List> shown = []; + void Function(NotificationResponse)? capturedOnTap; + NotificationAppLaunchDetails? launchDetails; @override Future initialize({ @@ -20,6 +23,7 @@ class FakeFlutterLocalNotificationsPlugin void Function(NotificationResponse)? onDidReceiveBackgroundNotificationResponse, }) async { + capturedOnTap = onDidReceiveNotificationResponse; return true; } @@ -87,7 +91,7 @@ class FakeFlutterLocalNotificationsPlugin @override Future getNotificationAppLaunchDetails() async { - return null; + return launchDetails; } @override @@ -124,9 +128,16 @@ void main() { late FakeFlutterLocalNotificationsPlugin fakePlugin; setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); tzdata.initializeTimeZones(); tz.setLocalLocation(tz.getLocation('Etc/UTC')); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('flutter_timezone'), + (call) async => + call.method == 'getLocalTimezone' ? 'Etc/UTC' : null); + NotificationService.isPlatformSupported = () => true; fakePlugin = FakeFlutterLocalNotificationsPlugin(); service = NotificationService(plugin: fakePlugin); @@ -139,6 +150,7 @@ void main() { test('scheduleNotification adds a notification', () async { await service.scheduleNotification( id: 1, + scheduleId: 1, title: 'Test', body: 'Body', scheduledTime: DateTime(2026, 2, 8, 10, 30), @@ -153,6 +165,24 @@ void main() { expect((n['date'] as tz.TZDateTime).minute, 30); }); + test('scheduleNotification writes the schedule id into the payload', + () async { + // Arrange + // Act + await service.scheduleNotification( + id: 1, + scheduleId: 42, + title: 'Test', + body: 'Body', + scheduledTime: DateTime(2026, 2, 8, 10, 30), + ); + + // Assert + final payload = jsonDecode(fakePlugin.scheduled.single['payload'] as String) + as Map; + expect(payload['scheduleId'], 42); + }); + test('showNotification adds a shown notification', () async { await service.showNotification(title: 'Show', body: 'Body'); expect(fakePlugin.shown.length, 1); @@ -164,6 +194,7 @@ void main() { test('cancelAllNotifications clears scheduled', () async { await service.scheduleNotification( id: 1, + scheduleId: 1, title: 'T1', body: 'B1', scheduledTime: DateTime(2026, 2, 8, 10, 0), @@ -175,12 +206,14 @@ void main() { test('cancelPendingNotifications removes only pending', () async { await service.scheduleNotification( id: 1, + scheduleId: 1, title: 'T1', body: 'B1', scheduledTime: DateTime(2026, 2, 8, 10, 0), ); await service.scheduleNotification( id: 2, + scheduleId: 2, title: 'T2', body: 'B2', scheduledTime: DateTime(2026, 2, 9, 10, 0), @@ -244,6 +277,7 @@ void main() { // Act await service.scheduleDailyNotification( id: 7, + scheduleId: 7, title: 'D', body: 'B', firstOccurrence: firstFire, @@ -265,6 +299,7 @@ void main() { // Act await service.scheduleWeeklyNotification( id: 9, + scheduleId: 9, title: 'W', body: 'B', firstOccurrence: firstFire, @@ -276,4 +311,99 @@ void main() { expect(n['matchDateTimeComponents'], DateTimeComponents.dayOfWeekAndTime); expect(payload['isRepeating'], isTrue); }); + + group('notification taps', () { + test('initialize forwards a parsed tap to onTap', () async { + // Arrange + NotificationPayload? capturedPayload; + await service.initialize(onTap: (payload) => capturedPayload = payload); + + // Act + fakePlugin.capturedOnTap!(NotificationResponse( + notificationResponseType: NotificationResponseType.selectedNotification, + id: 99, + payload: jsonEncode({ + 'scheduleId': 5, + 'scheduledTime': DateTime(2026, 2, 8, 9, 0).toIso8601String(), + }), + )); + + // Assert + expect(capturedPayload, ( + scheduleId: 5, + scheduledTime: DateTime(2026, 2, 8, 9, 0), + isRepeating: false, + notificationId: 99, + )); + }); + + test('getAppLaunchTap returns the tap when a notification launched the app', + () async { + // Arrange + fakePlugin.launchDetails = NotificationAppLaunchDetails( + true, + notificationResponse: NotificationResponse( + notificationResponseType: + NotificationResponseType.selectedNotification, + id: 7, + payload: jsonEncode({ + 'scheduleId': 3, + 'scheduledTime': DateTime(2026, 2, 8, 9, 0).toIso8601String(), + 'isRepeating': true, + }), + ), + ); + + // Act + final payload = await service.getAppLaunchTap(); + + // Assert + expect(payload, ( + scheduleId: 3, + scheduledTime: DateTime(2026, 2, 8, 9, 0), + isRepeating: true, + notificationId: 7, + )); + }); + + test('getAppLaunchTap returns null when no notification launched the app', + () async { + // Arrange + fakePlugin.launchDetails = NotificationAppLaunchDetails( + false, + notificationResponse: NotificationResponse( + notificationResponseType: + NotificationResponseType.selectedNotification, + id: 7, + payload: jsonEncode({ + 'scheduleId': 3, + 'scheduledTime': DateTime(2026, 2, 8, 9, 0).toIso8601String(), + }), + ), + ); + + // Act + final payload = await service.getAppLaunchTap(); + + // Assert + expect(payload, isNull); + }); + }); + + test('cancel removes the scheduled notification', () async { + // Arrange + await service.scheduleNotification( + id: 1, + scheduleId: 1, + title: 'T', + body: 'B', + scheduledTime: DateTime(2026, 2, 8, 10, 0), + ); + + // Act + await service.cancel(1); + + // Assert + expect(fakePlugin.scheduled, isEmpty); + }); } diff --git a/tool/i18next_to_android_widget_res.dart b/tool/i18n/i18next_to_android_widget_res.dart similarity index 100% rename from tool/i18next_to_android_widget_res.dart rename to tool/i18n/i18next_to_android_widget_res.dart diff --git a/tool/i18next_to_slang.dart b/tool/i18n/i18next_to_slang.dart similarity index 86% rename from tool/i18next_to_slang.dart rename to tool/i18n/i18next_to_slang.dart index bc9024f34..0602ada0b 100644 --- a/tool/i18next_to_slang.dart +++ b/tool/i18n/i18next_to_slang.dart @@ -49,7 +49,19 @@ void _convert(File input, Directory outDir) { } for (final entry in plurals.entries) { - result['${entry.key}(param=count)'] = entry.value; + final forms = entry.value; + // put back decimal forms that are not handled by Weblate (see weblate issue #7520) + forms.putIfAbsent( + 'other', + () => + forms['few'] ?? + forms['many'] ?? + forms['two'] ?? + forms['one'] ?? + forms['zero'] ?? + '', + ); + result['${entry.key}(param=count)'] = forms; } final outPath = '${outDir.path}/$locale$_outputSuffix'; diff --git a/tool/sanitize_generated.dart b/tool/i18n/sanitize_generated.dart similarity index 100% rename from tool/sanitize_generated.dart rename to tool/i18n/sanitize_generated.dart diff --git a/tool/screenshots/fonts/Roboto-LICENSE.txt b/tool/screenshots/fonts/Roboto-LICENSE.txt new file mode 100644 index 000000000..98aa7310c --- /dev/null +++ b/tool/screenshots/fonts/Roboto-LICENSE.txt @@ -0,0 +1,94 @@ +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + diff --git a/tool/screenshots/fonts/Roboto-SemiBold.ttf b/tool/screenshots/fonts/Roboto-SemiBold.ttf new file mode 100644 index 000000000..7a8ef87d5 Binary files /dev/null and b/tool/screenshots/fonts/Roboto-SemiBold.ttf differ diff --git a/tool/screenshots/frame_screenshots.dart b/tool/screenshots/frame_screenshots.dart new file mode 100644 index 000000000..71add5230 --- /dev/null +++ b/tool/screenshots/frame_screenshots.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mona/theme/default_color_schemes.dart'; + +import 'screenshot_fonts.dart'; +import 'screenshot_frame.dart'; + +const String _inputDir = String.fromEnvironment('FRAME_INPUT_DIR'); +const String _outputDir = String.fromEnvironment('FRAME_OUTPUT_DIR'); +const String _captionsPath = String.fromEnvironment('FRAME_CAPTIONS'); + +void main() { + testWidgets('frame every screenshot', (tester) async { + await tester.runAsync(() => loadScreenshotFonts()); + + final input = Directory(_inputDir); + final files = input + .listSync() + .whereType() + .where((f) => f.path.endsWith('.png')) + .toList() + ..sort((a, b) => a.path.compareTo(b.path)); + + final captions = + (await tester.runAsync(() => loadScreenshotCaptions(_captionsPath)))!; + + await tester.runAsync(() => Directory(_outputDir).create(recursive: true)); + + for (final file in files) { + final name = file.uri.pathSegments.last; + final stem = name.substring(0, name.length - '.png'.length); + final bytes = await tester.runAsync(() => file.readAsBytes()); + + late ui.Image image; + await tester.runAsync(() async { + final codec = await ui.instantiateImageCodec(bytes!); + image = (await codec.getNextFrame()).image; + }); + + final size = Size(image.width.toDouble(), image.height.toDouble()); + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.reset); + + final key = GlobalKey(); + await tester.pumpWidget( + RepaintBoundary( + key: key, + child: Directionality( + textDirection: TextDirection.ltr, + child: ScreenshotFrame( + image: image, + caption: captions[stem]!, + colors: DefaultColorSchemes.light, + size: size, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final png = await tester.runAsync(() async { + final boundary = + key.currentContext!.findRenderObject()! as RenderRepaintBoundary; + final rendered = await boundary.toImage(pixelRatio: 1.0); + return rendered.toByteData(format: ui.ImageByteFormat.png); + }); + + await tester.runAsync( + () => File('$_outputDir/$name').writeAsBytes(png!.buffer.asUint8List()), + ); + debugPrint('framed $name (${image.width}x${image.height})'); + } + + expect(files, isNotEmpty, reason: 'no PNGs found in $_inputDir'); + }); +} + +Future> loadScreenshotCaptions(String path) async { + final decoded = + jsonDecode(await File(path).readAsString()) as Map; + return decoded.map((key, value) => MapEntry(key, value as String)); +} diff --git a/tool/screenshots/screenshot_fonts.dart b/tool/screenshots/screenshot_fonts.dart new file mode 100644 index 000000000..498b56f7d --- /dev/null +++ b/tool/screenshots/screenshot_fonts.dart @@ -0,0 +1,11 @@ +import 'dart:io'; + +import 'package:flutter/services.dart' show FontLoader; + +Future loadScreenshotFonts() async { + final bytes = + await File('tool/screenshots/fonts/Roboto-SemiBold.ttf').readAsBytes(); + final loader = FontLoader('ScreenshotCaption') + ..addFont(Future.value(bytes.buffer.asByteData())); + await loader.load(); +} diff --git a/tool/screenshots/screenshot_frame.dart b/tool/screenshots/screenshot_frame.dart new file mode 100644 index 000000000..fcf019792 --- /dev/null +++ b/tool/screenshots/screenshot_frame.dart @@ -0,0 +1,86 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +class ScreenshotFrame extends StatelessWidget { + const ScreenshotFrame({ + super.key, + required this.image, + required this.caption, + required this.colors, + required this.size, + }); + + final ui.Image image; + final String caption; + final ColorScheme colors; + final Size size; + + @override + Widget build(BuildContext context) { + final bandHeight = size.height * 0.18; + final padX = size.width * 0.08; + + return SizedBox( + width: size.width, + height: size.height, + child: ColoredBox( + color: colors.surfaceContainerHighest, + child: Column( + children: [ + SizedBox( + height: bandHeight, + child: Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: padX), + child: Text( + caption, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'ScreenshotCaption', + fontWeight: FontWeight.w600, + fontSize: size.width * 0.06, + height: 1.2, + leadingDistribution: TextLeadingDistribution.even, + color: colors.onSurface, + ), + ), + ), + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.fromLTRB(padX, 0, padX, size.height * 0.06), + child: Center( + child: AspectRatio( + aspectRatio: image.width / image.height, + child: _bezel(), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _bezel() { + final radius = size.width * 0.11; + return ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: ColoredBox( + color: colors.surfaceContainerLow, + child: Padding( + padding: EdgeInsets.all(size.width * 0.015), + child: ClipRRect( + borderRadius: BorderRadius.circular(radius * 0.85), + child: RawImage(image: image, fit: BoxFit.cover), + ), + ), + ), + ); + } +} diff --git a/tool/screenshots/screenshot_seed.dart b/tool/screenshots/screenshot_seed.dart new file mode 100644 index 000000000..1147861dc --- /dev/null +++ b/tool/screenshots/screenshot_seed.dart @@ -0,0 +1,223 @@ +import 'package:clock/clock.dart'; +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:mona/data/model/administration_route.dart'; +import 'package:mona/data/model/blood_test.dart'; +import 'package:mona/data/model/date.dart'; +import 'package:mona/data/model/dosing_basis.dart'; +import 'package:mona/data/model/ester.dart'; +import 'package:mona/data/model/generic_supply_item.dart'; +import 'package:mona/data/model/medication_intake.dart'; +import 'package:mona/data/model/medication_schedule.dart'; +import 'package:mona/data/model/medication_supply_item.dart'; +import 'package:mona/data/model/molecule.dart'; +import 'package:mona/data/model/scheduling_strategy.dart'; +import 'package:mona/data/model/supply_item.dart'; +import 'package:mona/data/model/units.dart'; +import 'package:mona/services/db/app_database.dart'; +import 'package:sqflite/sqflite.dart'; + +final DateTime screenshotClockInstant = DateTime(2026, 6, 1, 12, 0); + +const String _tz = 'UTC'; + +DateTime _daysAgoUtc(int days, {int hour = 12}) { + final now = clock.now(); + return DateTime.utc(now.year, now.month, now.day, hour) + .subtract(Duration(days: days)); +} + +Decimal _d(String value) => Decimal.parse(value); + +Future seedScreenshotData() async { + final db = await AppDatabase.getInstance().database; + await _clear(db); + await _insertSchedules(db); + await _insertIntakes(db); + await _insertBloodTests(db); + await _insertSupplies(db); +} + +Future _clear(Database db) async { + for (final table in const [ + 'medication_intakes', + 'medication_schedules', + 'blood_tests', + 'supply_items', + ]) { + await db.delete(table); + } +} + +Future _insert(Database db, String table, Map map) { + return db.insert(table, map, conflictAlgorithm: ConflictAlgorithm.replace); +} + +Future _insertSchedules(Database db) async { + final today = Date.today(); + + final daily = MedicationSchedule( + id: 101, + name: 'Estradiol gel', + dose: _d('1.5'), + molecule: KnownMolecules.estradiol, + administrationRoute: AdministrationRoute.gel, + dosingBasis: DosingBasis.mass, + startDate: today, + scheduling: const DailySchedule( + intakeTimes: [ + TimeOfDay(hour: 8, minute: 0), + TimeOfDay(hour: 10, minute: 0), + TimeOfDay(hour: 18, minute: 0), + ], + ), + ); + + final optional = MedicationSchedule( + id: 102, + name: 'Progesterone', + dose: _d('100'), + molecule: KnownMolecules.progesterone, + administrationRoute: AdministrationRoute.oral, + dosingBasis: DosingBasis.mass, + startDate: today.subtract(const Duration(days: 30)), + scheduling: const AsNeededSchedule(), + ); + + final upcoming = MedicationSchedule( + id: 103, + name: 'Decapeptyl', + dose: _d('11.25'), + molecule: KnownMolecules.decapeptyl, + administrationRoute: AdministrationRoute.injection, + dosingBasis: DosingBasis.mass, + ester: Ester.enanthate, + startDate: today.add(const Duration(days: 3)), + scheduling: const IntervalDaysSchedule(intervalDays: 7), + ); + + for (final schedule in [daily, optional, upcoming]) { + await _insert(db, 'medication_schedules', schedule.toMap()); + } +} + +Future _insertIntakes(Database db) async { + final injections = [ + (id: 201, days: 3), + (id: 202, days: 10), + (id: 203, days: 17), + ].map( + (e) => MedicationIntake( + id: e.id, + takenDose: _d('4'), + takenDateTime: _daysAgoUtc(e.days), + takenTimeZone: _tz, + molecule: KnownMolecules.estradiol, + administrationRoute: AdministrationRoute.injection, + ester: Ester.enanthate, + dosingBasis: DosingBasis.mass, + ), + ); + + final oral = MedicationIntake( + id: 204, + takenDose: _d('100'), + takenDateTime: _daysAgoUtc(1), + takenTimeZone: _tz, + molecule: KnownMolecules.progesterone, + administrationRoute: AdministrationRoute.oral, + dosingBasis: DosingBasis.mass, + scheduleId: 102, + ); + + final patch = MedicationIntake( + id: 205, + takenDose: _d('100'), + takenDateTime: _daysAgoUtc(2), + takenTimeZone: _tz, + molecule: KnownMolecules.estradiol, + administrationRoute: AdministrationRoute.patch, + dosingBasis: DosingBasis.releaseRate, + notes: 'Left hip', + ); + + final dailyTaken = MedicationIntake( + id: 206, + scheduledTime: const TimeOfDay(hour: 8, minute: 0), + takenDose: _d('1.5'), + takenDateTime: _daysAgoUtc(0, hour: 8), + takenTimeZone: _tz, + molecule: KnownMolecules.estradiol, + administrationRoute: AdministrationRoute.gel, + dosingBasis: DosingBasis.mass, + scheduleId: 101, + ); + + for (final intake in [...injections, oral, patch, dailyTaken]) { + await _insert(db, 'medication_intakes', intake.toMap()); + } +} + +Future _insertBloodTests(Database db) async { + final rows = [ + (id: 301, days: 7, e: '150', t: '40'), + (id: 302, days: 21, e: '180', t: '20'), + (id: 303, days: 35, e: '120', t: '90'), + (id: 304, days: 49, e: '60', t: '150'), + (id: 305, days: 63, e: '90', t: '260'), + ]; + + for (final row in rows) { + final test = BloodTest( + id: row.id, + dateTime: _daysAgoUtc(row.days, hour: 9), + timeZone: _tz, + estradiolLevels: UnitValue(_d(row.e), EstradiolUnit.pg_mL), + testosteroneLevels: UnitValue(_d(row.t), TestosteroneUnit.ng_dL), + ); + await _insert(db, 'blood_tests', test.toMap()); + } +} + +Future _insertSupplies(Database db) async { + final vial = MedicationSupplyItem( + id: 401, + name: 'Estradiol Enanthate 20 mg/mL', + totalDose: _d('200'), + usedDose: _d('40'), + dosePerUnit: _d('20'), + molecule: KnownMolecules.estradiol, + administrationRoute: AdministrationRoute.injection, + ester: Ester.enanthate, + dosingBasis: DosingBasis.mass, + ); + + final gel = MedicationSupplyItem( + id: 402, + name: 'Estradiol gel 0.06%', + totalDose: _d('80'), + usedDose: _d('25'), + dosePerUnit: _d('0.6'), + molecule: KnownMolecules.estradiol, + administrationRoute: AdministrationRoute.gel, + dosingBasis: DosingBasis.mass, + ); + + final syringes = GenericSupply( + id: 403, + name: 'Syringes', + amount: 30, + genericSupplyType: GenericSupplyType.syringe, + ); + + final needles = GenericSupply( + id: 404, + name: 'Needles', + amount: 25, + genericSupplyType: GenericSupplyType.needle, + ); + + for (final item in [vial, gel, syringes, needles]) { + await _insert(db, 'supply_items', item.toMap()); + } +} diff --git a/tool/screenshots/screenshots.dart b/tool/screenshots/screenshots.dart new file mode 100644 index 000000000..c945fec43 --- /dev/null +++ b/tool/screenshots/screenshots.dart @@ -0,0 +1,24 @@ +import 'package:clock/clock.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:mona/main.dart' as app; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'screenshot_seed.dart'; + +const String _localeTag = + String.fromEnvironment('SCREENSHOT_LOCALE', defaultValue: 'en'); + +void main() { + withClock(Clock.fixed(screenshotClockInstant), () async { + enableFlutterDriverExtension(); + WidgetsApp.debugAllowBannerOverride = false; + + final prefs = await SharedPreferences.getInstance(); + await prefs.clear(); + await prefs.setString('language_tag', _localeTag); + await seedScreenshotData(); + + app.main(); + }); +} diff --git a/tool/screenshots/screenshots_driver.dart b/tool/screenshots/screenshots_driver.dart new file mode 100644 index 000000000..486dbb262 --- /dev/null +++ b/tool/screenshots/screenshots_driver.dart @@ -0,0 +1,111 @@ +import 'dart:io'; + +import 'package:flutter_driver/flutter_driver.dart'; + +const _screens = <(String, String?)>[ + ('1', null), + ('2', 'navTabLevels'), + ('3', 'navTabIntakes'), + ('4', 'navTabSupplies'), +]; + +Future _adb(String serial, List args) async { + final result = await Process.run( + 'adb', + [ + if (serial.isNotEmpty) ...['-s', serial], + ...args + ], + ); + if (result.exitCode != 0) { + throw Exception('adb ${args.join(' ')} failed: ${result.stderr}'); + } +} + +Future main() async { + final outDir = Platform.environment['SCREENSHOT_OUT'] ?? 'build/screenshots'; + final iosUdid = Platform.environment['SCREENSHOT_IOS_UDID'] ?? ''; + final androidSerial = Platform.environment['SCREENSHOT_ANDROID_SERIAL'] ?? ''; + + final driver = await FlutterDriver.connect(); + + for (var attempt = 0;; attempt++) { + try { + await driver.waitFor( + find.byValueKey('navTabIntakes'), + timeout: const Duration(seconds: 2), + ); + break; + } catch (_) { + if (attempt >= 60) rethrow; + await Future.delayed(const Duration(seconds: 1)); + } + } + + if (iosUdid.isNotEmpty) { + final result = await Process.run('xcrun', [ + 'simctl', 'status_bar', iosUdid, 'override', // + '--time', '14:28', + '--dataNetwork', 'wifi', '--wifiMode', 'active', '--wifiBars', '3', + '--cellularMode', 'active', '--cellularBars', '4', + '--batteryState', 'discharging', '--batteryLevel', '100', + ]); + if (result.exitCode != 0) { + throw Exception('simctl status_bar override failed: ${result.stderr}'); + } + } else { + const demo = [ + 'shell', + 'am', + 'broadcast', + '-a', + 'com.android.systemui.demo' + ]; + await _adb(androidSerial, + ['shell', 'settings', 'put', 'system', 'time_12_24', '24']); + await _adb(androidSerial, + ['shell', 'settings', 'put', 'global', 'sysui_demo_allowed', '1']); + await _adb(androidSerial, [...demo, '-e', 'command', 'enter']); + await _adb(androidSerial, + [...demo, '-e', 'command', 'clock', '-e', 'hhmm', '1428']); + } + + Future capture(String name) async { + await driver.waitUntilNoTransientCallbacks(); + await Future.delayed(const Duration(milliseconds: 800)); + final path = '$outDir/$name.png'; + if (iosUdid.isNotEmpty) { + final result = await Process.run( + 'xcrun', + ['simctl', 'io', iosUdid, 'screenshot', path], + ); + if (result.exitCode != 0) { + throw Exception('simctl screenshot failed: ${result.stderr}'); + } + } else { + final result = await Process.run( + 'adb', + [ + if (androidSerial.isNotEmpty) ...['-s', androidSerial], + 'exec-out', + 'screencap', + '-p', + ], + stdoutEncoding: null, + ); + if (result.exitCode != 0) { + throw Exception('adb screencap failed: ${result.stderr}'); + } + await File(path).writeAsBytes(result.stdout as List); + } + } + + for (final (name, tapKey) in _screens) { + if (tapKey != null) { + await driver.tap(find.byValueKey(tapKey)); + } + await capture(name); + } + + await driver.close(); +}