kasy-cli 1.17.0 → 1.19.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.
Files changed (110) hide show
  1. package/bin/kasy.js +16 -2
  2. package/lib/commands/add.js +7 -7
  3. package/lib/commands/configure.js +548 -0
  4. package/lib/commands/deploy.js +4 -4
  5. package/lib/commands/doctor.js +17 -0
  6. package/lib/commands/favicon.js +4 -4
  7. package/lib/commands/icon.js +5 -5
  8. package/lib/commands/new.js +483 -324
  9. package/lib/commands/run.js +17 -4
  10. package/lib/commands/splash.js +5 -5
  11. package/lib/commands/update.js +9 -9
  12. package/lib/scaffold/CHANGELOG.json +14 -0
  13. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +108 -0
  14. package/lib/scaffold/backends/firebase/setup-from-scratch.js +123 -5
  15. package/lib/scaffold/generate.js +24 -8
  16. package/lib/scaffold/shared/post-build.js +8 -0
  17. package/lib/utils/brand.js +16 -12
  18. package/lib/utils/flutter-run.js +139 -11
  19. package/lib/utils/i18n/messages-en.js +62 -5
  20. package/lib/utils/i18n/messages-es.js +62 -5
  21. package/lib/utils/i18n/messages-pt.js +63 -6
  22. package/lib/utils/ui.js +79 -4
  23. package/package.json +1 -2
  24. package/templates/firebase/README.en.md +1 -1
  25. package/templates/firebase/README.es.md +1 -1
  26. package/templates/firebase/README.md +1 -1
  27. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +0 -15
  28. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +65 -26
  29. package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +2 -2
  30. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +7 -2
  31. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +6 -6
  32. package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +1 -1
  33. package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +2 -2
  34. package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +1 -1
  35. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  36. package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
  37. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  38. package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
  39. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  40. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
  41. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  42. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
  43. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  44. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
  45. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  46. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
  47. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  48. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
  49. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  50. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
  51. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  52. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
  53. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  54. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
  55. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +3 -3
  56. package/templates/firebase/android/app/src/main/res/values/colors.xml +32 -0
  57. package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
  58. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  59. package/templates/firebase/assets/images/splash_logo_light.png +0 -0
  60. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  61. package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +75 -29
  62. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  63. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  64. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  65. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
  66. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
  67. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
  68. package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
  69. package/templates/firebase/lib/components/components.dart +1 -0
  70. package/templates/firebase/lib/components/kasy_avatar.dart +65 -17
  71. package/templates/firebase/lib/components/kasy_avatar_presets.dart +121 -97
  72. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  73. package/templates/firebase/lib/components/kasy_date_picker.dart +2173 -0
  74. package/templates/firebase/lib/components/kasy_tabs.dart +214 -91
  75. package/templates/firebase/lib/components/kasy_text_area.dart +9 -4
  76. package/templates/firebase/lib/components/kasy_text_field.dart +96 -36
  77. package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -2
  78. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +88 -35
  79. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +7 -43
  80. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +118 -16
  81. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +14 -20
  82. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -18
  83. package/templates/firebase/lib/core/security/secured_storage.dart +56 -15
  84. package/templates/firebase/lib/core/theme/providers/theme_provider.dart +3 -0
  85. package/templates/firebase/lib/core/theme/web_background_sync.dart +3 -0
  86. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +18 -0
  87. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -6
  88. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +6 -0
  89. package/templates/firebase/lib/features/home/home_components_page.dart +3 -2
  90. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +949 -77
  91. package/templates/firebase/lib/features/home/home_page.dart +17 -40
  92. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +1 -16
  93. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +0 -4
  94. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +10 -5
  95. package/templates/firebase/lib/i18n/en.i18n.json +2 -1
  96. package/templates/firebase/lib/i18n/es.i18n.json +2 -1
  97. package/templates/firebase/lib/i18n/pt.i18n.json +2 -1
  98. package/templates/firebase/lib/main.dart +34 -34
  99. package/templates/firebase/pubspec.yaml +2 -1
  100. package/templates/firebase/storage.cors.json +8 -0
  101. package/templates/firebase/web/index.html +24 -2
  102. package/templates/firebase/web/splash/img/dark-1x.png +0 -0
  103. package/templates/firebase/web/splash/img/dark-2x.png +0 -0
  104. package/templates/firebase/web/splash/img/dark-3x.png +0 -0
  105. package/templates/firebase/web/splash/img/dark-4x.png +0 -0
  106. package/templates/firebase/web/splash/img/light-1x.png +0 -0
  107. package/templates/firebase/web/splash/img/light-2x.png +0 -0
  108. package/templates/firebase/web/splash/img/light-3x.png +0 -0
  109. package/templates/firebase/web/splash/img/light-4x.png +0 -0
  110. package/templates/firebase/lib/core/bottom_menu/kasy_bart_navigation.dart +0 -22
