Files
sync_replay_project/apps/desktop/lib/src/update/whats_new.dart
T

151 lines
4.8 KiB
Dart

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(this.settings);
@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;
}
/// Made once by `AppGraph` (the shell and About read it from there).
class WhatsNew {
final String currentVersion;
final WhatsNewStore store;
WhatsNew({this.currentVersion = appVersion, required this.store});
/// 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;
}
}