kasy-cli 1.19.3 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kasy.js +1 -0
- package/lib/commands/new.js +9 -0
- package/lib/commands/run.js +7 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +91 -2
- package/lib/scaffold/engine.js +5 -0
- package/lib/scaffold/generate.js +4 -0
- package/lib/scaffold/shared/generator-utils.js +38 -1
- package/lib/utils/i18n/messages-en.js +1 -0
- package/lib/utils/i18n/messages-es.js +1 -0
- package/lib/utils/i18n/messages-pt.js +1 -0
- package/package.json +1 -1
- package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
- package/templates/firebase/lib/components/kasy_date_picker.dart +14 -8
- package/templates/firebase/lib/components/kasy_sidebar_pro.dart +1148 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +156 -43
- package/templates/firebase/lib/components/kasy_text_field.dart +37 -34
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +90 -69
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +30 -7
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +8 -1
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +433 -243
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +198 -83
- package/templates/firebase/lib/core/icons/kasy_icons.dart +1 -0
- package/templates/firebase/lib/core/theme/colors.dart +6 -2
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +119 -19
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +68 -27
- package/templates/firebase/lib/features/home/home_components_page.dart +3 -0
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +121 -66
- package/templates/firebase/lib/features/settings/settings_page.dart +27 -146
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +16 -3
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +22 -5
- package/templates/firebase/lib/i18n/en.i18n.json +3 -1
- package/templates/firebase/lib/i18n/es.i18n.json +3 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +3 -1
- package/templates/firebase/pubspec.yaml +6 -4
- package/templates/firebase/web/index.html +7 -17
- package/templates/firebase/lib/firebase_options.dart +0 -75
|
@@ -8,89 +8,128 @@ class DevInspectorService {
|
|
|
8
8
|
|
|
9
9
|
static const _screenRegex = r'(Page|Screen|Route|View)$';
|
|
10
10
|
|
|
11
|
+
// Widgets we refuse to return — even after climbing.
|
|
11
12
|
static const _skipSet = <String>{
|
|
12
13
|
'RenderView',
|
|
13
|
-
'RepaintBoundary',
|
|
14
|
-
'PhysicalModel',
|
|
15
|
-
'Semantics',
|
|
16
|
-
'BlockSemantics',
|
|
17
|
-
'ExcludeSemantics',
|
|
18
|
-
'MergeSemantics',
|
|
19
|
-
'Focus',
|
|
20
|
-
'FocusScope',
|
|
21
|
-
'FocusTraversalOrder',
|
|
22
|
-
'FocusTraversalGroup',
|
|
23
|
-
'Actions',
|
|
24
|
-
'Shortcuts',
|
|
25
|
-
'MouseRegion',
|
|
26
|
-
'Listener',
|
|
27
|
-
'AnnotatedRegion',
|
|
28
|
-
'Localizations',
|
|
29
|
-
'Directionality',
|
|
30
|
-
'Title',
|
|
31
|
-
'MediaQuery',
|
|
32
|
-
'DefaultTextStyle',
|
|
33
|
-
'IconTheme',
|
|
34
|
-
'NotificationListener',
|
|
35
|
-
'ScrollConfiguration',
|
|
36
|
-
'PrimaryScrollController',
|
|
37
|
-
'ScrollNotificationObserver',
|
|
38
|
-
'HeroControllerScope',
|
|
39
14
|
'WidgetInspector',
|
|
40
15
|
'CheckedModeBanner',
|
|
41
16
|
'Banner',
|
|
42
17
|
'DevInspector',
|
|
43
18
|
};
|
|
44
19
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'
|
|
51
|
-
|
|
52
|
-
|
|
20
|
+
// Widgets we climb past while looking for a meaningful target. These are
|
|
21
|
+
// framework-level wrappers (layout primitives, decorations, animations,
|
|
22
|
+
// scroll machinery, theming, etc.) — selecting them is rarely useful.
|
|
23
|
+
static const _climbPastSet = <String>{
|
|
24
|
+
// Repaint / physical
|
|
25
|
+
'RepaintBoundary', 'PhysicalModel', 'PhysicalShape', 'CustomPaint',
|
|
26
|
+
|
|
27
|
+
// Layout primitives
|
|
28
|
+
'Padding', 'Container', 'SizedBox', 'Center', 'Align',
|
|
29
|
+
'Expanded', 'Flexible', 'Spacer',
|
|
30
|
+
'ConstrainedBox', 'UnconstrainedBox', 'FractionallySizedBox',
|
|
31
|
+
'AspectRatio', 'LimitedBox', 'OverflowBox', 'FittedBox', 'Baseline',
|
|
32
|
+
'IntrinsicWidth', 'IntrinsicHeight',
|
|
33
|
+
'DecoratedBox', 'ColoredBox', 'DecoratedBoxTransition',
|
|
34
|
+
|
|
35
|
+
// Multi-child layout
|
|
36
|
+
'Column', 'Row', 'Stack', 'Wrap', 'Flow', 'IndexedStack',
|
|
37
|
+
'Positioned', 'PositionedDirectional',
|
|
38
|
+
|
|
39
|
+
// Material / Ink
|
|
40
|
+
'Material', 'Ink', 'InkWell', 'InkResponse',
|
|
41
|
+
|
|
42
|
+
// Pointer-related
|
|
43
|
+
'GestureDetector', 'RawGestureDetector', 'MouseRegion',
|
|
44
|
+
'Listener', 'AbsorbPointer', 'IgnorePointer', 'PointerInterceptor',
|
|
45
|
+
|
|
46
|
+
// Builders
|
|
47
|
+
'Builder', 'LayoutBuilder', 'StreamBuilder', 'FutureBuilder',
|
|
48
|
+
'ValueListenableBuilder', 'ListenableBuilder',
|
|
49
|
+
'OrientationBuilder', 'MediaQuery',
|
|
50
|
+
|
|
51
|
+
// Animations
|
|
52
|
+
'AnimatedContainer', 'AnimatedOpacity', 'AnimatedSize',
|
|
53
|
+
'AnimatedSwitcher', 'AnimatedDefaultTextStyle',
|
|
54
|
+
'AnimatedPhysicalModel', 'AnimatedBuilder', 'AnimatedAlign',
|
|
55
|
+
'AnimatedPositioned', 'AnimatedPadding', 'AnimatedTheme',
|
|
56
|
+
'FadeTransition', 'SlideTransition', 'ScaleTransition',
|
|
57
|
+
'RotationTransition', 'SizeTransition',
|
|
58
|
+
'PositionedTransition', 'RelativePositionedTransition',
|
|
59
|
+
'AlignTransition', 'DefaultTextStyleTransition',
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
|
|
61
|
+
// Transforms / effects
|
|
62
|
+
'Transform', 'Opacity', 'BackdropFilter', 'ImageFiltered',
|
|
63
|
+
'ClipRect', 'ClipRRect', 'ClipOval', 'ClipPath',
|
|
64
|
+
|
|
65
|
+
// Context propagation
|
|
66
|
+
'Theme', 'DefaultTextStyle', 'IconTheme', 'IconButtonTheme',
|
|
67
|
+
'Localizations', 'Directionality', 'Title',
|
|
68
|
+
'DefaultSelectionStyle', 'SelectionContainer', 'AnnotatedRegion',
|
|
69
|
+
|
|
70
|
+
// Routing / storage
|
|
71
|
+
'KeyedSubtree', 'AutomaticKeepAlive', 'KeepAlive', 'TickerMode',
|
|
72
|
+
'Offstage', 'PageStorage', 'RestorationScope',
|
|
73
|
+
'UnmanagedRestorationScope', 'HeroControllerScope', 'HeroMode',
|
|
74
|
+
'PopScope', 'WillPopScope', 'BackButtonListener', 'Visibility',
|
|
75
|
+
'NavigatorPopHandler',
|
|
76
|
+
|
|
77
|
+
// Scroll / slivers
|
|
78
|
+
'NotificationListener', 'SafeArea',
|
|
79
|
+
'SliverPadding', 'SliverToBoxAdapter', 'SliverList', 'SliverGrid',
|
|
80
|
+
'SliverFillRemaining', 'SliverFillViewport', 'SliverFixedExtentList',
|
|
81
|
+
'SliverPrototypeExtentList', 'SliverMainAxisGroup', 'SliverCrossAxisGroup',
|
|
82
|
+
'CustomScrollView', 'NestedScrollView', 'SingleChildScrollView',
|
|
83
|
+
'PrimaryScrollController', 'ScrollConfiguration', 'Scrollable',
|
|
84
|
+
'ScrollNotificationObserver', 'Viewport', 'ShrinkWrappingViewport',
|
|
85
|
+
|
|
86
|
+
// Focus / semantics / actions
|
|
87
|
+
'Focus', 'FocusScope', 'FocusTraversalOrder', 'FocusTraversalGroup',
|
|
88
|
+
'Actions', 'Shortcuts', 'CallbackShortcuts',
|
|
89
|
+
'Semantics', 'BlockSemantics', 'ExcludeSemantics', 'MergeSemantics',
|
|
79
90
|
'IndexedSemantics',
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
|
|
92
|
+
// Atomic-but-noisy
|
|
93
|
+
'RichText', 'RawMaterialButton',
|
|
82
94
|
};
|
|
83
95
|
|
|
84
|
-
static bool
|
|
85
|
-
if (
|
|
96
|
+
static bool _shouldClimbPast(String typeName) {
|
|
97
|
+
if (_climbPastSet.contains(typeName)) return true;
|
|
86
98
|
return typeName.startsWith('NotificationListener<') ||
|
|
87
99
|
typeName.startsWith('Listener<') ||
|
|
88
100
|
typeName.startsWith('Builder<');
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
// Used by ancestor-list builders (kept as-is — affects displayed tree only).
|
|
104
|
+
static bool _isGenericUi(String typeName) => _shouldClimbPast(typeName);
|
|
105
|
+
|
|
106
|
+
/// Private widgets we DO want to skip — usually framework internals that
|
|
107
|
+
/// add noise (focus scopes, restoration scopes, builder wrappers, …).
|
|
108
|
+
/// Anything else starting with `_` is treated as a user-defined private
|
|
109
|
+
/// widget (e.g. `_HomeCard`, `_NavItem`) and is fully selectable.
|
|
110
|
+
static const _privateFrameworkSet = <String>{
|
|
111
|
+
'_FocusInheritedScope', '_FocusScopeMarker', '_FocusMarker',
|
|
112
|
+
'_InheritedTheme', '_InheritedAnimatedTheme',
|
|
113
|
+
'_RestorationScope', '_UnmanagedRestorationScope',
|
|
114
|
+
'_NestedHero', '_HeroFlightManifest',
|
|
115
|
+
'_BodyBuilder', '_ScaffoldSlot',
|
|
116
|
+
'_ActionsScope', '_ShortcutsMarker',
|
|
117
|
+
'_LocalizationsScope', '_DirectionalityScope',
|
|
118
|
+
'_MediaQueryFromView', '_MediaQueryFromWindow',
|
|
119
|
+
'_OverlayEntryWidget', '_OverlayState',
|
|
120
|
+
'_ModalScope', '_ModalScopeStatus', '_RouteEntry',
|
|
121
|
+
'_BannerWidget', '_CheckedModeBanner',
|
|
122
|
+
'_TickerModeMarker',
|
|
123
|
+
'_Pending', '_LinearProgressIndicator', '_CircularProgressIndicator',
|
|
124
|
+
'_ScrollNotificationObserverScope',
|
|
125
|
+
'_PrimaryScrollControllerScope',
|
|
126
|
+
'_TapRegionRegistry', '_TapRegionSurface',
|
|
127
|
+
'_KeyedSubtree',
|
|
128
|
+
};
|
|
129
|
+
|
|
91
130
|
static bool _shouldSkip(String typeName) {
|
|
92
|
-
if (
|
|
93
|
-
return
|
|
131
|
+
if (_skipSet.contains(typeName)) return true;
|
|
132
|
+
return _privateFrameworkSet.contains(typeName);
|
|
94
133
|
}
|
|
95
134
|
|
|
96
135
|
static List<String> _extractProperties(Element element) {
|
|
@@ -163,16 +202,14 @@ class DevInspectorService {
|
|
|
163
202
|
// 1) Prioritize feature-level widgets/classes first.
|
|
164
203
|
for (final type in chain) {
|
|
165
204
|
if (_shouldSkip(type)) continue;
|
|
166
|
-
if (
|
|
167
|
-
if (_isGenericUi(type)) continue;
|
|
205
|
+
if (_shouldClimbPast(type)) continue;
|
|
168
206
|
if (featureRegex.hasMatch(type)) return type;
|
|
169
207
|
}
|
|
170
208
|
|
|
171
209
|
// 2) Fallback to first non-generic non-skipped widget.
|
|
172
210
|
for (final type in chain) {
|
|
173
211
|
if (_shouldSkip(type)) continue;
|
|
174
|
-
if (
|
|
175
|
-
if (_isGenericUi(type)) continue;
|
|
212
|
+
if (_shouldClimbPast(type)) continue;
|
|
176
213
|
return type;
|
|
177
214
|
}
|
|
178
215
|
return null;
|
|
@@ -242,16 +279,24 @@ class DevInspectorService {
|
|
|
242
279
|
var typeName = element.widget.runtimeType.toString();
|
|
243
280
|
var activeElement = element;
|
|
244
281
|
|
|
245
|
-
|
|
282
|
+
// Climb past framework wrappers (layout, decoration, animation, scroll,
|
|
283
|
+
// theming, etc.) to reach the most meaningful widget — typically a custom
|
|
284
|
+
// widget from the user's project or a semantic widget (Text, Icon, Card,
|
|
285
|
+
// Button, …).
|
|
286
|
+
if (_shouldClimbPast(typeName) || _shouldSkip(typeName)) {
|
|
287
|
+
Element? found;
|
|
288
|
+
String? foundName;
|
|
246
289
|
element.visitAncestorElements((ancestor) {
|
|
247
290
|
final name = ancestor.widget.runtimeType.toString();
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return true;
|
|
291
|
+
if (_shouldSkip(name)) return true;
|
|
292
|
+
if (_shouldClimbPast(name)) return true;
|
|
293
|
+
found = ancestor;
|
|
294
|
+
foundName = name;
|
|
295
|
+
return false;
|
|
254
296
|
});
|
|
297
|
+
if (found == null || foundName == null) return null;
|
|
298
|
+
typeName = foundName!;
|
|
299
|
+
activeElement = found!;
|
|
255
300
|
}
|
|
256
301
|
|
|
257
302
|
if (_shouldSkip(typeName)) return null;
|
|
@@ -280,18 +325,20 @@ class DevInspectorService {
|
|
|
280
325
|
);
|
|
281
326
|
}
|
|
282
327
|
|
|
283
|
-
/// Hit-tests at [globalPosition] and returns the
|
|
284
|
-
///
|
|
285
|
-
///
|
|
286
|
-
|
|
328
|
+
/// Hit-tests inside [contentBox] at [globalPosition] and returns the
|
|
329
|
+
/// inspector info plus the underlying render object. Starting the hit test
|
|
330
|
+
/// from the content box (instead of the root render view) skips the
|
|
331
|
+
/// DevInspector's own overlay subtree.
|
|
332
|
+
static ({DevInspectorInfo info, RenderObject renderObject})? pickAtInBox(
|
|
333
|
+
RenderBox contentBox,
|
|
287
334
|
Offset globalPosition,
|
|
288
335
|
) {
|
|
289
336
|
if (!kDebugMode) return null;
|
|
337
|
+
if (!contentBox.attached) return null;
|
|
290
338
|
try {
|
|
339
|
+
final Offset local = contentBox.globalToLocal(globalPosition);
|
|
291
340
|
final result = BoxHitTestResult();
|
|
292
|
-
|
|
293
|
-
if (renderView == null) return null;
|
|
294
|
-
renderView.hitTest(result, position: globalPosition);
|
|
341
|
+
contentBox.hitTest(result, position: local);
|
|
295
342
|
for (final entry in result.path) {
|
|
296
343
|
if (entry.target is! RenderObject) continue;
|
|
297
344
|
final RenderObject candidate = entry.target as RenderObject;
|
|
@@ -312,4 +359,72 @@ class DevInspectorService {
|
|
|
312
359
|
if (!target.attached) return null;
|
|
313
360
|
return _globalBoundingRect(target);
|
|
314
361
|
}
|
|
362
|
+
|
|
363
|
+
/// Climbs one level up the meaningful-widget hierarchy from [current].
|
|
364
|
+
/// Used to bubble a repeat-tap up to the parent widget when the current
|
|
365
|
+
/// selection isn't the one the user wanted.
|
|
366
|
+
static ({DevInspectorInfo info, RenderObject renderObject})? climbFrom(
|
|
367
|
+
RenderObject current,
|
|
368
|
+
) {
|
|
369
|
+
if (!kDebugMode) return null;
|
|
370
|
+
if (!current.attached) return null;
|
|
371
|
+
try {
|
|
372
|
+
final creator = current.debugCreator;
|
|
373
|
+
if (creator == null) return null;
|
|
374
|
+
Element? element;
|
|
375
|
+
try {
|
|
376
|
+
// ignore: avoid_dynamic_calls
|
|
377
|
+
element = (creator as dynamic).element as Element?;
|
|
378
|
+
} catch (_) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
if (element == null) return null;
|
|
382
|
+
|
|
383
|
+
Element? found;
|
|
384
|
+
element.visitAncestorElements((ancestor) {
|
|
385
|
+
final name = ancestor.widget.runtimeType.toString();
|
|
386
|
+
if (_shouldSkip(name)) return true;
|
|
387
|
+
if (_shouldClimbPast(name)) return true;
|
|
388
|
+
found = ancestor;
|
|
389
|
+
return false;
|
|
390
|
+
});
|
|
391
|
+
if (found == null) return null;
|
|
392
|
+
final RenderObject? ro = found!.findRenderObject();
|
|
393
|
+
if (ro == null || !ro.attached) return null;
|
|
394
|
+
final info = _buildInfoForElement(found!, ro);
|
|
395
|
+
return (info: info, renderObject: ro);
|
|
396
|
+
} catch (_) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/// Builds info from an element/render-object pair without re-running the
|
|
402
|
+
/// climb logic. Used by [climbFrom] (which already picked the element).
|
|
403
|
+
static DevInspectorInfo _buildInfoForElement(
|
|
404
|
+
Element element,
|
|
405
|
+
RenderObject ro,
|
|
406
|
+
) {
|
|
407
|
+
final typeName = element.widget.runtimeType.toString();
|
|
408
|
+
final Rect rect = _globalBoundingRect(ro);
|
|
409
|
+
final ancestors = _extractAncestors(element);
|
|
410
|
+
final properties = _extractProperties(element);
|
|
411
|
+
final screenHint = _extractScreenHint(element, ancestors);
|
|
412
|
+
final semanticWidget = _extractSemanticWidget(typeName, ancestors);
|
|
413
|
+
final searchHints = _extractSearchHints(
|
|
414
|
+
widgetType: typeName,
|
|
415
|
+
properties: properties,
|
|
416
|
+
ancestors: ancestors,
|
|
417
|
+
screenHint: screenHint,
|
|
418
|
+
semanticWidget: semanticWidget,
|
|
419
|
+
);
|
|
420
|
+
return DevInspectorInfo(
|
|
421
|
+
widgetType: typeName,
|
|
422
|
+
boundingBox: rect,
|
|
423
|
+
properties: properties,
|
|
424
|
+
ancestors: ancestors,
|
|
425
|
+
screenHint: screenHint,
|
|
426
|
+
semanticWidget: semanticWidget,
|
|
427
|
+
searchHints: searchHints,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
315
430
|
}
|
|
@@ -50,6 +50,7 @@ abstract final class KasyIcons {
|
|
|
50
50
|
static const IconData download = LucideIcons.download300;
|
|
51
51
|
static const IconData linkOutlined = LucideIcons.link300;
|
|
52
52
|
static const IconData message = LucideIcons.messageSquare300;
|
|
53
|
+
static const IconData monitor = LucideIcons.monitor300;
|
|
53
54
|
static const IconData moreVert = LucideIcons.ellipsisVertical300;
|
|
54
55
|
static const IconData microphone = LucideIcons.mic300;
|
|
55
56
|
static const IconData northEast = LucideIcons.arrowUpRight300;
|
|
@@ -109,7 +109,9 @@ class KasyColors extends ThemeExtension<KasyColors> {
|
|
|
109
109
|
factory KasyColors.light() => const KasyColors(
|
|
110
110
|
primary: Color(0xFF1E88E5),
|
|
111
111
|
onPrimary: Color(0xFFFFFFFF),
|
|
112
|
-
//
|
|
112
|
+
// Also used by the native splash. When changing, update
|
|
113
|
+
// `flutter_native_splash.color` in pubspec.yaml and run
|
|
114
|
+
// `dart run flutter_native_splash:create`.
|
|
113
115
|
background: Color(0xFFF7F7F7),
|
|
114
116
|
onBackground: Color(0xFF000000),
|
|
115
117
|
surface: Color(0xFFFFFFFF),
|
|
@@ -138,7 +140,9 @@ class KasyColors extends ThemeExtension<KasyColors> {
|
|
|
138
140
|
factory KasyColors.dark() => const KasyColors(
|
|
139
141
|
primary: Color(0xFF1E88E5),
|
|
140
142
|
onPrimary: Color(0xFFFFFFFF),
|
|
141
|
-
//
|
|
143
|
+
// Also used by the native splash. When changing, update
|
|
144
|
+
// `flutter_native_splash.color_dark` in pubspec.yaml and run
|
|
145
|
+
// `dart run flutter_native_splash:create`.
|
|
142
146
|
background: Color(0xFF060608),
|
|
143
147
|
onBackground: Color(0xFFFAFAFA),
|
|
144
148
|
surface: Color(0xFF18181B),
|
|
@@ -5,9 +5,9 @@ import 'package:device_preview/device_preview.dart';
|
|
|
5
5
|
import 'package:flutter/foundation.dart';
|
|
6
6
|
import 'package:flutter/material.dart';
|
|
7
7
|
import 'package:flutter/rendering.dart';
|
|
8
|
+
import 'package:flutter/services.dart';
|
|
8
9
|
import 'package:jiffy/jiffy.dart';
|
|
9
10
|
import 'package:kasy_kit/core/dev_inspector/dev_inspector.dart';
|
|
10
|
-
import 'package:kasy_kit/core/theme/theme.dart';
|
|
11
11
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
12
12
|
import 'package:provider/provider.dart';
|
|
13
13
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
@@ -25,6 +25,16 @@ const String _textScalePrefKey = 'web_device_preview_text_scale';
|
|
|
25
25
|
final ValueNotifier<bool> webDevicePreviewEnabledNotifier =
|
|
26
26
|
ValueNotifier<bool>(false);
|
|
27
27
|
|
|
28
|
+
/// Keyboard shortcut for toggling the web device preview chrome, formatted
|
|
29
|
+
/// for the current platform. Key names stay in English regardless of the
|
|
30
|
+
/// app locale — universal keyboard conventions.
|
|
31
|
+
String webDevicePreviewShortcutLabel() {
|
|
32
|
+
final String mod = defaultTargetPlatform == TargetPlatform.macOS
|
|
33
|
+
? 'Command'
|
|
34
|
+
: 'Ctrl';
|
|
35
|
+
return '$mod + Shift + D';
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
// Platform order: 0 = iOS, 1 = Android, 2 = iPad
|
|
29
39
|
|
|
30
40
|
// iOS: compact → standard → pro
|
|
@@ -76,7 +86,8 @@ class WebDevicePreview extends StatefulWidget {
|
|
|
76
86
|
State<WebDevicePreview> createState() => _WebDevicePreviewState();
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
89
|
+
class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
90
|
+
with WidgetsBindingObserver {
|
|
80
91
|
int _platform = 0; // 0 = iOS, 1 = Android, 2 = iPad
|
|
81
92
|
int _iosIndex = 1; // default: iPhone 16
|
|
82
93
|
int _androidIndex = 1; // default: Google Pixel 9
|
|
@@ -112,16 +123,52 @@ class _WebDevicePreviewState extends State<WebDevicePreview> {
|
|
|
112
123
|
@override
|
|
113
124
|
void initState() {
|
|
114
125
|
super.initState();
|
|
126
|
+
WidgetsBinding.instance.addObserver(this);
|
|
115
127
|
_localeNotifier = ValueNotifier<AppLocale>(
|
|
116
128
|
LocaleSettings.instance.currentLocale,
|
|
117
129
|
);
|
|
118
130
|
webDevicePreviewEnabledNotifier.addListener(_onEnabledChanged);
|
|
119
131
|
_bgDarkNotifier.addListener(_onBgChanged);
|
|
132
|
+
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
|
120
133
|
unawaited(_bootstrap());
|
|
121
134
|
}
|
|
122
135
|
|
|
136
|
+
@override
|
|
137
|
+
void didChangePlatformBrightness() {
|
|
138
|
+
super.didChangePlatformBrightness();
|
|
139
|
+
// The simulated MediaQuery below forwards the host browser's brightness;
|
|
140
|
+
// rebuild so the app inside the device frame follows OS dark/light when
|
|
141
|
+
// the user's theme is set to "system".
|
|
142
|
+
if (mounted) setState(() {});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Global toggle for the device preview chrome.
|
|
146
|
+
///
|
|
147
|
+
/// `Cmd/Ctrl + Shift + D` — works from anywhere (debug only). Mirrors what
|
|
148
|
+
/// the close button does, persisting the choice to prefs so subsequent
|
|
149
|
+
/// launches respect it.
|
|
150
|
+
bool _handleKeyEvent(KeyEvent event) {
|
|
151
|
+
if (!kDebugMode) return false;
|
|
152
|
+
if (event is! KeyDownEvent) return false;
|
|
153
|
+
if (event.logicalKey != LogicalKeyboardKey.keyD) return false;
|
|
154
|
+
final HardwareKeyboard kb = HardwareKeyboard.instance;
|
|
155
|
+
if (!kb.isShiftPressed) return false;
|
|
156
|
+
if (!(kb.isMetaPressed || kb.isControlPressed)) return false;
|
|
157
|
+
if (kb.isAltPressed) return false;
|
|
158
|
+
final bool turningOn = !webDevicePreviewEnabledNotifier.value;
|
|
159
|
+
webDevicePreviewEnabledNotifier.value = turningOn;
|
|
160
|
+
unawaited(() async {
|
|
161
|
+
final prefs = await SharedPreferences.getInstance();
|
|
162
|
+
await prefs.setBool(webDevicePreviewEnabledPrefKey, turningOn);
|
|
163
|
+
}());
|
|
164
|
+
HapticFeedback.lightImpact();
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
123
168
|
@override
|
|
124
169
|
void dispose() {
|
|
170
|
+
WidgetsBinding.instance.removeObserver(this);
|
|
171
|
+
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
|
125
172
|
webDevicePreviewEnabledNotifier.removeListener(_onEnabledChanged);
|
|
126
173
|
_bgDarkNotifier.removeListener(_onBgChanged);
|
|
127
174
|
_deviceNotifier.dispose();
|
|
@@ -135,6 +182,10 @@ class _WebDevicePreviewState extends State<WebDevicePreview> {
|
|
|
135
182
|
}
|
|
136
183
|
|
|
137
184
|
void _onEnabledChanged() {
|
|
185
|
+
// Our chrome already surfaces inspector state via the pill, so suppress
|
|
186
|
+
// the DevInspector's in-app status pill while the preview is on.
|
|
187
|
+
devInspectorSuppressStatusPillNotifier.value =
|
|
188
|
+
webDevicePreviewEnabledNotifier.value;
|
|
138
189
|
if (webDevicePreviewEnabledNotifier.value) {
|
|
139
190
|
_controlsTimer?.cancel();
|
|
140
191
|
_controlsTimer = Timer(const Duration(milliseconds: 800), () {
|
|
@@ -152,7 +203,10 @@ class _WebDevicePreviewState extends State<WebDevicePreview> {
|
|
|
152
203
|
Future<void> _bootstrap() async {
|
|
153
204
|
final prefs = await SharedPreferences.getInstance();
|
|
154
205
|
|
|
155
|
-
|
|
206
|
+
// Default ON for the first run: most devs hitting `kasy run --web` want to
|
|
207
|
+
// preview their mobile app inside a device frame. Closing the chrome
|
|
208
|
+
// persists the choice, so subsequent launches respect what the user left.
|
|
209
|
+
final savedEnabled = prefs.getBool(webDevicePreviewEnabledPrefKey) ?? true;
|
|
156
210
|
final savedPlatform = prefs.getInt(_platformPrefKey);
|
|
157
211
|
final savedIosIndex = prefs.getInt(_iosIndexPrefKey);
|
|
158
212
|
final savedAndroidIndex = prefs.getInt(_androidIndexPrefKey);
|
|
@@ -327,8 +381,6 @@ class _WebDevicePreviewState extends State<WebDevicePreview> {
|
|
|
327
381
|
@override
|
|
328
382
|
Widget build(BuildContext context) {
|
|
329
383
|
final enabled = webDevicePreviewEnabledNotifier.value;
|
|
330
|
-
// Depend on ThemeProvider so the pill icon updates when app theme changes.
|
|
331
|
-
final appIsDark = ThemeProvider.of(context).mode == ThemeMode.dark;
|
|
332
384
|
|
|
333
385
|
// ThemeData.light() in Flutter 3.x M3 uses a lavender-tinted surface.
|
|
334
386
|
// Passing backgroundColor directly avoids that and gives us an exact color.
|
|
@@ -347,6 +399,11 @@ class _WebDevicePreviewState extends State<WebDevicePreview> {
|
|
|
347
399
|
builder: (ctx, _) => MediaQuery(
|
|
348
400
|
data: MediaQuery.of(ctx).copyWith(
|
|
349
401
|
textScaler: TextScaler.linear(_textScaleNotifier.value),
|
|
402
|
+
// DevicePreview hard-codes platformBrightness to light inside the
|
|
403
|
+
// simulated frame, which breaks MaterialApp's ThemeMode.system.
|
|
404
|
+
// Forward the real host brightness so "system" tracks the OS.
|
|
405
|
+
platformBrightness: WidgetsBinding
|
|
406
|
+
.instance.platformDispatcher.platformBrightness,
|
|
350
407
|
),
|
|
351
408
|
child: _DeviceSwitchBridge(
|
|
352
409
|
deviceNotifier: _deviceNotifier,
|
|
@@ -367,6 +424,14 @@ class _WebDevicePreviewState extends State<WebDevicePreview> {
|
|
|
367
424
|
fit: StackFit.expand,
|
|
368
425
|
children: [
|
|
369
426
|
RepaintBoundary(key: _screenshotKey, child: preview),
|
|
427
|
+
// Inspector highlight drawn ABOVE the device frame so widgets that
|
|
428
|
+
// hug the edges of the simulated viewport (AppBar, bottom nav, …)
|
|
429
|
+
// still get a complete border instead of being clipped by the chrome.
|
|
430
|
+
const Positioned.fill(
|
|
431
|
+
child: IgnorePointer(
|
|
432
|
+
child: _DevInspectorExternalHighlight(),
|
|
433
|
+
),
|
|
434
|
+
),
|
|
370
435
|
Positioned(
|
|
371
436
|
top: 24,
|
|
372
437
|
right: 24,
|
|
@@ -389,7 +454,6 @@ class _WebDevicePreviewState extends State<WebDevicePreview> {
|
|
|
389
454
|
deviceName: _shortName(_currentDevice.name),
|
|
390
455
|
frameVisible: _frameVisibleNotifier.value,
|
|
391
456
|
bgDark: _bgDarkNotifier.value,
|
|
392
|
-
appIsDark: appIsDark,
|
|
393
457
|
isLandscape: _landscapeNotifier.value,
|
|
394
458
|
textScale: _textScaleNotifier.value,
|
|
395
459
|
currentLocale: _localeNotifier.value,
|
|
@@ -399,7 +463,6 @@ class _WebDevicePreviewState extends State<WebDevicePreview> {
|
|
|
399
463
|
onNext: _next,
|
|
400
464
|
onToggleFrame: _toggleFrame,
|
|
401
465
|
onToggleBg: _toggleBg,
|
|
402
|
-
onToggleAppTheme: () => ThemeProvider.of(context).toggle(),
|
|
403
466
|
onToggleLandscape: _toggleLandscape,
|
|
404
467
|
onCycleTextScale: _cycleTextScale,
|
|
405
468
|
onCycleLocale: _cycleLocale,
|
|
@@ -498,7 +561,6 @@ class _PreviewControls extends StatefulWidget {
|
|
|
498
561
|
required this.deviceName,
|
|
499
562
|
required this.frameVisible,
|
|
500
563
|
required this.bgDark,
|
|
501
|
-
required this.appIsDark,
|
|
502
564
|
required this.isLandscape,
|
|
503
565
|
required this.textScale,
|
|
504
566
|
required this.currentLocale,
|
|
@@ -508,7 +570,6 @@ class _PreviewControls extends StatefulWidget {
|
|
|
508
570
|
required this.onNext,
|
|
509
571
|
required this.onToggleFrame,
|
|
510
572
|
required this.onToggleBg,
|
|
511
|
-
required this.onToggleAppTheme,
|
|
512
573
|
required this.onToggleLandscape,
|
|
513
574
|
required this.onCycleTextScale,
|
|
514
575
|
required this.onCycleLocale,
|
|
@@ -522,7 +583,6 @@ class _PreviewControls extends StatefulWidget {
|
|
|
522
583
|
final String deviceName;
|
|
523
584
|
final bool frameVisible;
|
|
524
585
|
final bool bgDark;
|
|
525
|
-
final bool appIsDark;
|
|
526
586
|
final bool isLandscape;
|
|
527
587
|
final double textScale;
|
|
528
588
|
final AppLocale currentLocale;
|
|
@@ -532,7 +592,6 @@ class _PreviewControls extends StatefulWidget {
|
|
|
532
592
|
final VoidCallback onNext;
|
|
533
593
|
final VoidCallback onToggleFrame;
|
|
534
594
|
final VoidCallback onToggleBg;
|
|
535
|
-
final VoidCallback onToggleAppTheme;
|
|
536
595
|
final VoidCallback onToggleLandscape;
|
|
537
596
|
final VoidCallback onCycleTextScale;
|
|
538
597
|
final VoidCallback onCycleLocale;
|
|
@@ -895,14 +954,6 @@ class _PreviewControlsState extends State<_PreviewControls> {
|
|
|
895
954
|
checked: widget.bgDark,
|
|
896
955
|
onTap: widget.onToggleBg,
|
|
897
956
|
),
|
|
898
|
-
_ToolsItem(
|
|
899
|
-
icon: widget.appIsDark
|
|
900
|
-
? Icons.dark_mode_rounded
|
|
901
|
-
: Icons.light_mode_rounded,
|
|
902
|
-
label: t.webDevicePreview.darkTheme,
|
|
903
|
-
checked: widget.appIsDark,
|
|
904
|
-
onTap: widget.onToggleAppTheme,
|
|
905
|
-
),
|
|
906
957
|
_ToolsItem(
|
|
907
958
|
icon: widget.isLandscape
|
|
908
959
|
? Icons.stay_current_landscape_rounded
|
|
@@ -1158,3 +1209,52 @@ class _IconBtnState extends State<_IconBtn> {
|
|
|
1158
1209
|
);
|
|
1159
1210
|
}
|
|
1160
1211
|
}
|
|
1212
|
+
|
|
1213
|
+
// ---------------------------------------------------------------------------
|
|
1214
|
+
// External inspector highlight — draws the DevInspector selection rect ABOVE
|
|
1215
|
+
// the simulated device frame so widgets that hug the viewport edges keep a
|
|
1216
|
+
// complete, uncropped border. Listens to the global rect notifier published
|
|
1217
|
+
// by DevInspector.
|
|
1218
|
+
// ---------------------------------------------------------------------------
|
|
1219
|
+
|
|
1220
|
+
class _DevInspectorExternalHighlight extends StatelessWidget {
|
|
1221
|
+
const _DevInspectorExternalHighlight();
|
|
1222
|
+
|
|
1223
|
+
@override
|
|
1224
|
+
Widget build(BuildContext context) {
|
|
1225
|
+
return ValueListenableBuilder<Rect?>(
|
|
1226
|
+
valueListenable: devInspectorHighlightGlobalRect,
|
|
1227
|
+
builder: (BuildContext context, Rect? rect, Widget? _) {
|
|
1228
|
+
if (rect == null || rect.isEmpty) return const SizedBox.shrink();
|
|
1229
|
+
return CustomPaint(
|
|
1230
|
+
painter: _ExternalHighlightPainter(rect: rect),
|
|
1231
|
+
size: Size.infinite,
|
|
1232
|
+
);
|
|
1233
|
+
},
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
class _ExternalHighlightPainter extends CustomPainter {
|
|
1239
|
+
_ExternalHighlightPainter({required this.rect});
|
|
1240
|
+
|
|
1241
|
+
final Rect rect;
|
|
1242
|
+
|
|
1243
|
+
@override
|
|
1244
|
+
void paint(Canvas canvas, Size size) {
|
|
1245
|
+
if (rect.isEmpty) return;
|
|
1246
|
+
final Paint paint = Paint()
|
|
1247
|
+
..color = devInspectorHighlightColor
|
|
1248
|
+
..style = PaintingStyle.stroke
|
|
1249
|
+
..strokeWidth = devInspectorHighlightStrokeWidth;
|
|
1250
|
+
final Rect outer = rect.inflate(devInspectorHighlightStrokeWidth / 2);
|
|
1251
|
+
final RRect rrect = RRect.fromRectAndRadius(
|
|
1252
|
+
outer,
|
|
1253
|
+
const Radius.circular(devInspectorHighlightCornerRadius),
|
|
1254
|
+
);
|
|
1255
|
+
canvas.drawRRect(rrect, paint);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
@override
|
|
1259
|
+
bool shouldRepaint(_ExternalHighlightPainter old) => old.rect != rect;
|
|
1260
|
+
}
|