Skip to content

Commit 2b3f0da

Browse files
d
d
1 parent ebb79b1 commit 2b3f0da

6 files changed

Lines changed: 119 additions & 25 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Version 3.1.0 — a from-scratch rewrite in [arcane_jaspr](https://github.com/Ar
1414
- **Validation** engine encoding the plugin's real parsing rules (lowercase registry keys, action source spellings, silent-zero pitfalls like `volume: 0`, legacy and MiniMessage formatting, hitbox overlap warnings).
1515
- **Image library**: upload validated PNG pixel art, preview exactly as the plugin rasterizes it, export `images.zip` laid out for `plugins/holoui/images/`. Server sync preserves captured PNG, JPEG, GIF, WebP and BMP bytes losslessly.
1616
- **Workspace**: folders, multiple menus and flow boards, atomic IndexedDB autosave with previous-transaction recovery and cross-tab conflict protection, undo/redo, templates, and searchable catalogs. Existing `holoui.workspace.v1` / `v2` localStorage data migrates once without deleting the rollback copy.
17-
- **Optional server sync**: capability links opened from `/holoui edit` import an exact menu or persistent world-board graph through the configured relay. Board publication retains the bound no-delete baseline and adds only menus reachable from the board root, leaving unrelated folder documents local. Sync status, conflicts and expiry stay visible; only the explicit **Publish to Server** action sends changes, and revision conflicts never overwrite local work. Links use the configured HTTPS relay (the plugin defaults to `https://sync.holoui.volmitsoftware.com/v1`) or a localhost HTTP development relay; an unreachable provider is reported without implying that a deployment is available.
17+
- **Optional server sync**: capability links opened from `/holoui edit` import an exact menu or persistent world-board graph through the configured relay. Board publication retains the bound no-delete baseline and adds only menus reachable from the board root, leaving unrelated folder documents local. **Publish to Server** appears beside Import and Export only during a live capability session; it durably saves a board project and hot-reloads the running board, while revision conflicts never overwrite local work. Links use the configured HTTPS relay (the plugin defaults to `https://sync.holoui.volmitsoftware.com/v1`) or a localhost HTTP development relay; an unreachable provider is reported without implying that a deployment is available.
1818

1919
The editor itself remains a static client with no accounts. Normal authoring and autosave are local-only; opening a server-issued capability link enables bounded HTTPS requests to that link's relay until the tab disconnects. Capability tokens stay in the URL fragment and tab `sessionStorage`, never in workspace documents or exported bundles. If tab storage is blocked, the capability stays in the fragment and the sync bar tells you to copy the link before reloading.
2020

lib/app.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -709,9 +709,9 @@ class _AppState extends State<App> {
709709
onOpenValidation: () =>
710710
setState(() => _validationOpen = !_validationOpen),
711711
onCloseOverlay: _closeOverlay,
712-
syncBar: _syncBinding == null
712+
syncControls: _syncBinding == null
713713
? null
714-
: EditorSyncBar(
714+
: EditorSyncControls(
715715
status: _visibleSyncStatus,
716716
subjectId: _syncBinding!.subjectId,
717717
busy: _syncPublishing,

lib/components/shell/editor_shell.dart

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import '../../services/image_library.dart';
3131
import '../../state/editor_scope.dart';
3232
import '../../state/editor_store.dart';
3333
import 'command_palette.dart';
34+
import 'editor_sync_bar.dart';
3435
import 'key_listener.dart';
3536
import 'keyboard_shortcuts.dart';
3637
import 'pane_dom.dart';
@@ -65,7 +66,7 @@ class EditorShell extends StatefulWidget {
6566
this.onOpenHelp,
6667
this.onOpenSettings,
6768
this.onOpenValidation,
68-
this.syncBar,
69+
this.syncControls,
6970
this.onCloseOverlay,
7071
this.onToggleTheme,
7172
this.darkMode = true,
@@ -101,7 +102,7 @@ class EditorShell extends StatefulWidget {
101102
final void Function()? onOpenHelp;
102103
final void Function()? onOpenSettings;
103104
final void Function()? onOpenValidation;
104-
final Widget? syncBar;
105+
final EditorSyncControls? syncControls;
105106

106107
/// Escape handler for whichever dialog or sheet the owner has open.
107108
final void Function()? onCloseOverlay;
@@ -366,10 +367,18 @@ class _EditorShellState extends State<EditorShell> {
366367
onCloseOverlay: _escapeOverlay,
367368
child: dom.div(
368369
id: 'hui-shell',
369-
classes: component.syncBar == null ? 'hui-shell' : 'hui-shell has-sync',
370+
classes: component.syncControls == null
371+
? 'hui-shell'
372+
: 'hui-shell has-sync',
370373
<Widget>[
371-
TopBar(intents: intents, apple: _apple, darkMode: component.darkMode),
372-
if (component.syncBar != null) component.syncBar!,
374+
TopBar(
375+
intents: intents,
376+
syncControls: component.syncControls,
377+
apple: _apple,
378+
darkMode: component.darkMode,
379+
),
380+
if (component.syncControls != null)
381+
EditorSyncBar(controls: component.syncControls!),
373382
dom.div(classes: 'hui-shell-body', <Widget>[
374383
dom.aside(
375384
classes: 'hui-pane hui-rail',

lib/components/shell/editor_sync_bar.dart

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@ import 'package:jaspr/dom.dart' as dom;
55

66
import '../../services/editor_sync.dart';
77

8-
class EditorSyncBar extends StatelessWidget {
9-
const EditorSyncBar({
8+
class EditorSyncControls {
9+
const EditorSyncControls({
1010
required this.status,
1111
required this.subjectId,
1212
required this.busy,
1313
required this.onPublish,
1414
required this.onCopyLink,
1515
required this.onDisconnect,
1616
this.message,
17-
super.key,
1817
});
1918

2019
final EditorSyncStatus status;
@@ -25,38 +24,62 @@ class EditorSyncBar extends StatelessWidget {
2524
final VoidCallback onCopyLink;
2625
final VoidCallback onDisconnect;
2726

27+
bool get canPublish => busy == false && canPublishEditorSyncStatus(status);
28+
29+
String get publishLabel => busy ? 'Publishing…' : 'Publish to Server';
30+
31+
String get publishHint => switch (status) {
32+
EditorSyncStatus.connected =>
33+
'Publish the connected $subjectId project for server validation and apply.',
34+
EditorSyncStatus.applied =>
35+
'Publish new changes to the connected $subjectId server project.',
36+
EditorSyncStatus.rejected =>
37+
'The last publication was rejected. Fix the reported issue, then publish again.',
38+
EditorSyncStatus.pending =>
39+
'The server is still validating and applying the current publication.',
40+
EditorSyncStatus.conflict =>
41+
'Resolve the server revision conflict before publishing again.',
42+
EditorSyncStatus.expired =>
43+
'This server capability expired. Create a new link in Minecraft.',
44+
EditorSyncStatus.revoked =>
45+
'This server capability was revoked. Create a new link in Minecraft.',
46+
EditorSyncStatus.disconnected =>
47+
'This tab is disconnected from the server project.',
48+
EditorSyncStatus.unavailable =>
49+
'The configured sync relay is currently unavailable.',
50+
};
51+
}
52+
53+
class EditorSyncBar extends StatelessWidget {
54+
const EditorSyncBar({required this.controls, super.key});
55+
56+
final EditorSyncControls controls;
57+
2858
@override
2959
Widget build(BuildContext context) => dom.div(
30-
classes: 'hui-sync-bar is-${status.name}',
60+
classes: 'hui-sync-bar is-${controls.status.name}',
3161
attributes: const <String, String>{'role': 'status', 'aria-live': 'polite'},
3262
<Widget>[
3363
dom.div(classes: 'hui-sync-identity', <Widget>[
3464
ArcaneIcon.cloud(size: IconSize.sm),
35-
dom.strong(<Widget>[Text(_label(status))]),
36-
dom.code(<Widget>[Text(subjectId)]),
37-
if (message != null) dom.span(<Widget>[Text(message!)]),
65+
dom.strong(<Widget>[Text(_label(controls.status))]),
66+
dom.code(<Widget>[Text(controls.subjectId)]),
67+
if (controls.message != null)
68+
dom.span(<Widget>[Text(controls.message!)]),
3869
]),
3970
dom.div(classes: 'hui-sync-actions', <Widget>[
4071
Button.ghost(
4172
size: ButtonSize.sm,
42-
onPressed: onCopyLink,
73+
onPressed: controls.onCopyLink,
4374
icon: ArcaneIcon.copy(size: IconSize.sm),
4475
label: 'Copy link',
4576
),
4677
Button.ghost(
4778
size: ButtonSize.sm,
48-
onPressed: onDisconnect,
79+
onPressed: controls.onDisconnect,
4980
icon: ArcaneIcon.unlink(size: IconSize.sm),
5081
label: 'Disconnect',
5182
),
52-
Button(
53-
size: ButtonSize.sm,
54-
variant: ButtonVariant.primary,
55-
disabled: busy || !canPublishEditorSyncStatus(status),
56-
onPressed: busy ? null : onPublish,
57-
icon: ArcaneIcon.cloudUpload(size: IconSize.sm),
58-
label: busy ? 'Publishing…' : 'Publish to Server',
59-
),
6083
]),
6184
],
6285
);

lib/components/shell/top_bar.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import '../../state/editor_store.dart';
1515
import '../../state/workspace.dart';
1616
import '../common/class_names.dart';
1717
import 'bar_menu.dart';
18+
import 'editor_sync_bar.dart';
1819
import 'shell_intents.dart';
1920
import 'shell_keys.dart';
2021
import 'store_selector.dart';
@@ -23,12 +24,14 @@ import 'view_switcher.dart';
2324
class TopBar extends StatefulWidget {
2425
const TopBar({
2526
required this.intents,
27+
this.syncControls,
2628
this.apple = false,
2729
this.darkMode = true,
2830
super.key,
2931
});
3032

3133
final ShellIntents intents;
34+
final EditorSyncControls? syncControls;
3235
final bool apple;
3336
final bool darkMode;
3437

@@ -75,6 +78,14 @@ class _TopBarState extends State<TopBar> {
7578
..write('|')
7679
..write(_store.redoLabel)
7780
..write('|')
81+
..write(component.syncControls?.status.name)
82+
..write('|')
83+
..write(component.syncControls?.subjectId)
84+
..write('|')
85+
..write(component.syncControls?.busy)
86+
..write('|')
87+
..write(component.syncControls?.message)
88+
..write('|')
7889
..write(workspace.activeId);
7990
for (final WorkspaceDoc doc in workspace.recent) {
8091
buffer
@@ -156,6 +167,17 @@ class _TopBarState extends State<TopBar> {
156167
onPressed: _intents.exportMenu,
157168
disabled: !_store.canTransferDocument,
158169
),
170+
if (component.syncControls != null)
171+
_action(
172+
icon: ArcaneIcon.cloudUpload(size: IconSize.sm),
173+
label: component.syncControls!.publishLabel,
174+
hint: component.syncControls!.publishHint,
175+
variant: component.syncControls!.canPublish
176+
? ButtonVariant.primary
177+
: ButtonVariant.outline,
178+
onPressed: component.syncControls!.onPublish,
179+
disabled: component.syncControls!.canPublish == false,
180+
),
159181
_action(
160182
icon: ArcaneIcon.copy(size: IconSize.sm),
161183
label: 'Copy menu JSON',

test/editor_sync_bar_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import 'package:holoui_editor/components/shell/editor_sync_bar.dart';
2+
import 'package:holoui_editor/services/editor_sync.dart';
3+
import 'package:test/test.dart';
4+
5+
void main() {
6+
group('editor sync publish controls', () {
7+
test('connected and applied projects can publish', () {
8+
expect(_controls(EditorSyncStatus.connected).canPublish, isTrue);
9+
expect(_controls(EditorSyncStatus.applied).canPublish, isTrue);
10+
});
11+
12+
test('busy and pending projects cannot publish', () {
13+
expect(
14+
_controls(EditorSyncStatus.connected, busy: true).canPublish,
15+
isFalse,
16+
);
17+
expect(_controls(EditorSyncStatus.pending).canPublish, isFalse);
18+
});
19+
20+
test('rejected projects explain that publishing can be retried', () {
21+
final EditorSyncControls controls = _controls(EditorSyncStatus.rejected);
22+
23+
expect(controls.canPublish, isTrue);
24+
expect(controls.publishLabel, 'Publish to Server');
25+
expect(controls.publishHint, contains('publish again'));
26+
});
27+
});
28+
}
29+
30+
EditorSyncControls _controls(EditorSyncStatus status, {bool busy = false}) =>
31+
EditorSyncControls(
32+
status: status,
33+
subjectId: 'lobby-board',
34+
busy: busy,
35+
onPublish: _noop,
36+
onCopyLink: _noop,
37+
onDisconnect: _noop,
38+
);
39+
40+
void _noop() {}

0 commit comments

Comments
 (0)