From f69332edc22f99337caf3857919357e7587bbad6 Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Fri, 5 Jun 2026 17:32:01 +0300 Subject: [PATCH 01/15] fix: resolve all analyzer errors, warnings, and infos --- all_lint_rules.yaml | 237 ++++++++++++++++++ analysis_options.yaml | 9 +- analysis_options_without_plugins.yaml | 115 +++++++++ lib/config/fade_extension.dart | 12 +- lib/config/router_config.dart | 32 ++- lib/config/router_config.g.dart | 2 +- lib/layout/root_layout.dart | 12 +- lib/main.dart | 24 +- lib/models/settings_view_model.dart | 14 +- lib/models/sub_slice.dart | 8 +- lib/providers/brands_provider.dart | 14 +- lib/providers/brands_provider.g.dart | 6 +- lib/providers/settings_controller.dart | 18 +- lib/providers/settings_controller.g.dart | 6 +- lib/providers/subs_controller.dart | 45 ++-- lib/providers/subs_controller.g.dart | 6 +- lib/providers/theme_provider.dart | 5 +- lib/screens/analytics_screen.dart | 14 +- lib/screens/app_startup.dart | 6 +- lib/screens/calendar_screen.dart | 24 +- lib/screens/color_scheme_page.dart | 12 +- lib/screens/home_screen.dart | 87 ++++--- lib/screens/intro_page.dart | 3 +- lib/screens/onboarding_screen.dart | 20 +- lib/screens/popular_subs_page.dart | 24 +- lib/screens/profile_page.dart | 7 +- lib/screens/settings_screen.dart | 74 +++--- lib/screens/sub_detail_screen.dart | 23 +- lib/screens/user_name_page.dart | 9 +- lib/utils/brand_utils.dart | 2 +- lib/utils/color_utils.dart | 2 +- lib/utils/notification_service.dart | 24 +- lib/widgets/action_text_form_field.dart | 8 +- lib/widgets/add_subs_dialog.dart | 39 ++- lib/widgets/bottom_controls.dart | 2 +- lib/widgets/brand_logo.dart | 4 +- lib/widgets/color_scheme_picker.dart | 2 +- lib/widgets/edit_subs_dialog.dart | 34 ++- lib/widgets/edit_user_profile.dart | 18 +- lib/widgets/floating_sub_card.dart | 1 - lib/widgets/glass_nav_bar.dart | 3 +- lib/widgets/menu_bar.dart | 35 +-- lib/widgets/pie_chart.dart | 16 +- lib/widgets/sub_zilla_app_bar.dart | 9 +- pubspec.lock | 238 +++++++------------ pubspec.yaml | 66 +++-- test/models/sub_slice_test.dart | 1 - test/providers/subs_controller_test.dart | 13 +- test/screens/calendar_screen_test.dart | 7 +- test/widget_test.dart | 6 +- test/widgets/add_subs_dialog_test.dart | 20 +- test/widgets/color_scheme_settings_test.dart | 2 +- test/widgets/intro_page_test.dart | 2 +- 53 files changed, 839 insertions(+), 583 deletions(-) create mode 100644 all_lint_rules.yaml create mode 100644 analysis_options_without_plugins.yaml diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml new file mode 100644 index 0000000..0ed53c3 --- /dev/null +++ b/all_lint_rules.yaml @@ -0,0 +1,237 @@ +linter: + rules: + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + - always_specify_types + - always_use_package_imports + - annotate_overrides + - annotate_redeclares + - avoid_annotating_with_dynamic + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_dynamic_calls + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_futureor_void + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - cast_nullable_to_non_nullable + - close_sinks + - collection_methods_unrelated_type + - combinators_ordering + - comment_references + - conditional_uri_does_not_exist + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - deprecated_consistency + - deprecated_member_use_from_same_package + - diagnostic_describe_all_properties + - directives_ordering + - discarded_futures + - do_not_use_environment + - document_ignores + - empty_catches + - empty_constructor_bodies + - empty_statements + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - implicit_call_tearoffs + - implicit_reopen + - invalid_case_patterns + - invalid_runtime_check_with_js_interop_types + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_annotations + - library_names + - library_prefixes + - library_private_types_in_public_api + - lines_longer_than_80_chars + - literal_only_boolean_expressions + - matching_super_parameters + - missing_code_block_language_in_doc_comment + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons + - no_logic_in_create_state + - no_runtimeType_toString + - no_self_assignments + - no_wildcard_variable_uses + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - omit_obvious_local_variable_types + - omit_obvious_property_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_double_quotes + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_final_parameters + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_method_calls + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - remove_deprecations_in_breaking_versions + - require_trailing_commas + - secure_pubspec_urls + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - specify_nonobvious_local_variable_types + - specify_nonobvious_property_types + - strict_top_level_inference + - switch_on_type + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - type_init_formals + - type_literal_in_constant_pattern + - unawaited_futures + - unintended_html_in_doc_comment + - unnecessary_async + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_breaks + - unnecessary_const + - unnecessary_constructor_name + - unnecessary_final + - unnecessary_getters_setters + - unnecessary_ignore + - unnecessary_lambdas + - unnecessary_late + - unnecessary_library_directive + - unnecessary_library_name + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + - unnecessary_unawaited + - unnecessary_underscores + - unreachable_from_main + - unrelated_type_equality_checks + - unsafe_variance + - use_build_context_synchronously + - use_colored_box + - use_decorated_box + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_named_constants + - use_null_aware_elements + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_string_in_part_of_directives + - use_super_parameters + - use_test_throws_matchers + - use_to_and_as_if_applicable + - use_truncating_division + - valid_regexps + - void_checks \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 0682a3e..7a95142 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,9 +7,16 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +include: analysis_options_without_plugins.yaml + +plugins: + #! DONT REMOVE: its false positive and riverpod lints dont work without it + riverpod_lint: ^3.1.4-dev.3 + analyzer: + errors: + public_member_api_docs: ignore exclude: - '**/*.g.dart' # Exclude generated files - '**/*.freezed.dart' # Exclude freezed files diff --git a/analysis_options_without_plugins.yaml b/analysis_options_without_plugins.yaml new file mode 100644 index 0000000..304950d --- /dev/null +++ b/analysis_options_without_plugins.yaml @@ -0,0 +1,115 @@ +include: all_lint_rules.yaml +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + errors: + # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. + # We explicitly enabled even conflicting rules and are fixing the conflict + # in this file + included_file_warning: ignore + # false positive when using Freezed + invalid_annotation_target: ignore + # I prefer specifying a parameter on a widget even if they are unused (such as Key) + # for the sake of consistency. + unused_element_parameter: false + unrecognized_error_code: ignore + # Remove once we require analyzer 8.0.0 + # We currently support both 7.0 and 8.0, so we can't migrate yet. + deprecated_member_use: ignore + # Analyzer API is experimental, but kind of necessary for analyzer plugins + experimental_member_use: ignore + +linter: + rules: + plugins_in_inner_options: false + # No good way to "fix" the diagnostic. Cf https://github.com/dart-lang/sdk/issues/61976 + unsafe_variance: false + + # Conflicts with having to specify types on public properties + omit_obvious_property_types: false + + # Using it on purpose for signaling that an API can be both async and sync + avoid_futureor_void: false + + # I hate it + specify_nonobvious_property_types: false + specify_nonobvious_local_variable_types: false + + # Clashes with newer dartfmt + require_trailing_commas: false + + # Maybe later. Too many to fix right now. + document_ignores: false + + # Not an issue if using dartfmt + no_adjacent_strings_in_list: false + + # False positive for custom enum-like classes (such as Flutter's "Colors") + avoid_classes_with_only_static_members: false + + # False positive when the future is returned by the function + discarded_futures: false + + # Low value and lacks a quick fix + combinators_ordering: false + + # Conflicts with unused variables + no_leading_underscores_for_local_identifiers: false + + # false positive + one_member_abstracts: false + + # too verbose + prefer_final_parameters: false + + # Too verbose with little value, and this is taken care of by the Flutter devtool anyway. + diagnostic_describe_all_properties: false + + # Personal preference. I prefer "if (bool) return;" over having it in multiple lines + always_put_control_body_on_new_line: false + + # Personal preference. I don't find it more readable + cascade_invocations: false + + # Conflicts with `prefer_single_quotes` + # Single quotes are easier to type and don't compromise on readability. + prefer_double_quotes: false + + # Conflicts with `omit_local_variable_types` and other rules. + # As per Dart guidelines, we want to avoid unnecessary types to make the code + # more readable. + # See https://dart.dev/guides/language/effective-dart/design#avoid-type-annotating-initialized-local-variables + always_specify_types: false + + # Incompatible with `prefer_final_locals` + # Having immutable local variables makes larger functions more predictable + # so we will use `prefer_final_locals` instead. + unnecessary_final: false + + # Not quite suitable for Flutter, which may have a `build` method with a single + # return, but that return is still complex enough that a "body" is worth it. + prefer_expression_function_bodies: false + + # Conflicts with the convention used by flutter, which puts `Key key` + # and `@required Widget child` last. + always_put_required_named_parameters_first: false + + # This project doesn't use Flutter-style todos + flutter_style_todos: false + + # There are situations where we voluntarily want to catch everything, + # especially as a library. + avoid_catches_without_on_clauses: false + + # Boring as it sometimes force a line of 81 characters to be split in two. + # As long as we try to respect that 80 characters limit, going slightly + # above is fine. + lines_longer_than_80_chars: false + + # Conflicts with disabling `implicit-dynamic` + avoid_annotating_with_dynamic: false + + # conflicts with `prefer_relative_imports` + always_use_package_imports: false diff --git a/lib/config/fade_extension.dart b/lib/config/fade_extension.dart index 87d7299..b96dba7 100644 --- a/lib/config/fade_extension.dart +++ b/lib/config/fade_extension.dart @@ -1,4 +1,4 @@ -// ignore_for_file: strict_raw_type, always_specify_types +// ignore_for_file: strict_raw_type import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -8,10 +8,10 @@ class FadeTransitionPage extends CustomTransitionPage { : super( transitionsBuilder: ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, + context, + animation, + secondaryAnimation, + child, ) { return FadeTransition(opacity: animation, child: child); }, @@ -24,7 +24,7 @@ extension GoRouteExtension on GoRoute { GoRoute fade() { return GoRoute( path: path, - pageBuilder: (BuildContext context, GoRouterState state) { + pageBuilder: (context, state) { return FadeTransitionPage( key: ValueKey(path), child: builder!(context, state), diff --git a/lib/config/router_config.dart b/lib/config/router_config.dart index d0a9fb8..0c77ce8 100644 --- a/lib/config/router_config.dart +++ b/lib/config/router_config.dart @@ -1,17 +1,15 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:subs_tracker/layout/root_layout.dart'; -import 'package:subs_tracker/models/sub_slice.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/screens/analytics_screen.dart'; -import 'package:subs_tracker/screens/app_startup.dart'; -import 'package:subs_tracker/screens/calendar_screen.dart'; -import 'package:subs_tracker/screens/home_screen.dart'; -import 'package:subs_tracker/screens/onboarding_screen.dart'; -import 'package:subs_tracker/screens/settings_screen.dart'; -import 'package:subs_tracker/screens/sub_detail_screen.dart'; +import '../layout/root_layout.dart'; +import '../models/sub_slice.dart'; +import '../providers/settings_controller.dart'; +import '../screens/analytics_screen.dart'; +import '../screens/app_startup.dart'; +import '../screens/calendar_screen.dart'; +import '../screens/home_screen.dart'; +import '../screens/onboarding_screen.dart'; +import '../screens/settings_screen.dart'; +import '../screens/sub_detail_screen.dart'; part 'router_config.g.dart'; @@ -44,7 +42,7 @@ GoRouter goRouter(Ref ref) { routes: [ GoRoute( path: Routes.home.route, - builder: (BuildContext context, GoRouterState state) { + builder: (context, state) { return const AppStartup(child: HomeScreen()); }, redirect: (context, state) async { @@ -59,25 +57,25 @@ GoRouter goRouter(Ref ref) { ), GoRoute( path: Routes.intro.route, - builder: (BuildContext context, GoRouterState state) { + builder: (context, state) { return const OnboardingScreen(); }, ), GoRoute( path: Routes.calendar.route, - builder: (BuildContext context, GoRouterState state) { + builder: (context, state) { return const CalendarScreen(); }, ), GoRoute( path: Routes.analytics.route, - builder: (BuildContext context, GoRouterState state) { + builder: (context, state) { return const AnalyticsScreen(); }, ), GoRoute( path: Routes.settings.route, - builder: (BuildContext context, GoRouterState state) => + builder: (context, state) => const SettingsScreen(), ), ], diff --git a/lib/config/router_config.g.dart b/lib/config/router_config.g.dart index 6ea989c..a078218 100644 --- a/lib/config/router_config.g.dart +++ b/lib/config/router_config.g.dart @@ -48,4 +48,4 @@ final class GoRouterProvider } } -String _$goRouterHash() => r'28c139ada2c7aa11daa9a369d15ce92c81d83657'; +String _$goRouterHash() => r'6cae0d9f5642c9bf57e5ecaab026eccfa67411e5'; diff --git a/lib/layout/root_layout.dart b/lib/layout/root_layout.dart index 224545e..5307388 100644 --- a/lib/layout/root_layout.dart +++ b/lib/layout/root_layout.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/config/router_config.dart'; -import 'package:subs_tracker/widgets/glass_nav_bar.dart'; -import 'package:subs_tracker/widgets/menu_bar.dart'; +import '../config/router_config.dart'; +import '../widgets/glass_nav_bar.dart'; +import '../widgets/menu_bar.dart'; final rootScaffoldKey = GlobalKey(); @@ -19,7 +19,7 @@ class RootLayout extends ConsumerWidget { final currentPath = router.state.path; // Determine current tab index - int selectedIndex = 0; + var selectedIndex = 0; if (currentPath == Routes.calendar.route) { selectedIndex = 1; } else if (currentPath == Routes.analytics.route) { @@ -39,16 +39,12 @@ class RootLayout extends ConsumerWidget { switch (index) { case 0: context.go(Routes.home.route); - break; case 1: context.go(Routes.calendar.route); - break; case 2: context.go(Routes.analytics.route); - break; case 3: context.go(Routes.settings.route); - break; } }, ), diff --git a/lib/main.dart b/lib/main.dart index 7fccb6b..46345b7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:subs_tracker/config/router_config.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/providers/theme_provider.dart'; -import 'package:subs_tracker/utils/notification_service.dart'; + +import 'config/router_config.dart'; +import 'providers/settings_controller.dart'; +import 'providers/theme_provider.dart'; +import 'utils/notification_service.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -12,11 +13,13 @@ Future main() async { await LocalNotificationService.instance.init(); runApp( - EasyLocalization( - supportedLocales: const [Locale('en'), Locale('tr')], - path: 'assets/translations', - fallbackLocale: const Locale('en'), - child: const ProviderScope(child: MyApp()), + ProviderScope( + child: EasyLocalization( + supportedLocales: const [Locale('en'), Locale('tr')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + child: const MyApp(), + ), ), ); } @@ -26,7 +29,8 @@ class MyApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final themeMode = ref.watch( + final themeMode = + ref.watch( settingsControllerProvider.select((value) => value.value?.theme), ) ?? ThemeMode.system; diff --git a/lib/models/settings_view_model.dart b/lib/models/settings_view_model.dart index 6691fc7..8e09ec7 100644 --- a/lib/models/settings_view_model.dart +++ b/lib/models/settings_view_model.dart @@ -9,17 +9,17 @@ part 'settings_view_model.g.dart'; enum Currency { try_('₺', 'Turkish Lira'), - usd('\$', 'US Dollar'), + usd(r'$', 'US Dollar'), eur('€', 'Euro'), gbp('£', 'British Pound'), jpy('¥', 'Japanese Yen'), - cad('C\$', 'Canadian Dollar'), - aud('A\$', 'Australian Dollar'); + cad(r'C$', 'Canadian Dollar'), + aud(r'A$', 'Australian Dollar'); + + const Currency(this.symbol, this.label); final String symbol; final String label; - - const Currency(this.symbol, this.label); } @freezed @@ -40,11 +40,11 @@ abstract class SettingsViewModel with _$SettingsViewModel { _$SettingsViewModelFromJson(json); } -class Uint8ListConverter implements JsonConverter { +class Uint8ListConverter implements JsonConverter?> { const Uint8ListConverter(); @override - Uint8List? fromJson(List? json) { + Uint8List? fromJson(List? json) { if (json == null) return null; return Uint8List.fromList(List.from(json)); } diff --git a/lib/models/sub_slice.dart b/lib/models/sub_slice.dart index 3d44b55..7b64532 100644 --- a/lib/models/sub_slice.dart +++ b/lib/models/sub_slice.dart @@ -1,5 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:subs_tracker/models/brand.dart'; +import 'brand.dart'; part 'sub_slice.freezed.dart'; part 'sub_slice.g.dart'; @@ -23,6 +23,9 @@ abstract class SubSlice with _$SubSlice { String? category, }) = _SubSlice; + factory SubSlice.fromJson(Map json) => + _$SubSliceFromJson(json); + const SubSlice._(); double get monthlyAmount { @@ -37,7 +40,4 @@ abstract class SubSlice with _$SubSlice { return amount / 12; } } - - factory SubSlice.fromJson(Map json) => - _$SubSliceFromJson(json); } diff --git a/lib/providers/brands_provider.dart b/lib/providers/brands_provider.dart index ac12d0e..bbe3e60 100644 --- a/lib/providers/brands_provider.dart +++ b/lib/providers/brands_provider.dart @@ -7,18 +7,16 @@ import 'package:riverpod_annotation/experimental/persist.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_sqflite/riverpod_sqflite.dart'; import 'package:sqflite/sqflite.dart'; -import 'package:subs_tracker/models/brand.dart'; +import '../models/brand.dart'; part 'brands_provider.g.dart'; -@riverpod +@Riverpod(keepAlive: true) Future brandsStorage(Ref ref) async { - JsonSqFliteStorage storage = await JsonSqFliteStorage.open( + final storage = await JsonSqFliteStorage.open( join(await getDatabasesPath(), 'brands.db'), ); - ref - ..onDispose(storage.close) - ..keepAlive(); + ref.onDispose(storage.close); return storage; } @@ -29,9 +27,9 @@ class Brands extends _$Brands { FutureOr> build() async { await persist( ref.watch(brandsStorageProvider.future), - options: StorageOptions( + options: const StorageOptions( cacheTime: StorageCacheTime.unsafe_forever, - destroyKey: "v3", + destroyKey: 'v3', ), ).future; if (state.value != null) { diff --git a/lib/providers/brands_provider.g.dart b/lib/providers/brands_provider.g.dart index 783a892..6ccace5 100644 --- a/lib/providers/brands_provider.g.dart +++ b/lib/providers/brands_provider.g.dart @@ -28,7 +28,7 @@ final class BrandsStorageProvider argument: null, retry: null, name: r'brandsStorageProvider', - isAutoDispose: true, + isAutoDispose: false, dependencies: null, $allTransitiveDependencies: null, ); @@ -48,7 +48,7 @@ final class BrandsStorageProvider } } -String _$brandsStorageHash() => r'50746d0570d0e548601bf78a377e429911d66e17'; +String _$brandsStorageHash() => r'e6c82756df3f3111599fba738a880ba51c90b461'; @ProviderFor(Brands) @JsonPersist() @@ -75,7 +75,7 @@ final class BrandsProvider extends $AsyncNotifierProvider> { Brands create() => Brands(); } -String _$brandsHash() => r'97f03c4666ac0e5b65ce616e7e19160591ef1157'; +String _$brandsHash() => r'7e2ef10206303a9c80e491878ab37959549b938e'; @JsonPersist() abstract class _$BrandsBase extends $AsyncNotifier> { diff --git a/lib/providers/settings_controller.dart b/lib/providers/settings_controller.dart index 52f2e98..40e598f 100644 --- a/lib/providers/settings_controller.dart +++ b/lib/providers/settings_controller.dart @@ -6,18 +6,16 @@ import 'package:riverpod_annotation/experimental/persist.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_sqflite/riverpod_sqflite.dart'; import 'package:sqflite/sqflite.dart'; -import 'package:subs_tracker/models/settings_view_model.dart'; +import '../models/settings_view_model.dart'; part 'settings_controller.g.dart'; -@riverpod +@Riverpod(keepAlive: true) Future settingsStorage(Ref ref) async { - JsonSqFliteStorage storage = await JsonSqFliteStorage.open( + final storage = await JsonSqFliteStorage.open( join(await getDatabasesPath(), 'settings.db'), ); - ref - ..onDispose(storage.close) - ..keepAlive(); + ref.onDispose(storage.close); return storage; } @@ -28,13 +26,13 @@ class SettingsController extends _$SettingsController { FutureOr build() async { await persist( ref.watch(settingsStorageProvider.future), - options: StorageOptions( + options: const StorageOptions( cacheTime: StorageCacheTime.unsafe_forever, - destroyKey: "v1", + destroyKey: 'v1', ), ).future; return state.value ?? - SettingsViewModel( + const SettingsViewModel( theme: ThemeMode.light, currency: Currency.try_, isFirstTime: true, @@ -61,7 +59,7 @@ class SettingsController extends _$SettingsController { state = AsyncData(state.value!.copyWith(email: userEmail)); } - void updateIsFirstTime(bool isFirstTime) { + void updateIsFirstTime({required bool isFirstTime}) { state = AsyncData(state.value!.copyWith(isFirstTime: isFirstTime)); } } diff --git a/lib/providers/settings_controller.g.dart b/lib/providers/settings_controller.g.dart index 62bf6dc..1fe612b 100644 --- a/lib/providers/settings_controller.g.dart +++ b/lib/providers/settings_controller.g.dart @@ -28,7 +28,7 @@ final class SettingsStorageProvider argument: null, retry: null, name: r'settingsStorageProvider', - isAutoDispose: true, + isAutoDispose: false, dependencies: null, $allTransitiveDependencies: null, ); @@ -48,7 +48,7 @@ final class SettingsStorageProvider } } -String _$settingsStorageHash() => r'cbb6fe2ab51d09c16684e0bc77b87d31b9442467'; +String _$settingsStorageHash() => r'a3a38c976342a6e31db1075c1f8fad90f4a5eb0a'; @ProviderFor(SettingsController) @JsonPersist() @@ -77,7 +77,7 @@ final class SettingsControllerProvider } String _$settingsControllerHash() => - r'2acc9cf68f82765c13b6e7f5ec92eb535164615f'; + r'e2e76aed054ef57d469792f60299e181a62457fa'; @JsonPersist() abstract class _$SettingsControllerBase diff --git a/lib/providers/subs_controller.dart b/lib/providers/subs_controller.dart index d620e7f..6dd26d9 100644 --- a/lib/providers/subs_controller.dart +++ b/lib/providers/subs_controller.dart @@ -10,10 +10,11 @@ import 'package:riverpod_annotation/experimental/persist.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_sqflite/riverpod_sqflite.dart'; import 'package:sqflite/sqflite.dart'; -import 'package:subs_tracker/models/sub_slice.dart'; -import 'package:subs_tracker/utils/notification_service.dart'; import 'package:timezone/timezone.dart' as tz; +import '../models/sub_slice.dart'; +import '../utils/notification_service.dart'; + part 'subs_controller.g.dart'; // Clamps day to the last valid day of the given month (handles Feb, 30-day months, etc.) @@ -22,14 +23,12 @@ int _clampDay(int year, int month, int day) { return day.clamp(1, lastDay); } -@riverpod +@Riverpod(keepAlive: true) Future subsStorage(Ref ref) async { - JsonSqFliteStorage storage = await JsonSqFliteStorage.open( + final storage = await JsonSqFliteStorage.open( join(await getDatabasesPath(), 'subs.db'), ); - ref - ..onDispose(storage.close) - ..keepAlive(); + ref.onDispose(storage.close); return storage; } @@ -40,9 +39,9 @@ class SubsController extends _$SubsController { FutureOr> build() async { await persist( ref.watch(subsStorageProvider.future), - options: StorageOptions( + options: const StorageOptions( cacheTime: StorageCacheTime.unsafe_forever, - destroyKey: "v2", + destroyKey: 'v2', ), ).future; await scheduleNotification(); @@ -57,7 +56,7 @@ class SubsController extends _$SubsController { Future scheduleNotification() async { await LocalNotificationService.instance.cancelAllNotifications(); final subs = state.value ?? []; - for (int i = 0; i < subs.length; i++) { + for (var i = 0; i < subs.length; i++) { await scheduleRepeatingNotification(subs[i], i); } } @@ -71,37 +70,33 @@ class SubsController extends _$SubsController { switch (slice.frequency) { case Frequency.daily: - nextDate = tz.TZDateTime(tz.local, now.year, now.month, now.day, 12, 0, 0); + nextDate = tz.TZDateTime(tz.local, now.year, now.month, now.day, 12); if (nextDate.isBefore(now)) { nextDate = nextDate.add(const Duration(days: 1)); } repeatComponents = DateTimeComponents.time; - break; case Frequency.weekly: - nextDate = tz.TZDateTime(tz.local, now.year, now.month, now.day, 12, 0, 0); + nextDate = tz.TZDateTime(tz.local, now.year, now.month, now.day, 12); while (nextDate.weekday != slice.startDate.weekday || nextDate.isBefore(now)) { nextDate = nextDate.add(const Duration(days: 1)); } repeatComponents = DateTimeComponents.dayOfWeekAndTime; - break; case Frequency.monthly: final curDay = _clampDay(now.year, now.month, slice.startDate.day); - nextDate = tz.TZDateTime(tz.local, now.year, now.month, curDay, 12, 0, 0); + nextDate = tz.TZDateTime(tz.local, now.year, now.month, curDay, 12); if (nextDate.isBefore(now)) { final nextMonth = now.month == 12 ? 1 : now.month + 1; final nextYear = now.month == 12 ? now.year + 1 : now.year; final nextDay = _clampDay(nextYear, nextMonth, slice.startDate.day); - nextDate = tz.TZDateTime(tz.local, nextYear, nextMonth, nextDay, 12, 0, 0); + nextDate = tz.TZDateTime(tz.local, nextYear, nextMonth, nextDay, 12); } repeatComponents = DateTimeComponents.dayOfMonthAndTime; - break; case Frequency.yearly: - nextDate = tz.TZDateTime(tz.local, now.year, slice.startDate.month, slice.startDate.day, 12, 0, 0); + nextDate = tz.TZDateTime(tz.local, now.year, slice.startDate.month, slice.startDate.day, 12); if (nextDate.isBefore(now)) { - nextDate = tz.TZDateTime(tz.local, now.year + 1, slice.startDate.month, slice.startDate.day, 12, 0, 0); + nextDate = tz.TZDateTime(tz.local, now.year + 1, slice.startDate.month, slice.startDate.day, 12); } repeatComponents = DateTimeComponents.dateAndTime; - break; } final idDue = index * 2; @@ -110,8 +105,8 @@ class SubsController extends _$SubsController { // "Due today" — repeats at the correct cadence await LocalNotificationService.instance.scheduleNotification( id: idDue, - title: "Subscription Reminder", - body: "Your ${slice.name} subscription is due today.", + title: 'Subscription Reminder', + body: 'Your ${slice.name} subscription is due today.', scheduledDate: nextDate, matchDateTimeComponents: repeatComponents, ); @@ -120,8 +115,8 @@ class SubsController extends _$SubsController { if (slice.frequency != Frequency.daily) { await LocalNotificationService.instance.scheduleNotification( id: idDueTomorrow, - title: "Subscription Reminder", - body: "Your ${slice.name} subscription is due tomorrow.", + title: 'Subscription Reminder', + body: 'Your ${slice.name} subscription is due tomorrow.', scheduledDate: nextDate.subtract(const Duration(days: 1)), matchDateTimeComponents: repeatComponents, ); @@ -139,7 +134,7 @@ class SubsController extends _$SubsController { } void clear() { - state = AsyncValue.data([]); + state = const AsyncValue.data([]); scheduleNotification(); } diff --git a/lib/providers/subs_controller.g.dart b/lib/providers/subs_controller.g.dart index d1a715b..77e3810 100644 --- a/lib/providers/subs_controller.g.dart +++ b/lib/providers/subs_controller.g.dart @@ -28,7 +28,7 @@ final class SubsStorageProvider argument: null, retry: null, name: r'subsStorageProvider', - isAutoDispose: true, + isAutoDispose: false, dependencies: null, $allTransitiveDependencies: null, ); @@ -48,7 +48,7 @@ final class SubsStorageProvider } } -String _$subsStorageHash() => r'2e3ce2bce27a4b50ba13ce6a41863ac5131f4985'; +String _$subsStorageHash() => r'd34a562d430304a2e0dcc8e8db2a01f74e768ca3'; @ProviderFor(SubsController) @JsonPersist() @@ -76,7 +76,7 @@ final class SubsControllerProvider SubsController create() => SubsController(); } -String _$subsControllerHash() => r'7735dbc7ab35ec79d398abb485c9c56fd782f555'; +String _$subsControllerHash() => r'717f1c04678d20f5c57cc801dbd8ce5213dcdfc4'; @JsonPersist() abstract class _$SubsControllerBase extends $AsyncNotifier> { diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart index 5af1cf3..5147f8e 100644 --- a/lib/providers/theme_provider.dart +++ b/lib/providers/theme_provider.dart @@ -3,8 +3,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/utils/app_theme.dart'; + +import '../utils/app_theme.dart'; +import 'settings_controller.dart'; part 'theme_provider.g.dart'; diff --git a/lib/screens/analytics_screen.dart b/lib/screens/analytics_screen.dart index f194050..5218445 100644 --- a/lib/screens/analytics_screen.dart +++ b/lib/screens/analytics_screen.dart @@ -2,12 +2,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/models/settings_view_model.dart'; -import 'package:subs_tracker/models/sub_slice.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/providers/subs_controller.dart'; -import 'package:subs_tracker/widgets/pie_chart.dart'; -import 'package:subs_tracker/widgets/sub_zilla_app_bar.dart'; +import '../models/settings_view_model.dart'; +import '../models/sub_slice.dart'; +import '../providers/settings_controller.dart'; +import '../providers/subs_controller.dart'; +import '../widgets/pie_chart.dart'; +import '../widgets/sub_zilla_app_bar.dart'; enum _Period { month, quarter, year } @@ -41,7 +41,7 @@ class AnalyticsScreen extends HookConsumerWidget { final period = useState(_Period.month); return Scaffold( - appBar: SubZillaAppBar(), + appBar: const SubZillaAppBar(), body: slicesAsync.when( data: (slices) => settingsAsync.when( data: (settings) => _buildBody(slices, settings, context, period), diff --git a/lib/screens/app_startup.dart b/lib/screens/app_startup.dart index 458209d..d7ff34e 100644 --- a/lib/screens/app_startup.dart +++ b/lib/screens/app_startup.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; +import '../providers/settings_controller.dart'; class AppStartup extends ConsumerStatefulWidget { const AppStartup({super.key , required this.child}); @@ -22,8 +22,8 @@ class _AppStartupState extends ConsumerState { }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stackTrace) => Center( - child: Text("common.error_generic".tr()), + child: Text('common.error_generic'.tr()), ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart index 8150e9f..3a91e71 100644 --- a/lib/screens/calendar_screen.dart +++ b/lib/screens/calendar_screen.dart @@ -2,12 +2,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/models/sub_slice.dart'; -import 'package:subs_tracker/providers/subs_controller.dart'; -import 'package:subs_tracker/widgets/brand_logo.dart'; -import 'package:subs_tracker/widgets/sub_zilla_app_bar.dart'; import 'package:table_calendar/table_calendar.dart'; +import '../models/sub_slice.dart'; +import '../providers/subs_controller.dart'; +import '../widgets/brand_logo.dart'; +import '../widgets/sub_zilla_app_bar.dart'; + class CalendarScreen extends HookConsumerWidget { const CalendarScreen({super.key}); @@ -48,17 +49,17 @@ class CalendarScreen extends HookConsumerWidget { } return Scaffold( - appBar: SubZillaAppBar(), + appBar: const SubZillaAppBar(), body: subsAsync.when( data: (subs) { - final List selectedEvents = selectedDay.value == null - ? [] + final selectedEvents = selectedDay.value == null + ? [] : getEventsForDay(selectedDay.value!, subs); return Column( children: [ TableCalendar( - firstDay: DateTime.utc(2020, 1, 1), + firstDay: DateTime.utc(2020), lastDay: DateTime.utc(2030, 12, 31), focusedDay: focusedDay.value, selectedDayPredicate: (day) => isSameDay(selectedDay.value, day), @@ -68,7 +69,7 @@ class CalendarScreen extends HookConsumerWidget { }, eventLoader: (day) => getEventsForDay(day, subs), calendarStyle: CalendarStyle( - cellMargin: const EdgeInsets.all(8.0), + cellMargin: const EdgeInsets.all(8), markerDecoration: BoxDecoration( color: Theme.of(context).colorScheme.primary, shape: BoxShape.circle, @@ -92,12 +93,11 @@ class CalendarScreen extends HookConsumerWidget { child: ListView.builder( itemCount: selectedEvents.length, itemBuilder: (context, index) { - final sub = selectedEvents[index]; + final sub = selectedEvents[index]; return ListTile( leading: sub.brand != null ? BrandLogo( brand: sub.brand, - size: 32, ) : CircleAvatar( radius: 16, @@ -108,7 +108,7 @@ class CalendarScreen extends HookConsumerWidget { subtitle: Text( NumberFormat.simpleCurrency().format(sub.amount), ), - trailing: Text("frequency_names.${sub.frequency.name}".tr()), + trailing: Text('frequency_names.${sub.frequency.name}'.tr()), ); }, ), diff --git a/lib/screens/color_scheme_page.dart b/lib/screens/color_scheme_page.dart index a5d5a38..2769b41 100644 --- a/lib/screens/color_scheme_page.dart +++ b/lib/screens/color_scheme_page.dart @@ -3,10 +3,10 @@ import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:simple_icons/simple_icons.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/utils/app_theme.dart'; -import 'package:subs_tracker/widgets/color_scheme_picker.dart'; -import 'package:subs_tracker/widgets/floating_sub_card.dart'; +import '../providers/settings_controller.dart'; +import '../utils/app_theme.dart'; +import '../widgets/color_scheme_picker.dart'; +import '../widgets/floating_sub_card.dart'; class ColorSchemePage extends ConsumerWidget { const ColorSchemePage({super.key, this.pageOffset = 0.0}); @@ -48,7 +48,7 @@ class ColorSchemePage extends ConsumerWidget { gradient: RadialGradient( colors: [ col.withValues(alpha: 0.30), - col.withValues(alpha: 0.0), + col.withValues(alpha: 0), ], ), ), @@ -162,7 +162,7 @@ class _SchemeNameChip extends StatelessWidget { decoration: BoxDecoration( color: color.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(20), - border: Border.all(color: color.withValues(alpha: 0.28), width: 1), + border: Border.all(color: color.withValues(alpha: 0.28)), ), child: Text( name, diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index dad2e42..0faa8e1 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -2,14 +2,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/config/router_config.dart'; -import 'package:subs_tracker/models/settings_view_model.dart'; -import 'package:subs_tracker/models/sub_slice.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/providers/subs_controller.dart'; -import 'package:subs_tracker/widgets/add_subs_dialog.dart'; -import 'package:subs_tracker/widgets/brand_logo.dart'; -import 'package:subs_tracker/widgets/sub_zilla_app_bar.dart'; +import '../config/router_config.dart'; +import '../models/settings_view_model.dart'; +import '../models/sub_slice.dart'; +import '../providers/settings_controller.dart'; +import '../providers/subs_controller.dart'; +import '../widgets/add_subs_dialog.dart'; +import '../widgets/brand_logo.dart'; +import '../widgets/sub_zilla_app_bar.dart'; class HomeScreen extends HookConsumerWidget { const HomeScreen({super.key}); @@ -57,7 +57,7 @@ class HomeScreen extends HookConsumerWidget { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Center(child: Text("home.no_subs".tr())), + Center(child: Text('home.no_subs'.tr())), const SizedBox(height: 12), TextButton( onPressed: () => showModalBottomSheet( @@ -72,7 +72,7 @@ class HomeScreen extends HookConsumerWidget { child: const AddSubsSheet(), ), ), - child: Text("home.add_sub".tr()), + child: Text('home.add_sub'.tr()), ), ], ); @@ -96,7 +96,6 @@ class HomeScreen extends HookConsumerWidget { const SizedBox(height: 16), // Compact Subscription List Flexible( - flex: 1, fit: FlexFit.tight, child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 12), @@ -156,14 +155,14 @@ class _SummaryCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "home.total_spending".tr(), + 'home.total_spending'.tr(), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white70, ), ), const SizedBox(height: 4), Text( - "$currencySymbol${total.toStringAsFixed(2)}", + '$currencySymbol${total.toStringAsFixed(2)}', style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: Colors.white, fontWeight: FontWeight.bold, @@ -178,7 +177,7 @@ class _SummaryCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: Text( - "home.active_count".tr(args: [count.toString()]), + 'home.active_count'.tr(args: [count.toString()]), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white, fontWeight: FontWeight.w600, @@ -197,7 +196,7 @@ class _SummaryCard extends StatelessWidget { child: Row( children: [ Text( - "home.most_expensive".tr(), + 'home.most_expensive'.tr(), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white70, ), @@ -231,37 +230,35 @@ class _CompactSubscriptionTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - String frequencyShort = 'frequency.short.${slice.frequency.name.toLowerCase()}'.tr(); + final frequencyShort = 'frequency.short.${slice.frequency.name.toLowerCase()}'.tr(); return Padding( padding: const EdgeInsets.only(bottom: 8), child: Dismissible( key: Key('${slice.name}-$index'), direction: DismissDirection.endToStart, - confirmDismiss: (direction) async { - return await showAdaptiveDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog.adaptive( - title: Text( - "home.delete_title".tr(), - style: TextStyle(fontWeight: FontWeight.bold), + confirmDismiss: (direction) => showAdaptiveDialog( + context: context, + builder: (context) { + return AlertDialog.adaptive( + title: Text( + 'home.delete_title'.tr(), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + content: Text('home.delete_confirm'.tr(args: [slice.name])), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text('home.cancel'.tr()), ), - content: Text("home.delete_confirm".tr(args: [slice.name])), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text("home.cancel".tr()), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text("home.delete".tr()), - ), - ], - ); - }, - ); - }, + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text('home.delete'.tr()), + ), + ], + ); + }, + ), onDismissed: (direction) { ref.read(subsControllerProvider.notifier).removeAt(index); }, @@ -272,7 +269,7 @@ class _CompactSubscriptionTile extends ConsumerWidget { color: Theme.of(context).colorScheme.error, borderRadius: BorderRadius.circular(8), ), - child: Icon( + child: const Icon( Icons.delete_outline, color: Colors.white, ), @@ -312,9 +309,9 @@ class _CompactSubscriptionTile extends ConsumerWidget { ), const SizedBox(height: 2), Text( - "home.renews_info".tr(args: [ - "${slice.startDate.month}/${slice.startDate.day}", - "frequency.${slice.frequency.name.toLowerCase()}".tr() + 'home.renews_info'.tr(args: [ + '${slice.startDate.month}/${slice.startDate.day}', + 'frequency.${slice.frequency.name.toLowerCase()}'.tr() ]), style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, @@ -327,7 +324,7 @@ class _CompactSubscriptionTile extends ConsumerWidget { ), const SizedBox(width: 8), Text( - "$currencySymbol${slice.amount.toStringAsFixed(2)}$frequencyShort", + '$currencySymbol${slice.amount.toStringAsFixed(2)}$frequencyShort', style: Theme.of(context).textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, @@ -357,5 +354,3 @@ class _CompactSliceLeading extends StatelessWidget { ); } } - - diff --git a/lib/screens/intro_page.dart b/lib/screens/intro_page.dart index e307af7..1e6e183 100644 --- a/lib/screens/intro_page.dart +++ b/lib/screens/intro_page.dart @@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:simple_icons/simple_icons.dart'; -import 'package:subs_tracker/widgets/floating_sub_card.dart'; +import '../widgets/floating_sub_card.dart'; class IntroPage extends HookWidget { const IntroPage({ @@ -88,7 +88,6 @@ class IntroPage extends HookWidget { logoColor: const Color(0xFFE50914), logoInitials: 'N', logoIcon: SimpleIcons.values['netflix'], - phaseOffset: 0.0, ), ), ), diff --git a/lib/screens/onboarding_screen.dart b/lib/screens/onboarding_screen.dart index bbf5a58..890f981 100644 --- a/lib/screens/onboarding_screen.dart +++ b/lib/screens/onboarding_screen.dart @@ -5,12 +5,13 @@ import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/screens/color_scheme_page.dart'; -import 'package:subs_tracker/widgets/floating_sub_card.dart'; -import 'package:subs_tracker/screens/intro_page.dart'; -import 'package:subs_tracker/widgets/page_dots.dart'; -import 'package:subs_tracker/screens/popular_subs_page.dart'; + +import '../providers/settings_controller.dart'; +import '../widgets/floating_sub_card.dart'; +import '../widgets/page_dots.dart'; +import 'color_scheme_page.dart'; +import 'intro_page.dart'; +import 'popular_subs_page.dart'; class OnboardingScreen extends HookConsumerWidget { const OnboardingScreen({super.key}); @@ -76,8 +77,8 @@ class OnboardingScreen extends HookConsumerWidget { // Page 2 target: first tile in PopularSubsPage ListView. // tile0Left must match ListView's horizontal padding (16px each side). - final tile0Left = 16.0; - final tile0Top = _popularHeaderH; + const tile0Left = 16.0; + const tile0Top = _popularHeaderH; final tile0W = w - 32; return AnimatedBuilder( @@ -101,7 +102,7 @@ class OnboardingScreen extends HookConsumerWidget { Expanded( child: PageView( controller: pageController, - onPageChanged: (int page) => + onPageChanged: (page) => currentPage.value = page, children: [ IntroPage( @@ -192,7 +193,6 @@ class OnboardingScreen extends HookConsumerWidget { t2, )!, logoInitials: 'N', - rotation: 0.0, width: lerpDouble(_card1W, tile0W, t2)!, phaseOffset: lerpDouble(0.3, 0.0, t2)!, amplitude: lerpDouble(1.0, 0.0, t2)!, diff --git a/lib/screens/popular_subs_page.dart b/lib/screens/popular_subs_page.dart index 425a0d6..18dd88b 100644 --- a/lib/screens/popular_subs_page.dart +++ b/lib/screens/popular_subs_page.dart @@ -3,21 +3,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/config/router_config.dart'; -import 'package:subs_tracker/models/brand.dart'; -import 'package:subs_tracker/models/settings_view_model.dart'; -import 'package:subs_tracker/models/sub_slice.dart'; -import 'package:subs_tracker/providers/brands_provider.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/providers/subs_controller.dart'; -import 'package:subs_tracker/utils/brand_utils.dart'; -import 'package:subs_tracker/widgets/brand_logo.dart'; +import '../config/router_config.dart'; +import '../models/brand.dart'; +import '../models/settings_view_model.dart'; +import '../models/sub_slice.dart'; +import '../providers/brands_provider.dart'; +import '../providers/settings_controller.dart'; +import '../providers/subs_controller.dart'; +import '../utils/brand_utils.dart'; +import '../widgets/brand_logo.dart'; int _colorForBrand(Brand brand) { final c = brand.iconColor; if (c != null) return c.toARGB32(); final hash = brand.text.codeUnits.fold(0, (a, b) => (a * 31 + b) & 0xFFFFFFFF); - return HSLColor.fromAHSL(1.0, (hash % 360).toDouble(), 0.58, 0.45).toColor().toARGB32(); + return HSLColor.fromAHSL(1, (hash % 360).toDouble(), 0.58, 0.45).toColor().toARGB32(); } class PopularSubsPage extends HookConsumerWidget { @@ -117,7 +117,6 @@ class PopularSubsPage extends HookConsumerWidget { amount: amount, color: _colorForBrand(brand), startDate: DateTime.now(), - frequency: Frequency.monthly, brand: brand, ), ); @@ -135,7 +134,7 @@ class PopularSubsPage extends HookConsumerWidget { onPressed: () { ref .read(settingsControllerProvider.notifier) - .updateIsFirstTime(false); + .updateIsFirstTime(isFirstTime: false); context.go(Routes.home.route); }, style: FilledButton.styleFrom( @@ -258,7 +257,6 @@ class _PopularSubTile extends HookWidget { focusedBorder: UnderlineInputBorder( borderSide: BorderSide( color: cs.primary.withValues(alpha: 0.5), - width: 1, ), ), ), diff --git a/lib/screens/profile_page.dart b/lib/screens/profile_page.dart index cf6b8b4..f9fb834 100644 --- a/lib/screens/profile_page.dart +++ b/lib/screens/profile_page.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; +import '../providers/settings_controller.dart'; class ProfilePage extends HookConsumerWidget { const ProfilePage({super.key}); @@ -13,9 +13,8 @@ class ProfilePage extends HookConsumerWidget { return SingleChildScrollView( child: Padding( - padding: const EdgeInsets.fromLTRB(32.0, 60.0, 32.0, 24.0), + padding: const EdgeInsets.fromLTRB(32, 60, 32, 24), child: Column( - mainAxisAlignment: MainAxisAlignment.start, children: [ // Profile Image Preview Container( @@ -54,7 +53,7 @@ class ProfilePage extends HookConsumerWidget { const SizedBox(height: 16), // Email Input - Container( + DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(16), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index faf8c44..c825368 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -3,11 +3,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/models/settings_view_model.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/utils/app_theme.dart'; -import 'package:subs_tracker/widgets/color_scheme_picker.dart'; -import 'package:subs_tracker/widgets/sub_zilla_app_bar.dart'; +import '../models/settings_view_model.dart'; +import '../providers/settings_controller.dart'; +import '../utils/app_theme.dart'; +import '../widgets/color_scheme_picker.dart'; +import '../widgets/sub_zilla_app_bar.dart'; class SettingsScreen extends HookConsumerWidget { const SettingsScreen({super.key}); @@ -17,7 +17,7 @@ class SettingsScreen extends HookConsumerWidget { final settingsAsync = ref.watch(settingsControllerProvider); return Scaffold( - appBar: SubZillaAppBar(), + appBar: const SubZillaAppBar(), body: settingsAsync.when( data: (settings) => _buildSettings(context, settings, ref), loading: () => const Center(child: CircularProgressIndicator()), @@ -36,7 +36,7 @@ class SettingsScreen extends HookConsumerWidget { padding: const EdgeInsets.all(16), children: [ Text( - "settings.title", + 'settings.title', style: Theme.of( context, ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), @@ -44,15 +44,15 @@ class SettingsScreen extends HookConsumerWidget { const SizedBox(height: 24), // Theme Section _SettingsSection( - title: "settings.appearance".tr(), + title: 'settings.appearance'.tr(), children: [ _SettingsTile( - title: "settings.theme".tr(), + title: 'settings.theme'.tr(), subtitle: _getThemeLabel(settings.theme).tr(), onTap: () => _showThemeBottomSheet(context, settings, ref), ), _SettingsTile( - title: "settings.color_scheme".tr(), + title: 'settings.color_scheme'.tr(), subtitle: schemeDisplayName(settings.colorScheme), onTap: () => _showColorSchemeBottomSheet(context, settings, ref), @@ -62,12 +62,12 @@ class SettingsScreen extends HookConsumerWidget { const SizedBox(height: 24), // Currency Section _SettingsSection( - title: "settings.currency".tr(), + title: 'settings.currency'.tr(), children: [ _SettingsTile( - title: "settings.currency_unit".tr(), + title: 'settings.currency_unit'.tr(), subtitle: - "${settings.currency.label} (${settings.currency.symbol})", + '${settings.currency.label} (${settings.currency.symbol})', onTap: () => _showCurrencyBottomSheet(context, settings, ref), ), ], @@ -75,30 +75,30 @@ class SettingsScreen extends HookConsumerWidget { const SizedBox(height: 24), // Language Section _SettingsSection( - title: "settings.language".tr(), + title: 'settings.language'.tr(), children: [ _SettingsTile( - title: "settings.language".tr(), + title: 'settings.language'.tr(), subtitle: context.locale.languageCode == 'en' ? 'English' : 'Türkçe', - onTap: () async => await _showLanguageBottomSheet(context), + onTap: () => _showLanguageBottomSheet(context), ), ], ), const SizedBox(height: 24), // About Section _SettingsSection( - title: "settings.about".tr(), + title: 'settings.about'.tr(), children: [ _SettingsTile( - title: "settings.version".tr(), - subtitle: "1.0.0", + title: 'settings.version'.tr(), + subtitle: '1.0.0', onTap: null, ), _SettingsTile( - title: "settings.app_name".tr(), - subtitle: "SubZilla", + title: 'settings.app_name'.tr(), + subtitle: 'SubZilla', onTap: null, ), ], @@ -106,7 +106,7 @@ class SettingsScreen extends HookConsumerWidget { const SizedBox(height: 24), if (kDebugMode) _SettingsSection( - title: "Developer", + title: 'Developer', children: [_ResetOnboardingTile(ref: ref)], ), ], @@ -117,11 +117,11 @@ class SettingsScreen extends HookConsumerWidget { String _getThemeLabel(ThemeMode mode) { switch (mode) { case ThemeMode.light: - return "settings.light"; + return 'settings.light'; case ThemeMode.dark: - return "settings.dark"; + return 'settings.dark'; case ThemeMode.system: - return "settings.system"; + return 'settings.system'; } } @@ -130,7 +130,7 @@ class SettingsScreen extends HookConsumerWidget { SettingsViewModel settings, WidgetRef ref, ) { - showModalBottomSheet( + showModalBottomSheet( useRootNavigator: true, context: context, builder: (context) { @@ -141,7 +141,7 @@ class SettingsScreen extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(16), child: Text( - "settings.select_theme".tr(), + 'settings.select_theme'.tr(), style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -172,7 +172,7 @@ class SettingsScreen extends HookConsumerWidget { SettingsViewModel settings, WidgetRef ref, ) { - showModalBottomSheet( + showModalBottomSheet( useRootNavigator: true, context: context, builder: (context) { @@ -183,7 +183,7 @@ class SettingsScreen extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(16), child: Text( - "settings.select_color_scheme".tr(), + 'settings.select_color_scheme'.tr(), style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -211,7 +211,7 @@ class SettingsScreen extends HookConsumerWidget { SettingsViewModel settings, WidgetRef ref, ) { - showModalBottomSheet( + showModalBottomSheet( useRootNavigator: true, context: context, builder: (context) { @@ -222,7 +222,7 @@ class SettingsScreen extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(16), child: Text( - "settings.select_currency".tr(), + 'settings.select_currency'.tr(), style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -257,7 +257,7 @@ class SettingsScreen extends HookConsumerWidget { } Future _showLanguageBottomSheet(BuildContext context) async { - await showModalBottomSheet( + await showModalBottomSheet( useRootNavigator: true, context: context, builder: (context) { @@ -268,14 +268,14 @@ class SettingsScreen extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(16), child: Text( - "settings.select_language".tr(), + 'settings.select_language'.tr(), style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), LanguageOption( - label: "English", + label: 'English', isSelected: context.locale.languageCode == 'en', onTap: () { context.setLocale(const Locale('en')); @@ -283,7 +283,7 @@ class SettingsScreen extends HookConsumerWidget { }, ), LanguageOption( - label: "Türkçe", + label: 'Türkçe', isSelected: context.locale.languageCode == 'tr', onTap: () { context.setLocale(const Locale('tr')); @@ -464,10 +464,10 @@ class _ResetOnboardingTile extends StatelessWidget { ], ), ); - if (confirmed == true && context.mounted) { + if ((confirmed ?? false) && context.mounted) { ref .read(settingsControllerProvider.notifier) - .updateIsFirstTime(true); + .updateIsFirstTime(isFirstTime: true); context.go('/intro'); } }, diff --git a/lib/screens/sub_detail_screen.dart b/lib/screens/sub_detail_screen.dart index 0088125..de56d7f 100644 --- a/lib/screens/sub_detail_screen.dart +++ b/lib/screens/sub_detail_screen.dart @@ -5,11 +5,11 @@ import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/models/sub_slice.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/providers/subs_controller.dart'; -import 'package:subs_tracker/utils/color_palette.dart'; -import 'package:subs_tracker/widgets/brand_logo.dart'; +import '../models/sub_slice.dart'; +import '../providers/settings_controller.dart'; +import '../providers/subs_controller.dart'; +import '../utils/color_palette.dart'; +import '../widgets/brand_logo.dart'; // --------------------------------------------------------------------------- // Helper functions @@ -70,7 +70,7 @@ void _showFrequencyPicker( SubSlice draft, void Function(SubSlice) onUpdate, ) { - final frequencies = Frequency.values; + const frequencies = Frequency.values; final initialIndex = frequencies.indexOf(draft.frequency); showCupertinoModalPopup( @@ -176,7 +176,7 @@ void _showColorPicker( final theme = Theme.of(ctx); Future openCustom() async { - Color temp = currentColor; + var temp = currentColor; final result = await showDialog( context: ctx, builder: (dialogCtx) => StatefulBuilder( @@ -187,7 +187,6 @@ void _showColorPicker( pickerColor: temp, onColorChanged: (c) => setS(() => temp = c), enableAlpha: false, - paletteType: PaletteType.hsvWithHue, labelTypes: const [], pickerAreaBorderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -612,7 +611,6 @@ class _HeroSection extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ GestureDetector( onTap: slice.brand == null ? onTapColor : null, @@ -644,7 +642,6 @@ class _HeroSection extends StatelessWidget { children: [ if (slice.brand == null) Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: EditableText( @@ -790,7 +787,6 @@ class _StatCard extends StatelessWidget { const SizedBox(height: 4), if (controller != null && focusNode != null && onSave != null) Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Flexible( child: EditableText( @@ -922,7 +918,7 @@ class _DetailsSection extends StatelessWidget { ), ClipRRect( borderRadius: BorderRadius.circular(14), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainer, ), @@ -1008,7 +1004,6 @@ class _ColorDetailRow extends StatelessWidget { shape: BoxShape.circle, border: Border.all( color: theme.colorScheme.outline.withValues(alpha: 0.3), - width: 1, ), ), ), @@ -1074,7 +1069,7 @@ class _PaymentHistorySection extends StatelessWidget { if (history.isNotEmpty) ClipRRect( borderRadius: BorderRadius.circular(14), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainer, ), diff --git a/lib/screens/user_name_page.dart b/lib/screens/user_name_page.dart index 1ca417b..4b65807 100644 --- a/lib/screens/user_name_page.dart +++ b/lib/screens/user_name_page.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; +import '../providers/settings_controller.dart'; class UserNamePage extends HookConsumerWidget { const UserNamePage({super.key}); @@ -13,9 +13,8 @@ class UserNamePage extends HookConsumerWidget { return SingleChildScrollView( child: Padding( - padding: const EdgeInsets.fromLTRB(32.0, 60.0, 32.0, 24.0), + padding: const EdgeInsets.fromLTRB(32, 60, 32, 24), child: Column( - mainAxisAlignment: MainAxisAlignment.start, children: [ Container( height: 160, @@ -50,7 +49,7 @@ class UserNamePage extends HookConsumerWidget { textAlign: TextAlign.center, ), const SizedBox(height: 16), - Container( + DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(16), @@ -93,4 +92,4 @@ class UserNamePage extends HookConsumerWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/utils/brand_utils.dart b/lib/utils/brand_utils.dart index 834aece..143336b 100644 --- a/lib/utils/brand_utils.dart +++ b/lib/utils/brand_utils.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:simple_icons/simple_icons.dart'; -import 'package:subs_tracker/models/brand.dart'; +import '../models/brand.dart'; /// Extension on [Brand] to derive icon data from Simple Icons. /// diff --git a/lib/utils/color_utils.dart b/lib/utils/color_utils.dart index 0630895..38f1609 100644 --- a/lib/utils/color_utils.dart +++ b/lib/utils/color_utils.dart @@ -3,4 +3,4 @@ import 'package:flutter/material.dart'; Color darkerOf(Color c, [double amount = 0.2]) { final hsl = HSLColor.fromColor(c); return hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)).toColor(); -} \ No newline at end of file +} diff --git a/lib/utils/notification_service.dart b/lib/utils/notification_service.dart index 417d995..cd825f5 100644 --- a/lib/utils/notification_service.dart +++ b/lib/utils/notification_service.dart @@ -24,17 +24,17 @@ class LocalNotificationService implements NotificationService { final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - Future> get scheduledNotifications async => - await flutterLocalNotificationsPlugin.pendingNotificationRequests(); + Future> get scheduledNotifications => + flutterLocalNotificationsPlugin.pendingNotificationRequests(); @override Future init() async { - const AndroidInitializationSettings initializationSettingsAndroid = + const initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); - const DarwinInitializationSettings initializationSettingsIOS = + const initializationSettingsIOS = DarwinInitializationSettings(); - const InitializationSettings initializationSettings = + const initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS, @@ -42,7 +42,7 @@ class LocalNotificationService implements NotificationService { tz.initializeTimeZones(); - await flutterLocalNotificationsPlugin.initialize(initializationSettings); + await flutterLocalNotificationsPlugin.initialize(settings: initializationSettings); // ANDROID 13+: ask for runtime permission final androidImpl = flutterLocalNotificationsPlugin @@ -68,11 +68,11 @@ class LocalNotificationService implements NotificationService { DateTimeComponents? matchDateTimeComponents, }) async { await flutterLocalNotificationsPlugin.zonedSchedule( - id, - title, - body, - tz.TZDateTime.from(scheduledDate, tz.local), - const NotificationDetails( + id: id, + title: title, + body: body, + scheduledDate: tz.TZDateTime.from(scheduledDate, tz.local), + notificationDetails: const NotificationDetails( iOS: DarwinNotificationDetails(), android: AndroidNotificationDetails( 'subscription_channel_id', @@ -88,7 +88,7 @@ class LocalNotificationService implements NotificationService { @override Future cancelNotification(int id) async { - await flutterLocalNotificationsPlugin.cancel(id); + await flutterLocalNotificationsPlugin.cancel(id: id); } @override diff --git a/lib/widgets/action_text_form_field.dart b/lib/widgets/action_text_form_field.dart index 3883be7..756abe4 100644 --- a/lib/widgets/action_text_form_field.dart +++ b/lib/widgets/action_text_form_field.dart @@ -12,7 +12,7 @@ class ActionTextFormField extends StatefulWidget { final String labelText; final String initialValue; final IconData saveIcon; - final Function(String value) onSave; + final void Function(String value) onSave; @override State createState() => _ActionTextFormFieldState(); @@ -25,7 +25,7 @@ class _ActionTextFormFieldState extends State { bool _isModified = false; // Butonun animasyonlu genişliği için bir sabit - final double _kButtonWidth = 56.0; + final double _kButtonWidth = 56; @override void initState() { @@ -47,7 +47,7 @@ class _ActionTextFormFieldState extends State { // Controller her değiştiğinde bu fonksiyon çalışır void _handleTextChange() { // O anki metin, başlangıçtaki metinden farklı mı? - final bool currentlyModified = _controller.text != widget.initialValue; + final currentlyModified = _controller.text != widget.initialValue; // Durum değiştiyse (örn. false -> true olduysa) setState'i çağır if (_isModified != currentlyModified) { @@ -88,7 +88,7 @@ class _ActionTextFormFieldState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), ); - final BorderRadius defaultBorderRadius = BorderRadius.circular(10.0); + final defaultBorderRadius = BorderRadius.circular(10); return Material( elevation: 3, diff --git a/lib/widgets/add_subs_dialog.dart b/lib/widgets/add_subs_dialog.dart index 84f59e3..638efc7 100644 --- a/lib/widgets/add_subs_dialog.dart +++ b/lib/widgets/add_subs_dialog.dart @@ -3,13 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/models/brand.dart'; -import 'package:subs_tracker/models/sub_slice.dart'; -import 'package:subs_tracker/providers/brands_provider.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/providers/subs_controller.dart'; -import 'package:subs_tracker/utils/color_palette.dart'; -import 'package:subs_tracker/widgets/brand_logo.dart'; +import '../models/brand.dart'; +import '../models/sub_slice.dart'; +import '../providers/brands_provider.dart'; +import '../providers/settings_controller.dart'; +import '../providers/subs_controller.dart'; +import '../utils/color_palette.dart'; +import 'brand_logo.dart'; const _kPopularBrandNames = [ 'Netflix', @@ -25,7 +25,7 @@ class AddSubsSheet extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final formKey = useMemoized(() => GlobalKey()); + final formKey = useMemoized(GlobalKey.new); final searchCtrl = useTextEditingController(); final nameCtrl = useTextEditingController(); final amountCtrl = useTextEditingController(); @@ -34,7 +34,6 @@ class AddSubsSheet extends HookConsumerWidget { final draftSlice = useState( SubSlice( - brand: null, name: '', amount: 0, color: kSliceColors.first.toARGB32(), @@ -44,7 +43,7 @@ class AddSubsSheet extends HookConsumerWidget { final settingsAsync = ref.watch(settingsControllerProvider); final currencySymbol = - settingsAsync.asData?.value.currency.symbol ?? '\$'; + settingsAsync.asData?.value.currency.symbol ?? r'$'; void selectBrand(Brand brand) { draftSlice.value = draftSlice.value.copyWith(brand: brand, name: brand.text); @@ -57,8 +56,8 @@ class AddSubsSheet extends HookConsumerWidget { } Future openCustomColorPicker() async { - Color tempColor = Color(draftSlice.value.color); - final Color? result = await showDialog( + var tempColor = Color(draftSlice.value.color); + final result = await showDialog( context: context, builder: (dialogContext) { return StatefulBuilder( @@ -72,7 +71,6 @@ class AddSubsSheet extends HookConsumerWidget { tempColor = color; }), enableAlpha: false, - paletteType: PaletteType.hsvWithHue, labelTypes: const [], pickerAreaBorderRadius: const BorderRadius.all( Radius.circular(12), @@ -207,7 +205,7 @@ class AddSubsSheet extends HookConsumerWidget { textEditingController: searchCtrl, focusNode: brandFocusNode, displayStringForOption: (option) => option.text, - optionsBuilder: (TextEditingValue textEditingValue) { + optionsBuilder: (textEditingValue) { if (textEditingValue.text.trim().isEmpty) { return const Iterable.empty(); } @@ -217,9 +215,7 @@ class AddSubsSheet extends HookConsumerWidget { option.text.toLowerCase().contains(query), ); }, - onSelected: (Brand selection) { - selectBrand(selection); - }, + onSelected: selectBrand, fieldViewBuilder: ( context, textEditingController, @@ -381,7 +377,7 @@ class AddSubsSheet extends HookConsumerWidget { // Grouped form card ClipRRect( borderRadius: BorderRadius.circular(12), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(12), @@ -501,7 +497,6 @@ class AddSubsSheet extends HookConsumerWidget { ), Expanded( child: SegmentedButton( - multiSelectionEnabled: false, showSelectedIcon: false, style: SegmentedButton.styleFrom( padding: EdgeInsets.zero, @@ -739,9 +734,9 @@ class AddSubsSheet extends HookConsumerWidget { final brandsAsync = ref.watch(brandsProvider); return brandsAsync.when( - data: (allBrands) => buildSheetContent(allBrands), - loading: () => _SheetSkeleton( - child: const SizedBox( + data: buildSheetContent, + loading: () => const _SheetSkeleton( + child: SizedBox( height: 80, child: Center(child: CircularProgressIndicator()), ), diff --git a/lib/widgets/bottom_controls.dart b/lib/widgets/bottom_controls.dart index e15dd24..96e6a19 100644 --- a/lib/widgets/bottom_controls.dart +++ b/lib/widgets/bottom_controls.dart @@ -20,7 +20,7 @@ class BottomControls extends StatelessWidget { if (currentPage.value == 0) return const SizedBox.shrink(); return Padding( - padding: const EdgeInsets.all(24.0), + padding: const EdgeInsets.all(24), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/widgets/brand_logo.dart b/lib/widgets/brand_logo.dart index 0697f2a..1a190c4 100644 --- a/lib/widgets/brand_logo.dart +++ b/lib/widgets/brand_logo.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:subs_tracker/models/brand.dart'; -import 'package:subs_tracker/utils/brand_utils.dart'; +import '../models/brand.dart'; +import '../utils/brand_utils.dart'; class SubLeadingIcon extends StatelessWidget { const SubLeadingIcon({ diff --git a/lib/widgets/color_scheme_picker.dart b/lib/widgets/color_scheme_picker.dart index 3eef517..5a056e7 100644 --- a/lib/widgets/color_scheme_picker.dart +++ b/lib/widgets/color_scheme_picker.dart @@ -1,6 +1,6 @@ import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; -import 'package:subs_tracker/utils/app_theme.dart'; +import '../utils/app_theme.dart'; class ColorSchemePicker extends StatelessWidget { const ColorSchemePicker({ diff --git a/lib/widgets/edit_subs_dialog.dart b/lib/widgets/edit_subs_dialog.dart index 4042f73..9e2434e 100644 --- a/lib/widgets/edit_subs_dialog.dart +++ b/lib/widgets/edit_subs_dialog.dart @@ -3,12 +3,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/models/brand.dart'; -import 'package:subs_tracker/models/sub_slice.dart'; -import 'package:subs_tracker/providers/brands_provider.dart'; -import 'package:subs_tracker/providers/subs_controller.dart'; -import 'package:subs_tracker/utils/color_palette.dart'; -import 'package:subs_tracker/widgets/brand_logo.dart'; +import '../models/brand.dart'; +import '../models/sub_slice.dart'; +import '../providers/brands_provider.dart'; +import '../providers/subs_controller.dart'; +import '../utils/color_palette.dart'; +import 'brand_logo.dart'; class EditSubsDialog extends HookConsumerWidget { const EditSubsDialog({ @@ -22,7 +22,7 @@ class EditSubsDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final formKey = useMemoized(() => GlobalKey()); + final formKey = useMemoized(GlobalKey.new); final nameCtrl = useTextEditingController(text: slice.name); final amountCtrl = useTextEditingController(text: slice.amount.toString()); final brandFocusNode = useFocusNode(); @@ -31,8 +31,8 @@ class EditSubsDialog extends HookConsumerWidget { final theme = Theme.of(context); Future openCustomColorPicker() async { - Color tempColor = Color(draftSlice.value.color); - final Color? result = await showDialog( + var tempColor = Color(draftSlice.value.color); + final result = await showDialog( context: context, builder: (dialogContext) { return StatefulBuilder( @@ -46,7 +46,6 @@ class EditSubsDialog extends HookConsumerWidget { tempColor = color; }), enableAlpha: false, - paletteType: PaletteType.hsvWithHue, labelTypes: const [], pickerAreaBorderRadius: const BorderRadius.all( Radius.circular(12), @@ -75,7 +74,7 @@ class EditSubsDialog extends HookConsumerWidget { } void onSave() { - if (formKey.currentState?.validate() == true) { + if (formKey.currentState?.validate() ?? false) { final amount = double.parse(amountCtrl.text.replaceAll(',', '.')); ref.read(subsControllerProvider.notifier).updateAt( index, @@ -157,17 +156,17 @@ class EditSubsDialog extends HookConsumerWidget { textEditingController: nameCtrl, focusNode: brandFocusNode, displayStringForOption: (option) => option.text, - optionsBuilder: (TextEditingValue textEditingValue) { + optionsBuilder: (textEditingValue) { if (textEditingValue.text.trim().isEmpty) { return const Iterable.empty(); } final query = textEditingValue.text.toLowerCase(); return allBrands.where( - (Brand option) => + (option) => option.text.toLowerCase().contains(query), ); }, - onSelected: (Brand selection) { + onSelected: (selection) { draftSlice.value = draftSlice.value.copyWith( brand: selection, name: selection.text, @@ -196,13 +195,12 @@ class EditSubsDialog extends HookConsumerWidget { padding: const EdgeInsets.all(8), child: BrandLogo( brand: draftSlice.value.brand, - size: 32, ), ) : const Icon(Icons.search), suffixIcon: draftSlice.value.brand != null ? Tooltip( - message: "dialogs.selected_tooltip".tr( + message: 'dialogs.selected_tooltip'.tr( args: [draftSlice.value.brand!.text], ), child: Icon( @@ -245,7 +243,7 @@ class EditSubsDialog extends HookConsumerWidget { separatorBuilder: (_, _) => const Divider(height: 0), itemBuilder: (context, index) { - final Brand option = options.elementAt(index); + final option = options.elementAt(index); return ListTile( leading: BrandLogo( brand: option, @@ -339,7 +337,7 @@ class EditSubsDialog extends HookConsumerWidget { return DropdownMenuItem( value: f, child: Text( - "frequency_names.${f.name.toLowerCase()}".tr(), + 'frequency_names.${f.name.toLowerCase()}'.tr(), ), ); }).toList(), diff --git a/lib/widgets/edit_user_profile.dart b/lib/widgets/edit_user_profile.dart index 6ce61cf..ecd61d6 100644 --- a/lib/widgets/edit_user_profile.dart +++ b/lib/widgets/edit_user_profile.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/widgets/action_text_form_field.dart'; +import '../providers/settings_controller.dart'; +import 'action_text_form_field.dart'; class EditUserProfileDialog extends ConsumerStatefulWidget { const EditUserProfileDialog({super.key}); @@ -17,7 +17,7 @@ class _ChooseOrEditPPState extends ConsumerState { final settingsController = ref.watch(settingsControllerProvider); return AlertDialog.adaptive( - title: Text("intro.edit_picture".tr()), + title: Text('intro.edit_picture'.tr()), content: settingsController.when( error: (e, st) => Center(child: Text('Error: $e')), loading: () => @@ -36,22 +36,22 @@ class _ChooseOrEditPPState extends ConsumerState { ), const Divider(height: 32), ActionTextFormField( - labelText: "intro.username_label".tr(), - initialValue: slice.userName ?? "", + labelText: 'intro.username_label'.tr(), + initialValue: slice.userName ?? '', onSave: (newUserName) { ref .read(settingsControllerProvider.notifier) .updateUserName(newUserName); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("intro.username_saved".tr())), + SnackBar(content: Text('intro.username_saved'.tr())), ); }, ), const SizedBox(height: 8), ActionTextFormField( - labelText: "intro.email_label".tr(), - initialValue: slice.email ?? "", + labelText: 'intro.email_label'.tr(), + initialValue: slice.email ?? '', onSave: (newEmail) { ref .read(settingsControllerProvider.notifier) @@ -59,7 +59,7 @@ class _ChooseOrEditPPState extends ConsumerState { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text("intro.email_saved".tr()))); + ).showSnackBar(SnackBar(content: Text('intro.email_saved'.tr()))); }, ), ], diff --git a/lib/widgets/floating_sub_card.dart b/lib/widgets/floating_sub_card.dart index d9166e5..cb42d38 100644 --- a/lib/widgets/floating_sub_card.dart +++ b/lib/widgets/floating_sub_card.dart @@ -67,7 +67,6 @@ class FloatingSubCard extends HookWidget { ], ), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( width: 30, diff --git a/lib/widgets/glass_nav_bar.dart b/lib/widgets/glass_nav_bar.dart index 167081b..e6ca3a5 100644 --- a/lib/widgets/glass_nav_bar.dart +++ b/lib/widgets/glass_nav_bar.dart @@ -78,13 +78,12 @@ class _GlassNavBarState extends State borderRadius: BorderRadius.circular(28), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: cs.surfaceContainerHighest.withValues(alpha: 0.75), borderRadius: BorderRadius.circular(28), border: Border.all( color: cs.outline.withValues(alpha: 0.2), - width: 1, ), ), child: LayoutBuilder( diff --git a/lib/widgets/menu_bar.dart b/lib/widgets/menu_bar.dart index 6f3354e..2605a2c 100644 --- a/lib/widgets/menu_bar.dart +++ b/lib/widgets/menu_bar.dart @@ -7,12 +7,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:subs_tracker/config/router_config.dart'; -import 'package:subs_tracker/providers/settings_controller.dart'; -import 'package:subs_tracker/providers/subs_controller.dart'; -import 'package:subs_tracker/widgets/add_subs_dialog.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../config/router_config.dart'; +import '../providers/settings_controller.dart'; +import '../providers/subs_controller.dart'; +import 'add_subs_dialog.dart'; + class SidebarMenu extends ConsumerStatefulWidget { const SidebarMenu({super.key}); @@ -142,8 +143,8 @@ class _MenubarState extends ConsumerState { future: _pkg, builder: (ctx, snap) { final version = snap.hasData - ? "${snap.data!.version} (${snap.data!.buildNumber})" - : "—"; + ? '${snap.data!.version} (${snap.data!.buildNumber})' + : '—'; return AlertDialog.adaptive( title: Text('settings.app_name'.tr()), content: Column( @@ -152,7 +153,7 @@ class _MenubarState extends ConsumerState { children: [ Row( children: [ - Image.asset("assets/App_Logo.png", width: 48, height: 48), + Image.asset('assets/App_Logo.png', width: 48, height: 48), const SizedBox(width: 12), Expanded( child: Column( @@ -169,12 +170,12 @@ class _MenubarState extends ConsumerState { ], ), const SizedBox(height: 12), - Text("menu.about_desc".tr()), + Text('menu.about_desc'.tr()), const SizedBox(height: 12), InkWell( onTap: _launchUrl, child: Text( - "menu.view_github".tr(), + 'menu.view_github'.tr(), style: const TextStyle( color: Colors.blue, decoration: TextDecoration.underline, @@ -215,7 +216,7 @@ class _MenubarState extends ConsumerState { children: [ CircleAvatar( radius: 24, - child: ClipOval(child: Image.asset("assets/App_Logo.png", width: 48, height: 48)), + child: ClipOval(child: Image.asset('assets/App_Logo.png', width: 48, height: 48)), ), const SizedBox(width: 12), Text( @@ -230,7 +231,7 @@ class _MenubarState extends ConsumerState { ), ListTile( leading: const Icon(Icons.home_outlined), - title: Text("menu.home".tr()), + title: Text('menu.home'.tr()), onTap: () { Navigator.of(context).pop(); context.go(Routes.home.route); @@ -259,7 +260,7 @@ class _MenubarState extends ConsumerState { const Divider(height: 24, indent: 16, endIndent: 16), SwitchListTile( secondary: const Icon(Icons.dark_mode_outlined), - title: Text("menu.dark_mode".tr()), + title: Text('menu.dark_mode'.tr()), value: slice.theme == ThemeMode.dark, onChanged: (value) { ref @@ -269,7 +270,7 @@ class _MenubarState extends ConsumerState { ), ListTile( leading: const Icon(Icons.upload_file_outlined), - title: Text("menu.export".tr()), + title: Text('menu.export'.tr()), onTap: () { Navigator.of(context).pop(); _exportSubscriptions(); @@ -277,7 +278,7 @@ class _MenubarState extends ConsumerState { ), ListTile( leading: const Icon(Icons.download_outlined), - title: Text("menu.import".tr()), + title: Text('menu.import'.tr()), onTap: () async { final confirmed = await showAdaptiveDialog( context: context, @@ -305,7 +306,7 @@ class _MenubarState extends ConsumerState { const Divider(height: 24, indent: 16, endIndent: 16), ListTile( leading: const Icon(Icons.privacy_tip_outlined), - title: Text("menu.privacy_policy".tr()), + title: Text('menu.privacy_policy'.tr()), onTap: () { Navigator.of(context).pop(); _launchPrivacyPolicy(); @@ -313,7 +314,7 @@ class _MenubarState extends ConsumerState { ), ListTile( leading: const Icon(Icons.gavel_outlined), - title: Text("menu.terms_conditions".tr()), + title: Text('menu.terms_conditions'.tr()), onTap: () { Navigator.of(context).pop(); _launchTermsAndConditions(); @@ -321,7 +322,7 @@ class _MenubarState extends ConsumerState { ), ListTile( leading: const Icon(Icons.info_outline), - title: Text("menu.about".tr()), + title: Text('menu.about'.tr()), onTap: () { Navigator.of(context).pop(); _showAboutDialog(context); diff --git a/lib/widgets/pie_chart.dart b/lib/widgets/pie_chart.dart index 1fe2320..7b72e8e 100644 --- a/lib/widgets/pie_chart.dart +++ b/lib/widgets/pie_chart.dart @@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:subs_tracker/models/brand.dart'; -import 'package:subs_tracker/models/sub_slice.dart'; -import 'package:subs_tracker/providers/subs_controller.dart'; -import 'package:subs_tracker/utils/color_utils.dart'; -import 'package:subs_tracker/widgets/brand_logo.dart'; +import '../models/brand.dart'; +import '../models/sub_slice.dart'; +import '../providers/subs_controller.dart'; +import '../utils/color_utils.dart'; +import 'brand_logo.dart'; class SubsPie extends ConsumerStatefulWidget { const SubsPie({super.key}); @@ -23,9 +23,9 @@ class _SubsPieState extends ConsumerState { final slicesAsync = ref.watch(subsControllerProvider); return slicesAsync.when( - error: (e, st) => Center(child: Text("common.error_generic".tr())), + error: (e, st) => Center(child: Text('common.error_generic'.tr())), loading: () => const Center(child: CircularProgressIndicator.adaptive()), - data: (slices) => _buildChart(slices), + data: _buildChart, ); } @@ -89,7 +89,7 @@ class _Badge extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(10), diff --git a/lib/widgets/sub_zilla_app_bar.dart b/lib/widgets/sub_zilla_app_bar.dart index 3d40ce7..d8adb4d 100644 --- a/lib/widgets/sub_zilla_app_bar.dart +++ b/lib/widgets/sub_zilla_app_bar.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:subs_tracker/layout/root_layout.dart'; +import '../layout/root_layout.dart'; class SubZillaAppBar extends StatelessWidget implements PreferredSizeWidget { const SubZillaAppBar({this.trailing, super.key}); @@ -14,16 +14,15 @@ class SubZillaAppBar extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { return CupertinoNavigationBar( backgroundColor: Theme.of(context).colorScheme.surface, - automaticallyImplyLeading: true, - leading: rootScaffoldKey.currentState?.hasDrawer == true + leading: rootScaffoldKey.currentState?.hasDrawer ?? false ? IconButton( - icon: Icon(CupertinoIcons.bars), + icon: const Icon(CupertinoIcons.bars), onPressed: () { rootScaffoldKey.currentState?.openDrawer(); }, ) : null, - middle: Text("settings.app_name".tr()), + middle: Text('settings.app_name'.tr()), trailing: trailing, ); } diff --git a/pubspec.lock b/pubspec.lock index 0e051b1..754a6bd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,34 +5,42 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d + sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c url: "https://pub.dev" source: hosted - version: "91.0.0" + version: "99.0.0" + analysis_server_plugin: + dependency: transitive + description: + name: analysis_server_plugin + sha256: "3960b28ee740004df39f85d5ebfc91785f7a90e51fd7c9a185e86a36b2f581b4" + url: "https://pub.dev" + source: hosted + version: "0.3.14" analyzer: dependency: transitive description: name: analyzer - sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 + sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7" url: "https://pub.dev" source: hosted - version: "8.4.0" + version: "12.1.0" analyzer_buffer: dependency: transitive description: name: analyzer_buffer - sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033 + sha256: bf559bc54530827a92cc4d9ee340fc76b4f17f386218c2b9e7cd33ed468a7e4d url: "https://pub.dev" source: hosted - version: "0.1.11" + version: "0.3.2" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53" + sha256: "0057a98d64d7bb872b0c87dff6e73d2c2d80c77156e7a03f127a26f8aa240649" url: "https://pub.dev" source: hosted - version: "0.13.10" + version: "0.14.8" archive: dependency: transitive description: @@ -69,10 +77,10 @@ packages: dependency: transitive description: name: build - sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413 + sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10 url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.6" build_config: dependency: transitive description: @@ -93,10 +101,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "7981eb922842c77033026eb4341d5af651562008cdb116bdfa31fc46516b6462" + sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6" url: "https://pub.dev" source: hosted - version: "2.12.2" + version: "2.15.0" built_collection: dependency: transitive description: @@ -129,22 +137,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" - ci: - dependency: transitive - description: - name: ci - sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" - url: "https://pub.dev" - source: hosted - version: "0.1.0" - cli_completion: - dependency: transitive - description: - name: cli_completion - sha256: "158deec74a75cdc69bce061645fea08f94190dd6833f988f517c2dfcb45e9117" - url: "https://pub.dev" - source: hosted - version: "0.5.0" cli_config: dependency: transitive description: @@ -225,38 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" - custom_lint: - dependency: "direct dev" - description: - name: custom_lint - sha256: "751ee9440920f808266c3ec2553420dea56d3c7837dd2d62af76b11be3fcece5" - url: "https://pub.dev" - source: hosted - version: "0.8.1" - custom_lint_core: - dependency: transitive - description: - name: custom_lint_core - sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423" - url: "https://pub.dev" - source: hosted - version: "0.8.1" - custom_lint_visitor: - dependency: transitive - description: - name: custom_lint_visitor - sha256: "91f2a81e9f0abb4b9f3bb529f78b6227ce6050300d1ae5b1e2c69c66c7a566d8" - url: "https://pub.dev" - source: hosted - version: "1.0.0+8.4.0" dart_style: dependency: transitive description: name: dart_style - sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + sha256: a4c1ccfee44c7e75ed80484071a5c142a385345e658fd8bd7c4b5c97e7198f98 url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.8" dbus: dependency: transitive description: @@ -397,10 +365,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9" + sha256: b938f77d042cbcd822936a7a359a7235bad8bd72070de1f827efc2cc297ac888 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" flex_color_scheme: dependency: "direct main" description: @@ -458,34 +426,34 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875" + sha256: "0d9035862236fe38250fe1644d7ed3b8254e34a21b2c837c9f539fbb3bba5ef1" url: "https://pub.dev" source: hosted - version: "19.5.0" + version: "21.0.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5 + sha256: e0f25e243c6c44c825bbbc6b2b2e76f7d9222362adcfe9fd780bf01923c840bd url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "8.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe" + sha256: e7db3d5b49c2b7ecc68deba4aaaa67a348f92ee0fef34c8e4b4459dbef0d7307 url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "11.0.0" flutter_local_notifications_windows: dependency: transitive description: name: flutter_local_notifications_windows - sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf" + sha256: "3a2654ba104fbb52c618ebed9def24ef270228470718c43b3a6afcd5c81bef0c" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "3.0.0" flutter_localizations: dependency: transitive description: flutter @@ -495,10 +463,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02" + sha256: be3aa640f053064e2238f8a308baa5be7270645e8b53b08484fd305bd5c1eb5d url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.3.2-dev.2" flutter_test: dependency: "direct dev" description: flutter @@ -513,10 +481,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" + sha256: "8599ba37236328ff6f97269ecce875d251a367eb2929c9bbf9b811b2ce56c74e" url: "https://pub.dev" source: hosted - version: "3.2.3" + version: "3.2.6-dev.1" freezed_annotation: dependency: "direct main" description: @@ -545,10 +513,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a + sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a" url: "https://pub.dev" source: hosted - version: "17.0.1" + version: "17.3.0" graphs: dependency: transitive description: @@ -561,10 +529,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: b880efcd17757af0aa242e5dceac2fb781a014c22a32435a5daa8f17e9d5d8a9 + sha256: a217bbfaf32e43f925a545ba48470e59de4e7f21cc92920bc7944e4b450c1aba url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.3.2-dev.2" http: dependency: transitive description: @@ -617,18 +585,18 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.12.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 + sha256: ffcd10cde35a93b2abbbcc26bd9971f4ca93763e8abe78d855e3c4177797e501 url: "https://pub.dev" source: hosted - version: "6.11.2" + version: "6.14.0" leak_tracker: dependency: transitive description: @@ -669,14 +637,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - mason_logger: - dependency: transitive - description: - name: mason_logger - sha256: "1fdf5c76870eb6fc3611ed6fbae1973a3794abe581ea5e22e68af2f73c688b93" - url: "https://pub.dev" - source: hosted - version: "0.2.16" matcher: dependency: transitive description: @@ -713,10 +673,10 @@ packages: dependency: transitive description: name: mockito - sha256: dac24d461418d363778d53198d9ac0510b9d073869f078450f195766ec48d05e + sha256: eff30d002f0c8bf073b6f929df4483b543133fcafce056870163587b03f1d422 url: "https://pub.dev" source: hosted - version: "5.6.1" + version: "5.6.4" motor: dependency: "direct main" description: @@ -853,14 +813,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.3" - process: - dependency: transitive - description: - name: process - sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 - url: "https://pub.dev" - source: hosted - version: "5.0.5" pub_semver: dependency: transitive description: @@ -869,14 +821,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - pub_updater: - dependency: transitive - description: - name: pub_updater - sha256: "54e8dc865349059ebe7f163d6acce7c89eb958b8047e6d6e80ce93b13d7c9e60" - url: "https://pub.dev" - source: hosted - version: "0.4.0" pubspec_parse: dependency: transitive description: @@ -885,62 +829,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" - rider: - dependency: "direct dev" - description: - name: rider - sha256: dccd843014332e02b0682147e4228bc0a880cd46c2312bc0976a58bec771de37 - url: "https://pub.dev" - source: hosted - version: "0.0.1" riverpod: dependency: transitive description: name: riverpod - sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a" + sha256: da7233961958420e9d80edf4b7a735d5b6b732fe2381d2a12a388562e2042b3f url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.3.2-dev.2" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0" + sha256: "3e275138862ccc22ed61444a1f9a840f753094c367f28f4123f50289cd204d68" url: "https://pub.dev" source: hosted - version: "1.0.0-dev.8" + version: "1.0.0-dev.10" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: cc1474bc2df55ec3c1da1989d139dcef22cd5e2bd78da382e867a69a8eca2e46 + sha256: b7fec3dcdef4cc724116b9cad9fd1ca90fb241083cc16e9cf42a85256be7e87e url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.3-dev.2" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: e43b1537229cc8f487f09b0c20d15dba840acbadcf5fc6dad7ad5e8ab75950dc + sha256: "2ba125d0f0ece0b7f2a549613803ea1d4a5e1c8b887588223b19ed8671237504" url: "https://pub.dev" source: hosted - version: "4.0.0+1" - riverpod_sqflite: - dependency: "direct main" + version: "4.0.4-dev.3" + riverpod_lint: + dependency: "direct dev" description: - name: riverpod_sqflite - sha256: "51c89972d68ecb235c276278c084ac8621f173ec9d80d6bbf77bd2b6236bad9f" + name: riverpod_lint + sha256: "4ef15442a9f2254ed6ee59a48db84c2c784580ef804061443861b7e0360ec106" url: "https://pub.dev" source: hosted - version: "0.4.0" - rxdart: - dependency: transitive + version: "3.1.4-dev.3" + riverpod_sqflite: + dependency: "direct main" description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + name: riverpod_sqflite + sha256: "21905ab9ebfb02ec4f581ba28e26cc4b451c4195278c1427bb5379fb6e0b0bf1" url: "https://pub.dev" source: hosted - version: "0.28.0" + version: "0.4.3-dev.2" share_plus: dependency: "direct main" description: @@ -1069,18 +1005,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75" + sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.2.3" source_helper: dependency: transitive description: name: source_helper - sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" + sha256: "4227d54ceefd0bb8ca4c8fcb96e1719dc53f1ee1b6e2ca9d7a6069da160e4eae" url: "https://pub.dev" source: hosted - version: "1.3.8" + version: "1.3.12" source_map_stack_trace: dependency: transitive description: @@ -1109,42 +1045,42 @@ packages: dependency: "direct main" description: name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + sha256: "58a799e6ac17dd32fbab93813d39ed835a75ccc0f8f85b8955fe318c6712b082" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" sqflite_android: dependency: transitive description: name: sqflite_android - sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + sha256: d0548f9d7422a2dae99ec6f8b0a3074463b132d216fa5ba0d230eeefc901983b url: "https://pub.dev" source: hosted - version: "2.4.2+2" + version: "2.4.3" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + sha256: cce558075afe2a83f3fd7fc123acd6b090683e4f23910d44fbb31ecd7800b014 url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.5.9" sqflite_darwin: dependency: transitive description: name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + sha256: "164a5d73ab87a134566057219988bafde837029a64264e61f1f04376ef3cfcd2" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" sqflite_platform_interface: dependency: transitive description: name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + sha256: f84939f84350d92d04416f8bc4dc52d3896aec7716cc9e80cf0146342139dc50 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" stack_trace: dependency: transitive description: @@ -1189,10 +1125,10 @@ packages: dependency: transitive description: name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" table_calendar: dependency: "direct main" description: @@ -1237,10 +1173,10 @@ packages: dependency: "direct main" description: name: timezone - sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 + sha256: "784a5e34d2eb62e1326f24d6f600aaaee452eb8ca8ef2f384a59244e292d158b" url: "https://pub.dev" source: hosted - version: "0.10.1" + version: "0.11.0" typed_data: dependency: transitive description: @@ -1378,7 +1314,7 @@ packages: source: hosted version: "1.2.1" win32: - dependency: "direct overridden" + dependency: transitive description: name: win32 sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 @@ -1409,6 +1345,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488" + url: "https://pub.dev" + source: hosted + version: "2.2.4" sdks: dart: ">=3.12.0 <4.0.0" flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 18f206b..f538c66 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: subs_tracker description: "SubZilla is a powerful and intuitive Flutter application designed to help you manage your monthly subscriptions, track recurring expenses, and stay in control of your budget." publish_to: 'none' -version: 0.0.3+3 +version: 0.0.4+4 environment: sdk: ^3.10.7 @@ -14,60 +14,52 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + crypto: ^3.0.7 + cupertino_icons: ^1.0.9 + easy_localization: ^3.0.8 + file_selector: ^1.1.0 + fl_chart: ^1.2.0 + flex_color_scheme: ^8.4.0 flutter: sdk: flutter - - cupertino_icons: ^1.0.9 - package_info_plus: ^10.1.0 + flutter_colorpicker: ^1.1.0 + flutter_hooks: ^0.21.3+1 flutter_launcher_icons: ^0.14.4 - fl_chart: ^1.1.1 - flutter_riverpod: ^3.1.0 - url_launcher: ^6.3.2 - flex_color_scheme: ^8.3.1 + flutter_local_notifications: ^21.0.0 + flutter_riverpod: ^3.3.1 freezed_annotation: ^3.1.0 - json_annotation: ^4.9.0 - hooks_riverpod: ^3.1.0 - flutter_hooks: ^0.21.3+1 - riverpod_annotation: ^4.0.0 - riverpod_sqflite: ^0.4.0 - sqflite: ^2.4.2 - path: ^1.9.1 - flutter_colorpicker: ^1.0.3 - flutter_local_notifications: ^19.5.0 - timezone: ^0.10.1 - go_router: ^17.0.1 - table_calendar: ^3.2.0 + go_router: ^17.3.0 + hooks_riverpod: ^3.3.1 intl: ^0.20.2 + json_annotation: ^4.12.0 + motor: ^1.1.0 + package_info_plus: ^10.1.0 + path: ^1.9.1 path_provider: ^2.1.5 - file_selector: ^1.1.0 + riverpod_annotation: ^4.0.2 + riverpod_sqflite: ^0.4.2 share_plus: ^13.1.0 - crypto: ^3.0.7 shared_preferences: ^2.5.5 - easy_localization: ^3.0.8 simple_icons: ^14.6.1 - motor: ^1.1.0 + sqflite: ^2.4.3 + table_calendar: ^3.2.0 + timezone: ^0.11.0 + url_launcher: ^6.3.2 dependency_overrides: simple_icons: path: ./packages/simple_icons - win32: ^6.0.1 dev_dependencies: + build_runner: ^2.15.0 + flutter_lints: ^6.0.0 flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^6.0.0 - build_runner: ^2.12.2 freezed: ^3.2.3 - json_serializable: ^6.11.2 - custom_lint: ^0.8.0 - riverpod_generator: ^4.0.0+1 - rider: ^0.0.1 + json_serializable: ^6.14.0 + riverpod_generator: ^4.0.3 + riverpod_lint: 3.1.4-dev.3 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/models/sub_slice_test.dart b/test/models/sub_slice_test.dart index 6d650b6..06800c7 100644 --- a/test/models/sub_slice_test.dart +++ b/test/models/sub_slice_test.dart @@ -31,7 +31,6 @@ void main() { amount: 10, color: 0, startDate: DateTime.now(), - frequency: Frequency.monthly, ); expect(slice.monthlyAmount, 10); }); diff --git a/test/providers/subs_controller_test.dart b/test/providers/subs_controller_test.dart index 37264b4..5b7114d 100644 --- a/test/providers/subs_controller_test.dart +++ b/test/providers/subs_controller_test.dart @@ -19,12 +19,12 @@ void main() { setUp(() { // Mock Notification Channel to prevent crash - const MethodChannel notificationChannel = + const notificationChannel = MethodChannel('dexterous.com/flutter/local_notifications'); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( notificationChannel, - (MethodCall methodCall) async { + (methodCall) async { return null; }, ); @@ -33,7 +33,7 @@ void main() { test('SubsController adds a slice and updates state', () async { final container = ProviderContainer( overrides: [ - subsControllerProvider.overrideWith(() => TestSubsController()), + subsControllerProvider.overrideWith(TestSubsController.new), ], ); @@ -44,10 +44,9 @@ void main() { // Add a slice final slice = SubSlice( name: 'Netflix', - amount: 15.0, + amount: 15, color: 0xFF000000, startDate: DateTime.now(), - frequency: Frequency.monthly, ); container.read(subsControllerProvider.notifier).addSlice(slice); @@ -62,13 +61,13 @@ void main() { test('SubsController removes a slice', () async { final container = ProviderContainer( overrides: [ - subsControllerProvider.overrideWith(() => TestSubsController()), + subsControllerProvider.overrideWith(TestSubsController.new), ], ); final slice = SubSlice( name: 'Spotify', - amount: 10.0, + amount: 10, color: 0xFF000000, startDate: DateTime.now(), ); diff --git a/test/screens/calendar_screen_test.dart b/test/screens/calendar_screen_test.dart index e287ebf..dce5736 100644 --- a/test/screens/calendar_screen_test.dart +++ b/test/screens/calendar_screen_test.dart @@ -13,21 +13,20 @@ class MockSubsController extends SubsController { return [ SubSlice( name: 'Netflix', - amount: 15.0, + amount: 15, color: 0xFF000000, startDate: DateTime.now(), - frequency: Frequency.monthly, ), ]; } } void main() { - testWidgets('CalendarScreen renders and shows events', (WidgetTester tester) async { + testWidgets('CalendarScreen renders and shows events', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ - subsControllerProvider.overrideWith(() => MockSubsController()), + subsControllerProvider.overrideWith(MockSubsController.new), ], child: const MaterialApp( home: CalendarScreen(), diff --git a/test/widget_test.dart b/test/widget_test.dart index 6c12816..2042e58 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -17,14 +17,14 @@ import 'package:subs_tracker/providers/settings_controller.dart'; import 'package:subs_tracker/providers/subs_controller.dart'; void main() { - testWidgets('App smoke test', (WidgetTester tester) async { + testWidgets('App smoke test', (tester) async { // Override the settings and subs controllers to return a fixed state // This avoids database calls and platform channel issues await tester.pumpWidget( ProviderScope( overrides: [ - settingsControllerProvider.overrideWith(() => FakeSettingsController()), - subsControllerProvider.overrideWith(() => FakeSubsController()), + settingsControllerProvider.overrideWith(FakeSettingsController.new), + subsControllerProvider.overrideWith(FakeSubsController.new), ], child: const MyApp(), ), diff --git a/test/widgets/add_subs_dialog_test.dart b/test/widgets/add_subs_dialog_test.dart index cc17144..ecec513 100644 --- a/test/widgets/add_subs_dialog_test.dart +++ b/test/widgets/add_subs_dialog_test.dart @@ -12,16 +12,14 @@ class FakeBrands extends Brands { @override Future> build() async { return [ - const Brand(text: 'Netflix', logo: null), - const Brand(text: 'Spotify', logo: null), + const Brand(text: 'Netflix'), + const Brand(text: 'Spotify'), ]; } } // Mock SubsController class MockSubsController extends SubsController { - List addedSlices = []; - @override Future> build() async { return []; @@ -29,18 +27,18 @@ class MockSubsController extends SubsController { @override void addSlice(SubSlice slice) { - addedSlices.add(slice); + state = AsyncData([...(state.value ?? []), slice]); } } void main() { - testWidgets('AddSubsDialog allows adding a subscription', (WidgetTester tester) async { + testWidgets('AddSubsDialog allows adding a subscription', (tester) async { final mockSubsController = MockSubsController(); await tester.pumpWidget( ProviderScope( overrides: [ - brandsProvider.overrideWith(() => FakeBrands()), + brandsProvider.overrideWith(FakeBrands.new), subsControllerProvider.overrideWith(() => mockSubsController), ], child: const MaterialApp( @@ -82,9 +80,9 @@ void main() { await tester.pump(); // Verify slice was added - expect(mockSubsController.addedSlices, hasLength(1)); - expect(mockSubsController.addedSlices.first.name, 'My Sub'); - expect(mockSubsController.addedSlices.first.amount, 10.0); - expect(mockSubsController.addedSlices.first.frequency, Frequency.weekly); + expect(mockSubsController.state.value, hasLength(1)); + expect(mockSubsController.state.value!.first.name, 'My Sub'); + expect(mockSubsController.state.value!.first.amount, 10.0); + expect(mockSubsController.state.value!.first.frequency, Frequency.weekly); }); } diff --git a/test/widgets/color_scheme_settings_test.dart b/test/widgets/color_scheme_settings_test.dart index 0d8c8f8..5900cb6 100644 --- a/test/widgets/color_scheme_settings_test.dart +++ b/test/widgets/color_scheme_settings_test.dart @@ -6,7 +6,7 @@ import 'package:subs_tracker/models/settings_view_model.dart'; void main() { group('SettingsViewModel colorScheme', () { test('serializes FlexScheme by name string', () { - final vm = SettingsViewModel( + const vm = SettingsViewModel( theme: ThemeMode.light, currency: Currency.usd, colorScheme: FlexScheme.deepPurple, diff --git a/test/widgets/intro_page_test.dart b/test/widgets/intro_page_test.dart index d78dd9d..b212fb3 100644 --- a/test/widgets/intro_page_test.dart +++ b/test/widgets/intro_page_test.dart @@ -4,7 +4,7 @@ import 'package:subs_tracker/screens/intro_page.dart'; void main() { testWidgets('IntroPage calls onGetStarted when CTA tapped', (tester) async { - bool called = false; + var called = false; await tester.pumpWidget( MaterialApp( home: Scaffold( From 5649a6bb49e395754342db0d3301e58c17e4822e Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Sun, 7 Jun 2026 17:20:46 +0300 Subject: [PATCH 02/15] feat: Ios release and liquid glass ui updates with new barchart --- ...-40Z__lib-screens-analytics-screen-dart.md | 76 +++++++++ .metadata | 25 +-- assets/translations/en.json | 9 +- assets/translations/tr.json | 9 +- ios/Runner.xcodeproj/project.pbxproj | 40 +++-- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../contents.xcworkspacedata | 3 - ios/Runner/AppDelegate.swift | 17 +- ios/Runner/Info.plist | 113 ++++++++----- ios/Runner/SceneDelegate.swift | 6 + lib/layout/root_layout.dart | 3 + lib/providers/settings_controller.g.dart | 2 +- lib/screens/analytics_screen.dart | 76 ++++++++- lib/screens/calendar_screen.dart | 23 ++- lib/screens/home_screen.dart | 5 +- lib/screens/settings_screen.dart | 4 +- lib/widgets/bar_chart.dart | 157 ++++++++++++++++++ lib/widgets/brand_logo.dart | 1 + lib/widgets/glass_nav_bar.dart | 31 ++-- lib/widgets/pie_chart.dart | 1 - pubspec.lock | 144 +++++++++++----- pubspec.yaml | 1 + 22 files changed, 577 insertions(+), 171 deletions(-) create mode 100644 .impeccable/critique/2026-06-07T13-10-40Z__lib-screens-analytics-screen-dart.md create mode 100644 ios/Runner/SceneDelegate.swift create mode 100644 lib/widgets/bar_chart.dart diff --git a/.impeccable/critique/2026-06-07T13-10-40Z__lib-screens-analytics-screen-dart.md b/.impeccable/critique/2026-06-07T13-10-40Z__lib-screens-analytics-screen-dart.md new file mode 100644 index 0000000..3fcc978 --- /dev/null +++ b/.impeccable/critique/2026-06-07T13-10-40Z__lib-screens-analytics-screen-dart.md @@ -0,0 +1,76 @@ +--- +target: lib/screens/analytics_screen.dart +total_score: 23 +p0_count: 0 +p1_count: 2 +timestamp: 2026-06-07T13-10-40Z +slug: lib-screens-analytics-screen-dart +--- +## Analytics Screen — Design Critique + +### Design Health Score + +| # | Heuristic | Score | Key Issue | +|---|-----------|-------|-----------| +| 1 | Visibility of System Status | 3 | Loading states present; chart doesn't visually react to period change | +| 2 | Match System / Real World | 2 | Period toggle changes summary totals but leaves chart data untouched — broken contract | +| 3 | User Control and Freedom | 3 | All toggles freely reversible; no traps | +| 4 | Consistency and Standards | 2 | Period applies to summary card but not to SubsBar/SubsPie — same control, inconsistent scope | +| 5 | Error Prevention | 3 | Few error vectors; empty state handled; no destructive inputs | +| 6 | Recognition Rather Than Recall | 2 | `$`/`%` symbols require recall; no label or tooltip | +| 7 | Flexibility and Efficiency | 2 | 3 period presets, 2 chart modes; no sort, filter, or export | +| 8 | Aesthetic and Minimalist Design | 2 | Chart container double-boxed; N-identical breakdown cards | +| 9 | Error Recovery | 3 | Generic error string present; graceful empty state | +| 10 | Help and Documentation | 1 | Zero contextual help; no tooltips; no explanation of toggle scope | +| **Total** | | **23/40** | **Acceptable — significant improvements needed** | + +### Anti-Patterns Verdict + +Deterministic scan: clean (0 findings). No slop patterns detected. Design is coherent — coral accent restrained, border-radius 12px correct, no gradient text, no side-stripe borders. + +### Overall Impression + +Structurally sound but analytically misleading. The period toggle is the most prominent control and it only does half its job — charts always show monthly data regardless of period selected. That's the single biggest trust problem. Everything else is refinement. + +### What's Working + +1. Color-coded breakdown items — meaningful color, not decoration; semanticsLabel on progress bars is a deliberate accessibility choice. +2. Summary card data density — compact two-column layout with coral accent on total is clean and effective. +3. SegmentedButton reuse — consistent component for both controls; users learn it once. + +### Priority Issues + +**[P1] Period filter doesn't apply to chart data** +SubsBar and SubsPie always render `monthlyAmount` with no multiplier. Summary card says "Total Yearly: $1,200." Chart still shows monthly bars. Creates a misleading mental model. Fix: pass multiplier to chart widgets as a constructor parameter. + +**[P1] Chart section has no contextual label** +Chart container begins directly with SegmentedButton — no heading, no sub-title. Users don't know what temporal scope the chart represents. Fix: add a text heading or subtitle tying the chart to the active period. + +**[P2] `$`/`%` chart toggle labels too terse** +Symbols tell you the unit but not the chart type. First-time users have no way to infer `$` = bar chart, `%` = pie chart. Fix: use "Amount" / "Share" labels, or add icons. + +**[P2] N-identical breakdown cards, no visual rhythm** +ListView emits N copies of the same structure. On 5+ subscriptions this creates visual fatigue. Fix: differentiate the top item; add grouping by price tier or size variation. + +**[P3] Chart container double-boxes the chart widget** +Outer Container with surfaceContainer fill + 16px padding wraps SubsBar which already has its own data area. Fix: remove container fill color or reduce padding. + +### Persona Red Flags + +**Alex (Power User):** Selects "Year", summary updates, chart doesn't change → trust erosion. No sort, filter, date range, or export. Two controls total for an analytics screen. + +**Sam (Accessibility-Dependent):** SegmentedButton and LinearProgressIndicator semantics are good. fl_chart widgets expose no semantic descriptions of chart data — screen reader users hear nothing on the chart region. + +**Casey (Distracted Mobile User):** No loading skeleton — full-screen spinner on cold start. Period toggles sit above fold (good). State loss on backgrounding if period is non-default. + +### Minor Observations + +- `IntrinsicHeight` in `_SummaryCard` triggers double layout pass — cheaper to use fixed-height column children. +- `SubsPie` appends `SizedBox(height: 12)` inside already-padded chart container — ~28px dead space. +- `_coral` in `_SummaryCard` hardcodes `Color(0xFFD83434)` — same value as `kCoralAccent` in `app_theme.dart`; single source preferred. + +### Questions to Consider + +- "What does 'analytics' mean to a user who's tracked subscriptions for 6 months — 'what am I spending?' or 'what changed?'" +- "The period selector and chart mode toggle are the only two interactions — if you had to cut one, which gives more value?" +- "What if the chart were removed and the breakdown list did more heavy lifting — larger numbers, trend arrows, category grouping?" diff --git a/.metadata b/.metadata index af5cda8..344170c 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "edada7c56edf4a183c1735310e123c7f923584f1" + revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42" channel: "stable" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - - platform: android - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 - platform: ios - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - - platform: linux - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - - platform: macos - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - - platform: web - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - - platform: windows - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 # User provided section diff --git a/assets/translations/en.json b/assets/translations/en.json index c26980d..5c21b70 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -33,7 +33,9 @@ "renews_today_alert_subtitle": "{} subscription(s) renew today", "renews_today_badge": "Today", "sort_soonest": "Soonest", - "sort_default": "Default" + "sort_default": "Default", + "renewing_soon": "Renewing soon", + "tomorrow": "Tomorrow" }, "frequency": { "daily": "Daily", @@ -165,7 +167,10 @@ "percent_total": "% of total", "period_month": "Month", "period_quarter": "3 Months", - "period_year": "Year" + "period_year": "Year", + "chart_title": "Spend Overview", + "chart_mode_amount": "Amount", + "chart_mode_share": "Share" }, "frequency_names": { "daily": "DAILY", diff --git a/assets/translations/tr.json b/assets/translations/tr.json index 6b7c5b8..2385a49 100644 --- a/assets/translations/tr.json +++ b/assets/translations/tr.json @@ -33,7 +33,9 @@ "renews_today_alert_subtitle": "{} abonelik bugün yenileniyor", "renews_today_badge": "Bugün", "sort_soonest": "En yakın", - "sort_default": "Varsayılan" + "sort_default": "Varsayılan", + "renewing_soon": "Yakında yenileniyor", + "tomorrow": "Yarın" }, "frequency": { "daily": "Günlük", @@ -165,7 +167,10 @@ "percent_total": "toplamın %", "period_month": "Ay", "period_quarter": "3 Ay", - "period_year": "Yıl" + "period_year": "Yıl", + "chart_title": "Harcama Özeti", + "chart_mode_amount": "Tutar", + "chart_mode_share": "Pay" }, "frequency_names": { "daily": "GÜNLÜK", diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f6e6c15..ff2be36 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -48,6 +49,7 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -68,13 +70,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F765556E570AEFF79098AFFA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -127,6 +122,7 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; @@ -141,7 +137,6 @@ buildPhases = ( 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, - F765556E570AEFF79098AFFA /* Frameworks */, ); buildRules = ( ); @@ -288,6 +283,7 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -345,14 +341,16 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 6NLKUNN3FH; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -367,6 +365,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -379,6 +378,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 6NLKUNN3FH; ENABLE_BITCODE = NO; @@ -389,6 +390,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.devopen.subzilla; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -446,7 +448,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -466,14 +468,16 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 6NLKUNN3FH; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -495,6 +499,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -503,7 +508,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -523,14 +528,16 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 6NLKUNN3FH; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -545,6 +552,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -559,6 +567,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 6NLKUNN3FH; ENABLE_BITCODE = NO; @@ -569,6 +579,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.devopen.subzilla; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -582,6 +593,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 6NLKUNN3FH; ENABLE_BITCODE = NO; @@ -592,6 +605,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.devopen.subzilla; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c3fedb2..95d6e55 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 21a3cc1..1d526a1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,7 +4,4 @@ - - diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 769c8a9..c30b367 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,21 +1,16 @@ import Flutter import UIKit -import flutter_local_notifications @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in - GeneratedPluginRegistrant.register(with: registry) - } - GeneratedPluginRegistrant.register(with: self) - - if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate - } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } -} \ No newline at end of file + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e965447..bfa623a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,49 +1,70 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - SubZilla - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - SubZilla - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - \ No newline at end of file + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Subzilla + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + subzilla + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/lib/layout/root_layout.dart b/lib/layout/root_layout.dart index 5307388..50d8d61 100644 --- a/lib/layout/root_layout.dart +++ b/lib/layout/root_layout.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; + import '../config/router_config.dart'; import '../widgets/glass_nav_bar.dart'; import '../widgets/menu_bar.dart'; @@ -30,6 +31,8 @@ class RootLayout extends ConsumerWidget { return Scaffold( key: rootScaffoldKey, + extendBody: true, + resizeToAvoidBottomInset: false, backgroundColor: Theme.of(context).scaffoldBackgroundColor, drawer: const SidebarMenu(), body: child, diff --git a/lib/providers/settings_controller.g.dart b/lib/providers/settings_controller.g.dart index 1fe612b..4528d5a 100644 --- a/lib/providers/settings_controller.g.dart +++ b/lib/providers/settings_controller.g.dart @@ -77,7 +77,7 @@ final class SettingsControllerProvider } String _$settingsControllerHash() => - r'e2e76aed054ef57d469792f60299e181a62457fa'; + r'88fdadd65e77b9da23bbee69fdae4241afe4607c'; @JsonPersist() abstract class _$SettingsControllerBase diff --git a/lib/screens/analytics_screen.dart b/lib/screens/analytics_screen.dart index 5218445..c4f37da 100644 --- a/lib/screens/analytics_screen.dart +++ b/lib/screens/analytics_screen.dart @@ -2,15 +2,20 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; + import '../models/settings_view_model.dart'; import '../models/sub_slice.dart'; import '../providers/settings_controller.dart'; import '../providers/subs_controller.dart'; +import '../utils/app_theme.dart'; +import '../widgets/bar_chart.dart'; import '../widgets/pie_chart.dart'; import '../widgets/sub_zilla_app_bar.dart'; enum _Period { month, quarter, year } +enum _ChartMode { price, percentage } + extension _PeriodX on _Period { String get labelKey => switch (this) { _Period.month => 'analytics.period_month', @@ -39,12 +44,13 @@ class AnalyticsScreen extends HookConsumerWidget { final slicesAsync = ref.watch(subsControllerProvider); final settingsAsync = ref.watch(settingsControllerProvider); final period = useState(_Period.month); + final chartMode = useState(_ChartMode.price); return Scaffold( appBar: const SubZillaAppBar(), body: slicesAsync.when( data: (slices) => settingsAsync.when( - data: (settings) => _buildBody(slices, settings, context, period), + data: (settings) => _buildBody(slices, settings, context, period, chartMode), loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Center(child: Text('common.error_generic'.tr())), @@ -61,6 +67,7 @@ class AnalyticsScreen extends HookConsumerWidget { SettingsViewModel settings, BuildContext context, ValueNotifier<_Period> period, + ValueNotifier<_ChartMode> chartMode, ) { if (slices.isEmpty) { return Center(child: Text('analytics.no_data'.tr())); @@ -73,8 +80,9 @@ class AnalyticsScreen extends HookConsumerWidget { sorted.fold(0, (a, b) => a + b.monthlyAmount) * multiplier; return SafeArea( + bottom:false, child: SingleChildScrollView( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.fromLTRB(16, 16, 16, 16 + MediaQuery.paddingOf(context).bottom), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -109,7 +117,61 @@ class AnalyticsScreen extends HookConsumerWidget { borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.all(16), - child: const SubsPie(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'analytics.chart_title'.tr(), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + if (chartMode.value == _ChartMode.price) + Text( + period.value.totalLabelKey.tr(), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: SegmentedButton<_ChartMode>( + segments: [ + ButtonSegment( + value: _ChartMode.price, + label: Text('analytics.chart_mode_amount'.tr()), + ), + ButtonSegment( + value: _ChartMode.percentage, + label: Text('analytics.chart_mode_share'.tr()), + ), + ], + selected: {chartMode.value}, + onSelectionChanged: (s) => chartMode.value = s.first, + ), + ), + const SizedBox(height: 12), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: chartMode.value == _ChartMode.price + ? SubsBar( + key: const ValueKey(_ChartMode.price), + multiplier: period.value.multiplier, + ) + : const SubsPie(key: ValueKey(_ChartMode.percentage)), + ), + ], + ), ), const SizedBox(height: 32), Text( @@ -156,8 +218,6 @@ class _SummaryCard extends StatelessWidget { final String currency; final String totalLabel; - static const _coral = Color(0xFFD83434); - @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; @@ -190,7 +250,7 @@ class _SummaryCard extends StatelessWidget { '$currency${total.toStringAsFixed(2)}', style: textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, - color: _coral, + color: kCoralAccent, ), ), ], @@ -283,8 +343,8 @@ class _BreakdownItem extends StatelessWidget { child: Text( '#$rank', style: textTheme.bodySmall?.copyWith( - color: scheme.onSurfaceVariant, - fontWeight: FontWeight.w500, + color: rank == 1 ? kCoralAccent : scheme.onSurfaceVariant, + fontWeight: rank == 1 ? FontWeight.w700 : FontWeight.w500, ), textAlign: TextAlign.right, ), diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart index 3a91e71..4410197 100644 --- a/lib/screens/calendar_screen.dart +++ b/lib/screens/calendar_screen.dart @@ -68,12 +68,27 @@ class CalendarScreen extends HookConsumerWidget { focusedDay.value = focused; }, eventLoader: (day) => getEventsForDay(day, subs), + calendarBuilders: CalendarBuilders( + markerBuilder: (context, day, events) { + if (events.isEmpty) return null; + return Row( + mainAxisSize: MainAxisSize.min, + children: events.take(4).map((sub) { + return Container( + width: 6, + height: 6, + margin: const EdgeInsets.symmetric(horizontal: 1), + decoration: BoxDecoration( + color: Color(sub.color), + shape: BoxShape.circle, + ), + ); + }).toList(), + ); + }, + ), calendarStyle: CalendarStyle( cellMargin: const EdgeInsets.all(8), - markerDecoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - shape: BoxShape.circle, - ), todayDecoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), shape: BoxShape.circle, diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 0faa8e1..55bd497 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -20,6 +20,8 @@ class HomeScreen extends HookConsumerWidget { final settingsAsync = ref.watch(settingsControllerProvider); return Scaffold( + extendBody: true, + resizeToAvoidBottomInset: false, appBar: SubZillaAppBar( trailing: IconButton( icon: const Icon(Icons.add), @@ -84,6 +86,7 @@ class HomeScreen extends HookConsumerWidget { final mostExpensive = sortedSlices.first; return SafeArea( + bottom: false, child: Column( children: [ // Summary Card @@ -98,7 +101,7 @@ class HomeScreen extends HookConsumerWidget { Flexible( fit: FlexFit.tight, child: ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: EdgeInsets.fromLTRB(12, 0, 12, MediaQuery.paddingOf(context).bottom), itemCount: slices.length, itemBuilder: (context, index) { final slice = slices[index]; diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index c825368..b28b978 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; + import '../models/settings_view_model.dart'; import '../providers/settings_controller.dart'; import '../utils/app_theme.dart'; @@ -32,8 +33,9 @@ class SettingsScreen extends HookConsumerWidget { WidgetRef ref, ) { return SafeArea( + bottom: false, child: ListView( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.fromLTRB(16, 16, 16, 16 + MediaQuery.paddingOf(context).bottom), children: [ Text( 'settings.title', diff --git a/lib/widgets/bar_chart.dart b/lib/widgets/bar_chart.dart new file mode 100644 index 0000000..f164165 --- /dev/null +++ b/lib/widgets/bar_chart.dart @@ -0,0 +1,157 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/brand.dart'; +import '../models/sub_slice.dart'; +import '../providers/settings_controller.dart'; +import '../providers/subs_controller.dart'; +import 'brand_logo.dart'; + +class SubsBar extends ConsumerStatefulWidget { + const SubsBar({super.key, this.multiplier = 1.0}); + + final double multiplier; + + @override + ConsumerState createState() => _SubsBarState(); +} + +class _SubsBarState extends ConsumerState { + int? touchedIndex; + + @override + Widget build(BuildContext context) { + final slicesAsync = ref.watch(subsControllerProvider); + final settingsAsync = ref.watch(settingsControllerProvider); + + return slicesAsync.when( + error: (e, st) => const SizedBox.shrink(), + loading: () => const Center(child: CircularProgressIndicator.adaptive()), + data: (slices) => settingsAsync.when( + error: (e, st) => const SizedBox.shrink(), + loading: () => const Center(child: CircularProgressIndicator.adaptive()), + data: (settings) => _buildChart(slices, settings.currency.symbol, widget.multiplier), + ), + ); + } + + Widget _buildChart(List slices, String currency, double multiplier) { + if (slices.isEmpty) return const SizedBox.shrink(); + + final maxY = slices.fold(0, (a, b) => a > b.monthlyAmount ? a : b.monthlyAmount) * multiplier; + + return AspectRatio( + aspectRatio: 1.2, + child: BarChart( + BarChartData( + maxY: maxY * 1.25, + barGroups: List.generate(slices.length, (i) { + final s = slices[i]; + final isTouched = i == touchedIndex; + return BarChartGroupData( + x: i, + barRods: [ + BarChartRodData( + toY: s.monthlyAmount * multiplier, + color: Color(s.color), + width: isTouched ? 22 : 16, + borderRadius: BorderRadius.circular(5), + ), + ], + showingTooltipIndicators: isTouched ? [0] : [], + ); + }), + barTouchData: BarTouchData( + touchCallback: (event, response) { + setState(() { + touchedIndex = response?.spot?.touchedBarGroupIndex; + }); + }, + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (group) => Colors.black87, + getTooltipItem: (group, groupIndex, rod, rodIndex) { + return BarTooltipItem( + '$currency${rod.toY.toStringAsFixed(2)}', + const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ); + }, + ), + ), + titlesData: FlTitlesData( + topTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 52, + getTitlesWidget: (value, meta) { + if (value == meta.max || value == 0) return const SizedBox.shrink(); + return Text( + '$currency${value.toStringAsFixed(0)}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.right, + ); + }, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 44, + getTitlesWidget: (value, meta) { + final i = value.toInt(); + if (i < 0 || i >= slices.length) return const SizedBox.shrink(); + final s = slices[i]; + return Padding( + padding: const EdgeInsets.only(top: 4), + child: _BarBadge( + name: s.name, + brand: s.brand, + color: Color(s.color), + ), + ); + }, + ), + ), + ), + gridData: FlGridData( + drawVerticalLine: false, + getDrawingHorizontalLine: (value) => FlLine( + color: Theme.of(context).colorScheme.outlineVariant.withValues(alpha: 0.5), + strokeWidth: 1, + ), + ), + borderData: FlBorderData(show: false), + ), + ), + ); + } +} + +class _BarBadge extends StatelessWidget { + const _BarBadge({required this.name, required this.color, this.brand}); + final String name; + final Color color; + final Brand? brand; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color, width: 1.5), + ), + child: Padding( + padding: const EdgeInsets.all(3), + child: SubLeadingIcon(name: name, brand: brand, color: color, size: 18), + ), + ); + } +} diff --git a/lib/widgets/brand_logo.dart b/lib/widgets/brand_logo.dart index 1a190c4..f8a42ee 100644 --- a/lib/widgets/brand_logo.dart +++ b/lib/widgets/brand_logo.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import '../models/brand.dart'; import '../utils/brand_utils.dart'; diff --git a/lib/widgets/glass_nav_bar.dart b/lib/widgets/glass_nav_bar.dart index e6ca3a5..a7a13d2 100644 --- a/lib/widgets/glass_nav_bar.dart +++ b/lib/widgets/glass_nav_bar.dart @@ -1,6 +1,5 @@ -import 'dart:ui'; - import 'package:easy_localization/easy_localization.dart'; +import 'package:liquid_glass_renderer/liquid_glass_renderer.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:motor/motor.dart'; @@ -72,21 +71,20 @@ class _GlassNavBarState extends State final bottomPadding = MediaQuery.of(context).padding.bottom; return Container( - margin: EdgeInsets.only(left: 16, right: 16, bottom: 16 + bottomPadding), - child: ClipRRect( - borderRadius: BorderRadius.circular(28), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), - child: DecoratedBox( - decoration: BoxDecoration( - color: cs.surfaceContainerHighest.withValues(alpha: 0.75), - borderRadius: BorderRadius.circular(28), - border: Border.all( - color: cs.outline.withValues(alpha: 0.2), - ), - ), - child: LayoutBuilder( + child: LiquidGlassLayer( + settings: const LiquidGlassSettings( + blur: 18, + glassColor: Color.fromARGB(20, 255, 255, 255), + thickness: 32, + refractiveIndex: 1.45, + lightIntensity: 0.92, + ambientStrength: 0.25, + saturation: 1.9, + ), + child: LiquidGlass( + shape: const LiquidRoundedSuperellipse(borderRadius: 28), + child: LayoutBuilder( builder: (context, constraints) { final tabWidth = constraints.maxWidth / _tabs.length; return AnimatedBuilder( @@ -168,7 +166,6 @@ class _GlassNavBarState extends State ), ), ), - ), ); } } diff --git a/lib/widgets/pie_chart.dart b/lib/widgets/pie_chart.dart index 7b72e8e..99d6bfe 100644 --- a/lib/widgets/pie_chart.dart +++ b/lib/widgets/pie_chart.dart @@ -75,7 +75,6 @@ class _SubsPieState extends ConsumerState { duration: const Duration(milliseconds: 100), ), ), - const SizedBox(height: 12), ], ); } diff --git a/pubspec.lock b/pubspec.lock index 754a6bd..be53a57 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.9" args: dependency: transitive description: @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.13.1" boolean_selector: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: built_value - sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" url: "https://pub.dev" source: hosted - version: "8.12.1" + version: "8.12.6" characters: dependency: transitive description: @@ -161,14 +161,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.dev" + source: hosted + version: "1.2.1" code_builder: dependency: transitive description: name: code_builder - sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.11.1" collection: dependency: transitive description: @@ -229,10 +237,10 @@ packages: dependency: transitive description: name: dbus - sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.14" easy_localization: dependency: "direct main" description: @@ -253,10 +261,10 @@ packages: dependency: transitive description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" fake_async: dependency: transitive description: @@ -467,6 +475,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.2-dev.2" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_test: dependency: "direct dev" description: flutter @@ -525,6 +541,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hooks: + dependency: transitive + description: + name: hooks + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + url: "https://pub.dev" + source: hosted + version: "2.0.2" hooks_riverpod: dependency: "direct main" description: @@ -561,10 +585,10 @@ packages: dependency: transitive description: name: image - sha256: "48c11d0943b93b6fb29103d956ff89aafeae48f6058a3939649be2093dcff0bf" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.dev" source: hosted - version: "4.7.1" + version: "4.8.0" intl: dependency: "direct main" description: @@ -581,6 +605,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" json_annotation: dependency: "direct main" description: @@ -625,10 +665,18 @@ packages: dependency: transitive description: name: lints - sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.0" + liquid_glass_renderer: + dependency: "direct main" + description: + name: liquid_glass_renderer + sha256: "789be157494b38cdef7607bb9b63e7fe3fab76fae16149e52fa313c2d858cb3f" + url: "https://pub.dev" + source: hosted + version: "0.2.0-dev.4" logging: dependency: transitive description: @@ -693,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + url: "https://pub.dev" + source: hosted + version: "9.4.1" package_config: dependency: transitive description: @@ -737,18 +793,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" url: "https://pub.dev" source: hosted - version: "2.2.22" + version: "2.3.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -777,10 +833,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" platform: dependency: transitive description: @@ -809,10 +865,10 @@ packages: dependency: transitive description: name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.5.0" pub_semver: dependency: transitive description: @@ -829,6 +885,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" riverpod: dependency: transitive description: @@ -905,10 +969,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" + sha256: a2c49fc1fed7140cadd892d765bd47edbe4ac0b9c7e7e3c493dcb58126f99cf0 url: "https://pub.dev" source: hosted - version: "2.4.18" + version: "2.4.25" shared_preferences_foundation: dependency: transitive description: @@ -929,10 +993,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_web: dependency: transitive description: @@ -1037,10 +1101,10 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.2" sqflite: dependency: "direct main" description: @@ -1197,18 +1261,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 url: "https://pub.dev" source: hosted - version: "6.3.28" + version: "6.3.32" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.dev" source: hosted - version: "6.3.6" + version: "6.4.1" url_launcher_linux: dependency: transitive description: @@ -1253,10 +1317,10 @@ packages: dependency: transitive description: name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" vector_math: dependency: transitive description: @@ -1269,18 +1333,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.2.0" watcher: dependency: transitive description: name: watcher - sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249 + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f538c66..faf7646 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: hooks_riverpod: ^3.3.1 intl: ^0.20.2 json_annotation: ^4.12.0 + liquid_glass_renderer: ^0.2.0-dev.4 motor: ^1.1.0 package_info_plus: ^10.1.0 path: ^1.9.1 From e079b02c50fd7f223071b2abe64e5def0e4d92fb Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Mon, 8 Jun 2026 09:01:33 +0300 Subject: [PATCH 03/15] feat: add Android in-app update support via UpdateService --- lib/screens/app_startup.dart | 7 +++++++ lib/utils/update_service.dart | 34 ++++++++++++++++++++++++++++++++++ pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 4 files changed, 50 insertions(+) create mode 100644 lib/utils/update_service.dart diff --git a/lib/screens/app_startup.dart b/lib/screens/app_startup.dart index d7ff34e..65eb1da 100644 --- a/lib/screens/app_startup.dart +++ b/lib/screens/app_startup.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/settings_controller.dart'; +import '../utils/update_service.dart'; class AppStartup extends ConsumerStatefulWidget { const AppStartup({super.key , required this.child}); @@ -13,6 +14,12 @@ class AppStartup extends ConsumerStatefulWidget { } class _AppStartupState extends ConsumerState { + @override + void initState() { + super.initState(); + UpdateService.instance.checkAndUpdate(); + } + @override Widget build(BuildContext context) { final settingsAsync = ref.watch(settingsControllerProvider); diff --git a/lib/utils/update_service.dart b/lib/utils/update_service.dart new file mode 100644 index 0000000..b4e31fe --- /dev/null +++ b/lib/utils/update_service.dart @@ -0,0 +1,34 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:in_app_update/in_app_update.dart'; + +class UpdateService { + UpdateService._(); + static final UpdateService instance = UpdateService._(); + + static const _staleDays = 7; + StreamSubscription? _flexSub; + + Future checkAndUpdate() async { + if (!Platform.isAndroid) return; + try { + final info = await InAppUpdate.checkForUpdate(); + if (info.updateAvailability != UpdateAvailability.updateAvailable) return; + + final stale = info.clientVersionStalenessDays ?? 0; + if (stale >= _staleDays) { + await InAppUpdate.performImmediateUpdate(); + } else { + await InAppUpdate.startFlexibleUpdate(); + _flexSub = InAppUpdate.installUpdateListener.listen((status) { + if (status == InstallStatus.downloaded) { + _flexSub?.cancel(); + InAppUpdate.completeFlexibleUpdate(); + } + }); + } + } catch (_) { + // Silently ignore: emulator, sideloaded APK, Play Store unavailable + } + } +} diff --git a/pubspec.lock b/pubspec.lock index be53a57..7dff9ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -589,6 +589,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + in_app_update: + dependency: "direct main" + description: + name: in_app_update + sha256: "9924a3efe592e1c0ec89dda3683b3cfec3d4cd02d908e6de00c24b759038ddb1" + url: "https://pub.dev" + source: hosted + version: "4.2.5" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index faf7646..992eab4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: freezed_annotation: ^3.1.0 go_router: ^17.3.0 hooks_riverpod: ^3.3.1 + in_app_update: ^4.2.3 intl: ^0.20.2 json_annotation: ^4.12.0 liquid_glass_renderer: ^0.2.0-dev.4 From a49702bff06c8184356d1f2d158519da24462158 Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Mon, 8 Jun 2026 09:07:31 +0300 Subject: [PATCH 04/15] feat: Added in-app update support for Android via UpdateService singleton hooked into AppStartup. --- lib/screens/app_startup.dart | 1 + lib/utils/update_service.dart | 7 +++++-- lib/widgets/glass_nav_bar.dart | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/screens/app_startup.dart b/lib/screens/app_startup.dart index 65eb1da..64e2195 100644 --- a/lib/screens/app_startup.dart +++ b/lib/screens/app_startup.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../providers/settings_controller.dart'; import '../utils/update_service.dart'; diff --git a/lib/utils/update_service.dart b/lib/utils/update_service.dart index b4e31fe..2ce70e9 100644 --- a/lib/utils/update_service.dart +++ b/lib/utils/update_service.dart @@ -1,5 +1,6 @@ -import 'dart:async'; +import 'dart:async' show StreamSubscription; import 'dart:io'; + import 'package:in_app_update/in_app_update.dart'; class UpdateService { @@ -20,10 +21,12 @@ class UpdateService { await InAppUpdate.performImmediateUpdate(); } else { await InAppUpdate.startFlexibleUpdate(); + await _flexSub?.cancel(); _flexSub = InAppUpdate.installUpdateListener.listen((status) { if (status == InstallStatus.downloaded) { _flexSub?.cancel(); - InAppUpdate.completeFlexibleUpdate(); + _flexSub = null; + InAppUpdate.completeFlexibleUpdate().catchError((_) {}); } }); } diff --git a/lib/widgets/glass_nav_bar.dart b/lib/widgets/glass_nav_bar.dart index a7a13d2..ddbecc8 100644 --- a/lib/widgets/glass_nav_bar.dart +++ b/lib/widgets/glass_nav_bar.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:liquid_glass_renderer/liquid_glass_renderer.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:liquid_glass_renderer/liquid_glass_renderer.dart'; import 'package:motor/motor.dart'; const _snappyMotion = CupertinoMotion.snappy(); From 50c8920cfe147e8b8e8549e5aad0b3e3ced10db7 Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Mon, 8 Jun 2026 10:35:44 +0300 Subject: [PATCH 05/15] feat: trigger in-app review after every 3rd subscription added --- lib/providers/subs_controller.dart | 5 +++++ pubspec.lock | 16 ++++++++++++++++ pubspec.yaml | 1 + 3 files changed, 22 insertions(+) diff --git a/lib/providers/subs_controller.dart b/lib/providers/subs_controller.dart index 6dd26d9..b2a3f29 100644 --- a/lib/providers/subs_controller.dart +++ b/lib/providers/subs_controller.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart' show DateTimeComponents; +import 'package:in_app_review/in_app_review.dart'; import 'package:path/path.dart'; import 'package:riverpod_annotation/experimental/json_persist.dart'; import 'package:riverpod_annotation/experimental/persist.dart'; @@ -51,6 +52,10 @@ class SubsController extends _$SubsController { void addSlice(SubSlice slice) { state = AsyncValue.data(List.of(state.value ?? [])..add(slice)); scheduleNotification(); + final count = state.value?.length ?? 0; + if (count > 0 && count % 3 == 0) { + InAppReview.instance.requestReview(); + } } Future scheduleNotification() async { diff --git a/pubspec.lock b/pubspec.lock index 7dff9ed..e58f109 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -589,6 +589,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + in_app_review: + dependency: "direct main" + description: + name: in_app_review + sha256: "364db0c160b37fe7fad9cdaa18f968473924b17368869010af902760421b6bf8" + url: "https://pub.dev" + source: hosted + version: "2.0.12" + in_app_review_platform_interface: + dependency: transitive + description: + name: in_app_review_platform_interface + sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10 + url: "https://pub.dev" + source: hosted + version: "2.0.5" in_app_update: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 992eab4..a4e4f22 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: freezed_annotation: ^3.1.0 go_router: ^17.3.0 hooks_riverpod: ^3.3.1 + in_app_review: ^2.0.12 in_app_update: ^4.2.3 intl: ^0.20.2 json_annotation: ^4.12.0 From c8c246a480314dde25808c818c9d1e3ec014f22a Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Mon, 8 Jun 2026 10:40:53 +0300 Subject: [PATCH 06/15] fix: add isAvailable check and remove redundant count guard in addSlice --- lib/providers/subs_controller.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/providers/subs_controller.dart b/lib/providers/subs_controller.dart index b2a3f29..3e6dcee 100644 --- a/lib/providers/subs_controller.dart +++ b/lib/providers/subs_controller.dart @@ -53,8 +53,10 @@ class SubsController extends _$SubsController { state = AsyncValue.data(List.of(state.value ?? [])..add(slice)); scheduleNotification(); final count = state.value?.length ?? 0; - if (count > 0 && count % 3 == 0) { - InAppReview.instance.requestReview(); + if (count % 3 == 0) { + InAppReview.instance.isAvailable().then((available) { + if (available) InAppReview.instance.requestReview(); + }); } } From 454034dd037f43a89a202b751952481e5a536bb5 Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Mon, 8 Jun 2026 14:03:58 +0300 Subject: [PATCH 07/15] feat: add iOS WidgetKit widgets, Dynamic Island Live Activity, and Android Glance widgets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - iOS SubsWidget extension: MonthlySpend, NextDue, Upcoming widgets (all sizes) - iOS Live Activity / Dynamic Island via ActivityKit + MethodChannel - Android Glance widgets: MonthlySpend, NextDue, Upcoming with receivers - Flutter WidgetUpdateService wires SubsController + SettingsController → home_widget - LiveActivityService starts activity for subs due today on app launch - Debug tile in Settings to force-start Live Activity for testing - Fix SubsDataReader key: flutter_subs_data → subs_data --- android/app/build.gradle.kts | 2 + android/app/src/main/AndroidManifest.xml | 36 ++ .../subzilla/widget/NextDueDateHelper.kt | 53 +++ .../devopen/subzilla/widget/SubsDataReader.kt | 48 +++ .../subzilla/widget/SubsGlanceWidget.kt | 223 +++++++++++++ .../res/xml/monthly_spend_widget_info.xml | 10 + .../src/main/res/xml/next_due_widget_info.xml | 10 + .../src/main/res/xml/upcoming_widget_info.xml | 10 + ios/LiveActivity/ColorHelper.swift | 11 + ios/LiveActivity/SubsDueAttributes.swift | 17 + ios/LiveActivity/SubsDueWidget.swift | 128 ++++++++ ios/LiveActivityChannel.swift | 49 +++ ios/Runner.xcodeproj/project.pbxproj | 307 +++++++++++++++++- .../contents.xcworkspacedata | 3 + ios/Runner/AppDelegate.swift | 13 +- ios/Runner/Info.plist | 2 + ios/Runner/Runner.entitlements | 10 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ ios/SubsWidget/Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 11 + ios/SubsWidget/ColorHelper.swift | 11 + ios/SubsWidget/Info.plist | 13 + ios/SubsWidget/MonthlySpendWidget.swift | 112 +++++++ ios/SubsWidget/NextDueDateHelper.swift | 44 +++ ios/SubsWidget/NextDueWidget.swift | 132 ++++++++ ios/SubsWidget/SubsDataReader.swift | 11 + ios/SubsWidget/SubsDueAttributes.swift | 17 + ios/SubsWidget/SubsDueWidget.swift | 128 ++++++++ ios/SubsWidget/SubsEntry.swift | 15 + ios/SubsWidget/SubsModels.swift | 16 + ios/SubsWidget/SubsTimelineProvider.swift | 78 +++++ ios/SubsWidget/SubsWidgetBundle.swift | 19 ++ ios/SubsWidget/UpcomingWidget.swift | 101 ++++++ ios/SubsWidget/WidgetBackground.swift | 14 + ios/SubsWidgetExtension.entitlements | 10 + lib/main.dart | 2 + lib/providers/settings_controller.dart | 6 + lib/providers/settings_controller.g.dart | 2 +- lib/providers/subs_controller.dart | 11 + lib/providers/subs_controller.g.dart | 2 +- lib/screens/app_startup.dart | 20 +- lib/screens/settings_screen.dart | 27 +- lib/services/live_activity_service.dart | 71 ++++ lib/services/widget_update_service.dart | 47 +++ pubspec.lock | 8 + pubspec.yaml | 1 + test/services/widget_update_service_test.dart | 83 +++++ 48 files changed, 1982 insertions(+), 14 deletions(-) create mode 100644 android/app/src/main/kotlin/io/devopen/subzilla/widget/NextDueDateHelper.kt create mode 100644 android/app/src/main/kotlin/io/devopen/subzilla/widget/SubsDataReader.kt create mode 100644 android/app/src/main/kotlin/io/devopen/subzilla/widget/SubsGlanceWidget.kt create mode 100644 android/app/src/main/res/xml/monthly_spend_widget_info.xml create mode 100644 android/app/src/main/res/xml/next_due_widget_info.xml create mode 100644 android/app/src/main/res/xml/upcoming_widget_info.xml create mode 100644 ios/LiveActivity/ColorHelper.swift create mode 100644 ios/LiveActivity/SubsDueAttributes.swift create mode 100644 ios/LiveActivity/SubsDueWidget.swift create mode 100644 ios/LiveActivityChannel.swift create mode 100644 ios/Runner/Runner.entitlements create mode 100644 ios/SubsWidget/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ios/SubsWidget/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/SubsWidget/Assets.xcassets/Contents.json create mode 100644 ios/SubsWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 ios/SubsWidget/ColorHelper.swift create mode 100644 ios/SubsWidget/Info.plist create mode 100644 ios/SubsWidget/MonthlySpendWidget.swift create mode 100644 ios/SubsWidget/NextDueDateHelper.swift create mode 100644 ios/SubsWidget/NextDueWidget.swift create mode 100644 ios/SubsWidget/SubsDataReader.swift create mode 100644 ios/SubsWidget/SubsDueAttributes.swift create mode 100644 ios/SubsWidget/SubsDueWidget.swift create mode 100644 ios/SubsWidget/SubsEntry.swift create mode 100644 ios/SubsWidget/SubsModels.swift create mode 100644 ios/SubsWidget/SubsTimelineProvider.swift create mode 100644 ios/SubsWidget/SubsWidgetBundle.swift create mode 100644 ios/SubsWidget/UpcomingWidget.swift create mode 100644 ios/SubsWidget/WidgetBackground.swift create mode 100644 ios/SubsWidgetExtension.entitlements create mode 100644 lib/services/live_activity_service.dart create mode 100644 lib/services/widget_update_service.dart create mode 100644 test/services/widget_update_service_test.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 275b9c9..5ae009d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -80,6 +80,8 @@ dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") implementation("androidx.window:window:1.0.0") implementation("androidx.window:window-java:1.0.0") + implementation("androidx.glance:glance-appwidget:1.1.1") + implementation("androidx.glance:glance-material3:1.1.1") } flutter { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 96b4878..cc1a3e6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -36,6 +36,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + android:exported="true" + android:permission="android.permission.BIND_APPWIDGET"> @@ -52,7 +53,8 @@ + android:exported="true" + android:permission="android.permission.BIND_APPWIDGET"> @@ -64,7 +66,8 @@ + android:exported="true" + android:permission="android.permission.BIND_APPWIDGET"> diff --git a/android/app/src/main/kotlin/io/devopen/subzilla/widget/SubsGlanceWidget.kt b/android/app/src/main/kotlin/io/devopen/subzilla/widget/SubsGlanceWidget.kt index 7076234..eb8f518 100644 --- a/android/app/src/main/kotlin/io/devopen/subzilla/widget/SubsGlanceWidget.kt +++ b/android/app/src/main/kotlin/io/devopen/subzilla/widget/SubsGlanceWidget.kt @@ -112,7 +112,7 @@ private fun UpcomingSubRow(sub: WidgetSub, dueDate: LocalDate, today: LocalDate, ) Spacer(modifier = GlanceModifier.width(10.dp)) Text( - "$currency${String.format("%.2f", sub.amount)}", + "$currency${String.format(java.util.Locale.US, "%.2f", sub.amount)}", style = TextStyle(fontSize = 12.sp, color = GlanceTheme.colors.primary) ) } @@ -149,7 +149,7 @@ private fun MonthlySpendContent(data: WidgetData?) { ) Spacer(modifier = GlanceModifier.defaultWeight()) Text( - "${data.currency}${String.format("%.2f", data.monthlyTotal)}", + "${data.currency}${String.format(java.util.Locale.US, "%.2f", data.monthlyTotal)}", style = TextStyle( fontSize = 28.sp, fontWeight = FontWeight.Bold, @@ -240,7 +240,7 @@ private fun NextDueContent(data: WidgetData?, sorted: List Bool { GeneratedPluginRegistrant.register(with: self) + UNUserNotificationCenter.current().delegate = self if #available(iOS 16.2, *), let registrar = self.registrar(forPlugin: "LiveActivityChannel") { liveActivityChannel = LiveActivityChannel(binaryMessenger: registrar.messenger()) } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + override func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + completionHandler([.banner, .sound, .badge]) + } } diff --git a/ios/SubsWidget/MonthlySpendWidget.swift b/ios/SubsWidget/MonthlySpendWidget.swift index 76ce932..4b58d88 100644 --- a/ios/SubsWidget/MonthlySpendWidget.swift +++ b/ios/SubsWidget/MonthlySpendWidget.swift @@ -5,7 +5,7 @@ import SwiftUI struct MonthlySpendSmallView: View { let entry: SubsEntry var body: some View { - if let data = entry.widgetData { + if let data = entry.widgetData, !data.subs.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("This Month") .font(.caption2) diff --git a/ios/SubsWidget/SubsDueWidget.swift b/ios/SubsWidget/SubsDueWidget.swift index a444fba..364e646 100644 --- a/ios/SubsWidget/SubsDueWidget.swift +++ b/ios/SubsWidget/SubsDueWidget.swift @@ -40,7 +40,7 @@ struct SubsDueWidgetLiveActivity: Widget { Text(subs.count == 1 ? "Subscription due today" : "\(subs.count) subscriptions due today") .font(.caption) .fontWeight(.semibold) - ForEach(subs.prefix(3), id: \.name) { sub in + ForEach(Array(subs.prefix(3).enumerated()), id: \.offset) { _, sub in Text("\(sub.name) — \(sub.currency)\(String(format: "%.2f", sub.amount))") .font(.caption2) .foregroundStyle(.secondary) @@ -109,7 +109,7 @@ struct SubsDueWidgetLiveActivity: Widget { private func expandedBottom(context: ActivityViewContext) -> some View { let subs = context.state.dueSubs return VStack(alignment: .leading, spacing: 4) { - ForEach(subs.prefix(3), id: \.name) { sub in + ForEach(Array(subs.prefix(3).enumerated()), id: \.offset) { _, sub in HStack { Circle() .fill(Color(flutterARGB: sub.color)) diff --git a/ios/SubsWidget/SubsWidgetBundle.swift b/ios/SubsWidget/SubsWidgetBundle.swift index cc044ff..6130d27 100644 --- a/ios/SubsWidget/SubsWidgetBundle.swift +++ b/ios/SubsWidget/SubsWidgetBundle.swift @@ -14,6 +14,6 @@ struct SubsWidgetBundle: WidgetBundle { MonthlySpendWidget() NextDueWidget() UpcomingWidget() - SubsDueWidgetLiveActivity() // add this + SubsDueWidgetLiveActivity() } } diff --git a/ios/SubsWidget/UpcomingWidget.swift b/ios/SubsWidget/UpcomingWidget.swift index 5407a8d..e0c718a 100644 --- a/ios/SubsWidget/UpcomingWidget.swift +++ b/ios/SubsWidget/UpcomingWidget.swift @@ -32,7 +32,7 @@ private func upcomingList(entry: SubsEntry, maxItems: Int) -> some View { .font(.caption) .fontWeight(.semibold) .foregroundStyle(.secondary) - ForEach(items, id: \.sub.name) { item in + ForEach(Array(items.enumerated()), id: \.offset) { _, item in HStack { Circle() .fill(Color(flutterARGB: item.sub.color)) diff --git a/lib/screens/app_startup.dart b/lib/screens/app_startup.dart index ce6bc3c..9b12953 100644 --- a/lib/screens/app_startup.dart +++ b/lib/screens/app_startup.dart @@ -24,7 +24,7 @@ class _AppStartupState extends ConsumerState { @override void initState() { super.initState(); - UpdateService.instance.checkAndUpdate(); + unawaited(UpdateService.instance.checkAndUpdate()); } @override diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 1678302..27c7040 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -133,7 +133,7 @@ class HomeScreen extends HookConsumerWidget { itemBuilder: (context, index) { final slice = slices[index]; return _CompactSubscriptionTile( - key: ValueKey('${slice.name}-$index'), + key: ValueKey(slice.name), slice: slice, index: index, currencySymbol: settings.currency.symbol, @@ -428,7 +428,7 @@ class _SortButton extends ConsumerWidget { leading: const Icon(Icons.arrow_upward), title: Text('home.sort_amount_asc'.tr()), onTap: () { - notifier.sortSlices((a, b) => a.amount.compareTo(b.amount)); + notifier.sortSlices((a, b) => a.monthlyAmount.compareTo(b.monthlyAmount)); Navigator.pop(ctx); }, ), @@ -436,7 +436,7 @@ class _SortButton extends ConsumerWidget { leading: const Icon(Icons.arrow_downward), title: Text('home.sort_amount_desc'.tr()), onTap: () { - notifier.sortSlices((a, b) => b.amount.compareTo(a.amount)); + notifier.sortSlices((a, b) => b.monthlyAmount.compareTo(a.monthlyAmount)); Navigator.pop(ctx); }, ), diff --git a/lib/screens/sub_detail_screen.dart b/lib/screens/sub_detail_screen.dart index 46499dc..b5ff9f9 100644 --- a/lib/screens/sub_detail_screen.dart +++ b/lib/screens/sub_detail_screen.dart @@ -456,21 +456,43 @@ void _showTrialEndPicker( ValueChanged onSelected, ) { var selected = current ?? DateTime.now(); - showCupertinoModalPopup( + showCupertinoModalPopup( context: context, builder: (ctx) { final theme = Theme.of(ctx); return Container( - height: 250, color: theme.colorScheme.surface, - child: CupertinoDatePicker( - mode: CupertinoDatePickerMode.date, - initialDateTime: selected, - onDateTimeChanged: (date) => selected = date, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CupertinoButton( + child: Text('detail.cancel'.tr()), + onPressed: () => Navigator.of(ctx).pop(null), + ), + CupertinoButton( + child: Text('common.save'.tr()), + onPressed: () => Navigator.of(ctx).pop(selected), + ), + ], + ), + SizedBox( + height: 216, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: selected, + onDateTimeChanged: (date) => selected = date, + ), + ), + ], ), ); }, - ).then((_) => onSelected(selected)); + ).then((result) { + if (result != null) onSelected(result); + }); } void _showCardInput(BuildContext context, String? current, ValueChanged onSelected) { @@ -517,7 +539,7 @@ void _showCardInput(BuildContext context, String? current, ValueChanged ), ); }, - ); + ).whenComplete(controller.dispose); } void _showNoteInput(BuildContext context, String? current, ValueChanged onSelected) { @@ -559,7 +581,7 @@ void _showNoteInput(BuildContext context, String? current, ValueChanged ), ); }, - ); + ).whenComplete(controller.dispose); } // --------------------------------------------------------------------------- diff --git a/lib/services/live_activity_service.dart b/lib/services/live_activity_service.dart index 2ef8a5d..f92912a 100644 --- a/lib/services/live_activity_service.dart +++ b/lib/services/live_activity_service.dart @@ -46,14 +46,15 @@ class LiveActivityService { bool _isDueToday(SubSlice sub, DateTime today) { final todayDate = DateTime(today.year, today.month, today.day); var candidate = DateTime(sub.startDate.year, sub.startDate.month, sub.startDate.day); + final anchorDay = sub.startDate.day; // Advance until we pass today or land on today while (candidate.isBefore(todayDate)) { - candidate = _advance(candidate, sub.frequency); + candidate = _advance(candidate, sub.frequency, anchorDay); } return candidate == todayDate; } - DateTime _advance(DateTime date, Frequency freq) { + DateTime _advance(DateTime date, Frequency freq, int anchorDay) { switch (freq) { case Frequency.daily: return date.add(const Duration(days: 1)); @@ -63,7 +64,7 @@ class LiveActivityService { final nextMonth = date.month == 12 ? 1 : date.month + 1; final nextYear = date.month == 12 ? date.year + 1 : date.year; final lastDay = DateTime(nextYear, nextMonth + 1, 0).day; - return DateTime(nextYear, nextMonth, date.day.clamp(1, lastDay)); + return DateTime(nextYear, nextMonth, anchorDay.clamp(1, lastDay)); case Frequency.yearly: return DateTime(date.year + 1, date.month, date.day); } diff --git a/lib/widgets/edit_subs_dialog.dart b/lib/widgets/edit_subs_dialog.dart index 9e2434e..709a6c6 100644 --- a/lib/widgets/edit_subs_dialog.dart +++ b/lib/widgets/edit_subs_dialog.dart @@ -337,7 +337,7 @@ class EditSubsDialog extends HookConsumerWidget { return DropdownMenuItem( value: f, child: Text( - 'frequency_names.${f.name.toLowerCase()}'.tr(), + 'frequency.${f.name}'.tr(), ), ); }).toList(), From 6a647907db043badc6ed961d4e16dbfec953c3e7 Mon Sep 17 00:00:00 2001 From: Eren Gun Date: Thu, 25 Jun 2026 18:52:01 +0300 Subject: [PATCH 15/15] fix: analyzer warnings --- lib/screens/sub_detail_screen.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/screens/sub_detail_screen.dart b/lib/screens/sub_detail_screen.dart index b5ff9f9..ce4b9d8 100644 --- a/lib/screens/sub_detail_screen.dart +++ b/lib/screens/sub_detail_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; + import '../models/sub_slice.dart'; import '../providers/settings_controller.dart'; import '../providers/subs_controller.dart'; @@ -460,7 +461,7 @@ void _showTrialEndPicker( context: context, builder: (ctx) { final theme = Theme.of(ctx); - return Container( + return ColoredBox( color: theme.colorScheme.surface, child: Column( mainAxisSize: MainAxisSize.min, @@ -470,7 +471,7 @@ void _showTrialEndPicker( children: [ CupertinoButton( child: Text('detail.cancel'.tr()), - onPressed: () => Navigator.of(ctx).pop(null), + onPressed: () => Navigator.of(ctx).pop(), ), CupertinoButton( child: Text('common.save'.tr()),