kasy-cli 1.21.9 → 1.23.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/commands/add.js +93 -80
- package/lib/commands/configure.js +100 -32
- package/lib/commands/doctor.js +28 -2
- package/lib/commands/new.js +80 -37
- package/lib/commands/notifications.js +1 -1
- package/lib/commands/remove.js +43 -15
- package/lib/commands/run.js +2 -2
- package/lib/commands/update.js +2 -2
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/api/generator.js +14 -14
- package/lib/scaffold/backends/api/patch/README.md +83 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
- package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +5 -5
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/firebase/deploy.js +87 -13
- package/lib/scaffold/backends/firebase/generator.js +5 -5
- package/lib/scaffold/backends/firebase/tokens.js +4 -4
- package/lib/scaffold/backends/supabase/deploy.js +63 -11
- package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
- package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
- package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
- package/lib/scaffold/backends/supabase/generator.js +17 -17
- package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
- package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/supabase/tokens.js +3 -3
- package/lib/scaffold/catalog.js +9 -11
- package/lib/scaffold/generate.js +45 -31
- package/lib/scaffold/shared/generator-utils.js +188 -81
- package/lib/scaffold/shared/sort-imports.js +191 -0
- package/lib/scaffold/shared/template-strings.js +3 -3
- package/lib/utils/checks.js +22 -6
- package/lib/utils/env-tools.js +7 -0
- package/lib/utils/flutter-install.js +114 -0
- package/lib/utils/i18n/messages-en.js +52 -35
- package/lib/utils/i18n/messages-es.js +52 -35
- package/lib/utils/i18n/messages-pt.js +54 -37
- package/lib/utils/updates.js +15 -15
- package/package.json +1 -1
- package/templates/firebase/.env.example +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
- package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
- package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
- package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
- package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
- package/templates/firebase/firestore.rules +24 -5
- package/templates/firebase/functions/package-lock.json +22 -1
- package/templates/firebase/functions/package.json +2 -1
- package/templates/firebase/functions/src/admin/functions.ts +113 -0
- package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
- package/templates/firebase/functions/src/index.ts +8 -2
- package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
- package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
- package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
- package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
- package/templates/firebase/lib/components/components.dart +4 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
- package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
- package/templates/firebase/lib/components/kasy_button.dart +23 -99
- package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
- package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
- package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +702 -425
- package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
- package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
- package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
- package/templates/firebase/lib/components/kasy_toast.dart +2 -2
- package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
- package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +136 -23
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +54 -0
- package/templates/firebase/lib/core/config/app_env.dart +5 -11
- package/templates/firebase/lib/core/config/features.dart +5 -4
- package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
- package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
- package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
- package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
- package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
- package/templates/firebase/lib/core/data/models/user.dart +11 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
- package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
- package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
- package/templates/firebase/lib/core/states/logout_action.dart +25 -0
- package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
- package/templates/firebase/lib/core/theme/colors.dart +488 -188
- package/templates/firebase/lib/core/theme/radius.dart +22 -11
- package/templates/firebase/lib/core/theme/shadows.dart +66 -0
- package/templates/firebase/lib/core/theme/texts.dart +75 -41
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
- package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
- package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +53 -14
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
- package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
- package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
- package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
- package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +128 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
- package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
- package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
- package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
- package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
- package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
- package/templates/firebase/lib/features/home/home_feed.dart +289 -0
- package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
- package/templates/firebase/lib/features/home/home_page.dart +11 -250
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
- package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
- package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
- package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
- package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
- package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
- package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
- package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
- package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
- package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
- package/templates/firebase/lib/i18n/en.i18n.json +171 -46
- package/templates/firebase/lib/i18n/es.i18n.json +175 -50
- package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
- package/templates/firebase/lib/main.dart +6 -3
- package/templates/firebase/lib/router.dart +15 -23
- package/templates/firebase/pubspec.yaml +5 -6
- package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
- package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
- package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
- package/templates/firebase/test/test_utils.dart +6 -6
- package/templates/firebase/web/index.html +5 -2
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
- package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
- package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
- package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
- package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
- package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
- package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
- package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
- package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
- /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:kasy_kit/components/kasy_card.dart';
|
|
3
|
+
import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
|
|
4
|
+
import 'package:kasy_kit/core/theme/theme.dart';
|
|
5
|
+
import 'package:kasy_kit/features/home/home_image_grid.dart';
|
|
6
|
+
|
|
7
|
+
/// A browseable image category. Selecting one filters the feed below.
|
|
8
|
+
enum HomeCategory { productDesign, background, animated, icons3d, gradients }
|
|
9
|
+
|
|
10
|
+
class _CategoryData {
|
|
11
|
+
final String title;
|
|
12
|
+
final String subtitle;
|
|
13
|
+
|
|
14
|
+
/// Thumbnail shown inside the filter card.
|
|
15
|
+
final String thumbnailId;
|
|
16
|
+
|
|
17
|
+
/// Unsplash photo ids returned in the feed for this category.
|
|
18
|
+
final List<String> photoIds;
|
|
19
|
+
|
|
20
|
+
const _CategoryData({
|
|
21
|
+
required this.title,
|
|
22
|
+
required this.subtitle,
|
|
23
|
+
required this.thumbnailId,
|
|
24
|
+
required this.photoIds,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Map<HomeCategory, _CategoryData> _categories = <HomeCategory, _CategoryData>{
|
|
29
|
+
HomeCategory.productDesign: _CategoryData(
|
|
30
|
+
title: 'Product Design',
|
|
31
|
+
subtitle: 'Sleek, abstract objects',
|
|
32
|
+
thumbnailId: '1556228720-195a672e8a03',
|
|
33
|
+
photoIds: <String>[
|
|
34
|
+
'1556228720-195a672e8a03',
|
|
35
|
+
'1542291026-7eec264c27ff',
|
|
36
|
+
'1505740420928-5e560c06d30e',
|
|
37
|
+
'1522338242992-e1a54906a8da',
|
|
38
|
+
'1523275335684-37898b6baf30',
|
|
39
|
+
'1572635196237-14b3f281503f',
|
|
40
|
+
'1526170375885-4d8ecf77b99f',
|
|
41
|
+
'1560769629-975ec94e6a86',
|
|
42
|
+
'1491553895911-0055eca6402d',
|
|
43
|
+
'1525966222134-fcfa99b8ae77',
|
|
44
|
+
'1484704849700-f032a568e944',
|
|
45
|
+
'1542219550-37153d387c27',
|
|
46
|
+
],
|
|
47
|
+
),
|
|
48
|
+
HomeCategory.background: _CategoryData(
|
|
49
|
+
title: 'Background',
|
|
50
|
+
subtitle: 'Dreamy, scenic vibes',
|
|
51
|
+
thumbnailId: '1501854140801-50d01698950b',
|
|
52
|
+
photoIds: <String>[
|
|
53
|
+
'1506905925346-21bda4d32df4',
|
|
54
|
+
'1469474968028-56623f02e42e',
|
|
55
|
+
'1441974231531-c6227db76b6e',
|
|
56
|
+
'1472214103451-9374bd1c798e',
|
|
57
|
+
'1447752875215-b2761acb3c5d',
|
|
58
|
+
'1426604966848-d7adac402bff',
|
|
59
|
+
'1433086966358-54859d0ed716',
|
|
60
|
+
'1418985991508-e47386d96a71',
|
|
61
|
+
'1505765050516-f72dcac9c60e',
|
|
62
|
+
'1454496522488-7a8e488e8606',
|
|
63
|
+
'1439066615861-d1af74d74000',
|
|
64
|
+
'1470071459604-3b5ec3a7fe05',
|
|
65
|
+
],
|
|
66
|
+
),
|
|
67
|
+
HomeCategory.animated: _CategoryData(
|
|
68
|
+
title: 'Animated',
|
|
69
|
+
subtitle: 'Minimalist, soft',
|
|
70
|
+
thumbnailId: '1502691876148-a84978e59af8',
|
|
71
|
+
photoIds: <String>[
|
|
72
|
+
'1541701494587-cb58502866ab',
|
|
73
|
+
'1559827260-dc66d52bef19',
|
|
74
|
+
'1462331940025-496dfbfc7564',
|
|
75
|
+
'1534796636912-3b95b3ab5986',
|
|
76
|
+
'1518837695005-2083093ee35b',
|
|
77
|
+
'1502691876148-a84978e59af8',
|
|
78
|
+
'1574169208507-84376144848b',
|
|
79
|
+
'1620641788421-7a1c342ea42e',
|
|
80
|
+
'1550859492-d5da9d8e45f3',
|
|
81
|
+
'1579546929518-9e396f3cc809',
|
|
82
|
+
'1614851099175-e5b30eb6f696',
|
|
83
|
+
'1557672172-298e090bd0f1',
|
|
84
|
+
],
|
|
85
|
+
),
|
|
86
|
+
HomeCategory.icons3d: _CategoryData(
|
|
87
|
+
title: '3D Icons',
|
|
88
|
+
subtitle: 'Clean, rounded icons',
|
|
89
|
+
thumbnailId: '1639762681485-074b7f938ba0',
|
|
90
|
+
photoIds: <String>[
|
|
91
|
+
'1634986666676-ec8fd927c23d',
|
|
92
|
+
'1639762681485-074b7f938ba0',
|
|
93
|
+
'1633899306328-c5e70574aaa2',
|
|
94
|
+
'1634017839464-5c339ebe3cb4',
|
|
95
|
+
'1644143379190-08a5f055de1d',
|
|
96
|
+
'1617791160505-6f00504e3519',
|
|
97
|
+
'1618005182384-a83a8bd57fbe',
|
|
98
|
+
'1635776062127-d379bfcba9f8',
|
|
99
|
+
'1618556450994-a6a128ef0d9d',
|
|
100
|
+
'1620207418302-439b387441b0',
|
|
101
|
+
'1604079628040-94301bb21b91',
|
|
102
|
+
'1557682250-33bd709cbe85',
|
|
103
|
+
],
|
|
104
|
+
),
|
|
105
|
+
HomeCategory.gradients: _CategoryData(
|
|
106
|
+
title: 'Gradients',
|
|
107
|
+
subtitle: 'Smooth, vivid color',
|
|
108
|
+
thumbnailId: '1557672172-298e090bd0f1',
|
|
109
|
+
photoIds: <String>[
|
|
110
|
+
'1557672172-298e090bd0f1',
|
|
111
|
+
'1557683316-973673baf926',
|
|
112
|
+
'1579546929518-9e396f3cc809',
|
|
113
|
+
'1620207418302-439b387441b0',
|
|
114
|
+
'1614851099175-e5b30eb6f696',
|
|
115
|
+
'1550859492-d5da9d8e45f3',
|
|
116
|
+
'1557682250-33bd709cbe85',
|
|
117
|
+
'1557682224-5b8590cd9ec5',
|
|
118
|
+
'1604079628040-94301bb21b91',
|
|
119
|
+
'1508614999368-9260051292e5',
|
|
120
|
+
'1554189097-ffe88e998a2b',
|
|
121
|
+
'1620641788421-7a1c342ea42e',
|
|
122
|
+
],
|
|
123
|
+
),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Author / timestamp pools — varied per photo by index so captions feel real
|
|
127
|
+
// without hand-authoring metadata for every image.
|
|
128
|
+
const List<String> _authors = <String>[
|
|
129
|
+
'Elena Park', 'Marcus Vale', 'Liang Wu', 'Sofia Reis', 'Noah Fields',
|
|
130
|
+
'Aya Tanaka', 'Hugo Brandt', 'Petra Nilsson', 'Diego Sol', 'Mara Lopez',
|
|
131
|
+
'Theo Klein', 'Ines Costa', 'Omar Aziz', 'Lena Fox',
|
|
132
|
+
];
|
|
133
|
+
const List<String> _agos = <String>[
|
|
134
|
+
'1 day ago', '2 days ago', '3 days ago', '5 days ago',
|
|
135
|
+
'6 days ago', '1 week ago', '2 weeks ago', '4 days ago',
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
List<HomePhoto> _photosFor(HomeCategory category) {
|
|
139
|
+
final List<String> ids = _categories[category]!.photoIds;
|
|
140
|
+
return <HomePhoto>[
|
|
141
|
+
for (int i = 0; i < ids.length; i++)
|
|
142
|
+
HomePhoto(
|
|
143
|
+
id: ids[i],
|
|
144
|
+
author: _authors[i % _authors.length],
|
|
145
|
+
ago: _agos[i % _agos.length],
|
|
146
|
+
),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
String _thumbUrl(String id) =>
|
|
151
|
+
'https://images.unsplash.com/photo-$id?w=160&h=160&fit=crop&q=80&auto=format';
|
|
152
|
+
|
|
153
|
+
/// Home content: a selectable category filter row over a masonry image feed.
|
|
154
|
+
class HomeFeed extends StatefulWidget {
|
|
155
|
+
const HomeFeed({super.key});
|
|
156
|
+
|
|
157
|
+
@override
|
|
158
|
+
State<HomeFeed> createState() => _HomeFeedState();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
class _HomeFeedState extends State<HomeFeed> {
|
|
162
|
+
HomeCategory _selected = HomeCategory.productDesign;
|
|
163
|
+
|
|
164
|
+
void _select(HomeCategory category) {
|
|
165
|
+
if (category == _selected) return;
|
|
166
|
+
KasyHaptics.light(context);
|
|
167
|
+
setState(() => _selected = category);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@override
|
|
171
|
+
Widget build(BuildContext context) {
|
|
172
|
+
return SingleChildScrollView(
|
|
173
|
+
child: Column(
|
|
174
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
175
|
+
children: <Widget>[
|
|
176
|
+
const SizedBox(height: KasySpacing.belowChromeContentGap),
|
|
177
|
+
_FilterRow(selected: _selected, onSelect: _select),
|
|
178
|
+
const SizedBox(height: KasySpacing.lg),
|
|
179
|
+
Padding(
|
|
180
|
+
// Reserve the floating bar's height (injected as the bottom inset by
|
|
181
|
+
// the host Scaffold's extendBody) so the last row settles above it.
|
|
182
|
+
padding: EdgeInsets.fromLTRB(
|
|
183
|
+
KasySpacing.md,
|
|
184
|
+
0,
|
|
185
|
+
KasySpacing.md,
|
|
186
|
+
KasySpacing.lg + MediaQuery.paddingOf(context).bottom,
|
|
187
|
+
),
|
|
188
|
+
child: HomeImageGrid(photos: _photosFor(_selected)),
|
|
189
|
+
),
|
|
190
|
+
],
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
class _FilterRow extends StatelessWidget {
|
|
197
|
+
final HomeCategory selected;
|
|
198
|
+
final ValueChanged<HomeCategory> onSelect;
|
|
199
|
+
|
|
200
|
+
const _FilterRow({required this.selected, required this.onSelect});
|
|
201
|
+
|
|
202
|
+
@override
|
|
203
|
+
Widget build(BuildContext context) {
|
|
204
|
+
const List<HomeCategory> all = HomeCategory.values;
|
|
205
|
+
|
|
206
|
+
return SizedBox(
|
|
207
|
+
height: 111,
|
|
208
|
+
child: ListView.separated(
|
|
209
|
+
scrollDirection: Axis.horizontal,
|
|
210
|
+
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
|
|
211
|
+
itemCount: all.length,
|
|
212
|
+
separatorBuilder: (_, _) => const SizedBox(width: KasySpacing.md),
|
|
213
|
+
itemBuilder: (BuildContext context, int i) => SizedBox(
|
|
214
|
+
width: 302,
|
|
215
|
+
child: _FilterCard(
|
|
216
|
+
category: all[i],
|
|
217
|
+
selected: all[i] == selected,
|
|
218
|
+
onTap: () => onSelect(all[i]),
|
|
219
|
+
),
|
|
220
|
+
),
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
class _FilterCard extends StatelessWidget {
|
|
227
|
+
final HomeCategory category;
|
|
228
|
+
final bool selected;
|
|
229
|
+
final VoidCallback onTap;
|
|
230
|
+
|
|
231
|
+
const _FilterCard({
|
|
232
|
+
required this.category,
|
|
233
|
+
required this.selected,
|
|
234
|
+
required this.onTap,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
@override
|
|
238
|
+
Widget build(BuildContext context) {
|
|
239
|
+
final KasyColors c = context.colors;
|
|
240
|
+
final _CategoryData data = _categories[category]!;
|
|
241
|
+
|
|
242
|
+
return KasyCard(
|
|
243
|
+
onTap: onTap,
|
|
244
|
+
semanticLabel: data.title,
|
|
245
|
+
variant: selected ? KasyCardVariant.elevated : KasyCardVariant.filled,
|
|
246
|
+
borderColor: selected ? c.primary : null,
|
|
247
|
+
padding: const EdgeInsets.all(KasySpacing.md),
|
|
248
|
+
child: Row(
|
|
249
|
+
children: <Widget>[
|
|
250
|
+
ClipRRect(
|
|
251
|
+
borderRadius: BorderRadius.circular(KasyRadius.lg),
|
|
252
|
+
child: SizedBox(
|
|
253
|
+
width: 79,
|
|
254
|
+
height: 79,
|
|
255
|
+
child: KasyNetworkImage(url: _thumbUrl(data.thumbnailId)),
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
const SizedBox(width: KasySpacing.md),
|
|
259
|
+
Expanded(
|
|
260
|
+
child: Column(
|
|
261
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
262
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
263
|
+
mainAxisSize: MainAxisSize.min,
|
|
264
|
+
children: <Widget>[
|
|
265
|
+
Text(
|
|
266
|
+
data.title,
|
|
267
|
+
maxLines: 1,
|
|
268
|
+
overflow: TextOverflow.ellipsis,
|
|
269
|
+
style: context.textTheme.titleSmall?.copyWith(
|
|
270
|
+
color: selected ? c.primary : c.onSurface,
|
|
271
|
+
),
|
|
272
|
+
),
|
|
273
|
+
const SizedBox(height: KasySpacing.xs),
|
|
274
|
+
Text(
|
|
275
|
+
data.subtitle,
|
|
276
|
+
maxLines: 1,
|
|
277
|
+
overflow: TextOverflow.ellipsis,
|
|
278
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
279
|
+
color: c.muted,
|
|
280
|
+
),
|
|
281
|
+
),
|
|
282
|
+
],
|
|
283
|
+
),
|
|
284
|
+
),
|
|
285
|
+
],
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import 'dart:ui' as ui;
|
|
2
|
+
|
|
3
|
+
import 'package:flutter/material.dart';
|
|
4
|
+
import 'package:kasy_kit/components/kasy_card.dart';
|
|
5
|
+
import 'package:kasy_kit/components/kasy_image_viewer.dart';
|
|
6
|
+
import 'package:kasy_kit/components/kasy_skeleton.dart';
|
|
7
|
+
import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
|
|
8
|
+
import 'package:kasy_kit/core/theme/theme.dart';
|
|
9
|
+
|
|
10
|
+
/// A single image shown in the masonry feed.
|
|
11
|
+
class HomePhoto {
|
|
12
|
+
final String id;
|
|
13
|
+
final String author;
|
|
14
|
+
final String ago;
|
|
15
|
+
|
|
16
|
+
const HomePhoto({required this.id, required this.author, required this.ago});
|
|
17
|
+
|
|
18
|
+
String get url =>
|
|
19
|
+
'https://images.unsplash.com/photo-$id?w=600&q=75&fit=crop&auto=format';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Network image with a synced shimmer skeleton while loading and a graceful
|
|
23
|
+
/// fallback on error. Adapts to light/dark via [KasySkeleton]'s own palette.
|
|
24
|
+
class KasyNetworkImage extends StatelessWidget {
|
|
25
|
+
const KasyNetworkImage({super.key, required this.url, this.fit = BoxFit.cover});
|
|
26
|
+
|
|
27
|
+
final String url;
|
|
28
|
+
final BoxFit fit;
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
Widget build(BuildContext context) {
|
|
32
|
+
return Image.network(
|
|
33
|
+
url,
|
|
34
|
+
fit: fit,
|
|
35
|
+
gaplessPlayback: true,
|
|
36
|
+
loadingBuilder: (
|
|
37
|
+
BuildContext context,
|
|
38
|
+
Widget child,
|
|
39
|
+
ImageChunkEvent? progress,
|
|
40
|
+
) {
|
|
41
|
+
if (progress == null) return child;
|
|
42
|
+
return const _ImageSkeleton();
|
|
43
|
+
},
|
|
44
|
+
errorBuilder: (
|
|
45
|
+
BuildContext context,
|
|
46
|
+
Object error,
|
|
47
|
+
StackTrace? stackTrace,
|
|
48
|
+
) {
|
|
49
|
+
return ColoredBox(
|
|
50
|
+
color: context.colors.surfaceNeutralSoft,
|
|
51
|
+
child: Icon(KasyIcons.gallery, color: context.colors.muted),
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class _ImageSkeleton extends StatelessWidget {
|
|
59
|
+
const _ImageSkeleton();
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
Widget build(BuildContext context) {
|
|
63
|
+
return const KasySkeletonGroup(
|
|
64
|
+
child: KasySkeleton(
|
|
65
|
+
width: double.infinity,
|
|
66
|
+
height: double.infinity,
|
|
67
|
+
borderRadius: BorderRadius.zero,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Pinterest-style masonry feed. Tile heights vary by a repeating ratio set and
|
|
74
|
+
/// tiles are balanced across columns by running height. Column count is
|
|
75
|
+
/// responsive: 2 on phones, 3 on tablets, 4 on desktop.
|
|
76
|
+
class HomeImageGrid extends StatelessWidget {
|
|
77
|
+
const HomeImageGrid({super.key, required this.photos});
|
|
78
|
+
|
|
79
|
+
final List<HomePhoto> photos;
|
|
80
|
+
|
|
81
|
+
static const double _gap = KasySpacing.smd;
|
|
82
|
+
static const List<double> _ratios = <double>[
|
|
83
|
+
0.72, 1.0, 0.8, 1.32, 0.75, 1.0, 0.7, 1.25, 0.85, 1.4,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
int _columnsFor(double width) {
|
|
87
|
+
if (width >= 1024) return 4;
|
|
88
|
+
if (width >= 600) return 3;
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@override
|
|
93
|
+
Widget build(BuildContext context) {
|
|
94
|
+
return LayoutBuilder(
|
|
95
|
+
builder: (BuildContext context, BoxConstraints constraints) {
|
|
96
|
+
final double width = constraints.maxWidth;
|
|
97
|
+
final int columns = _columnsFor(width);
|
|
98
|
+
final double columnWidth = (width - _gap * (columns - 1)) / columns;
|
|
99
|
+
|
|
100
|
+
final List<List<Widget>> buckets =
|
|
101
|
+
List<List<Widget>>.generate(columns, (_) => <Widget>[]);
|
|
102
|
+
final List<double> heights = List<double>.filled(columns, 0);
|
|
103
|
+
|
|
104
|
+
for (int i = 0; i < photos.length; i++) {
|
|
105
|
+
final double ratio = _ratios[i % _ratios.length];
|
|
106
|
+
int shortest = 0;
|
|
107
|
+
for (int c = 1; c < columns; c++) {
|
|
108
|
+
if (heights[c] < heights[shortest]) shortest = c;
|
|
109
|
+
}
|
|
110
|
+
if (buckets[shortest].isNotEmpty) {
|
|
111
|
+
buckets[shortest].add(const SizedBox(height: _gap));
|
|
112
|
+
}
|
|
113
|
+
buckets[shortest]
|
|
114
|
+
.add(_PhotoTile(photo: photos[i], aspectRatio: ratio));
|
|
115
|
+
heights[shortest] += columnWidth / ratio + _gap;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return Row(
|
|
119
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
120
|
+
children: <Widget>[
|
|
121
|
+
for (int col = 0; col < columns; col++) ...<Widget>[
|
|
122
|
+
if (col > 0) const SizedBox(width: _gap),
|
|
123
|
+
Expanded(child: Column(children: buckets[col])),
|
|
124
|
+
],
|
|
125
|
+
],
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class _PhotoTile extends StatefulWidget {
|
|
133
|
+
final HomePhoto photo;
|
|
134
|
+
final double aspectRatio;
|
|
135
|
+
|
|
136
|
+
const _PhotoTile({required this.photo, required this.aspectRatio});
|
|
137
|
+
|
|
138
|
+
@override
|
|
139
|
+
State<_PhotoTile> createState() => _PhotoTileState();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
class _PhotoTileState extends State<_PhotoTile> {
|
|
143
|
+
bool _liked = false;
|
|
144
|
+
|
|
145
|
+
void _toggleLike() {
|
|
146
|
+
KasyHaptics.light(context);
|
|
147
|
+
setState(() => _liked = !_liked);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
void _openViewer() {
|
|
151
|
+
KasyHaptics.light(context);
|
|
152
|
+
showKasyImageViewer(
|
|
153
|
+
context,
|
|
154
|
+
imageUrl: widget.photo.url,
|
|
155
|
+
heroTag: widget.photo.id,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@override
|
|
160
|
+
Widget build(BuildContext context) {
|
|
161
|
+
final HomePhoto photo = widget.photo;
|
|
162
|
+
|
|
163
|
+
return KasyCard(
|
|
164
|
+
padding: EdgeInsets.zero,
|
|
165
|
+
child: AspectRatio(
|
|
166
|
+
aspectRatio: widget.aspectRatio,
|
|
167
|
+
child: Stack(
|
|
168
|
+
fit: StackFit.expand,
|
|
169
|
+
children: <Widget>[
|
|
170
|
+
// Tapping the photo opens the full-screen viewer; the Hero gives a
|
|
171
|
+
// smooth zoom into and out of it.
|
|
172
|
+
Positioned.fill(
|
|
173
|
+
child: GestureDetector(
|
|
174
|
+
onTap: _openViewer,
|
|
175
|
+
behavior: HitTestBehavior.opaque,
|
|
176
|
+
child: Hero(
|
|
177
|
+
tag: photo.id,
|
|
178
|
+
child: KasyNetworkImage(url: photo.url),
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
|
|
183
|
+
// Smooth caption scrim — keeps text legible over any photo, in
|
|
184
|
+
// both light and dark themes.
|
|
185
|
+
const Positioned(
|
|
186
|
+
left: 0,
|
|
187
|
+
right: 0,
|
|
188
|
+
bottom: 0,
|
|
189
|
+
child: _CaptionScrim(),
|
|
190
|
+
),
|
|
191
|
+
|
|
192
|
+
Positioned(
|
|
193
|
+
left: 0,
|
|
194
|
+
right: 0,
|
|
195
|
+
bottom: 0,
|
|
196
|
+
child: Padding(
|
|
197
|
+
padding: const EdgeInsets.all(KasySpacing.md),
|
|
198
|
+
child: Row(
|
|
199
|
+
crossAxisAlignment: CrossAxisAlignment.end,
|
|
200
|
+
children: <Widget>[
|
|
201
|
+
Expanded(
|
|
202
|
+
child: Column(
|
|
203
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
204
|
+
mainAxisSize: MainAxisSize.min,
|
|
205
|
+
children: <Widget>[
|
|
206
|
+
Text(
|
|
207
|
+
photo.author,
|
|
208
|
+
maxLines: 1,
|
|
209
|
+
overflow: TextOverflow.ellipsis,
|
|
210
|
+
style: context.textTheme.titleSmall?.copyWith(
|
|
211
|
+
color: context.colors.onSurface,
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
Text(
|
|
215
|
+
photo.ago,
|
|
216
|
+
maxLines: 1,
|
|
217
|
+
overflow: TextOverflow.ellipsis,
|
|
218
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
219
|
+
color: context.colors.muted,
|
|
220
|
+
),
|
|
221
|
+
),
|
|
222
|
+
],
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
const SizedBox(width: KasySpacing.sm),
|
|
226
|
+
_LikeButton(liked: _liked, onTap: _toggleLike),
|
|
227
|
+
],
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
],
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// A smooth caption scrim: one continuous [surface]-tinted gradient that fades
|
|
239
|
+
/// from solid at the bottom to fully transparent at the top — no banding, no
|
|
240
|
+
/// hard edges. Adapts to light/dark via the theme's [surface] colour.
|
|
241
|
+
class _CaptionScrim extends StatelessWidget {
|
|
242
|
+
const _CaptionScrim();
|
|
243
|
+
|
|
244
|
+
static const double _height = 88;
|
|
245
|
+
|
|
246
|
+
@override
|
|
247
|
+
Widget build(BuildContext context) {
|
|
248
|
+
final Color base = context.colors.surface;
|
|
249
|
+
|
|
250
|
+
return Container(
|
|
251
|
+
height: _height,
|
|
252
|
+
decoration: BoxDecoration(
|
|
253
|
+
gradient: LinearGradient(
|
|
254
|
+
begin: Alignment.bottomCenter,
|
|
255
|
+
end: Alignment.topCenter,
|
|
256
|
+
colors: <Color>[
|
|
257
|
+
base.withValues(alpha: 0.94),
|
|
258
|
+
base.withValues(alpha: 0.62),
|
|
259
|
+
base.withValues(alpha: 0.0),
|
|
260
|
+
],
|
|
261
|
+
stops: const <double>[0.0, 0.45, 1.0],
|
|
262
|
+
),
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Circular glass "like" button. The heart pops (scales with an overshoot) and
|
|
269
|
+
/// turns the theme's danger red when liked; idle state stays neutral.
|
|
270
|
+
class _LikeButton extends StatefulWidget {
|
|
271
|
+
final bool liked;
|
|
272
|
+
final VoidCallback onTap;
|
|
273
|
+
|
|
274
|
+
const _LikeButton({required this.liked, required this.onTap});
|
|
275
|
+
|
|
276
|
+
@override
|
|
277
|
+
State<_LikeButton> createState() => _LikeButtonState();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
class _LikeButtonState extends State<_LikeButton>
|
|
281
|
+
with SingleTickerProviderStateMixin {
|
|
282
|
+
late final AnimationController _controller;
|
|
283
|
+
late final Animation<double> _scale;
|
|
284
|
+
|
|
285
|
+
@override
|
|
286
|
+
void initState() {
|
|
287
|
+
super.initState();
|
|
288
|
+
_controller = AnimationController(
|
|
289
|
+
vsync: this,
|
|
290
|
+
duration: const Duration(milliseconds: 320),
|
|
291
|
+
);
|
|
292
|
+
_scale = TweenSequence<double>(<TweenSequenceItem<double>>[
|
|
293
|
+
TweenSequenceItem<double>(
|
|
294
|
+
tween: Tween<double>(begin: 1.0, end: 1.35)
|
|
295
|
+
.chain(CurveTween(curve: Curves.easeOut)),
|
|
296
|
+
weight: 40,
|
|
297
|
+
),
|
|
298
|
+
TweenSequenceItem<double>(
|
|
299
|
+
tween: Tween<double>(begin: 1.35, end: 1.0)
|
|
300
|
+
.chain(CurveTween(curve: Curves.elasticOut)),
|
|
301
|
+
weight: 60,
|
|
302
|
+
),
|
|
303
|
+
]).animate(_controller);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@override
|
|
307
|
+
void didUpdateWidget(_LikeButton old) {
|
|
308
|
+
super.didUpdateWidget(old);
|
|
309
|
+
// Pop only when transitioning into the liked state.
|
|
310
|
+
if (widget.liked && !old.liked) {
|
|
311
|
+
_controller.forward(from: 0);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@override
|
|
316
|
+
void dispose() {
|
|
317
|
+
_controller.dispose();
|
|
318
|
+
super.dispose();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@override
|
|
322
|
+
Widget build(BuildContext context) {
|
|
323
|
+
final bool dark = context.isDark;
|
|
324
|
+
final Color heart =
|
|
325
|
+
widget.liked ? context.colors.danger : context.colors.primary;
|
|
326
|
+
|
|
327
|
+
return GestureDetector(
|
|
328
|
+
onTap: widget.onTap,
|
|
329
|
+
behavior: HitTestBehavior.opaque,
|
|
330
|
+
child: ClipOval(
|
|
331
|
+
child: BackdropFilter(
|
|
332
|
+
filter: ui.ImageFilter.blur(sigmaX: 8, sigmaY: 8),
|
|
333
|
+
child: Container(
|
|
334
|
+
width: 36,
|
|
335
|
+
height: 36,
|
|
336
|
+
alignment: Alignment.center,
|
|
337
|
+
decoration: BoxDecoration(
|
|
338
|
+
color: context.colors.surface
|
|
339
|
+
.withValues(alpha: dark ? 0.5 : 0.7),
|
|
340
|
+
shape: BoxShape.circle,
|
|
341
|
+
),
|
|
342
|
+
child: ScaleTransition(
|
|
343
|
+
scale: _scale,
|
|
344
|
+
child: Icon(
|
|
345
|
+
widget.liked ? KasyIcons.favoriteFilled : KasyIcons.favorite,
|
|
346
|
+
size: 18,
|
|
347
|
+
color: heart,
|
|
348
|
+
),
|
|
349
|
+
),
|
|
350
|
+
),
|
|
351
|
+
),
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|