halkyon/packages/halkyon_app
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tizian 「ティツィアーン」 c36bd83c6a
feat(learn,todo,meditate): dictate through the phone's voice input app (ADR 0038, D18)
On Android the microphone now opens the app handling
RECOGNIZE_SPEECH (FUTO Voice Input, Google's), the default the person
chose in Android, and puts the text it returns where the cursor stood,
as Dictation does; cancelled, nothing. Only where none is installed, or
it can't be opened, does speech_to_text listen in line as before.

halkyon_app: VoiceInputApp (one listen, an answer once closed) on the
channel app.halkyon/voice_input, which DeviceSpeechInput asks first at
every listen, answering SpeechStart.finished so that Dictation doesn't
listen again; speech_to_text isn't touched on that route, so our app
asks for no microphone. SpeechInput.listen and Dictation.start take the
prompt the voice input app shows (the field's hint).

Each app: VoiceInput.kt (queryIntentActivities, startActivityForResult,
the first of EXTRA_RESULTS) attached in MainActivity, which hands it
activity results, and RECOGNIZE_SPEECH among the manifest's queries.
The privacy pages of Learn and Meditate say so.

Our own Whisper and Meditate's dream recordings are dropped (D18).

Whats-New: The microphone now dictates through your phone's voice input app, such as FUTO Voice Input.
Whats-New-De: Das Mikrofon diktiert jetzt über die Spracheingabe-App deines Telefons, etwa FUTO Voice Input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 11:48:42 +01:00
..
assets refactor(halkyon_app): the account section, About, the language and countries' names, shared 2026-10-22 18:11:08 +02:00
lib feat(learn,todo,meditate): dictate through the phone's voice input app (ADR 0038, D18) 2026-11-08 11:48:42 +01:00
test feat(learn,todo,meditate): dictate through the phone's voice input app (ADR 0038, D18) 2026-11-08 11:48:42 +01:00
tool refactor(halkyon_app): the account section, About, the language and countries' names, shared 2026-10-22 18:11:08 +02:00
.gitignore halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps 2026-10-19 15:31:40 +02:00
analysis_options.yaml halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps 2026-10-19 15:31:40 +02:00
CHANGELOG.md halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps 2026-10-19 15:31:40 +02:00
l10n.yaml halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps 2026-10-19 15:31:40 +02:00
LICENSE build: Melos, a Dart check and every path for the single repository 2026-10-22 18:11:04 +02:00
pubspec.yaml refactor(app): the device's speech recognition moves from Learn into halkyon_app, with dictation into a text 2026-11-04 09:31:58 +01:00
README.md feat(app): one Profile for every app, the person's own tab first, then each app's (ADR 0014) 2026-11-02 11:11:11 +01:00

halkyon_app

What every Halkyon app (Learn, Todo, Train) is built around, on an instance's halkyon_base module: the instances a device keeps an account on, signing in, each person's data opened and closed, and syncing it. Taken from Halkyon Learn, which it was written for, so it behaves as Learn's own did; the texts are in British English, American English and German, and where they name the app, they name yours.

Written as the apps are: Dart 3.13's primary constructors, Material from package:material_ui, Lucide icons, blocs (flutter_bloc), LeanCode's lints.

What it holds

  • Instances (lib/src/instance/): Instance (an address as people type it; isSecure: over HTTPS, or over plain HTTP on this machine alone), InstanceChoice (those the device keeps an account on, one in use; FileInstanceStore; allows an instance reached securely, or in a development build the build's own over HTTP, and leaves one it no longer allows, which InstanceGate never opens: it says why and offers another or removing it), InstanceStorage (each instance's people's folders and session key), InstanceChecker (does an address answer, where does it keep people's data; ServerpodInstanceChecker asks its instance.basics()), InstanceScope. The screens: FirstStartScreen (the build's instance and another, as tiles, InstanceTile), InstanceStep (an address, checked), showAccountSwitcher and AddAccountScreen, and where an instance keeps people's data, as a flag and words (InstanceLocationBuilder, InstanceFlag, instanceLocationWords): EU, a country or a subdivision (ISO 3166-2, US-CA) as its operator says, "Germany (EU)" for a member of the European Union (europeanUnionMembers) and "California, United States" for a subdivision, with the most specific flag there is (a few subdivisions have their own, such as Scotland; else the country's). Countries and subdivisions are named by Unicode CLDR's names in English and German, which come with the package (AssetCountryNames, AssetSubdivisionNames; the package's tool/country_names.py and tool/subdivision_names.py write them, from one release); tests give names of their own (CountryNames, SubdivisionNames).
  • Gates (lib/src/app/): InstanceGate opens the app's dependencies on the instance in use (AppDependencies, handed down by AppScope), anew on a switch, the previous ones closed first; SessionGate shows the app of whoever is signed in and asks how to start while nobody is (StartScreen: SimpleLogin, a password, another instance; an instance out of reach is said so, with Try again, and one offering no way to create an account too; where new accounts wait for the operator's approval, or none are made, it says so first), at a first start as after signing out; while an account signed in or up to here waits for the operator (Halkyon Learn's ADR 0021, D31), WaitScreen shows its handle and the four words the operator reads out for the person to match, asks every 30 seconds and as the app comes back, deletes a new account after asking, and once approved leads to signing in once more (the wait kept in the keyring per instance, SecureAccountWaitStore, so a restart shows it again); an anonymous account's session, left from before there were none, is deleted as the app starts (ADR 0020, D25), falling back to signing out on the device; PersonScope opens each person's own dependencies and closes them when the person goes. resolveAppLocale picks the app's language as every Halkyon app does: the first preferred language it speaks, never by country alone, British English otherwise.
  • The instance's operators (lib/src/operator/, Halkyon Learn's ADR 0021, D21 to D24 and D34): InstanceOperatorRepository on ServerpodInstanceOperatorGateway (the module's instanceOperator endpoint, refusals as OperatorFailed), OperatingCubit (how many accounts wait, null for anyone who doesn't operate the instance; asked as the person's app opens, after each change, and every 5 minutes only while they operate it), InstanceOperatorCard or InstanceOperatorTiles for an app's settings and WaitingAccountsBadge for its Settings cog, both nothing at all for anyone else, and InstanceOperatorScreen (D62): an overview of how many wait, operate and have accounts, and of the assistants set up, each row opening a screen of its own, every list read 50 at a time as it is scrolled (OperatorListCubit, OperatorPage): the accounts that wait, approved once their person confirmed the words read out, or refused; who operates, named by a handle picked among those suggested and taken back (never the last); accounts found by the start of their handle, suspended, named or granted assistants. A handle is only ever picked among the accounts there are (AccountSuggestionField, D61). Only the register's service answers: on a member, nothing shows.
  • Assistants (lib/src/assistant/, Halkyon Learn's ADR 0021, step 2): AssistantRepository on ServerpodAssistantGateway (the module's assistant endpoint; refusals as AssistantFailed) asks an AssistantChoice in halkyon_llm's terms: a GrantedAssistant through the instance, which counts it, at an effort among those the operator allows and with that effort's ceiling on output (efforts, outputCeilingAt, D63), or a PersonalAssistant with the person's own key directly (LlmClient), without an effort and at 4,096 output tokens, the instance seeing neither key nor question; assistant_messages.dart turns a conversation into the endpoint's models and back, part by part. AssistantsCard is an app's settings' card: what is granted, under each provider's name in its colour (ProviderLabel, providerColorIn, D60), what is left of the person's budget for them together (D58), each agreed to once (the consent sheet), the person's use by UTC month, oldest first (D57), the instance's privacy page, and the person's own assistants where the operator allows them, kept where the app says (OwnAssistantStore: Learn's sealed settings). For the operator, AssistantOperatorRepository and AssistantGrantsScreen, opened from InstanceOperatorScreen: the assistants set up, by provider, with their efforts and whether they are priced; the people granted some, each with one budget in money and tokens, a day and a month (D58, D59), found by their handle's start; one sheet granting several assistants, a provider's at once, to several people at once, or changing one person's (showAssistantGrantSheet); and everyone's use by month. package:halkyon_app/assistant_models.dart holds the models alone, in pure Dart, for an app's own models. For the app's warnings before a private studyset's content goes (ADR 0021, D9): providerTermsOf names a provider's terms by its host (checked for Anthropic, OpenAI, Google and OpenRouter, null for any other), and OwnAssistant.onThisMachine says whether a person's own assistant runs on this machine.
  • About (showAppAbout): the app's name (its frame's), its version, icon and legalese where it gives them, where the source of what the instance runs is (InstanceSourceCode, from AppDependencies.sourceCodeUrl; both licences require offering it), the app's own parts (its credits), and the licence list.
  • The account (AccountTiles): on which instance the account is signed in, switching to the account on another, signing out and deleting it (after asking), for an app's settings, in the app's words where only it can say them. While one runs, the person's app stays and its tile shows a spinner; one that fails keeps them signed in and says why once, in a snack bar (in a sheet, give it hasSnackBars). An instance out of reach signs out on the device all the same (Serverpod's signOutDevice), so only the device's own storage fails a sign-out.
  • The Profile (lib/src/profile/, decided 2026-11-02): every app's Profile screen (HalkyonProfile), the person's own tab first (the account and the ways they sign in, the app's own cards, their public profile), then the app's own tabs, the row folding into the title; and the public profile itself, one per person for every app (halkyon_base's register, ADR 0016): PublicProfileRepository on BasePublicProfileGateway, the form (PublicProfileSection, saving as it is written), the avatar drawn anew as a PNG on the device (avatarPng), and someone's profile in a sheet (showProfileOf). AppDependencies.profiles holds it, and AppDependencies.files the picker (FilePicker, FileSelectorFilePicker) its picture is chosen with.
  • The session (lib/src/auth/): AuthRepository on an AuthGateway; ServerpodAuthGateway follows the session the app's Serverpod client keeps, signs out, deletes the account through the instance's halkyon_base, and puts a session granted elsewhere in place (useSession: a password's or SimpleLogin's sign-in; GitHub signs nobody in since ADR 0020, D36); SessionBloc and SignInWaysCubit; SignInAddresses, PersonFolders.
  • Content sealed with data keys (lib/src/privacy/, ADR 0019): ContentCipher (SodiumContentCipher, on the isolate it is used on, as an app seals and opens rows one by one; FakeContentCipher in tests) makes a store's data keys, sealed to the account and signed by it, opens them, and seals and opens a row's content for the row it is stored in (its table, id and column), the payload holding the signature before the ciphertext.
  • A password and TOTP (lib/src/password/, ADR 0017): PasswordRepository on a PasswordGateway (ServerpodPasswordGateway), an AccountCrypto (SodiumAccountCrypto: halkyon_keys and SRP's client half, each step on an isolate of its own, whose results are copied before they cross back: sodium's are views of memory freed with the isolate that made them) and an AccountKeyStore (SecureAccountKeyStore: the account's unlocked keys in the system's keyring, per instance and person, gone once another person signs in or nobody; on iOS and macOS unlocked_this_device, so readable only while the device is unlocked, never in a backup restored elsewhere nor in iCloud Keychain, each write deleting the entry before it, and forgetting finding too the entries earlier builds kept with another accessibility, a keyring that can't be read now stopping nothing). Signing in (SRP's M2 checked and the keys opened before anything is trusted), signing up (the recovery phrase shown once and three of its words typed back before anything reaches the instance), a setup never finished, the authenticators (QR codes drawn with pub.dev's qr), recovery codes, changing the password, a new phrase, and getting back in. The screens: the start screen's Sign in and Create an account, showPasswordSignInSheet, showAccountSetupSheet, showAccountRecoverySheet, and AccountSecuritySection for an app's account part. Every change of how the account is secured asks for the password with the code, one answer per sheet (OwnerAnswer, the owner's proof: a session alone gets no code checked), each answer bound to the instance's base URL and to the change it approves (its purpose and the digest of what it sets, proof_digests.dart), so nobody between the device and the instance swaps anything in; deleting by a code by mail follows secondFactorWorks, as the instance tells it, and asks for the address only where the device doesn't know it. Secrets shown stay the person's: nothing secret is copied (no app can mark a copy as sensitive through Flutter, so clipboard histories and cloud clipboards would keep it): the recovery phrase and codes are saved or written down, an authenticator's key scanned, opened or typed; all of them show in a SecretScreen (the phrase typed back too, its fields offering Paste and Select all alone, their copy keys doing nothing), kept out of screenshots, screen recordings and recents where the app's window answers app.halkyon/screen_guard: Android's FLAG_SECURE (as Learn's and Todo's ScreenGuard.kt in their MainActivity), macOS's sharingType = .none (MainFlutterWindow.swift; screenshots, but not ScreenCaptureKit's captures on macOS 15 and later) and Windows' WDA_EXCLUDEFROMCAPTURE from Windows 10 2004 on, WDA_MONITOR (black in captures) before, by the version RtlGetVersion tells (flutter_window.cpp); neither run yet; covered on iOS in the app switcher's picture while the app isn't in front, screenshots there not held back; nothing on Linux; keyboards learn nothing typed into a password, a code or the phrase; no state's text (which a debug build's bloc observer logs) holds a secret, a code or the address; only an otpauth: URI is shown as a QR code or opened; and anything a step didn't expect (bytes of the wrong length from an instance, libsodium's or the keyring's own errors) fails it rather than leaving its sheet busy. register_interop_test.dart holds the real cryptography to the register's own checks. Every build of an app needs a C compiler and make: sodium's build hook compiles libsodium.
  • Signing in with SimpleLogin (lib/src/simplelogin/, ADR 0020): SimpleLoginRepository on a SimpleLoginGateway (ServerpodSimpleLoginGateway, halkyon_base's simpleLogin endpoint) and halkyon_oidc_flutter's OidcBrowser: SimpleLogin's page in the system's browser sheet, back by the app's scheme, or on Linux and Windows in the browser, back to the app's port on the loopback, as the instance's forwarding page names them (callbackScheme, loopbackPort: Learn's 43823, Todo's 43824, Train's 43825). Someone new creates an account (a handle, the one the instance suggests being free; the password twice, which makes the account's keys on the device; the recovery phrase shown once and three of its words typed back; nothing reaches the instance before) or links theirs again in place of a lost SimpleLogin account with its handle and recovery phrase, which open the keys on the device before any proof is sent; an account opens its keys with its password; an account of before gets its keys, and its handle where it has none. With link, SimpleLogin joins the account of the device's person instead. The screens: the start screen's Continue with SimpleLogin, first where the instance offers it, and showSimpleLoginSheet (SimpleLoginCubit), which waits for the browser with Cancel and stops waiting on the loopback once it closes. Adding it to an account (ADR 0020, D21): where the person stands (SimpleLoginRepository.standing, read anew after every change, SimpleLoginStandingCubit: nothing to show, linked, or missing), shown by SimpleLoginSection (each app places it above AccountSecuritySection: a heading and Add SimpleLogin; once added a row among the ways in), which reads it once where a SimpleLoginStandingScope is above it.
  • Files (FileSaver, PlatformFileSaver): keeping a file the app made where the person chooses (a recovery phrase, recovery codes, an app's exports): on Linux and macOS the own user's alone (writePrivately). On Linux a new file beside it, made 0600 with O_EXCL, written through its descriptor, renamed over the one chosen, never written into, and the folder flushed. On macOS, whose sandbox lets an app reach the file chosen and nothing beside it, the file itself, opened without following a link or waiting on a pipe, refused unless it is a file of the own user's, made 0600 and emptied before anything is written; whoever held it open reads what is saved. Each app's macOS entitlements grant reading and writing the files the person chooses (com.apple.security.files.user-selected.read-write), without which nothing could be saved there, Learn's exports neither. On both, a symbolic link or another user's file is refused with nothing written. On Windows written in place with its folder's rights, on iOS without the copy file_picker leaves in the temporary folder (forgetPickerCopy). None of it has run on a Mac yet.
  • Sync (lib/src/sync/): SyncService (once at start, shortly after local changes, on resume and every few minutes, one sync at a time, counting the changes not on the server yet, PendingChangesStore), AppCompatibility and compatibleSync (the instance's minimum app build, the engine's table check), SyncStatusBloc, syncAndWait, and the widgets: SyncStatusCard, AppMismatchBanner, SyncTroubleBadge.
  • A client of halkyon_base alone (BaseClient): to ask an instance about itself, and to sign in as another person than the app's client holds.
  • Testing (package:halkyon_app/testing.dart): fakes of every boundary (FakeAuthGateway, with waits, fakeWaitOf; FakePasswordGateway, FakeAccountCrypto, FakeInstanceChecker, FakeInstanceOperatorGateway, the stores in memory), a frame (testFrame), testApp, and TestAppDependencies.

Using it in an app

  1. The server includes halkyon_base with the nickname base (its README), so the app's generated client has client.modules.base.

  2. Depend on it by path, within the repository's workspace, as on halkyon_ui:

    dependencies:
      halkyon_app:
        path: ../../../packages/halkyon_app
    
  3. Texts: add HalkyonAppLocalizations.delegate to every MaterialApp's delegates, beside HalkyonUiLocalizations.delegate and material_ui's GlobalMaterialLocalizations.delegates.

  4. The frame (AppFrame): the app's name and wordmark word, its icon, an example address, its theme, delegates, languages (resolveAppLocale as its resolution) and launch screen, and the sentence only it can say: what the first start's instance keeps of the person's.

  5. Android: the app's MainActivity answers app.halkyon/screen_guard (copy Learn's or Todo's ScreenGuard.kt and call ScreenGuard.attach in configureFlutterEngine), or secrets show in screenshots and recents.

  6. The dependencies: the app's composition root implements AppDependencies<P> (serverName, auth, passwords, simpleLogin, instanceOperators, fileSaver, sourceCodeUrl, openPerson, dispose), its person's dependencies Disposable. Its passwords is a PasswordRepository on ServerpodPasswordGateway(client, base: client.modules.base), SodiumAccountCrypto(), SecureAccountKeyStore(storage.keysOf(instance)), the auth gateway and the app's SignInAddresses. Its auth is an AuthRepository on ServerpodAuthGateway(client, base: client.modules.base, waits: SecureAccountWaitStore(storage.waitOf(instance))), whose initialize() it awaits before anything asks who is signed in. Its instanceOperators is an InstanceOperatorRepository on ServerpodInstanceOperatorGateway(client.modules.base); the person's app provides an OperatingCubit on it, for the card and the badge.

  7. The tree:

    InstanceGate(
      frame: frame,
      choice: choice,
      checker: ServerpodInstanceChecker(app: 'todo'),
      readLook: …,           // the device's theme mode and language
      open: Dependencies.create,
      app: SessionGate(
        look: (context) => …,
        app: (person) => PersonApp(person: person), // with PersonScope inside
      ),
    )
    

    A person's widgets find their dependencies with PersonScope.of<PersonDependencies>(context) and the app's with AppScope.of<Dependencies>(context).

  8. Licences: call registerHalkyonAppLicenses() once at startup (the flags' and Unicode CLDR's notices), beside halkyon_ui's registerFontLicenses().

Licence

GNU Affero General Public License 3.0 or later (LICENSE). The flags are lipis' flag-icons, drawn by the flag package, under the MIT licence (assets/flags/LICENSE-flag-icons.txt). The subdivisions' names are Unicode CLDR's (release 48.2), under the Unicode License v3 (assets/subdivisions/LICENSE-CLDR.txt, assets/subdivisions/ATTRIBUTION.md).