From b90e4793a468da58e4cfc91a74f80da02e776074 Mon Sep 17 00:00:00 2001 From: Kyaw Zayar Tun Date: Fri, 20 Feb 2026 16:49:22 +0630 Subject: [PATCH 1/4] feat: Upgrade Dart SDK to 3.11, adopt null-aware map entry syntax, and update iOS plugin registration for implicit engine. --- analysis_options.yaml | 1 - .../mobile/ios/Flutter/AppFrameworkInfo.plist | 2 - apps/mobile/ios/Runner/AppDelegate.swift | 7 ++- apps/mobile/ios/Runner/Info.plist | 48 +++++++++++++------ apps/mobile/pubspec.yaml | 4 +- packages/common/env/pubspec.yaml | 2 +- packages/common/ui/pubspec.yaml | 2 +- packages/common/utils/pubspec.yaml | 2 +- packages/core/data/pubspec.yaml | 2 +- packages/core/domain/pubspec.yaml | 2 +- .../lib/src/core/analytics_event.dart | 4 +- .../analytics/lib/src/events/app_events.dart | 8 ++-- .../lib/src/events/commerce_events.dart | 6 +-- .../lib/src/events/engagement_events.dart | 12 ++--- .../analytics/lib/src/events/form_events.dart | 16 +++---- .../lib/src/events/media_events.dart | 2 +- .../lib/src/events/notification_events.dart | 8 ++-- .../lib/src/events/performance_events.dart | 2 +- .../lib/src/events/screen_events.dart | 14 +++--- .../lib/src/events/social_events.dart | 6 +-- packages/integrations/analytics/pubspec.yaml | 2 +- .../integrations/barcode_scanner/pubspec.yaml | 2 +- packages/integrations/database/pubspec.yaml | 2 +- packages/integrations/logger/pubspec.yaml | 2 +- .../lib/src/clients/google_books_client.dart | 4 +- .../lib/src/clients/tmdb_client.dart | 24 +++++----- .../integrations/metadata_api/pubspec.yaml | 2 +- packages/integrations/payment/pubspec.yaml | 2 +- packages/integrations/storage/pubspec.yaml | 4 +- pubspec.yaml | 9 +--- scripts/create_package.sh | 2 +- 31 files changed, 107 insertions(+), 98 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 94a87de..8dbc6ce 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -13,7 +13,6 @@ linter: rules: # Error rules avoid_print: true - avoid_returning_null_for_future: true cancel_subscriptions: true close_sinks: true valid_regexps: true diff --git a/apps/mobile/ios/Flutter/AppFrameworkInfo.plist b/apps/mobile/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7..391a902 100644 --- a/apps/mobile/ios/Flutter/AppFrameworkInfo.plist +++ b/apps/mobile/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/apps/mobile/ios/Runner/AppDelegate.swift b/apps/mobile/ios/Runner/AppDelegate.swift index 6266644..c30b367 100644 --- a/apps/mobile/ios/Runner/AppDelegate.swift +++ b/apps/mobile/ios/Runner/AppDelegate.swift @@ -2,12 +2,15 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/apps/mobile/ios/Runner/Info.plist b/apps/mobile/ios/Runner/Info.plist index 5f111a8..a1530b2 100644 --- a/apps/mobile/ios/Runner/Info.plist +++ b/apps/mobile/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,37 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + + NSCameraUsageDescription + This app needs camera access to scan barcodes for adding items to your collection + NSPhotoLibraryUsageDescription + This app needs photo library access to add images to your items + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UIFileSharingEnabled + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,20 +74,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - - NSCameraUsageDescription - This app needs camera access to scan barcodes for adding items to your collection - - NSPhotoLibraryUsageDescription - This app needs photo library access to add images to your items - UIFileSharingEnabled - - LSSupportsOpeningDocumentsInPlace - diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml index 14ed4ae..5936d3c 100644 --- a/apps/mobile/pubspec.yaml +++ b/apps/mobile/pubspec.yaml @@ -5,7 +5,7 @@ version: 1.0.0+1 resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 dependencies: flutter: @@ -64,8 +64,6 @@ dev_dependencies: flutter_launcher_icons: ^0.14.4 flutter: - config: - enable-swift-package-manager: true uses-material-design: true generate: true assets: diff --git a/packages/common/env/pubspec.yaml b/packages/common/env/pubspec.yaml index 774e7ee..c98eda7 100644 --- a/packages/common/env/pubspec.yaml +++ b/packages/common/env/pubspec.yaml @@ -4,7 +4,7 @@ version: 1.0.0 publish_to: 'none' environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: diff --git a/packages/common/ui/pubspec.yaml b/packages/common/ui/pubspec.yaml index 7bb9523..a634476 100644 --- a/packages/common/ui/pubspec.yaml +++ b/packages/common/ui/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: diff --git a/packages/common/utils/pubspec.yaml b/packages/common/utils/pubspec.yaml index 33d95b9..4935834 100644 --- a/packages/common/utils/pubspec.yaml +++ b/packages/common/utils/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: diff --git a/packages/core/data/pubspec.yaml b/packages/core/data/pubspec.yaml index d0d7486..8432892 100644 --- a/packages/core/data/pubspec.yaml +++ b/packages/core/data/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: diff --git a/packages/core/domain/pubspec.yaml b/packages/core/domain/pubspec.yaml index e312ef1..b5eb91c 100644 --- a/packages/core/domain/pubspec.yaml +++ b/packages/core/domain/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: diff --git a/packages/integrations/analytics/lib/src/core/analytics_event.dart b/packages/integrations/analytics/lib/src/core/analytics_event.dart index a10e940..c8e3570 100644 --- a/packages/integrations/analytics/lib/src/core/analytics_event.dart +++ b/packages/integrations/analytics/lib/src/core/analytics_event.dart @@ -68,7 +68,7 @@ abstract class AnalyticsEvent with _$AnalyticsEvent { name: 'screen_view', properties: { 'screen_name': screenName, - if (screenClass != null) 'screen_class': screenClass, + 'screen_class': ?screenClass, ...?properties, }, timestamp: DateTime.now(), @@ -86,7 +86,7 @@ abstract class AnalyticsEvent with _$AnalyticsEvent { name: 'button_clicked', properties: { 'button_name': buttonName, - if (screenName != null) 'screen_name': screenName, + 'screen_name': ?screenName, ...?properties, }, timestamp: DateTime.now(), diff --git a/packages/integrations/analytics/lib/src/events/app_events.dart b/packages/integrations/analytics/lib/src/events/app_events.dart index 2d24b50..198a2d8 100644 --- a/packages/integrations/analytics/lib/src/events/app_events.dart +++ b/packages/integrations/analytics/lib/src/events/app_events.dart @@ -10,7 +10,7 @@ class AppEvents { return AnalyticsEvent.custom( name: 'app_opened', category: 'lifecycle', - properties: {if (source != null) 'source': source, ...?properties}, + properties: {'source': ?source, ...?properties}, ); } @@ -43,7 +43,7 @@ class AppEvents { category: 'error', properties: { 'error': error, - if (stackTrace != null) 'stack_trace': stackTrace, + 'stack_trace': ?stackTrace, ...?properties, }, ); @@ -61,8 +61,8 @@ class AppEvents { category: 'error', properties: { 'error': error, - if (screen != null) 'screen': screen, - if (context != null) 'context': context, + 'screen': ?screen, + 'context': ?context, ...?properties, }, ); diff --git a/packages/integrations/analytics/lib/src/events/commerce_events.dart b/packages/integrations/analytics/lib/src/events/commerce_events.dart index b99b1ba..28c618b 100644 --- a/packages/integrations/analytics/lib/src/events/commerce_events.dart +++ b/packages/integrations/analytics/lib/src/events/commerce_events.dart @@ -16,8 +16,8 @@ class CommerceEvents { properties: { 'product_id': productId, 'product_name': productName, - if (category != null) 'category': category, - if (price != null) 'price': price, + 'category': ?category, + 'price': ?price, ...?properties, }, ); @@ -63,7 +63,7 @@ class CommerceEvents { 'plan_name': planName, 'price': price, 'currency': currency ?? 'USD', - if (billingPeriod != null) 'billing_period': billingPeriod, + 'billing_period': ?billingPeriod, ...?properties, }, ); diff --git a/packages/integrations/analytics/lib/src/events/engagement_events.dart b/packages/integrations/analytics/lib/src/events/engagement_events.dart index 098fc42..6d54b8e 100644 --- a/packages/integrations/analytics/lib/src/events/engagement_events.dart +++ b/packages/integrations/analytics/lib/src/events/engagement_events.dart @@ -16,8 +16,8 @@ class EngagementEvents { properties: { 'query_length': query.length, 'result_count': resultCount, - if (category != null) 'category': category, - if (duration != null) 'duration_ms': duration, + 'category': ?category, + 'duration_ms': ?duration, ...?properties, }, ); @@ -36,7 +36,7 @@ class EngagementEvents { properties: { 'filter_type': filterType, 'filter_value': filterValue.toString(), - if (resultCount != null) 'result_count': resultCount, + 'result_count': ?resultCount, ...?properties, }, ); @@ -107,7 +107,7 @@ class EngagementEvents { 'content_type': contentType, 'content_id': contentId, 'rating': rating, - if (maxRating != null) 'max_rating': maxRating, + 'max_rating': ?maxRating, ...?properties, }, ); @@ -126,7 +126,7 @@ class EngagementEvents { properties: { 'content_type': contentType, 'content_id': contentId, - if (commentLength != null) 'comment_length': commentLength, + 'comment_length': ?commentLength, ...?properties, }, ); @@ -145,7 +145,7 @@ class EngagementEvents { properties: { 'tutorial_name': tutorialName, 'step_count': stepCount, - if (duration != null) 'duration_ms': duration, + 'duration_ms': ?duration, ...?properties, }, ); diff --git a/packages/integrations/analytics/lib/src/events/form_events.dart b/packages/integrations/analytics/lib/src/events/form_events.dart index 66c29b1..5383335 100644 --- a/packages/integrations/analytics/lib/src/events/form_events.dart +++ b/packages/integrations/analytics/lib/src/events/form_events.dart @@ -13,7 +13,7 @@ class FormEvents { category: 'form', properties: { 'form_name': formName, - if (formId != null) 'form_id': formId, + 'form_id': ?formId, ...?properties, }, ); @@ -32,9 +32,9 @@ class FormEvents { category: 'form', properties: { 'form_name': formName, - if (formId != null) 'form_id': formId, - if (duration != null) 'duration_ms': duration, - if (fieldCount != null) 'field_count': fieldCount, + 'form_id': ?formId, + 'duration_ms': ?duration, + 'field_count': ?fieldCount, ...?properties, }, ); @@ -53,9 +53,9 @@ class FormEvents { category: 'form', properties: { 'form_name': formName, - if (formId != null) 'form_id': formId, - if (lastFieldIndex != null) 'last_field_index': lastFieldIndex, - if (duration != null) 'duration_ms': duration, + 'form_id': ?formId, + 'last_field_index': ?lastFieldIndex, + 'duration_ms': ?duration, ...?properties, }, ); @@ -110,7 +110,7 @@ class FormEvents { properties: { 'form_name': formName, 'field_name': fieldName, - if (duration != null) 'duration_ms': duration, + 'duration_ms': ?duration, ...?properties, }, ); diff --git a/packages/integrations/analytics/lib/src/events/media_events.dart b/packages/integrations/analytics/lib/src/events/media_events.dart index 0a5b32a..fa11f01 100644 --- a/packages/integrations/analytics/lib/src/events/media_events.dart +++ b/packages/integrations/analytics/lib/src/events/media_events.dart @@ -17,7 +17,7 @@ class MediaEvents { 'media_type': mediaType, 'media_id': mediaId, 'media_title': mediaTitle, - if (duration != null) 'duration_seconds': duration, + 'duration_seconds': ?duration, ...?properties, }, ); diff --git a/packages/integrations/analytics/lib/src/events/notification_events.dart b/packages/integrations/analytics/lib/src/events/notification_events.dart index 9020b7c..b769887 100644 --- a/packages/integrations/analytics/lib/src/events/notification_events.dart +++ b/packages/integrations/analytics/lib/src/events/notification_events.dart @@ -13,7 +13,7 @@ class NotificationEvents { category: 'notification', properties: { 'notification_type': notificationType, - if (campaignId != null) 'campaign_id': campaignId, + 'campaign_id': ?campaignId, ...?properties, }, ); @@ -31,8 +31,8 @@ class NotificationEvents { category: 'notification', properties: { 'notification_type': notificationType, - if (campaignId != null) 'campaign_id': campaignId, - if (action != null) 'action': action, + 'campaign_id': ?campaignId, + 'action': ?action, ...?properties, }, ); @@ -49,7 +49,7 @@ class NotificationEvents { category: 'notification', properties: { 'notification_type': notificationType, - if (campaignId != null) 'campaign_id': campaignId, + 'campaign_id': ?campaignId, ...?properties, }, ); diff --git a/packages/integrations/analytics/lib/src/events/performance_events.dart b/packages/integrations/analytics/lib/src/events/performance_events.dart index 6a589fe..fcac5b3 100644 --- a/packages/integrations/analytics/lib/src/events/performance_events.dart +++ b/packages/integrations/analytics/lib/src/events/performance_events.dart @@ -34,7 +34,7 @@ class PerformanceEvents { properties: { 'endpoint': endpoint, 'error': error, - if (statusCode != null) 'status_code': statusCode, + 'status_code': ?statusCode, ...?properties, }, ); diff --git a/packages/integrations/analytics/lib/src/events/screen_events.dart b/packages/integrations/analytics/lib/src/events/screen_events.dart index 1575663..38e4fe8 100644 --- a/packages/integrations/analytics/lib/src/events/screen_events.dart +++ b/packages/integrations/analytics/lib/src/events/screen_events.dart @@ -14,8 +14,8 @@ class ScreenEvents { category: 'navigation', properties: { 'screen_name': screenName, - if (screenClass != null) 'screen_class': screenClass, - if (previousScreen != null) 'previous_screen': previousScreen, + 'screen_class': ?screenClass, + 'previous_screen': ?previousScreen, ...?properties, }, ); @@ -34,7 +34,7 @@ class ScreenEvents { properties: { 'tab_name': tabName, 'tab_index': tabIndex, - if (screenName != null) 'screen_name': screenName, + 'screen_name': ?screenName, ...?properties, }, ); @@ -51,7 +51,7 @@ class ScreenEvents { category: 'navigation', properties: { 'modal_name': modalName, - if (trigger != null) 'trigger': trigger, + 'trigger': ?trigger, ...?properties, }, ); @@ -69,8 +69,8 @@ class ScreenEvents { category: 'navigation', properties: { 'modal_name': modalName, - if (action != null) 'action': action, - if (timeSpent != null) 'time_spent_ms': timeSpent, + 'action': ?action, + 'time_spent_ms': ?timeSpent, ...?properties, }, ); @@ -87,7 +87,7 @@ class ScreenEvents { category: 'navigation', properties: { 'deep_link': deepLink, - if (source != null) 'source': source, + 'source': ?source, ...?properties, }, ); diff --git a/packages/integrations/analytics/lib/src/events/social_events.dart b/packages/integrations/analytics/lib/src/events/social_events.dart index 9111980..19273af 100644 --- a/packages/integrations/analytics/lib/src/events/social_events.dart +++ b/packages/integrations/analytics/lib/src/events/social_events.dart @@ -13,7 +13,7 @@ class SocialEvents { category: 'social', properties: { 'method': method, - if (inviteCount != null) 'invite_count': inviteCount, + 'invite_count': ?inviteCount, ...?properties, }, ); @@ -30,7 +30,7 @@ class SocialEvents { category: 'social', properties: { 'referral_code': referralCode, - if (referrerId != null) 'referrer_id': referrerId, + 'referrer_id': ?referrerId, ...?properties, }, ); @@ -47,7 +47,7 @@ class SocialEvents { category: 'social', properties: { 'profile_id': profileId, - if (profileType != null) 'profile_type': profileType, + 'profile_type': ?profileType, ...?properties, }, ); diff --git a/packages/integrations/analytics/pubspec.yaml b/packages/integrations/analytics/pubspec.yaml index b02dbad..a99d4b3 100644 --- a/packages/integrations/analytics/pubspec.yaml +++ b/packages/integrations/analytics/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: diff --git a/packages/integrations/barcode_scanner/pubspec.yaml b/packages/integrations/barcode_scanner/pubspec.yaml index f16e0a5..655044e 100644 --- a/packages/integrations/barcode_scanner/pubspec.yaml +++ b/packages/integrations/barcode_scanner/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: diff --git a/packages/integrations/database/pubspec.yaml b/packages/integrations/database/pubspec.yaml index d1d0f48..66f069d 100644 --- a/packages/integrations/database/pubspec.yaml +++ b/packages/integrations/database/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: diff --git a/packages/integrations/logger/pubspec.yaml b/packages/integrations/logger/pubspec.yaml index 1b9cd8c..97bc1ff 100644 --- a/packages/integrations/logger/pubspec.yaml +++ b/packages/integrations/logger/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: diff --git a/packages/integrations/metadata_api/lib/src/clients/google_books_client.dart b/packages/integrations/metadata_api/lib/src/clients/google_books_client.dart index 2da8f6e..b726a82 100644 --- a/packages/integrations/metadata_api/lib/src/clients/google_books_client.dart +++ b/packages/integrations/metadata_api/lib/src/clients/google_books_client.dart @@ -66,8 +66,8 @@ class GoogleBooksClient { 'maxResults': pageSize, 'printType': 'books', if (_apiKey != null) 'key': _apiKey, - if (langRestrict != null) 'langRestrict': langRestrict, - if (orderBy != null) 'orderBy': orderBy, + 'langRestrict': ?langRestrict, + 'orderBy': ?orderBy, }; final response = await _dio.get('/volumes', queryParameters: queryParams); diff --git a/packages/integrations/metadata_api/lib/src/clients/tmdb_client.dart b/packages/integrations/metadata_api/lib/src/clients/tmdb_client.dart index d688371..c31d552 100644 --- a/packages/integrations/metadata_api/lib/src/clients/tmdb_client.dart +++ b/packages/integrations/metadata_api/lib/src/clients/tmdb_client.dart @@ -69,8 +69,8 @@ class TMDBClient { 'query': query, 'page': page, 'include_adult': includeAdult, - if (language != null) 'language': language, - if (year != null) 'year': year, + 'language': ?language, + 'year': ?year, }; final response = await _dio.get( @@ -105,7 +105,7 @@ class TMDBClient { final queryParams = { // 'api_key': _apiKey, - if (language != null) 'language': language, + 'language': ?language, }; final response = await _dio.get( @@ -157,7 +157,7 @@ class TMDBClient { final queryParams = { // 'api_key': _apiKey, 'external_source': 'imdb_id', - if (language != null) 'language': language, + 'language': ?language, }; final response = await _dio.get( @@ -195,7 +195,7 @@ class TMDBClient { final queryParams = { // 'api_key': _apiKey, 'page': page, - if (language != null) 'language': language, + 'language': ?language, }; final response = await _dio.get( @@ -223,7 +223,7 @@ class TMDBClient { final queryParams = { // 'api_key': _apiKey, 'page': page, - if (language != null) 'language': language, + 'language': ?language, }; final response = await _dio.get( @@ -251,7 +251,7 @@ class TMDBClient { final queryParams = { // 'api_key': _apiKey, 'page': page, - if (language != null) 'language': language, + 'language': ?language, }; final response = await _dio.get( @@ -279,7 +279,7 @@ class TMDBClient { final queryParams = { // 'api_key': _apiKey, 'page': page, - if (language != null) 'language': language, + 'language': ?language, }; final response = await _dio.get( @@ -316,14 +316,14 @@ class TMDBClient { final queryParams = { // 'api_key': _apiKey, 'page': page, - if (language != null) 'language': language, - if (sortBy != null) 'sort_by': sortBy, + 'language': ?language, + 'sort_by': ?sortBy, if (yearFrom != null) 'primary_release_date.gte': '$yearFrom-01-01', if (yearTo != null) 'primary_release_date.lte': '$yearTo-12-31', if (withGenres != null && withGenres.isNotEmpty) 'with_genres': withGenres.join(','), - if (voteAverageGte != null) 'vote_average.gte': voteAverageGte, - if (voteCountGte != null) 'vote_count.gte': voteCountGte, + 'vote_average.gte': ?voteAverageGte, + 'vote_count.gte': ?voteCountGte, }; final response = await _dio.get( diff --git a/packages/integrations/metadata_api/pubspec.yaml b/packages/integrations/metadata_api/pubspec.yaml index 64e1c9b..d3a43df 100644 --- a/packages/integrations/metadata_api/pubspec.yaml +++ b/packages/integrations/metadata_api/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: diff --git a/packages/integrations/payment/pubspec.yaml b/packages/integrations/payment/pubspec.yaml index 122696b..9e9299a 100644 --- a/packages/integrations/payment/pubspec.yaml +++ b/packages/integrations/payment/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: diff --git a/packages/integrations/storage/pubspec.yaml b/packages/integrations/storage/pubspec.yaml index bd1c2a6..579204a 100644 --- a/packages/integrations/storage/pubspec.yaml +++ b/packages/integrations/storage/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: @@ -15,7 +15,7 @@ dependencies: path_provider: ^2.1.5 path: ^1.9.1 image: ^4.7.2 - file_picker: 10.3.7 # upgrade to latest after android plugin fixed + file_picker: ^10.3.10 share_plus: ^12.0.1 permission_handler: ^12.0.1 flutter_secure_storage: ^10.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 639ea91..04f1585 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 1.0.0 publish_to: 'none' environment: - sdk: ^3.10.4 + sdk: ^3.11.0 dev_dependencies: commitlint_cli: ^0.8.1 @@ -27,10 +27,3 @@ workspace: - packages/integrations/metadata_api - packages/integrations/payment - packages/integrations/storage - -dependency_overrides: - analyzer: ^9.0.0 - dart_style: 3.1.3 - test: 1.29.0 - test_api: 0.7.9 - test_core: 0.6.15 diff --git a/scripts/create_package.sh b/scripts/create_package.sh index e10a671..635f0d5 100755 --- a/scripts/create_package.sh +++ b/scripts/create_package.sh @@ -31,7 +31,7 @@ version: 1.0.0 publish_to: 'none' environment: - sdk: ^3.10.4 + sdk: ^3.11.0 flutter: ">=1.17.0" dependencies: From b1ceed280fa46462b07fe5318cd7dec9f0ffe7ba Mon Sep 17 00:00:00 2001 From: Kyaw Zayar Tun Date: Fri, 20 Feb 2026 16:50:00 +0630 Subject: [PATCH 2/4] refactor: Reformat analytics event properties maps for conciseness. --- .../integrations/analytics/lib/src/events/app_events.dart | 6 +----- .../integrations/analytics/lib/src/events/form_events.dart | 6 +----- .../analytics/lib/src/events/screen_events.dart | 6 +----- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/integrations/analytics/lib/src/events/app_events.dart b/packages/integrations/analytics/lib/src/events/app_events.dart index 198a2d8..382dc56 100644 --- a/packages/integrations/analytics/lib/src/events/app_events.dart +++ b/packages/integrations/analytics/lib/src/events/app_events.dart @@ -41,11 +41,7 @@ class AppEvents { return AnalyticsEvent.custom( name: 'app_crashed', category: 'error', - properties: { - 'error': error, - 'stack_trace': ?stackTrace, - ...?properties, - }, + properties: {'error': error, 'stack_trace': ?stackTrace, ...?properties}, ); } diff --git a/packages/integrations/analytics/lib/src/events/form_events.dart b/packages/integrations/analytics/lib/src/events/form_events.dart index 5383335..b80b1fe 100644 --- a/packages/integrations/analytics/lib/src/events/form_events.dart +++ b/packages/integrations/analytics/lib/src/events/form_events.dart @@ -11,11 +11,7 @@ class FormEvents { return AnalyticsEvent.custom( name: 'form_started', category: 'form', - properties: { - 'form_name': formName, - 'form_id': ?formId, - ...?properties, - }, + properties: {'form_name': formName, 'form_id': ?formId, ...?properties}, ); } diff --git a/packages/integrations/analytics/lib/src/events/screen_events.dart b/packages/integrations/analytics/lib/src/events/screen_events.dart index 38e4fe8..4be9f4e 100644 --- a/packages/integrations/analytics/lib/src/events/screen_events.dart +++ b/packages/integrations/analytics/lib/src/events/screen_events.dart @@ -85,11 +85,7 @@ class ScreenEvents { return AnalyticsEvent.custom( name: 'deep_link_opened', category: 'navigation', - properties: { - 'deep_link': deepLink, - 'source': ?source, - ...?properties, - }, + properties: {'deep_link': deepLink, 'source': ?source, ...?properties}, ); } } From efcfe78966a082974cb415a26a6ac7551bc5d540 Mon Sep 17 00:00:00 2001 From: Kyaw Zayar Tun Date: Fri, 20 Feb 2026 18:23:09 +0630 Subject: [PATCH 3/4] feat: Implement item price tracking with historical data and display on item detail screen --- .../providers/price_tracking_provider.dart | 8 + .../view_models/items_view_model.dart | 6 + .../presentation/views/add_item_screen.dart | 102 ++++++ .../presentation/views/edit_item_screen.dart | 118 +++++++ .../views/item_detail_screen.dart | 294 ++++++++++++++++++ .../view_models/statistics_view_model.dart | 5 +- .../presentation/views/statistics_screen.dart | 2 +- .../repositories/item_repository_impl.dart | 7 + .../lib/src/repositories/item_repository.dart | 1 + .../database/lib/src/app_database.dart | 16 +- .../database/lib/src/daos/item_dao.dart | 113 ++++++- .../src/tables/item_price_history_table.dart | 16 + .../database/lib/src/tables/tables.dart | 1 + .../database/test/database_test.dart | 72 +++++ 14 files changed, 755 insertions(+), 6 deletions(-) create mode 100644 apps/mobile/lib/features/items/presentation/providers/price_tracking_provider.dart create mode 100644 packages/integrations/database/lib/src/tables/item_price_history_table.dart diff --git a/apps/mobile/lib/features/items/presentation/providers/price_tracking_provider.dart b/apps/mobile/lib/features/items/presentation/providers/price_tracking_provider.dart new file mode 100644 index 0000000..e5a5fb7 --- /dev/null +++ b/apps/mobile/lib/features/items/presentation/providers/price_tracking_provider.dart @@ -0,0 +1,8 @@ +import 'package:collection_tracker/core/providers/providers.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final itemPriceHistoryProvider = + StreamProvider.family, String>((ref, itemId) { + final repository = ref.watch(itemRepositoryProvider); + return repository.watchPriceHistory(itemId); + }); diff --git a/apps/mobile/lib/features/items/presentation/view_models/items_view_model.dart b/apps/mobile/lib/features/items/presentation/view_models/items_view_model.dart index a1ba1c8..9a0a271 100644 --- a/apps/mobile/lib/features/items/presentation/view_models/items_view_model.dart +++ b/apps/mobile/lib/features/items/presentation/view_models/items_view_model.dart @@ -22,6 +22,9 @@ Future createItem( String? coverImageUrl, String? coverImagePath, List tags = const [], + double? purchasePrice, + double? currentValue, + DateTime? purchaseDate, }) async { final repository = ref.read(itemRepositoryProvider); @@ -34,6 +37,9 @@ Future createItem( coverImageUrl: coverImageUrl, coverImagePath: coverImagePath, tags: tags, + purchasePrice: purchasePrice, + currentValue: currentValue, + purchaseDate: purchaseDate, createdAt: DateTime.now(), updatedAt: DateTime.now(), ); diff --git a/apps/mobile/lib/features/items/presentation/views/add_item_screen.dart b/apps/mobile/lib/features/items/presentation/views/add_item_screen.dart index e68fa60..5ee45ef 100644 --- a/apps/mobile/lib/features/items/presentation/views/add_item_screen.dart +++ b/apps/mobile/lib/features/items/presentation/views/add_item_screen.dart @@ -27,6 +27,9 @@ class _AddItemScreenState extends ConsumerState { final _titleController = TextEditingController(); final _barcodeController = TextEditingController(); final _descriptionController = TextEditingController(); + final _purchasePriceController = TextEditingController(); + final _currentValueController = TextEditingController(); + final _purchaseDateController = TextEditingController(); final _imageStorageService = ImageStorageService(); @@ -35,12 +38,16 @@ class _AddItemScreenState extends ConsumerState { String? _imagePath; String? _coverImageUrl; List _tags = const []; + DateTime? _selectedPurchaseDate; @override void dispose() { _titleController.dispose(); _barcodeController.dispose(); _descriptionController.dispose(); + _purchasePriceController.dispose(); + _currentValueController.dispose(); + _purchaseDateController.dispose(); super.dispose(); } @@ -184,6 +191,63 @@ class _AddItemScreenState extends ConsumerState { label: 'Tags (optional)', hintText: 'e.g., Rare, Completed Set', ), + const SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: TextFormField( + controller: _purchasePriceController, + decoration: const InputDecoration( + labelText: 'Purchase Price', + prefixText: '\$', + prefixIcon: Icon(Icons.attach_money), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + validator: _validatePriceInput, + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextFormField( + controller: _currentValueController, + decoration: const InputDecoration( + labelText: 'Current Value', + prefixText: '\$', + prefixIcon: Icon(Icons.show_chart), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + validator: _validatePriceInput, + ), + ), + ], + ), + const SizedBox(height: 16), + + TextFormField( + controller: _purchaseDateController, + readOnly: true, + decoration: InputDecoration( + labelText: 'Purchase Date (optional)', + prefixIcon: const Icon(Icons.calendar_today), + suffixIcon: _selectedPurchaseDate == null + ? null + : IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + setState(() { + _selectedPurchaseDate = null; + _purchaseDateController.clear(); + }); + }, + ), + ), + onTap: _pickPurchaseDate, + ), const SizedBox(height: 24), // Add button @@ -308,6 +372,9 @@ class _AddItemScreenState extends ConsumerState { coverImageUrl: _coverImageUrl, coverImagePath: _imagePath, tags: _tags, + purchasePrice: _parsePriceInput(_purchasePriceController.text), + currentValue: _parsePriceInput(_currentValueController.text), + purchaseDate: _selectedPurchaseDate, ).future, ); @@ -334,4 +401,39 @@ class _AddItemScreenState extends ConsumerState { } } } + + String? _validatePriceInput(String? value) { + if (value == null || value.trim().isEmpty) return null; + final parsed = _parsePriceInput(value); + if (parsed == null) return 'Invalid price'; + if (parsed < 0) return 'Must be positive'; + return null; + } + + double? _parsePriceInput(String raw) { + final normalized = raw.trim().replaceAll(',', ''); + if (normalized.isEmpty) return null; + return double.tryParse(normalized); + } + + Future _pickPurchaseDate() async { + final picked = await showDatePicker( + context: context, + initialDate: _selectedPurchaseDate ?? DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime.now().add(const Duration(days: 3650)), + ); + + if (picked == null || !mounted) return; + setState(() { + _selectedPurchaseDate = picked; + _purchaseDateController.text = _formatDate(picked); + }); + } + + String _formatDate(DateTime date) { + final month = date.month.toString().padLeft(2, '0'); + final day = date.day.toString().padLeft(2, '0'); + return '${date.year}-$month-$day'; + } } diff --git a/apps/mobile/lib/features/items/presentation/views/edit_item_screen.dart b/apps/mobile/lib/features/items/presentation/views/edit_item_screen.dart index 4295bc7..c64cb78 100644 --- a/apps/mobile/lib/features/items/presentation/views/edit_item_screen.dart +++ b/apps/mobile/lib/features/items/presentation/views/edit_item_screen.dart @@ -20,6 +20,9 @@ class _EditItemScreenState extends ConsumerState { final _titleController = TextEditingController(); final _barcodeController = TextEditingController(); final _descriptionController = TextEditingController(); + final _purchasePriceController = TextEditingController(); + final _currentValueController = TextEditingController(); + final _purchaseDateController = TextEditingController(); final _notesController = TextEditingController(); final _locationController = TextEditingController(); final _quantityController = TextEditingController(); @@ -29,6 +32,7 @@ class _EditItemScreenState extends ConsumerState { Item? _item; ItemCondition? _selectedCondition; List _tags = const []; + DateTime? _selectedPurchaseDate; @override void initState() { @@ -40,6 +44,9 @@ class _EditItemScreenState extends ConsumerState { _titleController.dispose(); _barcodeController.dispose(); _descriptionController.dispose(); + _purchasePriceController.dispose(); + _currentValueController.dispose(); + _purchaseDateController.dispose(); _notesController.dispose(); _locationController.dispose(); _quantityController.dispose(); @@ -64,6 +71,16 @@ class _EditItemScreenState extends ConsumerState { _titleController.text = item.title; _barcodeController.text = item.barcode ?? ''; _descriptionController.text = item.description ?? ''; + _purchasePriceController.text = item.purchasePrice != null + ? item.purchasePrice!.toStringAsFixed(2) + : ''; + _currentValueController.text = item.currentValue != null + ? item.currentValue!.toStringAsFixed(2) + : ''; + _selectedPurchaseDate = item.purchaseDate; + _purchaseDateController.text = _selectedPurchaseDate != null + ? _formatDate(_selectedPurchaseDate!) + : ''; _notesController.text = item.notes ?? ''; _locationController.text = item.location ?? ''; _quantityController.text = item.quantity.toString(); @@ -141,6 +158,63 @@ class _EditItemScreenState extends ConsumerState { ), const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextFormField( + controller: _purchasePriceController, + decoration: const InputDecoration( + labelText: 'Purchase Price', + prefixText: '\$', + prefixIcon: Icon(Icons.attach_money), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + validator: _validatePriceInput, + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextFormField( + controller: _currentValueController, + decoration: const InputDecoration( + labelText: 'Current Value', + prefixText: '\$', + prefixIcon: Icon(Icons.show_chart), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + validator: _validatePriceInput, + ), + ), + ], + ), + const SizedBox(height: 16), + + TextFormField( + controller: _purchaseDateController, + readOnly: true, + decoration: InputDecoration( + labelText: 'Purchase Date (optional)', + prefixIcon: const Icon(Icons.calendar_today), + suffixIcon: _selectedPurchaseDate == null + ? null + : IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + setState(() { + _selectedPurchaseDate = null; + _purchaseDateController.clear(); + }); + }, + ), + ), + onTap: _pickPurchaseDate, + ), + const SizedBox(height: 16), + // Condition selector DropdownButtonFormField( initialValue: _selectedCondition, @@ -261,6 +335,9 @@ class _EditItemScreenState extends ConsumerState { quantity: int.parse(_quantityController.text), condition: _selectedCondition, tags: _tags, + purchasePrice: _parsePriceInput(_purchasePriceController.text), + currentValue: _parsePriceInput(_currentValueController.text), + purchaseDate: _selectedPurchaseDate, ); await ref.read(updateItemProvider(updated).future); @@ -288,4 +365,45 @@ class _EditItemScreenState extends ConsumerState { } } } + + String? _validatePriceInput(String? value) { + if (value == null || value.trim().isEmpty) { + return null; + } + final parsed = _parsePriceInput(value); + if (parsed == null) { + return 'Invalid price'; + } + if (parsed < 0) { + return 'Must be positive'; + } + return null; + } + + double? _parsePriceInput(String raw) { + final normalized = raw.trim().replaceAll(',', ''); + if (normalized.isEmpty) return null; + return double.tryParse(normalized); + } + + Future _pickPurchaseDate() async { + final initialDate = _selectedPurchaseDate ?? DateTime.now(); + final picked = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: DateTime(1900), + lastDate: DateTime.now().add(const Duration(days: 3650)), + ); + if (picked == null || !mounted) return; + setState(() { + _selectedPurchaseDate = picked; + _purchaseDateController.text = _formatDate(picked); + }); + } + + String _formatDate(DateTime date) { + final month = date.month.toString().padLeft(2, '0'); + final day = date.day.toString().padLeft(2, '0'); + return '${date.year}-$month-$day'; + } } diff --git a/apps/mobile/lib/features/items/presentation/views/item_detail_screen.dart b/apps/mobile/lib/features/items/presentation/views/item_detail_screen.dart index 7732ffe..e51df94 100644 --- a/apps/mobile/lib/features/items/presentation/views/item_detail_screen.dart +++ b/apps/mobile/lib/features/items/presentation/views/item_detail_screen.dart @@ -1,10 +1,13 @@ import 'dart:io'; +import 'dart:math' as math; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:domain/domain.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import '../providers/price_tracking_provider.dart'; import '../view_models/items_view_model.dart'; class ItemDetailScreen extends ConsumerWidget { @@ -27,6 +30,7 @@ class ItemDetailScreen extends ConsumerWidget { } final theme = Theme.of(context); + final priceHistoryAsync = ref.watch(itemPriceHistoryProvider(item.id)); return Scaffold( appBar: AppBar( @@ -181,6 +185,118 @@ class ItemDetailScreen extends ConsumerWidget { const SizedBox(height: 16), ], + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Price Tracking', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + TextButton.icon( + onPressed: () => + _showUpdateCurrentValueDialog( + context, + ref, + item, + ), + icon: const Icon(Icons.show_chart), + label: const Text('Update'), + ), + ], + ), + if (item.currentValue != null) ...[ + const SizedBox(height: 4), + Text( + _formatCurrency(item.currentValue!), + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, + color: theme.colorScheme.primary, + ), + ), + ] else ...[ + const SizedBox(height: 4), + Text( + 'No current value set', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 12), + priceHistoryAsync.when( + data: (history) { + if (history.isEmpty) { + return Text( + 'No historical points yet. Update current value to start tracking.', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ); + } + + final recent = history.reversed + .take(5) + .toList(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _PriceHistoryChart(points: history), + const SizedBox(height: 12), + ...recent.map( + (entry) => Padding( + padding: const EdgeInsets.only( + bottom: 6, + ), + child: Row( + children: [ + Text( + _formatDate(entry.$1), + style: theme.textTheme.bodySmall, + ), + const Spacer(), + Text( + _formatCurrency(entry.$2), + style: theme.textTheme.bodyMedium + ?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ); + }, + loading: () => const SizedBox( + height: 80, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + ), + error: (_, _) => Text( + 'Unable to load price history', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.error, + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + // Details Card Card( child: Padding( @@ -220,6 +336,12 @@ class ItemDetailScreen extends ConsumerWidget { value: '\$${item.purchasePrice!.toStringAsFixed(2)}', ), + if (item.currentValue != null) + _DetailRow( + label: 'Current Value', + value: + '\$${item.currentValue!.toStringAsFixed(2)}', + ), if (item.purchaseDate != null) _DetailRow( label: 'Purchase Date', @@ -276,6 +398,178 @@ class ItemDetailScreen extends ConsumerWidget { String _formatDate(DateTime date) { return '${date.day}/${date.month}/${date.year}'; } + + String _formatCurrency(double value) { + return '\$${value.toStringAsFixed(2)}'; + } + + Future _showUpdateCurrentValueDialog( + BuildContext context, + WidgetRef ref, + Item item, + ) async { + var draftValue = item.currentValue?.toStringAsFixed(2) ?? ''; + + final value = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Update Current Value'), + content: TextFormField( + initialValue: draftValue, + autofocus: true, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + labelText: 'Current value', + prefixText: '\$', + hintText: '0.00', + ), + onChanged: (value) { + draftValue = value; + }, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () { + final parsed = double.tryParse(draftValue.trim()); + if (parsed == null || parsed < 0) return; + Navigator.pop(context, parsed); + }, + child: const Text('Save'), + ), + ], + ), + ); + + if (value == null || !context.mounted) return; + + try { + await ref.read( + updateItemProvider(item.copyWith(currentValue: value)).future, + ); + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Current value updated'))); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to update value: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } +} + +class _PriceHistoryChart extends StatelessWidget { + final List<(DateTime, double)> points; + + const _PriceHistoryChart({required this.points}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + height: 110, + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(12), + ), + child: CustomPaint( + painter: _PriceHistoryPainter( + points: points, + lineColor: theme.colorScheme.primary, + pointColor: theme.colorScheme.primaryContainer, + ), + ), + ); + } +} + +class _PriceHistoryPainter extends CustomPainter { + final List<(DateTime, double)> points; + final Color lineColor; + final Color pointColor; + + _PriceHistoryPainter({ + required this.points, + required this.lineColor, + required this.pointColor, + }); + + @override + void paint(Canvas canvas, Size size) { + if (points.isEmpty) return; + + final minY = points.map((e) => e.$2).reduce(math.min); + final maxY = points.map((e) => e.$2).reduce(math.max); + final yRange = (maxY - minY).abs() < 0.001 ? 1.0 : maxY - minY; + final xStep = points.length == 1 + ? size.width + : size.width / (points.length - 1); + + final linePaint = Paint() + ..color = lineColor + ..strokeWidth = 2 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + final fillPaint = Paint() + ..shader = LinearGradient( + colors: [lineColor.withValues(alpha: 0.18), Colors.transparent], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); + + final linePath = Path(); + final fillPath = Path(); + + for (var i = 0; i < points.length; i++) { + final x = xStep * i; + final normalizedY = (points[i].$2 - minY) / yRange; + final y = size.height - (normalizedY * (size.height - 10)) - 5; + + if (i == 0) { + linePath.moveTo(x, y); + fillPath.moveTo(x, size.height); + fillPath.lineTo(x, y); + } else { + linePath.lineTo(x, y); + fillPath.lineTo(x, y); + } + } + + final lastX = xStep * (points.length - 1); + fillPath.lineTo(lastX, size.height); + fillPath.close(); + + canvas.drawPath(fillPath, fillPaint); + canvas.drawPath(linePath, linePaint); + + final pointPaint = Paint()..color = pointColor; + for (var i = 0; i < points.length; i++) { + final x = xStep * i; + final normalizedY = (points[i].$2 - minY) / yRange; + final y = size.height - (normalizedY * (size.height - 10)) - 5; + canvas.drawCircle(Offset(x, y), 3, pointPaint); + } + } + + @override + bool shouldRepaint(covariant _PriceHistoryPainter oldDelegate) { + return oldDelegate.points != points || + oldDelegate.lineColor != lineColor || + oldDelegate.pointColor != pointColor; + } } class _DetailRow extends StatelessWidget { diff --git a/apps/mobile/lib/features/statistics/presentation/view_models/statistics_view_model.dart b/apps/mobile/lib/features/statistics/presentation/view_models/statistics_view_model.dart index 852548d..60ee8d6 100644 --- a/apps/mobile/lib/features/statistics/presentation/view_models/statistics_view_model.dart +++ b/apps/mobile/lib/features/statistics/presentation/view_models/statistics_view_model.dart @@ -66,8 +66,9 @@ class StatisticsViewModel extends _$StatisticsViewModel { if (item.isFavorite) { favoriteCount++; } - if (item.purchasePrice != null) { - totalValue += item.purchasePrice! * item.quantity; + final effectiveValue = item.currentValue ?? item.purchasePrice; + if (effectiveValue != null) { + totalValue += effectiveValue * item.quantity; } } diff --git a/apps/mobile/lib/features/statistics/presentation/views/statistics_screen.dart b/apps/mobile/lib/features/statistics/presentation/views/statistics_screen.dart index edc6f16..afb7a16 100644 --- a/apps/mobile/lib/features/statistics/presentation/views/statistics_screen.dart +++ b/apps/mobile/lib/features/statistics/presentation/views/statistics_screen.dart @@ -70,7 +70,7 @@ class StatisticsScreen extends ConsumerWidget { Expanded( child: StatCard( title: 'Total Value', - value: '\$${stats.totalValue.toStringAsFixed(0)}', + value: '\$${stats.totalValue.toStringAsFixed(2)}', icon: Icons.attach_money, color: Colors.orange, ), diff --git a/packages/core/data/lib/src/repositories/item_repository_impl.dart b/packages/core/data/lib/src/repositories/item_repository_impl.dart index 8219628..e9b649e 100644 --- a/packages/core/data/lib/src/repositories/item_repository_impl.dart +++ b/packages/core/data/lib/src/repositories/item_repository_impl.dart @@ -176,6 +176,13 @@ class ItemRepositoryImpl implements ItemRepository { }); } + @override + Stream> watchPriceHistory(String itemId) { + return _dao + .watchPriceHistoryForItem(itemId) + .map((rows) => rows.map((row) => (row.recordedAt, row.value)).toList()); + } + @override Stream> watchTagsWithUsage() { return _dao.watchTagsWithUsage(); diff --git a/packages/core/domain/lib/src/repositories/item_repository.dart b/packages/core/domain/lib/src/repositories/item_repository.dart index 9034086..44c7d14 100644 --- a/packages/core/domain/lib/src/repositories/item_repository.dart +++ b/packages/core/domain/lib/src/repositories/item_repository.dart @@ -19,6 +19,7 @@ abstract class ItemRepository { Stream> watchAllFavoriteItems(); Stream> watchAllWishlistItems(); Stream> watchItemsByTag(String tagName); + Stream> watchPriceHistory(String itemId); Stream> watchTagsWithUsage(); Future>> searchItems({ diff --git a/packages/integrations/database/lib/src/app_database.dart b/packages/integrations/database/lib/src/app_database.dart index 7dce502..21f1eaa 100644 --- a/packages/integrations/database/lib/src/app_database.dart +++ b/packages/integrations/database/lib/src/app_database.dart @@ -7,14 +7,14 @@ import 'package:path_provider/path_provider.dart'; part 'app_database.g.dart'; @DriftDatabase( - tables: [Collections, Items, Tags, ItemTags], + tables: [Collections, Items, Tags, ItemTags, ItemPriceHistory], daos: [CollectionDao, ItemDao], ) class AppDatabase extends _$AppDatabase { AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); @override - int get schemaVersion => 4; + int get schemaVersion => 5; @override MigrationStrategy get migration { @@ -27,6 +27,10 @@ class AppDatabase extends _$AppDatabase { 'CREATE INDEX idx_collections_name ON collections(name);', ); await customStatement('CREATE INDEX idx_items_name ON items(title);'); + await customStatement( + 'CREATE INDEX idx_item_price_history_item_time ' + 'ON item_price_history(item_id, recorded_at DESC);', + ); }, onUpgrade: (Migrator m, int from, int to) async { if (from < 2) { @@ -41,6 +45,14 @@ class AppDatabase extends _$AppDatabase { await m.createTable(tags); await m.createTable(itemTags); } + + if (from < 5) { + await m.createTable(itemPriceHistory); + await customStatement( + 'CREATE INDEX idx_item_price_history_item_time ' + 'ON item_price_history(item_id, recorded_at DESC);', + ); + } }, beforeOpen: (details) async { await customStatement('PRAGMA foreign_keys = ON'); diff --git a/packages/integrations/database/lib/src/daos/item_dao.dart b/packages/integrations/database/lib/src/daos/item_dao.dart index dc16112..11aca3e 100644 --- a/packages/integrations/database/lib/src/daos/item_dao.dart +++ b/packages/integrations/database/lib/src/daos/item_dao.dart @@ -4,7 +4,7 @@ import 'package:drift/drift.dart'; part 'item_dao.g.dart'; -@DriftAccessor(tables: [Items, Collections, ItemTags, Tags]) +@DriftAccessor(tables: [Items, Collections, ItemTags, Tags, ItemPriceHistory]) class ItemDao extends DatabaseAccessor with _$ItemDaoMixin { ItemDao(super.db); @@ -150,6 +150,28 @@ class ItemDao extends DatabaseAccessor with _$ItemDaoMixin { .watch(); } + // Get historical price points for an item (oldest first) + Future> getPriceHistoryForItem(String itemId) { + return (select(itemPriceHistory) + ..where((tbl) => tbl.itemId.equals(itemId)) + ..orderBy([ + (tbl) => OrderingTerm.asc(tbl.recordedAt), + (tbl) => OrderingTerm.asc(tbl.id), + ])) + .get(); + } + + // Watch historical price points for an item (oldest first) + Stream> watchPriceHistoryForItem(String itemId) { + return (select(itemPriceHistory) + ..where((tbl) => tbl.itemId.equals(itemId)) + ..orderBy([ + (tbl) => OrderingTerm.asc(tbl.recordedAt), + (tbl) => OrderingTerm.asc(tbl.id), + ])) + .watch(); + } + // Get items that contain a specific tag Future> getItemsByTag(String tagName) async { final query = @@ -360,6 +382,36 @@ class ItemDao extends DatabaseAccessor with _$ItemDaoMixin { await _updateItemTags(item.id.value, tags); } + final purchasePrice = item.purchasePrice.present + ? item.purchasePrice.value + : null; + final currentValue = item.currentValue.present + ? item.currentValue.value + : null; + final purchaseDate = item.purchaseDate.present + ? item.purchaseDate.value + : null; + final createdAt = item.createdAt.value; + final updatedAt = item.updatedAt.value; + + if (purchasePrice != null) { + await _recordPricePoint( + itemId: item.id.value, + value: purchasePrice, + recordedAt: purchaseDate ?? createdAt, + source: 'purchase', + ); + } + + if (currentValue != null && currentValue != purchasePrice) { + await _recordPricePoint( + itemId: item.id.value, + value: currentValue, + recordedAt: updatedAt, + source: 'current', + ); + } + final collection = await (select( collections, )..where((tbl) => tbl.id.equals(collectionId))).getSingleOrNull(); @@ -382,6 +434,9 @@ class ItemDao extends DatabaseAccessor with _$ItemDaoMixin { // Update item with tags Future updateItem(ItemsCompanion item, {List? tags}) { return transaction(() async { + final existingItem = await getItemById(item.id.value); + if (existingItem == null) return 0; + final rowsAffected = await (update( items, )..where((tbl) => tbl.id.equals(item.id.value))).write(item); @@ -390,10 +445,66 @@ class ItemDao extends DatabaseAccessor with _$ItemDaoMixin { await _updateItemTags(item.id.value, tags); } + if (rowsAffected > 0) { + final previousPurchasePrice = existingItem.purchasePrice; + final nextPurchasePrice = item.purchasePrice.present + ? item.purchasePrice.value + : previousPurchasePrice; + + if (nextPurchasePrice != null && + nextPurchasePrice != previousPurchasePrice) { + final recordedAt = item.purchaseDate.present + ? item.purchaseDate.value + : existingItem.purchaseDate; + await _recordPricePoint( + itemId: item.id.value, + value: nextPurchasePrice, + recordedAt: recordedAt ?? DateTime.now(), + source: 'purchase', + ); + } + + final previousCurrentValue = existingItem.currentValue; + final nextCurrentValue = item.currentValue.present + ? item.currentValue.value + : previousCurrentValue; + if (nextCurrentValue != null && + nextCurrentValue != previousCurrentValue) { + final recordedAt = item.updatedAt.present + ? item.updatedAt.value + : DateTime.now(); + await _recordPricePoint( + itemId: item.id.value, + value: nextCurrentValue, + recordedAt: recordedAt, + source: 'current', + ); + } + } + return rowsAffected; }); } + Future _recordPricePoint({ + required String itemId, + required double value, + required DateTime recordedAt, + required String source, + }) async { + final id = + '${itemId}_${recordedAt.microsecondsSinceEpoch}_${DateTime.now().microsecondsSinceEpoch}'; + await into(itemPriceHistory).insert( + ItemPriceHistoryCompanion.insert( + id: id, + itemId: itemId, + value: value, + recordedAt: recordedAt, + source: Value(source), + ), + ); + } + Future _updateItemTags(String itemId, List tagNames) async { // 1. Get or create tags final tagIds = []; diff --git a/packages/integrations/database/lib/src/tables/item_price_history_table.dart b/packages/integrations/database/lib/src/tables/item_price_history_table.dart new file mode 100644 index 0000000..b2f7771 --- /dev/null +++ b/packages/integrations/database/lib/src/tables/item_price_history_table.dart @@ -0,0 +1,16 @@ +import 'package:drift/drift.dart'; + +import 'items_table.dart'; + +@DataClassName('ItemPriceHistoryData') +class ItemPriceHistory extends Table { + TextColumn get id => text()(); + TextColumn get itemId => + text().references(Items, #id, onDelete: KeyAction.cascade)(); + RealColumn get value => real()(); + DateTimeColumn get recordedAt => dateTime()(); + TextColumn get source => text().withDefault(const Constant('manual'))(); + + @override + Set get primaryKey => {id}; +} diff --git a/packages/integrations/database/lib/src/tables/tables.dart b/packages/integrations/database/lib/src/tables/tables.dart index 54bd61d..669dff1 100644 --- a/packages/integrations/database/lib/src/tables/tables.dart +++ b/packages/integrations/database/lib/src/tables/tables.dart @@ -2,3 +2,4 @@ export 'collections_table.dart'; export 'items_table.dart'; export 'tags_table.dart'; export 'item_tags_table.dart'; +export 'item_price_history_table.dart'; diff --git a/packages/integrations/database/test/database_test.dart b/packages/integrations/database/test/database_test.dart index 552d36f..c00ae98 100644 --- a/packages/integrations/database/test/database_test.dart +++ b/packages/integrations/database/test/database_test.dart @@ -560,5 +560,77 @@ void main() { ); expect(filtered.map((item) => item.title), isNot(contains('No Match'))); }); + + test('insert item records initial price history points', () async { + final now = DateTime.now(); + await db.itemDao.insertItem( + ItemsCompanion.insert( + id: 'item-price-1', + collectionId: collectionId, + title: 'Price Seed', + purchasePrice: const Value(10.0), + purchaseDate: Value(now.subtract(const Duration(days: 1))), + currentValue: const Value(12.5), + createdAt: now, + updatedAt: now, + ), + ); + + final history = await db.itemDao.getPriceHistoryForItem('item-price-1'); + expect(history.length, 2); + expect(history.map((entry) => entry.value), containsAll([10.0, 12.5])); + }); + + test('updating current value appends price history point', () async { + final now = DateTime.now(); + await db.itemDao.insertItem( + ItemsCompanion.insert( + id: 'item-price-2', + collectionId: collectionId, + title: 'Track Current', + currentValue: const Value(20.0), + createdAt: now, + updatedAt: now, + ), + ); + + await db.itemDao.updateItem( + ItemsCompanion( + id: const Value('item-price-2'), + currentValue: const Value(24.0), + updatedAt: Value(now.add(const Duration(minutes: 1))), + ), + ); + + final history = await db.itemDao.getPriceHistoryForItem('item-price-2'); + expect(history.length, 2); + expect(history.last.value, 24.0); + }); + + test('updating non-price fields does not add history point', () async { + final now = DateTime.now(); + await db.itemDao.insertItem( + ItemsCompanion.insert( + id: 'item-price-3', + collectionId: collectionId, + title: 'No Price Change', + currentValue: const Value(30.0), + createdAt: now, + updatedAt: now, + ), + ); + + await db.itemDao.updateItem( + ItemsCompanion( + id: const Value('item-price-3'), + title: const Value('Renamed'), + updatedAt: Value(now.add(const Duration(minutes: 2))), + ), + ); + + final history = await db.itemDao.getPriceHistoryForItem('item-price-3'); + expect(history.length, 1); + expect(history.first.value, 30.0); + }); }); } From 4a8b7cc3caef2f105419cf8ee289cabdf533c87f Mon Sep 17 00:00:00 2001 From: Kyaw Zayar Tun Date: Fri, 20 Feb 2026 18:29:19 +0630 Subject: [PATCH 4/4] chore: Update Flutter version from 3.38.x to 3.41.x across all GitHub Actions workflows. --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/pr-checks.yaml | 2 +- .github/workflows/release.yaml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5377c46..495f65c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,7 +34,7 @@ jobs: - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.38.x' + flutter-version: '3.41.x' channel: 'stable' cache: true @@ -76,7 +76,7 @@ jobs: - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.38.x' + flutter-version: '3.41.x' channel: 'stable' cache: true diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index 9767cf7..6ecfa8f 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -32,7 +32,7 @@ jobs: - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.38.x' + flutter-version: '3.41.x' channel: 'stable' - name: 📦 Get dependencies diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2aafd32..f83b388 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -31,7 +31,7 @@ jobs: - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.38.x' + flutter-version: '3.41.x' channel: 'stable' cache: true @@ -123,7 +123,7 @@ jobs: # - name: �🐦 Setup Flutter # uses: subosito/flutter-action@v2 # with: - # flutter-version: '3.38.x' + # flutter-version: '3.41.x' # channel: 'stable' # cache: true