feat(app): what's new after an update, drafted by the release tool (ADR 0030, D61)

halkyon_app's WhatsNew reads what each release changed from the app's
assets/whats_new/ (<version>.<language>.txt, English and German, a change
a line): after an update a sheet lists every release since the one the
device showed last, newest first, once; Settings shows the running
version's again, above About. Nothing on a fresh install.

tool/release.dart drafts the missing texts from the commits that count
after "Go on?" (German marked TRANSLATE:), opens them in the editor until
they fit (no mark left, F-Droid's 500 characters) and commits them with
the version. The English is the Forgejo notes' What's new, each language
F-Droid's changelog of the build, written at publishing and copied in by
tool/fdroid_repo.sh. Learn and Train bundle their texts, Learn's 0.22.0
and Train's 0.1.0 written to start.

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-31 19:36:20 +01:00
commit 7727317c68
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 1272 additions and 76 deletions

View file

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_learn_flutter/src/app/app_icon_license.dart';
@ -42,15 +43,24 @@ Future<void> main() async {
);
await instances.load(usedBefore: device.storage.usedBefore());
runApp(
InstanceGate(
frame: learnFrame,
choice: instances,
// Only an instance of Halkyon Learn: one of another Halkyon app's
// answers too, but holds nothing of this app's.
checker: ServerpodInstanceChecker(app: 'learn'),
open: (instance) => Dependencies.create(instance, device),
readLook: () async => (await device.deviceSettingsStore.read()).look,
app: const LearnSessions(),
WhatsNewScope(
whatsNew: WhatsNew(
AssetWhatsNewSource(rootBundle),
FileWhatsNewStore(File(p.join(device.support, 'whats_new.json'))),
version: device.appVersion.version,
),
// An instance chosen before: the app ran on this device already.
usedBefore: instances.current != null,
child: InstanceGate(
frame: learnFrame,
choice: instances,
// Only an instance of Halkyon Learn: one of another Halkyon app's
// answers too, but holds nothing of this app's.
checker: ServerpodInstanceChecker(app: 'learn'),
open: (instance) => Dependencies.create(instance, device),
readLook: () async => (await device.deviceSettingsStore.read()).look,
app: const LearnSessions(),
),
),
);
}

View file

@ -1,4 +1,5 @@
import 'package:go_router/go_router.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
@ -40,26 +41,29 @@ class const AppShell({
child: Icon(data),
)
: Icon(data);
return NavigationFrame(
appName: l10n.appTitle,
// The kingfisher has a white background, which the frame's rounded
// tile keeps reading on light and dark rails alike.
icon: const AssetImage('assets/icon/halkyon_icon.png'),
selectedIndex: navigationShell.currentIndex,
onSelected: (index) => navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
// After an update, what's new shows once.
return WhatsNewOnUpdate(
child: NavigationFrame(
appName: l10n.appTitle,
// The kingfisher has a white background, which the frame's rounded
// tile keeps reading on light and dark rails alike.
icon: const AssetImage('assets/icon/halkyon_icon.png'),
selectedIndex: navigationShell.currentIndex,
onSelected: (index) => navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
),
hidesNavigationBar: hidesNavigationBar,
destinations: [
for (final destination in destinations)
(
label: destination.label,
icon: icon(destination, destination.icon),
selectedIcon: icon(destination, destination.selectedIcon),
),
],
child: navigationShell,
),
hidesNavigationBar: hidesNavigationBar,
destinations: [
for (final destination in destinations)
(
label: destination.label,
icon: icon(destination, destination.icon),
selectedIcon: icon(destination, destination.selectedIcon),
),
],
child: navigationShell,
);
}
}

View file

@ -106,10 +106,17 @@ class const SettingsScreen({super.key}) extends StatelessWidget {
const SizedBox(height: 12),
SectionCard(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ListTile(
leading: const Icon(LucideIcons.info),
title: Text(l10n.settingsAbout),
onTap: () => showAbout(context),
child: Column(
children: [
// What changed in this version, as shown once after
// the update (halkyon_app's).
const WhatsNewTile(),
ListTile(
leading: const Icon(LucideIcons.info),
title: Text(l10n.settingsAbout),
onTap: () => showAbout(context),
),
],
),
),
const SizedBox(height: 24),