feat(meditate): the app: Today, Explore, Profile, the sitting and its bells
halkyon_meditate_flutter on halkyon_app and halkyon_ui (ADR 0038): the first start, sign-in and account as every Halkyon app's, a bar below medium windows and a rail with the icon from medium on, fern washes, Settings in cards (Sync, the operator's, Sitting, Practice, the account, What's new, About, the version), English and German. - Today: a greeting by the hour, Sit now at 5, 10, 20 minutes and the timer's own length, the practice of the day, the week with the days in a row, favourites, recently sat (D9). - Explore: six practices of Halkyon's own, written for the app, found by a search without case or accents, by goal and by kind; each page with how it goes, its lengths, its heart and Start (D4, D10). - The sitting, over the tabs: its time from the clock, a ring counting down, the cue of the moment, Pause and End, the bells at the start, between and at the end, the screen kept on; ended early, kept from a minute on where the person says so (D5, D6). - Profile: every Halkyon app's, then the person's practice: minutes, sittings, days, the week and every sitting kept, to delete (D8). - Settings and sittings sealed with the person's key and synced (D7). The bells are synthesised by apps/meditate/tool/bells.py, the icon is Lucide's sprout from cobalt through pacific into fern (tool/app_icons.py). Releases and the deploy are a later step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
235d9cf75b
commit
cb026dce8d
47 changed files with 13502 additions and 5 deletions
14
apps/meditate/halkyon_meditate_flutter/lib/driver.dart
Normal file
14
apps/meditate/halkyon_meditate_flutter/lib/driver.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// The app with the Flutter driver extension, as `serverpod start` launches it (the
|
||||
// server's pubspec, `flutter_apps`); never meant for real use, just development.
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:flutter_driver/driver_extension.dart';
|
||||
import 'package:halkyon_meditate_flutter/main.dart' as app;
|
||||
|
||||
void main() {
|
||||
enableFlutterDriverExtension(
|
||||
// Off by default, so the app stays usable by hand; agents typing through
|
||||
// the app set it to true.
|
||||
enableTextEntryEmulation: false,
|
||||
);
|
||||
app.main();
|
||||
}
|
||||
124
apps/meditate/halkyon_meditate_flutter/lib/l10n/app_de.arb
Normal file
124
apps/meditate/halkyon_meditate_flutter/lib/l10n/app_de.arb
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
{
|
||||
"@@locale": "de",
|
||||
"appTitle": "Halkyon Meditate",
|
||||
"launchProduct": "Meditate",
|
||||
"instanceFirstStartSubtitle": "Wähle die Instanz, auf der deine Praxis liegen soll. Jede betreibt jemand; weitere kannst du später hinzufügen.",
|
||||
"accountTitle": "Konto",
|
||||
"accountDeleteMessage": "Dein Konto wird auf dem Server und auf all deinen Geräten gelöscht, mit jeder gespeicherten Sitzung. Dies kann nicht rückgängig gemacht werden.",
|
||||
"aboutTitle": "Über Halkyon Meditate",
|
||||
"settingsTitle": "Einstellungen",
|
||||
"settingsVersion": "Version {version} (Build {build})",
|
||||
"keysMissing": "Dieses Gerät kann nichts speichern: Es hat die Schlüssel deines Kontos nicht. Melde dich mit deinem Passwort oder deiner Wiederherstellungsphrase an, um sie hinzuzufügen.",
|
||||
"shellTabToday": "Heute",
|
||||
"shellTabExplore": "Entdecken",
|
||||
"shellTabProfile": "Profil",
|
||||
"greetingMorning": "Guten Morgen",
|
||||
"greetingAfternoon": "Guten Tag",
|
||||
"greetingEvening": "Guten Abend",
|
||||
"greetingNight": "Eine stille Nacht",
|
||||
"minutesUnit": "Min.",
|
||||
"minutes": "{minutes} Min.",
|
||||
"todaySitNow": "Jetzt sitzen",
|
||||
"todaySitNowHint": "Eine stille Sitzung mit Klängen, so lange du willst.",
|
||||
"todayPracticeOfDay": "Übung des Tages",
|
||||
"todayThisWeek": "Diese Woche",
|
||||
"streakDays": "{count, plural, =0{Noch keine Tage in Folge} =1{1 Tag in Folge} other{{count} Tage in Folge}}",
|
||||
"favouritesTitle": "Favoriten",
|
||||
"favouritesNone": "Noch keine Favoriten. Markiere eine Übung unter Entdecken mit ihrem Herz.",
|
||||
"recentTitle": "Zuletzt gesessen",
|
||||
"recentNone": "Noch nichts gesessen.",
|
||||
"silentTimer": "Stiller Timer",
|
||||
"sittingRow": "{date} · {minutes} Min.",
|
||||
"exploreSearch": "Übungen suchen",
|
||||
"exploreGoals": "Was führt dich her?",
|
||||
"exploreAll": "Alle",
|
||||
"exploreNoMatch": "Keine Übung passt.",
|
||||
"goalCalm": "Zur Ruhe kommen",
|
||||
"goalSleep": "Schlafen",
|
||||
"goalFocus": "Fokus",
|
||||
"goalBegin": "Anfangen",
|
||||
"kindBreathing": "Atem",
|
||||
"kindBody": "Körper",
|
||||
"kindAwareness": "Gewahrsein",
|
||||
"kindKindness": "Güte",
|
||||
"kindSleep": "Schlaf",
|
||||
"kindPause": "Pause",
|
||||
"practiceStart": "Starten",
|
||||
"practiceLength": "Dauer",
|
||||
"practiceHowItGoes": "So geht es",
|
||||
"practiceFavourite": "Zu den Favoriten",
|
||||
"practiceUnfavourite": "Aus den Favoriten entfernen",
|
||||
"practiceGone": "Diese Übung gibt es in dieser Version der App nicht.",
|
||||
"practiceBreathCountingTitle": "Atemzüge zählen",
|
||||
"practiceBreathCountingSummary": "Zähle jedes Ausatmen von eins bis zehn, dann fang wieder an.",
|
||||
"practiceBreathCountingCue1": "Setz dich bequem hin. Lass den Atem von selbst kommen und gehen.",
|
||||
"practiceBreathCountingCue2": "Zähle mit jedem Ausatmen: eins, zwei und weiter bis zehn.",
|
||||
"practiceBreathCountingCue3": "Verzählt? Bemerke es freundlich und fang wieder bei eins an.",
|
||||
"practiceBreathCountingCue4": "Lass das Zählen los. Ruh beim Atem, wie er ist.",
|
||||
"practiceThreeMinutePauseTitle": "Drei-Minuten-Pause",
|
||||
"practiceThreeMinutePauseSummary": "Ein kurzer Halt im Tag: wahrnehmen, sammeln, weiten.",
|
||||
"practiceThreeMinutePauseCue1": "Was ist gerade da? Gedanken, Gefühle, Empfindungen.",
|
||||
"practiceThreeMinutePauseCue2": "Sammle deine Aufmerksamkeit beim Atem, tief im Bauch.",
|
||||
"practiceThreeMinutePauseCue3": "Weite sie auf den ganzen Körper und den Raum um dich.",
|
||||
"practiceBodyScanTitle": "Körperreise",
|
||||
"practiceBodyScanSummary": "Wandere mit der Aufmerksamkeit langsam durch den Körper, von den Füßen aufwärts.",
|
||||
"practiceBodyScanCue1": "Leg oder setz dich bequem hin. Spür, wo der Körper den Boden berührt.",
|
||||
"practiceBodyScanCue2": "Bring deine Aufmerksamkeit zu den Füßen und Beinen. Nimm einfach wahr.",
|
||||
"practiceBodyScanCue3": "Wandere hinauf durch Bauch, Rücken und Brust.",
|
||||
"practiceBodyScanCue4": "Spür die Hände, die Arme, die Schultern und den Nacken.",
|
||||
"practiceBodyScanCue5": "Das Gesicht und den Kopf. Dann den ganzen Körper auf einmal.",
|
||||
"practiceOpenAwarenessTitle": "Offenes Gewahrsein",
|
||||
"practiceOpenAwarenessSummary": "Lass die Aufmerksamkeit auf nichts Bestimmtem ruhen und alles vorüberziehen.",
|
||||
"practiceOpenAwarenessCue1": "Beginne mit ein paar Atemzügen und komm an.",
|
||||
"practiceOpenAwarenessCue2": "Lass den Atem los. Nimm Geräusche wahr, wie sie kommen und gehen.",
|
||||
"practiceOpenAwarenessCue3": "Auch Gedanken dürfen kommen. Sieh sie auftauchen und gehen.",
|
||||
"practiceOpenAwarenessCue4": "Sei einfach gewahr, ohne etwas zu tun.",
|
||||
"practiceLovingKindnessTitle": "Gute Wünsche",
|
||||
"practiceLovingKindnessSummary": "Wünsch dir und anderen Gutes, Kreis um Kreis.",
|
||||
"practiceLovingKindnessCue1": "Denk an dich: Möge ich sicher sein, möge es mir leicht sein.",
|
||||
"practiceLovingKindnessCue2": "Denk an jemanden, der dir lieb ist: Mögest du sicher sein, möge es dir leicht sein.",
|
||||
"practiceLovingKindnessCue3": "Denk an jemanden, den du kaum kennst, und wünsch ihm dasselbe.",
|
||||
"practiceLovingKindnessCue4": "Lass die Wünsche zu allen reichen, überall.",
|
||||
"practiceSettlingForSleepTitle": "Zur Nacht ankommen",
|
||||
"practiceSettlingForSleepSummary": "Verlangsame den Atem und lass den Tag los, bevor du schläfst.",
|
||||
"practiceSettlingForSleepCue1": "Leg dich hin. Lass dich vom Bett tragen.",
|
||||
"practiceSettlingForSleepCue2": "Atme etwas länger aus, als du einatmest.",
|
||||
"practiceSettlingForSleepCue3": "Lass die Gedanken des Tages wie Wolken vorüberziehen.",
|
||||
"practiceSettlingForSleepCue4": "Es gibt nichts mehr zu tun. Lass den Schlaf kommen, wenn er kommt.",
|
||||
"timerCue": "Sitz in Stille. Der Klang sagt dir, wann es vorbei ist.",
|
||||
"sittingPause": "Pause",
|
||||
"sittingResume": "Weiter",
|
||||
"sittingEnd": "Beenden",
|
||||
"sittingPaused": "Pausiert",
|
||||
"sittingDoneTitle": "Gut gesessen",
|
||||
"sittingDoneMessage": "{minutes, plural, =1{1 Minute gesessen.} other{{minutes} Minuten gesessen.}}",
|
||||
"sittingDone": "Fertig",
|
||||
"sittingEndTitle": "Sitzung beenden?",
|
||||
"sittingEndMessage": "{minutes, plural, =1{Bisher 1 Minute gesessen. Bei deinen Sitzungen behalten?} other{Bisher {minutes} Minuten gesessen. Bei deinen Sitzungen behalten?}}",
|
||||
"sittingKeep": "Behalten",
|
||||
"sittingDiscard": "Verwerfen",
|
||||
"sittingSitOn": "Weitersitzen",
|
||||
"profileTabPractice": "Praxis",
|
||||
"figuresMinutes": "Minuten",
|
||||
"figuresSittings": "Sitzungen",
|
||||
"figuresDays": "Tage",
|
||||
"historyTitle": "Deine Sitzungen",
|
||||
"sittingDelete": "Löschen",
|
||||
"sittingDeleteTitle": "Diese Sitzung löschen?",
|
||||
"sittingDeleteMessage": "Sie verschwindet aus deinen Zahlen, auf all deinen Geräten.",
|
||||
"settingsSitting": "Sitzen",
|
||||
"bellTitle": "Klang",
|
||||
"bellBowl": "Klangschale",
|
||||
"bellSmallBell": "Kleine Glocke",
|
||||
"bellWoodBlock": "Holzblock",
|
||||
"startBellTitle": "Klang zu Beginn",
|
||||
"intervalTitle": "Klänge dazwischen",
|
||||
"intervalNone": "Keine",
|
||||
"intervalEvery": "Alle {minutes} Min.",
|
||||
"keepScreenOnTitle": "Bildschirm anlassen",
|
||||
"keepScreenOnHint": "Solange eine Sitzung läuft",
|
||||
"timerLengthTitle": "Dauer des stillen Timers",
|
||||
"settingsPractice": "Praxis",
|
||||
"showStreakTitle": "Tage in Folge zeigen",
|
||||
"showStreakHint": "Unter Heute und in deinem Profil"
|
||||
}
|
||||
190
apps/meditate/halkyon_meditate_flutter/lib/l10n/app_en.arb
Normal file
190
apps/meditate/halkyon_meditate_flutter/lib/l10n/app_en.arb
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
{
|
||||
"@@locale": "en",
|
||||
"appTitle": "Halkyon Meditate",
|
||||
"@appTitle": {
|
||||
"description": "The app's name: the window's title, and where halkyon_app's texts name the app."
|
||||
},
|
||||
"launchProduct": "Meditate",
|
||||
"@launchProduct": {
|
||||
"description": "The app's word in its wordmark, under \"Halkyon\"."
|
||||
},
|
||||
"instanceFirstStartSubtitle": "Choose the instance to keep your practice on. Someone runs each one; you can add others later.",
|
||||
"accountTitle": "Account",
|
||||
"accountDeleteMessage": "Your account is deleted on the server and on all your devices, with every sitting kept. This can't be undone.",
|
||||
"aboutTitle": "About Halkyon Meditate",
|
||||
"settingsTitle": "Settings",
|
||||
"settingsVersion": "Version {version} (build {build})",
|
||||
"@settingsVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
},
|
||||
"build": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"keysMissing": "This device can't save: it doesn't hold your account's keys. Sign in with your password or recovery phrase to add them.",
|
||||
"shellTabToday": "Today",
|
||||
"shellTabExplore": "Explore",
|
||||
"shellTabProfile": "Profile",
|
||||
"greetingMorning": "Good morning",
|
||||
"greetingAfternoon": "Good afternoon",
|
||||
"greetingEvening": "Good evening",
|
||||
"greetingNight": "A quiet night",
|
||||
"minutesUnit": "min",
|
||||
"@minutesUnit": {
|
||||
"description": "Minutes, after a number typed or stepped."
|
||||
},
|
||||
"minutes": "{minutes} min",
|
||||
"@minutes": {
|
||||
"description": "A length in minutes, short, on chips and rows.",
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"todaySitNow": "Sit now",
|
||||
"todaySitNowHint": "A silent sitting with bells, as long as you choose.",
|
||||
"todayPracticeOfDay": "Practice of the day",
|
||||
"todayThisWeek": "This week",
|
||||
"streakDays": "{count, plural, =0{No days in a row yet} =1{1 day in a row} other{{count} days in a row}}",
|
||||
"@streakDays": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"favouritesTitle": "Favourites",
|
||||
"favouritesNone": "No favourites yet. Mark a practice with its heart in Explore.",
|
||||
"recentTitle": "Recently sat",
|
||||
"recentNone": "Nothing sat yet.",
|
||||
"silentTimer": "Silent timer",
|
||||
"sittingRow": "{date} · {minutes} min",
|
||||
"@sittingRow": {
|
||||
"description": "A sitting kept: when it began and how long it was sat.",
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"type": "String"
|
||||
},
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exploreSearch": "Search practices",
|
||||
"exploreGoals": "What brings you here?",
|
||||
"exploreAll": "All",
|
||||
"exploreNoMatch": "No practice matches.",
|
||||
"goalCalm": "Calm down",
|
||||
"goalSleep": "Sleep",
|
||||
"goalFocus": "Focus",
|
||||
"goalBegin": "Begin",
|
||||
"kindBreathing": "Breathing",
|
||||
"kindBody": "Body",
|
||||
"kindAwareness": "Awareness",
|
||||
"kindKindness": "Kindness",
|
||||
"kindSleep": "Sleep",
|
||||
"kindPause": "Pause",
|
||||
"practiceStart": "Start",
|
||||
"practiceLength": "Length",
|
||||
"practiceHowItGoes": "How it goes",
|
||||
"practiceFavourite": "Add to favourites",
|
||||
"practiceUnfavourite": "Remove from favourites",
|
||||
"practiceGone": "This practice isn't in this version of the app.",
|
||||
"practiceBreathCountingTitle": "Counting breaths",
|
||||
"practiceBreathCountingSummary": "Count each out-breath from one to ten, then begin again.",
|
||||
"practiceBreathCountingCue1": "Sit comfortably. Let the breath come and go by itself.",
|
||||
"practiceBreathCountingCue2": "With each out-breath, count: one, two, and on up to ten.",
|
||||
"practiceBreathCountingCue3": "Lost count? Notice it kindly and begin again at one.",
|
||||
"practiceBreathCountingCue4": "Let the counting go. Rest with the breath as it is.",
|
||||
"practiceThreeMinutePauseTitle": "Three-minute pause",
|
||||
"practiceThreeMinutePauseSummary": "A short stop in the day: notice, gather, widen.",
|
||||
"practiceThreeMinutePauseCue1": "What is here right now? Thoughts, feelings, sensations.",
|
||||
"practiceThreeMinutePauseCue2": "Gather your attention on the breath, low in the belly.",
|
||||
"practiceThreeMinutePauseCue3": "Widen it to the whole body, and to the room around you.",
|
||||
"practiceBodyScanTitle": "Body scan",
|
||||
"practiceBodyScanSummary": "Move your attention slowly through the body, from the feet up.",
|
||||
"practiceBodyScanCue1": "Lie or sit comfortably. Feel where the body meets the ground.",
|
||||
"practiceBodyScanCue2": "Bring your attention to your feet and legs. Just notice.",
|
||||
"practiceBodyScanCue3": "Move up through the belly, the back and the chest.",
|
||||
"practiceBodyScanCue4": "Notice the hands, the arms, the shoulders and the neck.",
|
||||
"practiceBodyScanCue5": "The face and the head. Then feel the whole body at once.",
|
||||
"practiceOpenAwarenessTitle": "Open awareness",
|
||||
"practiceOpenAwarenessSummary": "Rest your attention on nothing in particular, and let whatever comes pass.",
|
||||
"practiceOpenAwarenessCue1": "Begin with a few breaths, settling in.",
|
||||
"practiceOpenAwarenessCue2": "Let the breath go. Notice sounds as they come and go.",
|
||||
"practiceOpenAwarenessCue3": "Thoughts may come too. See them arrive, and leave.",
|
||||
"practiceOpenAwarenessCue4": "Simply be aware, with nothing to do.",
|
||||
"practiceLovingKindnessTitle": "Kind wishes",
|
||||
"practiceLovingKindnessSummary": "Wish yourself and others well, one circle after the other.",
|
||||
"practiceLovingKindnessCue1": "Think of yourself: may I be safe, may I be at ease.",
|
||||
"practiceLovingKindnessCue2": "Think of someone dear to you: may you be safe, may you be at ease.",
|
||||
"practiceLovingKindnessCue3": "Think of someone you hardly know, and wish them the same.",
|
||||
"practiceLovingKindnessCue4": "Let the wishes spread to everyone, everywhere.",
|
||||
"practiceSettlingForSleepTitle": "Settling for sleep",
|
||||
"practiceSettlingForSleepSummary": "Slow the breath and let the day go before you sleep.",
|
||||
"practiceSettlingForSleepCue1": "Lie down. Let the bed carry your weight.",
|
||||
"practiceSettlingForSleepCue2": "Breathe out a little longer than you breathe in.",
|
||||
"practiceSettlingForSleepCue3": "Let the day's thoughts drift past like clouds.",
|
||||
"practiceSettlingForSleepCue4": "Nothing more to do. Let sleep come when it comes.",
|
||||
"timerCue": "Sit in silence. The bell tells you when it is over.",
|
||||
"sittingPause": "Pause",
|
||||
"sittingResume": "Resume",
|
||||
"sittingEnd": "End",
|
||||
"sittingPaused": "Paused",
|
||||
"sittingDoneTitle": "Well sat",
|
||||
"sittingDoneMessage": "{minutes, plural, =1{1 minute sat.} other{{minutes} minutes sat.}}",
|
||||
"@sittingDoneMessage": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sittingDone": "Done",
|
||||
"sittingEndTitle": "End the sitting?",
|
||||
"sittingEndMessage": "{minutes, plural, =1{1 minute sat so far. Keep it among your sittings?} other{{minutes} minutes sat so far. Keep them among your sittings?}}",
|
||||
"@sittingEndMessage": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sittingKeep": "Keep",
|
||||
"sittingDiscard": "Discard",
|
||||
"sittingSitOn": "Sit on",
|
||||
"profileTabPractice": "Practice",
|
||||
"figuresMinutes": "Minutes",
|
||||
"figuresSittings": "Sittings",
|
||||
"figuresDays": "Days",
|
||||
"historyTitle": "Your sittings",
|
||||
"sittingDelete": "Delete",
|
||||
"sittingDeleteTitle": "Delete this sitting?",
|
||||
"sittingDeleteMessage": "It goes from your figures on all your devices.",
|
||||
"settingsSitting": "Sitting",
|
||||
"bellTitle": "Bell",
|
||||
"bellBowl": "Singing bowl",
|
||||
"bellSmallBell": "Small bell",
|
||||
"bellWoodBlock": "Wooden block",
|
||||
"startBellTitle": "Bell at the start",
|
||||
"intervalTitle": "Bells between",
|
||||
"intervalNone": "None",
|
||||
"intervalEvery": "Every {minutes} min",
|
||||
"@intervalEvery": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"keepScreenOnTitle": "Keep the screen on",
|
||||
"keepScreenOnHint": "While a sitting runs",
|
||||
"timerLengthTitle": "Silent timer's length",
|
||||
"settingsPractice": "Practice",
|
||||
"showStreakTitle": "Show the days in a row",
|
||||
"showStreakHint": "On Today and in your Profile"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"@@locale": "en_GB"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"@@locale": "en_US"
|
||||
}
|
||||
|
|
@ -0,0 +1,876 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_de.dart';
|
||||
import 'app_localizations_en.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||
/// returned by `AppLocalizations.of(context)`.
|
||||
///
|
||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||
/// `localizationDelegates` list, and the locales they support in the app's
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'l10n/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||
/// home: MyApplicationHome(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Update pubspec.yaml
|
||||
///
|
||||
/// Please make sure to update your pubspec.yaml to include the following
|
||||
/// packages:
|
||||
///
|
||||
/// ```yaml
|
||||
/// dependencies:
|
||||
/// # Internationalization support.
|
||||
/// flutter_localizations:
|
||||
/// sdk: flutter
|
||||
/// intl: any # Use the pinned version from flutter_localizations
|
||||
///
|
||||
/// # Rest of dependencies
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS Applications
|
||||
///
|
||||
/// iOS applications define key application metadata, including supported
|
||||
/// locales, in an Info.plist file that is built into the application bundle.
|
||||
/// To configure the locales supported by your app, you’ll need to edit this
|
||||
/// file.
|
||||
///
|
||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||
/// project’s Runner folder.
|
||||
///
|
||||
/// Next, select the Information Property List item, select Add Item from the
|
||||
/// Editor menu, then select Localizations from the pop-up menu.
|
||||
///
|
||||
/// Select and expand the newly-created Localizations item then, for each
|
||||
/// locale your application supports, add a new item and select the locale
|
||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
static AppLocalizations of(BuildContext context) {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||
_AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
///
|
||||
/// Returns a list of localizations delegates containing this delegate along with
|
||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||
/// and GlobalWidgetsLocalizations.delegate.
|
||||
///
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('de'),
|
||||
Locale('en'),
|
||||
Locale('en', 'GB'),
|
||||
Locale('en', 'US'),
|
||||
];
|
||||
|
||||
/// The app's name: the window's title, and where halkyon_app's texts name the app.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Halkyon Meditate'**
|
||||
String get appTitle;
|
||||
|
||||
/// The app's word in its wordmark, under "Halkyon".
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Meditate'**
|
||||
String get launchProduct;
|
||||
|
||||
/// No description provided for @instanceFirstStartSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose the instance to keep your practice on. Someone runs each one; you can add others later.'**
|
||||
String get instanceFirstStartSubtitle;
|
||||
|
||||
/// No description provided for @accountTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Account'**
|
||||
String get accountTitle;
|
||||
|
||||
/// No description provided for @accountDeleteMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your account is deleted on the server and on all your devices, with every sitting kept. This can\'t be undone.'**
|
||||
String get accountDeleteMessage;
|
||||
|
||||
/// No description provided for @aboutTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'About Halkyon Meditate'**
|
||||
String get aboutTitle;
|
||||
|
||||
/// No description provided for @settingsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settings'**
|
||||
String get settingsTitle;
|
||||
|
||||
/// No description provided for @settingsVersion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Version {version} (build {build})'**
|
||||
String settingsVersion(String version, String build);
|
||||
|
||||
/// No description provided for @keysMissing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This device can\'t save: it doesn\'t hold your account\'s keys. Sign in with your password or recovery phrase to add them.'**
|
||||
String get keysMissing;
|
||||
|
||||
/// No description provided for @shellTabToday.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Today'**
|
||||
String get shellTabToday;
|
||||
|
||||
/// No description provided for @shellTabExplore.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Explore'**
|
||||
String get shellTabExplore;
|
||||
|
||||
/// No description provided for @shellTabProfile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Profile'**
|
||||
String get shellTabProfile;
|
||||
|
||||
/// No description provided for @greetingMorning.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Good morning'**
|
||||
String get greetingMorning;
|
||||
|
||||
/// No description provided for @greetingAfternoon.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Good afternoon'**
|
||||
String get greetingAfternoon;
|
||||
|
||||
/// No description provided for @greetingEvening.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Good evening'**
|
||||
String get greetingEvening;
|
||||
|
||||
/// No description provided for @greetingNight.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A quiet night'**
|
||||
String get greetingNight;
|
||||
|
||||
/// Minutes, after a number typed or stepped.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'min'**
|
||||
String get minutesUnit;
|
||||
|
||||
/// A length in minutes, short, on chips and rows.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{minutes} min'**
|
||||
String minutes(int minutes);
|
||||
|
||||
/// No description provided for @todaySitNow.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sit now'**
|
||||
String get todaySitNow;
|
||||
|
||||
/// No description provided for @todaySitNowHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A silent sitting with bells, as long as you choose.'**
|
||||
String get todaySitNowHint;
|
||||
|
||||
/// No description provided for @todayPracticeOfDay.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Practice of the day'**
|
||||
String get todayPracticeOfDay;
|
||||
|
||||
/// No description provided for @todayThisWeek.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This week'**
|
||||
String get todayThisWeek;
|
||||
|
||||
/// No description provided for @streakDays.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =0{No days in a row yet} =1{1 day in a row} other{{count} days in a row}}'**
|
||||
String streakDays(int count);
|
||||
|
||||
/// No description provided for @favouritesTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Favourites'**
|
||||
String get favouritesTitle;
|
||||
|
||||
/// No description provided for @favouritesNone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No favourites yet. Mark a practice with its heart in Explore.'**
|
||||
String get favouritesNone;
|
||||
|
||||
/// No description provided for @recentTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recently sat'**
|
||||
String get recentTitle;
|
||||
|
||||
/// No description provided for @recentNone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Nothing sat yet.'**
|
||||
String get recentNone;
|
||||
|
||||
/// No description provided for @silentTimer.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Silent timer'**
|
||||
String get silentTimer;
|
||||
|
||||
/// A sitting kept: when it began and how long it was sat.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{date} · {minutes} min'**
|
||||
String sittingRow(String date, int minutes);
|
||||
|
||||
/// No description provided for @exploreSearch.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search practices'**
|
||||
String get exploreSearch;
|
||||
|
||||
/// No description provided for @exploreGoals.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'What brings you here?'**
|
||||
String get exploreGoals;
|
||||
|
||||
/// No description provided for @exploreAll.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'All'**
|
||||
String get exploreAll;
|
||||
|
||||
/// No description provided for @exploreNoMatch.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No practice matches.'**
|
||||
String get exploreNoMatch;
|
||||
|
||||
/// No description provided for @goalCalm.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Calm down'**
|
||||
String get goalCalm;
|
||||
|
||||
/// No description provided for @goalSleep.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sleep'**
|
||||
String get goalSleep;
|
||||
|
||||
/// No description provided for @goalFocus.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Focus'**
|
||||
String get goalFocus;
|
||||
|
||||
/// No description provided for @goalBegin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Begin'**
|
||||
String get goalBegin;
|
||||
|
||||
/// No description provided for @kindBreathing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Breathing'**
|
||||
String get kindBreathing;
|
||||
|
||||
/// No description provided for @kindBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Body'**
|
||||
String get kindBody;
|
||||
|
||||
/// No description provided for @kindAwareness.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Awareness'**
|
||||
String get kindAwareness;
|
||||
|
||||
/// No description provided for @kindKindness.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Kindness'**
|
||||
String get kindKindness;
|
||||
|
||||
/// No description provided for @kindSleep.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sleep'**
|
||||
String get kindSleep;
|
||||
|
||||
/// No description provided for @kindPause.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pause'**
|
||||
String get kindPause;
|
||||
|
||||
/// No description provided for @practiceStart.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start'**
|
||||
String get practiceStart;
|
||||
|
||||
/// No description provided for @practiceLength.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Length'**
|
||||
String get practiceLength;
|
||||
|
||||
/// No description provided for @practiceHowItGoes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'How it goes'**
|
||||
String get practiceHowItGoes;
|
||||
|
||||
/// No description provided for @practiceFavourite.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add to favourites'**
|
||||
String get practiceFavourite;
|
||||
|
||||
/// No description provided for @practiceUnfavourite.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove from favourites'**
|
||||
String get practiceUnfavourite;
|
||||
|
||||
/// No description provided for @practiceGone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This practice isn\'t in this version of the app.'**
|
||||
String get practiceGone;
|
||||
|
||||
/// No description provided for @practiceBreathCountingTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Counting breaths'**
|
||||
String get practiceBreathCountingTitle;
|
||||
|
||||
/// No description provided for @practiceBreathCountingSummary.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Count each out-breath from one to ten, then begin again.'**
|
||||
String get practiceBreathCountingSummary;
|
||||
|
||||
/// No description provided for @practiceBreathCountingCue1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sit comfortably. Let the breath come and go by itself.'**
|
||||
String get practiceBreathCountingCue1;
|
||||
|
||||
/// No description provided for @practiceBreathCountingCue2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'With each out-breath, count: one, two, and on up to ten.'**
|
||||
String get practiceBreathCountingCue2;
|
||||
|
||||
/// No description provided for @practiceBreathCountingCue3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Lost count? Notice it kindly and begin again at one.'**
|
||||
String get practiceBreathCountingCue3;
|
||||
|
||||
/// No description provided for @practiceBreathCountingCue4.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Let the counting go. Rest with the breath as it is.'**
|
||||
String get practiceBreathCountingCue4;
|
||||
|
||||
/// No description provided for @practiceThreeMinutePauseTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Three-minute pause'**
|
||||
String get practiceThreeMinutePauseTitle;
|
||||
|
||||
/// No description provided for @practiceThreeMinutePauseSummary.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A short stop in the day: notice, gather, widen.'**
|
||||
String get practiceThreeMinutePauseSummary;
|
||||
|
||||
/// No description provided for @practiceThreeMinutePauseCue1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'What is here right now? Thoughts, feelings, sensations.'**
|
||||
String get practiceThreeMinutePauseCue1;
|
||||
|
||||
/// No description provided for @practiceThreeMinutePauseCue2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Gather your attention on the breath, low in the belly.'**
|
||||
String get practiceThreeMinutePauseCue2;
|
||||
|
||||
/// No description provided for @practiceThreeMinutePauseCue3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Widen it to the whole body, and to the room around you.'**
|
||||
String get practiceThreeMinutePauseCue3;
|
||||
|
||||
/// No description provided for @practiceBodyScanTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Body scan'**
|
||||
String get practiceBodyScanTitle;
|
||||
|
||||
/// No description provided for @practiceBodyScanSummary.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Move your attention slowly through the body, from the feet up.'**
|
||||
String get practiceBodyScanSummary;
|
||||
|
||||
/// No description provided for @practiceBodyScanCue1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Lie or sit comfortably. Feel where the body meets the ground.'**
|
||||
String get practiceBodyScanCue1;
|
||||
|
||||
/// No description provided for @practiceBodyScanCue2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bring your attention to your feet and legs. Just notice.'**
|
||||
String get practiceBodyScanCue2;
|
||||
|
||||
/// No description provided for @practiceBodyScanCue3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Move up through the belly, the back and the chest.'**
|
||||
String get practiceBodyScanCue3;
|
||||
|
||||
/// No description provided for @practiceBodyScanCue4.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Notice the hands, the arms, the shoulders and the neck.'**
|
||||
String get practiceBodyScanCue4;
|
||||
|
||||
/// No description provided for @practiceBodyScanCue5.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The face and the head. Then feel the whole body at once.'**
|
||||
String get practiceBodyScanCue5;
|
||||
|
||||
/// No description provided for @practiceOpenAwarenessTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open awareness'**
|
||||
String get practiceOpenAwarenessTitle;
|
||||
|
||||
/// No description provided for @practiceOpenAwarenessSummary.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Rest your attention on nothing in particular, and let whatever comes pass.'**
|
||||
String get practiceOpenAwarenessSummary;
|
||||
|
||||
/// No description provided for @practiceOpenAwarenessCue1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Begin with a few breaths, settling in.'**
|
||||
String get practiceOpenAwarenessCue1;
|
||||
|
||||
/// No description provided for @practiceOpenAwarenessCue2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Let the breath go. Notice sounds as they come and go.'**
|
||||
String get practiceOpenAwarenessCue2;
|
||||
|
||||
/// No description provided for @practiceOpenAwarenessCue3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Thoughts may come too. See them arrive, and leave.'**
|
||||
String get practiceOpenAwarenessCue3;
|
||||
|
||||
/// No description provided for @practiceOpenAwarenessCue4.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Simply be aware, with nothing to do.'**
|
||||
String get practiceOpenAwarenessCue4;
|
||||
|
||||
/// No description provided for @practiceLovingKindnessTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Kind wishes'**
|
||||
String get practiceLovingKindnessTitle;
|
||||
|
||||
/// No description provided for @practiceLovingKindnessSummary.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Wish yourself and others well, one circle after the other.'**
|
||||
String get practiceLovingKindnessSummary;
|
||||
|
||||
/// No description provided for @practiceLovingKindnessCue1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Think of yourself: may I be safe, may I be at ease.'**
|
||||
String get practiceLovingKindnessCue1;
|
||||
|
||||
/// No description provided for @practiceLovingKindnessCue2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Think of someone dear to you: may you be safe, may you be at ease.'**
|
||||
String get practiceLovingKindnessCue2;
|
||||
|
||||
/// No description provided for @practiceLovingKindnessCue3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Think of someone you hardly know, and wish them the same.'**
|
||||
String get practiceLovingKindnessCue3;
|
||||
|
||||
/// No description provided for @practiceLovingKindnessCue4.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Let the wishes spread to everyone, everywhere.'**
|
||||
String get practiceLovingKindnessCue4;
|
||||
|
||||
/// No description provided for @practiceSettlingForSleepTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settling for sleep'**
|
||||
String get practiceSettlingForSleepTitle;
|
||||
|
||||
/// No description provided for @practiceSettlingForSleepSummary.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Slow the breath and let the day go before you sleep.'**
|
||||
String get practiceSettlingForSleepSummary;
|
||||
|
||||
/// No description provided for @practiceSettlingForSleepCue1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Lie down. Let the bed carry your weight.'**
|
||||
String get practiceSettlingForSleepCue1;
|
||||
|
||||
/// No description provided for @practiceSettlingForSleepCue2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Breathe out a little longer than you breathe in.'**
|
||||
String get practiceSettlingForSleepCue2;
|
||||
|
||||
/// No description provided for @practiceSettlingForSleepCue3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Let the day\'s thoughts drift past like clouds.'**
|
||||
String get practiceSettlingForSleepCue3;
|
||||
|
||||
/// No description provided for @practiceSettlingForSleepCue4.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Nothing more to do. Let sleep come when it comes.'**
|
||||
String get practiceSettlingForSleepCue4;
|
||||
|
||||
/// No description provided for @timerCue.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sit in silence. The bell tells you when it is over.'**
|
||||
String get timerCue;
|
||||
|
||||
/// No description provided for @sittingPause.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pause'**
|
||||
String get sittingPause;
|
||||
|
||||
/// No description provided for @sittingResume.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Resume'**
|
||||
String get sittingResume;
|
||||
|
||||
/// No description provided for @sittingEnd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'End'**
|
||||
String get sittingEnd;
|
||||
|
||||
/// No description provided for @sittingPaused.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Paused'**
|
||||
String get sittingPaused;
|
||||
|
||||
/// No description provided for @sittingDoneTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Well sat'**
|
||||
String get sittingDoneTitle;
|
||||
|
||||
/// No description provided for @sittingDoneMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{minutes, plural, =1{1 minute sat.} other{{minutes} minutes sat.}}'**
|
||||
String sittingDoneMessage(int minutes);
|
||||
|
||||
/// No description provided for @sittingDone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Done'**
|
||||
String get sittingDone;
|
||||
|
||||
/// No description provided for @sittingEndTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'End the sitting?'**
|
||||
String get sittingEndTitle;
|
||||
|
||||
/// No description provided for @sittingEndMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{minutes, plural, =1{1 minute sat so far. Keep it among your sittings?} other{{minutes} minutes sat so far. Keep them among your sittings?}}'**
|
||||
String sittingEndMessage(int minutes);
|
||||
|
||||
/// No description provided for @sittingKeep.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Keep'**
|
||||
String get sittingKeep;
|
||||
|
||||
/// No description provided for @sittingDiscard.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Discard'**
|
||||
String get sittingDiscard;
|
||||
|
||||
/// No description provided for @sittingSitOn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sit on'**
|
||||
String get sittingSitOn;
|
||||
|
||||
/// No description provided for @profileTabPractice.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Practice'**
|
||||
String get profileTabPractice;
|
||||
|
||||
/// No description provided for @figuresMinutes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Minutes'**
|
||||
String get figuresMinutes;
|
||||
|
||||
/// No description provided for @figuresSittings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sittings'**
|
||||
String get figuresSittings;
|
||||
|
||||
/// No description provided for @figuresDays.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Days'**
|
||||
String get figuresDays;
|
||||
|
||||
/// No description provided for @historyTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your sittings'**
|
||||
String get historyTitle;
|
||||
|
||||
/// No description provided for @sittingDelete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete'**
|
||||
String get sittingDelete;
|
||||
|
||||
/// No description provided for @sittingDeleteTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete this sitting?'**
|
||||
String get sittingDeleteTitle;
|
||||
|
||||
/// No description provided for @sittingDeleteMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'It goes from your figures on all your devices.'**
|
||||
String get sittingDeleteMessage;
|
||||
|
||||
/// No description provided for @settingsSitting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sitting'**
|
||||
String get settingsSitting;
|
||||
|
||||
/// No description provided for @bellTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bell'**
|
||||
String get bellTitle;
|
||||
|
||||
/// No description provided for @bellBowl.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Singing bowl'**
|
||||
String get bellBowl;
|
||||
|
||||
/// No description provided for @bellSmallBell.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Small bell'**
|
||||
String get bellSmallBell;
|
||||
|
||||
/// No description provided for @bellWoodBlock.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Wooden block'**
|
||||
String get bellWoodBlock;
|
||||
|
||||
/// No description provided for @startBellTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bell at the start'**
|
||||
String get startBellTitle;
|
||||
|
||||
/// No description provided for @intervalTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bells between'**
|
||||
String get intervalTitle;
|
||||
|
||||
/// No description provided for @intervalNone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'None'**
|
||||
String get intervalNone;
|
||||
|
||||
/// No description provided for @intervalEvery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Every {minutes} min'**
|
||||
String intervalEvery(int minutes);
|
||||
|
||||
/// No description provided for @keepScreenOnTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Keep the screen on'**
|
||||
String get keepScreenOnTitle;
|
||||
|
||||
/// No description provided for @keepScreenOnHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'While a sitting runs'**
|
||||
String get keepScreenOnHint;
|
||||
|
||||
/// No description provided for @timerLengthTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Silent timer\'s length'**
|
||||
String get timerLengthTitle;
|
||||
|
||||
/// No description provided for @settingsPractice.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Practice'**
|
||||
String get settingsPractice;
|
||||
|
||||
/// No description provided for @showStreakTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show the days in a row'**
|
||||
String get showStreakTitle;
|
||||
|
||||
/// No description provided for @showStreakHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'On Today and in your Profile'**
|
||||
String get showStreakHint;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
<String>['de', 'en'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
// Lookup logic when language+country codes are specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'en':
|
||||
{
|
||||
switch (locale.countryCode) {
|
||||
case 'GB':
|
||||
return AppLocalizationsEnGb();
|
||||
case 'US':
|
||||
return AppLocalizationsEnUs();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'de':
|
||||
return AppLocalizationsDe();
|
||||
case 'en':
|
||||
return AppLocalizationsEn();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,445 @@
|
|||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for German (`de`).
|
||||
class AppLocalizationsDe extends AppLocalizations {
|
||||
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'Halkyon Meditate';
|
||||
|
||||
@override
|
||||
String get launchProduct => 'Meditate';
|
||||
|
||||
@override
|
||||
String get instanceFirstStartSubtitle =>
|
||||
'Wähle die Instanz, auf der deine Praxis liegen soll. Jede betreibt jemand; weitere kannst du später hinzufügen.';
|
||||
|
||||
@override
|
||||
String get accountTitle => 'Konto';
|
||||
|
||||
@override
|
||||
String get accountDeleteMessage =>
|
||||
'Dein Konto wird auf dem Server und auf all deinen Geräten gelöscht, mit jeder gespeicherten Sitzung. Dies kann nicht rückgängig gemacht werden.';
|
||||
|
||||
@override
|
||||
String get aboutTitle => 'Über Halkyon Meditate';
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Einstellungen';
|
||||
|
||||
@override
|
||||
String settingsVersion(String version, String build) {
|
||||
return 'Version $version (Build $build)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get keysMissing =>
|
||||
'Dieses Gerät kann nichts speichern: Es hat die Schlüssel deines Kontos nicht. Melde dich mit deinem Passwort oder deiner Wiederherstellungsphrase an, um sie hinzuzufügen.';
|
||||
|
||||
@override
|
||||
String get shellTabToday => 'Heute';
|
||||
|
||||
@override
|
||||
String get shellTabExplore => 'Entdecken';
|
||||
|
||||
@override
|
||||
String get shellTabProfile => 'Profil';
|
||||
|
||||
@override
|
||||
String get greetingMorning => 'Guten Morgen';
|
||||
|
||||
@override
|
||||
String get greetingAfternoon => 'Guten Tag';
|
||||
|
||||
@override
|
||||
String get greetingEvening => 'Guten Abend';
|
||||
|
||||
@override
|
||||
String get greetingNight => 'Eine stille Nacht';
|
||||
|
||||
@override
|
||||
String get minutesUnit => 'Min.';
|
||||
|
||||
@override
|
||||
String minutes(int minutes) {
|
||||
return '$minutes Min.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get todaySitNow => 'Jetzt sitzen';
|
||||
|
||||
@override
|
||||
String get todaySitNowHint =>
|
||||
'Eine stille Sitzung mit Klängen, so lange du willst.';
|
||||
|
||||
@override
|
||||
String get todayPracticeOfDay => 'Übung des Tages';
|
||||
|
||||
@override
|
||||
String get todayThisWeek => 'Diese Woche';
|
||||
|
||||
@override
|
||||
String streakDays(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count Tage in Folge',
|
||||
one: '1 Tag in Folge',
|
||||
zero: 'Noch keine Tage in Folge',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get favouritesTitle => 'Favoriten';
|
||||
|
||||
@override
|
||||
String get favouritesNone =>
|
||||
'Noch keine Favoriten. Markiere eine Übung unter Entdecken mit ihrem Herz.';
|
||||
|
||||
@override
|
||||
String get recentTitle => 'Zuletzt gesessen';
|
||||
|
||||
@override
|
||||
String get recentNone => 'Noch nichts gesessen.';
|
||||
|
||||
@override
|
||||
String get silentTimer => 'Stiller Timer';
|
||||
|
||||
@override
|
||||
String sittingRow(String date, int minutes) {
|
||||
return '$date · $minutes Min.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get exploreSearch => 'Übungen suchen';
|
||||
|
||||
@override
|
||||
String get exploreGoals => 'Was führt dich her?';
|
||||
|
||||
@override
|
||||
String get exploreAll => 'Alle';
|
||||
|
||||
@override
|
||||
String get exploreNoMatch => 'Keine Übung passt.';
|
||||
|
||||
@override
|
||||
String get goalCalm => 'Zur Ruhe kommen';
|
||||
|
||||
@override
|
||||
String get goalSleep => 'Schlafen';
|
||||
|
||||
@override
|
||||
String get goalFocus => 'Fokus';
|
||||
|
||||
@override
|
||||
String get goalBegin => 'Anfangen';
|
||||
|
||||
@override
|
||||
String get kindBreathing => 'Atem';
|
||||
|
||||
@override
|
||||
String get kindBody => 'Körper';
|
||||
|
||||
@override
|
||||
String get kindAwareness => 'Gewahrsein';
|
||||
|
||||
@override
|
||||
String get kindKindness => 'Güte';
|
||||
|
||||
@override
|
||||
String get kindSleep => 'Schlaf';
|
||||
|
||||
@override
|
||||
String get kindPause => 'Pause';
|
||||
|
||||
@override
|
||||
String get practiceStart => 'Starten';
|
||||
|
||||
@override
|
||||
String get practiceLength => 'Dauer';
|
||||
|
||||
@override
|
||||
String get practiceHowItGoes => 'So geht es';
|
||||
|
||||
@override
|
||||
String get practiceFavourite => 'Zu den Favoriten';
|
||||
|
||||
@override
|
||||
String get practiceUnfavourite => 'Aus den Favoriten entfernen';
|
||||
|
||||
@override
|
||||
String get practiceGone =>
|
||||
'Diese Übung gibt es in dieser Version der App nicht.';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingTitle => 'Atemzüge zählen';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingSummary =>
|
||||
'Zähle jedes Ausatmen von eins bis zehn, dann fang wieder an.';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingCue1 =>
|
||||
'Setz dich bequem hin. Lass den Atem von selbst kommen und gehen.';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingCue2 =>
|
||||
'Zähle mit jedem Ausatmen: eins, zwei und weiter bis zehn.';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingCue3 =>
|
||||
'Verzählt? Bemerke es freundlich und fang wieder bei eins an.';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingCue4 =>
|
||||
'Lass das Zählen los. Ruh beim Atem, wie er ist.';
|
||||
|
||||
@override
|
||||
String get practiceThreeMinutePauseTitle => 'Drei-Minuten-Pause';
|
||||
|
||||
@override
|
||||
String get practiceThreeMinutePauseSummary =>
|
||||
'Ein kurzer Halt im Tag: wahrnehmen, sammeln, weiten.';
|
||||
|
||||
@override
|
||||
String get practiceThreeMinutePauseCue1 =>
|
||||
'Was ist gerade da? Gedanken, Gefühle, Empfindungen.';
|
||||
|
||||
@override
|
||||
String get practiceThreeMinutePauseCue2 =>
|
||||
'Sammle deine Aufmerksamkeit beim Atem, tief im Bauch.';
|
||||
|
||||
@override
|
||||
String get practiceThreeMinutePauseCue3 =>
|
||||
'Weite sie auf den ganzen Körper und den Raum um dich.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanTitle => 'Körperreise';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanSummary =>
|
||||
'Wandere mit der Aufmerksamkeit langsam durch den Körper, von den Füßen aufwärts.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanCue1 =>
|
||||
'Leg oder setz dich bequem hin. Spür, wo der Körper den Boden berührt.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanCue2 =>
|
||||
'Bring deine Aufmerksamkeit zu den Füßen und Beinen. Nimm einfach wahr.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanCue3 =>
|
||||
'Wandere hinauf durch Bauch, Rücken und Brust.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanCue4 =>
|
||||
'Spür die Hände, die Arme, die Schultern und den Nacken.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanCue5 =>
|
||||
'Das Gesicht und den Kopf. Dann den ganzen Körper auf einmal.';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessTitle => 'Offenes Gewahrsein';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessSummary =>
|
||||
'Lass die Aufmerksamkeit auf nichts Bestimmtem ruhen und alles vorüberziehen.';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessCue1 =>
|
||||
'Beginne mit ein paar Atemzügen und komm an.';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessCue2 =>
|
||||
'Lass den Atem los. Nimm Geräusche wahr, wie sie kommen und gehen.';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessCue3 =>
|
||||
'Auch Gedanken dürfen kommen. Sieh sie auftauchen und gehen.';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessCue4 =>
|
||||
'Sei einfach gewahr, ohne etwas zu tun.';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessTitle => 'Gute Wünsche';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessSummary =>
|
||||
'Wünsch dir und anderen Gutes, Kreis um Kreis.';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessCue1 =>
|
||||
'Denk an dich: Möge ich sicher sein, möge es mir leicht sein.';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessCue2 =>
|
||||
'Denk an jemanden, der dir lieb ist: Mögest du sicher sein, möge es dir leicht sein.';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessCue3 =>
|
||||
'Denk an jemanden, den du kaum kennst, und wünsch ihm dasselbe.';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessCue4 =>
|
||||
'Lass die Wünsche zu allen reichen, überall.';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepTitle => 'Zur Nacht ankommen';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepSummary =>
|
||||
'Verlangsame den Atem und lass den Tag los, bevor du schläfst.';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepCue1 =>
|
||||
'Leg dich hin. Lass dich vom Bett tragen.';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepCue2 =>
|
||||
'Atme etwas länger aus, als du einatmest.';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepCue3 =>
|
||||
'Lass die Gedanken des Tages wie Wolken vorüberziehen.';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepCue4 =>
|
||||
'Es gibt nichts mehr zu tun. Lass den Schlaf kommen, wenn er kommt.';
|
||||
|
||||
@override
|
||||
String get timerCue =>
|
||||
'Sitz in Stille. Der Klang sagt dir, wann es vorbei ist.';
|
||||
|
||||
@override
|
||||
String get sittingPause => 'Pause';
|
||||
|
||||
@override
|
||||
String get sittingResume => 'Weiter';
|
||||
|
||||
@override
|
||||
String get sittingEnd => 'Beenden';
|
||||
|
||||
@override
|
||||
String get sittingPaused => 'Pausiert';
|
||||
|
||||
@override
|
||||
String get sittingDoneTitle => 'Gut gesessen';
|
||||
|
||||
@override
|
||||
String sittingDoneMessage(int minutes) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
minutes,
|
||||
locale: localeName,
|
||||
other: '$minutes Minuten gesessen.',
|
||||
one: '1 Minute gesessen.',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sittingDone => 'Fertig';
|
||||
|
||||
@override
|
||||
String get sittingEndTitle => 'Sitzung beenden?';
|
||||
|
||||
@override
|
||||
String sittingEndMessage(int minutes) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
minutes,
|
||||
locale: localeName,
|
||||
other: 'Bisher $minutes Minuten gesessen. Bei deinen Sitzungen behalten?',
|
||||
one: 'Bisher 1 Minute gesessen. Bei deinen Sitzungen behalten?',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sittingKeep => 'Behalten';
|
||||
|
||||
@override
|
||||
String get sittingDiscard => 'Verwerfen';
|
||||
|
||||
@override
|
||||
String get sittingSitOn => 'Weitersitzen';
|
||||
|
||||
@override
|
||||
String get profileTabPractice => 'Praxis';
|
||||
|
||||
@override
|
||||
String get figuresMinutes => 'Minuten';
|
||||
|
||||
@override
|
||||
String get figuresSittings => 'Sitzungen';
|
||||
|
||||
@override
|
||||
String get figuresDays => 'Tage';
|
||||
|
||||
@override
|
||||
String get historyTitle => 'Deine Sitzungen';
|
||||
|
||||
@override
|
||||
String get sittingDelete => 'Löschen';
|
||||
|
||||
@override
|
||||
String get sittingDeleteTitle => 'Diese Sitzung löschen?';
|
||||
|
||||
@override
|
||||
String get sittingDeleteMessage =>
|
||||
'Sie verschwindet aus deinen Zahlen, auf all deinen Geräten.';
|
||||
|
||||
@override
|
||||
String get settingsSitting => 'Sitzen';
|
||||
|
||||
@override
|
||||
String get bellTitle => 'Klang';
|
||||
|
||||
@override
|
||||
String get bellBowl => 'Klangschale';
|
||||
|
||||
@override
|
||||
String get bellSmallBell => 'Kleine Glocke';
|
||||
|
||||
@override
|
||||
String get bellWoodBlock => 'Holzblock';
|
||||
|
||||
@override
|
||||
String get startBellTitle => 'Klang zu Beginn';
|
||||
|
||||
@override
|
||||
String get intervalTitle => 'Klänge dazwischen';
|
||||
|
||||
@override
|
||||
String get intervalNone => 'Keine';
|
||||
|
||||
@override
|
||||
String intervalEvery(int minutes) {
|
||||
return 'Alle $minutes Min.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get keepScreenOnTitle => 'Bildschirm anlassen';
|
||||
|
||||
@override
|
||||
String get keepScreenOnHint => 'Solange eine Sitzung läuft';
|
||||
|
||||
@override
|
||||
String get timerLengthTitle => 'Dauer des stillen Timers';
|
||||
|
||||
@override
|
||||
String get settingsPractice => 'Praxis';
|
||||
|
||||
@override
|
||||
String get showStreakTitle => 'Tage in Folge zeigen';
|
||||
|
||||
@override
|
||||
String get showStreakHint => 'Unter Heute und in deinem Profil';
|
||||
}
|
||||
|
|
@ -0,0 +1,453 @@
|
|||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'Halkyon Meditate';
|
||||
|
||||
@override
|
||||
String get launchProduct => 'Meditate';
|
||||
|
||||
@override
|
||||
String get instanceFirstStartSubtitle =>
|
||||
'Choose the instance to keep your practice on. Someone runs each one; you can add others later.';
|
||||
|
||||
@override
|
||||
String get accountTitle => 'Account';
|
||||
|
||||
@override
|
||||
String get accountDeleteMessage =>
|
||||
'Your account is deleted on the server and on all your devices, with every sitting kept. This can\'t be undone.';
|
||||
|
||||
@override
|
||||
String get aboutTitle => 'About Halkyon Meditate';
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Settings';
|
||||
|
||||
@override
|
||||
String settingsVersion(String version, String build) {
|
||||
return 'Version $version (build $build)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get keysMissing =>
|
||||
'This device can\'t save: it doesn\'t hold your account\'s keys. Sign in with your password or recovery phrase to add them.';
|
||||
|
||||
@override
|
||||
String get shellTabToday => 'Today';
|
||||
|
||||
@override
|
||||
String get shellTabExplore => 'Explore';
|
||||
|
||||
@override
|
||||
String get shellTabProfile => 'Profile';
|
||||
|
||||
@override
|
||||
String get greetingMorning => 'Good morning';
|
||||
|
||||
@override
|
||||
String get greetingAfternoon => 'Good afternoon';
|
||||
|
||||
@override
|
||||
String get greetingEvening => 'Good evening';
|
||||
|
||||
@override
|
||||
String get greetingNight => 'A quiet night';
|
||||
|
||||
@override
|
||||
String get minutesUnit => 'min';
|
||||
|
||||
@override
|
||||
String minutes(int minutes) {
|
||||
return '$minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String get todaySitNow => 'Sit now';
|
||||
|
||||
@override
|
||||
String get todaySitNowHint =>
|
||||
'A silent sitting with bells, as long as you choose.';
|
||||
|
||||
@override
|
||||
String get todayPracticeOfDay => 'Practice of the day';
|
||||
|
||||
@override
|
||||
String get todayThisWeek => 'This week';
|
||||
|
||||
@override
|
||||
String streakDays(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count days in a row',
|
||||
one: '1 day in a row',
|
||||
zero: 'No days in a row yet',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get favouritesTitle => 'Favourites';
|
||||
|
||||
@override
|
||||
String get favouritesNone =>
|
||||
'No favourites yet. Mark a practice with its heart in Explore.';
|
||||
|
||||
@override
|
||||
String get recentTitle => 'Recently sat';
|
||||
|
||||
@override
|
||||
String get recentNone => 'Nothing sat yet.';
|
||||
|
||||
@override
|
||||
String get silentTimer => 'Silent timer';
|
||||
|
||||
@override
|
||||
String sittingRow(String date, int minutes) {
|
||||
return '$date · $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String get exploreSearch => 'Search practices';
|
||||
|
||||
@override
|
||||
String get exploreGoals => 'What brings you here?';
|
||||
|
||||
@override
|
||||
String get exploreAll => 'All';
|
||||
|
||||
@override
|
||||
String get exploreNoMatch => 'No practice matches.';
|
||||
|
||||
@override
|
||||
String get goalCalm => 'Calm down';
|
||||
|
||||
@override
|
||||
String get goalSleep => 'Sleep';
|
||||
|
||||
@override
|
||||
String get goalFocus => 'Focus';
|
||||
|
||||
@override
|
||||
String get goalBegin => 'Begin';
|
||||
|
||||
@override
|
||||
String get kindBreathing => 'Breathing';
|
||||
|
||||
@override
|
||||
String get kindBody => 'Body';
|
||||
|
||||
@override
|
||||
String get kindAwareness => 'Awareness';
|
||||
|
||||
@override
|
||||
String get kindKindness => 'Kindness';
|
||||
|
||||
@override
|
||||
String get kindSleep => 'Sleep';
|
||||
|
||||
@override
|
||||
String get kindPause => 'Pause';
|
||||
|
||||
@override
|
||||
String get practiceStart => 'Start';
|
||||
|
||||
@override
|
||||
String get practiceLength => 'Length';
|
||||
|
||||
@override
|
||||
String get practiceHowItGoes => 'How it goes';
|
||||
|
||||
@override
|
||||
String get practiceFavourite => 'Add to favourites';
|
||||
|
||||
@override
|
||||
String get practiceUnfavourite => 'Remove from favourites';
|
||||
|
||||
@override
|
||||
String get practiceGone => 'This practice isn\'t in this version of the app.';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingTitle => 'Counting breaths';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingSummary =>
|
||||
'Count each out-breath from one to ten, then begin again.';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingCue1 =>
|
||||
'Sit comfortably. Let the breath come and go by itself.';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingCue2 =>
|
||||
'With each out-breath, count: one, two, and on up to ten.';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingCue3 =>
|
||||
'Lost count? Notice it kindly and begin again at one.';
|
||||
|
||||
@override
|
||||
String get practiceBreathCountingCue4 =>
|
||||
'Let the counting go. Rest with the breath as it is.';
|
||||
|
||||
@override
|
||||
String get practiceThreeMinutePauseTitle => 'Three-minute pause';
|
||||
|
||||
@override
|
||||
String get practiceThreeMinutePauseSummary =>
|
||||
'A short stop in the day: notice, gather, widen.';
|
||||
|
||||
@override
|
||||
String get practiceThreeMinutePauseCue1 =>
|
||||
'What is here right now? Thoughts, feelings, sensations.';
|
||||
|
||||
@override
|
||||
String get practiceThreeMinutePauseCue2 =>
|
||||
'Gather your attention on the breath, low in the belly.';
|
||||
|
||||
@override
|
||||
String get practiceThreeMinutePauseCue3 =>
|
||||
'Widen it to the whole body, and to the room around you.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanTitle => 'Body scan';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanSummary =>
|
||||
'Move your attention slowly through the body, from the feet up.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanCue1 =>
|
||||
'Lie or sit comfortably. Feel where the body meets the ground.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanCue2 =>
|
||||
'Bring your attention to your feet and legs. Just notice.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanCue3 =>
|
||||
'Move up through the belly, the back and the chest.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanCue4 =>
|
||||
'Notice the hands, the arms, the shoulders and the neck.';
|
||||
|
||||
@override
|
||||
String get practiceBodyScanCue5 =>
|
||||
'The face and the head. Then feel the whole body at once.';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessTitle => 'Open awareness';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessSummary =>
|
||||
'Rest your attention on nothing in particular, and let whatever comes pass.';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessCue1 =>
|
||||
'Begin with a few breaths, settling in.';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessCue2 =>
|
||||
'Let the breath go. Notice sounds as they come and go.';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessCue3 =>
|
||||
'Thoughts may come too. See them arrive, and leave.';
|
||||
|
||||
@override
|
||||
String get practiceOpenAwarenessCue4 =>
|
||||
'Simply be aware, with nothing to do.';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessTitle => 'Kind wishes';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessSummary =>
|
||||
'Wish yourself and others well, one circle after the other.';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessCue1 =>
|
||||
'Think of yourself: may I be safe, may I be at ease.';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessCue2 =>
|
||||
'Think of someone dear to you: may you be safe, may you be at ease.';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessCue3 =>
|
||||
'Think of someone you hardly know, and wish them the same.';
|
||||
|
||||
@override
|
||||
String get practiceLovingKindnessCue4 =>
|
||||
'Let the wishes spread to everyone, everywhere.';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepTitle => 'Settling for sleep';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepSummary =>
|
||||
'Slow the breath and let the day go before you sleep.';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepCue1 =>
|
||||
'Lie down. Let the bed carry your weight.';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepCue2 =>
|
||||
'Breathe out a little longer than you breathe in.';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepCue3 =>
|
||||
'Let the day\'s thoughts drift past like clouds.';
|
||||
|
||||
@override
|
||||
String get practiceSettlingForSleepCue4 =>
|
||||
'Nothing more to do. Let sleep come when it comes.';
|
||||
|
||||
@override
|
||||
String get timerCue => 'Sit in silence. The bell tells you when it is over.';
|
||||
|
||||
@override
|
||||
String get sittingPause => 'Pause';
|
||||
|
||||
@override
|
||||
String get sittingResume => 'Resume';
|
||||
|
||||
@override
|
||||
String get sittingEnd => 'End';
|
||||
|
||||
@override
|
||||
String get sittingPaused => 'Paused';
|
||||
|
||||
@override
|
||||
String get sittingDoneTitle => 'Well sat';
|
||||
|
||||
@override
|
||||
String sittingDoneMessage(int minutes) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
minutes,
|
||||
locale: localeName,
|
||||
other: '$minutes minutes sat.',
|
||||
one: '1 minute sat.',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sittingDone => 'Done';
|
||||
|
||||
@override
|
||||
String get sittingEndTitle => 'End the sitting?';
|
||||
|
||||
@override
|
||||
String sittingEndMessage(int minutes) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
minutes,
|
||||
locale: localeName,
|
||||
other: '$minutes minutes sat so far. Keep them among your sittings?',
|
||||
one: '1 minute sat so far. Keep it among your sittings?',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sittingKeep => 'Keep';
|
||||
|
||||
@override
|
||||
String get sittingDiscard => 'Discard';
|
||||
|
||||
@override
|
||||
String get sittingSitOn => 'Sit on';
|
||||
|
||||
@override
|
||||
String get profileTabPractice => 'Practice';
|
||||
|
||||
@override
|
||||
String get figuresMinutes => 'Minutes';
|
||||
|
||||
@override
|
||||
String get figuresSittings => 'Sittings';
|
||||
|
||||
@override
|
||||
String get figuresDays => 'Days';
|
||||
|
||||
@override
|
||||
String get historyTitle => 'Your sittings';
|
||||
|
||||
@override
|
||||
String get sittingDelete => 'Delete';
|
||||
|
||||
@override
|
||||
String get sittingDeleteTitle => 'Delete this sitting?';
|
||||
|
||||
@override
|
||||
String get sittingDeleteMessage =>
|
||||
'It goes from your figures on all your devices.';
|
||||
|
||||
@override
|
||||
String get settingsSitting => 'Sitting';
|
||||
|
||||
@override
|
||||
String get bellTitle => 'Bell';
|
||||
|
||||
@override
|
||||
String get bellBowl => 'Singing bowl';
|
||||
|
||||
@override
|
||||
String get bellSmallBell => 'Small bell';
|
||||
|
||||
@override
|
||||
String get bellWoodBlock => 'Wooden block';
|
||||
|
||||
@override
|
||||
String get startBellTitle => 'Bell at the start';
|
||||
|
||||
@override
|
||||
String get intervalTitle => 'Bells between';
|
||||
|
||||
@override
|
||||
String get intervalNone => 'None';
|
||||
|
||||
@override
|
||||
String intervalEvery(int minutes) {
|
||||
return 'Every $minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String get keepScreenOnTitle => 'Keep the screen on';
|
||||
|
||||
@override
|
||||
String get keepScreenOnHint => 'While a sitting runs';
|
||||
|
||||
@override
|
||||
String get timerLengthTitle => 'Silent timer\'s length';
|
||||
|
||||
@override
|
||||
String get settingsPractice => 'Practice';
|
||||
|
||||
@override
|
||||
String get showStreakTitle => 'Show the days in a row';
|
||||
|
||||
@override
|
||||
String get showStreakHint => 'On Today and in your Profile';
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
class AppLocalizationsEnGb extends AppLocalizationsEn {
|
||||
AppLocalizationsEnGb() : super('en_GB');
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United States (`en_US`).
|
||||
class AppLocalizationsEnUs extends AppLocalizationsEn {
|
||||
AppLocalizationsEnUs() : super('en_US');
|
||||
}
|
||||
51
apps/meditate/halkyon_meditate_flutter/lib/main.dart
Normal file
51
apps/meditate/halkyon_meditate_flutter/lib/main.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/app/licences.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/app/meditate_app.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/app/meditate_dependencies.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:serverpod_flutter/serverpod_flutter.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// Noto Sans' notice (halkyon_ui), the flags' and Unicode CLDR's
|
||||
// (halkyon_app), and Lucide's for the app's icon.
|
||||
registerFontLicenses();
|
||||
registerHalkyonAppLicenses();
|
||||
registerLucideLicense();
|
||||
// The instance the build names (`--dart-define=SERVER_URL=…`, or
|
||||
// `assets/config.json`): the first offered at the first start.
|
||||
final builtIn = Instance.parse(await getServerUrl())!;
|
||||
final device = await MeditateDevice.create(builtIn: builtIn);
|
||||
final instances = InstanceChoice(
|
||||
FileInstanceStore(File(p.join(device.support, 'instances.json'))),
|
||||
builtIn: builtIn,
|
||||
forget: device.storage.forget,
|
||||
sharesStorage: device.storage.sharesWith,
|
||||
);
|
||||
await instances.load(usedBefore: device.storage.usedBefore());
|
||||
runApp(
|
||||
// What's new in each release (Train's ADR 0030, D61), from
|
||||
// assets/whats_new/, shown once after an update.
|
||||
WhatsNewScope(
|
||||
whatsNew: WhatsNew(
|
||||
AssetWhatsNewSource(rootBundle),
|
||||
FileWhatsNewStore(File(p.join(device.support, 'whats_new.json'))),
|
||||
version: device.appVersion,
|
||||
),
|
||||
// An instance chosen before: the app ran on this device already.
|
||||
usedBefore: instances.current != null,
|
||||
child: MeditateApp(
|
||||
choice: instances,
|
||||
// Only an instance of Halkyon Meditate: one of another Halkyon app's
|
||||
// answers too, but holds nothing of this app's.
|
||||
checker: ServerpodInstanceChecker(app: 'meditate'),
|
||||
open: (instance) => MeditateDependencies.create(instance, device),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Where Lucide's notice is: its licence, verbatim from the release the app's
|
||||
/// icon was drawn from (ADR 0031).
|
||||
const lucideLicenseAsset = 'assets/icon/LICENSE-lucide.txt';
|
||||
|
||||
/// Adds Lucide's notice (ISC) to the app's licence list: the app's icon is
|
||||
/// Lucide's (ADR 0031), and its licence asks that the notice go with it.
|
||||
void registerLucideLicense() => LicenseRegistry.addLicense(() async* {
|
||||
yield LicenseEntryWithLineBreaks(const [
|
||||
'Lucide (the app icon)',
|
||||
], await rootBundle.loadString(lucideLicenseAsset));
|
||||
});
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_meditate_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/app/meditate_dependencies.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/app/meditate_frame.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/app/meditate_languages.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/app/meditate_router.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/sitting/bloc/practice_cubit.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The device's look until Meditate has settings: the system's theme mode and
|
||||
/// language.
|
||||
const _systemLook = (themeMode: ThemeMode.system, locale: null);
|
||||
|
||||
/// Halkyon Meditate: halkyon_app's [InstanceGate] (the first start, the account
|
||||
/// on each instance) with Meditate's [meditateFrame], and on the instance in use
|
||||
/// halkyon_app's [SessionGate], which shows each signed-in person their own
|
||||
/// [MeditatePersonApp].
|
||||
class const MeditateApp({
|
||||
/// The instances the device keeps an account on, one in use.
|
||||
required final InstanceChoice choice,
|
||||
|
||||
/// Checks an address before the app moves there: only an instance of
|
||||
/// Meditate's.
|
||||
required final InstanceChecker checker,
|
||||
|
||||
/// Names the countries instances keep their data in: Unicode CLDR's,
|
||||
/// halkyon_app's, unless a test gives others.
|
||||
final CountryNames countryNames = const AssetCountryNames(),
|
||||
|
||||
/// Meditate's dependencies on an instance.
|
||||
required final Future<MeditateDependencies> Function(Instance instance) open,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => InstanceGate(
|
||||
frame: meditateFrame,
|
||||
choice: choice,
|
||||
checker: checker,
|
||||
countryNames: countryNames,
|
||||
open: open,
|
||||
readLook: () async => _systemLook,
|
||||
app: SessionGate(
|
||||
look: (context) => _systemLook,
|
||||
app: (person) => MeditatePersonApp(person: person),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// A signed-in person's app, built anew for every person ([SessionGate]):
|
||||
/// their [PersonScope] around its router's navigator, so every screen and
|
||||
/// sheet is inside it, and the tabs ([buildMeditateRouter]). Every screen washes
|
||||
/// down from the top in fern (ADR 0038, D3, the developer's of 2 Nov), as
|
||||
/// Learn's in cobalt and Train's in carrot.
|
||||
class const MeditatePersonApp({
|
||||
/// Whose app it is.
|
||||
required final SignedIn person,
|
||||
super.key,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
State<MeditatePersonApp> createState() => _MeditatePersonAppState();
|
||||
}
|
||||
|
||||
class _MeditatePersonAppState() extends State<MeditatePersonApp> {
|
||||
final _router = buildMeditateRouter();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_router.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
|
||||
theme: appTheme(Brightness.light),
|
||||
darkTheme: appTheme(Brightness.dark),
|
||||
localizationsDelegates: meditateLocalizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localeListResolutionCallback: resolveAppLocale,
|
||||
// How many accounts wait, for whoever operates the instance (Settings'
|
||||
// operator card and the badge on Profile and the cogs), and where sync
|
||||
// stands.
|
||||
builder: (context, child) => PersonScope(
|
||||
person: widget.person,
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
OperatingCubit(AppScope.of(context).instanceOperators),
|
||||
),
|
||||
// The person's settings and sittings, for every tab and screen.
|
||||
BlocProvider(
|
||||
create: (context) {
|
||||
final person = PersonScope.of<MeditatePerson>(context);
|
||||
return PracticeCubit(person.settings, person.sittings)..open();
|
||||
},
|
||||
),
|
||||
// Where sync stands: Settings' card and the dot on the cog.
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
SyncStatusBloc(PersonScope.of<MeditatePerson>(context).sync),
|
||||
),
|
||||
],
|
||||
child: ScreenColor(
|
||||
color: PaletteColor.fern.background,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,360 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_meditate_client/halkyon_meditate_client.dart'
|
||||
show Client, UuidValue;
|
||||
import 'package:halkyon_meditate_flutter/src/core/device/screen_awake.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/core/privacy/meditate_keys.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/core/privacy/sync_content_key_gateway.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/core/sound/bell_player.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/settings/data/sync_settings_gateway.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/sitting/data/sitting_repository.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/sitting/data/sync_sitting_gateway.dart';
|
||||
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart';
|
||||
import 'package:serverpod_flutter/serverpod_flutter.dart';
|
||||
import 'package:serverpod_offline_sync_client/offline_sync.dart';
|
||||
|
||||
/// A signed-in person's dependencies: who they are, their settings and
|
||||
/// sittings on their own local database in their folder, which the sync
|
||||
/// engine keeps in step with their other devices, every row sealed with
|
||||
/// their key (ADR 0038, D7), and the device's bells and screen for sitting
|
||||
/// (D5, D6). halkyon_app's `PersonScope` opens and disposes them with the
|
||||
/// person.
|
||||
class MeditatePerson(
|
||||
/// Whose they are.
|
||||
final SignedIn person, {
|
||||
|
||||
/// Their sync with the instance: where it stands, for Settings' card.
|
||||
required final SyncService sync,
|
||||
|
||||
/// Their settings of Meditate's, sealed and synced.
|
||||
required final SettingsRepository settings,
|
||||
|
||||
/// The sittings they kept, sealed and synced.
|
||||
required final SittingRepository sittings,
|
||||
|
||||
/// The device's bells; none in tests that don't say.
|
||||
final BellPlayer bells = const NoBells(),
|
||||
|
||||
/// The device's screen, kept on while sitting; nothing in tests that
|
||||
/// don't say.
|
||||
final ScreenAwake screen = const NoScreenAwake(),
|
||||
final Future<void> Function()? _onDispose,
|
||||
}) implements Disposable {
|
||||
/// Closes what was opened for the person.
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await _onDispose?.call();
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a signed-in person's dependencies ([MeditateDependencies.openPerson]).
|
||||
typedef OpenMeditatePerson = Future<MeditatePerson> Function(SignedIn person);
|
||||
|
||||
/// The app's composition root on one instance, built as the instance opens and
|
||||
/// handed down through halkyon_app's [AppScope] (`InstanceGate`), whose parts
|
||||
/// it provides ([AppDependencies]). Tests build it with halkyon_app's fakes.
|
||||
class MeditateDependencies({
|
||||
/// The server's name as people know it (its host), for messages about it.
|
||||
@override required final String serverName,
|
||||
|
||||
/// Who is signed in, and signing in and out.
|
||||
@override required final AuthRepository auth,
|
||||
|
||||
/// Signing in with a password and TOTP, and what keeps an account (ADR
|
||||
/// 0017).
|
||||
@override required final PasswordRepository passwords,
|
||||
|
||||
/// Signing in with SimpleLogin, and the keys of the accounts that do (ADR
|
||||
/// 0020).
|
||||
@override required final SimpleLoginRepository simpleLogin,
|
||||
|
||||
/// What the instance's operators do there (Halkyon Learn's ADR 0021,
|
||||
/// D21 to D24). Meditate's server is a member of the instance, which leaves
|
||||
/// the operators' work to the register's service, so it shows nothing
|
||||
/// until Meditate's server holds the register.
|
||||
@override required final InstanceOperatorRepository instanceOperators,
|
||||
|
||||
/// The assistants the operator granted the person (Halkyon Learn's ADR
|
||||
/// 0021, step 2). A member of the instance offers none for now, so Meditate
|
||||
/// shows none; it asks all the same, as an instance whose register Meditate's
|
||||
/// server holds would answer.
|
||||
@override required final AssistantRepository assistants,
|
||||
|
||||
/// Granting assistants, as the instance's operators do (ADR 0021); as
|
||||
/// [instanceOperators], nothing on a member.
|
||||
@override required final AssistantOperatorRepository assistantOperators,
|
||||
|
||||
/// Keeps a file the app made where the person chooses (a recovery phrase,
|
||||
/// recovery codes).
|
||||
@override required final FileSaver fileSaver,
|
||||
|
||||
/// Lets the person pick a file of the device (their profile's picture).
|
||||
@override required final FilePicker files,
|
||||
|
||||
/// People's public profiles on the instance, the person's own among them:
|
||||
/// one for every Halkyon app (decided 2026-11-02).
|
||||
@override required final PublicProfileRepository profiles,
|
||||
|
||||
/// Where the source of what the instance runs is, as its basics name it
|
||||
/// (halkyon_base): both licences ask to offer it, which About does
|
||||
/// ([sourceCodeUrl]).
|
||||
required final Future<String> Function() _sourceCodeUrl,
|
||||
|
||||
/// Opens a signed-in person's dependencies ([openPerson]).
|
||||
required final OpenMeditatePerson _openPerson,
|
||||
|
||||
/// This build's version and number, which Settings shows at its foot so
|
||||
/// that a report can name the release; none in tests that don't say.
|
||||
final ({String version, int? build}) appVersion = (version: '', build: null),
|
||||
final Future<void> Function()? _onDispose,
|
||||
}) implements AppDependencies<MeditatePerson> {
|
||||
/// The dependencies above [context] ([AppScope]).
|
||||
static MeditateDependencies of(BuildContext context) =>
|
||||
AppScope.of<MeditateDependencies>(context);
|
||||
|
||||
@override
|
||||
Future<MeditatePerson> openPerson(SignedIn person) => _openPerson(person);
|
||||
|
||||
@override
|
||||
Future<String> sourceCodeUrl() => _sourceCodeUrl();
|
||||
|
||||
/// Builds the production graph on [instance], around one Serverpod client
|
||||
/// of its own, with what [device] shares across instances. Its session and
|
||||
/// its people's folders are the instance's own (`InstanceStorage`); the
|
||||
/// client holds no secrets.
|
||||
static Future<MeditateDependencies> create(
|
||||
Instance instance,
|
||||
MeditateDevice device,
|
||||
) async {
|
||||
final client = Client('${instance.url}')
|
||||
..connectivityMonitor = FlutterConnectivityMonitor()
|
||||
..authSessionManager = FlutterAuthSessionManager(
|
||||
storage: SecureClientAuthSuccessStorage(
|
||||
authSuccessStorageKey: device.storage.sessionKeyOf(instance),
|
||||
),
|
||||
);
|
||||
// Restores a stored session before anything asks who is signed in, so
|
||||
// the app doesn't ask how to start while someone is.
|
||||
await client.auth.initialize();
|
||||
|
||||
final authGateway = ServerpodAuthGateway(
|
||||
client,
|
||||
base: client.modules.base,
|
||||
waits: SecureAccountWaitStore(device.storage.waitOf(instance)),
|
||||
);
|
||||
// As the session above: an account waiting for the operator shows its
|
||||
// wait, not the start screen.
|
||||
await authGateway.initialize();
|
||||
// Whether the server still works with this build: asked before the
|
||||
// first sync of each launch.
|
||||
final compatibility = AppCompatibility(
|
||||
appBuild: device.appBuild,
|
||||
minimumAppBuild: () async =>
|
||||
(await client.modules.base.instance.basics()).minimumAppBuild,
|
||||
);
|
||||
final people = device.storage.peopleOf(instance);
|
||||
final folders = PersonFolders(people);
|
||||
final addresses = FileSignInAddresses(people);
|
||||
final accountCrypto = SodiumAccountCrypto();
|
||||
final accountKeys = SecureAccountKeyStore(device.storage.keysOf(instance));
|
||||
final passwords = PasswordRepository(
|
||||
ServerpodPasswordGateway(client, base: client.modules.base),
|
||||
accountCrypto,
|
||||
accountKeys,
|
||||
authGateway,
|
||||
addresses,
|
||||
);
|
||||
final simpleLogin = SimpleLoginRepository(
|
||||
ServerpodSimpleLoginGateway(client, base: client.modules.base),
|
||||
OidcBrowser(loopback: LoopbackWebAuth(appName: 'Halkyon Meditate')),
|
||||
accountCrypto,
|
||||
accountKeys,
|
||||
authGateway,
|
||||
// The ways back the instance's forwarding page sends Meditate's sign-ins
|
||||
// on by (halkyon_base's `simplelogin_callback.dart`): the scheme each
|
||||
// platform's project registers for the app, and Meditate's loopback port.
|
||||
callbackScheme: 'app.halkyon.meditate',
|
||||
loopbackPort: 43826,
|
||||
);
|
||||
final instanceOperators = InstanceOperatorRepository(
|
||||
ServerpodInstanceOperatorGateway(client.modules.base),
|
||||
);
|
||||
final assistants = AssistantRepository(
|
||||
ServerpodAssistantGateway(client.modules.base),
|
||||
);
|
||||
final assistantOperators = AssistantOperatorRepository(
|
||||
ServerpodAssistantOperatorGateway(client.modules.base),
|
||||
ServerpodInstanceOperatorGateway(client.modules.base),
|
||||
);
|
||||
// The last person's data closing, before the next one's opens.
|
||||
var previousClosed = Future<void>.value();
|
||||
return MeditateDependencies(
|
||||
serverName: instance.name,
|
||||
auth: AuthRepository(authGateway, addresses),
|
||||
passwords: passwords,
|
||||
simpleLogin: simpleLogin,
|
||||
instanceOperators: instanceOperators,
|
||||
assistants: assistants,
|
||||
assistantOperators: assistantOperators,
|
||||
fileSaver: PlatformFileSaver(),
|
||||
files: FileSelectorFilePicker(),
|
||||
profiles: PublicProfileRepository(
|
||||
BasePublicProfileGateway(client.modules.base),
|
||||
),
|
||||
appVersion: (version: device.appVersion, build: device.appBuild),
|
||||
sourceCodeUrl: () async =>
|
||||
(await client.modules.base.instance.basics()).sourceCodeUrl,
|
||||
openPerson: (person) async {
|
||||
// The person before may still be closing their database, which may
|
||||
// be this one's (signed in again): waited for, but not for ever.
|
||||
await previousClosed.timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {},
|
||||
);
|
||||
final closed = Completer<void>();
|
||||
previousClosed = closed.future;
|
||||
try {
|
||||
return await _openPersonData(
|
||||
client,
|
||||
compatibility,
|
||||
person,
|
||||
// One person's folder on the device at a time, as Learn keeps it.
|
||||
await folders.openOnly(person.authUserId),
|
||||
accountKeys: () => accountKeys.read(person.authUserId),
|
||||
bells: device.bells,
|
||||
screen: device.screen,
|
||||
onClosed: closed.complete,
|
||||
);
|
||||
} on Object {
|
||||
// Nothing was opened, so there is nothing to wait for.
|
||||
closed.complete();
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
onDispose: () async {
|
||||
await passwords.dispose();
|
||||
await simpleLogin.dispose();
|
||||
await instanceOperators.dispose();
|
||||
await assistants.dispose();
|
||||
await assistantOperators.dispose();
|
||||
await authGateway.dispose();
|
||||
client.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Releases what [create] opened.
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await _onDispose?.call();
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens [person]'s own local database in [folder] (see [PersonFolders]), its
|
||||
/// sync, their key, settings and sittings; [onClosed] runs once all is closed again.
|
||||
Future<MeditatePerson> _openPersonData(
|
||||
Client client,
|
||||
AppCompatibility compatibility,
|
||||
SignedIn person,
|
||||
Directory folder, {
|
||||
required Future<UnlockedAccountKeys?> Function() accountKeys,
|
||||
required BellPlayer bells,
|
||||
required ScreenAwake screen,
|
||||
required void Function() onClosed,
|
||||
}) async {
|
||||
final db = await client.createSyncSession(
|
||||
p.join(folder.path, 'sync.sqlite'),
|
||||
persistentUserId: UuidValue.withValidation(person.authUserId),
|
||||
);
|
||||
final kept = FilePendingChangesStore(
|
||||
File(p.join(folder.path, 'sync_pending.json')),
|
||||
);
|
||||
final sync = SyncService(
|
||||
compatibleSync(
|
||||
({required onMerge}) => client.offlineSync.syncOnce(
|
||||
db,
|
||||
// Only when changes from other devices came in: a sync that only
|
||||
// sent this device's changes has nothing new to read.
|
||||
onInboundMerge: (space) => onMerge(),
|
||||
),
|
||||
compatibility,
|
||||
),
|
||||
pendingChanges: kept,
|
||||
syncedBefore: (await kept.read()).synced,
|
||||
)..start();
|
||||
// The person's key, with which everything of theirs is sealed (D5); read
|
||||
// again whenever the rows are, as a sync may have brought it.
|
||||
final keys = MeditateKeys(
|
||||
SyncContentKeyGateway(db, sync),
|
||||
SodiumContentCipher(),
|
||||
accountKeys,
|
||||
);
|
||||
await keys.refresh();
|
||||
final settings = SettingsRepository(SyncSettingsGateway(db, sync), keys);
|
||||
final sittings = SittingRepository(SyncSittingGateway(db, sync), keys);
|
||||
return MeditatePerson(
|
||||
person,
|
||||
sync: sync,
|
||||
settings: settings,
|
||||
sittings: sittings,
|
||||
bells: bells,
|
||||
screen: screen,
|
||||
onDispose: () async {
|
||||
try {
|
||||
await settings.dispose();
|
||||
await sittings.dispose();
|
||||
await sync.dispose();
|
||||
await db.close();
|
||||
} finally {
|
||||
// The key opened here goes once nothing can write any more.
|
||||
keys.dispose();
|
||||
onClosed();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// What the dependencies on every instance share: the device's own parts,
|
||||
/// made once when the app starts, outliving every switch of instance.
|
||||
class MeditateDevice({
|
||||
/// The app's support folder, where the device keeps its files.
|
||||
required final String support,
|
||||
|
||||
/// Where the device keeps what it holds of the account on each instance.
|
||||
required final InstanceStorage storage,
|
||||
|
||||
/// This build's number (after the `+` in the pubspec's version), which the
|
||||
/// server knows apps by (`AppCompatibility`); null where it has none.
|
||||
required final int? appBuild,
|
||||
|
||||
/// This build's version (before the `+`).
|
||||
final String appVersion = '',
|
||||
|
||||
/// The bells, one player for the whole app (ADR 0038, D5).
|
||||
final BellPlayer bells = const NoBells(),
|
||||
|
||||
/// The screen, kept on while sitting (D6).
|
||||
final ScreenAwake screen = const NoScreenAwake(),
|
||||
}) {
|
||||
/// Makes them, [builtIn] being the instance the build names.
|
||||
static Future<MeditateDevice> create({required Instance builtIn}) async {
|
||||
final package = await PackageInfo.fromPlatform();
|
||||
final support = (await getApplicationSupportDirectory()).path;
|
||||
return MeditateDevice(
|
||||
support: support,
|
||||
storage: InstanceStorage(support, builtIn: builtIn),
|
||||
appBuild: int.tryParse(package.buildNumber),
|
||||
appVersion: package.version,
|
||||
bells: AudioplayersBellPlayer(),
|
||||
screen: const WakelockScreenAwake(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_meditate_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/app/meditate_languages.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The app's icon: Lucide's `sprout` on white, fading from cobalt through
|
||||
/// pacific into fern, the app's backdrop (ADR 0031, D5).
|
||||
const meditateIcon = AssetImage('assets/icon/halkyon_meditate_icon.png');
|
||||
|
||||
/// Halkyon Meditate as halkyon_app's own screens know it (the first start, the
|
||||
/// start screen, the account switcher, signing in): its name and word, its
|
||||
/// icon, its look and languages, and the sentence only it can say.
|
||||
const meditateFrame = AppFrame(
|
||||
name: _name,
|
||||
product: _product,
|
||||
icon: meditateIcon,
|
||||
exampleAddress: 'meditate.example.org',
|
||||
firstStartSubtitle: _firstStartSubtitle,
|
||||
theme: appTheme,
|
||||
localizationsDelegates: meditateLocalizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localeListResolutionCallback: resolveAppLocale,
|
||||
launchScreen: MeditateLaunchScreen(),
|
||||
);
|
||||
|
||||
String _name(BuildContext context) => AppLocalizations.of(context).appTitle;
|
||||
|
||||
String _product(BuildContext context) =>
|
||||
AppLocalizations.of(context).launchProduct;
|
||||
|
||||
String _firstStartSubtitle(BuildContext context) =>
|
||||
AppLocalizations.of(context).instanceFirstStartSubtitle;
|
||||
|
||||
/// What shows while the app opens: halkyon_ui's [LaunchScreen] with Meditate's
|
||||
/// icon, "Meditate" beneath "Halkyon", read out as the app's name.
|
||||
class const MeditateLaunchScreen({super.key}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return LaunchScreen(
|
||||
icon: meditateIcon,
|
||||
product: l10n.launchProduct,
|
||||
name: l10n.appTitle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_meditate_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The app's own texts, halkyon_app's and halkyon_ui's, and Material's,
|
||||
/// Cupertino's and the widgets library's, in every language the app speaks:
|
||||
/// material_ui's, not flutter_localizations', which serve only the SDK's
|
||||
/// Material.
|
||||
const meditateLocalizationsDelegates = <LocalizationsDelegate<Object?>>[
|
||||
AppLocalizations.delegate,
|
||||
HalkyonAppLocalizations.delegate,
|
||||
HalkyonUiLocalizations.delegate,
|
||||
...GlobalMaterialLocalizations.delegates,
|
||||
];
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/// The app's locations (ADR 0038, D2): the three tabs, a practice's page in
|
||||
/// Explore, and the sitting and Settings over the tabs.
|
||||
abstract final class MeditatePaths() {
|
||||
/// Today, where the app opens.
|
||||
static const today = '/';
|
||||
|
||||
/// Explore: every practice, by goal and kind.
|
||||
static const explore = '/explore';
|
||||
|
||||
/// A practice's page, in Explore.
|
||||
static String practice(String id) => '/explore/practices/$id';
|
||||
|
||||
/// Profile: every Halkyon app's, then the person's practice.
|
||||
static const profile = '/profile';
|
||||
|
||||
/// A sitting under way, over the tabs.
|
||||
static const sitting = '/sitting';
|
||||
|
||||
/// Settings, over the tabs.
|
||||
static const settings = '/settings';
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/app/meditate_paths.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/practice/widget/practice_screen.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/sitting/bloc/sitting_cubit.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/feature/sitting/widget/sitting_screen.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/settings/settings_screen.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/tabs/explore_tab.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/tabs/meditate_shell.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/tabs/profile_tab.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/tabs/today_tab.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
|
||||
/// The router of one person's app (ADR 0038, D1, D2), as Learn's and
|
||||
/// Train's: the three tabs in a shell, each with its own stack of screens,
|
||||
/// and the sitting and Settings over them. Every route is built inside the
|
||||
/// person's scope, so screens read their repositories from `PersonScope`.
|
||||
/// A sitting is handed its plan as it is (`extra`); without one (a link
|
||||
/// from elsewhere) it goes back to Today.
|
||||
GoRouter buildMeditateRouter() => GoRouter(
|
||||
routes: [
|
||||
StatefulShellRoute(
|
||||
builder: (context, state, shell) => MeditateShell(shell: shell),
|
||||
navigatorContainerBuilder: (context, shell, tabs) =>
|
||||
TabsStack(index: shell.currentIndex, children: tabs),
|
||||
branches: [
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: MeditatePaths.today,
|
||||
builder: (context, state) => const TodayTab(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: MeditatePaths.explore,
|
||||
builder: (context, state) => const ExploreTab(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'practices/:id',
|
||||
builder: (context, state) =>
|
||||
PracticeScreen(practiceId: state.pathParameters['id']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: MeditatePaths.profile,
|
||||
builder: (context, state) => const ProfileTab(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: MeditatePaths.sitting,
|
||||
redirect: (context, state) =>
|
||||
state.extra is SittingPlan ? null : MeditatePaths.today,
|
||||
builder: (context, state) =>
|
||||
SittingScreen(plan: state.extra! as SittingPlan),
|
||||
),
|
||||
// In the frame without the rail from medium windows on, as Learn's.
|
||||
GoRoute(
|
||||
path: MeditatePaths.settings,
|
||||
builder: (context, state) => const FramedScreen(child: SettingsScreen()),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
/// Keeps the screen on while a sitting runs, where the person wants it
|
||||
/// (ADR 0038, D6).
|
||||
abstract interface class ScreenAwake() {
|
||||
/// Keeps the screen on, or lets it go dark again.
|
||||
Future<void> keepOn({required bool on});
|
||||
}
|
||||
|
||||
/// [ScreenAwake] that keeps nothing on: where the device can't.
|
||||
class const NoScreenAwake() implements ScreenAwake {
|
||||
@override
|
||||
Future<void> keepOn({required bool on}) async {}
|
||||
}
|
||||
|
||||
/// [ScreenAwake] on `wakelock_plus`.
|
||||
class const WakelockScreenAwake() implements ScreenAwake {
|
||||
@override
|
||||
Future<void> keepOn({required bool on}) async {
|
||||
try {
|
||||
await WakelockPlus.toggle(enable: on);
|
||||
} on Object catch (error) {
|
||||
// The sitting runs on with the screen as the system keeps it.
|
||||
log('The screen not kept on: $error', name: 'Sitting');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
/// A `content_key` row (ADR 0038, D7; Train's ADR 0030, D15): the key's ID
|
||||
/// as the row says it, which is never trusted, and the key sealed to the account.
|
||||
typedef ContentKeyRow = ({String dataKeyId, Uint8List sealedKey});
|
||||
|
||||
/// Where the person's data keys are kept: their synced `content_key` rows.
|
||||
abstract interface class ContentKeyGateway() {
|
||||
/// Every key row of the person's.
|
||||
Future<List<ContentKeyRow>> all();
|
||||
|
||||
/// Keeps a new key.
|
||||
Future<void> insert({
|
||||
required String dataKeyId,
|
||||
required Uint8List sealedKey,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_meditate_flutter/src/core/privacy/content_key_gateway.dart';
|
||||
|
||||
/// Refused: this device holds none of the account's keys (signed in without
|
||||
/// them), so it opens none of the person's data and writes none (ADR 0038,
|
||||
/// D7; as Train's ADR 0030, D15, and Learn's ADR 0019 have it).
|
||||
final class KeysMissing() implements Exception {
|
||||
@override
|
||||
String toString() => "KeysMissing: the account's keys aren't on this device";
|
||||
}
|
||||
|
||||
/// The person's key on this device (ADR 0038, D7; Train's `TrainKeys`, copied
|
||||
/// with Meditate's own IDs, ADR 0030, D15): one key, with which
|
||||
/// everything of Meditate's is sealed (ADR 0038, D7), kept in their synced `content_key`
|
||||
/// rows sealed to the account, so their other devices open it.
|
||||
///
|
||||
/// A key's ID is sealed with it and signed by the account (halkyon_keys'
|
||||
/// `sealDataKey` opens a key only under the ID it was sealed with), so the
|
||||
/// instance can't relabel one, and only IDs of Meditate's own form are opened:
|
||||
/// a key the account sealed for another app (Learn's personal key) never
|
||||
/// counts here. Two devices that each made a key while offline both write
|
||||
/// with the older once they see both; every key that opens still reads.
|
||||
class MeditateKeys(
|
||||
final ContentKeyGateway _gateway,
|
||||
|
||||
/// Seals and opens with the keys; [MeditateKeys] owns it.
|
||||
final ContentCipher cipher,
|
||||
|
||||
/// The account's keys on this device; null where none are.
|
||||
final Future<UnlockedAccountKeys?> Function() _accountKeys,
|
||||
) {
|
||||
var _unlocked = false;
|
||||
String? _writeKey;
|
||||
|
||||
/// Reads the key rows again and opens the person's keys among them: as
|
||||
/// the person's data opens, and after a sync brought others' rows.
|
||||
Future<void> refresh() async {
|
||||
final account = await _accountKeys();
|
||||
if (account == null) {
|
||||
return;
|
||||
}
|
||||
if (!_unlocked) {
|
||||
await cipher.unlock(account);
|
||||
_unlocked = true;
|
||||
}
|
||||
for (final row in await _gateway.all()) {
|
||||
final id = row.dataKeyId;
|
||||
if (_madeAt(id) != null &&
|
||||
(cipher.has(id) || cipher.openKey(id, row.sealedKey)) &&
|
||||
_older(id, _writeKey)) {
|
||||
_writeKey = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the account's keys are open here: [refresh] found them.
|
||||
bool get unlocked => _unlocked;
|
||||
|
||||
/// The ID of the row named [name], keyed by the account (D57): the same
|
||||
/// on each of its devices, nothing of [name] read from it by the
|
||||
/// instance. Throws [KeysMissing] where [refresh] found no keys.
|
||||
String rowId(String name) =>
|
||||
_unlocked ? cipher.rowId(name) : throw KeysMissing();
|
||||
|
||||
/// Whether what is sealed with the key [keyId] opens here.
|
||||
bool canOpen(String keyId) =>
|
||||
_unlocked && _madeAt(keyId) != null && cipher.has(keyId);
|
||||
|
||||
/// The key to seal with, made and kept the first time; throws
|
||||
/// [KeysMissing] without the account's keys. The key rows are read first
|
||||
/// where none was chosen yet, so that no second key is made beside one
|
||||
/// already there.
|
||||
Future<String> keyToWrite() async {
|
||||
if (_writeKey == null) {
|
||||
await refresh();
|
||||
}
|
||||
if (_writeKey case final key?) {
|
||||
return key;
|
||||
}
|
||||
if (!_unlocked) {
|
||||
throw KeysMissing();
|
||||
}
|
||||
final random = Random.secure();
|
||||
final nonce = base64Url
|
||||
.encode([for (var i = 0; i < 12; i++) random.nextInt(256)])
|
||||
.replaceAll('=', '');
|
||||
final id = 'meditate1.${clock.now().toUtc().millisecondsSinceEpoch}.$nonce';
|
||||
await _gateway.insert(dataKeyId: id, sealedKey: cipher.newKey(id));
|
||||
return _writeKey = id;
|
||||
}
|
||||
|
||||
/// Wipes the keys kept open here.
|
||||
void dispose() => cipher.dispose();
|
||||
|
||||
/// When the key [id] was made, as its ID says (`meditate1.<ms>.<nonce>`);
|
||||
/// null for an ID of another form, which is never Meditate's.
|
||||
static int? _madeAt(String id) => switch (id.split('.')) {
|
||||
['meditate1', final ms, final nonce] when nonce.isNotEmpty => int.tryParse(
|
||||
ms,
|
||||
),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// Whether [id] was made before [other] (none at all: yes), ties going to
|
||||
/// the smaller ID, so every device picks the same.
|
||||
static bool _older(String id, String? other) {
|
||||
if (other == null) {
|
||||
return true;
|
||||
}
|
||||
final (a, b) = (_madeAt(id)!, _madeAt(other)!);
|
||||
return a < b || (a == b && id.compareTo(other) < 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_meditate_client/halkyon_meditate_client.dart'
|
||||
as protocol;
|
||||
import 'package:halkyon_meditate_flutter/src/core/privacy/content_key_gateway.dart';
|
||||
import 'package:serverpod_offline_sync_client/offline_sync.dart';
|
||||
|
||||
/// [ContentKeyGateway] on the person's local database, which the sync engine
|
||||
/// keeps in step with their other devices.
|
||||
class SyncContentKeyGateway(
|
||||
final OfflineSyncDatabaseSession _db,
|
||||
final SyncService _sync,
|
||||
) implements ContentKeyGateway {
|
||||
@override
|
||||
Future<List<ContentKeyRow>> all() async => [
|
||||
for (final key in await protocol.ContentKey.db.find(
|
||||
_db,
|
||||
orderBy: (t) => t.id,
|
||||
))
|
||||
(
|
||||
dataKeyId: key.dataKeyId,
|
||||
sealedKey: Uint8List.sublistView(key.sealedKey),
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<void> insert({
|
||||
required String dataKeyId,
|
||||
required Uint8List sealedKey,
|
||||
}) async {
|
||||
await protocol.ContentKey.db.insertRow(
|
||||
_db,
|
||||
protocol.ContentKey(
|
||||
dataKeyId: dataKeyId,
|
||||
sealedKey: ByteData.sublistView(sealedKey),
|
||||
),
|
||||
);
|
||||
_sync.requestSync();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue