Restructure step 3: your server, Invite, version gate, AppGraph
- One server per app run (loopback, no password); Invite attaches an all-interfaces listener to the same server and Stop sharing detaches it (guests told with 4016); members, media and Up next never reset. No solo mode: you're home on your own server unless in a friend's. - Navigation: none / yours / friend; a join passes a version gate on /health first (clear "they/you need to update", update offer). - Invite as a value (host, port, name, password flag, version, fp reserved); addresses filtered (loopback, link-local, 198.18/15, bridges) and ranked; Sharing chip, Copy invite, Stop sharing. - Alone looks like a plain player (alone replaces solo). - AppGraph builds and disposes systems; widgets use AppScope; SessionState deleted; a test caps the remaining singletons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
629be0d9ed
commit
4d00b7455e
86 files changed
+2993
-2071
No files matched your search
@@ -9,7 +9,6 @@ import 'package:signals/signals_flutter.dart';
|
||||
import 'package:sync_relay_core/core.dart' show validClientId;
|
||||
|
||||
import '../state/connect_prefs.dart';
|
||||
import '../state/session_state.dart';
|
||||
import '../state/settings.dart' show settingsBoxName;
|
||||
import '../widgets/avatar_pipeline.dart';
|
||||
|
||||
@@ -96,17 +95,11 @@ const _keyColor = 'profileColor';
|
||||
/// avatar = removed). Swappable so tests can watch it.
|
||||
typedef ProfilePush = void Function({String? name, String? avatar});
|
||||
|
||||
/// The one place a profile change reaches the server.
|
||||
void pushProfileToSession({String? name, String? avatar}) {
|
||||
final session = SessionState.instance;
|
||||
if (session.route.peek() != AppRoute.session) return;
|
||||
session.coordinator.updateProfile(name: name, avatar: avatar);
|
||||
}
|
||||
|
||||
/// Display name, picture and colour of this app's user. Backed by
|
||||
/// [SessionState.lastForm] (name) and the connect-form box (the cropped
|
||||
/// Display name, picture and colour of this app's user. Backed by the
|
||||
/// connect form ([forms]: the name) and the connect-form box (the cropped
|
||||
/// 256 px PNG and the colour); every change is persisted, and while in a
|
||||
/// server it is pushed through [push] so everyone sees it at once.
|
||||
/// server it is pushed through [push] so everyone sees it at once. The
|
||||
/// composition root sets both.
|
||||
class Profile {
|
||||
static final Profile instance = Profile._();
|
||||
Profile._();
|
||||
@@ -116,7 +109,8 @@ class Profile {
|
||||
final picture = signal<Uint8List?>(null);
|
||||
final color = signal<Color>(profileColors.first);
|
||||
final error = signal<String?>(null);
|
||||
ProfilePush push = pushProfileToSession;
|
||||
ProfilePush push = ({String? name, String? avatar}) {};
|
||||
ConnectStore forms = ConnectStore();
|
||||
bool _loaded = false;
|
||||
Timer? _nameTimer;
|
||||
|
||||
@@ -131,7 +125,7 @@ class Profile {
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final form = SessionState.instance.lastForm;
|
||||
final form = forms.form;
|
||||
name.value = form.name;
|
||||
try {
|
||||
final box = connectFormBoxIfOpen() ?? await openConnectFormBox();
|
||||
@@ -162,7 +156,7 @@ class Profile {
|
||||
final v = value.trim();
|
||||
if (v.isEmpty || v == name.peek()) return;
|
||||
name.value = v;
|
||||
SessionState.instance.lastForm.name = v;
|
||||
forms.form.name = v;
|
||||
unawaited(_save());
|
||||
// Typing renames once it settles, not on every key.
|
||||
_nameTimer?.cancel();
|
||||
@@ -187,7 +181,7 @@ class Profile {
|
||||
|
||||
Future<void> _save() async {
|
||||
try {
|
||||
await SessionState.instance.saveForm();
|
||||
await forms.save();
|
||||
final box = connectFormBoxIfOpen() ?? await openConnectFormBox();
|
||||
final png = picture.peek();
|
||||
await box.putAll({
|
||||
|
||||
Reference in new issue
Block a user