kasy-cli 1.32.0 → 1.35.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/README.md +1 -1
- package/bin/kasy.js +66 -2
- package/docs/cli-reference.md +7 -7
- package/lib/commands/apple-web.js +222 -0
- package/lib/commands/configure.js +3 -91
- package/lib/commands/doctor.js +20 -0
- package/lib/commands/facebook.js +189 -0
- package/lib/commands/new.js +61 -11
- package/lib/commands/release-version.js +234 -0
- package/lib/commands/update.js +27 -0
- package/lib/scaffold/CHANGELOG.json +27 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +199 -21
- package/lib/scaffold/backends/patch-base-hashes.json +66 -0
- package/lib/scaffold/backends/supabase/deploy.js +92 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
- package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +92 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
- package/lib/scaffold/generate.js +53 -4
- package/lib/scaffold/shared/generator-utils.js +18 -6
- package/lib/utils/apple-web.js +147 -0
- package/lib/utils/facebook.js +162 -0
- package/lib/utils/i18n/messages-en.js +85 -0
- package/lib/utils/i18n/messages-es.js +85 -0
- package/lib/utils/i18n/messages-pt.js +85 -0
- package/package.json +5 -2
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
- package/templates/firebase/AGENTS.md +170 -0
- package/templates/firebase/CLAUDE.md +16 -0
- package/templates/firebase/DESIGN_SYSTEM.md +269 -0
- package/templates/firebase/docs/auth-setup.en.md +4 -2
- package/templates/firebase/docs/auth-setup.es.md +4 -2
- package/templates/firebase/docs/auth-setup.pt.md +4 -2
- package/templates/firebase/firebase.json +56 -1
- package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
- package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
- package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
- package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
- package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_alert.dart +0 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +35 -17
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
- package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
- package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
- package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
- package/templates/firebase/lib/components/kasy_screen.dart +114 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
- package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
- package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
- package/templates/firebase/lib/components/kasy_toast.dart +108 -73
- package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
- package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
- package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
- package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
- package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
- package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
- package/templates/firebase/lib/core/config/features.dart +5 -0
- package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
- package/templates/firebase/lib/core/guards/guard.dart +16 -2
- package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
- package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +48 -124
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
- package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
- package/templates/firebase/lib/core/states/logout_action.dart +5 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
- package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
- package/templates/firebase/lib/core/theme/texts.dart +90 -57
- package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
- package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
- package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
- package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
- package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
- package/templates/firebase/lib/core/web_screen_width.dart +15 -0
- package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
- package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
- package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
- package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
- package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -8
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
- package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +266 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
- package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
- package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
- package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
- package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
- package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
- package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
- package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +80 -15
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -2
- package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
- package/templates/firebase/lib/features/home/home_components_page.dart +8 -1
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +186 -56
- package/templates/firebase/lib/features/home/home_page.dart +4 -0
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +169 -208
- package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -4
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +84 -128
- package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +2 -1
- package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +58 -21
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
- package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
- package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
- package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
- package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
- package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
- package/templates/firebase/lib/i18n/en.i18n.json +54 -7
- package/templates/firebase/lib/i18n/es.i18n.json +54 -7
- package/templates/firebase/lib/i18n/pt.i18n.json +54 -7
- package/templates/firebase/lib/main.dart +11 -2
- package/templates/firebase/lib/router.dart +94 -13
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
- package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
- package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
- package/templates/firebase/tool/design_check.dart +152 -0
- package/templates/firebase/web/index.html +162 -14
- package/templates/firebase/assets/images/review.png +0 -0
- package/templates/firebase/assets/images/update.png +0 -0
- package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
- package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import 'package:flutter/gestures.dart';
|
|
2
|
+
import 'package:flutter/services.dart';
|
|
3
|
+
import 'package:flutter/widgets.dart';
|
|
4
|
+
|
|
5
|
+
/// Makes focus rings keyboard-only — the web's `:focus-visible` behaviour.
|
|
6
|
+
///
|
|
7
|
+
/// Flutter's default [FocusHighlightStrategy.automatic] only hides focus rings
|
|
8
|
+
/// for *touch*; on desktop a mouse click keeps them in "traditional" mode, so a
|
|
9
|
+
/// ring raised by Tab navigation never clears once the user grabs the mouse.
|
|
10
|
+
/// This wraps the app and flips the strategy by input type:
|
|
11
|
+
/// - a navigation key (Tab / arrows / Home / End / Page Up·Down) shows rings;
|
|
12
|
+
/// - any pointer press hides them, and — when the user was mid keyboard
|
|
13
|
+
/// navigation — also drops focus, so the next Tab restarts from the top of
|
|
14
|
+
/// the traversal (skip link → sidebar → content) instead of resuming
|
|
15
|
+
/// wherever the ring happened to be.
|
|
16
|
+
///
|
|
17
|
+
/// Plain typing does NOT turn rings on (only navigation keys do), so editing a
|
|
18
|
+
/// text field with the mouse stays ring-free. Uses a global pointer route so it
|
|
19
|
+
/// catches clicks anywhere, including empty areas with no hit target.
|
|
20
|
+
class FocusVisibility extends StatefulWidget {
|
|
21
|
+
const FocusVisibility({super.key, required this.child});
|
|
22
|
+
|
|
23
|
+
final Widget child;
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
State<FocusVisibility> createState() => _FocusVisibilityState();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class _FocusVisibilityState extends State<FocusVisibility> {
|
|
30
|
+
static final Set<LogicalKeyboardKey> _navigationKeys = <LogicalKeyboardKey>{
|
|
31
|
+
LogicalKeyboardKey.tab,
|
|
32
|
+
LogicalKeyboardKey.arrowUp,
|
|
33
|
+
LogicalKeyboardKey.arrowDown,
|
|
34
|
+
LogicalKeyboardKey.arrowLeft,
|
|
35
|
+
LogicalKeyboardKey.arrowRight,
|
|
36
|
+
LogicalKeyboardKey.home,
|
|
37
|
+
LogicalKeyboardKey.end,
|
|
38
|
+
LogicalKeyboardKey.pageUp,
|
|
39
|
+
LogicalKeyboardKey.pageDown,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
void initState() {
|
|
44
|
+
super.initState();
|
|
45
|
+
// Start ring-free; the first navigation key turns them on.
|
|
46
|
+
FocusManager.instance.highlightStrategy =
|
|
47
|
+
FocusHighlightStrategy.alwaysTouch;
|
|
48
|
+
HardwareKeyboard.instance.addHandler(_handleKey);
|
|
49
|
+
GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointer);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
void dispose() {
|
|
54
|
+
GestureBinding.instance.pointerRouter.removeGlobalRoute(_handlePointer);
|
|
55
|
+
HardwareKeyboard.instance.removeHandler(_handleKey);
|
|
56
|
+
super.dispose();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
bool _handleKey(KeyEvent event) {
|
|
60
|
+
if (event is KeyDownEvent && _navigationKeys.contains(event.logicalKey)) {
|
|
61
|
+
_setStrategy(FocusHighlightStrategy.alwaysTraditional);
|
|
62
|
+
}
|
|
63
|
+
return false; // observe only — never consume the event
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
void _handlePointer(PointerEvent event) {
|
|
67
|
+
if (event is! PointerDownEvent) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
final FocusManager fm = FocusManager.instance;
|
|
71
|
+
final bool wasKeyboardNav =
|
|
72
|
+
fm.highlightMode == FocusHighlightMode.traditional;
|
|
73
|
+
_setStrategy(FocusHighlightStrategy.alwaysTouch);
|
|
74
|
+
// Only reset focus when leaving keyboard navigation; a normal click (e.g.
|
|
75
|
+
// into a text field) then keeps the focus it's about to request.
|
|
76
|
+
if (wasKeyboardNav) {
|
|
77
|
+
fm.primaryFocus?.unfocus();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
void _setStrategy(FocusHighlightStrategy strategy) {
|
|
82
|
+
if (FocusManager.instance.highlightStrategy != strategy) {
|
|
83
|
+
FocusManager.instance.highlightStrategy = strategy;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@override
|
|
88
|
+
Widget build(BuildContext context) => widget.child;
|
|
89
|
+
}
|
|
@@ -3,17 +3,19 @@ import 'package:kasy_kit/components/components.dart';
|
|
|
3
3
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
4
4
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
5
5
|
|
|
6
|
-
/// Shows
|
|
6
|
+
/// Shows the "what's new" bottom sheet, built on [KasyBottomSheet].
|
|
7
|
+
///
|
|
8
|
+
/// Dismissing it — the Continue button, the drag handle, or tapping the dim
|
|
9
|
+
/// barrier outside — simply closes the sheet and returns the user to the screen
|
|
10
|
+
/// they were on; it never navigates anywhere. The version is already recorded as
|
|
11
|
+
/// seen before this is shown (see `MaybeShowUpdateBottomSheet`), so it won't pop
|
|
12
|
+
/// up again for the same version.
|
|
7
13
|
Future<void> showUpdateBottomSheet({
|
|
8
14
|
required BuildContext context,
|
|
9
15
|
required String version,
|
|
10
|
-
bool useRootNavigator = true,
|
|
11
16
|
}) async {
|
|
12
|
-
await
|
|
17
|
+
await showKasyBottomSheet<void>(
|
|
13
18
|
context: context,
|
|
14
|
-
useSafeArea: true,
|
|
15
|
-
useRootNavigator: useRootNavigator,
|
|
16
|
-
barrierColor: context.colors.background.withValues(alpha: 0.90),
|
|
17
19
|
isScrollControlled: true,
|
|
18
20
|
builder: (context) => _UpdateBottomSheet(version: version),
|
|
19
21
|
);
|
|
@@ -28,130 +30,31 @@ class _UpdateBottomSheet extends StatelessWidget {
|
|
|
28
30
|
Widget build(BuildContext context) {
|
|
29
31
|
final translations = Translations.of(context).update_bottom_sheet;
|
|
30
32
|
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
return KasyBottomSheet(
|
|
34
|
+
icon: KasyIcons.star,
|
|
35
|
+
title: translations.title,
|
|
36
|
+
message: 'Version $version',
|
|
37
|
+
body: ConstrainedBox(
|
|
38
|
+
constraints: BoxConstraints(
|
|
39
|
+
maxHeight: MediaQuery.sizeOf(context).height * 0.5,
|
|
37
40
|
),
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
child: ListView.separated(
|
|
42
|
+
shrinkWrap: true,
|
|
43
|
+
padding: EdgeInsets.zero,
|
|
44
|
+
itemBuilder: (context, index) =>
|
|
45
|
+
_UpdateHighlightTile(highlight: translations.highlights[index]),
|
|
46
|
+
separatorBuilder: (context, index) =>
|
|
47
|
+
const SizedBox(height: KasySpacing.sm),
|
|
48
|
+
itemCount: translations.highlights.length,
|
|
42
49
|
),
|
|
43
50
|
),
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
children: [
|
|
50
|
-
Flexible(
|
|
51
|
-
flex: 0,
|
|
52
|
-
child: Align(
|
|
53
|
-
child: ClipRRect(
|
|
54
|
-
borderRadius: const BorderRadius.only(
|
|
55
|
-
topLeft: Radius.circular(KasyRadius.xl),
|
|
56
|
-
topRight: Radius.circular(KasyRadius.xl),
|
|
57
|
-
),
|
|
58
|
-
child: Image.asset(
|
|
59
|
-
'assets/images/update.png',
|
|
60
|
-
fit: BoxFit.cover,
|
|
61
|
-
),
|
|
62
|
-
),
|
|
63
|
-
),
|
|
64
|
-
),
|
|
65
|
-
Padding(
|
|
66
|
-
padding: const EdgeInsets.fromLTRB(
|
|
67
|
-
KasySpacing.lg,
|
|
68
|
-
KasySpacing.lg,
|
|
69
|
-
KasySpacing.md,
|
|
70
|
-
0,
|
|
71
|
-
),
|
|
72
|
-
child: Row(
|
|
73
|
-
children: [
|
|
74
|
-
Container(
|
|
75
|
-
width: 48,
|
|
76
|
-
height: 48,
|
|
77
|
-
decoration: BoxDecoration(
|
|
78
|
-
color: context.colors.primary.withValues(alpha: 0.1),
|
|
79
|
-
borderRadius: KasyRadius.mdBorderRadius,
|
|
80
|
-
),
|
|
81
|
-
child: Icon(
|
|
82
|
-
KasyIcons.star,
|
|
83
|
-
color: context.colors.primary,
|
|
84
|
-
size: KasyIconSize.xl,
|
|
85
|
-
),
|
|
86
|
-
),
|
|
87
|
-
const SizedBox(width: KasySpacing.md),
|
|
88
|
-
Expanded(
|
|
89
|
-
child: Column(
|
|
90
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
91
|
-
children: [
|
|
92
|
-
Text(
|
|
93
|
-
translations.title,
|
|
94
|
-
style: context.textTheme.headlineSmall?.copyWith(
|
|
95
|
-
color: context.colors.onSurface,
|
|
96
|
-
fontWeight: FontWeight.w700,
|
|
97
|
-
),
|
|
98
|
-
),
|
|
99
|
-
Text(
|
|
100
|
-
"Version $version",
|
|
101
|
-
style: context.textTheme.bodyMedium?.copyWith(
|
|
102
|
-
color: context.colors.muted,
|
|
103
|
-
fontWeight: FontWeight.w500,
|
|
104
|
-
),
|
|
105
|
-
),
|
|
106
|
-
],
|
|
107
|
-
),
|
|
108
|
-
),
|
|
109
|
-
KasyButton.iconOnly(
|
|
110
|
-
icon: KasyIcons.close,
|
|
111
|
-
variant: KasyButtonVariant.ghost,
|
|
112
|
-
foregroundColor: context.colors.muted,
|
|
113
|
-
onPressed: () => Navigator.of(context).pop(),
|
|
114
|
-
semanticLabel: translations.title,
|
|
115
|
-
),
|
|
116
|
-
],
|
|
117
|
-
),
|
|
118
|
-
),
|
|
119
|
-
const SizedBox(height: KasySpacing.lg),
|
|
120
|
-
Flexible(
|
|
121
|
-
child: ConstrainedBox(
|
|
122
|
-
constraints: BoxConstraints(
|
|
123
|
-
maxHeight: MediaQuery.of(context).size.height * 0.6,
|
|
124
|
-
),
|
|
125
|
-
child: ListView.separated(
|
|
126
|
-
shrinkWrap: true,
|
|
127
|
-
padding: const EdgeInsets.symmetric(
|
|
128
|
-
horizontal: KasySpacing.lg,
|
|
129
|
-
),
|
|
130
|
-
itemBuilder: (context, index) {
|
|
131
|
-
final highlight = translations.highlights[index];
|
|
132
|
-
return _UpdateHighlightTile(highlight: highlight);
|
|
133
|
-
},
|
|
134
|
-
separatorBuilder: (context, index) =>
|
|
135
|
-
const SizedBox(height: KasySpacing.sm),
|
|
136
|
-
itemCount: translations.highlights.length,
|
|
137
|
-
),
|
|
138
|
-
),
|
|
139
|
-
),
|
|
140
|
-
const SizedBox(height: KasySpacing.xl),
|
|
141
|
-
Padding(
|
|
142
|
-
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.lg),
|
|
143
|
-
child: SizedBox(
|
|
144
|
-
width: double.infinity,
|
|
145
|
-
child: KasyButton(
|
|
146
|
-
label: translations.continue_button,
|
|
147
|
-
expand: true,
|
|
148
|
-
onPressed: () => Navigator.of(context).pop(),
|
|
149
|
-
),
|
|
150
|
-
),
|
|
151
|
-
),
|
|
152
|
-
],
|
|
51
|
+
actions: [
|
|
52
|
+
KasyButton(
|
|
53
|
+
label: translations.continue_button,
|
|
54
|
+
expand: true,
|
|
55
|
+
onPressed: () => Navigator.of(context).pop(),
|
|
153
56
|
),
|
|
154
|
-
|
|
57
|
+
],
|
|
155
58
|
);
|
|
156
59
|
}
|
|
157
60
|
}
|
|
@@ -115,13 +115,8 @@ class _DetailContainer extends StatelessWidget {
|
|
|
115
115
|
|
|
116
116
|
@override
|
|
117
117
|
Widget build(BuildContext context) {
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
decoration: BoxDecoration(
|
|
121
|
-
color: context.colors.surface,
|
|
122
|
-
borderRadius: BorderRadius.circular(24),
|
|
123
|
-
border: Border.all(color: context.colors.border, width: 0.5),
|
|
124
|
-
),
|
|
118
|
+
return KasyCard(
|
|
119
|
+
borderRadius: BorderRadius.circular(KasyRadius.xl),
|
|
125
120
|
child: child,
|
|
126
121
|
);
|
|
127
122
|
}
|
|
@@ -143,10 +138,18 @@ class _DetailPlaceholder extends StatelessWidget {
|
|
|
143
138
|
showShadow: true,
|
|
144
139
|
),
|
|
145
140
|
const SizedBox(height: KasySpacing.md),
|
|
141
|
+
Text(
|
|
142
|
+
t.home.cards.assistant_title,
|
|
143
|
+
textAlign: TextAlign.center,
|
|
144
|
+
style: context.textTheme.titleMedium?.copyWith(
|
|
145
|
+
color: context.colors.onBackground,
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
const SizedBox(height: KasySpacing.xs),
|
|
146
149
|
Text(
|
|
147
150
|
t.ai_chat.no_conversation_selected,
|
|
148
151
|
textAlign: TextAlign.center,
|
|
149
|
-
style: context.textTheme.
|
|
152
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
150
153
|
color: context.colors.muted,
|
|
151
154
|
height: 1.4,
|
|
152
155
|
),
|
|
@@ -30,6 +30,12 @@ class AiChatComposer extends StatefulWidget {
|
|
|
30
30
|
class _AiChatComposerState extends State<AiChatComposer> {
|
|
31
31
|
bool _canSend = false;
|
|
32
32
|
|
|
33
|
+
// Owns the field focus so we can intercept hardware Enter: plain Enter sends,
|
|
34
|
+
// Shift+Enter inserts a newline (the standard chat-composer behaviour). On
|
|
35
|
+
// touch keyboards (no hardware key event) the return key keeps inserting a
|
|
36
|
+
// newline and the send orb is used, so mobile is unchanged.
|
|
37
|
+
late final FocusNode _focusNode = FocusNode(onKeyEvent: _onKeyEvent);
|
|
38
|
+
|
|
33
39
|
@override
|
|
34
40
|
void initState() {
|
|
35
41
|
super.initState();
|
|
@@ -40,9 +46,23 @@ class _AiChatComposerState extends State<AiChatComposer> {
|
|
|
40
46
|
@override
|
|
41
47
|
void dispose() {
|
|
42
48
|
widget.controller.removeListener(_onTextChanged);
|
|
49
|
+
_focusNode.dispose();
|
|
43
50
|
super.dispose();
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
|
54
|
+
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
|
55
|
+
final bool isEnter = event.logicalKey == LogicalKeyboardKey.enter ||
|
|
56
|
+
event.logicalKey == LogicalKeyboardKey.numpadEnter;
|
|
57
|
+
if (!isEnter) return KeyEventResult.ignored;
|
|
58
|
+
// Shift+Enter falls through to the field and inserts a newline.
|
|
59
|
+
if (HardwareKeyboard.instance.isShiftPressed) {
|
|
60
|
+
return KeyEventResult.ignored;
|
|
61
|
+
}
|
|
62
|
+
_handleSend();
|
|
63
|
+
return KeyEventResult.handled;
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
bool _hasSendableText(String value) {
|
|
47
67
|
final String trimmed = value.trim();
|
|
48
68
|
return trimmed.isNotEmpty && trimmed.length <= kAiChatMaxMessageLength;
|
|
@@ -88,6 +108,7 @@ class _AiChatComposerState extends State<AiChatComposer> {
|
|
|
88
108
|
Expanded(
|
|
89
109
|
child: KasyTextField(
|
|
90
110
|
controller: widget.controller,
|
|
111
|
+
focusNode: _focusNode,
|
|
91
112
|
enabled: enabled,
|
|
92
113
|
variant: KasyTextFieldVariant.embedded,
|
|
93
114
|
hint: t.ai_chat.hint,
|
|
@@ -186,7 +186,6 @@ class _EmptyState extends StatelessWidget {
|
|
|
186
186
|
t.home.cards.assistant_title,
|
|
187
187
|
textAlign: TextAlign.center,
|
|
188
188
|
style: context.textTheme.titleMedium?.copyWith(
|
|
189
|
-
fontWeight: FontWeight.w600,
|
|
190
189
|
color: context.colors.onBackground,
|
|
191
190
|
),
|
|
192
191
|
),
|
|
@@ -194,7 +193,7 @@ class _EmptyState extends StatelessWidget {
|
|
|
194
193
|
Text(
|
|
195
194
|
t.ai_chat.empty_state,
|
|
196
195
|
textAlign: TextAlign.center,
|
|
197
|
-
style: context.textTheme.
|
|
196
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
198
197
|
color: context.colors.muted,
|
|
199
198
|
height: 1.4,
|
|
200
199
|
),
|
|
@@ -207,8 +207,7 @@ class _Header extends StatelessWidget {
|
|
|
207
207
|
t.ai_chat.title,
|
|
208
208
|
maxLines: 1,
|
|
209
209
|
overflow: TextOverflow.ellipsis,
|
|
210
|
-
style: context.
|
|
211
|
-
fontWeight: FontWeight.w800,
|
|
210
|
+
style: context.kasyTextTheme.pageTitle.copyWith(
|
|
212
211
|
color: context.colors.onBackground,
|
|
213
212
|
),
|
|
214
213
|
),
|
|
@@ -274,7 +273,7 @@ class _EmptyConversations extends StatelessWidget {
|
|
|
274
273
|
child: Text(
|
|
275
274
|
t.ai_chat.conversations_empty,
|
|
276
275
|
textAlign: TextAlign.center,
|
|
277
|
-
style: context.textTheme.
|
|
276
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
278
277
|
color: context.colors.muted,
|
|
279
278
|
height: 1.4,
|
|
280
279
|
),
|
|
@@ -75,8 +75,7 @@ class _AiConversationTileState extends ConsumerState<AiConversationTile> {
|
|
|
75
75
|
title,
|
|
76
76
|
maxLines: 1,
|
|
77
77
|
overflow: TextOverflow.ellipsis,
|
|
78
|
-
style: context.
|
|
79
|
-
fontWeight: FontWeight.w600,
|
|
78
|
+
style: context.kasyTextTheme.rowTitle.copyWith(
|
|
80
79
|
color: titleColor,
|
|
81
80
|
),
|
|
82
81
|
),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// Whether the app is running inside a *mobile* web browser (phone/tablet).
|
|
2
|
+
///
|
|
3
|
+
/// Drives the OAuth flow choice on web: mobile browsers handle provider popups
|
|
4
|
+
/// unreliably — the provider opens in a new tab and the popup result is
|
|
5
|
+
/// frequently lost when the browser reclaims the backgrounded opener tab, so the
|
|
6
|
+
/// sign-in future never resolves and the app stays stuck "signing in". On mobile
|
|
7
|
+
/// we use a full-page redirect instead (completed at startup by
|
|
8
|
+
/// `getRedirectResult`); desktop web keeps the smoother popup.
|
|
9
|
+
///
|
|
10
|
+
/// Non-web stub: always false. The real implementation lives in
|
|
11
|
+
/// `auth_web_support_web.dart` and is selected via conditional import on web.
|
|
12
|
+
bool isMobileWebBrowser() => false;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import 'package:web/web.dart' as web;
|
|
2
|
+
|
|
3
|
+
/// Web implementation: detects a mobile browser from the user-agent (iOS, Android
|
|
4
|
+
/// and other common phone/tablet agents). iPadOS 13+ reports a desktop Safari
|
|
5
|
+
/// user-agent, so we also treat a touch-capable "Macintosh" as mobile.
|
|
6
|
+
///
|
|
7
|
+
/// A false positive only costs the (also-working) redirect flow instead of the
|
|
8
|
+
/// popup, so the heuristic deliberately errs toward catching mobiles.
|
|
9
|
+
bool isMobileWebBrowser() {
|
|
10
|
+
final ua = web.window.navigator.userAgent.toLowerCase();
|
|
11
|
+
const needles = [
|
|
12
|
+
'android',
|
|
13
|
+
'iphone',
|
|
14
|
+
'ipad',
|
|
15
|
+
'ipod',
|
|
16
|
+
'mobile',
|
|
17
|
+
'windows phone',
|
|
18
|
+
'blackberry',
|
|
19
|
+
'opera mini',
|
|
20
|
+
'iemobile',
|
|
21
|
+
];
|
|
22
|
+
if (needles.any(ua.contains)) return true;
|
|
23
|
+
// iPadOS 13+ masquerades as desktop Safari but still exposes touch points.
|
|
24
|
+
return ua.contains('macintosh') && web.window.navigator.maxTouchPoints > 1;
|
|
25
|
+
}
|