kasy-cli 1.19.1 → 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 +22 -6
- 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/flutter-run.js +16 -4
- package/lib/utils/i18n/messages-en.js +2 -1
- package/lib/utils/i18n/messages-es.js +2 -1
- package/lib/utils/i18n/messages-pt.js +2 -1
- 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 +39 -29
- 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 +439 -232
- 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 +202 -79
- 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
|
@@ -6,29 +6,29 @@ import 'package:flutter/scheduler.dart';
|
|
|
6
6
|
import 'package:flutter/services.dart';
|
|
7
7
|
import 'package:kasy_kit/core/dev_inspector/dev_inspector_info.dart';
|
|
8
8
|
import 'package:kasy_kit/core/dev_inspector/dev_inspector_service.dart';
|
|
9
|
-
import 'package:kasy_kit/core/icons/kasy_icons.dart';
|
|
10
|
-
import 'package:kasy_kit/core/theme/colors.dart';
|
|
11
|
-
import 'package:kasy_kit/core/theme/extensions/theme_extension.dart';
|
|
12
9
|
import 'package:kasy_kit/core/theme/providers/theme_provider.dart';
|
|
13
10
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
14
11
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
/// Same lime used by the Kasy CLI brand color (Tailwind lime-500). Balanced
|
|
14
|
+
/// for visibility on both light and dark surfaces.
|
|
15
|
+
const Color _highlightColor = Color(0xFF84CC16);
|
|
17
16
|
const double _highlightStrokeWidth = 2.0;
|
|
18
17
|
const double _highlightCornerRadius = 4.0;
|
|
19
18
|
|
|
20
19
|
final GlobalKey<ScaffoldMessengerState> devInspectorRootScaffoldMessengerKey =
|
|
21
20
|
GlobalKey<ScaffoldMessengerState>(debugLabel: 'devInspectorRoot');
|
|
22
21
|
|
|
23
|
-
/// Persisted
|
|
24
|
-
|
|
22
|
+
/// Persisted master switch flipped from the admin settings sheet. When `true`
|
|
23
|
+
/// the inspector is permanently armed (`devInspectorActiveNotifier` follows it).
|
|
24
|
+
/// Esc/pill toggles flip this same flag, which is then persisted to prefs so
|
|
25
|
+
/// the next launch matches what the user left.
|
|
26
|
+
///
|
|
27
|
+
/// Key kept as `dev_inspector_fab_enabled` for backwards compatibility with
|
|
28
|
+
/// users who already had the toggle on before the FAB was removed.
|
|
29
|
+
const String devInspectorEnabledPrefKey = 'dev_inspector_fab_enabled';
|
|
25
30
|
|
|
26
|
-
final ValueNotifier<bool>
|
|
27
|
-
ValueNotifier<bool>(false);
|
|
28
|
-
|
|
29
|
-
/// Set to true to reveal the FAB immediately (bypasses the startup delay).
|
|
30
|
-
/// [DevInspector] resets it back to false after consuming the signal.
|
|
31
|
-
final ValueNotifier<bool> devInspectorRevealNowNotifier =
|
|
31
|
+
final ValueNotifier<bool> devInspectorEnabledNotifier =
|
|
32
32
|
ValueNotifier<bool>(false);
|
|
33
33
|
|
|
34
34
|
/// Set to true to trigger a copy of the currently selected widget.
|
|
@@ -36,12 +36,40 @@ final ValueNotifier<bool> devInspectorRevealNowNotifier =
|
|
|
36
36
|
final ValueNotifier<bool> devInspectorCopyTriggerNotifier =
|
|
37
37
|
ValueNotifier<bool>(false);
|
|
38
38
|
|
|
39
|
-
///
|
|
40
|
-
/// the Web Device Preview pill
|
|
41
|
-
///
|
|
39
|
+
/// Runtime active state of the inspector. Mirrors [devInspectorEnabledNotifier]
|
|
40
|
+
/// — the Web Device Preview pill, the admin toggle and the Esc shortcut all
|
|
41
|
+
/// flip the persisted notifier, and this one follows.
|
|
42
42
|
final ValueNotifier<bool> devInspectorActiveNotifier =
|
|
43
43
|
ValueNotifier<bool>(false);
|
|
44
44
|
|
|
45
|
+
/// Set to true to hide the in-app status pill that the [DevInspector] shows
|
|
46
|
+
/// while active (e.g. when the WebDevicePreview chrome is already displaying
|
|
47
|
+
/// its own inspector state).
|
|
48
|
+
final ValueNotifier<bool> devInspectorSuppressStatusPillNotifier =
|
|
49
|
+
ValueNotifier<bool>(false);
|
|
50
|
+
|
|
51
|
+
/// Rect of the currently selected widget expressed in **root view coordinates**
|
|
52
|
+
/// (the browser window / native window). External surfaces — e.g. the Web
|
|
53
|
+
/// Device Preview chrome which renders OUTSIDE the device frame — listen to
|
|
54
|
+
/// this and draw the highlight above the frame so it stays visible even when
|
|
55
|
+
/// the selected widget hugs the edges of the simulated device.
|
|
56
|
+
final ValueNotifier<Rect?> devInspectorHighlightGlobalRect =
|
|
57
|
+
ValueNotifier<Rect?>(null);
|
|
58
|
+
|
|
59
|
+
const Color devInspectorHighlightColor = _highlightColor;
|
|
60
|
+
const double devInspectorHighlightStrokeWidth = _highlightStrokeWidth;
|
|
61
|
+
const double devInspectorHighlightCornerRadius = _highlightCornerRadius;
|
|
62
|
+
|
|
63
|
+
/// Keyboard shortcut for toggling the inspector, formatted for the current
|
|
64
|
+
/// platform. Key names are kept in English regardless of the app locale —
|
|
65
|
+
/// "Command", "Ctrl" and "Shift" are universal keyboard conventions.
|
|
66
|
+
String devInspectorShortcutLabel() {
|
|
67
|
+
final String mod = defaultTargetPlatform == TargetPlatform.macOS
|
|
68
|
+
? 'Command'
|
|
69
|
+
: 'Ctrl';
|
|
70
|
+
return '$mod + Shift + P';
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
class DevInspector extends StatefulWidget {
|
|
46
74
|
const DevInspector({super.key, required this.child});
|
|
47
75
|
|
|
@@ -57,55 +85,59 @@ class DevInspector extends StatefulWidget {
|
|
|
57
85
|
}
|
|
58
86
|
|
|
59
87
|
class _DevInspectorState extends State<DevInspector>
|
|
60
|
-
with
|
|
61
|
-
static const Duration _fabRevealDelay = Duration(seconds: 2);
|
|
62
|
-
static const Duration _fabDeemphasizeDelay = Duration(seconds: 2);
|
|
63
|
-
static const Duration _fabOpacityAnimation = Duration(milliseconds: 700);
|
|
64
|
-
static const double _fabOpacityIdle = 0.75;
|
|
65
|
-
static const double _fabOpacityInspectorOn = 1.0;
|
|
88
|
+
with TickerProviderStateMixin {
|
|
66
89
|
static const Duration _copyFeedbackVisible = Duration(milliseconds: 2200);
|
|
67
|
-
|
|
68
|
-
static const
|
|
90
|
+
static const double _copyFeedbackRightInset = 16;
|
|
91
|
+
static const int _historyCapacity = 20;
|
|
69
92
|
|
|
70
93
|
bool _active = false;
|
|
71
94
|
bool _copyBusy = false;
|
|
72
95
|
|
|
73
|
-
double? _fabDx;
|
|
74
|
-
double? _fabDy;
|
|
75
|
-
bool _dragging = false;
|
|
76
|
-
|
|
77
|
-
Timer? _revealTimer;
|
|
78
|
-
Timer? _fabDeemphasizeTimer;
|
|
79
96
|
Timer? _copyFeedbackTimer;
|
|
80
97
|
String? _copyFeedbackText;
|
|
81
98
|
bool _copyFeedbackIsError = false;
|
|
82
|
-
bool _fabLayerMounted = false;
|
|
83
|
-
bool _fabFadeIn = false;
|
|
84
|
-
bool _fabEmphasized = false;
|
|
85
99
|
|
|
86
100
|
DevInspectorInfo? _selectedInfo;
|
|
87
101
|
RenderObject? _selectedRender;
|
|
88
102
|
Rect? _highlightRect;
|
|
89
103
|
Ticker? _highlightTicker;
|
|
104
|
+
late final AnimationController _transitionCtrl;
|
|
105
|
+
Rect? _transitionFromRect;
|
|
106
|
+
Rect? _transitionToRect;
|
|
107
|
+
Rect? _transitionFromGlobalRect;
|
|
108
|
+
Rect? _transitionToGlobalRect;
|
|
109
|
+
final GlobalKey _overlayKey = GlobalKey(debugLabel: 'devInspectorOverlay');
|
|
110
|
+
final GlobalKey _contentKey = GlobalKey(debugLabel: 'devInspectorContent');
|
|
111
|
+
|
|
112
|
+
/// Past selections, oldest → newest. [_historyCursor] points at the current
|
|
113
|
+
/// position; ← and → keys walk this list. Cleared when the inspector is
|
|
114
|
+
/// disabled.
|
|
115
|
+
final List<({DevInspectorInfo info, RenderObject renderObject})> _history =
|
|
116
|
+
[];
|
|
117
|
+
int _historyCursor = -1;
|
|
118
|
+
bool _navigatingHistory = false;
|
|
90
119
|
|
|
91
120
|
@override
|
|
92
121
|
void initState() {
|
|
93
122
|
super.initState();
|
|
123
|
+
_transitionCtrl = AnimationController(
|
|
124
|
+
vsync: this,
|
|
125
|
+
duration: const Duration(milliseconds: 140),
|
|
126
|
+
)..addListener(_handleTransitionTick);
|
|
94
127
|
devInspectorActiveNotifier.addListener(_handleActiveChanged);
|
|
95
|
-
|
|
96
|
-
devInspectorRevealNowNotifier.addListener(_onRevealNow);
|
|
128
|
+
devInspectorEnabledNotifier.addListener(_handleEnabledChanged);
|
|
97
129
|
devInspectorCopyTriggerNotifier.addListener(_onCopyTriggered);
|
|
98
|
-
|
|
130
|
+
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
|
131
|
+
unawaited(_bootstrapEnabledPreference());
|
|
99
132
|
}
|
|
100
133
|
|
|
101
134
|
@override
|
|
102
135
|
void dispose() {
|
|
103
|
-
_revealTimer?.cancel();
|
|
104
|
-
_fabDeemphasizeTimer?.cancel();
|
|
105
136
|
_copyFeedbackTimer?.cancel();
|
|
106
137
|
_highlightTicker?.dispose();
|
|
107
|
-
|
|
108
|
-
|
|
138
|
+
_transitionCtrl.dispose();
|
|
139
|
+
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
|
140
|
+
devInspectorEnabledNotifier.removeListener(_handleEnabledChanged);
|
|
109
141
|
devInspectorCopyTriggerNotifier.removeListener(_onCopyTriggered);
|
|
110
142
|
devInspectorActiveNotifier.removeListener(_handleActiveChanged);
|
|
111
143
|
if (devInspectorActiveNotifier.value) {
|
|
@@ -114,103 +146,170 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
114
146
|
super.dispose();
|
|
115
147
|
}
|
|
116
148
|
|
|
117
|
-
|
|
118
|
-
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
149
|
+
void _handleTransitionTick() {
|
|
119
150
|
if (!mounted) return;
|
|
120
|
-
final
|
|
121
|
-
final
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
151
|
+
final Rect? from = _transitionFromRect;
|
|
152
|
+
final Rect? to = _transitionToRect;
|
|
153
|
+
if (from == null || to == null) return;
|
|
154
|
+
final double t = Curves.easeOutCubic.transform(_transitionCtrl.value);
|
|
155
|
+
setState(() => _highlightRect = Rect.lerp(from, to, t));
|
|
156
|
+
|
|
157
|
+
final Rect? gFrom = _transitionFromGlobalRect;
|
|
158
|
+
final Rect? gTo = _transitionToGlobalRect;
|
|
159
|
+
if (gFrom != null && gTo != null) {
|
|
160
|
+
devInspectorHighlightGlobalRect.value = Rect.lerp(gFrom, gTo, t);
|
|
125
161
|
}
|
|
126
162
|
}
|
|
127
163
|
|
|
128
|
-
|
|
129
|
-
|
|
164
|
+
/// Global keyboard shortcuts. The "wake up" combo works from anywhere,
|
|
165
|
+
/// even when the inspector is off; the rest only fire when it's active.
|
|
166
|
+
/// • Cmd/Ctrl + Shift + P → toggle inspector on/off (debug only)
|
|
167
|
+
/// • Esc → deactivate (and persist OFF)
|
|
168
|
+
/// • C → copy the selected widget (no modifier; the
|
|
169
|
+
/// usual Cmd/Ctrl+C still copies text elsewhere)
|
|
170
|
+
/// • ← / → → step backward / forward through history
|
|
171
|
+
bool _handleKeyEvent(KeyEvent event) {
|
|
172
|
+
if (event is! KeyDownEvent) return false;
|
|
173
|
+
final HardwareKeyboard kb = HardwareKeyboard.instance;
|
|
174
|
+
|
|
175
|
+
// Cmd/Ctrl + Shift + P — works regardless of active state.
|
|
176
|
+
if (kDebugMode &&
|
|
177
|
+
event.logicalKey == LogicalKeyboardKey.keyP &&
|
|
178
|
+
(kb.isMetaPressed || kb.isControlPressed) &&
|
|
179
|
+
kb.isShiftPressed &&
|
|
180
|
+
!kb.isAltPressed) {
|
|
181
|
+
_toggleInspectorFromShortcut();
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!_active) return false;
|
|
186
|
+
|
|
187
|
+
final bool hasModifier = kb.isMetaPressed ||
|
|
188
|
+
kb.isControlPressed ||
|
|
189
|
+
kb.isAltPressed ||
|
|
190
|
+
kb.isShiftPressed;
|
|
191
|
+
|
|
192
|
+
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
|
193
|
+
// Esc must turn the inspector off no matter HOW it was activated:
|
|
194
|
+
// • Admin toggle → flip Enabled (and let it cascade to Active).
|
|
195
|
+
// • WebDevicePreview pill → only Active is set; flip it directly.
|
|
196
|
+
// We touch both so ValueNotifier always observes a real change.
|
|
197
|
+
if (devInspectorEnabledNotifier.value) {
|
|
198
|
+
devInspectorEnabledNotifier.value = false;
|
|
199
|
+
}
|
|
200
|
+
if (devInspectorActiveNotifier.value) {
|
|
201
|
+
devInspectorActiveNotifier.value = false;
|
|
202
|
+
}
|
|
203
|
+
HapticFeedback.lightImpact();
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (event.logicalKey == LogicalKeyboardKey.keyC && !hasModifier) {
|
|
207
|
+
if (_selectedInfo == null) return false;
|
|
208
|
+
unawaited(_copySelection());
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (event.logicalKey == LogicalKeyboardKey.arrowLeft && !hasModifier) {
|
|
212
|
+
return _stepHistory(-1);
|
|
213
|
+
}
|
|
214
|
+
if (event.logicalKey == LogicalKeyboardKey.arrowRight && !hasModifier) {
|
|
215
|
+
return _stepHistory(1);
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
130
218
|
}
|
|
131
219
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
220
|
+
/// Flip the inspector on or off via the global shortcut. Mirrors what the
|
|
221
|
+
/// admin toggle does on the ON side, and what Esc does on the OFF side.
|
|
222
|
+
void _toggleInspectorFromShortcut() {
|
|
223
|
+
final bool turningOn = !_active;
|
|
224
|
+
if (turningOn) {
|
|
225
|
+
devInspectorEnabledNotifier.value = true;
|
|
226
|
+
} else {
|
|
227
|
+
if (devInspectorEnabledNotifier.value) {
|
|
228
|
+
devInspectorEnabledNotifier.value = false;
|
|
229
|
+
}
|
|
230
|
+
if (devInspectorActiveNotifier.value) {
|
|
231
|
+
devInspectorActiveNotifier.value = false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
HapticFeedback.lightImpact();
|
|
137
235
|
}
|
|
138
236
|
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
237
|
+
bool _stepHistory(int delta) {
|
|
238
|
+
if (_history.isEmpty) return false;
|
|
239
|
+
final int next = _historyCursor + delta;
|
|
240
|
+
if (next < 0 || next >= _history.length) return false;
|
|
241
|
+
_historyCursor = next;
|
|
242
|
+
final entry = _history[next];
|
|
243
|
+
if (!entry.renderObject.attached) return false;
|
|
244
|
+
|
|
245
|
+
final Rect? newRect = _rectInOverlaySpace(entry.renderObject);
|
|
246
|
+
final Rect? newGlobalRect = _rectInRootSpace(entry.renderObject);
|
|
247
|
+
final Rect? fromRect = _highlightRect;
|
|
248
|
+
final Rect? fromGlobalRect = devInspectorHighlightGlobalRect.value;
|
|
249
|
+
|
|
250
|
+
_navigatingHistory = true;
|
|
144
251
|
setState(() {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
});
|
|
148
|
-
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
149
|
-
if (!mounted) return;
|
|
150
|
-
setState(() => _fabFadeIn = true);
|
|
252
|
+
_selectedInfo = entry.info;
|
|
253
|
+
_selectedRender = entry.renderObject;
|
|
151
254
|
});
|
|
255
|
+
_animateHighlightTo(
|
|
256
|
+
fromRect: fromRect,
|
|
257
|
+
toRect: newRect,
|
|
258
|
+
fromGlobalRect: fromGlobalRect,
|
|
259
|
+
toGlobalRect: newGlobalRect,
|
|
260
|
+
);
|
|
261
|
+
_navigatingHistory = false;
|
|
262
|
+
HapticFeedback.selectionClick();
|
|
263
|
+
return true;
|
|
152
264
|
}
|
|
153
265
|
|
|
154
|
-
void
|
|
155
|
-
|
|
156
|
-
_fabDeemphasizeTimer?.cancel();
|
|
266
|
+
Future<void> _bootstrapEnabledPreference() async {
|
|
267
|
+
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
157
268
|
if (!mounted) return;
|
|
269
|
+
final bool enabled = prefs.getBool(devInspectorEnabledPrefKey) ?? false;
|
|
270
|
+
if (devInspectorEnabledNotifier.value != enabled) {
|
|
271
|
+
devInspectorEnabledNotifier.value = enabled;
|
|
272
|
+
} else {
|
|
273
|
+
_applyEnabled(enabled);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
Future<void> _persistEnabled(bool value) async {
|
|
278
|
+
try {
|
|
279
|
+
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
280
|
+
await prefs.setBool(devInspectorEnabledPrefKey, value);
|
|
281
|
+
} catch (_) {
|
|
282
|
+
// Best-effort persistence — don't crash the inspector if prefs fail.
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
void _handleEnabledChanged() {
|
|
287
|
+
_applyEnabled(devInspectorEnabledNotifier.value);
|
|
288
|
+
unawaited(_persistEnabled(devInspectorEnabledNotifier.value));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
void _applyEnabled(bool enabled) {
|
|
292
|
+
if (!mounted) return;
|
|
293
|
+
// Master switch directly drives the runtime active flag — no FAB stage.
|
|
294
|
+
if (devInspectorActiveNotifier.value != enabled) {
|
|
295
|
+
devInspectorActiveNotifier.value = enabled;
|
|
296
|
+
}
|
|
158
297
|
if (!enabled) {
|
|
159
|
-
if (_active) {
|
|
160
|
-
devInspectorActiveNotifier.value = false;
|
|
161
|
-
}
|
|
162
298
|
_copyFeedbackTimer?.cancel();
|
|
163
299
|
setState(() {
|
|
164
300
|
_active = false;
|
|
165
|
-
_fabLayerMounted = false;
|
|
166
|
-
_fabFadeIn = false;
|
|
167
|
-
_fabEmphasized = false;
|
|
168
|
-
_dragging = false;
|
|
169
301
|
_copyFeedbackText = null;
|
|
170
302
|
_copyFeedbackIsError = false;
|
|
171
303
|
_clearSelection();
|
|
172
304
|
});
|
|
173
|
-
_copyFeedbackTimer?.cancel();
|
|
174
|
-
return;
|
|
175
305
|
}
|
|
176
|
-
setState(() {
|
|
177
|
-
_fabLayerMounted = false;
|
|
178
|
-
_fabFadeIn = false;
|
|
179
|
-
});
|
|
180
|
-
_revealTimer = Timer(_fabRevealDelay, _onFabRevealDelayElapsed);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
void _onFabRevealDelayElapsed() {
|
|
184
|
-
if (!mounted) return;
|
|
185
|
-
setState(() {
|
|
186
|
-
_fabLayerMounted = true;
|
|
187
|
-
_fabFadeIn = false;
|
|
188
|
-
});
|
|
189
|
-
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
190
|
-
if (!mounted) return;
|
|
191
|
-
setState(() => _fabFadeIn = true);
|
|
192
|
-
});
|
|
193
306
|
}
|
|
194
307
|
|
|
195
|
-
void
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
_fabDeemphasizeTimer = null;
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
double get _fabAnimatedOpacity {
|
|
205
|
-
if (!_fabLayerMounted) return 0.0;
|
|
206
|
-
if (!_fabFadeIn) return 0.0;
|
|
207
|
-
if (_dragging || _copyBusy || _fabEmphasized) return 1.0;
|
|
208
|
-
return _active ? _fabOpacityInspectorOn : _fabOpacityIdle;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
void _setInspectorActive(bool value) {
|
|
212
|
-
devInspectorActiveNotifier.value = value;
|
|
213
|
-
HapticFeedback.lightImpact();
|
|
308
|
+
void _onCopyTriggered() {
|
|
309
|
+
if (!devInspectorCopyTriggerNotifier.value) return;
|
|
310
|
+
devInspectorCopyTriggerNotifier.value = false;
|
|
311
|
+
if (!_active) return;
|
|
312
|
+
unawaited(_copySelection());
|
|
214
313
|
}
|
|
215
314
|
|
|
216
315
|
void _handleActiveChanged() {
|
|
@@ -227,24 +326,140 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
227
326
|
_selectedInfo = null;
|
|
228
327
|
_selectedRender = null;
|
|
229
328
|
_highlightRect = null;
|
|
329
|
+
_transitionFromRect = null;
|
|
330
|
+
_transitionToRect = null;
|
|
331
|
+
_transitionFromGlobalRect = null;
|
|
332
|
+
_transitionToGlobalRect = null;
|
|
333
|
+
if (_transitionCtrl.isAnimating) _transitionCtrl.stop();
|
|
230
334
|
_stopHighlightTicker();
|
|
335
|
+
devInspectorHighlightGlobalRect.value = null;
|
|
336
|
+
_history.clear();
|
|
337
|
+
_historyCursor = -1;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
void _pushHistory(DevInspectorInfo info, RenderObject renderObject) {
|
|
341
|
+
// Drop anything ahead of the cursor — selecting a new widget after
|
|
342
|
+
// pressing ← invalidates the "forward" history (same as a browser).
|
|
343
|
+
if (_historyCursor < _history.length - 1) {
|
|
344
|
+
_history.removeRange(_historyCursor + 1, _history.length);
|
|
345
|
+
}
|
|
346
|
+
_history.add((info: info, renderObject: renderObject));
|
|
347
|
+
if (_history.length > _historyCapacity) {
|
|
348
|
+
_history.removeAt(0);
|
|
349
|
+
}
|
|
350
|
+
_historyCursor = _history.length - 1;
|
|
231
351
|
}
|
|
232
352
|
|
|
233
353
|
void _onInspectorTap(Offset globalPosition) {
|
|
234
|
-
final
|
|
354
|
+
final RenderObject? content = _contentKey.currentContext?.findRenderObject();
|
|
355
|
+
if (content is! RenderBox) {
|
|
356
|
+
HapticFeedback.heavyImpact();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
var picked = DevInspectorService.pickAtInBox(content, globalPosition);
|
|
235
360
|
if (picked == null) {
|
|
236
361
|
HapticFeedback.heavyImpact();
|
|
237
362
|
return;
|
|
238
363
|
}
|
|
364
|
+
|
|
365
|
+
// Repeat-click bubbles the selection up: if the tap landed on the SAME
|
|
366
|
+
// widget that's already selected, climb one level up the meaningful
|
|
367
|
+
// hierarchy instead of re-selecting it.
|
|
368
|
+
if (_selectedRender != null &&
|
|
369
|
+
identical(picked.renderObject, _selectedRender)) {
|
|
370
|
+
final climbed = DevInspectorService.climbFrom(picked.renderObject);
|
|
371
|
+
if (climbed != null) picked = climbed;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
final Rect? newRect = _rectInOverlaySpace(picked.renderObject);
|
|
375
|
+
final Rect? newGlobalRect = _rectInRootSpace(picked.renderObject);
|
|
376
|
+
final Rect? fromRect = _highlightRect;
|
|
377
|
+
final Rect? fromGlobalRect = devInspectorHighlightGlobalRect.value;
|
|
378
|
+
|
|
239
379
|
setState(() {
|
|
240
|
-
_selectedInfo = picked
|
|
380
|
+
_selectedInfo = picked!.info;
|
|
241
381
|
_selectedRender = picked.renderObject;
|
|
242
|
-
_highlightRect = picked.info.boundingBox;
|
|
243
382
|
});
|
|
244
|
-
|
|
383
|
+
|
|
384
|
+
if (!_navigatingHistory) {
|
|
385
|
+
_pushHistory(picked.info, picked.renderObject);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
_animateHighlightTo(
|
|
389
|
+
fromRect: fromRect,
|
|
390
|
+
toRect: newRect,
|
|
391
|
+
fromGlobalRect: fromGlobalRect,
|
|
392
|
+
toGlobalRect: newGlobalRect,
|
|
393
|
+
);
|
|
245
394
|
HapticFeedback.selectionClick();
|
|
246
395
|
}
|
|
247
396
|
|
|
397
|
+
/// Animates the highlight from [fromRect] to [toRect] in ~140 ms, then hands
|
|
398
|
+
/// off to the per-frame ticker (which keeps the highlight glued to the
|
|
399
|
+
/// widget if it animates internally). If there's no previous rect or no
|
|
400
|
+
/// change in geometry, the new rect is applied instantly.
|
|
401
|
+
void _animateHighlightTo({
|
|
402
|
+
required Rect? fromRect,
|
|
403
|
+
required Rect? toRect,
|
|
404
|
+
required Rect? fromGlobalRect,
|
|
405
|
+
required Rect? toGlobalRect,
|
|
406
|
+
}) {
|
|
407
|
+
_stopHighlightTicker();
|
|
408
|
+
if (_transitionCtrl.isAnimating) _transitionCtrl.stop();
|
|
409
|
+
|
|
410
|
+
if (fromRect == null || toRect == null || fromRect == toRect) {
|
|
411
|
+
setState(() => _highlightRect = toRect);
|
|
412
|
+
devInspectorHighlightGlobalRect.value = toGlobalRect;
|
|
413
|
+
if (toRect != null && _selectedRender != null) _startHighlightTicker();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
_transitionFromRect = fromRect;
|
|
418
|
+
_transitionToRect = toRect;
|
|
419
|
+
_transitionFromGlobalRect = fromGlobalRect;
|
|
420
|
+
_transitionToGlobalRect = toGlobalRect;
|
|
421
|
+
_transitionCtrl.forward(from: 0.0).whenComplete(() {
|
|
422
|
+
if (!mounted) return;
|
|
423
|
+
if (_transitionCtrl.status != AnimationStatus.completed) return;
|
|
424
|
+
setState(() => _highlightRect = toRect);
|
|
425
|
+
devInspectorHighlightGlobalRect.value = toGlobalRect;
|
|
426
|
+
_transitionFromRect = null;
|
|
427
|
+
_transitionToRect = null;
|
|
428
|
+
_transitionFromGlobalRect = null;
|
|
429
|
+
_transitionToGlobalRect = null;
|
|
430
|
+
if (_selectedRender != null) _startHighlightTicker();
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/// Computes the bounds of [target] in the overlay's local coordinate space,
|
|
435
|
+
/// walking the paint-transform matrix directly. This works regardless of any
|
|
436
|
+
/// transforms applied above the overlay (e.g. WebDevicePreview's scale).
|
|
437
|
+
Rect? _rectInOverlaySpace(RenderObject target) {
|
|
438
|
+
if (!target.attached) return null;
|
|
439
|
+
final RenderObject? overlay =
|
|
440
|
+
_overlayKey.currentContext?.findRenderObject();
|
|
441
|
+
if (overlay == null || !overlay.attached) return null;
|
|
442
|
+
try {
|
|
443
|
+
final Matrix4 transform = target.getTransformTo(overlay);
|
|
444
|
+
return MatrixUtils.transformRect(transform, target.paintBounds);
|
|
445
|
+
} catch (_) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/// Computes the bounds of [target] in **root view coordinates** (the browser
|
|
451
|
+
/// window / native window). Used to drive the external highlight overlay
|
|
452
|
+
/// that sits ABOVE the device frame in WebDevicePreview.
|
|
453
|
+
Rect? _rectInRootSpace(RenderObject target) {
|
|
454
|
+
if (!target.attached) return null;
|
|
455
|
+
try {
|
|
456
|
+
final Matrix4 transform = target.getTransformTo(null);
|
|
457
|
+
return MatrixUtils.transformRect(transform, target.paintBounds);
|
|
458
|
+
} catch (_) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
248
463
|
void _startHighlightTicker() {
|
|
249
464
|
_highlightTicker ??= createTicker(_onHighlightTick);
|
|
250
465
|
if (!_highlightTicker!.isActive) _highlightTicker!.start();
|
|
@@ -257,13 +472,17 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
257
472
|
void _onHighlightTick(Duration _) {
|
|
258
473
|
final RenderObject? target = _selectedRender;
|
|
259
474
|
if (target == null) return;
|
|
260
|
-
|
|
261
|
-
if (rect == null) {
|
|
475
|
+
if (!target.attached) {
|
|
262
476
|
setState(_clearSelection);
|
|
263
477
|
return;
|
|
264
478
|
}
|
|
265
|
-
|
|
266
|
-
|
|
479
|
+
final Rect? local = _rectInOverlaySpace(target);
|
|
480
|
+
if (local != null && _highlightRect != local) {
|
|
481
|
+
setState(() => _highlightRect = local);
|
|
482
|
+
}
|
|
483
|
+
final Rect? global = _rectInRootSpace(target);
|
|
484
|
+
if (global != devInspectorHighlightGlobalRect.value) {
|
|
485
|
+
devInspectorHighlightGlobalRect.value = global;
|
|
267
486
|
}
|
|
268
487
|
}
|
|
269
488
|
|
|
@@ -359,17 +578,10 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
359
578
|
data: shellTheme,
|
|
360
579
|
child: Builder(
|
|
361
580
|
builder: (BuildContext context) {
|
|
362
|
-
final KasyColors colors = context.colors;
|
|
363
|
-
|
|
364
|
-
// Default: bottom-right, just above the home bottom nav (still draggable).
|
|
365
|
-
// ~16px from right; larger bottom inset → sits a bit higher above the bar.
|
|
366
|
-
_fabDx ??= size.width - 68.0;
|
|
367
|
-
_fabDy ??= size.height - padding.bottom - 152.0;
|
|
368
|
-
|
|
369
581
|
return Stack(
|
|
370
582
|
clipBehavior: Clip.none,
|
|
371
583
|
children: <Widget>[
|
|
372
|
-
widget.child,
|
|
584
|
+
KeyedSubtree(key: _contentKey, child: widget.child),
|
|
373
585
|
if (_active)
|
|
374
586
|
Positioned.fill(
|
|
375
587
|
child: Listener(
|
|
@@ -378,61 +590,19 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
378
590
|
_onInspectorTap(ev.position),
|
|
379
591
|
child: IgnorePointer(
|
|
380
592
|
child: CustomPaint(
|
|
593
|
+
key: _overlayKey,
|
|
381
594
|
painter: _HighlightPainter(rect: _highlightRect),
|
|
382
595
|
size: Size.infinite,
|
|
383
596
|
),
|
|
384
597
|
),
|
|
385
598
|
),
|
|
386
599
|
),
|
|
387
|
-
if (
|
|
388
|
-
devInspectorFabEnabledNotifier.value)
|
|
600
|
+
if (_active)
|
|
389
601
|
Positioned(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
child:
|
|
393
|
-
|
|
394
|
-
duration: _fabOpacityAnimation,
|
|
395
|
-
curve: Curves.easeOutCubic,
|
|
396
|
-
child: GestureDetector(
|
|
397
|
-
onTapDown: (_) => setState(() => _fabEmphasized = true),
|
|
398
|
-
onTap: () {
|
|
399
|
-
if (_active) {
|
|
400
|
-
_copySelection().whenComplete(
|
|
401
|
-
_scheduleFabDeemphasize,
|
|
402
|
-
);
|
|
403
|
-
} else {
|
|
404
|
-
_setInspectorActive(true);
|
|
405
|
-
_scheduleFabDeemphasize();
|
|
406
|
-
}
|
|
407
|
-
},
|
|
408
|
-
onPanStart: (_) => setState(() {
|
|
409
|
-
_dragging = true;
|
|
410
|
-
_fabEmphasized = true;
|
|
411
|
-
}),
|
|
412
|
-
onPanUpdate: (DragUpdateDetails details) {
|
|
413
|
-
setState(() {
|
|
414
|
-
_fabDx = (_fabDx! + details.delta.dx).clamp(
|
|
415
|
-
8.0,
|
|
416
|
-
size.width - 60.0,
|
|
417
|
-
);
|
|
418
|
-
_fabDy = (_fabDy! + details.delta.dy).clamp(
|
|
419
|
-
padding.top + 8.0,
|
|
420
|
-
size.height - padding.bottom - 60.0,
|
|
421
|
-
);
|
|
422
|
-
});
|
|
423
|
-
},
|
|
424
|
-
onPanEnd: (_) {
|
|
425
|
-
if (!mounted) return;
|
|
426
|
-
setState(() => _dragging = false);
|
|
427
|
-
_scheduleFabDeemphasize();
|
|
428
|
-
},
|
|
429
|
-
child: _EntryFab(
|
|
430
|
-
dragging: _dragging,
|
|
431
|
-
active: _active,
|
|
432
|
-
busy: _copyBusy,
|
|
433
|
-
colors: colors,
|
|
434
|
-
),
|
|
435
|
-
),
|
|
602
|
+
bottom: padding.bottom + 12,
|
|
603
|
+
right: 12,
|
|
604
|
+
child: const IgnorePointer(
|
|
605
|
+
child: _InspectorStatusPill(),
|
|
436
606
|
),
|
|
437
607
|
),
|
|
438
608
|
if (_copyFeedbackText != null)
|
|
@@ -483,62 +653,6 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
483
653
|
}
|
|
484
654
|
}
|
|
485
655
|
|
|
486
|
-
class _EntryFab extends StatelessWidget {
|
|
487
|
-
const _EntryFab({
|
|
488
|
-
required this.dragging,
|
|
489
|
-
required this.active,
|
|
490
|
-
required this.busy,
|
|
491
|
-
required this.colors,
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
final bool dragging;
|
|
495
|
-
final bool active;
|
|
496
|
-
final bool busy;
|
|
497
|
-
final KasyColors colors;
|
|
498
|
-
|
|
499
|
-
@override
|
|
500
|
-
Widget build(BuildContext context) {
|
|
501
|
-
return Semantics(
|
|
502
|
-
label: active ? t.devInspector.copyForAi : t.devInspector.activate,
|
|
503
|
-
button: true,
|
|
504
|
-
child: AnimatedContainer(
|
|
505
|
-
duration: const Duration(milliseconds: 180),
|
|
506
|
-
width: 52,
|
|
507
|
-
height: 52,
|
|
508
|
-
decoration: BoxDecoration(
|
|
509
|
-
color: colors.surface,
|
|
510
|
-
shape: BoxShape.circle,
|
|
511
|
-
border: Border.all(
|
|
512
|
-
color: colors.onSurface.withValues(alpha: 0.14),
|
|
513
|
-
width: 1.5,
|
|
514
|
-
),
|
|
515
|
-
boxShadow: <BoxShadow>[
|
|
516
|
-
BoxShadow(
|
|
517
|
-
color: colors.onSurface.withValues(alpha: dragging ? 0.20 : 0.08),
|
|
518
|
-
blurRadius: dragging ? 18 : 10,
|
|
519
|
-
offset: Offset(0, dragging ? 6 : 3),
|
|
520
|
-
),
|
|
521
|
-
],
|
|
522
|
-
),
|
|
523
|
-
child: busy
|
|
524
|
-
? SizedBox(
|
|
525
|
-
width: 20,
|
|
526
|
-
height: 20,
|
|
527
|
-
child: CircularProgressIndicator(
|
|
528
|
-
strokeWidth: 2,
|
|
529
|
-
valueColor: AlwaysStoppedAnimation<Color>(colors.primary),
|
|
530
|
-
),
|
|
531
|
-
)
|
|
532
|
-
: Icon(
|
|
533
|
-
active ? KasyIcons.copy : KasyIcons.widgets,
|
|
534
|
-
color: colors.primary,
|
|
535
|
-
size: 22,
|
|
536
|
-
),
|
|
537
|
-
),
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
656
|
class _HighlightPainter extends CustomPainter {
|
|
543
657
|
_HighlightPainter({required this.rect});
|
|
544
658
|
|
|
@@ -564,3 +678,96 @@ class _HighlightPainter extends CustomPainter {
|
|
|
564
678
|
bool shouldRepaint(_HighlightPainter oldDelegate) =>
|
|
565
679
|
oldDelegate.rect != rect;
|
|
566
680
|
}
|
|
681
|
+
|
|
682
|
+
/// Minimal status pill tucked in the bottom-right corner while the inspector
|
|
683
|
+
/// is active. Mirrors the role of the WebDevicePreview chrome (which already
|
|
684
|
+
/// shows inspector state); hidden when that chrome is present via
|
|
685
|
+
/// [devInspectorSuppressStatusPillNotifier].
|
|
686
|
+
class _InspectorStatusPill extends StatelessWidget {
|
|
687
|
+
const _InspectorStatusPill();
|
|
688
|
+
|
|
689
|
+
@override
|
|
690
|
+
Widget build(BuildContext context) {
|
|
691
|
+
return ValueListenableBuilder<bool>(
|
|
692
|
+
valueListenable: devInspectorSuppressStatusPillNotifier,
|
|
693
|
+
builder: (BuildContext context, bool suppressed, Widget? _) {
|
|
694
|
+
if (suppressed) return const SizedBox.shrink();
|
|
695
|
+
return Container(
|
|
696
|
+
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5),
|
|
697
|
+
decoration: BoxDecoration(
|
|
698
|
+
color: const Color(0xB3111111),
|
|
699
|
+
borderRadius: BorderRadius.circular(999),
|
|
700
|
+
boxShadow: const <BoxShadow>[
|
|
701
|
+
BoxShadow(
|
|
702
|
+
color: Color(0x33000000),
|
|
703
|
+
blurRadius: 8,
|
|
704
|
+
offset: Offset(0, 2),
|
|
705
|
+
),
|
|
706
|
+
],
|
|
707
|
+
),
|
|
708
|
+
child: Row(
|
|
709
|
+
mainAxisSize: MainAxisSize.min,
|
|
710
|
+
children: <Widget>[
|
|
711
|
+
const _PulsingDot(),
|
|
712
|
+
const SizedBox(width: 7),
|
|
713
|
+
Text(
|
|
714
|
+
t.devInspector.statusActive,
|
|
715
|
+
style: const TextStyle(
|
|
716
|
+
color: Color(0xE6FFFFFF),
|
|
717
|
+
fontSize: 10.5,
|
|
718
|
+
fontWeight: FontWeight.w500,
|
|
719
|
+
letterSpacing: 0.2,
|
|
720
|
+
),
|
|
721
|
+
),
|
|
722
|
+
],
|
|
723
|
+
),
|
|
724
|
+
);
|
|
725
|
+
},
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
class _PulsingDot extends StatefulWidget {
|
|
731
|
+
const _PulsingDot();
|
|
732
|
+
@override
|
|
733
|
+
State<_PulsingDot> createState() => _PulsingDotState();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
class _PulsingDotState extends State<_PulsingDot>
|
|
737
|
+
with SingleTickerProviderStateMixin {
|
|
738
|
+
late final AnimationController _ctrl;
|
|
739
|
+
|
|
740
|
+
@override
|
|
741
|
+
void initState() {
|
|
742
|
+
super.initState();
|
|
743
|
+
_ctrl = AnimationController(
|
|
744
|
+
vsync: this,
|
|
745
|
+
duration: const Duration(milliseconds: 1100),
|
|
746
|
+
)..repeat(reverse: true);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
@override
|
|
750
|
+
void dispose() {
|
|
751
|
+
_ctrl.dispose();
|
|
752
|
+
super.dispose();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
@override
|
|
756
|
+
Widget build(BuildContext context) {
|
|
757
|
+
return AnimatedBuilder(
|
|
758
|
+
animation: _ctrl,
|
|
759
|
+
builder: (BuildContext context, Widget? _) {
|
|
760
|
+
final double t = Curves.easeInOut.transform(_ctrl.value);
|
|
761
|
+
final double alpha = 0.45 + 0.55 * t;
|
|
762
|
+
return Container(
|
|
763
|
+
width: 7,
|
|
764
|
+
height: 7,
|
|
765
|
+
decoration: BoxDecoration(
|
|
766
|
+
color: _highlightColor.withValues(alpha: alpha),
|
|
767
|
+
shape: BoxShape.circle,
|
|
768
|
+
),
|
|
769
|
+
);
|
|
770
|
+
},
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
}
|