kasy-cli 1.19.3 → 1.20.1
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/README.md +11 -3
- package/bin/kasy.js +1 -0
- package/lib/commands/new.js +87 -37
- package/lib/commands/run.js +14 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/meta_ads_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +4 -5
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_vote_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/providers/llm_chat_notifier.dart +317 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +40 -1
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/notifications_entity.dart +2 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -2
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -0
- package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +11 -8
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +91 -2
- package/lib/scaffold/backends/supabase/deploy.js +56 -3
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/storage_api.dart +5 -11
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +2 -2
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +31 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -0
- package/lib/scaffold/catalog.js +2 -2
- package/lib/scaffold/engine.js +5 -0
- package/lib/scaffold/generate.js +23 -3
- package/lib/scaffold/shared/generator-utils.js +303 -56
- package/lib/scaffold/shared/post-build.js +11 -0
- package/lib/utils/i18n/messages-en.js +6 -1
- package/lib/utils/i18n/messages-es.js +6 -1
- package/lib/utils/i18n/messages-pt.js +6 -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 +1150 -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 +13 -82
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +6 -102
- 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/states/user_state_notifier.dart +8 -10
- 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 +11 -14
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +121 -66
- package/templates/firebase/lib/features/home/home_page.dart +7 -8
- 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/lib/router.dart +60 -0
- package/templates/firebase/pubspec.yaml +6 -4
- package/templates/firebase/test/core/bottom_menu/detail_route_menu_test.dart +57 -0
- package/templates/firebase/web/index.html +7 -17
- package/lib/scaffold/backends/api/patch/lib/core/rating/widgets/review_popup.dart +0 -211
- package/lib/scaffold/backends/api/patch/lib/features/notifications/providers/models/notification.dart +0 -185
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
- package/lib/scaffold/backends/api/patch/lib/main.dart +0 -275
- package/lib/scaffold/backends/api/patch/lib/router.dart +0 -133
- package/lib/scaffold/backends/supabase/patch/lib/core/rating/widgets/review_popup.dart +0 -211
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/providers/models/notification.dart +0 -174
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
- package/lib/scaffold/backends/supabase/patch/lib/main.dart +0 -307
- package/lib/scaffold/backends/supabase/patch/lib/router.dart +0 -133
- 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,56 +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;
|
|
90
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;
|
|
91
119
|
|
|
92
120
|
@override
|
|
93
121
|
void initState() {
|
|
94
122
|
super.initState();
|
|
123
|
+
_transitionCtrl = AnimationController(
|
|
124
|
+
vsync: this,
|
|
125
|
+
duration: const Duration(milliseconds: 140),
|
|
126
|
+
)..addListener(_handleTransitionTick);
|
|
95
127
|
devInspectorActiveNotifier.addListener(_handleActiveChanged);
|
|
96
|
-
|
|
97
|
-
devInspectorRevealNowNotifier.addListener(_onRevealNow);
|
|
128
|
+
devInspectorEnabledNotifier.addListener(_handleEnabledChanged);
|
|
98
129
|
devInspectorCopyTriggerNotifier.addListener(_onCopyTriggered);
|
|
99
|
-
|
|
130
|
+
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
|
131
|
+
unawaited(_bootstrapEnabledPreference());
|
|
100
132
|
}
|
|
101
133
|
|
|
102
134
|
@override
|
|
103
135
|
void dispose() {
|
|
104
|
-
_revealTimer?.cancel();
|
|
105
|
-
_fabDeemphasizeTimer?.cancel();
|
|
106
136
|
_copyFeedbackTimer?.cancel();
|
|
107
137
|
_highlightTicker?.dispose();
|
|
108
|
-
|
|
109
|
-
|
|
138
|
+
_transitionCtrl.dispose();
|
|
139
|
+
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
|
140
|
+
devInspectorEnabledNotifier.removeListener(_handleEnabledChanged);
|
|
110
141
|
devInspectorCopyTriggerNotifier.removeListener(_onCopyTriggered);
|
|
111
142
|
devInspectorActiveNotifier.removeListener(_handleActiveChanged);
|
|
112
143
|
if (devInspectorActiveNotifier.value) {
|
|
@@ -115,103 +146,170 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
115
146
|
super.dispose();
|
|
116
147
|
}
|
|
117
148
|
|
|
118
|
-
|
|
119
|
-
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
149
|
+
void _handleTransitionTick() {
|
|
120
150
|
if (!mounted) return;
|
|
121
|
-
final
|
|
122
|
-
final
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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);
|
|
126
161
|
}
|
|
127
162
|
}
|
|
128
163
|
|
|
129
|
-
|
|
130
|
-
|
|
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;
|
|
131
218
|
}
|
|
132
219
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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();
|
|
138
235
|
}
|
|
139
236
|
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
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;
|
|
145
251
|
setState(() {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
});
|
|
149
|
-
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
150
|
-
if (!mounted) return;
|
|
151
|
-
setState(() => _fabFadeIn = true);
|
|
252
|
+
_selectedInfo = entry.info;
|
|
253
|
+
_selectedRender = entry.renderObject;
|
|
152
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;
|
|
153
264
|
}
|
|
154
265
|
|
|
155
|
-
void
|
|
156
|
-
|
|
157
|
-
_fabDeemphasizeTimer?.cancel();
|
|
266
|
+
Future<void> _bootstrapEnabledPreference() async {
|
|
267
|
+
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
158
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
|
+
}
|
|
159
297
|
if (!enabled) {
|
|
160
|
-
if (_active) {
|
|
161
|
-
devInspectorActiveNotifier.value = false;
|
|
162
|
-
}
|
|
163
298
|
_copyFeedbackTimer?.cancel();
|
|
164
299
|
setState(() {
|
|
165
300
|
_active = false;
|
|
166
|
-
_fabLayerMounted = false;
|
|
167
|
-
_fabFadeIn = false;
|
|
168
|
-
_fabEmphasized = false;
|
|
169
|
-
_dragging = false;
|
|
170
301
|
_copyFeedbackText = null;
|
|
171
302
|
_copyFeedbackIsError = false;
|
|
172
303
|
_clearSelection();
|
|
173
304
|
});
|
|
174
|
-
_copyFeedbackTimer?.cancel();
|
|
175
|
-
return;
|
|
176
305
|
}
|
|
177
|
-
setState(() {
|
|
178
|
-
_fabLayerMounted = false;
|
|
179
|
-
_fabFadeIn = false;
|
|
180
|
-
});
|
|
181
|
-
_revealTimer = Timer(_fabRevealDelay, _onFabRevealDelayElapsed);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
void _onFabRevealDelayElapsed() {
|
|
185
|
-
if (!mounted) return;
|
|
186
|
-
setState(() {
|
|
187
|
-
_fabLayerMounted = true;
|
|
188
|
-
_fabFadeIn = false;
|
|
189
|
-
});
|
|
190
|
-
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
191
|
-
if (!mounted) return;
|
|
192
|
-
setState(() => _fabFadeIn = true);
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
void _scheduleFabDeemphasize() {
|
|
197
|
-
_fabDeemphasizeTimer?.cancel();
|
|
198
|
-
_fabDeemphasizeTimer = Timer(_fabDeemphasizeDelay, () {
|
|
199
|
-
if (!mounted) return;
|
|
200
|
-
setState(() => _fabEmphasized = false);
|
|
201
|
-
_fabDeemphasizeTimer = null;
|
|
202
|
-
});
|
|
203
306
|
}
|
|
204
307
|
|
|
205
|
-
|
|
206
|
-
if (!
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
void _setInspectorActive(bool value) {
|
|
213
|
-
devInspectorActiveNotifier.value = value;
|
|
214
|
-
HapticFeedback.lightImpact();
|
|
308
|
+
void _onCopyTriggered() {
|
|
309
|
+
if (!devInspectorCopyTriggerNotifier.value) return;
|
|
310
|
+
devInspectorCopyTriggerNotifier.value = false;
|
|
311
|
+
if (!_active) return;
|
|
312
|
+
unawaited(_copySelection());
|
|
215
313
|
}
|
|
216
314
|
|
|
217
315
|
void _handleActiveChanged() {
|
|
@@ -228,36 +326,138 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
228
326
|
_selectedInfo = null;
|
|
229
327
|
_selectedRender = null;
|
|
230
328
|
_highlightRect = null;
|
|
329
|
+
_transitionFromRect = null;
|
|
330
|
+
_transitionToRect = null;
|
|
331
|
+
_transitionFromGlobalRect = null;
|
|
332
|
+
_transitionToGlobalRect = null;
|
|
333
|
+
if (_transitionCtrl.isAnimating) _transitionCtrl.stop();
|
|
231
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;
|
|
232
351
|
}
|
|
233
352
|
|
|
234
353
|
void _onInspectorTap(Offset globalPosition) {
|
|
235
|
-
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);
|
|
236
360
|
if (picked == null) {
|
|
237
361
|
HapticFeedback.heavyImpact();
|
|
238
362
|
return;
|
|
239
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
|
+
|
|
240
379
|
setState(() {
|
|
241
|
-
_selectedInfo = picked
|
|
380
|
+
_selectedInfo = picked!.info;
|
|
242
381
|
_selectedRender = picked.renderObject;
|
|
243
|
-
_highlightRect = _toOverlayLocal(picked.info.boundingBox);
|
|
244
382
|
});
|
|
245
|
-
|
|
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
|
+
);
|
|
246
394
|
HapticFeedback.selectionClick();
|
|
247
395
|
}
|
|
248
396
|
|
|
249
|
-
///
|
|
250
|
-
///
|
|
251
|
-
///
|
|
252
|
-
|
|
253
|
-
|
|
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 =
|
|
254
440
|
_overlayKey.currentContext?.findRenderObject();
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
}
|
|
261
461
|
}
|
|
262
462
|
|
|
263
463
|
void _startHighlightTicker() {
|
|
@@ -272,15 +472,18 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
272
472
|
void _onHighlightTick(Duration _) {
|
|
273
473
|
final RenderObject? target = _selectedRender;
|
|
274
474
|
if (target == null) return;
|
|
275
|
-
|
|
276
|
-
if (rect == null) {
|
|
475
|
+
if (!target.attached) {
|
|
277
476
|
setState(_clearSelection);
|
|
278
477
|
return;
|
|
279
478
|
}
|
|
280
|
-
final Rect? local =
|
|
281
|
-
if (_highlightRect != local) {
|
|
479
|
+
final Rect? local = _rectInOverlaySpace(target);
|
|
480
|
+
if (local != null && _highlightRect != local) {
|
|
282
481
|
setState(() => _highlightRect = local);
|
|
283
482
|
}
|
|
483
|
+
final Rect? global = _rectInRootSpace(target);
|
|
484
|
+
if (global != devInspectorHighlightGlobalRect.value) {
|
|
485
|
+
devInspectorHighlightGlobalRect.value = global;
|
|
486
|
+
}
|
|
284
487
|
}
|
|
285
488
|
|
|
286
489
|
String? _extractRouteHint(DevInspectorInfo info) {
|
|
@@ -375,17 +578,10 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
375
578
|
data: shellTheme,
|
|
376
579
|
child: Builder(
|
|
377
580
|
builder: (BuildContext context) {
|
|
378
|
-
final KasyColors colors = context.colors;
|
|
379
|
-
|
|
380
|
-
// Default: bottom-right, just above the home bottom nav (still draggable).
|
|
381
|
-
// ~16px from right; larger bottom inset → sits a bit higher above the bar.
|
|
382
|
-
_fabDx ??= size.width - 68.0;
|
|
383
|
-
_fabDy ??= size.height - padding.bottom - 152.0;
|
|
384
|
-
|
|
385
581
|
return Stack(
|
|
386
582
|
clipBehavior: Clip.none,
|
|
387
583
|
children: <Widget>[
|
|
388
|
-
widget.child,
|
|
584
|
+
KeyedSubtree(key: _contentKey, child: widget.child),
|
|
389
585
|
if (_active)
|
|
390
586
|
Positioned.fill(
|
|
391
587
|
child: Listener(
|
|
@@ -401,55 +597,12 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
401
597
|
),
|
|
402
598
|
),
|
|
403
599
|
),
|
|
404
|
-
if (
|
|
405
|
-
devInspectorFabEnabledNotifier.value)
|
|
600
|
+
if (_active)
|
|
406
601
|
Positioned(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
child:
|
|
410
|
-
|
|
411
|
-
duration: _fabOpacityAnimation,
|
|
412
|
-
curve: Curves.easeOutCubic,
|
|
413
|
-
child: GestureDetector(
|
|
414
|
-
onTapDown: (_) => setState(() => _fabEmphasized = true),
|
|
415
|
-
onTap: () {
|
|
416
|
-
if (_active) {
|
|
417
|
-
_copySelection().whenComplete(
|
|
418
|
-
_scheduleFabDeemphasize,
|
|
419
|
-
);
|
|
420
|
-
} else {
|
|
421
|
-
_setInspectorActive(true);
|
|
422
|
-
_scheduleFabDeemphasize();
|
|
423
|
-
}
|
|
424
|
-
},
|
|
425
|
-
onPanStart: (_) => setState(() {
|
|
426
|
-
_dragging = true;
|
|
427
|
-
_fabEmphasized = true;
|
|
428
|
-
}),
|
|
429
|
-
onPanUpdate: (DragUpdateDetails details) {
|
|
430
|
-
setState(() {
|
|
431
|
-
_fabDx = (_fabDx! + details.delta.dx).clamp(
|
|
432
|
-
8.0,
|
|
433
|
-
size.width - 60.0,
|
|
434
|
-
);
|
|
435
|
-
_fabDy = (_fabDy! + details.delta.dy).clamp(
|
|
436
|
-
padding.top + 8.0,
|
|
437
|
-
size.height - padding.bottom - 60.0,
|
|
438
|
-
);
|
|
439
|
-
});
|
|
440
|
-
},
|
|
441
|
-
onPanEnd: (_) {
|
|
442
|
-
if (!mounted) return;
|
|
443
|
-
setState(() => _dragging = false);
|
|
444
|
-
_scheduleFabDeemphasize();
|
|
445
|
-
},
|
|
446
|
-
child: _EntryFab(
|
|
447
|
-
dragging: _dragging,
|
|
448
|
-
active: _active,
|
|
449
|
-
busy: _copyBusy,
|
|
450
|
-
colors: colors,
|
|
451
|
-
),
|
|
452
|
-
),
|
|
602
|
+
bottom: padding.bottom + 12,
|
|
603
|
+
right: 12,
|
|
604
|
+
child: const IgnorePointer(
|
|
605
|
+
child: _InspectorStatusPill(),
|
|
453
606
|
),
|
|
454
607
|
),
|
|
455
608
|
if (_copyFeedbackText != null)
|
|
@@ -500,62 +653,6 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
500
653
|
}
|
|
501
654
|
}
|
|
502
655
|
|
|
503
|
-
class _EntryFab extends StatelessWidget {
|
|
504
|
-
const _EntryFab({
|
|
505
|
-
required this.dragging,
|
|
506
|
-
required this.active,
|
|
507
|
-
required this.busy,
|
|
508
|
-
required this.colors,
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
final bool dragging;
|
|
512
|
-
final bool active;
|
|
513
|
-
final bool busy;
|
|
514
|
-
final KasyColors colors;
|
|
515
|
-
|
|
516
|
-
@override
|
|
517
|
-
Widget build(BuildContext context) {
|
|
518
|
-
return Semantics(
|
|
519
|
-
label: active ? t.devInspector.copyForAi : t.devInspector.activate,
|
|
520
|
-
button: true,
|
|
521
|
-
child: AnimatedContainer(
|
|
522
|
-
duration: const Duration(milliseconds: 180),
|
|
523
|
-
width: 52,
|
|
524
|
-
height: 52,
|
|
525
|
-
decoration: BoxDecoration(
|
|
526
|
-
color: colors.surface,
|
|
527
|
-
shape: BoxShape.circle,
|
|
528
|
-
border: Border.all(
|
|
529
|
-
color: colors.onSurface.withValues(alpha: 0.14),
|
|
530
|
-
width: 1.5,
|
|
531
|
-
),
|
|
532
|
-
boxShadow: <BoxShadow>[
|
|
533
|
-
BoxShadow(
|
|
534
|
-
color: colors.onSurface.withValues(alpha: dragging ? 0.20 : 0.08),
|
|
535
|
-
blurRadius: dragging ? 18 : 10,
|
|
536
|
-
offset: Offset(0, dragging ? 6 : 3),
|
|
537
|
-
),
|
|
538
|
-
],
|
|
539
|
-
),
|
|
540
|
-
child: busy
|
|
541
|
-
? SizedBox(
|
|
542
|
-
width: 20,
|
|
543
|
-
height: 20,
|
|
544
|
-
child: CircularProgressIndicator(
|
|
545
|
-
strokeWidth: 2,
|
|
546
|
-
valueColor: AlwaysStoppedAnimation<Color>(colors.primary),
|
|
547
|
-
),
|
|
548
|
-
)
|
|
549
|
-
: Icon(
|
|
550
|
-
active ? KasyIcons.copy : KasyIcons.widgets,
|
|
551
|
-
color: colors.primary,
|
|
552
|
-
size: 22,
|
|
553
|
-
),
|
|
554
|
-
),
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
656
|
class _HighlightPainter extends CustomPainter {
|
|
560
657
|
_HighlightPainter({required this.rect});
|
|
561
658
|
|
|
@@ -581,3 +678,96 @@ class _HighlightPainter extends CustomPainter {
|
|
|
581
678
|
bool shouldRepaint(_HighlightPainter oldDelegate) =>
|
|
582
679
|
oldDelegate.rect != rect;
|
|
583
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
|
+
}
|