Halkyon's shared packages

A pub workspace of the Dart packages Halkyon's apps share: halkyon_ui,
Halkyon's look (the theme, fonts, layout and shared widgets, with texts
in English and German), halkyon_core's low-level primitives and
halkyon_interop's cross-app contracts. Dart 3.13 with LeanCode's lints,
configured once at the root; apps take a package by git, pinned to a
signed commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-18 21:46:55 +02:00
commit 80fa7b9628
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 6762 additions and 0 deletions

View file

@ -0,0 +1,13 @@
/// Cross-app interoperability contracts for the Halkyon ecosystem.
///
/// This package is pure Dart and defines:
/// - `TaskShape`: a minimal type for representing tasks across apps
/// (e.g., a Halkyon Learn review session surfaced in Halkyon ToDo).
/// - `HalkyonEvent`: a sealed class hierarchy for cross-app events.
/// - `EventPublisher`: an interface for emitting events to other apps,
/// with `DormantPublisher` as a no-op default for apps running standalone.
library;
export 'src/event_publisher.dart';
export 'src/events.dart';
export 'src/task_shape.dart';

View file

@ -0,0 +1,24 @@
import 'package:halkyon_interop/src/events.dart';
/// Publishes [HalkyonEvent]s to other Halkyon apps installed alongside this one.
///
/// The actual transport (platform channels, intents, App Group containers,
/// etc.) is implementation-specific and lives in the app's composition root,
/// not in this package.
abstract interface class EventPublisher() {
/// Publish an event. Implementations should be non-blocking and best-effort:
/// failing to deliver to peer apps must not fail the originating action.
Future<void> publish(HalkyonEvent event);
}
/// A no-op [EventPublisher] for apps running in isolation.
///
/// When a Halkyon app starts and detects no peer apps to broadcast to (or
/// when the user has disabled cross-app communication), inject this. All
/// `publish` calls are silently dropped.
class const DormantPublisher() implements EventPublisher {
@override
Future<void> publish(HalkyonEvent event) async {
// Intentionally empty. Events are dropped when there are no peers.
}
}

View file

@ -0,0 +1,43 @@
import 'package:meta/meta.dart';
/// Base type for events published across Halkyon apps.
///
/// Sealed: all subtypes must be declared in this file. Adding a new event
/// type is a deliberate cross-app contract change, not something that should
/// happen incidentally inside a feature.
@immutable
sealed class const HalkyonEvent({
/// When the event occurred. Use the app's injected `Clock` to set this,
/// not `DateTime.now()` directly, so tests can control time.
required final DateTime occurredAt,
});
/// Emitted when a Halkyon Learn review becomes due for the user.
///
/// Consuming apps (e.g., Halkyon ToDo) can subscribe to surface this as a
/// task or notification.
final class const ReviewDueEvent({
required super.occurredAt,
/// Identifier of the studyset whose cards are due.
required final String studysetId,
/// Number of cards in this studyset currently due for review.
required final int dueCount,
}) extends HalkyonEvent {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ReviewDueEvent &&
runtimeType == other.runtimeType &&
occurredAt == other.occurredAt &&
studysetId == other.studysetId &&
dueCount == other.dueCount;
@override
int get hashCode => Object.hash(occurredAt, studysetId, dueCount);
@override
String toString() =>
'ReviewDueEvent(studyset=$studysetId, due=$dueCount, at=$occurredAt)';
}

View file

@ -0,0 +1,53 @@
import 'package:meta/meta.dart';
/// A minimal cross-app representation of a task.
///
/// This is the contract for surfacing one app's actionable items in another
/// app's task views. Halkyon Learn might surface "review 12 cards in Spanish"
/// as a [TaskShape] that Halkyon ToDo displays alongside native todos.
///
/// Kept deliberately minimal. Apps consuming a [TaskShape] should treat it
/// as opaque metadata; deep-linking back to the originating app is via
/// [deepLink], not by interpreting [sourceAppId] or [externalId].
@immutable
class const TaskShape({
/// Identifier of the app that owns this task. Conventionally a reverse-DNS
/// string like `eu.halkyon.learn`.
required final String sourceAppId,
/// The originating app's stable identifier for this task. Opaque to
/// consumers; used together with [sourceAppId] for deduplication.
required final String externalId,
/// Human-readable title displayed in the consuming app's task list.
required final String title,
/// A URI the consuming app can open to hand off back to the originating
/// app. Format is app-specific (custom scheme or universal link).
required final Uri deepLink,
/// When the task becomes actionable or overdue. Null means "no due date."
final DateTime? dueAt,
/// Optional secondary line. Keep short — consumers may truncate.
final String? subtitle,
}) {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TaskShape &&
runtimeType == other.runtimeType &&
sourceAppId == other.sourceAppId &&
externalId == other.externalId &&
title == other.title &&
subtitle == other.subtitle &&
dueAt == other.dueAt &&
deepLink == other.deepLink;
@override
int get hashCode =>
Object.hash(sourceAppId, externalId, title, subtitle, dueAt, deepLink);
@override
String toString() => 'TaskShape($sourceAppId/$externalId: $title)';
}