kasy-cli 1.39.1 → 1.40.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/lib/scaffold/CHANGELOG.json +23 -0
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +27 -27
- package/templates/firebase/lib/components/components.dart +2 -0
- package/templates/firebase/lib/components/kasy_accordion.dart +4 -1
- package/templates/firebase/lib/components/kasy_alert.dart +5 -2
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +25 -7
- package/templates/firebase/lib/components/kasy_dialog.dart +3 -1
- package/templates/firebase/lib/components/kasy_menu.dart +926 -0
- package/templates/firebase/lib/components/kasy_popover.dart +267 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +20 -10
- package/templates/firebase/lib/core/navigation/kasy_route_observer.dart +8 -0
- package/templates/firebase/lib/features/home/home_components_page.dart +23 -4
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +320 -0
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +38 -94
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +32 -107
- package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +21 -21
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +35 -27
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +22 -17
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +12 -7
- package/templates/firebase/lib/router.dart +2 -0
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +0 -81
|
@@ -332,6 +332,21 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
332
332
|
),
|
|
333
333
|
],
|
|
334
334
|
);
|
|
335
|
+
case 'Menu':
|
|
336
|
+
return const ComponentPreviewDefinition(
|
|
337
|
+
title: 'Menu',
|
|
338
|
+
variants: [
|
|
339
|
+
ComponentPreviewVariant(
|
|
340
|
+
label: 'Basic usage',
|
|
341
|
+
builder: _buildMenuBasicUsage,
|
|
342
|
+
),
|
|
343
|
+
ComponentPreviewVariant(
|
|
344
|
+
label: 'Selection',
|
|
345
|
+
builder: _buildMenuStyles,
|
|
346
|
+
),
|
|
347
|
+
ComponentPreviewVariant(label: 'Submenu', builder: _buildMenuSubmenu),
|
|
348
|
+
],
|
|
349
|
+
);
|
|
335
350
|
case 'Badge':
|
|
336
351
|
return const ComponentPreviewDefinition(
|
|
337
352
|
title: 'Badge',
|
|
@@ -5417,6 +5432,311 @@ Widget _previewIntrinsicLaunch(Widget child) {
|
|
|
5417
5432
|
);
|
|
5418
5433
|
}
|
|
5419
5434
|
|
|
5435
|
+
// ── Menu (KasyMenu) previews ─────────────────────────────────────────────────
|
|
5436
|
+
// Each preset is triggered by a button — a real menu opens, anchored to it.
|
|
5437
|
+
// Most open as the default floating menu (popover, both platforms); one opens as
|
|
5438
|
+
// a bottom sheet to show the opt-in presentation.
|
|
5439
|
+
|
|
5440
|
+
// Basic usage menu — mirrors the HeroUI "Actions / Danger zone" dropdown
|
|
5441
|
+
// (Figma 17840:22571): grouped rows with an icon, title + description and a
|
|
5442
|
+
// keyboard shortcut, plus a destructive "Danger zone". The kit has no pencil
|
|
5443
|
+
// glyph yet, so Edit file borrows the note icon.
|
|
5444
|
+
const List<KasyMenuSection> _kMenuActions = [
|
|
5445
|
+
KasyMenuSection(
|
|
5446
|
+
label: 'Actions',
|
|
5447
|
+
items: [
|
|
5448
|
+
KasyMenuItem(
|
|
5449
|
+
icon: KasyIcons.add,
|
|
5450
|
+
title: 'New file',
|
|
5451
|
+
description: 'Create a new file',
|
|
5452
|
+
shortcut: '⌘ N',
|
|
5453
|
+
onTap: _triggerTapFeedback,
|
|
5454
|
+
),
|
|
5455
|
+
KasyMenuItem(
|
|
5456
|
+
icon: KasyIcons.copy,
|
|
5457
|
+
title: 'Copy link',
|
|
5458
|
+
description: 'Copy the file link',
|
|
5459
|
+
shortcut: '⌘ L',
|
|
5460
|
+
onTap: _triggerTapFeedback,
|
|
5461
|
+
),
|
|
5462
|
+
KasyMenuItem(
|
|
5463
|
+
icon: KasyIcons.note,
|
|
5464
|
+
title: 'Edit file',
|
|
5465
|
+
description: 'Make changes',
|
|
5466
|
+
shortcut: '⌘ E',
|
|
5467
|
+
onTap: _triggerTapFeedback,
|
|
5468
|
+
),
|
|
5469
|
+
],
|
|
5470
|
+
),
|
|
5471
|
+
KasyMenuSection(
|
|
5472
|
+
label: 'Danger zone',
|
|
5473
|
+
items: [
|
|
5474
|
+
KasyMenuItem(
|
|
5475
|
+
icon: KasyIcons.trash,
|
|
5476
|
+
title: 'Delete file',
|
|
5477
|
+
description: 'Move to trash',
|
|
5478
|
+
shortcut: '⌘ ⇧ K',
|
|
5479
|
+
tone: KasyMenuItemTone.danger,
|
|
5480
|
+
onTap: _triggerTapFeedback,
|
|
5481
|
+
),
|
|
5482
|
+
],
|
|
5483
|
+
),
|
|
5484
|
+
];
|
|
5485
|
+
|
|
5486
|
+
const List<KasyMenuSection> _kMenuSubmenu = [
|
|
5487
|
+
KasyMenuSection(
|
|
5488
|
+
items: [
|
|
5489
|
+
KasyMenuItem(
|
|
5490
|
+
icon: KasyIcons.share,
|
|
5491
|
+
title: 'Share',
|
|
5492
|
+
submenu: [
|
|
5493
|
+
KasyMenuSection(
|
|
5494
|
+
items: [
|
|
5495
|
+
KasyMenuItem(title: 'WhatsApp', onTap: _triggerTapFeedback),
|
|
5496
|
+
KasyMenuItem(title: 'Telegram', onTap: _triggerTapFeedback),
|
|
5497
|
+
KasyMenuItem(title: 'Email', onTap: _triggerTapFeedback),
|
|
5498
|
+
],
|
|
5499
|
+
),
|
|
5500
|
+
],
|
|
5501
|
+
),
|
|
5502
|
+
KasyMenuItem(
|
|
5503
|
+
icon: KasyIcons.copy,
|
|
5504
|
+
title: 'Copy link',
|
|
5505
|
+
onTap: _triggerTapFeedback,
|
|
5506
|
+
),
|
|
5507
|
+
],
|
|
5508
|
+
),
|
|
5509
|
+
];
|
|
5510
|
+
|
|
5511
|
+
|
|
5512
|
+
// Layout shared by the two interactive menu demos: the trigger floats centered
|
|
5513
|
+
// in the open area (so the menu has room to anchor against it) and a toggle is
|
|
5514
|
+
// pinned at the bottom, just above the variant title. Height is a slice of the
|
|
5515
|
+
// viewport, clamped, and kept under the preview height so it still centers.
|
|
5516
|
+
class _MenuDemoFrame extends StatelessWidget {
|
|
5517
|
+
final Widget trigger;
|
|
5518
|
+
final String controlTitle;
|
|
5519
|
+
final String controlSubtitle;
|
|
5520
|
+
final bool controlValue;
|
|
5521
|
+
final ValueChanged<bool> onControlChanged;
|
|
5522
|
+
|
|
5523
|
+
const _MenuDemoFrame({
|
|
5524
|
+
required this.trigger,
|
|
5525
|
+
required this.controlTitle,
|
|
5526
|
+
required this.controlSubtitle,
|
|
5527
|
+
required this.controlValue,
|
|
5528
|
+
required this.onControlChanged,
|
|
5529
|
+
});
|
|
5530
|
+
|
|
5531
|
+
@override
|
|
5532
|
+
Widget build(BuildContext context) {
|
|
5533
|
+
// Give the demo real vertical room so the control row can sit near the
|
|
5534
|
+
// bottom (like a settings row) instead of crowding under the trigger. The
|
|
5535
|
+
// trigger still lives in a plain centering Row — the menu anchors to its
|
|
5536
|
+
// render box via LayerLink, so the surrounding height never moves the menu.
|
|
5537
|
+
final double viewportHeight = MediaQuery.sizeOf(context).height;
|
|
5538
|
+
final double frameHeight = (viewportHeight - 260).clamp(320.0, 520.0);
|
|
5539
|
+
return SizedBox(
|
|
5540
|
+
height: frameHeight,
|
|
5541
|
+
child: Column(
|
|
5542
|
+
children: [
|
|
5543
|
+
const Spacer(flex: 4),
|
|
5544
|
+
Row(mainAxisAlignment: MainAxisAlignment.center, children: [trigger]),
|
|
5545
|
+
const Spacer(flex: 6),
|
|
5546
|
+
Row(
|
|
5547
|
+
children: [
|
|
5548
|
+
Expanded(
|
|
5549
|
+
child: Column(
|
|
5550
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5551
|
+
mainAxisSize: MainAxisSize.min,
|
|
5552
|
+
children: [
|
|
5553
|
+
Text(
|
|
5554
|
+
controlTitle,
|
|
5555
|
+
style: context.textTheme.titleSmall?.copyWith(
|
|
5556
|
+
color: context.colors.onSurface,
|
|
5557
|
+
),
|
|
5558
|
+
),
|
|
5559
|
+
Text(
|
|
5560
|
+
controlSubtitle,
|
|
5561
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
5562
|
+
color: context.colors.muted,
|
|
5563
|
+
),
|
|
5564
|
+
),
|
|
5565
|
+
],
|
|
5566
|
+
),
|
|
5567
|
+
),
|
|
5568
|
+
// The same switch the Settings screen uses: adaptive (Cupertino on
|
|
5569
|
+
// iOS, Material elsewhere — identical look per platform), scaled
|
|
5570
|
+
// down, themed to the primary color.
|
|
5571
|
+
Transform.scale(
|
|
5572
|
+
scale: 0.82,
|
|
5573
|
+
child: Switch.adaptive(
|
|
5574
|
+
value: controlValue,
|
|
5575
|
+
onChanged: onControlChanged,
|
|
5576
|
+
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
5577
|
+
),
|
|
5578
|
+
),
|
|
5579
|
+
],
|
|
5580
|
+
),
|
|
5581
|
+
],
|
|
5582
|
+
),
|
|
5583
|
+
);
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
|
|
5587
|
+
Widget _buildMenuBasicUsage(BuildContext context) => const _MenuBasicUsageDemo();
|
|
5588
|
+
|
|
5589
|
+
// First preview: the trigger + a "Bottom Sheet" toggle. Off → the menu opens as
|
|
5590
|
+
// a popover anchored to the button; on → it slides up as a bottom sheet. The
|
|
5591
|
+
// same compact menu either way — only the container changes.
|
|
5592
|
+
class _MenuBasicUsageDemo extends StatefulWidget {
|
|
5593
|
+
const _MenuBasicUsageDemo();
|
|
5594
|
+
|
|
5595
|
+
@override
|
|
5596
|
+
State<_MenuBasicUsageDemo> createState() => _MenuBasicUsageDemoState();
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
class _MenuBasicUsageDemoState extends State<_MenuBasicUsageDemo> {
|
|
5600
|
+
bool _bottomSheet = false;
|
|
5601
|
+
|
|
5602
|
+
@override
|
|
5603
|
+
Widget build(BuildContext context) {
|
|
5604
|
+
return _MenuDemoFrame(
|
|
5605
|
+
controlTitle: 'Bottom Sheet',
|
|
5606
|
+
controlSubtitle: 'Toggle bottom sheet presentation',
|
|
5607
|
+
controlValue: _bottomSheet,
|
|
5608
|
+
onControlChanged: (bool v) => setState(() => _bottomSheet = v),
|
|
5609
|
+
trigger: KasyMenuAnchor(
|
|
5610
|
+
sections: _kMenuActions,
|
|
5611
|
+
// Wider panel: rows carry a title + description (HeroUI menu).
|
|
5612
|
+
width: 288,
|
|
5613
|
+
presentation: _bottomSheet
|
|
5614
|
+
? KasyMenuPresentation.bottomSheet
|
|
5615
|
+
: KasyMenuPresentation.popover,
|
|
5616
|
+
builder: (BuildContext context, VoidCallback open) => KasyButton(
|
|
5617
|
+
label: 'Actions',
|
|
5618
|
+
variant: KasyButtonVariant.soft,
|
|
5619
|
+
onPressed: () {
|
|
5620
|
+
_triggerTapFeedback();
|
|
5621
|
+
open();
|
|
5622
|
+
},
|
|
5623
|
+
),
|
|
5624
|
+
),
|
|
5625
|
+
);
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
Widget _buildMenuStyles(BuildContext context) => const _MenuSelectionDemo();
|
|
5630
|
+
|
|
5631
|
+
// Second preview: a real, interactive selection menu. Tapping a "Text Style"
|
|
5632
|
+
// row toggles its check (multi-select); tapping a "Text Alignment" row moves the
|
|
5633
|
+
// filled dot (single-select). The selection persists across reopens. A "Should
|
|
5634
|
+
// Close On Select" toggle controls whether picking an item dismisses the menu:
|
|
5635
|
+
// off → keep picking, tap outside to close (HeroUI editor menu); on → one pick
|
|
5636
|
+
// closes it.
|
|
5637
|
+
class _MenuSelectionDemo extends StatefulWidget {
|
|
5638
|
+
const _MenuSelectionDemo();
|
|
5639
|
+
|
|
5640
|
+
@override
|
|
5641
|
+
State<_MenuSelectionDemo> createState() => _MenuSelectionDemoState();
|
|
5642
|
+
}
|
|
5643
|
+
|
|
5644
|
+
class _MenuSelectionDemoState extends State<_MenuSelectionDemo> {
|
|
5645
|
+
// Multi-select set + single-select value, kept on the demo so reopening the
|
|
5646
|
+
// menu shows the last selection.
|
|
5647
|
+
final Set<String> _styles = <String>{'Bold', 'Underline'};
|
|
5648
|
+
String _align = 'Right';
|
|
5649
|
+
bool _closeOnSelect = false;
|
|
5650
|
+
|
|
5651
|
+
static const List<(String, String)> _styleItems = [
|
|
5652
|
+
('Bold', '⌘ B'),
|
|
5653
|
+
('Italic', '⌘ I'),
|
|
5654
|
+
('Underline', '⌘ U'),
|
|
5655
|
+
];
|
|
5656
|
+
static const List<(String, String)> _alignItems = [
|
|
5657
|
+
('Left', '⌥ A'),
|
|
5658
|
+
('Center', '⌥ H'),
|
|
5659
|
+
('Right', '⌥ D'),
|
|
5660
|
+
];
|
|
5661
|
+
|
|
5662
|
+
void _toggleStyle(String title) {
|
|
5663
|
+
setState(() {
|
|
5664
|
+
if (!_styles.remove(title)) _styles.add(title);
|
|
5665
|
+
});
|
|
5666
|
+
}
|
|
5667
|
+
|
|
5668
|
+
void _selectAlign(String title) => setState(() => _align = title);
|
|
5669
|
+
|
|
5670
|
+
// Built from state so each tap (via setState) rebuilds the anchor and the
|
|
5671
|
+
// open menu reflects the new selection in place.
|
|
5672
|
+
List<KasyMenuSection> _buildSections() => [
|
|
5673
|
+
KasyMenuSection(
|
|
5674
|
+
label: 'Text Style',
|
|
5675
|
+
selectionMode: KasyMenuSelectionMode.multiple,
|
|
5676
|
+
items: [
|
|
5677
|
+
for (final (String title, String shortcut) in _styleItems)
|
|
5678
|
+
KasyMenuItem(
|
|
5679
|
+
title: title,
|
|
5680
|
+
shortcut: shortcut,
|
|
5681
|
+
selected: _styles.contains(title),
|
|
5682
|
+
onTap: () => _toggleStyle(title),
|
|
5683
|
+
),
|
|
5684
|
+
],
|
|
5685
|
+
),
|
|
5686
|
+
KasyMenuSection(
|
|
5687
|
+
label: 'Text Alignment',
|
|
5688
|
+
selectionMode: KasyMenuSelectionMode.single,
|
|
5689
|
+
items: [
|
|
5690
|
+
for (final (String title, String shortcut) in _alignItems)
|
|
5691
|
+
KasyMenuItem(
|
|
5692
|
+
title: title,
|
|
5693
|
+
shortcut: shortcut,
|
|
5694
|
+
selected: _align == title,
|
|
5695
|
+
onTap: () => _selectAlign(title),
|
|
5696
|
+
),
|
|
5697
|
+
],
|
|
5698
|
+
),
|
|
5699
|
+
];
|
|
5700
|
+
|
|
5701
|
+
@override
|
|
5702
|
+
Widget build(BuildContext context) {
|
|
5703
|
+
return _MenuDemoFrame(
|
|
5704
|
+
controlTitle: 'Should Close On Select',
|
|
5705
|
+
controlSubtitle: 'Toggle should close on select',
|
|
5706
|
+
controlValue: _closeOnSelect,
|
|
5707
|
+
onControlChanged: (bool v) => setState(() => _closeOnSelect = v),
|
|
5708
|
+
trigger: KasyMenuAnchor(
|
|
5709
|
+
sections: _buildSections(),
|
|
5710
|
+
closeOnSelect: _closeOnSelect,
|
|
5711
|
+
builder: (BuildContext context, VoidCallback open) => KasyButton(
|
|
5712
|
+
label: 'Text styles',
|
|
5713
|
+
variant: KasyButtonVariant.soft,
|
|
5714
|
+
onPressed: () {
|
|
5715
|
+
_triggerTapFeedback();
|
|
5716
|
+
open();
|
|
5717
|
+
},
|
|
5718
|
+
),
|
|
5719
|
+
),
|
|
5720
|
+
);
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
|
|
5724
|
+
Widget _buildMenuSubmenu(BuildContext context) {
|
|
5725
|
+
return _previewIntrinsicLaunch(
|
|
5726
|
+
KasyMenuAnchor(
|
|
5727
|
+
sections: _kMenuSubmenu,
|
|
5728
|
+
builder: (BuildContext context, VoidCallback open) => KasyButton(
|
|
5729
|
+
label: 'Share',
|
|
5730
|
+
variant: KasyButtonVariant.soft,
|
|
5731
|
+
onPressed: () {
|
|
5732
|
+
_triggerTapFeedback();
|
|
5733
|
+
open();
|
|
5734
|
+
},
|
|
5735
|
+
),
|
|
5736
|
+
),
|
|
5737
|
+
);
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5420
5740
|
const List<KasyAccordionItemData> _kAccordionItems = <KasyAccordionItemData>[
|
|
5421
5741
|
KasyAccordionItemData(
|
|
5422
5742
|
icon: KasyIcons.send,
|
|
@@ -5,7 +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/
|
|
8
|
+
import 'package:kasy_kit/components/kasy_menu.dart';
|
|
9
9
|
import 'package:kasy_kit/core/data/entities/upload_result.dart';
|
|
10
10
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
11
11
|
import 'package:kasy_kit/core/data/repositories/user_repository.dart';
|
|
@@ -13,7 +13,6 @@ import 'package:kasy_kit/core/states/models/user_state.dart';
|
|
|
13
13
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
14
14
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
15
15
|
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
16
|
-
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
17
16
|
import 'package:kasy_kit/features/settings/ui/widgets/avatar_utils.dart';
|
|
18
17
|
import 'package:kasy_kit/features/settings/ui/widgets/round_progress.dart';
|
|
19
18
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
@@ -121,69 +120,56 @@ class _EditableUserAvatarState extends ConsumerState<EditableUserAvatar> {
|
|
|
121
120
|
BuildContext context,
|
|
122
121
|
UserState userState,
|
|
123
122
|
String? avatarPath,
|
|
124
|
-
)
|
|
123
|
+
) {
|
|
125
124
|
final userId = userState.user.idOrNull;
|
|
126
125
|
final cachedUrl = userId == null ? null : getCachedAvatarUrl(userId);
|
|
127
126
|
final hasAvatar =
|
|
128
127
|
_hasAvatarUrl(avatarPath) ||
|
|
129
128
|
_hasAvatarUrl(cachedUrl) ||
|
|
130
129
|
temporaryAvatarBytes != null;
|
|
131
|
-
final
|
|
132
|
-
|
|
133
|
-
switch (result) {
|
|
134
|
-
case _AvatarAction.camera:
|
|
135
|
-
final file = await ImagePicker().pickImage(source: ImageSource.camera);
|
|
136
|
-
await _uploadTask(file, userState);
|
|
137
|
-
case _AvatarAction.library:
|
|
138
|
-
final file = await ImagePicker().pickImage(source: ImageSource.gallery);
|
|
139
|
-
await _uploadTask(file, userState);
|
|
140
|
-
case _AvatarAction.remove:
|
|
141
|
-
await _removeAvatar(userState);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
Future<_AvatarAction?> _showAvatarBottomSheet(
|
|
146
|
-
BuildContext context, {
|
|
147
|
-
required bool hasAvatar,
|
|
148
|
-
}) {
|
|
149
|
-
return showKasyBottomSheet<_AvatarAction>(
|
|
130
|
+
final avatar = t.settings.avatar;
|
|
131
|
+
return showKasyMenu<void>(
|
|
150
132
|
context: context,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
133
|
+
anchorContext: context,
|
|
134
|
+
desktopWidth: 248,
|
|
135
|
+
sections: [
|
|
136
|
+
KasyMenuSection(
|
|
137
|
+
items: [
|
|
138
|
+
KasyMenuItem(
|
|
139
|
+
icon: KasyIcons.cameraAlt,
|
|
140
|
+
title: avatar.take_photo,
|
|
141
|
+
onTap: () => _pickAndUpload(ImageSource.camera, userState),
|
|
142
|
+
),
|
|
143
|
+
KasyMenuItem(
|
|
144
|
+
icon: KasyIcons.gallery,
|
|
145
|
+
title: avatar.choose_library,
|
|
146
|
+
onTap: () => _pickAndUpload(ImageSource.gallery, userState),
|
|
147
|
+
),
|
|
148
|
+
],
|
|
149
|
+
),
|
|
150
|
+
// Destructive action lives in its own ("Danger zone") group, per the
|
|
151
|
+
// HeroUI menu guidance.
|
|
152
|
+
if (hasAvatar)
|
|
153
|
+
KasyMenuSection(
|
|
154
|
+
items: [
|
|
155
|
+
KasyMenuItem(
|
|
156
|
+
icon: KasyIcons.trash,
|
|
157
|
+
title: avatar.remove_photo,
|
|
158
|
+
tone: KasyMenuItemTone.danger,
|
|
159
|
+
onTap: () => _removeAvatar(userState),
|
|
167
160
|
),
|
|
168
|
-
if (hasAvatar) ...[
|
|
169
|
-
Divider(
|
|
170
|
-
color: sheetContext.colors.onSurface.withValues(alpha: .08),
|
|
171
|
-
height: 1,
|
|
172
|
-
),
|
|
173
|
-
_BottomSheetTile(
|
|
174
|
-
icon: KasyIcons.trash,
|
|
175
|
-
label: t.settings.avatar.remove_photo,
|
|
176
|
-
color: sheetContext.colors.error,
|
|
177
|
-
onTap: () => Navigator.pop(sheetContext, _AvatarAction.remove),
|
|
178
|
-
),
|
|
179
|
-
],
|
|
180
161
|
],
|
|
181
162
|
),
|
|
182
|
-
|
|
183
|
-
),
|
|
163
|
+
],
|
|
184
164
|
);
|
|
185
165
|
}
|
|
186
166
|
|
|
167
|
+
Future<void> _pickAndUpload(ImageSource source, UserState userState) async {
|
|
168
|
+
final file = await ImagePicker().pickImage(source: source);
|
|
169
|
+
if (!mounted) return;
|
|
170
|
+
await _uploadTask(file, userState);
|
|
171
|
+
}
|
|
172
|
+
|
|
187
173
|
Future<void> _uploadTask(XFile? file, UserState userState) async {
|
|
188
174
|
if (file == null) return;
|
|
189
175
|
final userId = userState.user.idOrNull;
|
|
@@ -301,48 +287,6 @@ class _EditableUserAvatarState extends ConsumerState<EditableUserAvatar> {
|
|
|
301
287
|
}
|
|
302
288
|
}
|
|
303
289
|
|
|
304
|
-
enum _AvatarAction { camera, library, remove }
|
|
305
|
-
|
|
306
|
-
class _BottomSheetTile extends StatelessWidget {
|
|
307
|
-
final IconData? icon;
|
|
308
|
-
final String label;
|
|
309
|
-
final Color? color;
|
|
310
|
-
final VoidCallback onTap;
|
|
311
|
-
|
|
312
|
-
const _BottomSheetTile({
|
|
313
|
-
this.icon,
|
|
314
|
-
required this.label,
|
|
315
|
-
this.color,
|
|
316
|
-
required this.onTap,
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
@override
|
|
320
|
-
Widget build(BuildContext context) {
|
|
321
|
-
final Color fg = color ?? context.colors.onSurface;
|
|
322
|
-
return KasyHover(
|
|
323
|
-
onTap: onTap,
|
|
324
|
-
focusable: true,
|
|
325
|
-
focusGapColor: context.colors.surface,
|
|
326
|
-
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
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),
|
|
340
|
-
),
|
|
341
|
-
],
|
|
342
|
-
),
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
290
|
|
|
347
291
|
/// Resolves the avatar [ImageProvider] and renders a [KasyAvatar] with
|
|
348
292
|
/// [KasyAvatarFallbackSurface.soft]. Firebase Storage URL is loaded async.
|
|
@@ -2,7 +2,8 @@ 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/
|
|
5
|
+
import 'package:kasy_kit/components/kasy_menu.dart';
|
|
6
|
+
import 'package:kasy_kit/components/kasy_popover.dart';
|
|
6
7
|
import 'package:kasy_kit/core/home_widgets/home_widget_mywidget_service.dart';
|
|
7
8
|
import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
|
|
8
9
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
@@ -63,118 +64,42 @@ class LanguageSwitcher extends ConsumerWidget {
|
|
|
63
64
|
WidgetRef ref,
|
|
64
65
|
AppLocale current,
|
|
65
66
|
) {
|
|
66
|
-
|
|
67
|
-
showKasyBottomSheet<void>(
|
|
67
|
+
showKasyMenu<void>(
|
|
68
68
|
context: context,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
sheetTitle,
|
|
84
|
-
style: sheetContext.textTheme.titleMedium?.copyWith(
|
|
85
|
-
color: sheetContext.colors.onSurface,
|
|
86
|
-
),
|
|
87
|
-
),
|
|
69
|
+
anchorContext: context,
|
|
70
|
+
// Desktop: hang the menu off the right edge of the settings row, where the
|
|
71
|
+
// current value sits — a native desktop menu, not a centered modal.
|
|
72
|
+
desktopAlign: KasyPopoverAlign.end,
|
|
73
|
+
sections: [
|
|
74
|
+
KasyMenuSection(
|
|
75
|
+
label: context.t.settings.language_title,
|
|
76
|
+
selectionMode: KasyMenuSelectionMode.single,
|
|
77
|
+
items: AppLocale.values
|
|
78
|
+
.map(
|
|
79
|
+
(AppLocale locale) => KasyMenuItem(
|
|
80
|
+
title: locale.nativeName,
|
|
81
|
+
selected: locale == current,
|
|
82
|
+
onTap: () => _applyLocale(ref, locale),
|
|
88
83
|
),
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
...AppLocale.values.map((AppLocale locale) {
|
|
92
|
-
final bool isSelected = locale == current;
|
|
93
|
-
return _LocaleOptionTile(
|
|
94
|
-
locale: locale,
|
|
95
|
-
isSelected: isSelected,
|
|
96
|
-
onTap: () async {
|
|
97
|
-
// Close the sheet FIRST. Awaiting work before pop
|
|
98
|
-
// races with the rebuild triggered by setLocale and
|
|
99
|
-
// crashed Navigator.pop with !_debugLocked.
|
|
100
|
-
Navigator.pop(sheetContext);
|
|
101
|
-
// Await setLocale so the locale bundle finishes loading
|
|
102
|
-
// BEFORE the widget tries to read its translations.
|
|
103
|
-
// Slang lazy-loads non-base locales and its loadLocale
|
|
104
|
-
// returns immediately when another caller (this setLocale)
|
|
105
|
-
// is already loading — so a parallel updateForLocale would
|
|
106
|
-
// see translations not yet in the map and silently fall
|
|
107
|
-
// back to the base locale (English). That was the root
|
|
108
|
-
// cause of "first language tap doesn't update the widget".
|
|
109
|
-
await LocaleSettings.setLocale(locale);
|
|
110
|
-
unawaited(
|
|
111
|
-
ref
|
|
112
|
-
.read(sharedPreferencesProvider)
|
|
113
|
-
.setAppLocale(locale.languageCode),
|
|
114
|
-
);
|
|
115
|
-
unawaited(
|
|
116
|
-
ref
|
|
117
|
-
.read(myWidgetHomeWidgetProvider.notifier)
|
|
118
|
-
.updateForLocale(locale),
|
|
119
|
-
);
|
|
120
|
-
},
|
|
121
|
-
);
|
|
122
|
-
}),
|
|
123
|
-
],
|
|
124
|
-
),
|
|
84
|
+
)
|
|
85
|
+
.toList(),
|
|
125
86
|
),
|
|
126
|
-
|
|
87
|
+
],
|
|
127
88
|
);
|
|
128
89
|
}
|
|
129
|
-
}
|
|
130
90
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Widget build(BuildContext context) {
|
|
144
|
-
final Color primary = context.colors.primary;
|
|
145
|
-
final Color fg = isSelected ? primary : context.colors.onSurface;
|
|
146
|
-
|
|
147
|
-
return KasyHover(
|
|
148
|
-
onTap: onTap,
|
|
149
|
-
focusable: true,
|
|
150
|
-
// Full-bleed rectangular highlight (default radius): the sheet rounds its
|
|
151
|
-
// own corners, so options span edge-to-edge like a native menu/list.
|
|
152
|
-
padding: const EdgeInsets.symmetric(
|
|
153
|
-
horizontal: KasySpacing.md,
|
|
154
|
-
vertical: KasySpacing.smd,
|
|
155
|
-
),
|
|
156
|
-
child: Row(
|
|
157
|
-
children: <Widget>[
|
|
158
|
-
Expanded(
|
|
159
|
-
child: Text(
|
|
160
|
-
locale.nativeName,
|
|
161
|
-
style: context.textTheme.bodyLarge?.copyWith(
|
|
162
|
-
color: fg,
|
|
163
|
-
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
|
164
|
-
),
|
|
165
|
-
),
|
|
166
|
-
),
|
|
167
|
-
if (isSelected)
|
|
168
|
-
Container(
|
|
169
|
-
width: 10,
|
|
170
|
-
height: 10,
|
|
171
|
-
decoration: BoxDecoration(
|
|
172
|
-
color: primary,
|
|
173
|
-
shape: BoxShape.circle,
|
|
174
|
-
),
|
|
175
|
-
),
|
|
176
|
-
],
|
|
177
|
-
),
|
|
91
|
+
// The menu closes itself before this runs (KasyMenuItem pops first), so the
|
|
92
|
+
// awaited setLocale never races the rebuild it triggers. setLocale is awaited
|
|
93
|
+
// so Slang finishes loading the locale bundle before any widget reads its
|
|
94
|
+
// translations — otherwise a parallel updateForLocale sees an unloaded map and
|
|
95
|
+
// silently falls back to English (the old "first tap doesn't update" bug).
|
|
96
|
+
Future<void> _applyLocale(WidgetRef ref, AppLocale locale) async {
|
|
97
|
+
await LocaleSettings.setLocale(locale);
|
|
98
|
+
unawaited(
|
|
99
|
+
ref.read(sharedPreferencesProvider).setAppLocale(locale.languageCode),
|
|
100
|
+
);
|
|
101
|
+
unawaited(
|
|
102
|
+
ref.read(myWidgetHomeWidgetProvider.notifier).updateForLocale(locale),
|
|
178
103
|
);
|
|
179
104
|
}
|
|
180
105
|
}
|