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:
Tizian 「ティツィアーン」 2026-11-03 09:25:30 +01:00
commit cb026dce8d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
47 changed files with 13502 additions and 5 deletions

View 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();
}

View 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"
}

View 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"
}

View file

@ -0,0 +1,3 @@
{
"@@locale": "en_GB"
}

View file

@ -0,0 +1,3 @@
{
"@@locale": "en_US"
}

View file

@ -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.',
);
}

View file

@ -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';
}

View file

@ -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');
}

View 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),
),
),
);
}

View file

@ -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));
});

View file

@ -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!,
),
),
),
);
}

View file

@ -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(),
);
}
}

View file

@ -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,
);
}
}

View file

@ -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,
];

View file

@ -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';
}

View file

@ -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()),
),
],
);

View file

@ -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');
}
}
}

View file

@ -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,
});
}

View file

@ -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);
}
}

View file

@ -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