Changelog and What's new: CHANGELOG.md cut into each release, shown in the update band, a What's new card and once after updating
CHANGELOG.md keeps an Unreleased section (user-facing changes add a line in the same commit); tool/release.sh cuts it into the version, the manifest gets a structured "changes" field next to the one-line "notes" older apps read, and the Gitea release text is that section. The app's update band shows the top items, What's new opens a card (Added / Changed / Fixed), and the first start after an update shows it once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ec0849eb01
commit
638a30e16c
22 files changed
+1637
-58
No files matched your search
@@ -0,0 +1,150 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sync_relay_core/core.dart';
|
||||
|
||||
import '../state/settings.dart';
|
||||
import 'update_controller.dart';
|
||||
|
||||
// "What's new in X.Y.Z" once after an update. The notes come from the
|
||||
// manifest: the one installed (kept before the restart) or, for an update
|
||||
// made another way (1.3.x's updater, `--upgrade`, by hand), the manifest of
|
||||
// the first check that finds this very version. A fresh install shows
|
||||
// nothing; Settings → About opens the card again.
|
||||
|
||||
/// What the card shows for one version.
|
||||
class ReleaseNotes {
|
||||
final String version;
|
||||
|
||||
/// The one-line notes (all a manifest from before the changelog has).
|
||||
final String notes;
|
||||
final ReleaseChanges? changes;
|
||||
const ReleaseNotes({required this.version, this.notes = '', this.changes});
|
||||
|
||||
factory ReleaseNotes.of(UpdateManifest m) => ReleaseNotes(version: m.version, notes: m.notes, changes: m.changes);
|
||||
|
||||
/// Null for anything unreadable.
|
||||
static ReleaseNotes? fromJson(Object? json) {
|
||||
if (json is! Map) return null;
|
||||
final version = json['version'];
|
||||
if (version is! String || version.isEmpty) return null;
|
||||
final notes = json['notes'];
|
||||
return ReleaseNotes(
|
||||
version: version,
|
||||
notes: notes is String ? notes : '',
|
||||
changes: ReleaseChanges.fromJson(json['changes']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'version': version,
|
||||
'notes': notes,
|
||||
if (changes != null) 'changes': changes!.toJson(),
|
||||
};
|
||||
|
||||
bool get isEmpty => notes.trim().isEmpty && (changes?.isEmpty ?? true);
|
||||
}
|
||||
|
||||
/// Where [WhatsNew] keeps its two values (settings box; tests: memory).
|
||||
abstract class WhatsNewStore {
|
||||
/// The version whose card was shown (or that was installed fresh).
|
||||
String? get seen;
|
||||
Future<void> setSeen(String version);
|
||||
|
||||
/// The notes last known for an installed or current version.
|
||||
ReleaseNotes? get notes;
|
||||
Future<void> setNotes(ReleaseNotes notes);
|
||||
|
||||
/// The settings box was empty at start: a fresh install.
|
||||
bool get fresh;
|
||||
}
|
||||
|
||||
class SettingsWhatsNewStore implements WhatsNewStore {
|
||||
final AppSettings settings;
|
||||
SettingsWhatsNewStore([AppSettings? settings]) : settings = settings ?? AppSettings.instance;
|
||||
@override
|
||||
String? get seen => settings.whatsNewSeen;
|
||||
@override
|
||||
Future<void> setSeen(String version) => settings.setWhatsNewSeen(version);
|
||||
@override
|
||||
ReleaseNotes? get notes => ReleaseNotes.fromJson(settings.whatsNewNotes);
|
||||
@override
|
||||
Future<void> setNotes(ReleaseNotes notes) => settings.setWhatsNewNotes(notes.toJson());
|
||||
@override
|
||||
bool get fresh => settings.freshInstall;
|
||||
}
|
||||
|
||||
class MemoryWhatsNewStore implements WhatsNewStore {
|
||||
@override
|
||||
String? seen;
|
||||
@override
|
||||
ReleaseNotes? notes;
|
||||
@override
|
||||
bool fresh;
|
||||
MemoryWhatsNewStore({this.seen, this.notes, this.fresh = false});
|
||||
@override
|
||||
Future<void> setSeen(String version) async => seen = version;
|
||||
@override
|
||||
Future<void> setNotes(ReleaseNotes notes) async => this.notes = notes;
|
||||
}
|
||||
|
||||
class WhatsNew {
|
||||
static final instance = WhatsNew();
|
||||
|
||||
final String currentVersion;
|
||||
|
||||
final WhatsNewStore store;
|
||||
|
||||
WhatsNew({this.currentVersion = appVersion, WhatsNewStore? store}) : store = store ?? SettingsWhatsNewStore();
|
||||
|
||||
/// The card to show now (once); null when there is none.
|
||||
final due = ValueNotifier<ReleaseNotes?>(null);
|
||||
UpdateController? _updates;
|
||||
|
||||
/// At start, settings loaded: a pending card for this version, and
|
||||
/// [updates]' checks from now on.
|
||||
void start(UpdateController updates) {
|
||||
if (!identical(_updates, updates)) {
|
||||
_updates?.latest.removeListener(_onManifest);
|
||||
_updates = updates..latest.addListener(_onManifest);
|
||||
}
|
||||
final s = store;
|
||||
if (s.seen == currentVersion) return;
|
||||
if (s.seen == null && s.fresh) {
|
||||
// A new install: nothing changed for this person.
|
||||
unawaited(s.setSeen(currentVersion));
|
||||
return;
|
||||
}
|
||||
final known = s.notes;
|
||||
if (known != null && known.version == currentVersion && !known.isEmpty) due.value = known;
|
||||
}
|
||||
|
||||
void _onManifest() {
|
||||
final m = _updates?.latest.value;
|
||||
if (m != null) offer(ReleaseNotes.of(m));
|
||||
}
|
||||
|
||||
/// Notes for some version (an update check's manifest). Kept when they
|
||||
/// are this version's; due when its card was not shown yet.
|
||||
void offer(ReleaseNotes n) {
|
||||
if (n.version != currentVersion || n.isEmpty) return;
|
||||
final s = store;
|
||||
unawaited(s.setNotes(n));
|
||||
if (s.seen != currentVersion && !s.fresh) due.value = n;
|
||||
}
|
||||
|
||||
/// Before the restart into [m]: its notes for the first start.
|
||||
Future<void> installing(UpdateManifest m) => store.setNotes(ReleaseNotes.of(m));
|
||||
|
||||
/// The card was shown: never again for this version.
|
||||
void shown() {
|
||||
due.value = null;
|
||||
unawaited(store.setSeen(currentVersion));
|
||||
}
|
||||
|
||||
/// This version's notes, when known (Settings → About).
|
||||
ReleaseNotes? get current {
|
||||
final n = store.notes;
|
||||
return n != null && n.version == currentVersion && !n.isEmpty ? n : null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user