Skip to content

Commit b84c2f6

Browse files
dddddd
1 parent 962a4f1 commit b84c2f6

42 files changed

Lines changed: 1764 additions & 433 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

lib/app.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,7 @@ class _AppState extends State<App> {
735735
store: _store,
736736
contents: ComponentsRail(store: _store),
737737
syncBinding: _syncBinding,
738+
isDarkMode: _brightness == Brightness.dark,
738739
),
739740
canvas: CanvasViewport(
740741
store: _store,

lib/components/common/common.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ library;
33

44
export 'class_names.dart';
55
export 'hui_field.dart';
6+
export 'hui_action_menu.dart';
67
export 'hui_number_field.dart';
78
export 'hui_panel.dart';
89
export 'hui_vec3_field.dart';
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
library;
2+
3+
import 'package:arcane_jaspr/arcane_jaspr.dart';
4+
import 'package:arcane_jaspr/core/dom_value.dart';
5+
import 'package:jaspr/dom.dart' as dom;
6+
7+
import 'class_names.dart';
8+
import 'hui_action_menu_dom.dart';
9+
10+
final class HuiActionMenuItem {
11+
const HuiActionMenuItem({
12+
required this.label,
13+
required this.icon,
14+
required this.onSelect,
15+
this.hint,
16+
this.disabled = false,
17+
this.destructive = false,
18+
this.separatorBefore = false,
19+
});
20+
21+
final String label;
22+
final Widget icon;
23+
final void Function() onSelect;
24+
final String? hint;
25+
final bool disabled;
26+
final bool destructive;
27+
final bool separatorBefore;
28+
}
29+
30+
final class HuiActionMenuPoint {
31+
const HuiActionMenuPoint(this.x, this.y);
32+
33+
final double x;
34+
final double y;
35+
}
36+
37+
HuiActionMenuPoint huiActionMenuEventPoint(Object? event) {
38+
final ({double x, double y}) point = huiActionMenuEventCoordinates(event);
39+
return HuiActionMenuPoint(point.x, point.y);
40+
}
41+
42+
HuiActionMenuPoint huiActionMenuAnchor(String elementId) {
43+
final ({double x, double y}) point = huiActionMenuAnchorCoordinates(
44+
elementId,
45+
);
46+
return HuiActionMenuPoint(point.x, point.y);
47+
}
48+
49+
void focusHuiActionMenu(String elementId) =>
50+
focusHuiActionMenuElement(elementId);
51+
52+
class HuiActionMenu extends StatelessWidget {
53+
const HuiActionMenu({
54+
required this.id,
55+
required this.label,
56+
required this.point,
57+
required this.items,
58+
required this.onClose,
59+
super.key,
60+
});
61+
62+
final String id;
63+
final String label;
64+
final HuiActionMenuPoint point;
65+
final List<HuiActionMenuItem> items;
66+
final void Function() onClose;
67+
68+
@override
69+
Widget build(BuildContext context) => dom.div(
70+
classes: 'hui-action-menu-layer',
71+
<Widget>[
72+
dom.div(
73+
classes: 'hui-action-menu-scrim',
74+
attributes: const <String, String>{'aria-hidden': 'true'},
75+
events: <String, void Function(Object)>{
76+
'pointerdown': (Object _) => onClose(),
77+
'contextmenu': (Object event) {
78+
domPreventDefault(event);
79+
onClose();
80+
},
81+
},
82+
const <Widget>[],
83+
),
84+
dom.div(
85+
id: id,
86+
classes: 'hui-action-menu',
87+
styles: dom.Styles(
88+
raw: <String, String>{
89+
'--hui-menu-x': '${point.x.round()}px',
90+
'--hui-menu-y': '${point.y.round()}px',
91+
'--hui-menu-height': '${(items.length * 42 + 20).clamp(96, 440)}px',
92+
},
93+
),
94+
attributes: <String, String>{
95+
'role': 'menu',
96+
'aria-label': label,
97+
'tabindex': '-1',
98+
},
99+
events: <String, void Function(Object)>{
100+
'keydown': (Object event) {
101+
final String key = domEventKey(event);
102+
if (key == 'Escape') {
103+
domStopPropagation(event);
104+
onClose();
105+
return;
106+
}
107+
if (key != 'ArrowDown' &&
108+
key != 'ArrowUp' &&
109+
key != 'Home' &&
110+
key != 'End') {
111+
return;
112+
}
113+
domPreventDefault(event);
114+
domStopPropagation(event);
115+
moveHuiActionMenuFocus(id, key);
116+
},
117+
},
118+
<Widget>[
119+
for (final HuiActionMenuItem item in items) ...<Widget>[
120+
if (item.separatorBefore)
121+
const dom.div(
122+
classes: 'hui-action-menu-separator',
123+
attributes: <String, String>{'role': 'separator'},
124+
<Widget>[],
125+
),
126+
dom.button(
127+
classes: classNames(<String?>[
128+
'hui-action-menu-item',
129+
item.destructive ? 'is-destructive' : null,
130+
]),
131+
attributes: <String, String>{
132+
'type': 'button',
133+
'role': 'menuitem',
134+
if (item.disabled) 'disabled': '',
135+
if (item.disabled) 'aria-disabled': 'true',
136+
},
137+
events: <String, void Function(Object)>{
138+
'click': (Object _) {
139+
if (item.disabled) return;
140+
onClose();
141+
item.onSelect();
142+
},
143+
},
144+
<Widget>[
145+
dom.span(
146+
classes: 'hui-action-menu-icon',
147+
attributes: const <String, String>{'aria-hidden': 'true'},
148+
<Widget>[item.icon],
149+
),
150+
dom.span(classes: 'hui-action-menu-label', <Widget>[
151+
Text(item.label),
152+
]),
153+
if (item.hint != null)
154+
dom.span(classes: 'hui-action-menu-hint', <Widget>[
155+
Text(item.hint!),
156+
]),
157+
],
158+
),
159+
],
160+
],
161+
),
162+
],
163+
);
164+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
library;
2+
3+
export 'hui_action_menu_dom_stub.dart'
4+
if (dart.library.js_interop) 'hui_action_menu_dom_web.dart';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
library;
2+
3+
({double x, double y}) huiActionMenuEventCoordinates(Object? event) =>
4+
(x: 8, y: 8);
5+
6+
({double x, double y}) huiActionMenuAnchorCoordinates(String elementId) =>
7+
(x: 8, y: 8);
8+
9+
void focusHuiActionMenuElement(String elementId) {}
10+
11+
void moveHuiActionMenuFocus(String elementId, String key) {}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
library;
2+
3+
import 'dart:js_interop';
4+
import 'dart:js_interop_unsafe';
5+
6+
import 'package:web/web.dart' as web;
7+
8+
({double x, double y}) huiActionMenuEventCoordinates(Object? event) {
9+
final JSObject? object = event as JSObject?;
10+
final JSAny? rawX = object?.getProperty<JSAny?>('clientX'.toJS);
11+
final JSAny? rawY = object?.getProperty<JSAny?>('clientY'.toJS);
12+
final double x = rawX.isA<JSNumber>() ? (rawX! as JSNumber).toDartDouble : 8;
13+
final double y = rawY.isA<JSNumber>() ? (rawY! as JSNumber).toDartDouble : 8;
14+
return (x: x, y: y);
15+
}
16+
17+
({double x, double y}) huiActionMenuAnchorCoordinates(String elementId) {
18+
final web.Element? element = web.document.getElementById(elementId);
19+
if (element == null) return (x: 8, y: 8);
20+
final web.DOMRect rect = element.getBoundingClientRect();
21+
return (x: rect.right - 8, y: rect.bottom + 4);
22+
}
23+
24+
void focusHuiActionMenuElement(String elementId) {
25+
(web.document.getElementById(elementId) as web.HTMLElement?)?.focus();
26+
}
27+
28+
void moveHuiActionMenuFocus(String elementId, String key) {
29+
final web.Element? menu = web.document.getElementById(elementId);
30+
if (menu == null) return;
31+
final web.NodeList buttons = menu.querySelectorAll(
32+
'.hui-action-menu-item:not(:disabled)',
33+
);
34+
if (buttons.length == 0) return;
35+
final web.Element? active = web.document.activeElement;
36+
int current = -1;
37+
for (int index = 0; index < buttons.length; index += 1) {
38+
if (identical(buttons.item(index), active)) {
39+
current = index;
40+
break;
41+
}
42+
}
43+
final int target = switch (key) {
44+
'Home' => 0,
45+
'End' => buttons.length - 1,
46+
'ArrowUp' => current <= 0 ? buttons.length - 1 : current - 1,
47+
_ => current >= buttons.length - 1 ? 0 : current + 1,
48+
};
49+
(buttons.item(target) as web.HTMLElement?)?.focus();
50+
}

lib/components/dialogs/settings_dialog.dart

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,13 @@ import 'package:jaspr/jaspr.dart' show ListenableBuilder;
1212

1313
import '../../services/catalogs.dart';
1414
import '../../services/file_transfer.dart';
15+
import '../../services/local_data_reset.dart';
1516
import '../../services/storage_service.dart';
1617
import '../../state/editor_store.dart';
1718
import '../common/common.dart';
1819
import '../panels/two_step_button.dart';
1920
import 'dialog_parts.dart';
2021

21-
/// Mirrors the key in `lib/theme/theme_state.dart`. Only re-stamped here, never
22-
/// read: the shell owns the brightness.
23-
const String _themeStorageKey = 'gloss.theme';
24-
2522
class SettingsDialog extends StatelessWidget {
2623
const SettingsDialog({
2724
required this.store,
@@ -47,37 +44,15 @@ class SettingsDialog extends StatelessWidget {
4744
/// flags), then storage is dropped, then the in-memory state is rebuilt from
4845
/// the now empty keys.
4946
Future<void> _resetLocalData() async {
50-
store.flushAutosave();
51-
final bool reset = await store.workspace.reset();
52-
if (!reset) {
53-
toast.error(
54-
store.workspace.lastError ?? 'Browser storage refused the reset.',
55-
);
56-
return;
57-
}
58-
final bool imagesCleared = store.images?.clear() ?? true;
59-
final bool localStorageCleared = StorageService.clearAll();
60-
// The brightness belongs to the shell, not to the document data, so it is
61-
// re-stamped: a reload after a reset keeps the theme on screen.
62-
final bool themeSaved = StorageService.write(
63-
_themeStorageKey,
64-
isDarkMode ? 'dark' : 'light',
47+
final LocalDataResetResult result = await resetAllLocalEditorData(
48+
store,
49+
isDarkMode: isDarkMode,
6550
);
66-
store.images?.load();
67-
store.resetLocalPreferences();
68-
final bool documentCreated = store.newDocument();
69-
await store.workspace.writesSettled;
70-
final bool workspaceReady =
71-
documentCreated && !store.workspace.hasUnsavedChanges;
72-
if (imagesCleared && localStorageCleared && themeSaved && workspaceReady) {
73-
toast.warning('Local data cleared. A new empty workspace is ready.');
74-
return;
51+
if (result.success) {
52+
toast.warning(result.message);
53+
} else {
54+
toast.error(result.message);
7555
}
76-
toast.error(
77-
'The workspace was reset, but some browser data could not be cleared or '
78-
'the new empty document could not be saved. Reload and verify local data '
79-
'before continuing.',
80-
);
8156
}
8257

8358
@override

lib/components/motd/motd_view.dart

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/// entry (`handlePing`: `ThreadLocalRandom.nextInt(entries.size())`) and
66
/// renders its joined lines through `renderStatic` — text functions (so
77
/// `|animation.<id>|`) and colours apply, PlaceholderAPI tokens stay literal
8-
/// because a ping has no viewer. The Randomize button replays that pick; the
8+
/// because a ping has no viewer. Shuffle preview replays that pick; the
99
/// entry chips address one entry directly. The ping bars and player count are
1010
/// cosmetic — the client fills those, never the plugin.
1111
///
@@ -88,9 +88,13 @@ class _MotdViewState extends State<MotdView> {
8888
}
8989

9090
/// `MotdService.handlePing`'s pick, replayed on demand.
91-
void _randomize(GlossMotdDoc doc) {
91+
void _shufflePreview(GlossMotdDoc doc) {
9292
if (doc.entries.length <= 1) return;
93-
setState(() => _entryIndex = _random.nextInt(doc.entries.length));
93+
final int current = _entryIndex.clamp(0, doc.entries.length - 1);
94+
final int next =
95+
(current + 1 + _random.nextInt(doc.entries.length - 1)) %
96+
doc.entries.length;
97+
setState(() => _entryIndex = next);
9498
}
9599

96100
@override
@@ -170,8 +174,10 @@ class _MotdViewState extends State<MotdView> {
170174
variant: ButtonVariant.outline,
171175
size: ButtonSize.sm,
172176
icon: ArcaneIcon.dices(size: IconSize.sm),
173-
onPressed: doc.entries.length > 1 ? () => _randomize(doc) : null,
174-
child: const Text('Randomize'),
177+
onPressed: doc.entries.length > 1
178+
? () => _shufflePreview(doc)
179+
: null,
180+
child: const Text('Shuffle preview'),
175181
),
176182
dom.div(classes: 'hui-motd-entry-chips', <Widget>[
177183
for (int index = 0; index < doc.entries.length; index++)

0 commit comments

Comments
 (0)