@@ -1,4 +1,5 @@
1
1
  import 'package:bart/bart.dart' as bart;
2
+ import 'package:bart/bart/bart_bottombar_actions.dart';
2
3
  import 'package:flutter/foundation.dart';
3
4
  import 'package:flutter/material.dart';
4
5
  import 'package:flutter/services.dart';
@@ -8,22 +9,92 @@ import 'package:kasy_kit/core/sidebar/kasy_sidebar.dart';
8
9
  import 'package:kasy_kit/core/theme/theme.dart';
9
10
  import 'package:kasy_kit/core/widgets/responsive_layout.dart';
10
11
 
11
- /// This bottom menu is powered by Bart packages
12
- /// https://pub.dev/packages/bart
13
- /// It allows you to create a bottom menu with a router and handle
14
- /// all tabs navigation separately.
15
- /// See the bottom_router.dart file to add tabs or subpages to show on tabs
16
- class BottomMenu extends StatelessWidget {
12
+ /// Bottom navigation host powered by Bart (https://pub.dev/packages/bart).
13
+ ///
14
+ /// Bart stores its bottom-bar visibility inside a [ValueNotifier] created in
15
+ /// the [bart.BartScaffold] constructor. Because [ResponsiveLayout] recreates
16
+ /// one of three [bart.BartScaffold]s on every rebuild (breakpoint change,
17
+ /// theme toggle, device-preview resize), that notifier resets to its initial
18
+ /// value and a previously-hidden bar can come back wrong — or, when returning
19
+ /// from a native overlay (FaceID, photo picker, permission dialog), Bart's
20
+ /// internal state ends up out of sync with the visible route.
21
+ ///
22
+ /// This widget owns visibility end-to-end so feature pages never need to
23
+ /// touch it. The rules are simple:
24
+ /// * Bottom-bar tab (1 path segment) → bar visible.
25
+ /// * Inner route (multi-segment, or [bart.BartMenuRoute.showBottomBar] =
26
+ /// false) → bar hidden.
27
+ ///
28
+ /// Sync points: [onRouteChanged], post-frame after every [build] (catches
29
+ /// scaffold rebuilds), and [didChangeAppLifecycleState] on resume.
30
+ class BottomMenu extends StatefulWidget {
17
31
  final String? initialRoute;
18
32
 
19
33
  const BottomMenu({super.key, this.initialRoute});
20
34
 
35
+ @override
36
+ State<BottomMenu> createState() => _BottomMenuState();
37
+ }
38
+
39
+ class _BottomMenuState extends State<BottomMenu>
40
+ with WidgetsBindingObserver, BartNotifier {
41
+ String? _currentRoutePath;
42
+
43
+ @override
44
+ void initState() {
45
+ super.initState();
46
+ WidgetsBinding.instance.addObserver(this);
47
+ _currentRoutePath = _resolveInitialRoute(widget.initialRoute);
48
+ }
49
+
50
+ @override
51
+ void dispose() {
52
+ WidgetsBinding.instance.removeObserver(this);
53
+ super.dispose();
54
+ }
55
+
56
+ @override
57
+ void didChangeAppLifecycleState(AppLifecycleState state) {
58
+ super.didChangeAppLifecycleState(state);
59
+ if (state == AppLifecycleState.resumed) {
60
+ _scheduleSync();
61
+ }
62
+ }
63
+
64
+ void _onRouteChanged(bart.BartMenuRoute route) {
65
+ _currentRoutePath = route.path;
66
+ _scheduleSync();
67
+ }
68
+
69
+ void _scheduleSync() {
70
+ WidgetsBinding.instance.addPostFrameCallback((_) {
71
+ if (!mounted) return;
72
+ _applyVisibility();
73
+ });
74
+ }
75
+
76
+ void _applyVisibility() {
77
+ if (_shouldShowBottomBar(_currentRoutePath)) {
78
+ showBottomBar(context);
79
+ } else {
80
+ hideBottomBar(context);
81
+ }
82
+ }
83
+
21
84
  @override
22
85
  Widget build(BuildContext context) {
23
- final String? resolvedInitialRoute = _resolveInitialRoute(initialRoute);
24
- final bool showBottomBarOnStart = _showBottomBarOnStart(
25
- resolvedInitialRoute,
86
+ // Re-assert visibility after every rebuild: the Bart scaffold underneath
87
+ // may have been freshly instantiated (see class doc).
88
+ _scheduleSync();
89
+
90
+ final String? resolvedInitialRoute = _resolveInitialRoute(
91
+ widget.initialRoute,
92
+ );
93
+ final bool showBottomBarOnStart = _shouldShowBottomBar(resolvedInitialRoute);
94
+ final scaffoldOptions = bart.ScaffoldOptions(
95
+ backgroundColor: context.colors.background,
26
96
  );
97
+
27
98
  return AnnotatedRegion<SystemUiOverlayStyle>(
28
99
  value: switch (Theme.brightnessOf(context)) {
29
100
  Brightness.dark => SystemUiOverlayStyle.light,
@@ -35,13 +106,8 @@ class BottomMenu extends StatelessWidget {
35
106
  bottomBar: kasyPaddedSurfaceBottomBar(),
36
107
  initialRoute: resolvedInitialRoute,
37
108
  showBottomBarOnStart: showBottomBarOnStart,
38
- scaffoldOptions: bart.ScaffoldOptions(
39
- backgroundColor: context.colors.background,
40
- ),
41
- onRouteChanged: (route) {
42
- // If you want to log tab events to analytics
43
- // MixpanelAnalyticsApi.instance().logEvent('home/$route', {});
44
- },
109
+ scaffoldOptions: scaffoldOptions,
110
+ onRouteChanged: _onRouteChanged,
45
111
  ),
46
112
  // medium (768–1024 px): icon-only collapsed rail
47
113
  medium: bart.BartScaffold(
@@ -49,16 +115,11 @@ class BottomMenu extends StatelessWidget {
49
115
  bottomBar: kasyPaddedSurfaceBottomBar(),
50
116
  initialRoute: resolvedInitialRoute,
51
117
  showBottomBarOnStart: showBottomBarOnStart,
52
- scaffoldOptions: bart.ScaffoldOptions(
53
- backgroundColor: context.colors.background,
54
- ),
118
+ scaffoldOptions: scaffoldOptions,
55
119
  sideBarOptions: bart.CustomSideBarOptions(
56
120
  sideBarBuilder: kasySidebarCollapsedBuilder,
57
121
  ),
58
- onRouteChanged: (route) {
59
- // If you want to log tab events to analytics
60
- // MixpanelAnalyticsApi.instance().logEvent('home/$route', {});
61
- },
122
+ onRouteChanged: _onRouteChanged,
62
123
  ),
63
124
  // large (1024 px+): full expanded sidebar
64
125
  large: bart.BartScaffold(
@@ -66,16 +127,11 @@ class BottomMenu extends StatelessWidget {
66
127
  bottomBar: kasyPaddedSurfaceBottomBar(),
67
128
  initialRoute: resolvedInitialRoute,
68
129
  showBottomBarOnStart: showBottomBarOnStart,
69
- scaffoldOptions: bart.ScaffoldOptions(
70
- backgroundColor: context.colors.background,
71
- ),
130
+ scaffoldOptions: scaffoldOptions,
72
131
  sideBarOptions: bart.CustomSideBarOptions(
73
132
  sideBarBuilder: kasySidebarBuilder,
74
133
  ),
75
- onRouteChanged: (route) {
76
- // If you want to log tab events to analytics
77
- // MixpanelAnalyticsApi.instance().logEvent('home/$route', {});
78
- },
134
+ onRouteChanged: _onRouteChanged,
79
135
  ),
80
136
  ),
81
137
  );
@@ -114,14 +170,11 @@ class BottomMenu extends StatelessWidget {
114
170
  return path;
115
171
  }
116
172
 
117
- bool _showBottomBarOnStart(String? route) {
173
+ bool _shouldShowBottomBar(String? route) {
118
174
  if (route == null) {
119
175
  return true;
120
176
  }
121
177
  final segments = Uri.parse(route).pathSegments;
122
- if (segments.length < 2) {
123
- return true;
124
- }
125
- return false;
178
+ return segments.length < 2;
126
179
  }
127
180
  }
@@ -2,7 +2,6 @@ import 'package:bart/bart.dart';
2
2
  import 'package:flutter/material.dart';
3
3
  import 'package:kasy_kit/components/kasy_app_bar.dart';
4
4
  import 'package:kasy_kit/core/bottom_menu/bart_inner_paths.dart';
5
- import 'package:kasy_kit/core/bottom_menu/kasy_bart_navigation.dart';
6
5
  import 'package:kasy_kit/core/bottom_menu/notification_bottom_item.dart';
7
6
  import 'package:kasy_kit/core/navigation/kasy_navigation_config.dart';
8
7
  import 'package:kasy_kit/core/navigation/kasy_route_transition.dart';
@@ -110,53 +109,18 @@ List<BartMenuRoute> subRoutes() {
110
109
  ];
111
110
  }
112
111
 
113
- class _HomeInnerRoute extends StatefulWidget {
112
+ /// Marker wrapper for inner home routes. Visibility is owned by
113
+ /// [BottomMenu] (see [BottomMenu._onRouteChanged] + [BartMenuRoute.innerRoute]
114
+ /// `showBottomBar: false`); this widget exists for symmetry with the dashboard
115
+ /// cards and to keep `HomeFeaturesPage` / `HomeComponentsPage` / etc. agnostic
116
+ /// of how they were pushed.
117
+ class _HomeInnerRoute extends StatelessWidget {
114
118
  final Widget child;
115
119
 
116
120
  const _HomeInnerRoute({required this.child});
117
121
 
118
122
  @override
119
- State<_HomeInnerRoute> createState() => _HomeInnerRouteState();
120
- }
121
-
122
- class _HomeInnerRouteState extends State<_HomeInnerRoute> {
123
- ModalRoute<Object?>? _route;
124
-
125
- @override
126
- void didChangeDependencies() {
127
- super.didChangeDependencies();
128
- final ModalRoute<Object?>? route = ModalRoute.of(context);
129
- if (_route != route) {
130
- _route?.animation?.removeStatusListener(_onRouteAnimStatus);
131
- _route = route;
132
- final Animation<double>? anim = route?.animation;
133
- if (anim != null && !anim.isCompleted) {
134
- anim.addStatusListener(_onRouteAnimStatus);
135
- }
136
- }
137
- kasyHideBottomBar(context);
138
- }
139
-
140
- void _onRouteAnimStatus(AnimationStatus status) {
141
- if (mounted && status == AnimationStatus.completed) {
142
- kasyHideBottomBar(context);
143
- }
144
- }
145
-
146
- @override
147
- void dispose() {
148
- _route?.animation?.removeStatusListener(_onRouteAnimStatus);
149
- super.dispose();
150
- }
151
-
152
- @override
153
- void reassemble() {
154
- super.reassemble();
155
- kasyHideBottomBar(context);
156
- }
157
-
158
- @override
159
- Widget build(BuildContext context) => widget.child;
123
+ Widget build(BuildContext context) => child;
160
124
  }
161
125
 
162
126
  /// Placeholder page for the wishlist tab.
@@ -2,6 +2,7 @@ import 'dart:async';
2
2
 
3
3
  import 'package:flutter/foundation.dart';
4
4
  import 'package:flutter/material.dart';
5
+ import 'package:flutter/scheduler.dart';
5
6
  import 'package:flutter/services.dart';
6
7
  import 'package:kasy_kit/core/dev_inspector/dev_inspector_info.dart';
7
8
  import 'package:kasy_kit/core/dev_inspector/dev_inspector_service.dart';
@@ -12,6 +13,10 @@ import 'package:kasy_kit/core/theme/providers/theme_provider.dart';
12
13
  import 'package:kasy_kit/i18n/translations.g.dart';
13
14
  import 'package:shared_preferences/shared_preferences.dart';
14
15
 
16
+ const Color _highlightColor = Color(0xFFD2F51E);
17
+ const double _highlightStrokeWidth = 2.0;
18
+ const double _highlightCornerRadius = 4.0;
19
+
15
20
  final GlobalKey<ScaffoldMessengerState> devInspectorRootScaffoldMessengerKey =
16
21
  GlobalKey<ScaffoldMessengerState>(debugLabel: 'devInspectorRoot');
17
22
 
@@ -31,6 +36,12 @@ final ValueNotifier<bool> devInspectorRevealNowNotifier =
31
36
  final ValueNotifier<bool> devInspectorCopyTriggerNotifier =
32
37
  ValueNotifier<bool>(false);
33
38
 
39
+ /// Active state of the custom inspector. Toggled by the FAB (native) and by
40
+ /// the Web Device Preview pill; observed by [DevInspector] to mount the
41
+ /// tap-absorbing overlay and the highlight.
42
+ final ValueNotifier<bool> devInspectorActiveNotifier =
43
+ ValueNotifier<bool>(false);
44
+
34
45
  class DevInspector extends StatefulWidget {
35
46
  const DevInspector({super.key, required this.child});
36
47
 
@@ -45,7 +56,8 @@ class DevInspector extends StatefulWidget {
45
56
  State<DevInspector> createState() => _DevInspectorState();
46
57
  }
47
58
 
48
- class _DevInspectorState extends State<DevInspector> {
59
+ class _DevInspectorState extends State<DevInspector>
60
+ with SingleTickerProviderStateMixin {
49
61
  static const Duration _fabRevealDelay = Duration(seconds: 2);
50
62
  static const Duration _fabDeemphasizeDelay = Duration(seconds: 2);
51
63
  static const Duration _fabOpacityAnimation = Duration(milliseconds: 700);
@@ -71,11 +83,15 @@ class _DevInspectorState extends State<DevInspector> {
71
83
  bool _fabFadeIn = false;
72
84
  bool _fabEmphasized = false;
73
85
 
86
+ DevInspectorInfo? _selectedInfo;
87
+ RenderObject? _selectedRender;
88
+ Rect? _highlightRect;
89
+ Ticker? _highlightTicker;
90
+
74
91
  @override
75
92
  void initState() {
76
93
  super.initState();
77
- WidgetsBinding.instance.debugShowWidgetInspectorOverrideNotifier
78
- .addListener(_handleInspectorOverrideChanged);
94
+ devInspectorActiveNotifier.addListener(_handleActiveChanged);
79
95
  devInspectorFabEnabledNotifier.addListener(_onFabEnabledNotifierChanged);
80
96
  devInspectorRevealNowNotifier.addListener(_onRevealNow);
81
97
  devInspectorCopyTriggerNotifier.addListener(_onCopyTriggered);
@@ -87,13 +103,13 @@ class _DevInspectorState extends State<DevInspector> {
87
103
  _revealTimer?.cancel();
88
104
  _fabDeemphasizeTimer?.cancel();
89
105
  _copyFeedbackTimer?.cancel();
106
+ _highlightTicker?.dispose();
90
107
  devInspectorFabEnabledNotifier.removeListener(_onFabEnabledNotifierChanged);
91
108
  devInspectorRevealNowNotifier.removeListener(_onRevealNow);
92
109
  devInspectorCopyTriggerNotifier.removeListener(_onCopyTriggered);
93
- WidgetsBinding.instance.debugShowWidgetInspectorOverrideNotifier
94
- .removeListener(_handleInspectorOverrideChanged);
95
- if (_active) {
96
- WidgetsBinding.instance.debugShowWidgetInspectorOverride = false;
110
+ devInspectorActiveNotifier.removeListener(_handleActiveChanged);
111
+ if (devInspectorActiveNotifier.value) {
112
+ devInspectorActiveNotifier.value = false;
97
113
  }
98
114
  super.dispose();
99
115
  }
@@ -141,7 +157,7 @@ class _DevInspectorState extends State<DevInspector> {
141
157
  if (!mounted) return;
142
158
  if (!enabled) {
143
159
  if (_active) {
144
- WidgetsBinding.instance.debugShowWidgetInspectorOverride = false;
160
+ devInspectorActiveNotifier.value = false;
145
161
  }
146
162
  _copyFeedbackTimer?.cancel();
147
163
  setState(() {
@@ -152,6 +168,7 @@ class _DevInspectorState extends State<DevInspector> {
152
168
  _dragging = false;
153
169
  _copyFeedbackText = null;
154
170
  _copyFeedbackIsError = false;
171
+ _clearSelection();
155
172
  });
156
173
  _copyFeedbackTimer?.cancel();
157
174
  return;
@@ -192,17 +209,62 @@ class _DevInspectorState extends State<DevInspector> {
192
209
  }
193
210
 
194
211
  void _setInspectorActive(bool value) {
195
- setState(() => _active = value);
196
- WidgetsBinding.instance.debugShowWidgetInspectorOverride = value;
212
+ devInspectorActiveNotifier.value = value;
197
213
  HapticFeedback.lightImpact();
198
214
  }
199
215
 
200
- void _handleInspectorOverrideChanged() {
201
- final bool override =
202
- WidgetsBinding.instance.debugShowWidgetInspectorOverride;
203
- if (_active == override) return;
216
+ void _handleActiveChanged() {
217
+ final bool active = devInspectorActiveNotifier.value;
218
+ if (_active == active) return;
204
219
  if (!mounted) return;
205
- setState(() => _active = override);
220
+ setState(() {
221
+ _active = active;
222
+ if (!active) _clearSelection();
223
+ });
224
+ }
225
+
226
+ void _clearSelection() {
227
+ _selectedInfo = null;
228
+ _selectedRender = null;
229
+ _highlightRect = null;
230
+ _stopHighlightTicker();
231
+ }
232
+
233
+ void _onInspectorTap(Offset globalPosition) {
234
+ final picked = DevInspectorService.pickAt(globalPosition);
235
+ if (picked == null) {
236
+ HapticFeedback.heavyImpact();
237
+ return;
238
+ }
239
+ setState(() {
240
+ _selectedInfo = picked.info;
241
+ _selectedRender = picked.renderObject;
242
+ _highlightRect = picked.info.boundingBox;
243
+ });
244
+ _startHighlightTicker();
245
+ HapticFeedback.selectionClick();
246
+ }
247
+
248
+ void _startHighlightTicker() {
249
+ _highlightTicker ??= createTicker(_onHighlightTick);
250
+ if (!_highlightTicker!.isActive) _highlightTicker!.start();
251
+ }
252
+
253
+ void _stopHighlightTicker() {
254
+ if (_highlightTicker?.isActive ?? false) _highlightTicker!.stop();
255
+ }
256
+
257
+ void _onHighlightTick(Duration _) {
258
+ final RenderObject? target = _selectedRender;
259
+ if (target == null) return;
260
+ final Rect? rect = DevInspectorService.remeasure(target);
261
+ if (rect == null) {
262
+ setState(_clearSelection);
263
+ return;
264
+ }
265
+ if (_highlightRect != rect) {
266
+ setState(() => _highlightRect = rect);
267
+ }
206
268
  }
207
269
 
208
270
  String? _extractRouteHint(DevInspectorInfo info) {
@@ -233,7 +295,7 @@ class _DevInspectorState extends State<DevInspector> {
233
295
  if (!_active || _copyBusy) return;
234
296
  _copyBusy = true;
235
297
  try {
236
- final DevInspectorInfo? info = DevInspectorService.inspectSelected();
298
+ final DevInspectorInfo? info = _selectedInfo;
237
299
  if (!mounted) return;
238
300
  if (info == null) {
239
301
  _showCopyFeedback(t.devInspector.selectWidgetFirst, isError: true);
@@ -308,6 +370,20 @@ class _DevInspectorState extends State<DevInspector> {
308
370
  clipBehavior: Clip.none,
309
371
  children: <Widget>[
310
372
  widget.child,
373
+ if (_active)
374
+ Positioned.fill(
375
+ child: Listener(
376
+ behavior: HitTestBehavior.opaque,
377
+ onPointerDown: (PointerDownEvent ev) =>
378
+ _onInspectorTap(ev.position),
379
+ child: IgnorePointer(
380
+ child: CustomPaint(
381
+ painter: _HighlightPainter(rect: _highlightRect),
382
+ size: Size.infinite,
383
+ ),
384
+ ),
385
+ ),
386
+ ),
311
387
  if (_fabLayerMounted &&
312
388
  devInspectorFabEnabledNotifier.value)
313
389
  Positioned(
@@ -462,3 +538,29 @@ class _EntryFab extends StatelessWidget {
462
538
  );
463
539
  }
464
540
  }
541
+
542
+ class _HighlightPainter extends CustomPainter {
543
+ _HighlightPainter({required this.rect});
544
+
545
+ final Rect? rect;
546
+
547
+ @override
548
+ void paint(Canvas canvas, Size size) {
549
+ final Rect? r = rect;
550
+ if (r == null || r.isEmpty) return;
551
+ final Paint paint = Paint()
552
+ ..color = _highlightColor
553
+ ..style = PaintingStyle.stroke
554
+ ..strokeWidth = _highlightStrokeWidth;
555
+ final Rect outer = r.inflate(_highlightStrokeWidth / 2);
556
+ final RRect rrect = RRect.fromRectAndRadius(
557
+ outer,
558
+ const Radius.circular(_highlightCornerRadius),
559
+ );
560
+ canvas.drawRRect(rrect, paint);
561
+ }
562
+
563
+ @override
564
+ bool shouldRepaint(_HighlightPainter oldDelegate) =>
565
+ oldDelegate.rect != rect;
566
+ }
@@ -280,23 +280,12 @@ class DevInspectorService {
280
280
  );
281
281
  }
282
282
 
283
- /// Reads the widget currently selected in Flutter's native inspector.
284
- static DevInspectorInfo? inspectSelected() {
285
- if (!kDebugMode) return null;
286
- try {
287
- // ignore: invalid_use_of_visible_for_testing_member
288
- final RenderObject? renderObject =
289
- WidgetInspectorService.instance.selection.current;
290
- if (renderObject == null) return null;
291
- return _extractFromRenderObject(renderObject);
292
- } catch (_) {
293
- return null;
294
- }
295
- }
296
-
297
- /// Inspects the widget at a global position, syncs the native inspector
298
- /// selection (tooltip + highlight), and return the info.
299
- static DevInspectorInfo? inspectAndSelect(Offset globalPosition) {
283
+ /// Hit-tests at [globalPosition] and returns the inspector info plus the
284
+ /// underlying render object so the caller can re-measure the bounds during
285
+ /// animations.
286
+ static ({DevInspectorInfo info, RenderObject renderObject})? pickAt(
287
+ Offset globalPosition,
288
+ ) {
300
289
  if (!kDebugMode) return null;
301
290
  try {
302
291
  final result = BoxHitTestResult();
@@ -308,9 +297,7 @@ class DevInspectorService {
308
297
  final RenderObject candidate = entry.target as RenderObject;
309
298
  final DevInspectorInfo? info = _extractFromRenderObject(candidate);
310
299
  if (info != null) {
311
- // ignore: invalid_use_of_protected_member
312
- WidgetInspectorService.instance.setSelection(candidate, 'kit_inspector');
313
- return info;
300
+ return (info: info, renderObject: candidate);
314
301
  }
315
302
  }
316
303
  return null;
@@ -318,4 +305,11 @@ class DevInspectorService {
318
305
  return null;
319
306
  }
320
307
  }
308
+
309
+ /// Re-measures the global bounding rect of a previously picked render
310
+ /// object. Returns null if it became detached from the tree.
311
+ static Rect? remeasure(RenderObject target) {
312
+ if (!target.attached) return null;
313
+ return _globalBoundingRect(target);
314
+ }
321
315
  }
@@ -71,12 +71,12 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
71
71
  final user = ref.read(userStateNotifierProvider).user;
72
72
 
73
73
  // Slang lazy-loads non-base locales: AppLocale.pt.translations falls back
74
- // silently to the base locale (en) until the locale's bundle is loaded
75
- // into memory. setLocale() kicks off that load asynchronously, so a tap
76
- // on "Português" immediately followed by updateForLocale(pt) would push
77
- // English text to the widget on the first try and the correct one on
78
- // the retry. Awaiting loadLocale here removes the race.
79
- await LocaleSettings.instance.loadLocale(locale);
74
+ // silently to the base locale (en) if the bundle isn't in the translation
75
+ // map yet. The async loadLocale() can't be used here as a safety net
76
+ // because it short-circuits ("already loading") when setLocale() is in
77
+ // flight in parallel leaving us reading translations that aren't loaded
78
+ // yet. loadLocaleSync forces the bundle into the map right now, no race.
79
+ LocaleSettings.instance.loadLocaleSync(locale);
80
80
  final t = locale.translations;
81
81
 
82
82
  // "Logged out" = no user id at all (post-logout in authRequired mode, or
@@ -111,6 +111,7 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
111
111
  ? ''
112
112
  : (isPro ? t.home_widget.plan_pro : t.home_widget.plan_free);
113
113
  final quote = t.home_widget.quote;
114
+ final quoteAuthor = t.home_widget.quote_author;
114
115
 
115
116
  logger.d(
116
117
  'Widget payload → greeting: "$greeting", title: "$title", '
@@ -123,6 +124,7 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
123
124
  'planText': planText,
124
125
  'isPro': isPro.toString(),
125
126
  'quote': quote,
127
+ 'quoteAuthor': quoteAuthor,
126
128
  });
127
129
  }
128
130
 
@@ -164,6 +166,10 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
164
166
  await HomeWidget.saveWidgetData<String>('planText', data['planText'] ?? '');
165
167
  await HomeWidget.saveWidgetData<String>('isPro', data['isPro'] ?? 'false');
166
168
  await HomeWidget.saveWidgetData<String>('quote', data['quote'] ?? '');
169
+ await HomeWidget.saveWidgetData<String>(
170
+ 'quoteAuthor',
171
+ data['quoteAuthor'] ?? '',
172
+ );
167
173
 
168
174
  await HomeWidget.updateWidget(
169
175
  name: _androidWidgetName,
@@ -171,18 +177,6 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
171
177
  );
172
178
  }
173
179
 
174
- Future<Map<String, dynamic>> getWidgetData() async {
175
- return {
176
- 'greeting': await HomeWidget.getWidgetData<String>('greeting'),
177
- 'title': await HomeWidget.getWidgetData<String>('title'),
178
- 'planText': await HomeWidget.getWidgetData<String>('planText'),
179
- 'isPro': await HomeWidget.getWidgetData<String>(
180
- 'isPro',
181
- defaultValue: 'false',
182
- ),
183
- };
184
- }
185
-
186
180
  /// Time-of-day greeting in the app language (matches what the user sees
187
181
  /// inside the app, not the device language).
188
182
  static String _greeting(Translations t) {