kasy-cli 1.37.1 → 1.38.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/lib/scaffold/CHANGELOG.json +9 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
- package/lib/scaffold/backends/patch-base-hashes.json +2 -2
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
- package/templates/firebase/AGENTS.md +7 -1
- package/templates/firebase/DESIGN_SYSTEM.md +13 -0
- package/templates/firebase/lib/components/kasy_app_bar.dart +57 -16
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
- package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
- package/templates/firebase/lib/components/kasy_sidebar.dart +394 -25
- package/templates/firebase/lib/components/kasy_web_header.dart +11 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +2 -213
- package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -4
- package/templates/firebase/lib/core/chrome/web_header_scope.dart +20 -0
- package/templates/firebase/lib/core/data/api/user_api.dart +4 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
- package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +41 -0
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
- package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
- package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +498 -466
- package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
- package/templates/firebase/lib/features/home/home_components_page.dart +16 -6
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +7 -0
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +23 -13
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
- package/templates/firebase/lib/features/settings/settings_page.dart +51 -38
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +549 -376
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +266 -141
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +23 -30
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +30 -49
- package/templates/firebase/lib/i18n/en.i18n.json +23 -9
- package/templates/firebase/lib/i18n/es.i18n.json +23 -9
- package/templates/firebase/lib/i18n/pt.i18n.json +23 -9
- package/templates/firebase/lib/router.dart +43 -25
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/admin_shell_chrome_test.dart +104 -0
- package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import 'package:kasy_kit/features/subscriptions/ui/component/premium_page_factory.dart';
|
|
2
2
|
|
|
3
|
-
///
|
|
3
|
+
/// The four "Ferramentas" sub-screens are real admin SECTIONS: each is its own
|
|
4
|
+
/// branch in the console's [StatefulShellRoute] (so the rail persists and it
|
|
5
|
+
/// gets the standard header chrome) and is reached from the sidebar's
|
|
6
|
+
/// "Ferramentas" submenu, not pushed. These paths are the branch locations —
|
|
7
|
+
/// `adminSections()` reads them so the router and the rail can't drift.
|
|
4
8
|
///
|
|
5
|
-
///
|
|
6
|
-
|
|
9
|
+
/// Send push and Paywalls are admin actions (production); Components and Debug
|
|
10
|
+
/// are developer-only (their branches register only when [kDebugMode] is true).
|
|
11
|
+
const String adminRouteSendPush = '/admin/tools/send-push';
|
|
12
|
+
const String adminRoutePaywalls = '/admin/tools/paywalls';
|
|
13
|
+
const String adminRouteComponents = '/admin/tools/components';
|
|
14
|
+
const String adminRouteDebug = '/admin/tools/debug';
|
|
15
|
+
|
|
16
|
+
/// Full-screen pushed routes (debug-only), reached from inside the Debug
|
|
17
|
+
/// section as drill-downs (they cover the shell and pop back to it).
|
|
7
18
|
const String adminRouteHomeWidgets = '/admin/home-widgets';
|
|
8
|
-
const String adminRouteSendPush = '/admin/send-push';
|
|
9
19
|
|
|
10
20
|
String adminRoutePremiumPreview(String variant) => '/admin/premium/$variant';
|
|
11
21
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
-
import 'package:go_router/go_router.dart';
|
|
4
|
-
import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
5
3
|
import 'package:kasy_kit/components/kasy_button.dart';
|
|
4
|
+
import 'package:kasy_kit/components/kasy_tabs.dart';
|
|
6
5
|
import 'package:kasy_kit/components/kasy_text_area.dart';
|
|
7
6
|
import 'package:kasy_kit/components/kasy_text_field.dart';
|
|
8
7
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
@@ -10,7 +9,6 @@ import 'package:kasy_kit/core/toast/toast_service.dart';
|
|
|
10
9
|
import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
|
|
11
10
|
import 'package:kasy_kit/features/notifications/api/notifications_api.dart';
|
|
12
11
|
import 'package:kasy_kit/features/settings/ui/components/admin/send_push_notifier.dart';
|
|
13
|
-
import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
|
|
14
12
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
15
13
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
16
14
|
|
|
@@ -78,6 +76,21 @@ class _SendPushNotificationPageState
|
|
|
78
76
|
setState(() => _emails.remove(email));
|
|
79
77
|
}
|
|
80
78
|
|
|
79
|
+
/// Clears every field after a successful send. The screen is a console
|
|
80
|
+
/// section (reached from the sidebar), so there is nothing to pop — we reset
|
|
81
|
+
/// in place so the admin can fire another notification right away.
|
|
82
|
+
void _resetForm() {
|
|
83
|
+
_titleCtrl.clear();
|
|
84
|
+
_bodyCtrl.clear();
|
|
85
|
+
_imageCtrl.clear();
|
|
86
|
+
_routeCtrl.clear();
|
|
87
|
+
_emailCtrl.clear();
|
|
88
|
+
setState(() {
|
|
89
|
+
_emails.clear();
|
|
90
|
+
_sendToAll = false;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
bool get _canSend {
|
|
82
95
|
if (_titleCtrl.text.trim().isEmpty) return false;
|
|
83
96
|
if (_bodyCtrl.text.trim().isEmpty) return false;
|
|
@@ -128,159 +141,271 @@ class _SendPushNotificationPageState
|
|
|
128
141
|
ref
|
|
129
142
|
.read(toastProvider)
|
|
130
143
|
.success(title: tr.send_push_title, text: tr.send_push_success);
|
|
131
|
-
|
|
144
|
+
_resetForm();
|
|
132
145
|
}
|
|
133
146
|
});
|
|
134
147
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
Widget buildSubmit({required bool expand}) {
|
|
149
|
+
final KasyButton button = KasyButton(
|
|
150
|
+
label: tr.send_push_send_button,
|
|
151
|
+
expand: expand,
|
|
152
|
+
isLoading: state is AsyncLoading,
|
|
153
|
+
onPressed: _canSend ? _send : null,
|
|
154
|
+
);
|
|
155
|
+
// Disabled: tapping explains what's missing instead of doing nothing.
|
|
156
|
+
return _canSend
|
|
157
|
+
? button
|
|
158
|
+
: GestureDetector(onTap: _onSendAttempt, child: button);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Recipients: a segmented audience selector (everyone / specific) over the
|
|
162
|
+
// e-mail list, instead of a lone switch — reads as a deliberate choice.
|
|
163
|
+
final Widget recipients = Column(
|
|
164
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
165
|
+
mainAxisSize: MainAxisSize.min,
|
|
166
|
+
children: [
|
|
167
|
+
KasyTabs(
|
|
168
|
+
tabs: [tr.send_push_audience_all, tr.send_push_audience_specific],
|
|
169
|
+
selectedIndex: _sendToAll ? 0 : 1,
|
|
170
|
+
onTabSelected: (i) => setState(() => _sendToAll = i == 0),
|
|
171
|
+
mode: KasyTabsMode.fill,
|
|
172
|
+
),
|
|
173
|
+
const SizedBox(height: KasySpacing.md),
|
|
174
|
+
if (_sendToAll)
|
|
175
|
+
Text(
|
|
176
|
+
tr.send_push_audience_all_hint,
|
|
177
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
178
|
+
color: context.colors.muted,
|
|
179
|
+
height: 1.35,
|
|
180
|
+
),
|
|
141
181
|
)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
182
|
+
else
|
|
183
|
+
_EmailsSection(
|
|
184
|
+
emails: _emails,
|
|
185
|
+
controller: _emailCtrl,
|
|
186
|
+
focusNode: _emailFocus,
|
|
187
|
+
onAdd: _addEmail,
|
|
188
|
+
onRemove: _removeEmail,
|
|
189
|
+
label: tr.send_push_email_label,
|
|
190
|
+
hint: tr.send_push_email_hint,
|
|
191
|
+
),
|
|
192
|
+
],
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
final Widget contentFields = Column(
|
|
196
|
+
mainAxisSize: MainAxisSize.min,
|
|
197
|
+
children: [
|
|
198
|
+
KasyTextField(
|
|
199
|
+
label: tr.send_push_title_label,
|
|
200
|
+
hint: tr.send_push_title_hint,
|
|
201
|
+
controller: _titleCtrl,
|
|
202
|
+
focusNode: _titleFocus,
|
|
203
|
+
maxLength: 64,
|
|
204
|
+
showRequiredIndicator: true,
|
|
205
|
+
textInputAction: TextInputAction.next,
|
|
206
|
+
onChanged: (_) => setState(() {}),
|
|
207
|
+
onEditingComplete: () => _bodyFocus.requestFocus(),
|
|
208
|
+
),
|
|
209
|
+
const SizedBox(height: KasySpacing.md),
|
|
210
|
+
KasyTextArea(
|
|
211
|
+
label: tr.send_push_body_label,
|
|
212
|
+
hint: tr.send_push_body_hint,
|
|
213
|
+
controller: _bodyCtrl,
|
|
214
|
+
focusNode: _bodyFocus,
|
|
215
|
+
maxLength: 250,
|
|
216
|
+
showRequiredIndicator: true,
|
|
217
|
+
minLines: 3,
|
|
218
|
+
maxLines: 5,
|
|
219
|
+
onChanged: (_) => setState(() {}),
|
|
220
|
+
),
|
|
221
|
+
const SizedBox(height: KasySpacing.md),
|
|
222
|
+
KasyTextField(
|
|
223
|
+
label: tr.send_push_image_label,
|
|
224
|
+
hint: tr.send_push_image_hint,
|
|
225
|
+
controller: _imageCtrl,
|
|
226
|
+
focusNode: _imageFocus,
|
|
227
|
+
onChanged: (_) => setState(() {}),
|
|
228
|
+
textInputAction: TextInputAction.next,
|
|
229
|
+
onEditingComplete: () => _routeFocus.requestFocus(),
|
|
230
|
+
),
|
|
231
|
+
],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
final Widget advancedFields = KasyTextField(
|
|
235
|
+
label: tr.send_push_route_label,
|
|
236
|
+
hint: tr.send_push_route_hint,
|
|
237
|
+
controller: _routeCtrl,
|
|
238
|
+
focusNode: _routeFocus,
|
|
239
|
+
textInputAction: TextInputAction.done,
|
|
240
|
+
onEditingComplete: () => _routeFocus.unfocus(),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
final Widget formColumn = Column(
|
|
244
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
245
|
+
children: [
|
|
246
|
+
_FormSection(label: tr.send_push_section_recipients, child: recipients),
|
|
247
|
+
const SizedBox(height: KasySpacing.lg),
|
|
248
|
+
_FormSection(label: tr.send_push_section_content, child: contentFields),
|
|
249
|
+
const SizedBox(height: KasySpacing.lg),
|
|
250
|
+
_FormSection(label: tr.send_push_section_advanced, child: advancedFields),
|
|
251
|
+
// Send closes the form (route is optional), so the button flows right
|
|
252
|
+
// after the last section and scrolls with the content — not pinned.
|
|
253
|
+
const SizedBox(height: KasySpacing.lg),
|
|
254
|
+
buildSubmit(expand: true),
|
|
255
|
+
],
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
final Widget previewPanel = Column(
|
|
259
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
260
|
+
mainAxisSize: MainAxisSize.min,
|
|
261
|
+
children: [
|
|
262
|
+
Padding(
|
|
263
|
+
padding: const EdgeInsets.only(
|
|
264
|
+
left: KasySpacing.xs,
|
|
265
|
+
bottom: KasySpacing.smd,
|
|
266
|
+
),
|
|
267
|
+
child: Text(
|
|
268
|
+
tr.send_push_preview_label.toUpperCase(),
|
|
269
|
+
style: context.kasyTextTheme.sectionLabel.copyWith(
|
|
270
|
+
color: context.colors.muted,
|
|
148
271
|
),
|
|
149
|
-
)
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
_NotificationPreview(
|
|
275
|
+
appName: _appName,
|
|
276
|
+
title: _titleCtrl.text,
|
|
277
|
+
body: _bodyCtrl.text,
|
|
278
|
+
imageUrl: _imageCtrl.text,
|
|
279
|
+
),
|
|
280
|
+
],
|
|
281
|
+
);
|
|
150
282
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
_NotificationPreview(
|
|
191
|
-
appName: _appName,
|
|
192
|
-
title: _titleCtrl.text,
|
|
193
|
-
body: _bodyCtrl.text,
|
|
194
|
-
imageUrl: _imageCtrl.text,
|
|
195
|
-
),
|
|
196
|
-
const SizedBox(height: KasySpacing.lg),
|
|
197
|
-
SettingsSwitchTile(
|
|
198
|
-
icon: KasyIcons.notificationActive,
|
|
199
|
-
title: tr.send_push_to_all,
|
|
200
|
-
value: _sendToAll,
|
|
201
|
-
onChanged: (v) => setState(() => _sendToAll = v),
|
|
202
|
-
),
|
|
203
|
-
const SettingsDivider(),
|
|
204
|
-
const SizedBox(height: KasySpacing.lg),
|
|
205
|
-
if (!_sendToAll) ...[
|
|
206
|
-
_EmailsSection(
|
|
207
|
-
emails: _emails,
|
|
208
|
-
controller: _emailCtrl,
|
|
209
|
-
focusNode: _emailFocus,
|
|
210
|
-
onAdd: _addEmail,
|
|
211
|
-
onRemove: _removeEmail,
|
|
212
|
-
label: tr.send_push_email_label,
|
|
213
|
-
hint: tr.send_push_email_hint,
|
|
214
|
-
),
|
|
215
|
-
const SizedBox(height: KasySpacing.lg),
|
|
216
|
-
],
|
|
217
|
-
KasyTextField(
|
|
218
|
-
label: tr.send_push_title_label,
|
|
219
|
-
hint: tr.send_push_title_hint,
|
|
220
|
-
controller: _titleCtrl,
|
|
221
|
-
focusNode: _titleFocus,
|
|
222
|
-
maxLength: 64,
|
|
223
|
-
showRequiredIndicator: true,
|
|
224
|
-
textInputAction: TextInputAction.next,
|
|
225
|
-
onChanged: (_) => setState(() {}),
|
|
226
|
-
onEditingComplete: () => _bodyFocus.requestFocus(),
|
|
227
|
-
),
|
|
228
|
-
const SizedBox(height: KasySpacing.md),
|
|
229
|
-
KasyTextArea(
|
|
230
|
-
label: tr.send_push_body_label,
|
|
231
|
-
hint: tr.send_push_body_hint,
|
|
232
|
-
controller: _bodyCtrl,
|
|
233
|
-
focusNode: _bodyFocus,
|
|
234
|
-
maxLength: 250,
|
|
235
|
-
showRequiredIndicator: true,
|
|
236
|
-
minLines: 3,
|
|
237
|
-
maxLines: 5,
|
|
238
|
-
onChanged: (_) => setState(() {}),
|
|
239
|
-
),
|
|
240
|
-
const SizedBox(height: KasySpacing.md),
|
|
241
|
-
KasyTextField(
|
|
242
|
-
label: tr.send_push_image_label,
|
|
243
|
-
hint: tr.send_push_image_hint,
|
|
244
|
-
controller: _imageCtrl,
|
|
245
|
-
focusNode: _imageFocus,
|
|
246
|
-
onChanged: (_) => setState(() {}),
|
|
247
|
-
textInputAction: TextInputAction.next,
|
|
248
|
-
onEditingComplete: () => _routeFocus.requestFocus(),
|
|
249
|
-
),
|
|
250
|
-
const SizedBox(height: KasySpacing.md),
|
|
251
|
-
KasyTextField(
|
|
252
|
-
label: tr.send_push_route_label,
|
|
253
|
-
hint: tr.send_push_route_hint,
|
|
254
|
-
controller: _routeCtrl,
|
|
255
|
-
focusNode: _routeFocus,
|
|
256
|
-
textInputAction: TextInputAction.done,
|
|
257
|
-
onEditingComplete: () => _routeFocus.unfocus(),
|
|
283
|
+
const double gutter = KasySpacing.pageHorizontalGutter;
|
|
284
|
+
|
|
285
|
+
// Body-only: the admin shell supplies the chrome (web header on desktop,
|
|
286
|
+
// titled app bar on tablet/phone). Wide viewports get two columns (form +
|
|
287
|
+
// a fixed live preview); narrow stacks the preview on top. The send button
|
|
288
|
+
// flows at the end of the form and scrolls with it (not pinned).
|
|
289
|
+
return LayoutBuilder(
|
|
290
|
+
builder: (context, constraints) {
|
|
291
|
+
final bool wide = constraints.maxWidth >= 820;
|
|
292
|
+
// Clear the home indicator / nav bar below the last control.
|
|
293
|
+
final double bottomInset =
|
|
294
|
+
MediaQuery.paddingOf(context).bottom + KasySpacing.xl;
|
|
295
|
+
|
|
296
|
+
SingleChildScrollView scroll({
|
|
297
|
+
required EdgeInsets padding,
|
|
298
|
+
required Widget child,
|
|
299
|
+
}) => SingleChildScrollView(
|
|
300
|
+
// Drag down to dismiss keyboard — the professional standard.
|
|
301
|
+
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
|
302
|
+
padding: padding,
|
|
303
|
+
child: child,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
// Wide: only the form scrolls; the live preview stays FIXED beside it.
|
|
307
|
+
// Narrow: the preview sits on top and the whole stack scrolls.
|
|
308
|
+
final Widget bodyContent = wide
|
|
309
|
+
? Center(
|
|
310
|
+
child: ConstrainedBox(
|
|
311
|
+
constraints: const BoxConstraints(maxWidth: 1100),
|
|
312
|
+
child: Row(
|
|
313
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
314
|
+
children: [
|
|
315
|
+
Expanded(
|
|
316
|
+
child: scroll(
|
|
317
|
+
padding: EdgeInsets.fromLTRB(
|
|
318
|
+
gutter,
|
|
319
|
+
KasySpacing.belowChromeContentGap,
|
|
320
|
+
0,
|
|
321
|
+
bottomInset,
|
|
258
322
|
),
|
|
259
|
-
|
|
323
|
+
child: formColumn,
|
|
324
|
+
),
|
|
260
325
|
),
|
|
261
|
-
|
|
326
|
+
const SizedBox(width: KasySpacing.xl),
|
|
327
|
+
Padding(
|
|
328
|
+
padding: const EdgeInsets.fromLTRB(
|
|
329
|
+
0,
|
|
330
|
+
KasySpacing.belowChromeContentGap,
|
|
331
|
+
gutter,
|
|
332
|
+
KasySpacing.xl,
|
|
333
|
+
),
|
|
334
|
+
child: SizedBox(width: 340, child: previewPanel),
|
|
335
|
+
),
|
|
336
|
+
],
|
|
337
|
+
),
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
: scroll(
|
|
341
|
+
padding: EdgeInsets.fromLTRB(
|
|
342
|
+
gutter,
|
|
343
|
+
KasySpacing.belowChromeContentGap,
|
|
344
|
+
gutter,
|
|
345
|
+
bottomInset,
|
|
346
|
+
),
|
|
347
|
+
child: Column(
|
|
348
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
349
|
+
children: [
|
|
350
|
+
previewPanel,
|
|
351
|
+
const SizedBox(height: KasySpacing.lg),
|
|
352
|
+
formColumn,
|
|
262
353
|
],
|
|
263
354
|
),
|
|
264
|
-
)
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
return ScrollConfiguration(
|
|
358
|
+
behavior: const KasyKitScrollBehavior(),
|
|
359
|
+
child: bodyContent,
|
|
360
|
+
);
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// A labelled card grouping form fields — the console's clean section look
|
|
367
|
+
/// (uppercase label + surface card with a hairline border and the component
|
|
368
|
+
/// shadow), so Send push reads as the same product as the rest of the admin.
|
|
369
|
+
class _FormSection extends StatelessWidget {
|
|
370
|
+
final String label;
|
|
371
|
+
final Widget child;
|
|
372
|
+
const _FormSection({required this.label, required this.child});
|
|
373
|
+
|
|
374
|
+
@override
|
|
375
|
+
Widget build(BuildContext context) {
|
|
376
|
+
return Column(
|
|
377
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
378
|
+
mainAxisSize: MainAxisSize.min,
|
|
379
|
+
children: [
|
|
380
|
+
Padding(
|
|
381
|
+
padding: const EdgeInsets.only(
|
|
382
|
+
left: KasySpacing.xs,
|
|
383
|
+
bottom: KasySpacing.smd,
|
|
384
|
+
),
|
|
385
|
+
child: Text(
|
|
386
|
+
label.toUpperCase(),
|
|
387
|
+
style: context.kasyTextTheme.sectionLabel.copyWith(
|
|
388
|
+
color: context.colors.muted,
|
|
265
389
|
),
|
|
266
390
|
),
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
391
|
+
),
|
|
392
|
+
DecoratedBox(
|
|
393
|
+
decoration: BoxDecoration(
|
|
394
|
+
color: context.colors.surface,
|
|
395
|
+
borderRadius: BorderRadius.circular(18),
|
|
396
|
+
border: Border.all(
|
|
397
|
+
color: context.colors.outline.withValues(
|
|
398
|
+
alpha: context.isDark ? 0.45 : 0.6,
|
|
399
|
+
),
|
|
274
400
|
),
|
|
401
|
+
boxShadow: [KasyShadows.component(context)],
|
|
275
402
|
),
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
bottom: MediaQuery.paddingOf(context).bottom + KasySpacing.sm,
|
|
280
|
-
child: submitButton,
|
|
403
|
+
child: Padding(
|
|
404
|
+
padding: const EdgeInsets.all(KasySpacing.md),
|
|
405
|
+
child: child,
|
|
281
406
|
),
|
|
282
|
-
|
|
283
|
-
|
|
407
|
+
),
|
|
408
|
+
],
|
|
284
409
|
);
|
|
285
410
|
}
|
|
286
411
|
}
|
|
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|
|
5
5
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
6
6
|
import 'package:image_picker/image_picker.dart';
|
|
7
7
|
import 'package:kasy_kit/components/kasy_avatar.dart';
|
|
8
|
+
import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
|
|
8
9
|
import 'package:kasy_kit/core/data/entities/upload_result.dart';
|
|
9
10
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
10
11
|
import 'package:kasy_kit/core/data/repositories/user_repository.dart';
|
|
@@ -12,6 +13,7 @@ import 'package:kasy_kit/core/states/models/user_state.dart';
|
|
|
12
13
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
13
14
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
14
15
|
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
16
|
+
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
15
17
|
import 'package:kasy_kit/features/settings/ui/widgets/avatar_utils.dart';
|
|
16
18
|
import 'package:kasy_kit/features/settings/ui/widgets/round_progress.dart';
|
|
17
19
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
@@ -144,14 +146,10 @@ class _EditableUserAvatarState extends ConsumerState<EditableUserAvatar> {
|
|
|
144
146
|
BuildContext context, {
|
|
145
147
|
required bool hasAvatar,
|
|
146
148
|
}) {
|
|
147
|
-
return
|
|
149
|
+
return showKasyBottomSheet<_AvatarAction>(
|
|
148
150
|
context: context,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
shape: const RoundedRectangleBorder(
|
|
152
|
-
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
|
153
|
-
),
|
|
154
|
-
builder: (sheetContext) => SafeArea(
|
|
151
|
+
builder: (sheetContext) => KasySheetSurface(
|
|
152
|
+
showDragHandle: false,
|
|
155
153
|
child: Padding(
|
|
156
154
|
padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
|
|
157
155
|
child: Column(
|
|
@@ -321,31 +319,26 @@ class _BottomSheetTile extends StatelessWidget {
|
|
|
321
319
|
@override
|
|
322
320
|
Widget build(BuildContext context) {
|
|
323
321
|
final Color fg = color ?? context.colors.onSurface;
|
|
324
|
-
return
|
|
325
|
-
|
|
322
|
+
return KasyHover(
|
|
323
|
+
onTap: onTap,
|
|
324
|
+
focusable: true,
|
|
325
|
+
focusGapColor: context.colors.surface,
|
|
326
326
|
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const SizedBox(width: KasySpacing.sm),
|
|
341
|
-
],
|
|
342
|
-
Text(
|
|
343
|
-
label,
|
|
344
|
-
style: context.textTheme.bodyLarge?.copyWith(color: fg),
|
|
345
|
-
),
|
|
346
|
-
],
|
|
327
|
+
padding: const EdgeInsets.symmetric(
|
|
328
|
+
horizontal: KasySpacing.md,
|
|
329
|
+
vertical: KasySpacing.smd,
|
|
330
|
+
),
|
|
331
|
+
child: Row(
|
|
332
|
+
children: [
|
|
333
|
+
if (icon != null) ...[
|
|
334
|
+
Icon(icon, size: KasyIconSize.lg, color: fg),
|
|
335
|
+
const SizedBox(width: KasySpacing.sm),
|
|
336
|
+
],
|
|
337
|
+
Text(
|
|
338
|
+
label,
|
|
339
|
+
style: context.textTheme.bodyLarge?.copyWith(color: fg),
|
|
347
340
|
),
|
|
348
|
-
|
|
341
|
+
],
|
|
349
342
|
),
|
|
350
343
|
);
|
|
351
344
|
}
|
|
@@ -2,10 +2,12 @@ import 'dart:async';
|
|
|
2
2
|
|
|
3
3
|
import 'package:flutter/material.dart';
|
|
4
4
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
5
|
+
import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
|
|
5
6
|
import 'package:kasy_kit/core/home_widgets/home_widget_mywidget_service.dart';
|
|
6
7
|
import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
|
|
7
8
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
8
9
|
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
10
|
+
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
9
11
|
import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
|
|
10
12
|
import 'package:kasy_kit/i18n/app_locale_display.dart';
|
|
11
13
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
@@ -63,14 +65,9 @@ class LanguageSwitcher extends ConsumerWidget {
|
|
|
63
65
|
AppLocale current,
|
|
64
66
|
) {
|
|
65
67
|
final String sheetTitle = context.t.settings.language_title;
|
|
66
|
-
|
|
68
|
+
showKasyBottomSheet<void>(
|
|
67
69
|
context: context,
|
|
68
|
-
|
|
69
|
-
backgroundColor: context.colors.surface,
|
|
70
|
-
shape: const RoundedRectangleBorder(
|
|
71
|
-
borderRadius: BorderRadius.vertical(top: Radius.circular(KasyRadius.lg)),
|
|
72
|
-
),
|
|
73
|
-
builder: (sheetContext) => SafeArea(
|
|
70
|
+
builder: (sheetContext) => KasySheetSurface(
|
|
74
71
|
child: Padding(
|
|
75
72
|
padding: const EdgeInsets.fromLTRB(
|
|
76
73
|
KasySpacing.md,
|
|
@@ -81,15 +78,6 @@ class LanguageSwitcher extends ConsumerWidget {
|
|
|
81
78
|
child: Column(
|
|
82
79
|
mainAxisSize: MainAxisSize.min,
|
|
83
80
|
children: <Widget>[
|
|
84
|
-
Container(
|
|
85
|
-
width: 36,
|
|
86
|
-
height: 4,
|
|
87
|
-
margin: const EdgeInsets.only(bottom: KasySpacing.md),
|
|
88
|
-
decoration: BoxDecoration(
|
|
89
|
-
color: sheetContext.colors.onSurface.withValues(alpha: 0.18),
|
|
90
|
-
borderRadius: BorderRadius.circular(2),
|
|
91
|
-
),
|
|
92
|
-
),
|
|
93
81
|
Align(
|
|
94
82
|
alignment: Alignment.centerLeft,
|
|
95
83
|
child: Text(
|
|
@@ -156,42 +144,35 @@ class _LocaleOptionTile extends StatelessWidget {
|
|
|
156
144
|
final Color primary = context.colors.primary;
|
|
157
145
|
final Color fg = isSelected ? primary : context.colors.onSurface;
|
|
158
146
|
|
|
159
|
-
return
|
|
160
|
-
|
|
147
|
+
return KasyHover(
|
|
148
|
+
onTap: onTap,
|
|
149
|
+
focusable: true,
|
|
161
150
|
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
child: Text(
|
|
175
|
-
locale.nativeName,
|
|
176
|
-
style: context.textTheme.bodyLarge?.copyWith(
|
|
177
|
-
color: fg,
|
|
178
|
-
fontWeight:
|
|
179
|
-
isSelected ? FontWeight.w600 : FontWeight.w400,
|
|
180
|
-
),
|
|
181
|
-
),
|
|
151
|
+
padding: const EdgeInsets.symmetric(
|
|
152
|
+
horizontal: KasySpacing.xs,
|
|
153
|
+
vertical: KasySpacing.smd,
|
|
154
|
+
),
|
|
155
|
+
child: Row(
|
|
156
|
+
children: <Widget>[
|
|
157
|
+
Expanded(
|
|
158
|
+
child: Text(
|
|
159
|
+
locale.nativeName,
|
|
160
|
+
style: context.textTheme.bodyLarge?.copyWith(
|
|
161
|
+
color: fg,
|
|
162
|
+
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
|
182
163
|
),
|
|
183
|
-
|
|
184
|
-
Container(
|
|
185
|
-
width: 10,
|
|
186
|
-
height: 10,
|
|
187
|
-
decoration: BoxDecoration(
|
|
188
|
-
color: primary,
|
|
189
|
-
shape: BoxShape.circle,
|
|
190
|
-
),
|
|
191
|
-
),
|
|
192
|
-
],
|
|
164
|
+
),
|
|
193
165
|
),
|
|
194
|
-
|
|
166
|
+
if (isSelected)
|
|
167
|
+
Container(
|
|
168
|
+
width: 10,
|
|
169
|
+
height: 10,
|
|
170
|
+
decoration: BoxDecoration(
|
|
171
|
+
color: primary,
|
|
172
|
+
shape: BoxShape.circle,
|
|
173
|
+
),
|
|
174
|
+
),
|
|
175
|
+
],
|
|
195
176
|
),
|
|
196
177
|
);
|
|
197
178
|
}
|