From 289e9d8ac950122a58bdbaf974b8f42c671f9ef6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 18:26:09 +0100 Subject: [PATCH 01/69] feat(bootstrap): migrate from Category to Topic model Replaced all instances of the `Category` model with the new `Topic` model in the dependency injection setup. - Updated `HtDataClient` and `HtDataRepository` to use `Topic`. - Renamed `categoriesClient` and `categoriesRepository` to `topicsClient` and `topicsRepository` respectively. - Updated `HtDataInMemory` and `HtDataApi` instantiations for the `Topic` model, including the model name for API calls. - Updated the `App` widget instantiation to pass the new `htTopicsRepository`. --- lib/bootstrap.dart | 28 ++++++++++++++-------------- pubspec.lock | 16 ++++++++-------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 2c1827e4..ff81cff8 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -55,7 +55,7 @@ Future bootstrap( } HtDataClient headlinesClient; - HtDataClient categoriesClient; + HtDataClient topicsClient; HtDataClient countriesClient; HtDataClient sourcesClient; HtDataClient userContentPreferencesClient; @@ -69,10 +69,10 @@ Future bootstrap( getId: (i) => i.id, initialData: headlinesFixturesData.map(Headline.fromJson).toList(), ); - categoriesClient = HtDataInMemory( + topicsClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, - initialData: categoriesFixturesData.map(Category.fromJson).toList(), + initialData: categoriesFixturesData.map(Topic.fromJson).toList(), ); countriesClient = HtDataInMemory( toJson: (i) => i.toJson(), @@ -111,11 +111,11 @@ Future bootstrap( fromJson: Headline.fromJson, toJson: (headline) => headline.toJson(), ); - categoriesClient = HtDataApi( + topicsClient = HtDataApi( httpClient: httpClient, - modelName: 'category', - fromJson: Category.fromJson, - toJson: (category) => category.toJson(), + modelName: 'topic', + fromJson: Topic.fromJson, + toJson: (topic) => topic.toJson(), ); countriesClient = HtDataApi( httpClient: httpClient, @@ -160,11 +160,11 @@ Future bootstrap( fromJson: Headline.fromJson, toJson: (headline) => headline.toJson(), ); - categoriesClient = HtDataApi( + topicsClient = HtDataApi( httpClient: httpClient, - modelName: 'category', - fromJson: Category.fromJson, - toJson: (category) => category.toJson(), + modelName: 'topic', + fromJson: Topic.fromJson, + toJson: (topic) => topic.toJson(), ); countriesClient = HtDataApi( httpClient: httpClient, @@ -207,8 +207,8 @@ Future bootstrap( final headlinesRepository = HtDataRepository( dataClient: headlinesClient, ); - final categoriesRepository = HtDataRepository( - dataClient: categoriesClient, + final topicsRepository = HtDataRepository( + dataClient: topicsClient, ); final countriesRepository = HtDataRepository( dataClient: countriesClient, @@ -231,7 +231,7 @@ Future bootstrap( return App( htAuthenticationRepository: authenticationRepository, htHeadlinesRepository: headlinesRepository, - htCategoriesRepository: categoriesRepository, + htTopicsRepository: topicsRepository, htCountriesRepository: countriesRepository, htSourcesRepository: sourcesRepository, htUserAppSettingsRepository: userAppSettingsRepository, diff --git a/pubspec.lock b/pubspec.lock index 1024b5c1..2ee8eb21 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -205,7 +205,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: f89241dfd482d2a72b1168f979597a34b1004df5 + resolved-ref: a2fc2a651494831a461fff96807141bdba9cc28b url: "https://github.com/headlines-toolkit/ht-auth-api.git" source: git version: "0.0.0" @@ -223,7 +223,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "721a028b926a5a8af2b5176de039cd6394a21724" + resolved-ref: ea9ad0361b1e0beab4690959889b7056091d29dc url: "https://github.com/headlines-toolkit/ht-auth-inmemory" source: git version: "0.0.0" @@ -241,7 +241,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: b3073b812b4d5216f0ce5658be1be52e193083bb + resolved-ref: e6decb1f81ca233199f271539ddbf04bb9d63984 url: "https://github.com/headlines-toolkit/ht-data-api.git" source: git version: "0.0.0" @@ -250,7 +250,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "0077622bbd5c8886a4d7bfdf65540e6965564ad1" + resolved-ref: e566ee6eae5261c00d0987972efc77a37a0c4c3a url: "https://github.com/headlines-toolkit/ht-data-client.git" source: git version: "0.0.0" @@ -259,7 +259,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "948d8a237c9465b8fd9d3ab78bcea10841dc9a90" + resolved-ref: abef81e5294d70ace82d3e87f1efc94fca6a8445 url: "https://github.com/headlines-toolkit/ht-data-inmemory.git" source: git version: "0.0.0" @@ -268,7 +268,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "434c7a02cc85b7243a9a2f1bd662557ee19c3479" + resolved-ref: f19fe64c67a2febdef853b15f6df9c63240ad48e url: "https://github.com/headlines-toolkit/ht-data-repository.git" source: git version: "0.0.0" @@ -277,7 +277,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "648e8549d7ceb2ffb293fa66bdd02f5e0ca8def6" + resolved-ref: "0b56d92624769ca3175d5ce2c7da27ab29514f8a" url: "https://github.com/headlines-toolkit/ht-http-client.git" source: git version: "0.0.0" @@ -304,7 +304,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "30aff4d0e2661ff79f2b84070af5f7982d88ba66" + resolved-ref: "83d73bdbc965b75425db346da8802be414b9ec0c" url: "https://github.com/headlines-toolkit/ht-shared.git" source: git version: "0.0.0" From eef30c534c9c7a139338ddef16bd184a51836f7b Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 18:27:18 +0100 Subject: [PATCH 02/69] refactor(data): Remove unnecessary map calls - Removed `.map(Type.fromJson).toList()` calls. - Simplified data initialization. - Improved code readability. - Minor performance optimization. - No functional changes. --- lib/bootstrap.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index ff81cff8..a610123a 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -67,22 +67,22 @@ Future bootstrap( headlinesClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, - initialData: headlinesFixturesData.map(Headline.fromJson).toList(), + initialData: headlinesFixturesData, ); topicsClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, - initialData: categoriesFixturesData.map(Topic.fromJson).toList(), + initialData: topicsFixturesData, ); countriesClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, - initialData: countriesFixturesData.map(Country.fromJson).toList(), + initialData: countriesFixturesData, ); sourcesClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, - initialData: sourcesFixturesData.map(Source.fromJson).toList(), + initialData: sourcesFixturesData, ); userContentPreferencesClient = HtDataInMemory( toJson: (i) => i.toJson(), From 39dada335a5978f0cc59eb37b66d7347f56cd193 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 18:29:38 +0100 Subject: [PATCH 03/69] feat(bootstrap): migrate from AppConfig to RemoteConfig model Replaced all instances of the `AppConfig` model with the new `RemoteConfig` model in the dependency injection setup. - Updated `HtDataClient` and `HtDataRepository` to use `RemoteConfig`. - Renamed `appConfigClient` and `appConfigRepository` to `remoteConfigClient` and `remoteConfigRepository` respectively. - Updated `HtDataInMemory` and `HtDataApi` instantiations for the `RemoteConfig` model, including the model name for API calls. - Updated the `App` widget instantiation to pass the new `htRemoteConfigRepository`. --- lib/bootstrap.dart | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index a610123a..788bae59 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -60,7 +60,7 @@ Future bootstrap( HtDataClient sourcesClient; HtDataClient userContentPreferencesClient; HtDataClient userAppSettingsClient; - HtDataClient appConfigClient; + HtDataClient remoteConfigClient; HtDataClient dashboardSummaryClient; if (appConfig.environment == app_config.AppEnvironment.demo) { @@ -92,10 +92,10 @@ Future bootstrap( toJson: (i) => i.toJson(), getId: (i) => i.id, ); - appConfigClient = HtDataInMemory( + remoteConfigClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, - initialData: [AppConfig.fromJson(appConfigFixtureData)], + initialData: [RemoteConfig.fromJson(appConfigFixtureData)], ); dashboardSummaryClient = HtDataInMemory( toJson: (i) => i.toJson(), @@ -141,10 +141,10 @@ Future bootstrap( fromJson: UserAppSettings.fromJson, toJson: (settings) => settings.toJson(), ); - appConfigClient = HtDataApi( + remoteConfigClient = HtDataApi( httpClient: httpClient, - modelName: 'app_config', - fromJson: AppConfig.fromJson, + modelName: 'remote_config', + fromJson: RemoteConfig.fromJson, toJson: (config) => config.toJson(), ); dashboardSummaryClient = HtDataApi( @@ -190,10 +190,10 @@ Future bootstrap( fromJson: UserAppSettings.fromJson, toJson: (settings) => settings.toJson(), ); - appConfigClient = HtDataApi( + remoteConfigClient = HtDataApi( httpClient: httpClient, - modelName: 'app_config', - fromJson: AppConfig.fromJson, + modelName: 'remote_config', + fromJson: RemoteConfig.fromJson, toJson: (config) => config.toJson(), ); dashboardSummaryClient = HtDataApi( @@ -221,8 +221,8 @@ Future bootstrap( final userAppSettingsRepository = HtDataRepository( dataClient: userAppSettingsClient, ); - final appConfigRepository = HtDataRepository( - dataClient: appConfigClient, + final remoteConfigRepository = HtDataRepository( + dataClient: remoteConfigClient, ); final dashboardSummaryRepository = HtDataRepository( dataClient: dashboardSummaryClient, @@ -236,7 +236,7 @@ Future bootstrap( htSourcesRepository: sourcesRepository, htUserAppSettingsRepository: userAppSettingsRepository, htUserContentPreferencesRepository: userContentPreferencesRepository, - htAppConfigRepository: appConfigRepository, + htRemoteConfigRepository: remoteConfigRepository, htDashboardSummaryRepository: dashboardSummaryRepository, kvStorageService: kvStorage, environment: environment, From 8963f1cf4d427ab287a0d0e34c88f306a4dad8a8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 18:33:58 +0100 Subject: [PATCH 04/69] feat(bootstrap): migrate from AppConfig to RemoteConfig model Replaced all instances of the `AppConfig` model with the new `RemoteConfig` model in the dependency injection setup. - Updated `HtDataClient` and `HtDataRepository` to use `RemoteConfig`. - Renamed `appConfigClient` and `appConfigRepository` to `remoteConfigClient` and `remoteConfigRepository` respectively. - Updated `HtDataInMemory` and `HtDataApi` instantiations for the `RemoteConfig` model, including the model name for API calls. - Updated the `App` widget instantiation to pass the new `htRemoteConfigRepository`. --- lib/bootstrap.dart | 40 ++++++++++++++++++++++++++++++++++------ pubspec.lock | 2 +- pubspec.yaml | 1 + 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 788bae59..71747338 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -17,6 +17,7 @@ import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_http_client/ht_http_client.dart'; import 'package:ht_kv_storage_shared_preferences/ht_kv_storage_shared_preferences.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:logging/logging.dart'; import 'package:timeago/timeago.dart' as timeago; Future bootstrap( @@ -36,7 +37,9 @@ Future bootstrap( HtHttpClient? httpClient; if (appConfig.environment == app_config.AppEnvironment.demo) { - authClient = HtAuthInmemory(); + authClient = HtAuthInmemory( + logger: Logger('HtAuthInmemory'), + ); authenticationRepository = HtAuthRepository( authClient: authClient, storageService: kvStorage, @@ -47,7 +50,10 @@ Future bootstrap( tokenProvider: () => authenticationRepository.getAuthToken(), isWeb: kIsWeb, ); - authClient = HtAuthApi(httpClient: httpClient); + authClient = HtAuthApi( + httpClient: httpClient, + logger: Logger('HtAuthApi'), + ); authenticationRepository = HtAuthRepository( authClient: authClient, storageService: kvStorage, @@ -68,41 +74,47 @@ Future bootstrap( toJson: (i) => i.toJson(), getId: (i) => i.id, initialData: headlinesFixturesData, + logger: Logger('HtDataInMemory'), ); topicsClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, initialData: topicsFixturesData, + logger: Logger('HtDataInMemory'), ); countriesClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, initialData: countriesFixturesData, + logger: Logger('HtDataInMemory'), ); sourcesClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, initialData: sourcesFixturesData, + logger: Logger('HtDataInMemory'), ); userContentPreferencesClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, + logger: Logger('HtDataInMemory'), ); userAppSettingsClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, + logger: Logger('HtDataInMemory'), ); remoteConfigClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, - initialData: [RemoteConfig.fromJson(appConfigFixtureData)], + initialData: remoteConfigsFixturesData, + logger: Logger('HtDataInMemory'), ); dashboardSummaryClient = HtDataInMemory( toJson: (i) => i.toJson(), getId: (i) => i.id, - initialData: [ - DashboardSummary.fromJson(dashboardSummaryFixtureData), - ], + initialData: dashboardSummaryFixturesData, + logger: Logger('HtDataInMemory'), ); } else if (appConfig.environment == app_config.AppEnvironment.development) { headlinesClient = HtDataApi( @@ -110,48 +122,56 @@ Future bootstrap( modelName: 'headline', fromJson: Headline.fromJson, toJson: (headline) => headline.toJson(), + logger: Logger('HtDataApi'), ); topicsClient = HtDataApi( httpClient: httpClient, modelName: 'topic', fromJson: Topic.fromJson, toJson: (topic) => topic.toJson(), + logger: Logger('HtDataApi'), ); countriesClient = HtDataApi( httpClient: httpClient, modelName: 'country', fromJson: Country.fromJson, toJson: (country) => country.toJson(), + logger: Logger('HtDataApi'), ); sourcesClient = HtDataApi( httpClient: httpClient, modelName: 'source', fromJson: Source.fromJson, toJson: (source) => source.toJson(), + logger: Logger('HtDataApi'), ); userContentPreferencesClient = HtDataApi( httpClient: httpClient, modelName: 'user_content_preferences', fromJson: UserContentPreferences.fromJson, toJson: (prefs) => prefs.toJson(), + logger: Logger('HtDataApi'), ); userAppSettingsClient = HtDataApi( httpClient: httpClient, modelName: 'user_app_settings', fromJson: UserAppSettings.fromJson, toJson: (settings) => settings.toJson(), + logger: Logger('HtDataApi'), ); remoteConfigClient = HtDataApi( httpClient: httpClient, modelName: 'remote_config', fromJson: RemoteConfig.fromJson, toJson: (config) => config.toJson(), + logger: Logger('HtDataApi'), ); dashboardSummaryClient = HtDataApi( httpClient: httpClient, modelName: 'dashboard_summary', fromJson: DashboardSummary.fromJson, toJson: (summary) => summary.toJson(), + logger: Logger('HtDataApi'), ); } else { headlinesClient = HtDataApi( @@ -159,48 +179,56 @@ Future bootstrap( modelName: 'headline', fromJson: Headline.fromJson, toJson: (headline) => headline.toJson(), + logger: Logger('HtDataApi'), ); topicsClient = HtDataApi( httpClient: httpClient, modelName: 'topic', fromJson: Topic.fromJson, toJson: (topic) => topic.toJson(), + logger: Logger('HtDataApi'), ); countriesClient = HtDataApi( httpClient: httpClient, modelName: 'country', fromJson: Country.fromJson, toJson: (country) => country.toJson(), + logger: Logger('HtDataApi'), ); sourcesClient = HtDataApi( httpClient: httpClient, modelName: 'source', fromJson: Source.fromJson, toJson: (source) => source.toJson(), + logger: Logger('HtDataApi'), ); userContentPreferencesClient = HtDataApi( httpClient: httpClient, modelName: 'user_content_preferences', fromJson: UserContentPreferences.fromJson, toJson: (prefs) => prefs.toJson(), + logger: Logger('HtDataApi'), ); userAppSettingsClient = HtDataApi( httpClient: httpClient, modelName: 'user_app_settings', fromJson: UserAppSettings.fromJson, toJson: (settings) => settings.toJson(), + logger: Logger('HtDataApi'), ); remoteConfigClient = HtDataApi( httpClient: httpClient, modelName: 'remote_config', fromJson: RemoteConfig.fromJson, toJson: (config) => config.toJson(), + logger: Logger('HtDataApi'), ); dashboardSummaryClient = HtDataApi( httpClient: httpClient, modelName: 'dashboard_summary', fromJson: DashboardSummary.fromJson, toJson: (summary) => summary.toJson(), + logger: Logger('HtDataApi'), ); } diff --git a/pubspec.lock b/pubspec.lock index 2ee8eb21..eb5b409c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -341,7 +341,7 @@ packages: source: hosted version: "4.9.0" logging: - dependency: transitive + dependency: "direct main" description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 diff --git a/pubspec.yaml b/pubspec.yaml index 8534f3d3..89a80c4c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: git: url: https://github.com/headlines-toolkit/ht-shared.git intl: ^0.20.2 + logging: ^1.3.0 timeago: ^3.7.1 From 7a986fb94c7523e07a64671f31881146e963b813 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 18:35:09 +0100 Subject: [PATCH 05/69] feat(app): update App widget with new repositories and providers Migrated the App widget to use the new `Topic` and `RemoteConfig` models. - Updated the `App` widget constructor to accept `HtDataRepository` and `HtDataRepository`. - Updated the `MultiRepositoryProvider` to provide the new repositories to the widget tree. - Updated the `MultiBlocProvider` to inject the correct repositories into the `AppBloc`, `ContentManagementBloc`, `AppConfigurationBloc`, and `DashboardBloc`. --- lib/app/view/app.dart | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 44c6e6f0..58c02f5f 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -24,38 +24,36 @@ class App extends StatelessWidget { const App({ required HtAuthRepository htAuthenticationRepository, required HtDataRepository htHeadlinesRepository, - required HtDataRepository htCategoriesRepository, + required HtDataRepository htTopicsRepository, required HtDataRepository htCountriesRepository, required HtDataRepository htSourcesRepository, required HtDataRepository htUserAppSettingsRepository, - required HtDataRepository - htUserContentPreferencesRepository, - required HtDataRepository htAppConfigRepository, + required HtDataRepository htUserContentPreferencesRepository, + required HtDataRepository htRemoteConfigRepository, required HtDataRepository htDashboardSummaryRepository, required HtKVStorageService kvStorageService, required AppEnvironment environment, super.key, }) : _htAuthenticationRepository = htAuthenticationRepository, _htHeadlinesRepository = htHeadlinesRepository, - _htCategoriesRepository = htCategoriesRepository, + _htTopicsRepository = htTopicsRepository, _htCountriesRepository = htCountriesRepository, _htSourcesRepository = htSourcesRepository, _htUserAppSettingsRepository = htUserAppSettingsRepository, _htUserContentPreferencesRepository = htUserContentPreferencesRepository, - _htAppConfigRepository = htAppConfigRepository, + _htRemoteConfigRepository = htRemoteConfigRepository, _kvStorageService = kvStorageService, _htDashboardSummaryRepository = htDashboardSummaryRepository, _environment = environment; final HtAuthRepository _htAuthenticationRepository; final HtDataRepository _htHeadlinesRepository; - final HtDataRepository _htCategoriesRepository; + final HtDataRepository _htTopicsRepository; final HtDataRepository _htCountriesRepository; final HtDataRepository _htSourcesRepository; final HtDataRepository _htUserAppSettingsRepository; - final HtDataRepository - _htUserContentPreferencesRepository; - final HtDataRepository _htAppConfigRepository; + final HtDataRepository _htUserContentPreferencesRepository; + final HtDataRepository _htRemoteConfigRepository; final HtDataRepository _htDashboardSummaryRepository; final HtKVStorageService _kvStorageService; final AppEnvironment _environment; @@ -66,12 +64,12 @@ class App extends StatelessWidget { providers: [ RepositoryProvider.value(value: _htAuthenticationRepository), RepositoryProvider.value(value: _htHeadlinesRepository), - RepositoryProvider.value(value: _htCategoriesRepository), + RepositoryProvider.value(value: _htTopicsRepository), RepositoryProvider.value(value: _htCountriesRepository), RepositoryProvider.value(value: _htSourcesRepository), RepositoryProvider.value(value: _htUserAppSettingsRepository), RepositoryProvider.value(value: _htUserContentPreferencesRepository), - RepositoryProvider.value(value: _htAppConfigRepository), + RepositoryProvider.value(value: _htRemoteConfigRepository), RepositoryProvider.value(value: _htDashboardSummaryRepository), RepositoryProvider.value(value: _kvStorageService), ], @@ -82,7 +80,8 @@ class App extends StatelessWidget { authenticationRepository: context.read(), userAppSettingsRepository: context .read>(), - appConfigRepository: context.read>(), + appConfigRepository: + context.read>(), environment: _environment, ), ), @@ -93,21 +92,23 @@ class App extends StatelessWidget { ), BlocProvider( create: (context) => AppConfigurationBloc( - appConfigRepository: context.read>(), + appConfigRepository: + context.read>(), ), ), BlocProvider( create: (context) => ContentManagementBloc( headlinesRepository: context.read>(), - categoriesRepository: context.read>(), + topicsRepository: context.read>(), sourcesRepository: context.read>(), ), ), BlocProvider( create: (context) => DashboardBloc( - dashboardSummaryRepository: context - .read>(), - appConfigRepository: context.read>(), + dashboardSummaryRepository: + context.read>(), + appConfigRepository: + context.read>(), headlinesRepository: context.read>(), ), ), From e500f561a164adb6456360ac33c122231ad2b938 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 18:35:59 +0100 Subject: [PATCH 06/69] refactor(app): remove AppStatus import conflict - Renamed conflicting AppStatus - Updated import statements --- lib/app/view/app.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 58c02f5f..8442e8c9 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -18,7 +18,7 @@ import 'package:ht_dashboard/router/router.dart'; import 'package:ht_dashboard/shared/theme/app_theme.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_kv_storage_service/ht_kv_storage_service.dart'; -import 'package:ht_shared/ht_shared.dart'; +import 'package:ht_shared/ht_shared.dart' hide AppStatus; class App extends StatelessWidget { const App({ From b5f0ce16dc5b197c3b342691a25154ef25554e5c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 18:37:49 +0100 Subject: [PATCH 07/69] feat(app): update AppBloc to use new models and logic Refactored the `AppBloc` to align with recent model changes. - Replaced `HtDataRepository` with `HtDataRepository`. - Updated the user role check in `_onAppUserChanged` to use the new `user.dashboardRole` enum. - Implemented the creation of a complete, default `UserAppSettings` object in the `NotFoundException` catch block, providing all required fields. --- lib/app/bloc/app_bloc.dart | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index 9523a255..e3b02733 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -16,7 +16,7 @@ class AppBloc extends Bloc { AppBloc({ required HtAuthRepository authenticationRepository, required HtDataRepository userAppSettingsRepository, - required HtDataRepository appConfigRepository, + required HtDataRepository appConfigRepository, required local_config.AppEnvironment environment, }) : _authenticationRepository = authenticationRepository, _userAppSettingsRepository = userAppSettingsRepository, @@ -35,7 +35,7 @@ class AppBloc extends Bloc { final HtAuthRepository _authenticationRepository; final HtDataRepository _userAppSettingsRepository; - final HtDataRepository _appConfigRepository; + final HtDataRepository _appConfigRepository; late final StreamSubscription _userSubscription; /// Handles user changes and loads initial settings once user is available. @@ -47,8 +47,8 @@ class AppBloc extends Bloc { final AppStatus status; if (user != null && - (user.roles.contains(UserRoles.admin) || - user.roles.contains(UserRoles.publisher))) { + (user.dashboardRole == DashboardUserRole.admin || + user.dashboardRole == DashboardUserRole.publisher)) { status = AppStatus.authenticated; } else { status = AppStatus.unauthenticated; @@ -66,7 +66,23 @@ class AppBloc extends Bloc { emit(state.copyWith(userAppSettings: userAppSettings)); } on NotFoundException { // If settings not found, create default ones - final defaultSettings = UserAppSettings(id: user.id); + const defaultSettings = UserAppSettings( + id: 'default', + displaySettings: DisplaySettings( + baseTheme: AppBaseTheme.system, + accentTheme: AppAccentTheme.defaultBlue, + fontFamily: 'SystemDefault', + textScaleFactor: AppTextScaleFactor.medium, + fontWeight: AppFontWeight.regular, + ), + language: 'en', + feedPreferences: FeedDisplayPreferences( + headlineDensity: HeadlineDensity.standard, + headlineImageStyle: HeadlineImageStyle.largeThumbnail, + showSourceInHeadlineFeed: true, + showPublishDateInHeadlineFeed: true, + ), + ); await _userAppSettingsRepository.create(item: defaultSettings); emit(state.copyWith(userAppSettings: defaultSettings)); } on HtHttpException catch (e) { From be5b355dee5891df65ce92eaa8df3dcc6446cb4c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 18:51:25 +0100 Subject: [PATCH 08/69] refactor(content): migrate bloc from Category to Topic and update pagination --- .../bloc/content_management_bloc.dart | 84 ++++++++++--------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/lib/content_management/bloc/content_management_bloc.dart b/lib/content_management/bloc/content_management_bloc.dart index cad4988c..e8ae83e3 100644 --- a/lib/content_management/bloc/content_management_bloc.dart +++ b/lib/content_management/bloc/content_management_bloc.dart @@ -11,8 +11,8 @@ enum ContentManagementTab { /// Represents the Headlines tab. headlines, - /// Represents the Categories tab. - categories, + /// Represents the Topics tab. + topics, /// Represents the Sources tab. sources, @@ -22,26 +22,26 @@ class ContentManagementBloc extends Bloc { ContentManagementBloc({ required HtDataRepository headlinesRepository, - required HtDataRepository categoriesRepository, + required HtDataRepository topicsRepository, required HtDataRepository sourcesRepository, }) : _headlinesRepository = headlinesRepository, - _categoriesRepository = categoriesRepository, + _topicsRepository = topicsRepository, _sourcesRepository = sourcesRepository, super(const ContentManagementState()) { on(_onContentManagementTabChanged); on(_onLoadHeadlinesRequested); on(_onHeadlineUpdated); on(_onDeleteHeadlineRequested); - on(_onLoadCategoriesRequested); - on(_onCategoryUpdated); - on(_onDeleteCategoryRequested); + on(_onLoadTopicsRequested); + on(_onTopicUpdated); + on(_onDeleteTopicRequested); on(_onLoadSourcesRequested); on(_onSourceUpdated); on(_onOnDeleteSourceRequested); } final HtDataRepository _headlinesRepository; - final HtDataRepository _categoriesRepository; + final HtDataRepository _topicsRepository; final HtDataRepository _sourcesRepository; void _onContentManagementTabChanged( @@ -61,8 +61,10 @@ class ContentManagementBloc final previousHeadlines = isPaginating ? state.headlines : []; final paginatedHeadlines = await _headlinesRepository.readAll( - startAfterId: event.startAfterId, - limit: event.limit, + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), ); emit( state.copyWith( @@ -128,82 +130,82 @@ class ContentManagementBloc } } - Future _onLoadCategoriesRequested( - LoadCategoriesRequested event, + Future _onLoadTopicsRequested( + LoadTopicsRequested event, Emitter emit, ) async { - emit(state.copyWith(categoriesStatus: ContentManagementStatus.loading)); + emit(state.copyWith(topicsStatus: ContentManagementStatus.loading)); try { final isPaginating = event.startAfterId != null; - final previousCategories = isPaginating ? state.categories : []; + final previousTopics = isPaginating ? state.topics : []; - final paginatedCategories = await _categoriesRepository.readAll( - startAfterId: event.startAfterId, - limit: event.limit, + final paginatedTopics = await _topicsRepository.readAll( + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), ); emit( state.copyWith( - categoriesStatus: ContentManagementStatus.success, - categories: [...previousCategories, ...paginatedCategories.items], - categoriesCursor: paginatedCategories.cursor, - categoriesHasMore: paginatedCategories.hasMore, + topicsStatus: ContentManagementStatus.success, + topics: [...previousTopics, ...paginatedTopics.items], + topicsCursor: paginatedTopics.cursor, + topicsHasMore: paginatedTopics.hasMore, ), ); } on HtHttpException catch (e) { emit( state.copyWith( - categoriesStatus: ContentManagementStatus.failure, + topicsStatus: ContentManagementStatus.failure, errorMessage: e.message, ), ); } catch (e) { emit( state.copyWith( - categoriesStatus: ContentManagementStatus.failure, + topicsStatus: ContentManagementStatus.failure, errorMessage: e.toString(), ), ); } } - Future _onDeleteCategoryRequested( - DeleteCategoryRequested event, + Future _onDeleteTopicRequested( + DeleteTopicRequested event, Emitter emit, ) async { try { - await _categoriesRepository.delete(id: event.id); - final updatedCategories = state.categories + await _topicsRepository.delete(id: event.id); + final updatedTopics = state.topics .where((c) => c.id != event.id) .toList(); - emit(state.copyWith(categories: updatedCategories)); + emit(state.copyWith(topics: updatedTopics)); } on HtHttpException catch (e) { emit( state.copyWith( - categoriesStatus: ContentManagementStatus.failure, + topicsStatus: ContentManagementStatus.failure, errorMessage: e.message, ), ); } catch (e) { emit( state.copyWith( - categoriesStatus: ContentManagementStatus.failure, + topicsStatus: ContentManagementStatus.failure, errorMessage: e.toString(), ), ); } } - void _onCategoryUpdated( - CategoryUpdated event, + void _onTopicUpdated( + TopicUpdated event, Emitter emit, ) { - final updatedCategories = List.from(state.categories); - final index = updatedCategories.indexWhere( - (c) => c.id == event.category.id, - ); + final updatedTopics = List.from(state.topics); + final index = updatedTopics.indexWhere((t) => t.id == event.topic.id); if (index != -1) { - updatedCategories[index] = event.category; - emit(state.copyWith(categories: updatedCategories)); + updatedTopics[index] = event.topic; + emit(state.copyWith(topics: updatedTopics)); } } @@ -217,8 +219,10 @@ class ContentManagementBloc final previousSources = isPaginating ? state.sources : []; final paginatedSources = await _sourcesRepository.readAll( - startAfterId: event.startAfterId, - limit: event.limit, + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), ); emit( state.copyWith( From 932e507cd403b293d23b9c8a9a57d87d15c7b33b Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 18:52:31 +0100 Subject: [PATCH 09/69] refactor(content): migrate state and events from Category to Topic --- .../bloc/content_management_state.dart | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/content_management/bloc/content_management_state.dart b/lib/content_management/bloc/content_management_state.dart index d566cc63..8728613e 100644 --- a/lib/content_management/bloc/content_management_state.dart +++ b/lib/content_management/bloc/content_management_state.dart @@ -24,10 +24,10 @@ class ContentManagementState extends Equatable { this.headlines = const [], this.headlinesCursor, this.headlinesHasMore = false, - this.categoriesStatus = ContentManagementStatus.initial, - this.categories = const [], - this.categoriesCursor, - this.categoriesHasMore = false, + this.topicsStatus = ContentManagementStatus.initial, + this.topics = const [], + this.topicsCursor, + this.topicsHasMore = false, this.sourcesStatus = ContentManagementStatus.initial, this.sources = const [], this.sourcesCursor, @@ -50,17 +50,17 @@ class ContentManagementState extends Equatable { /// Indicates if there are more headlines to load. final bool headlinesHasMore; - /// Status of category data operations. - final ContentManagementStatus categoriesStatus; + /// Status of topic data operations. + final ContentManagementStatus topicsStatus; - /// List of categories. - final List categories; + /// List of topics. + final List topics; - /// Cursor for category pagination. - final String? categoriesCursor; + /// Cursor for topic pagination. + final String? topicsCursor; - /// Indicates if there are more categories to load. - final bool categoriesHasMore; + /// Indicates if there are more topics to load. + final bool topicsHasMore; /// Status of source data operations. final ContentManagementStatus sourcesStatus; @@ -84,10 +84,10 @@ class ContentManagementState extends Equatable { List? headlines, String? headlinesCursor, bool? headlinesHasMore, - ContentManagementStatus? categoriesStatus, - List? categories, - String? categoriesCursor, - bool? categoriesHasMore, + ContentManagementStatus? topicsStatus, + List? topics, + String? topicsCursor, + bool? topicsHasMore, ContentManagementStatus? sourcesStatus, List? sources, String? sourcesCursor, @@ -100,10 +100,10 @@ class ContentManagementState extends Equatable { headlines: headlines ?? this.headlines, headlinesCursor: headlinesCursor ?? this.headlinesCursor, headlinesHasMore: headlinesHasMore ?? this.headlinesHasMore, - categoriesStatus: categoriesStatus ?? this.categoriesStatus, - categories: categories ?? this.categories, - categoriesCursor: categoriesCursor ?? this.categoriesCursor, - categoriesHasMore: categoriesHasMore ?? this.categoriesHasMore, + topicsStatus: topicsStatus ?? this.topicsStatus, + topics: topics ?? this.topics, + topicsCursor: topicsCursor ?? this.topicsCursor, + topicsHasMore: topicsHasMore ?? this.topicsHasMore, sourcesStatus: sourcesStatus ?? this.sourcesStatus, sources: sources ?? this.sources, sourcesCursor: sourcesCursor ?? this.sourcesCursor, @@ -119,10 +119,10 @@ class ContentManagementState extends Equatable { headlines, headlinesCursor, headlinesHasMore, - categoriesStatus, - categories, - categoriesCursor, - categoriesHasMore, + topicsStatus, + topics, + topicsCursor, + topicsHasMore, sourcesStatus, sources, sourcesCursor, From 0fa16b69d8a346b1f53457610c910f5a7b41cd96 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:04:44 +0100 Subject: [PATCH 10/69] refactor(content): migrate create_category bloc to create_topic --- .../create_category/create_category_bloc.dart | 115 ----------------- .../bloc/create_topic/create_topic_bloc.dart | 117 ++++++++++++++++++ .../create_topic_event.dart} | 2 +- .../create_topic_state.dart} | 2 +- .../edit_topic_bloc.dart} | 4 +- .../edit_topic_event.dart} | 2 +- .../edit_topic_state.dart} | 2 +- .../view/create_category_page.dart | 2 +- .../view/edit_category_page.dart | 2 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 11 files changed, 127 insertions(+), 124 deletions(-) delete mode 100644 lib/content_management/bloc/create_category/create_category_bloc.dart create mode 100644 lib/content_management/bloc/create_topic/create_topic_bloc.dart rename lib/content_management/bloc/{create_category/create_category_event.dart => create_topic/create_topic_event.dart} (97%) rename lib/content_management/bloc/{create_category/create_category_state.dart => create_topic/create_topic_state.dart} (97%) rename lib/content_management/bloc/{edit_category/edit_category_bloc.dart => edit_topic/edit_topic_bloc.dart} (98%) rename lib/content_management/bloc/{edit_category/edit_category_event.dart => edit_topic/edit_topic_event.dart} (97%) rename lib/content_management/bloc/{edit_category/edit_category_state.dart => edit_topic/edit_topic_state.dart} (98%) diff --git a/lib/content_management/bloc/create_category/create_category_bloc.dart b/lib/content_management/bloc/create_category/create_category_bloc.dart deleted file mode 100644 index 773d6e34..00000000 --- a/lib/content_management/bloc/create_category/create_category_bloc.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:ht_data_repository/ht_data_repository.dart'; -import 'package:ht_shared/ht_shared.dart'; - -part 'create_category_event.dart'; -part 'create_category_state.dart'; - -/// A BLoC to manage the state of creating a new category. -class CreateCategoryBloc - extends Bloc { - /// {@macro create_category_bloc} - CreateCategoryBloc({ - required HtDataRepository categoriesRepository, - }) : _categoriesRepository = categoriesRepository, - super(const CreateCategoryState()) { - on(_onNameChanged); - on(_onDescriptionChanged); - on(_onIconUrlChanged); - on(_onStatusChanged); - on(_onSubmitted); - } - - final HtDataRepository _categoriesRepository; - - void _onNameChanged( - CreateCategoryNameChanged event, - Emitter emit, - ) { - emit( - state.copyWith( - name: event.name, - status: CreateCategoryStatus.initial, - ), - ); - } - - void _onDescriptionChanged( - CreateCategoryDescriptionChanged event, - Emitter emit, - ) { - emit( - state.copyWith( - description: event.description, - status: CreateCategoryStatus.initial, - ), - ); - } - - void _onIconUrlChanged( - CreateCategoryIconUrlChanged event, - Emitter emit, - ) { - emit( - state.copyWith( - iconUrl: event.iconUrl, - status: CreateCategoryStatus.initial, - ), - ); - } - - void _onStatusChanged( - CreateCategoryStatusChanged event, - Emitter emit, - ) { - emit( - state.copyWith( - contentStatus: event.status, - status: CreateCategoryStatus.initial, - ), - ); - } - - Future _onSubmitted( - CreateCategorySubmitted event, - Emitter emit, - ) async { - if (!state.isFormValid) return; - - emit(state.copyWith(status: CreateCategoryStatus.submitting)); - try { - final now = DateTime.now(); - final newCategory = Category( - name: state.name, - description: state.description.isNotEmpty ? state.description : null, - iconUrl: state.iconUrl.isNotEmpty ? state.iconUrl : null, - status: state.contentStatus, - createdAt: now, - updatedAt: now, - ); - - await _categoriesRepository.create(item: newCategory); - emit( - state.copyWith( - status: CreateCategoryStatus.success, - createdCategory: newCategory, - ), - ); - } on HtHttpException catch (e) { - emit( - state.copyWith( - status: CreateCategoryStatus.failure, - errorMessage: e.message, - ), - ); - } catch (e) { - emit( - state.copyWith( - status: CreateCategoryStatus.failure, - errorMessage: e.toString(), - ), - ); - } - } -} diff --git a/lib/content_management/bloc/create_topic/create_topic_bloc.dart b/lib/content_management/bloc/create_topic/create_topic_bloc.dart new file mode 100644 index 00000000..f7f17465 --- /dev/null +++ b/lib/content_management/bloc/create_topic/create_topic_bloc.dart @@ -0,0 +1,117 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_shared/ht_shared.dart'; +import 'package:uuid/uuid.dart'; + +part 'create_topic_event.dart'; +part 'create_topic_state.dart'; + +/// A BLoC to manage the state of creating a new topic. +class CreateTopicBloc extends Bloc { + /// {@macro create_topic_bloc} + CreateTopicBloc({ + required HtDataRepository topicsRepository, + }) : _topicsRepository = topicsRepository, + super(const CreateTopicState()) { + on(_onNameChanged); + on(_onDescriptionChanged); + on(_onIconUrlChanged); + on(_onStatusChanged); + on(_onSubmitted); + } + + final HtDataRepository _topicsRepository; + final _uuid = const Uuid(); + + void _onNameChanged( + CreateTopicNameChanged event, + Emitter emit, + ) { + emit( + state.copyWith( + name: event.name, + status: CreateTopicStatus.initial, + ), + ); + } + + void _onDescriptionChanged( + CreateTopicDescriptionChanged event, + Emitter emit, + ) { + emit( + state.copyWith( + description: event.description, + status: CreateTopicStatus.initial, + ), + ); + } + + void _onIconUrlChanged( + CreateTopicIconUrlChanged event, + Emitter emit, + ) { + emit( + state.copyWith( + iconUrl: event.iconUrl, + status: CreateTopicStatus.initial, + ), + ); + } + + void _onStatusChanged( + CreateTopicStatusChanged event, + Emitter emit, + ) { + emit( + state.copyWith( + contentStatus: event.status, + status: CreateTopicStatus.initial, + ), + ); + } + + Future _onSubmitted( + CreateTopicSubmitted event, + Emitter emit, + ) async { + if (!state.isFormValid) return; + + emit(state.copyWith(status: CreateTopicStatus.submitting)); + try { + final now = DateTime.now(); + final newTopic = Topic( + id: _uuid.v4(), + name: state.name, + description: state.description, + iconUrl: state.iconUrl, + status: state.contentStatus, + createdAt: now, + updatedAt: now, + ); + + await _topicsRepository.create(item: newTopic); + emit( + state.copyWith( + status: CreateTopicStatus.success, + createdTopic: newTopic, + ), + ); + } on HtHttpException catch (e) { + emit( + state.copyWith( + status: CreateTopicStatus.failure, + errorMessage: e.message, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: CreateTopicStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } +} diff --git a/lib/content_management/bloc/create_category/create_category_event.dart b/lib/content_management/bloc/create_topic/create_topic_event.dart similarity index 97% rename from lib/content_management/bloc/create_category/create_category_event.dart rename to lib/content_management/bloc/create_topic/create_topic_event.dart index 591e5706..35837061 100644 --- a/lib/content_management/bloc/create_category/create_category_event.dart +++ b/lib/content_management/bloc/create_topic/create_topic_event.dart @@ -1,4 +1,4 @@ -part of 'create_category_bloc.dart'; +part of 'create_topic_bloc.dart'; /// Base class for all events related to the [CreateCategoryBloc]. sealed class CreateCategoryEvent extends Equatable { diff --git a/lib/content_management/bloc/create_category/create_category_state.dart b/lib/content_management/bloc/create_topic/create_topic_state.dart similarity index 97% rename from lib/content_management/bloc/create_category/create_category_state.dart rename to lib/content_management/bloc/create_topic/create_topic_state.dart index ec6a272e..14634d38 100644 --- a/lib/content_management/bloc/create_category/create_category_state.dart +++ b/lib/content_management/bloc/create_topic/create_topic_state.dart @@ -1,4 +1,4 @@ -part of 'create_category_bloc.dart'; +part of 'create_topic_bloc.dart'; /// Represents the status of the create category operation. enum CreateCategoryStatus { diff --git a/lib/content_management/bloc/edit_category/edit_category_bloc.dart b/lib/content_management/bloc/edit_topic/edit_topic_bloc.dart similarity index 98% rename from lib/content_management/bloc/edit_category/edit_category_bloc.dart rename to lib/content_management/bloc/edit_topic/edit_topic_bloc.dart index 04d08ca6..b060a252 100644 --- a/lib/content_management/bloc/edit_category/edit_category_bloc.dart +++ b/lib/content_management/bloc/edit_topic/edit_topic_bloc.dart @@ -3,8 +3,8 @@ import 'package:equatable/equatable.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; -part 'edit_category_event.dart'; -part 'edit_category_state.dart'; +part 'edit_topic_event.dart'; +part 'edit_topic_state.dart'; /// A BLoC to manage the state of editing a single category. class EditCategoryBloc extends Bloc { diff --git a/lib/content_management/bloc/edit_category/edit_category_event.dart b/lib/content_management/bloc/edit_topic/edit_topic_event.dart similarity index 97% rename from lib/content_management/bloc/edit_category/edit_category_event.dart rename to lib/content_management/bloc/edit_topic/edit_topic_event.dart index f8301950..13d57ec2 100644 --- a/lib/content_management/bloc/edit_category/edit_category_event.dart +++ b/lib/content_management/bloc/edit_topic/edit_topic_event.dart @@ -1,4 +1,4 @@ -part of 'edit_category_bloc.dart'; +part of 'edit_topic_bloc.dart'; /// Base class for all events related to the [EditCategoryBloc]. sealed class EditCategoryEvent extends Equatable { diff --git a/lib/content_management/bloc/edit_category/edit_category_state.dart b/lib/content_management/bloc/edit_topic/edit_topic_state.dart similarity index 98% rename from lib/content_management/bloc/edit_category/edit_category_state.dart rename to lib/content_management/bloc/edit_topic/edit_topic_state.dart index 0ec53432..8399bb5b 100644 --- a/lib/content_management/bloc/edit_category/edit_category_state.dart +++ b/lib/content_management/bloc/edit_topic/edit_topic_state.dart @@ -1,4 +1,4 @@ -part of 'edit_category_bloc.dart'; +part of 'edit_topic_bloc.dart'; /// Represents the status of the edit category operation. enum EditCategoryStatus { diff --git a/lib/content_management/view/create_category_page.dart b/lib/content_management/view/create_category_page.dart index bb546213..81391de9 100644 --- a/lib/content_management/view/create_category_page.dart +++ b/lib/content_management/view/create_category_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart'; -import 'package:ht_dashboard/content_management/bloc/create_category/create_category_bloc.dart'; +import 'package:ht_dashboard/content_management/bloc/create_topic/create_topic_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/shared.dart'; diff --git a/lib/content_management/view/edit_category_page.dart b/lib/content_management/view/edit_category_page.dart index 2a888969..ead35af8 100644 --- a/lib/content_management/view/edit_category_page.dart +++ b/lib/content_management/view/edit_category_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart'; -import 'package:ht_dashboard/content_management/bloc/edit_category/edit_category_bloc.dart'; +import 'package:ht_dashboard/content_management/bloc/edit_topic/edit_topic_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; diff --git a/pubspec.lock b/pubspec.lock index eb5b409c..6e85eec2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -570,7 +570,7 @@ packages: source: hosted version: "1.4.0" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff diff --git a/pubspec.yaml b/pubspec.yaml index 89a80c4c..f47610d2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: intl: ^0.20.2 logging: ^1.3.0 timeago: ^3.7.1 + uuid: ^4.5.1 dev_dependencies: From 04e34846b76c0b8fc1857438234f879edc65bc29 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:05:44 +0100 Subject: [PATCH 11/69] refactor(content): migrate create_topic state and event files --- .../bloc/create_topic/create_topic_event.dart | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/content_management/bloc/create_topic/create_topic_event.dart b/lib/content_management/bloc/create_topic/create_topic_event.dart index 35837061..7faa8bfd 100644 --- a/lib/content_management/bloc/create_topic/create_topic_event.dart +++ b/lib/content_management/bloc/create_topic/create_topic_event.dart @@ -1,40 +1,40 @@ part of 'create_topic_bloc.dart'; -/// Base class for all events related to the [CreateCategoryBloc]. -sealed class CreateCategoryEvent extends Equatable { - const CreateCategoryEvent(); +/// Base class for all events related to the [CreateTopicBloc]. +sealed class CreateTopicEvent extends Equatable { + const CreateTopicEvent(); @override List get props => []; } -/// Event for when the category's name is changed. -final class CreateCategoryNameChanged extends CreateCategoryEvent { - const CreateCategoryNameChanged(this.name); +/// Event for when the topic's name is changed. +final class CreateTopicNameChanged extends CreateTopicEvent { + const CreateTopicNameChanged(this.name); final String name; @override List get props => [name]; } -/// Event for when the category's description is changed. -final class CreateCategoryDescriptionChanged extends CreateCategoryEvent { - const CreateCategoryDescriptionChanged(this.description); +/// Event for when the topic's description is changed. +final class CreateTopicDescriptionChanged extends CreateTopicEvent { + const CreateTopicDescriptionChanged(this.description); final String description; @override List get props => [description]; } -/// Event for when the category's icon URL is changed. -final class CreateCategoryIconUrlChanged extends CreateCategoryEvent { - const CreateCategoryIconUrlChanged(this.iconUrl); +/// Event for when the topic's icon URL is changed. +final class CreateTopicIconUrlChanged extends CreateTopicEvent { + const CreateTopicIconUrlChanged(this.iconUrl); final String iconUrl; @override List get props => [iconUrl]; } -/// Event for when the category's status is changed. -final class CreateCategoryStatusChanged extends CreateCategoryEvent { - const CreateCategoryStatusChanged(this.status); +/// Event for when the topic's status is changed. +final class CreateTopicStatusChanged extends CreateTopicEvent { + const CreateTopicStatusChanged(this.status); final ContentStatus status; @override @@ -42,6 +42,6 @@ final class CreateCategoryStatusChanged extends CreateCategoryEvent { } /// Event to signal that the form should be submitted. -final class CreateCategorySubmitted extends CreateCategoryEvent { - const CreateCategorySubmitted(); +final class CreateTopicSubmitted extends CreateTopicEvent { + const CreateTopicSubmitted(); } From 742d5bac5537292441742a1d6b4e3b8f6607209e Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:08:21 +0100 Subject: [PATCH 12/69] fix(content): correct create_topic_state to use Topic model --- .../bloc/create_topic/create_topic_state.dart | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/content_management/bloc/create_topic/create_topic_state.dart b/lib/content_management/bloc/create_topic/create_topic_state.dart index 14634d38..c5e4c0d3 100644 --- a/lib/content_management/bloc/create_topic/create_topic_state.dart +++ b/lib/content_management/bloc/create_topic/create_topic_state.dart @@ -1,7 +1,7 @@ part of 'create_topic_bloc.dart'; -/// Represents the status of the create category operation. -enum CreateCategoryStatus { +/// Represents the status of the create topic operation. +enum CreateTopicStatus { /// Initial state. initial, @@ -15,59 +15,59 @@ enum CreateCategoryStatus { failure, } -/// The state for the [CreateCategoryBloc]. -final class CreateCategoryState extends Equatable { - /// {@macro create_category_state} - const CreateCategoryState({ - this.status = CreateCategoryStatus.initial, +/// The state for the [CreateTopicBloc]. +final class CreateTopicState extends Equatable { + /// {@macro create_topic_state} + const CreateTopicState({ + this.status = CreateTopicStatus.initial, this.name = '', this.description = '', this.iconUrl = '', this.contentStatus = ContentStatus.active, this.errorMessage, - this.createdCategory, + this.createdTopic, }); - final CreateCategoryStatus status; + final CreateTopicStatus status; final String name; final String description; final String iconUrl; final ContentStatus contentStatus; final String? errorMessage; - final Category? createdCategory; + final Topic? createdTopic; /// Returns true if the form is valid and can be submitted. - /// Based on the Category model, only the name is required. + /// Based on the Topic model, only the name is required. bool get isFormValid => name.isNotEmpty; - CreateCategoryState copyWith({ - CreateCategoryStatus? status, + CreateTopicState copyWith({ + CreateTopicStatus? status, String? name, String? description, String? iconUrl, ContentStatus? contentStatus, String? errorMessage, - Category? createdCategory, + Topic? createdTopic, }) { - return CreateCategoryState( + return CreateTopicState( status: status ?? this.status, name: name ?? this.name, description: description ?? this.description, iconUrl: iconUrl ?? this.iconUrl, contentStatus: contentStatus ?? this.contentStatus, errorMessage: errorMessage, - createdCategory: createdCategory ?? this.createdCategory, + createdTopic: createdTopic ?? this.createdTopic, ); } @override List get props => [ - status, - name, - description, - iconUrl, - contentStatus, - errorMessage, - createdCategory, - ]; + status, + name, + description, + iconUrl, + contentStatus, + errorMessage, + createdTopic, + ]; } From 6c09c72dcc93eb3e91a08eecb001c7442791ce34 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:10:03 +0100 Subject: [PATCH 13/69] refactor(content): migrate edit_category bloc to edit_topic --- .../bloc/edit_topic/edit_topic_bloc.dart | 118 +++++++++--------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/lib/content_management/bloc/edit_topic/edit_topic_bloc.dart b/lib/content_management/bloc/edit_topic/edit_topic_bloc.dart index b060a252..32b1a5f2 100644 --- a/lib/content_management/bloc/edit_topic/edit_topic_bloc.dart +++ b/lib/content_management/bloc/edit_topic/edit_topic_bloc.dart @@ -6,54 +6,54 @@ import 'package:ht_shared/ht_shared.dart'; part 'edit_topic_event.dart'; part 'edit_topic_state.dart'; -/// A BLoC to manage the state of editing a single category. -class EditCategoryBloc extends Bloc { - /// {@macro edit_category_bloc} - EditCategoryBloc({ - required HtDataRepository categoriesRepository, - required String categoryId, - }) : _categoriesRepository = categoriesRepository, - _categoryId = categoryId, - super(const EditCategoryState()) { - on(_onLoaded); - on(_onNameChanged); - on(_onDescriptionChanged); - on(_onIconUrlChanged); - on(_onStatusChanged); - on(_onSubmitted); +/// A BLoC to manage the state of editing a single topic. +class EditTopicBloc extends Bloc { + /// {@macro edit_topic_bloc} + EditTopicBloc({ + required HtDataRepository topicsRepository, + required String topicId, + }) : _topicsRepository = topicsRepository, + _topicId = topicId, + super(const EditTopicState()) { + on(_onLoaded); + on(_onNameChanged); + on(_onDescriptionChanged); + on(_onIconUrlChanged); + on(_onStatusChanged); + on(_onSubmitted); } - final HtDataRepository _categoriesRepository; - final String _categoryId; + final HtDataRepository _topicsRepository; + final String _topicId; Future _onLoaded( - EditCategoryLoaded event, - Emitter emit, + EditTopicLoaded event, + Emitter emit, ) async { - emit(state.copyWith(status: EditCategoryStatus.loading)); + emit(state.copyWith(status: EditTopicStatus.loading)); try { - final category = await _categoriesRepository.read(id: _categoryId); + final topic = await _topicsRepository.read(id: _topicId); emit( state.copyWith( - status: EditCategoryStatus.initial, - initialCategory: category, - name: category.name, - description: category.description ?? '', - iconUrl: category.iconUrl ?? '', - contentStatus: category.status, + status: EditTopicStatus.initial, + initialTopic: topic, + name: topic.name, + description: topic.description, + iconUrl: topic.iconUrl, + contentStatus: topic.status, ), ); } on HtHttpException catch (e) { emit( state.copyWith( - status: EditCategoryStatus.failure, + status: EditTopicStatus.failure, errorMessage: e.message, ), ); } catch (e) { emit( state.copyWith( - status: EditCategoryStatus.failure, + status: EditTopicStatus.failure, errorMessage: e.toString(), ), ); @@ -61,104 +61,104 @@ class EditCategoryBloc extends Bloc { } void _onNameChanged( - EditCategoryNameChanged event, - Emitter emit, + EditTopicNameChanged event, + Emitter emit, ) { emit( state.copyWith( name: event.name, // Reset status to allow for re-submission after a failure. - status: EditCategoryStatus.initial, + status: EditTopicStatus.initial, ), ); } void _onDescriptionChanged( - EditCategoryDescriptionChanged event, - Emitter emit, + EditTopicDescriptionChanged event, + Emitter emit, ) { emit( state.copyWith( description: event.description, - status: EditCategoryStatus.initial, + status: EditTopicStatus.initial, ), ); } void _onIconUrlChanged( - EditCategoryIconUrlChanged event, - Emitter emit, + EditTopicIconUrlChanged event, + Emitter emit, ) { emit( state.copyWith( iconUrl: event.iconUrl, - status: EditCategoryStatus.initial, + status: EditTopicStatus.initial, ), ); } void _onStatusChanged( - EditCategoryStatusChanged event, - Emitter emit, + EditTopicStatusChanged event, + Emitter emit, ) { emit( state.copyWith( contentStatus: event.status, - status: EditCategoryStatus.initial, + status: EditTopicStatus.initial, ), ); } Future _onSubmitted( - EditCategorySubmitted event, - Emitter emit, + EditTopicSubmitted event, + Emitter emit, ) async { if (!state.isFormValid) return; - // Safely access the initial category to prevent null errors. - final initialCategory = state.initialCategory; - if (initialCategory == null) { + // Safely access the initial topic to prevent null errors. + final initialTopic = state.initialTopic; + if (initialTopic == null) { emit( state.copyWith( - status: EditCategoryStatus.failure, - errorMessage: 'Cannot update: Original category data not loaded.', + status: EditTopicStatus.failure, + errorMessage: 'Cannot update: Original topic data not loaded.', ), ); return; } - emit(state.copyWith(status: EditCategoryStatus.submitting)); + emit(state.copyWith(status: EditTopicStatus.submitting)); try { // Use null for empty optional fields, which is cleaner for APIs. - final updatedCategory = initialCategory.copyWith( + final updatedTopic = initialTopic.copyWith( name: state.name, - description: state.description.isNotEmpty ? state.description : null, - iconUrl: state.iconUrl.isNotEmpty ? state.iconUrl : null, + description: state.description, + iconUrl: state.iconUrl, status: state.contentStatus, updatedAt: DateTime.now(), ); - await _categoriesRepository.update( - id: _categoryId, - item: updatedCategory, + await _topicsRepository.update( + id: _topicId, + item: updatedTopic, ); emit( state.copyWith( - status: EditCategoryStatus.success, - updatedCategory: updatedCategory, + status: EditTopicStatus.success, + updatedTopic: updatedTopic, ), ); } on HtHttpException catch (e) { emit( state.copyWith( - status: EditCategoryStatus.failure, + status: EditTopicStatus.failure, errorMessage: e.message, ), ); } catch (e) { emit( state.copyWith( - status: EditCategoryStatus.failure, + status: EditTopicStatus.failure, errorMessage: e.toString(), ), ); From 4a147aaa92906c291cb53c50805a5d9c3ca88b8d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:11:43 +0100 Subject: [PATCH 14/69] refactor(content): migrate edit_topic state and event files --- .../bloc/edit_topic/edit_topic_event.dart | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/content_management/bloc/edit_topic/edit_topic_event.dart b/lib/content_management/bloc/edit_topic/edit_topic_event.dart index 13d57ec2..83b5fbb4 100644 --- a/lib/content_management/bloc/edit_topic/edit_topic_event.dart +++ b/lib/content_management/bloc/edit_topic/edit_topic_event.dart @@ -1,21 +1,21 @@ part of 'edit_topic_bloc.dart'; -/// Base class for all events related to the [EditCategoryBloc]. -sealed class EditCategoryEvent extends Equatable { - const EditCategoryEvent(); +/// Base class for all events related to the [EditTopicBloc]. +sealed class EditTopicEvent extends Equatable { + const EditTopicEvent(); @override List get props => []; } -/// Event to load the initial category data for editing. -final class EditCategoryLoaded extends EditCategoryEvent { - const EditCategoryLoaded(); +/// Event to load the initial topic data for editing. +final class EditTopicLoaded extends EditTopicEvent { + const EditTopicLoaded(); } -/// Event triggered when the category name input changes. -final class EditCategoryNameChanged extends EditCategoryEvent { - const EditCategoryNameChanged(this.name); +/// Event triggered when the topic name input changes. +final class EditTopicNameChanged extends EditTopicEvent { + const EditTopicNameChanged(this.name); final String name; @@ -23,9 +23,9 @@ final class EditCategoryNameChanged extends EditCategoryEvent { List get props => [name]; } -/// Event triggered when the category description input changes. -final class EditCategoryDescriptionChanged extends EditCategoryEvent { - const EditCategoryDescriptionChanged(this.description); +/// Event triggered when the topic description input changes. +final class EditTopicDescriptionChanged extends EditTopicEvent { + const EditTopicDescriptionChanged(this.description); final String description; @@ -33,9 +33,9 @@ final class EditCategoryDescriptionChanged extends EditCategoryEvent { List get props => [description]; } -/// Event triggered when the category icon URL input changes. -final class EditCategoryIconUrlChanged extends EditCategoryEvent { - const EditCategoryIconUrlChanged(this.iconUrl); +/// Event triggered when the topic icon URL input changes. +final class EditTopicIconUrlChanged extends EditTopicEvent { + const EditTopicIconUrlChanged(this.iconUrl); final String iconUrl; @@ -43,9 +43,9 @@ final class EditCategoryIconUrlChanged extends EditCategoryEvent { List get props => [iconUrl]; } -/// Event for when the category's status is changed. -final class EditCategoryStatusChanged extends EditCategoryEvent { - const EditCategoryStatusChanged(this.status); +/// Event for when the topic's status is changed. +final class EditTopicStatusChanged extends EditTopicEvent { + const EditTopicStatusChanged(this.status); final ContentStatus status; @@ -53,7 +53,7 @@ final class EditCategoryStatusChanged extends EditCategoryEvent { List get props => [status]; } -/// Event to submit the edited category data. -final class EditCategorySubmitted extends EditCategoryEvent { - const EditCategorySubmitted(); +/// Event to submit the edited topic data. +final class EditTopicSubmitted extends EditTopicEvent { + const EditTopicSubmitted(); } From 0caa4ac7f8d9933f69242394829435a1a72f89aa Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:12:54 +0100 Subject: [PATCH 15/69] fix(content): refactor edit_topic_state to use Topic model --- .../bloc/edit_topic/edit_topic_state.dart | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/content_management/bloc/edit_topic/edit_topic_state.dart b/lib/content_management/bloc/edit_topic/edit_topic_state.dart index 8399bb5b..7a8e92c8 100644 --- a/lib/content_management/bloc/edit_topic/edit_topic_state.dart +++ b/lib/content_management/bloc/edit_topic/edit_topic_state.dart @@ -1,7 +1,7 @@ part of 'edit_topic_bloc.dart'; -/// Represents the status of the edit category operation. -enum EditCategoryStatus { +/// Represents the status of the edit topic operation. +enum EditTopicStatus { /// Initial state, before any data is loaded. initial, @@ -18,62 +18,62 @@ enum EditCategoryStatus { submitting, } -/// The state for the [EditCategoryBloc]. -final class EditCategoryState extends Equatable { - const EditCategoryState({ - this.status = EditCategoryStatus.initial, - this.initialCategory, +/// The state for the [EditTopicBloc]. +final class EditTopicState extends Equatable { + const EditTopicState({ + this.status = EditTopicStatus.initial, + this.initialTopic, this.name = '', this.description = '', this.iconUrl = '', this.contentStatus = ContentStatus.active, this.errorMessage, - this.updatedCategory, + this.updatedTopic, }); - final EditCategoryStatus status; - final Category? initialCategory; + final EditTopicStatus status; + final Topic? initialTopic; final String name; final String description; final String iconUrl; final ContentStatus contentStatus; final String? errorMessage; - final Category? updatedCategory; + final Topic? updatedTopic; /// Returns true if the form is valid and can be submitted. bool get isFormValid => name.isNotEmpty; - EditCategoryState copyWith({ - EditCategoryStatus? status, - Category? initialCategory, + EditTopicState copyWith({ + EditTopicStatus? status, + Topic? initialTopic, String? name, String? description, String? iconUrl, ContentStatus? contentStatus, String? errorMessage, - Category? updatedCategory, + Topic? updatedTopic, }) { - return EditCategoryState( + return EditTopicState( status: status ?? this.status, - initialCategory: initialCategory ?? this.initialCategory, + initialTopic: initialTopic ?? this.initialTopic, name: name ?? this.name, description: description ?? this.description, iconUrl: iconUrl ?? this.iconUrl, contentStatus: contentStatus ?? this.contentStatus, errorMessage: errorMessage ?? this.errorMessage, - updatedCategory: updatedCategory ?? this.updatedCategory, + updatedTopic: updatedTopic ?? this.updatedTopic, ); } @override List get props => [ - status, - initialCategory, - name, - description, - iconUrl, - contentStatus, - errorMessage, - updatedCategory, - ]; + status, + initialTopic, + name, + description, + iconUrl, + contentStatus, + errorMessage, + updatedTopic, + ]; } From f30824a965674c110f4c38163220c663dc37880b Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:16:40 +0100 Subject: [PATCH 16/69] refactor(headline): begin create_headline_bloc migration to Topic --- .../bloc/create_headline/create_headline_bloc.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_bloc.dart b/lib/content_management/bloc/create_headline/create_headline_bloc.dart index 45f029dd..62c600ef 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -1,6 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart' hide Category; +import 'package:flutter/foundation.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; @@ -14,10 +14,10 @@ class CreateHeadlineBloc CreateHeadlineBloc({ required HtDataRepository headlinesRepository, required HtDataRepository sourcesRepository, - required HtDataRepository categoriesRepository, + required HtDataRepository topicsRepository, }) : _headlinesRepository = headlinesRepository, _sourcesRepository = sourcesRepository, - _categoriesRepository = categoriesRepository, + _topicsRepository = topicsRepository, super(const CreateHeadlineState()) { on(_onDataLoaded); on(_onTitleChanged); @@ -25,14 +25,14 @@ class CreateHeadlineBloc on(_onUrlChanged); on(_onImageUrlChanged); on(_onSourceChanged); - on(_onCategoryChanged); + on(_onTopicChanged); on(_onStatusChanged); on(_onSubmitted); } final HtDataRepository _headlinesRepository; final HtDataRepository _sourcesRepository; - final HtDataRepository _categoriesRepository; + final HtDataRepository _topicsRepository; Future _onDataLoaded( CreateHeadlineDataLoaded event, From a985c951b08860454bbbb3728f08d8f5fcd899ec Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:17:59 +0100 Subject: [PATCH 17/69] refactor(headline): migrate create_headline state and events to Topic --- .../create_headline_state.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_state.dart b/lib/content_management/bloc/create_headline/create_headline_state.dart index 605b078e..d9c17937 100644 --- a/lib/content_management/bloc/create_headline/create_headline_state.dart +++ b/lib/content_management/bloc/create_headline/create_headline_state.dart @@ -27,9 +27,9 @@ final class CreateHeadlineState extends Equatable { this.url = '', this.imageUrl = '', this.source, - this.category, + this.topic, this.sources = const [], - this.categories = const [], + this.topics = const [], this.contentStatus = ContentStatus.active, this.errorMessage, this.createdHeadline, @@ -41,9 +41,9 @@ final class CreateHeadlineState extends Equatable { final String url; final String imageUrl; final Source? source; - final Category? category; + final Topic? topic; final List sources; - final List categories; + final List topics; final ContentStatus contentStatus; final String? errorMessage; final Headline? createdHeadline; @@ -58,9 +58,9 @@ final class CreateHeadlineState extends Equatable { String? url, String? imageUrl, ValueGetter? source, - ValueGetter? category, + ValueGetter? topic, List? sources, - List? categories, + List? topics, ContentStatus? contentStatus, String? errorMessage, Headline? createdHeadline, @@ -72,9 +72,9 @@ final class CreateHeadlineState extends Equatable { url: url ?? this.url, imageUrl: imageUrl ?? this.imageUrl, source: source != null ? source() : this.source, - category: category != null ? category() : this.category, + topic: topic != null ? topic() : this.topic, sources: sources ?? this.sources, - categories: categories ?? this.categories, + topics: topics ?? this.topics, contentStatus: contentStatus ?? this.contentStatus, errorMessage: errorMessage, createdHeadline: createdHeadline ?? this.createdHeadline, @@ -89,9 +89,9 @@ final class CreateHeadlineState extends Equatable { url, imageUrl, source, - category, + topic, sources, - categories, + topics, contentStatus, errorMessage, createdHeadline, From 27aa2b959ee666b97ae663ddd194bb0e2c04505c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:20:09 +0100 Subject: [PATCH 18/69] refactor(headline): complete create_headline_bloc migration to new model --- .../create_headline/create_headline_bloc.dart | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_bloc.dart b/lib/content_management/bloc/create_headline/create_headline_bloc.dart index 62c600ef..3d91fdc6 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -1,8 +1,9 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' hide Topic; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:uuid/uuid.dart'; part 'create_headline_event.dart'; part 'create_headline_state.dart'; @@ -15,10 +16,12 @@ class CreateHeadlineBloc required HtDataRepository headlinesRepository, required HtDataRepository sourcesRepository, required HtDataRepository topicsRepository, - }) : _headlinesRepository = headlinesRepository, - _sourcesRepository = sourcesRepository, - _topicsRepository = topicsRepository, - super(const CreateHeadlineState()) { + required HtDataRepository countriesRepository, + }) : _headlinesRepository = headlinesRepository, + _sourcesRepository = sourcesRepository, + _topicsRepository = topicsRepository, + _countriesRepository = countriesRepository, + super(const CreateHeadlineState()) { on(_onDataLoaded); on(_onTitleChanged); on(_onDescriptionChanged); @@ -26,6 +29,7 @@ class CreateHeadlineBloc on(_onImageUrlChanged); on(_onSourceChanged); on(_onTopicChanged); + on(_onCountryChanged); on(_onStatusChanged); on(_onSubmitted); } @@ -33,6 +37,8 @@ class CreateHeadlineBloc final HtDataRepository _headlinesRepository; final HtDataRepository _sourcesRepository; final HtDataRepository _topicsRepository; + final HtDataRepository _countriesRepository; + final _uuid = const Uuid(); Future _onDataLoaded( CreateHeadlineDataLoaded event, @@ -40,20 +46,23 @@ class CreateHeadlineBloc ) async { emit(state.copyWith(status: CreateHeadlineStatus.loading)); try { - final [sourcesResponse, categoriesResponse] = await Future.wait([ + final [sourcesResponse, topicsResponse, countriesResponse] = + await Future.wait([ _sourcesRepository.readAll(), - _categoriesRepository.readAll(), + _topicsRepository.readAll(), + _countriesRepository.readAll(), ]); - final sources = (sourcesResponse as PaginatedResponse).items; - final categories = - (categoriesResponse as PaginatedResponse).items; + final sources = sourcesResponse.items; + final topics = topicsResponse.items; + final countries = countriesResponse.items; emit( state.copyWith( status: CreateHeadlineStatus.initial, sources: sources, - categories: categories, + topics: topics, + countries: countries, ), ); } on HtHttpException catch (e) { @@ -108,11 +117,18 @@ class CreateHeadlineBloc emit(state.copyWith(source: () => event.source)); } - void _onCategoryChanged( - CreateHeadlineCategoryChanged event, + void _onTopicChanged( + CreateHeadlineTopicChanged event, Emitter emit, ) { - emit(state.copyWith(category: () => event.category)); + emit(state.copyWith(topic: () => event.topic)); + } + + void _onCountryChanged( + CreateHeadlineCountryChanged event, + Emitter emit, + ) { + emit(state.copyWith(eventCountry: () => event.country)); } void _onStatusChanged( @@ -137,15 +153,16 @@ class CreateHeadlineBloc try { final now = DateTime.now(); final newHeadline = Headline( + id: _uuid.v4(), title: state.title, - description: state.description.isNotEmpty ? state.description : null, - url: state.url.isNotEmpty ? state.url : null, - imageUrl: state.imageUrl.isNotEmpty ? state.imageUrl : null, - source: state.source, - publishedAt: now, + excerpt: state.description, + url: state.url, + imageUrl: state.imageUrl, + source: state.source!, + eventCountry: state.eventCountry!, + topic: state.topic!, createdAt: now, updatedAt: now, - category: state.category, status: state.contentStatus, ); From 28e39e96b1ed80a5ebaf35f600912888428d6764 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:23:21 +0100 Subject: [PATCH 19/69] fix(headline): align create_headline_state with Headline model using excerpt --- .../bloc/create_headline/create_headline_bloc.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_bloc.dart b/lib/content_management/bloc/create_headline/create_headline_bloc.dart index 3d91fdc6..f7d94049 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -93,7 +93,7 @@ class CreateHeadlineBloc CreateHeadlineDescriptionChanged event, Emitter emit, ) { - emit(state.copyWith(description: event.description)); + emit(state.copyWith(excerpt: event.description)); } void _onUrlChanged( @@ -155,7 +155,7 @@ class CreateHeadlineBloc final newHeadline = Headline( id: _uuid.v4(), title: state.title, - excerpt: state.description, + excerpt: state.excerpt, url: state.url, imageUrl: state.imageUrl, source: state.source!, From 325832fc06d957dcb7f7038dc7748bfdc18c1461 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:24:38 +0100 Subject: [PATCH 20/69] fix(headline): synchronize create_headline event and bloc with excerpt field --- .../bloc/create_headline/create_headline_bloc.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_bloc.dart b/lib/content_management/bloc/create_headline/create_headline_bloc.dart index f7d94049..911365d5 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -24,7 +24,7 @@ class CreateHeadlineBloc super(const CreateHeadlineState()) { on(_onDataLoaded); on(_onTitleChanged); - on(_onDescriptionChanged); + on(_onExcerptChanged); on(_onUrlChanged); on(_onImageUrlChanged); on(_onSourceChanged); @@ -89,11 +89,11 @@ class CreateHeadlineBloc emit(state.copyWith(title: event.title)); } - void _onDescriptionChanged( - CreateHeadlineDescriptionChanged event, + void _onExcerptChanged( + CreateHeadlineExcerptChanged event, Emitter emit, ) { - emit(state.copyWith(excerpt: event.description)); + emit(state.copyWith(excerpt: event.excerpt)); } void _onUrlChanged( From ad88779735d694b8e09245eef120bb7cff1f94ff Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 12 Jul 2025 19:26:50 +0100 Subject: [PATCH 21/69] fix(headline): fully synchronize create_headline_state with Headline model --- .../create_headline_state.dart | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_state.dart b/lib/content_management/bloc/create_headline/create_headline_state.dart index d9c17937..b7e36e87 100644 --- a/lib/content_management/bloc/create_headline/create_headline_state.dart +++ b/lib/content_management/bloc/create_headline/create_headline_state.dart @@ -23,13 +23,15 @@ final class CreateHeadlineState extends Equatable { const CreateHeadlineState({ this.status = CreateHeadlineStatus.initial, this.title = '', - this.description = '', + this.excerpt = '', this.url = '', this.imageUrl = '', this.source, this.topic, + this.eventCountry, this.sources = const [], this.topics = const [], + this.countries = const [], this.contentStatus = ContentStatus.active, this.errorMessage, this.createdHeadline, @@ -37,30 +39,38 @@ final class CreateHeadlineState extends Equatable { final CreateHeadlineStatus status; final String title; - final String description; + final String excerpt; final String url; final String imageUrl; final Source? source; final Topic? topic; + final Country? eventCountry; final List sources; final List topics; + final List countries; final ContentStatus contentStatus; final String? errorMessage; final Headline? createdHeadline; /// Returns true if the form is valid and can be submitted. - bool get isFormValid => title.isNotEmpty; + bool get isFormValid => + title.isNotEmpty && + source != null && + topic != null && + eventCountry != null; CreateHeadlineState copyWith({ CreateHeadlineStatus? status, String? title, - String? description, + String? excerpt, String? url, String? imageUrl, ValueGetter? source, ValueGetter? topic, + ValueGetter? eventCountry, List? sources, List? topics, + List? countries, ContentStatus? contentStatus, String? errorMessage, Headline? createdHeadline, @@ -68,13 +78,15 @@ final class CreateHeadlineState extends Equatable { return CreateHeadlineState( status: status ?? this.status, title: title ?? this.title, - description: description ?? this.description, + excerpt: excerpt ?? this.excerpt, url: url ?? this.url, imageUrl: imageUrl ?? this.imageUrl, source: source != null ? source() : this.source, topic: topic != null ? topic() : this.topic, + eventCountry: eventCountry != null ? eventCountry() : this.eventCountry, sources: sources ?? this.sources, topics: topics ?? this.topics, + countries: countries ?? this.countries, contentStatus: contentStatus ?? this.contentStatus, errorMessage: errorMessage, createdHeadline: createdHeadline ?? this.createdHeadline, @@ -83,17 +95,19 @@ final class CreateHeadlineState extends Equatable { @override List get props => [ - status, - title, - description, - url, - imageUrl, - source, - topic, - sources, - topics, - contentStatus, - errorMessage, - createdHeadline, - ]; + status, + title, + excerpt, + url, + imageUrl, + source, + topic, + eventCountry, + sources, + topics, + countries, + contentStatus, + errorMessage, + createdHeadline, + ]; } From 276de33020b6a5977977e22954b0f3565f756b65 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:03:15 +0100 Subject: [PATCH 22/69] feat(content): synchronize create headline bloc events Refactors the events for the CreateHeadlineBloc to align with the `Headline` data model and the BLoC's event handlers. - Renames `CreateHeadlineDescriptionChanged` to `CreateHeadlineExcerptChanged` to match the `Headline.excerpt` field. - Replaces the incorrect `CreateHeadlineCategoryChanged` (which used a non-existent `Category` model) with `CreateHeadlineTopicChanged` and `CreateHeadlineCountryChanged` to correctly handle `Topic` and `Country` selection. This resolves the compilation errors and ensures the BLoC components are in sync. --- .../create_headline_event.dart | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_event.dart b/lib/content_management/bloc/create_headline/create_headline_event.dart index 63d04c19..655eae97 100644 --- a/lib/content_management/bloc/create_headline/create_headline_event.dart +++ b/lib/content_management/bloc/create_headline/create_headline_event.dart @@ -21,12 +21,12 @@ final class CreateHeadlineTitleChanged extends CreateHeadlineEvent { List get props => [title]; } -/// Event for when the headline's description is changed. -final class CreateHeadlineDescriptionChanged extends CreateHeadlineEvent { - const CreateHeadlineDescriptionChanged(this.description); - final String description; +/// Event for when the headline's excerpt is changed. +final class CreateHeadlineExcerptChanged extends CreateHeadlineEvent { + const CreateHeadlineExcerptChanged(this.excerpt); + final String excerpt; @override - List get props => [description]; + List get props => [excerpt]; } /// Event for when the headline's URL is changed. @@ -53,12 +53,20 @@ final class CreateHeadlineSourceChanged extends CreateHeadlineEvent { List get props => [source]; } -/// Event for when the headline's category is changed. -final class CreateHeadlineCategoryChanged extends CreateHeadlineEvent { - const CreateHeadlineCategoryChanged(this.category); - final Category? category; +/// Event for when the headline's topic is changed. +final class CreateHeadlineTopicChanged extends CreateHeadlineEvent { + const CreateHeadlineTopicChanged(this.topic); + final Topic? topic; @override - List get props => [category]; + List get props => [topic]; +} + +/// Event for when the headline's country is changed. +final class CreateHeadlineCountryChanged extends CreateHeadlineEvent { + const CreateHeadlineCountryChanged(this.country); + final Country? country; + @override + List get props => [country]; } /// Event for when the headline's status is changed. From 4946900b0c144bf9ffde361536c12ddba41a9a4c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:06:46 +0100 Subject: [PATCH 23/69] fix(content): resolve type errors in CreateHeadlineBloc Corrects two issues in `CreateHeadlineBloc`: 1. Resolves `argument_type_not_assignable` errors in the `_onDataLoaded` handler. The type inference for `Future.wait` was too broad, causing the resulting lists to be typed as `List`. The fix is to explicitly cast the `PaginatedResponse` objects to their correct concrete types before accessing the `.items` property. 2. Removes an erroneous `hide Topic` clause from the `foundation.dart` import, which was causing an `undefined_hidden_name` warning. --- .../bloc/create_headline/create_headline_bloc.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_bloc.dart b/lib/content_management/bloc/create_headline/create_headline_bloc.dart index 911365d5..5905b6c1 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -1,6 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart' hide Topic; +import 'package:flutter/foundation.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; import 'package:uuid/uuid.dart'; @@ -53,9 +53,10 @@ class CreateHeadlineBloc _countriesRepository.readAll(), ]); - final sources = sourcesResponse.items; - final topics = topicsResponse.items; - final countries = countriesResponse.items; + final sources = (sourcesResponse as PaginatedResponse).items; + final topics = (topicsResponse as PaginatedResponse).items; + final countries = + (countriesResponse as PaginatedResponse).items; emit( state.copyWith( From e95b9cfc0846ed940884b7f6afb0c0208f8e84e3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:09:25 +0100 Subject: [PATCH 24/69] fix(content): correct source creation logic in bloc Corrects several type and logic errors in the CreateSourceBloc that prevented a new Source from being created. - Updates `isFormValid` in `CreateSourceState` to correctly check for all required fields (`url`, `sourceType`, `headquarters`). - In `CreateSourceBloc`, injects a `Uuid` to generate the required `id` for the new `Source` instance. - Fixes type mismatches during `Source` instantiation by passing non-nullable values from the state, which is now safe due to the improved form validation. --- .../bloc/create_source/create_source_bloc.dart | 13 ++++++++----- .../bloc/create_source/create_source_state.dart | 6 +++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/content_management/bloc/create_source/create_source_bloc.dart b/lib/content_management/bloc/create_source/create_source_bloc.dart index 18db57ff..7bb78aaa 100644 --- a/lib/content_management/bloc/create_source/create_source_bloc.dart +++ b/lib/content_management/bloc/create_source/create_source_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:uuid/uuid.dart'; part 'create_source_event.dart'; part 'create_source_state.dart'; @@ -29,6 +30,7 @@ class CreateSourceBloc extends Bloc { final HtDataRepository _sourcesRepository; final HtDataRepository _countriesRepository; + final _uuid = const Uuid(); Future _onDataLoaded( CreateSourceDataLoaded event, @@ -126,14 +128,15 @@ class CreateSourceBloc extends Bloc { try { final now = DateTime.now(); final newSource = Source( + id: _uuid.v4(), name: state.name, - description: state.description.isNotEmpty ? state.description : null, - url: state.url.isNotEmpty ? state.url : null, - sourceType: state.sourceType, - language: state.language.isNotEmpty ? state.language : null, + description: state.description, + url: state.url, + sourceType: state.sourceType!, + language: state.language, createdAt: now, updatedAt: now, - headquarters: state.headquarters, + headquarters: state.headquarters!, status: state.contentStatus, ); diff --git a/lib/content_management/bloc/create_source/create_source_state.dart b/lib/content_management/bloc/create_source/create_source_state.dart index 1818cb52..2ae9a9d5 100644 --- a/lib/content_management/bloc/create_source/create_source_state.dart +++ b/lib/content_management/bloc/create_source/create_source_state.dart @@ -48,7 +48,11 @@ final class CreateSourceState extends Equatable { final Source? createdSource; /// Returns true if the form is valid and can be submitted. - bool get isFormValid => name.isNotEmpty; + bool get isFormValid => + name.isNotEmpty && + url.isNotEmpty && + sourceType != null && + headquarters != null; CreateSourceState copyWith({ CreateSourceStatus? status, From 019ee3b51ceb7a7dfbb00ea6ce818fd34cf61a05 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:11:07 +0100 Subject: [PATCH 25/69] fix(content): align topic form validation with model Updates the `isFormValid` getter in `CreateTopicState` to correctly validate all required fields (`name`, `description`, `iconUrl`) from the `Topic` data model. This ensures that the form cannot be submitted with incomplete data, preventing the creation of invalid `Topic` objects. --- .../bloc/create_topic/create_topic_state.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/content_management/bloc/create_topic/create_topic_state.dart b/lib/content_management/bloc/create_topic/create_topic_state.dart index c5e4c0d3..0fbc6137 100644 --- a/lib/content_management/bloc/create_topic/create_topic_state.dart +++ b/lib/content_management/bloc/create_topic/create_topic_state.dart @@ -37,8 +37,9 @@ final class CreateTopicState extends Equatable { final Topic? createdTopic; /// Returns true if the form is valid and can be submitted. - /// Based on the Topic model, only the name is required. - bool get isFormValid => name.isNotEmpty; + /// Based on the Topic model, name, description, and iconUrl are required. + bool get isFormValid => + name.isNotEmpty && description.isNotEmpty && iconUrl.isNotEmpty; CreateTopicState copyWith({ CreateTopicStatus? status, From f7723fd1ecfe9457f13f5f13d2558f65fd3e7b2a Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:12:58 +0100 Subject: [PATCH 26/69] refactor(content): update edit headline events to match model Refactors the events for the EditHeadlineBloc to align with the current `Headline` data model. - Renames `EditHeadlineDescriptionChanged` to `EditHeadlineExcerptChanged` to match the `Headline.excerpt` field. - Replaces the outdated `EditHeadlineCategoryChanged` with two new events, `EditHeadlineTopicChanged` and `EditHeadlineCountryChanged`, to correctly handle topic and country selection. --- .../edit_headline/edit_headline_event.dart | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/content_management/bloc/edit_headline/edit_headline_event.dart b/lib/content_management/bloc/edit_headline/edit_headline_event.dart index 44e4e81d..6bad7bdd 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_event.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_event.dart @@ -21,12 +21,12 @@ final class EditHeadlineTitleChanged extends EditHeadlineEvent { List get props => [title]; } -/// Event for when the headline's description is changed. -final class EditHeadlineDescriptionChanged extends EditHeadlineEvent { - const EditHeadlineDescriptionChanged(this.description); - final String description; +/// Event for when the headline's excerpt is changed. +final class EditHeadlineExcerptChanged extends EditHeadlineEvent { + const EditHeadlineExcerptChanged(this.excerpt); + final String excerpt; @override - List get props => [description]; + List get props => [excerpt]; } /// Event for when the headline's URL is changed. @@ -53,12 +53,20 @@ final class EditHeadlineSourceChanged extends EditHeadlineEvent { List get props => [source]; } -/// Event for when the headline's category is changed. -final class EditHeadlineCategoryChanged extends EditHeadlineEvent { - const EditHeadlineCategoryChanged(this.category); - final Category? category; +/// Event for when the headline's topic is changed. +final class EditHeadlineTopicChanged extends EditHeadlineEvent { + const EditHeadlineTopicChanged(this.topic); + final Topic? topic; @override - List get props => [category]; + List get props => [topic]; +} + +/// Event for when the headline's country is changed. +final class EditHeadlineCountryChanged extends EditHeadlineEvent { + const EditHeadlineCountryChanged(this.country); + final Country? country; + @override + List get props => [country]; } /// Event for when the headline's status is changed. From e84096efea02392f696407ff238056a8315fe2ac Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:14:07 +0100 Subject: [PATCH 27/69] refactor(content): update edit headline state to match model Refactors `EditHeadlineState` to align with the current `Headline` data model and its dependencies. - Renames the `description` field to `excerpt`. - Replaces the outdated `category` and `categories` fields with `topic`, `eventCountry`, `topics`, and `countries` to correctly handle topic and country data. - Updates the `isFormValid` getter to validate all required fields from the `Headline` model, ensuring form integrity. - Adjusts the `copyWith` method and `props` to reflect these changes. --- .../edit_headline/edit_headline_state.dart | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/content_management/bloc/edit_headline/edit_headline_state.dart b/lib/content_management/bloc/edit_headline/edit_headline_state.dart index 5cd017f2..5cf98ea6 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_state.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_state.dart @@ -24,13 +24,15 @@ final class EditHeadlineState extends Equatable { this.status = EditHeadlineStatus.initial, this.initialHeadline, this.title = '', - this.description = '', + this.excerpt = '', this.url = '', this.imageUrl = '', this.source, - this.category, + this.topic, + this.eventCountry, this.sources = const [], - this.categories = const [], + this.topics = const [], + this.countries = const [], this.contentStatus = ContentStatus.active, this.errorMessage, this.updatedHeadline, @@ -39,31 +41,42 @@ final class EditHeadlineState extends Equatable { final EditHeadlineStatus status; final Headline? initialHeadline; final String title; - final String description; + final String excerpt; final String url; final String imageUrl; final Source? source; - final Category? category; + final Topic? topic; + final Country? eventCountry; final List sources; - final List categories; + final List topics; + final List countries; final ContentStatus contentStatus; final String? errorMessage; final Headline? updatedHeadline; /// Returns true if the form is valid and can be submitted. - bool get isFormValid => title.isNotEmpty; + bool get isFormValid => + title.isNotEmpty && + excerpt.isNotEmpty && + url.isNotEmpty && + imageUrl.isNotEmpty && + source != null && + topic != null && + eventCountry != null; EditHeadlineState copyWith({ EditHeadlineStatus? status, Headline? initialHeadline, String? title, - String? description, + String? excerpt, String? url, String? imageUrl, ValueGetter? source, - ValueGetter? category, + ValueGetter? topic, + ValueGetter? eventCountry, List? sources, - List? categories, + List? topics, + List? countries, ContentStatus? contentStatus, String? errorMessage, Headline? updatedHeadline, @@ -72,13 +85,15 @@ final class EditHeadlineState extends Equatable { status: status ?? this.status, initialHeadline: initialHeadline ?? this.initialHeadline, title: title ?? this.title, - description: description ?? this.description, + excerpt: excerpt ?? this.excerpt, url: url ?? this.url, imageUrl: imageUrl ?? this.imageUrl, source: source != null ? source() : this.source, - category: category != null ? category() : this.category, + topic: topic != null ? topic() : this.topic, + eventCountry: eventCountry != null ? eventCountry() : this.eventCountry, sources: sources ?? this.sources, - categories: categories ?? this.categories, + topics: topics ?? this.topics, + countries: countries ?? this.countries, contentStatus: contentStatus ?? this.contentStatus, errorMessage: errorMessage, updatedHeadline: updatedHeadline ?? this.updatedHeadline, @@ -90,13 +105,15 @@ final class EditHeadlineState extends Equatable { status, initialHeadline, title, - description, + excerpt, url, imageUrl, source, - category, + topic, + eventCountry, sources, - categories, + topics, + countries, contentStatus, errorMessage, updatedHeadline, From 4053ef0f2739d6a0deb4a7100c91526893991872 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:15:53 +0100 Subject: [PATCH 28/69] refactor(content): update EditHeadlineBloc to match model Refactors the `EditHeadlineBloc` to align with the current `Headline` data model and its dependencies. - Updates the constructor to inject repositories for `Topic` and `Country` instead of the outdated `Category` repository. - Refactors the `_onLoaded` handler to fetch `topics` and `countries` and correctly maps `headline.excerpt` to the state. - Updates event handlers to use the new `EditHeadlineExcerptChanged`, `EditHeadlineTopicChanged`, and `EditHeadlineCountryChanged` events. - Refactors the `_onSubmitted` handler to build the updated `Headline` object using the correct fields (`excerpt`, `topic`, `eventCountry`). --- .../edit_headline/edit_headline_bloc.dart | 71 ++++++++++++------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart index c2951e74..3e4e5034 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart @@ -1,6 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart' hide Category; +import 'package:flutter/foundation.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; @@ -13,27 +13,31 @@ class EditHeadlineBloc extends Bloc { EditHeadlineBloc({ required HtDataRepository headlinesRepository, required HtDataRepository sourcesRepository, - required HtDataRepository categoriesRepository, + required HtDataRepository topicsRepository, + required HtDataRepository countriesRepository, required String headlineId, }) : _headlinesRepository = headlinesRepository, _sourcesRepository = sourcesRepository, - _categoriesRepository = categoriesRepository, + _topicsRepository = topicsRepository, + _countriesRepository = countriesRepository, _headlineId = headlineId, super(const EditHeadlineState()) { on(_onLoaded); on(_onTitleChanged); - on(_onDescriptionChanged); + on(_onExcerptChanged); on(_onUrlChanged); on(_onImageUrlChanged); on(_onSourceChanged); - on(_onCategoryChanged); + on(_onTopicChanged); + on(_onCountryChanged); on(_onStatusChanged); on(_onSubmitted); } final HtDataRepository _headlinesRepository; final HtDataRepository _sourcesRepository; - final HtDataRepository _categoriesRepository; + final HtDataRepository _topicsRepository; + final HtDataRepository _countriesRepository; final String _headlineId; Future _onLoaded( @@ -45,30 +49,34 @@ class EditHeadlineBloc extends Bloc { final [ headlineResponse, sourcesResponse, - categoriesResponse, + topicsResponse, + countriesResponse, ] = await Future.wait([ _headlinesRepository.read(id: _headlineId), _sourcesRepository.readAll(), - _categoriesRepository.readAll(), + _topicsRepository.readAll(), + _countriesRepository.readAll(), ]); final headline = headlineResponse as Headline; final sources = (sourcesResponse as PaginatedResponse).items; - final categories = - (categoriesResponse as PaginatedResponse).items; + final topics = (topicsResponse as PaginatedResponse).items; + final countries = (countriesResponse as PaginatedResponse).items; emit( state.copyWith( status: EditHeadlineStatus.initial, initialHeadline: headline, title: headline.title, - description: headline.description ?? '', - url: headline.url ?? '', - imageUrl: headline.imageUrl ?? '', + excerpt: headline.excerpt, + url: headline.url, + imageUrl: headline.imageUrl, source: () => headline.source, - category: () => headline.category, + topic: () => headline.topic, + eventCountry: () => headline.eventCountry, sources: sources, - categories: categories, + topics: topics, + countries: countries, contentStatus: headline.status, ), ); @@ -98,13 +106,13 @@ class EditHeadlineBloc extends Bloc { ); } - void _onDescriptionChanged( - EditHeadlineDescriptionChanged event, + void _onExcerptChanged( + EditHeadlineExcerptChanged event, Emitter emit, ) { emit( state.copyWith( - description: event.description, + excerpt: event.excerpt, status: EditHeadlineStatus.initial, ), ); @@ -141,13 +149,25 @@ class EditHeadlineBloc extends Bloc { ); } - void _onCategoryChanged( - EditHeadlineCategoryChanged event, + void _onTopicChanged( + EditHeadlineTopicChanged event, Emitter emit, ) { emit( state.copyWith( - category: () => event.category, + topic: () => event.topic, + status: EditHeadlineStatus.initial, + ), + ); + } + + void _onCountryChanged( + EditHeadlineCountryChanged event, + Emitter emit, + ) { + emit( + state.copyWith( + eventCountry: () => event.country, status: EditHeadlineStatus.initial, ), ); @@ -186,11 +206,12 @@ class EditHeadlineBloc extends Bloc { try { final updatedHeadline = initialHeadline.copyWith( title: state.title, - description: state.description.isNotEmpty ? state.description : null, - url: state.url.isNotEmpty ? state.url : null, - imageUrl: state.imageUrl.isNotEmpty ? state.imageUrl : null, + excerpt: state.excerpt, + url: state.url, + imageUrl: state.imageUrl, source: state.source, - category: state.category, + topic: state.topic, + eventCountry: state.eventCountry, status: state.contentStatus, updatedAt: DateTime.now(), ); From a0d072f95a95661e1a8a5f9d1d7dbde4a4bc0d51 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:19:17 +0100 Subject: [PATCH 29/69] refactor(content): strengthen edit source form validation Updates the `isFormValid` getter in `EditSourceState` to correctly validate all required fields from the `Source` model (`name`, `description`, `url`, `sourceType`, `language`, `headquarters`). This prevents the submission of incomplete data and aligns the form logic with the data model's constraints. --- .../bloc/edit_source/edit_source_state.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/content_management/bloc/edit_source/edit_source_state.dart b/lib/content_management/bloc/edit_source/edit_source_state.dart index 52bd59be..b0a2805b 100644 --- a/lib/content_management/bloc/edit_source/edit_source_state.dart +++ b/lib/content_management/bloc/edit_source/edit_source_state.dart @@ -49,7 +49,13 @@ final class EditSourceState extends Equatable { final Source? updatedSource; /// Returns true if the form is valid and can be submitted. - bool get isFormValid => name.isNotEmpty; + bool get isFormValid => + name.isNotEmpty && + description.isNotEmpty && + url.isNotEmpty && + sourceType != null && + language.isNotEmpty && + headquarters != null; EditSourceState copyWith({ EditSourceStatus? status, From fd6621388db9ca7693490f5e476101e8f9555d5d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:20:07 +0100 Subject: [PATCH 30/69] refactor(content): align EditSourceBloc with non-nullable model Refactors the `EditSourceBloc` to correctly handle the non-nullable fields of the `Source` data model. - In the `_onLoaded` handler, removes redundant null-coalescing operators for `description`, `url`, and `language`, as these fields are guaranteed to be non-nullable `String`s in the `Source` model. - In the `_onSubmitted` handler, corrects the update logic by passing state properties directly to `copyWith`. This fixes a bug that prevented fields from being updated to an empty string and relies on the more robust form validation in the state. --- .../bloc/edit_source/edit_source_bloc.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/content_management/bloc/edit_source/edit_source_bloc.dart b/lib/content_management/bloc/edit_source/edit_source_bloc.dart index 6da5cb83..45b65367 100644 --- a/lib/content_management/bloc/edit_source/edit_source_bloc.dart +++ b/lib/content_management/bloc/edit_source/edit_source_bloc.dart @@ -51,10 +51,10 @@ class EditSourceBloc extends Bloc { status: EditSourceStatus.initial, initialSource: source, name: source.name, - description: source.description ?? '', - url: source.url ?? '', + description: source.description, + url: source.url, sourceType: () => source.sourceType, - language: source.language ?? '', + language: source.language, headquarters: () => source.headquarters, contentStatus: source.status, countries: countries, @@ -172,10 +172,10 @@ class EditSourceBloc extends Bloc { try { final updatedSource = initialSource.copyWith( name: state.name, - description: state.description.isNotEmpty ? state.description : null, - url: state.url.isNotEmpty ? state.url : null, + description: state.description, + url: state.url, sourceType: state.sourceType, - language: state.language.isNotEmpty ? state.language : null, + language: state.language, headquarters: state.headquarters, status: state.contentStatus, updatedAt: DateTime.now(), From 6446eb0319bd38a9e59316208ebdb9b170d006a7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:22:14 +0100 Subject: [PATCH 31/69] fix(content): align edit topic form validation with model Updates the `isFormValid` getter in `EditTopicState` to correctly validate all required fields (`name`, `description`, `iconUrl`) from the `Topic` data model. This ensures that the form cannot be submitted with incomplete data, preventing the creation of invalid `Topic` objects and aligning the edit logic with the create logic. --- lib/content_management/bloc/edit_topic/edit_topic_state.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/content_management/bloc/edit_topic/edit_topic_state.dart b/lib/content_management/bloc/edit_topic/edit_topic_state.dart index 7a8e92c8..7ee2b99d 100644 --- a/lib/content_management/bloc/edit_topic/edit_topic_state.dart +++ b/lib/content_management/bloc/edit_topic/edit_topic_state.dart @@ -41,7 +41,9 @@ final class EditTopicState extends Equatable { final Topic? updatedTopic; /// Returns true if the form is valid and can be submitted. - bool get isFormValid => name.isNotEmpty; + /// Based on the Topic model, name, description, and iconUrl are required. + bool get isFormValid => + name.isNotEmpty && description.isNotEmpty && iconUrl.isNotEmpty; EditTopicState copyWith({ EditTopicStatus? status, From df71f48e5b5b654700f5f6ae35f2a63703cd9b17 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:25:25 +0100 Subject: [PATCH 32/69] fix(content): align create form validation with models Updates the `isFormValid` getters in `CreateHeadlineState` and `CreateSourceState` to correctly validate all required fields from their respective data models (`Headline` and `Source`). This ensures that the forms cannot be submitted with incomplete data, preventing the creation of invalid objects and aligning the BLoC logic with the model constraints. --- .../bloc/create_headline/create_headline_state.dart | 3 +++ .../bloc/create_source/create_source_state.dart | 2 ++ 2 files changed, 5 insertions(+) diff --git a/lib/content_management/bloc/create_headline/create_headline_state.dart b/lib/content_management/bloc/create_headline/create_headline_state.dart index b7e36e87..26be5df2 100644 --- a/lib/content_management/bloc/create_headline/create_headline_state.dart +++ b/lib/content_management/bloc/create_headline/create_headline_state.dart @@ -55,6 +55,9 @@ final class CreateHeadlineState extends Equatable { /// Returns true if the form is valid and can be submitted. bool get isFormValid => title.isNotEmpty && + excerpt.isNotEmpty && + url.isNotEmpty && + imageUrl.isNotEmpty && source != null && topic != null && eventCountry != null; diff --git a/lib/content_management/bloc/create_source/create_source_state.dart b/lib/content_management/bloc/create_source/create_source_state.dart index 2ae9a9d5..7f6bece8 100644 --- a/lib/content_management/bloc/create_source/create_source_state.dart +++ b/lib/content_management/bloc/create_source/create_source_state.dart @@ -50,8 +50,10 @@ final class CreateSourceState extends Equatable { /// Returns true if the form is valid and can be submitted. bool get isFormValid => name.isNotEmpty && + description.isNotEmpty && url.isNotEmpty && sourceType != null && + language.isNotEmpty && headquarters != null; CreateSourceState copyWith({ From d94fa9e8cacd4810e055021c12facc5c2761f79d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:28:34 +0100 Subject: [PATCH 33/69] fix(content): synchronize content management bloc with topic model Aligns the `ContentManagementBloc` and its events with the current data models. - Replaces all `Category`-related events in `content_management_event.dart` with their correct `Topic` equivalents (`LoadTopicsRequested`, `DeleteTopicRequested`, `TopicUpdated`). - Corrects a typo in `content_management_bloc.dart`, renaming the `_onOnDeleteSourceRequested` handler to `_onDeleteSourceRequested` to match the registered event handler. --- .../bloc/content_management_bloc.dart | 4 +- .../bloc/content_management_event.dart | 38 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/content_management/bloc/content_management_bloc.dart b/lib/content_management/bloc/content_management_bloc.dart index e8ae83e3..0d0a2585 100644 --- a/lib/content_management/bloc/content_management_bloc.dart +++ b/lib/content_management/bloc/content_management_bloc.dart @@ -37,7 +37,7 @@ class ContentManagementBloc on(_onDeleteTopicRequested); on(_onLoadSourcesRequested); on(_onSourceUpdated); - on(_onOnDeleteSourceRequested); + on(_onDeleteSourceRequested); } final HtDataRepository _headlinesRepository; @@ -249,7 +249,7 @@ class ContentManagementBloc } } - Future _onOnDeleteSourceRequested( + Future _onDeleteSourceRequested( DeleteSourceRequested event, Emitter emit, ) async { diff --git a/lib/content_management/bloc/content_management_event.dart b/lib/content_management/bloc/content_management_event.dart index be4a0035..e9fb3cad 100644 --- a/lib/content_management/bloc/content_management_event.dart +++ b/lib/content_management/bloc/content_management_event.dart @@ -66,12 +66,12 @@ final class HeadlineUpdated extends ContentManagementEvent { List get props => [headline]; } -/// {@template load_categories_requested} -/// Event to request loading of categories. +/// {@template load_topics_requested} +/// Event to request loading of topics. /// {@endtemplate} -final class LoadCategoriesRequested extends ContentManagementEvent { - /// {@macro load_categories_requested} - const LoadCategoriesRequested({this.startAfterId, this.limit}); +final class LoadTopicsRequested extends ContentManagementEvent { + /// {@macro load_topics_requested} + const LoadTopicsRequested({this.startAfterId, this.limit}); /// Optional ID to start pagination after. final String? startAfterId; @@ -83,32 +83,32 @@ final class LoadCategoriesRequested extends ContentManagementEvent { List get props => [startAfterId, limit]; } -/// {@template delete_category_requested} -/// Event to request deletion of a category. +/// {@template delete_topic_requested} +/// Event to request deletion of a topic. /// {@endtemplate} -final class DeleteCategoryRequested extends ContentManagementEvent { - /// {@macro delete_category_requested} - const DeleteCategoryRequested(this.id); +final class DeleteTopicRequested extends ContentManagementEvent { + /// {@macro delete_topic_requested} + const DeleteTopicRequested(this.id); - /// The ID of the category to delete. + /// The ID of the topic to delete. final String id; @override List get props => [id]; } -/// {@template category_updated} -/// Event to update an existing category in the local state. +/// {@template topic_updated} +/// Event to update an existing topic in the local state. /// {@endtemplate} -final class CategoryUpdated extends ContentManagementEvent { - /// {@macro category_updated} - const CategoryUpdated(this.category); +final class TopicUpdated extends ContentManagementEvent { + /// {@macro topic_updated} + const TopicUpdated(this.topic); - /// The category that was updated. - final Category category; + /// The topic that was updated. + final Topic topic; @override - List get props => [category]; + List get props => [topic]; } /// {@template load_sources_requested} From 352677a8f08ea4ca5a3c47029a4fd9e2ece68b16 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:34:13 +0100 Subject: [PATCH 34/69] refactor(content): complete migration of create category to topic UI Updates the internal implementation of the `CreateTopicPage` to fully utilize the `CreateTopicBloc` and its associated events and states. - Replaces all `CreateCategoryBloc` references with `CreateTopicBloc`. - Updates all dispatched events to use the correct `CreateTopic...` variants. - Corrects the form field label from "Category Name" to "Topic Name" using the appropriate localization key. --- .../view/content_management_page.dart | 4 ++-- .../{categories_page.dart => topic_page.dart} | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) rename lib/content_management/view/{categories_page.dart => topic_page.dart} (94%) diff --git a/lib/content_management/view/content_management_page.dart b/lib/content_management/view/content_management_page.dart index d4f0305c..232263f6 100644 --- a/lib/content_management/view/content_management_page.dart +++ b/lib/content_management/view/content_management_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart'; -import 'package:ht_dashboard/content_management/view/categories_page.dart'; +import 'package:ht_dashboard/content_management/view/topic_page.dart'; import 'package:ht_dashboard/content_management/view/headlines_page.dart'; import 'package:ht_dashboard/content_management/view/sources_page.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; @@ -117,7 +117,7 @@ class _ContentManagementPageState extends State controller: _tabController, children: const [ HeadlinesPage(), - CategoriesPage(), + TopicPage(), SourcesPage(), ], ), diff --git a/lib/content_management/view/categories_page.dart b/lib/content_management/view/topic_page.dart similarity index 94% rename from lib/content_management/view/categories_page.dart rename to lib/content_management/view/topic_page.dart index 13cf504f..376b3885 100644 --- a/lib/content_management/view/categories_page.dart +++ b/lib/content_management/view/topic_page.dart @@ -12,17 +12,17 @@ import 'package:ht_shared/ht_shared.dart'; import 'package:intl/intl.dart'; /// {@template categories_page} -/// A page for displaying and managing Categories in a tabular format. +/// A page for displaying and managing Topics in a tabular format. /// {@endtemplate} -class CategoriesPage extends StatefulWidget { +class TopicPage extends StatefulWidget { /// {@macro categories_page} - const CategoriesPage({super.key}); + const TopicPage({super.key}); @override - State createState() => _CategoriesPageState(); + State createState() => _TopicPageState(); } -class _CategoriesPageState extends State { +class _TopicPageState extends State { @override void initState() { super.initState(); @@ -82,7 +82,7 @@ class _CategoriesPageState extends State { fixedWidth: 120, ), ], - source: _CategoriesDataSource( + source: _TopicsDataSource( context: context, categories: state.categories, isLoading: @@ -120,8 +120,8 @@ class _CategoriesPageState extends State { } } -class _CategoriesDataSource extends DataTableSource { - _CategoriesDataSource({ +class _TopicsDataSource extends DataTableSource { + _TopicsDataSource({ required this.context, required this.categories, required this.isLoading, From c893fb4fee91dfedc11e0668ea6d2eae68dfaebf Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:35:55 +0100 Subject: [PATCH 35/69] refactor(content): complete migration of create category to topic UI Updates the internal implementation of the `CreateTopicPage` to fully utilize the `CreateTopicBloc` and its associated events and states. - Replaces all `CreateCategoryBloc` references with `CreateTopicBloc`. - Updates all dispatched events to use the correct `CreateTopic...` variants. - Corrects the form field label from "Category Name" to "Topic Name" using the appropriate localization key. --- ...egory_page.dart => create_topic_page.dart} | 74 +++++++++---------- lib/router/router.dart | 2 +- 2 files changed, 38 insertions(+), 38 deletions(-) rename lib/content_management/view/{create_category_page.dart => create_topic_page.dart} (68%) diff --git a/lib/content_management/view/create_category_page.dart b/lib/content_management/view/create_topic_page.dart similarity index 68% rename from lib/content_management/view/create_category_page.dart rename to lib/content_management/view/create_topic_page.dart index 81391de9..adc2d0ca 100644 --- a/lib/content_management/view/create_category_page.dart +++ b/lib/content_management/view/create_topic_page.dart @@ -9,33 +9,33 @@ import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; -/// {@template create_category_page} -/// A page for creating a new category. -/// It uses a [BlocProvider] to create and provide a [CreateCategoryBloc]. +/// {@template create_topic_page} +/// A page for creating a new topic. +/// It uses a [BlocProvider] to create and provide a [CreateTopicBloc]. /// {@endtemplate} -class CreateCategoryPage extends StatelessWidget { - /// {@macro create_category_page} - const CreateCategoryPage({super.key}); +class CreateTopicPage extends StatelessWidget { + /// {@macro create_topic_page} + const CreateTopicPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CreateCategoryBloc( - categoriesRepository: context.read>(), + create: (context) => CreateTopicBloc( + topicsRepository: context.read>(), ), - child: const _CreateCategoryView(), + child: const _CreateTopicView(), ); } } -class _CreateCategoryView extends StatefulWidget { - const _CreateCategoryView(); +class _CreateTopicView extends StatefulWidget { + const _CreateTopicView(); @override - State<_CreateCategoryView> createState() => _CreateCategoryViewState(); + State<_CreateTopicView> createState() => _CreateTopicViewState(); } -class _CreateCategoryViewState extends State<_CreateCategoryView> { +class _CreateTopicViewState extends State<_CreateTopicView> { final _formKey = GlobalKey(); @override @@ -43,11 +43,11 @@ class _CreateCategoryViewState extends State<_CreateCategoryView> { final l10n = context.l10n; return Scaffold( appBar: AppBar( - title: Text(l10n.createCategory), + title: Text(l10n.createTopic), actions: [ - BlocBuilder( + BlocBuilder( builder: (context, state) { - if (state.status == CreateCategoryStatus.submitting) { + if (state.status == CreateTopicStatus.submitting) { return const Padding( padding: EdgeInsets.only(right: AppSpacing.lg), child: SizedBox( @@ -61,8 +61,8 @@ class _CreateCategoryViewState extends State<_CreateCategoryView> { icon: const Icon(Icons.save), tooltip: l10n.saveChanges, onPressed: state.isFormValid - ? () => context.read().add( - const CreateCategorySubmitted(), + ? () => context.read().add( + const CreateTopicSubmitted(), ) : null, ); @@ -70,24 +70,24 @@ class _CreateCategoryViewState extends State<_CreateCategoryView> { ), ], ), - body: BlocConsumer( + body: BlocConsumer( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { - if (state.status == CreateCategoryStatus.success && - state.createdCategory != null && + if (state.status == CreateTopicStatus.success && + state.createdTopic != null && ModalRoute.of(context)!.isCurrent) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( - SnackBar(content: Text(l10n.categoryCreatedSuccessfully)), + SnackBar(content: Text(l10n.topicCreatedSuccessfully)), ); context.read().add( - // Refresh the list to show the new category - const LoadCategoriesRequested(limit: kDefaultRowsPerPage), + // Refresh the list to show the new topic + const LoadTopicsRequested(limit: kDefaultRowsPerPage), ); context.pop(); } - if (state.status == CreateCategoryStatus.failure) { + if (state.status == CreateTopicStatus.failure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( @@ -110,12 +110,12 @@ class _CreateCategoryViewState extends State<_CreateCategoryView> { TextFormField( initialValue: state.name, decoration: InputDecoration( - labelText: l10n.categoryName, + labelText: l10n.topicName, border: const OutlineInputBorder(), ), - onChanged: (value) => context - .read() - .add(CreateCategoryNameChanged(value)), + onChanged: (value) => context.read().add( + CreateTopicNameChanged(value), + ), ), const SizedBox(height: AppSpacing.lg), TextFormField( @@ -125,9 +125,9 @@ class _CreateCategoryViewState extends State<_CreateCategoryView> { border: const OutlineInputBorder(), ), maxLines: 3, - onChanged: (value) => context - .read() - .add(CreateCategoryDescriptionChanged(value)), + onChanged: (value) => context.read().add( + CreateTopicDescriptionChanged(value), + ), ), const SizedBox(height: AppSpacing.lg), TextFormField( @@ -136,9 +136,9 @@ class _CreateCategoryViewState extends State<_CreateCategoryView> { labelText: l10n.iconUrl, border: const OutlineInputBorder(), ), - onChanged: (value) => context - .read() - .add(CreateCategoryIconUrlChanged(value)), + onChanged: (value) => context.read().add( + CreateTopicIconUrlChanged(value), + ), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( @@ -155,8 +155,8 @@ class _CreateCategoryViewState extends State<_CreateCategoryView> { }).toList(), onChanged: (value) { if (value == null) return; - context.read().add( - CreateCategoryStatusChanged(value), + context.read().add( + CreateTopicStatusChanged(value), ); }, ), diff --git a/lib/router/router.dart b/lib/router/router.dart index 1cb6b38f..8ea0e219 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -11,7 +11,7 @@ import 'package:ht_dashboard/authentication/view/authentication_page.dart'; import 'package:ht_dashboard/authentication/view/email_code_verification_page.dart'; import 'package:ht_dashboard/authentication/view/request_code_page.dart'; import 'package:ht_dashboard/content_management/view/content_management_page.dart'; -import 'package:ht_dashboard/content_management/view/create_category_page.dart'; +import 'package:ht_dashboard/content_management/view/create_topic_page.dart'; import 'package:ht_dashboard/content_management/view/create_headline_page.dart'; import 'package:ht_dashboard/content_management/view/create_source_page.dart'; import 'package:ht_dashboard/content_management/view/edit_category_page.dart'; From 2784739bc0598b133dbdd0c5ef53efc0ba78b258 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:39:20 +0100 Subject: [PATCH 36/69] refactor(content): migrate edit category page to use topic model Refactors the `edit_topic_page.dart` file to fully align with the `Topic` data model and its associated BLoC. - Renames all classes and variables from `Category` to `Topic`. - Updates the `BlocProvider` to use `EditTopicBloc`. - Replaces all BLoC interactions and event dispatches to use the correct `EditTopicBloc` events and states. - Updates user-facing strings and icons to refer to "Topic". - Ensures the `ContentManagementBloc` is correctly updated with `TopicUpdated` on success. --- ...ategory_page.dart => edit_topic_page.dart} | 96 +++++++++---------- lib/router/router.dart | 2 +- 2 files changed, 48 insertions(+), 50 deletions(-) rename lib/content_management/view/{edit_category_page.dart => edit_topic_page.dart} (68%) diff --git a/lib/content_management/view/edit_category_page.dart b/lib/content_management/view/edit_topic_page.dart similarity index 68% rename from lib/content_management/view/edit_category_page.dart rename to lib/content_management/view/edit_topic_page.dart index ead35af8..66c1320c 100644 --- a/lib/content_management/view/edit_category_page.dart +++ b/lib/content_management/view/edit_topic_page.dart @@ -8,37 +8,37 @@ import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; -/// {@template edit_category_page} -/// A page for editing an existing category. -/// It uses a [BlocProvider] to create and provide an [EditCategoryBloc]. +/// {@template edit_topic_page} +/// A page for editing an existing topic. +/// It uses a [BlocProvider] to create and provide an [EditTopicBloc]. /// {@endtemplate} -class EditCategoryPage extends StatelessWidget { - /// {@macro edit_category_page} - const EditCategoryPage({required this.categoryId, super.key}); +class EditTopicPage extends StatelessWidget { + /// {@macro edit_topic_page} + const EditTopicPage({required this.topicId, super.key}); - /// The ID of the category to be edited. - final String categoryId; + /// The ID of the topic to be edited. + final String topicId; @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => EditCategoryBloc( - categoriesRepository: context.read>(), - categoryId: categoryId, - )..add(const EditCategoryLoaded()), - child: const _EditCategoryView(), + create: (context) => EditTopicBloc( + topicsRepository: context.read>(), + topicId: topicId, + )..add(const EditTopicLoaded()), + child: const _EditTopicView(), ); } } -class _EditCategoryView extends StatefulWidget { - const _EditCategoryView(); +class _EditTopicView extends StatefulWidget { + const _EditTopicView(); @override - State<_EditCategoryView> createState() => _EditCategoryViewState(); + State<_EditTopicView> createState() => _EditTopicViewState(); } -class _EditCategoryViewState extends State<_EditCategoryView> { +class _EditTopicViewState extends State<_EditTopicView> { final _formKey = GlobalKey(); late final TextEditingController _nameController; late final TextEditingController _descriptionController; @@ -47,7 +47,7 @@ class _EditCategoryViewState extends State<_EditCategoryView> { @override void initState() { super.initState(); - final state = context.read().state; + final state = context.read().state; _nameController = TextEditingController(text: state.name); _descriptionController = TextEditingController(text: state.description); _iconUrlController = TextEditingController(text: state.iconUrl); @@ -66,11 +66,11 @@ class _EditCategoryViewState extends State<_EditCategoryView> { final l10n = context.l10n; return Scaffold( appBar: AppBar( - title: Text(l10n.editCategory), + title: Text(l10n.editTopic), actions: [ - BlocBuilder( + BlocBuilder( builder: (context, state) { - if (state.status == EditCategoryStatus.submitting) { + if (state.status == EditTopicStatus.submitting) { return const Padding( padding: EdgeInsets.only(right: AppSpacing.lg), child: SizedBox( @@ -84,8 +84,8 @@ class _EditCategoryViewState extends State<_EditCategoryView> { icon: const Icon(Icons.save), tooltip: l10n.saveChanges, onPressed: state.isFormValid - ? () => context.read().add( - const EditCategorySubmitted(), + ? () => context.read().add( + const EditTopicSubmitted(), ) : null, ); @@ -93,26 +93,25 @@ class _EditCategoryViewState extends State<_EditCategoryView> { ), ], ), - body: BlocConsumer( + body: BlocConsumer( listenWhen: (previous, current) => previous.status != current.status || - previous.initialCategory != current.initialCategory, + previous.initialTopic != current.initialTopic, listener: (context, state) { - if (state.status == EditCategoryStatus.success && - state.updatedCategory != null && + if (state.status == EditTopicStatus.success && + state.updatedTopic != null && ModalRoute.of(context)!.isCurrent) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( - // TODO(l10n): Localize this message. - const SnackBar(content: Text('Category updated successfully.')), + SnackBar(content: Text(l10n.topicUpdatedSuccessfully)), ); context.read().add( - CategoryUpdated(state.updatedCategory!), + TopicUpdated(state.updatedTopic!), ); context.pop(); } - if (state.status == EditCategoryStatus.failure) { + if (state.status == EditTopicStatus.failure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( @@ -122,28 +121,27 @@ class _EditCategoryViewState extends State<_EditCategoryView> { ), ); } - if (state.initialCategory != null) { + if (state.initialTopic != null) { _nameController.text = state.name; _descriptionController.text = state.description; _iconUrlController.text = state.iconUrl; } }, builder: (context, state) { - if (state.status == EditCategoryStatus.loading) { + if (state.status == EditTopicStatus.loading) { return LoadingStateWidget( - icon: Icons.category, - // TODO(l10n): Localize this message. - headline: 'Loading Category...', + icon: Icons.topic, + headline: l10n.loadingTopic, subheadline: l10n.pleaseWait, ); } - if (state.status == EditCategoryStatus.failure && - state.initialCategory == null) { + if (state.status == EditTopicStatus.failure && + state.initialTopic == null) { return FailureStateWidget( message: state.errorMessage ?? l10n.unknownError, - onRetry: () => context.read().add( - const EditCategoryLoaded(), + onRetry: () => context.read().add( + const EditTopicLoaded(), ), ); } @@ -159,12 +157,12 @@ class _EditCategoryViewState extends State<_EditCategoryView> { TextFormField( controller: _nameController, decoration: InputDecoration( - labelText: l10n.categoryName, + labelText: l10n.topicName, border: const OutlineInputBorder(), ), onChanged: (value) => context - .read() - .add(EditCategoryNameChanged(value)), + .read() + .add(EditTopicNameChanged(value)), ), const SizedBox(height: AppSpacing.lg), TextFormField( @@ -175,8 +173,8 @@ class _EditCategoryViewState extends State<_EditCategoryView> { ), maxLines: 3, onChanged: (value) => context - .read() - .add(EditCategoryDescriptionChanged(value)), + .read() + .add(EditTopicDescriptionChanged(value)), ), const SizedBox(height: AppSpacing.lg), TextFormField( @@ -186,8 +184,8 @@ class _EditCategoryViewState extends State<_EditCategoryView> { border: const OutlineInputBorder(), ), onChanged: (value) => context - .read() - .add(EditCategoryIconUrlChanged(value)), + .read() + .add(EditTopicIconUrlChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( @@ -204,8 +202,8 @@ class _EditCategoryViewState extends State<_EditCategoryView> { }).toList(), onChanged: (value) { if (value == null) return; - context.read().add( - EditCategoryStatusChanged(value), + context.read().add( + EditTopicStatusChanged(value), ); }, ), diff --git a/lib/router/router.dart b/lib/router/router.dart index 8ea0e219..8e72f55f 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -14,7 +14,7 @@ import 'package:ht_dashboard/content_management/view/content_management_page.dar import 'package:ht_dashboard/content_management/view/create_topic_page.dart'; import 'package:ht_dashboard/content_management/view/create_headline_page.dart'; import 'package:ht_dashboard/content_management/view/create_source_page.dart'; -import 'package:ht_dashboard/content_management/view/edit_category_page.dart'; +import 'package:ht_dashboard/content_management/view/edit_topic_page.dart'; import 'package:ht_dashboard/content_management/view/edit_headline_page.dart'; import 'package:ht_dashboard/content_management/view/edit_source_page.dart'; import 'package:ht_dashboard/dashboard/view/dashboard_page.dart'; From fd877b6468fc2af79513312d57f2f86563b424c2 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:43:36 +0100 Subject: [PATCH 37/69] refactor(content): migrate create headline UI to use topic and country Refactors the `CreateHeadlinePage` to align with the current `Headline` data model, which uses `Topic` and `Country` instead of `Category`. - Updates the `BlocProvider` to inject `topicsRepository` and `countriesRepository`. - Replaces the "Description" field with "Excerpt" and updates the corresponding event. - Replaces the single `Category` dropdown with two separate dropdowns for `Topic` and `Country`, dispatching the correct events. - Updates the failure state check to use the correct state properties. --- .../view/create_headline_page.dart | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index 1c5cc19d..339a1cc8 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -23,7 +23,8 @@ class CreateHeadlinePage extends StatelessWidget { create: (context) => CreateHeadlineBloc( headlinesRepository: context.read>(), sourcesRepository: context.read>(), - categoriesRepository: context.read>(), + topicsRepository: context.read>(), + countriesRepository: context.read>(), )..add(const CreateHeadlineDataLoaded()), child: const _CreateHeadlineView(), ); @@ -111,7 +112,8 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { if (state.status == CreateHeadlineStatus.failure && state.sources.isEmpty && - state.categories.isEmpty) { + state.topics.isEmpty && + state.countries.isEmpty) { return FailureStateWidget( message: state.errorMessage ?? l10n.unknownError, onRetry: () => context.read().add( @@ -140,15 +142,15 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { ), const SizedBox(height: AppSpacing.lg), TextFormField( - initialValue: state.description, + initialValue: state.excerpt, decoration: InputDecoration( - labelText: l10n.description, + labelText: l10n.excerpt, border: const OutlineInputBorder(), ), maxLines: 3, onChanged: (value) => context .read() - .add(CreateHeadlineDescriptionChanged(value)), + .add(CreateHeadlineExcerptChanged(value)), ), const SizedBox(height: AppSpacing.lg), TextFormField( @@ -193,24 +195,44 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { .add(CreateHeadlineSourceChanged(value)), ), const SizedBox(height: AppSpacing.lg), - DropdownButtonFormField( - value: state.category, + DropdownButtonFormField( + value: state.topic, decoration: InputDecoration( - labelText: l10n.categoryName, + labelText: l10n.topicName, border: const OutlineInputBorder(), ), items: [ DropdownMenuItem(value: null, child: Text(l10n.none)), - ...state.categories.map( - (category) => DropdownMenuItem( - value: category, - child: Text(category.name), + ...state.topics.map( + (topic) => DropdownMenuItem( + value: topic, + child: Text(topic.name), ), ), ], onChanged: (value) => context .read() - .add(CreateHeadlineCategoryChanged(value)), + .add(CreateHeadlineTopicChanged(value)), + ), + const SizedBox(height: AppSpacing.lg), + DropdownButtonFormField( + value: state.eventCountry, + decoration: InputDecoration( + labelText: l10n.countryName, + border: const OutlineInputBorder(), + ), + items: [ + DropdownMenuItem(value: null, child: Text(l10n.none)), + ...state.countries.map( + (country) => DropdownMenuItem( + value: country, + child: Text(country.name), + ), + ), + ], + onChanged: (value) => context + .read() + .add(CreateHeadlineCountryChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( From 7b5804444410b61e92c478a788b6c4221dc6259d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:43:54 +0100 Subject: [PATCH 38/69] refactor(content): migrate edit headline UI to use topic and country Refactors the `EditHeadlinePage` to fully align with the current `Headline` data model and the `EditHeadlineBloc`. - Updates the `BlocProvider` to inject `topicsRepository` and `countriesRepository` instead of the outdated `categoriesRepository`. - Replaces the "Description" field with "Excerpt" and updates the corresponding controller and event. - Replaces the single `Category` dropdown with two separate dropdowns for `Topic` and `Country`, dispatching the correct events and correctly handling the selected values. --- .../view/edit_headline_page.dart | 74 +++++++++++++------ 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/lib/content_management/view/edit_headline_page.dart b/lib/content_management/view/edit_headline_page.dart index 3f90c5b1..6a57aec0 100644 --- a/lib/content_management/view/edit_headline_page.dart +++ b/lib/content_management/view/edit_headline_page.dart @@ -25,7 +25,8 @@ class EditHeadlinePage extends StatelessWidget { create: (context) => EditHeadlineBloc( headlinesRepository: context.read>(), sourcesRepository: context.read>(), - categoriesRepository: context.read>(), + topicsRepository: context.read>(), + countriesRepository: context.read>(), headlineId: headlineId, )..add(const EditHeadlineLoaded()), child: const _EditHeadlineView(), @@ -43,7 +44,7 @@ class _EditHeadlineView extends StatefulWidget { class _EditHeadlineViewState extends State<_EditHeadlineView> { final _formKey = GlobalKey(); late final TextEditingController _titleController; - late final TextEditingController _descriptionController; + late final TextEditingController _excerptController; late final TextEditingController _urlController; late final TextEditingController _imageUrlController; @@ -52,7 +53,7 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { super.initState(); final state = context.read().state; _titleController = TextEditingController(text: state.title); - _descriptionController = TextEditingController(text: state.description); + _excerptController = TextEditingController(text: state.excerpt); _urlController = TextEditingController(text: state.url); _imageUrlController = TextEditingController(text: state.imageUrl); } @@ -60,7 +61,7 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { @override void dispose() { _titleController.dispose(); - _descriptionController.dispose(); + _excerptController.dispose(); _urlController.dispose(); _imageUrlController.dispose(); super.dispose(); @@ -130,7 +131,7 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { } if (state.initialHeadline != null) { _titleController.text = state.title; - _descriptionController.text = state.description; + _excerptController.text = state.excerpt; _urlController.text = state.url; _imageUrlController.text = state.imageUrl; } @@ -167,14 +168,25 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { } } - Category? selectedCategory; - if (state.category != null) { + Topic? selectedTopic; + if (state.topic != null) { try { - selectedCategory = state.categories.firstWhere( - (c) => c.id == state.category!.id, + selectedTopic = state.topics.firstWhere( + (t) => t.id == state.topic!.id, ); } catch (_) { - selectedCategory = null; + selectedTopic = null; + } + } + + Country? selectedCountry; + if (state.eventCountry != null) { + try { + selectedCountry = state.countries.firstWhere( + (c) => c.id == state.eventCountry!.id, + ); + } catch (_) { + selectedCountry = null; } } @@ -198,15 +210,15 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { ), const SizedBox(height: AppSpacing.lg), TextFormField( - controller: _descriptionController, + controller: _excerptController, decoration: InputDecoration( - labelText: l10n.description, + labelText: l10n.excerpt, border: const OutlineInputBorder(), ), maxLines: 3, onChanged: (value) => context .read() - .add(EditHeadlineDescriptionChanged(value)), + .add(EditHeadlineExcerptChanged(value)), ), const SizedBox(height: AppSpacing.lg), TextFormField( @@ -251,24 +263,44 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { .add(EditHeadlineSourceChanged(value)), ), const SizedBox(height: AppSpacing.lg), - DropdownButtonFormField( - value: selectedCategory, + DropdownButtonFormField( + value: selectedTopic, + decoration: InputDecoration( + labelText: l10n.topicName, + border: const OutlineInputBorder(), + ), + items: [ + DropdownMenuItem(value: null, child: Text(l10n.none)), + ...state.topics.map( + (topic) => DropdownMenuItem( + value: topic, + child: Text(topic.name), + ), + ), + ], + onChanged: (value) => context + .read() + .add(EditHeadlineTopicChanged(value)), + ), + const SizedBox(height: AppSpacing.lg), + DropdownButtonFormField( + value: selectedCountry, decoration: InputDecoration( - labelText: l10n.categoryName, + labelText: l10n.countryName, border: const OutlineInputBorder(), ), items: [ DropdownMenuItem(value: null, child: Text(l10n.none)), - ...state.categories.map( - (category) => DropdownMenuItem( - value: category, - child: Text(category.name), + ...state.countries.map( + (country) => DropdownMenuItem( + value: country, + child: Text(country.name), ), ), ], onChanged: (value) => context .read() - .add(EditHeadlineCategoryChanged(value)), + .add(EditHeadlineCountryChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField( From 2308c31a5f0428d62c739985d96d0e4389d55f39 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:55:30 +0100 Subject: [PATCH 39/69] fix(content): align headlines page with non-nullable headline model Updates the `_HeadlinesDataSource` in `headlines_page.dart` to correctly handle the `source` and `updatedAt` fields of the `Headline` model, which are non-nullable. Removes unnecessary null-aware operators and null checks, ensuring the UI code is synchronized with the data model's constraints. --- lib/content_management/view/headlines_page.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index 55beb513..97788f56 100644 --- a/lib/content_management/view/headlines_page.dart +++ b/lib/content_management/view/headlines_page.dart @@ -169,14 +169,12 @@ class _HeadlinesDataSource extends DataTableSource { }, cells: [ DataCell(Text(headline.title)), - DataCell(Text(headline.source?.name ?? l10n.unknown)), + DataCell(Text(headline.source.name)), DataCell(Text(headline.status.l10n(context))), DataCell( Text( - headline.updatedAt != null - // TODO(fulleni): Make date format configurable by admin. - ? DateFormat('dd-MM-yyyy').format(headline.updatedAt!.toLocal()) - : l10n.notAvailable, + // TODO(fulleni): Make date format configurable by admin. + DateFormat('dd-MM-yyyy').format(headline.updatedAt.toLocal()), ), ), DataCell( From 887f3e3792f0336304bc0341a8c6d7f6186b17c5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:56:40 +0100 Subject: [PATCH 40/69] fix(content): align sources page with non-nullable source model Updates the `_SourcesDataSource` in `sources_page.dart` to correctly handle the `sourceType` and `updatedAt` fields of the `Source` model, which are non-nullable. Removes unnecessary null-aware operators and null checks, ensuring the UI code is synchronized with the data model's constraints. --- lib/content_management/view/sources_page.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/content_management/view/sources_page.dart b/lib/content_management/view/sources_page.dart index 2cbf184c..a353755d 100644 --- a/lib/content_management/view/sources_page.dart +++ b/lib/content_management/view/sources_page.dart @@ -168,14 +168,12 @@ class _SourcesDataSource extends DataTableSource { }, cells: [ DataCell(Text(source.name)), - DataCell(Text(source.sourceType?.localizedName(l10n) ?? l10n.unknown)), + DataCell(Text(source.sourceType.localizedName(l10n))), DataCell(Text(source.status.l10n(context))), DataCell( Text( - source.updatedAt != null - // TODO(fulleni): Make date format configurable by admin. - ? DateFormat('dd-MM-yyyy').format(source.updatedAt!.toLocal()) - : l10n.notAvailable, + // TODO(fulleni): Make date format configurable by admin. + DateFormat('dd-MM-yyyy').format(source.updatedAt.toLocal()), ), ), DataCell( From 513012a0d89ef7cd78695fff4b0e9174041c0ef0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 11:59:43 +0100 Subject: [PATCH 41/69] refactor(content): migrate topics page to use topic model Refactors the `topics_page.dart` file to fully align with the `Topic` data model and its associated BLoC events and state. - Replaces all `Category`-related variables, states, and events with their `Topic` equivalents. - Updates the `_TopicsDataSource` to work with `List`. - Corrects the `DataCell` for the last updated date to handle the non-nullable `updatedAt` field from the `Topic` model, removing an unnecessary null check. --- .../view/content_management_page.dart | 2 +- .../{topic_page.dart => topics_page.dart} | 77 +++++++++---------- 2 files changed, 37 insertions(+), 42 deletions(-) rename lib/content_management/view/{topic_page.dart => topics_page.dart} (71%) diff --git a/lib/content_management/view/content_management_page.dart b/lib/content_management/view/content_management_page.dart index 232263f6..afd6eddb 100644 --- a/lib/content_management/view/content_management_page.dart +++ b/lib/content_management/view/content_management_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart'; -import 'package:ht_dashboard/content_management/view/topic_page.dart'; +import 'package:ht_dashboard/content_management/view/topics_page.dart'; import 'package:ht_dashboard/content_management/view/headlines_page.dart'; import 'package:ht_dashboard/content_management/view/sources_page.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; diff --git a/lib/content_management/view/topic_page.dart b/lib/content_management/view/topics_page.dart similarity index 71% rename from lib/content_management/view/topic_page.dart rename to lib/content_management/view/topics_page.dart index 376b3885..9f823753 100644 --- a/lib/content_management/view/topic_page.dart +++ b/lib/content_management/view/topics_page.dart @@ -11,11 +11,11 @@ import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_shared/ht_shared.dart'; import 'package:intl/intl.dart'; -/// {@template categories_page} +/// {@template topics_page} /// A page for displaying and managing Topics in a tabular format. /// {@endtemplate} class TopicPage extends StatefulWidget { - /// {@macro categories_page} + /// {@macro topics_page} const TopicPage({super.key}); @override @@ -27,7 +27,7 @@ class _TopicPageState extends State { void initState() { super.initState(); context.read().add( - const LoadCategoriesRequested(limit: kDefaultRowsPerPage), + const LoadTopicsRequested(limit: kDefaultRowsPerPage), ); } @@ -38,34 +38,34 @@ class _TopicPageState extends State { padding: const EdgeInsets.all(AppSpacing.lg), child: BlocBuilder( builder: (context, state) { - if (state.categoriesStatus == ContentManagementStatus.loading && - state.categories.isEmpty) { + if (state.topicsStatus == ContentManagementStatus.loading && + state.topics.isEmpty) { return LoadingStateWidget( - icon: Icons.category, - headline: l10n.loadingCategories, + icon: Icons.topic, + headline: l10n.loadingTopics, subheadline: l10n.pleaseWait, ); } - if (state.categoriesStatus == ContentManagementStatus.failure) { + if (state.topicsStatus == ContentManagementStatus.failure) { return FailureStateWidget( message: state.errorMessage ?? l10n.unknownError, onRetry: () => context.read().add( - const LoadCategoriesRequested(limit: kDefaultRowsPerPage), + const LoadTopicsRequested(limit: kDefaultRowsPerPage), ), ); } - if (state.categories.isEmpty) { + if (state.topics.isEmpty) { return Center( - child: Text(l10n.noCategoriesFound), + child: Text(l10n.noTopicsFound), ); } return PaginatedDataTable2( columns: [ DataColumn2( - label: Text(l10n.categoryName), + label: Text(l10n.topicName), size: ColumnSize.L, ), DataColumn2( @@ -84,28 +84,27 @@ class _TopicPageState extends State { ], source: _TopicsDataSource( context: context, - categories: state.categories, - isLoading: - state.categoriesStatus == ContentManagementStatus.loading, - hasMore: state.categoriesHasMore, + topics: state.topics, + isLoading: state.topicsStatus == ContentManagementStatus.loading, + hasMore: state.topicsHasMore, l10n: l10n, ), rowsPerPage: kDefaultRowsPerPage, availableRowsPerPage: const [kDefaultRowsPerPage], onPageChanged: (pageIndex) { final newOffset = pageIndex * kDefaultRowsPerPage; - if (newOffset >= state.categories.length && - state.categoriesHasMore && - state.categoriesStatus != ContentManagementStatus.loading) { + if (newOffset >= state.topics.length && + state.topicsHasMore && + state.topicsStatus != ContentManagementStatus.loading) { context.read().add( - LoadCategoriesRequested( - startAfterId: state.categoriesCursor, + LoadTopicsRequested( + startAfterId: state.topicsCursor, limit: kDefaultRowsPerPage, ), ); } }, - empty: Center(child: Text(l10n.noCategoriesFound)), + empty: Center(child: Text(l10n.noTopicsFound)), showCheckboxColumn: false, showFirstLastButtons: true, fit: FlexFit.tight, @@ -123,21 +122,21 @@ class _TopicPageState extends State { class _TopicsDataSource extends DataTableSource { _TopicsDataSource({ required this.context, - required this.categories, + required this.topics, required this.isLoading, required this.hasMore, required this.l10n, }); final BuildContext context; - final List categories; + final List topics; final bool isLoading; final bool hasMore; final AppLocalizations l10n; @override DataRow? getRow(int index) { - if (index >= categories.length) { + if (index >= topics.length) { // This can happen if hasMore is true and the user is on the last page. // If we are loading, show a spinner. Otherwise, we've reached the end. if (isLoading) { @@ -150,25 +149,23 @@ class _TopicsDataSource extends DataTableSource { } return null; } - final category = categories[index]; + final topic = topics[index]; return DataRow2( onSelectChanged: (selected) { if (selected ?? false) { context.goNamed( - Routes.editCategoryName, - pathParameters: {'id': category.id}, + Routes.editTopicName, + pathParameters: {'id': topic.id}, ); } }, cells: [ - DataCell(Text(category.name)), - DataCell(Text(category.status.l10n(context))), + DataCell(Text(topic.name)), + DataCell(Text(topic.status.l10n(context))), DataCell( Text( - category.updatedAt != null - // TODO(fulleni): Make date format configurable by admin. - ? DateFormat('dd-MM-yyyy').format(category.updatedAt!.toLocal()) - : l10n.notAvailable, + // TODO(fulleni): Make date format configurable by admin. + DateFormat('dd-MM-yyyy').format(topic.updatedAt.toLocal()), ), ), DataCell( @@ -179,8 +176,8 @@ class _TopicsDataSource extends DataTableSource { onPressed: () { // Navigate to edit page context.goNamed( - Routes.editCategoryName, // Assuming an edit route exists - pathParameters: {'id': category.id}, + Routes.editTopicName, // Assuming an edit route exists + pathParameters: {'id': topic.id}, ); }, ), @@ -189,7 +186,7 @@ class _TopicsDataSource extends DataTableSource { onPressed: () { // Dispatch delete event context.read().add( - DeleteCategoryRequested(category.id), + DeleteTopicRequested(topic.id), ); }, ), @@ -211,11 +208,9 @@ class _TopicsDataSource extends DataTableSource { if (hasMore) { // When loading, we show an extra row for the spinner. // Otherwise, we just indicate that there are more rows. - return isLoading - ? categories.length + 1 - : categories.length + kDefaultRowsPerPage; + return isLoading ? topics.length + 1 : topics.length + kDefaultRowsPerPage; } - return categories.length; + return topics.length; } @override From 0491ea42ab18f4aad8cae6e7f11616f15611e3a7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 12:01:56 +0100 Subject: [PATCH 42/69] fix(content): complete migration of content management page to use topic Updates the `ContentManagementPage` to fully align with the migration from `Category` to `Topic`. - Changes the `TabBar` label from "Categories" to "Topics". - Updates the "Add New Item" button logic to correctly handle the `ContentManagementTab.topics` case and navigate to the `createTopicName` route. --- lib/content_management/view/content_management_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/content_management/view/content_management_page.dart b/lib/content_management/view/content_management_page.dart index afd6eddb..1585bbe6 100644 --- a/lib/content_management/view/content_management_page.dart +++ b/lib/content_management/view/content_management_page.dart @@ -84,7 +84,7 @@ class _ContentManagementPageState extends State isScrollable: true, tabs: [ Tab(text: l10n.headlines), - Tab(text: l10n.categories), + Tab(text: l10n.topics), Tab(text: l10n.sources), ], ), @@ -103,8 +103,8 @@ class _ContentManagementPageState extends State switch (currentTab) { case ContentManagementTab.headlines: context.goNamed(Routes.createHeadlineName); - case ContentManagementTab.categories: - context.goNamed(Routes.createCategoryName); + case ContentManagementTab.topics: + context.goNamed(Routes.createTopicName); case ContentManagementTab.sources: context.goNamed(Routes.createSourceName); } From 91bdc3d478d9c9c419585cb896f1207546c5bf25 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 12:04:46 +0100 Subject: [PATCH 43/69] refactor(l10n): migrate category localization to topic Updates the English localization file (`app_en.arb`) to reflect the application-wide migration from "Category" to "Topic". This involves renaming all relevant keys and updating all user-facing strings to ensure the UI is consistent with the new data model. --- lib/l10n/arb/app_en.arb | 66 ++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index d423deb1..62e62f8c 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -101,9 +101,9 @@ "@headlines": { "description": "Label for the headlines subpage" }, - "categories": "Categories", - "@categories": { - "description": "Label for the categories subpage" + "topics": "Topics", + "@topics": { + "description": "Label for the topics subpage" }, "sources": "Sources", "@sources": { @@ -648,17 +648,17 @@ "@unknown": { "description": "Fallback text for unknown values" }, - "loadingCategories": "Loading Categories", - "@loadingCategories": { - "description": "Headline for loading state of categories" + "loadingTopics": "Loading Topics", + "@loadingTopics": { + "description": "Headline for loading state of topics" }, - "noCategoriesFound": "No categories found.", - "@noCategoriesFound": { - "description": "Message when no categories are found" + "noTopicsFound": "No topics found.", + "@noTopicsFound": { + "description": "Message when no topics are found" }, - "categoryName": "Category Name", - "@categoryName": { - "description": "Label for the category name field in forms and tables." + "topicName": "Topic Name", + "@topicName": { + "description": "Label for the topic name field in forms and tables." }, "description": "Description", "@description": { @@ -688,37 +688,37 @@ "@language": { "description": "Column header for language" }, - "editCategory": "Edit Category", - "@editCategory": { - "description": "Title for the Edit Category page" + "editTopic": "Edit Topic", + "@editTopic": { + "description": "Title for the Edit Topic page" }, "saveChanges": "Save Changes", "@saveChanges": { "description": "Tooltip for the save changes button" }, - "loadingCategory": "Loading Category", - "@loadingCategory": { - "description": "Message displayed while loading category data" + "loadingTopic": "Loading Topic", + "@loadingTopic": { + "description": "Message displayed while loading topic data" }, "iconUrl": "Icon URL", "@iconUrl": { "description": "Label for the icon URL input field" }, - "categoryUpdatedSuccessfully": "Category updated successfully.", - "@categoryUpdatedSuccessfully": { - "description": "Message displayed when a category is updated successfully" + "topicUpdatedSuccessfully": "Topic updated successfully.", + "@topicUpdatedSuccessfully": { + "description": "Message displayed when a topic is updated successfully" }, - "cannotUpdateCategoryError": "Cannot update: Original category data not loaded.", - "@cannotUpdateCategoryError": { - "description": "Error message when updating a category fails because the original data wasn't loaded" + "cannotUpdateTopicError": "Cannot update: Original topic data not loaded.", + "@cannotUpdateTopicError": { + "description": "Error message when updating a topic fails because the original data wasn't loaded" }, - "createCategory": "Create Category", - "@createCategory": { - "description": "Title for the Create Category page" + "createTopic": "Create Topic", + "@createTopic": { + "description": "Title for the Create Topic page" }, - "categoryCreatedSuccessfully": "Category created successfully.", - "@categoryCreatedSuccessfully": { - "description": "Message displayed when a category is created successfully" + "topicCreatedSuccessfully": "Topic created successfully.", + "@topicCreatedSuccessfully": { + "description": "Message displayed when a topic is created successfully" }, "editSource": "Edit Source", "@editSource": { @@ -838,9 +838,9 @@ "@totalHeadlines": { "description": "Label for the total headlines summary card on the dashboard" }, - "totalCategories": "Total Categories", - "@totalCategories": { - "description": "Label for the total categories summary card on the dashboard" + "totalTopics": "Total Topics", + "@totalTopics": { + "description": "Label for the total topics summary card on the dashboard" }, "totalSources": "Total Sources", "@totalSources": { From 908c7a65559a77ad7be41bf92df7e8e4f13eea2b Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 12:08:17 +0100 Subject: [PATCH 44/69] fix(l10n): complete migration of category to topic in descriptions Updates the remaining user-facing description strings in `app_en.arb` to replace "categories" with "topics". This ensures the localization file is fully consistent with the application's data model. --- lib/l10n/arb/app_en.arb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 62e62f8c..5c84913b 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -93,7 +93,7 @@ "@contentManagement": { "description": "Label for the content management navigation item" }, - "contentManagementPageDescription": "Manage news headlines, categories, and sources for the Dashboard.", + "contentManagementPageDescription": "Manage news headlines, topics, and sources for the Dashboard.", "@contentManagementPageDescription": { "description": "Description for the Content Management page" }, @@ -202,7 +202,7 @@ "@confirmSaveButton": { "description": "Confirm save button label in dialog" }, - "userContentLimitsDescription": "These settings define the maximum number of countries, news sources, categories, and saved headlines a user can follow or save. Limits vary by user type (Guest, Standard, Premium) and directly impact what content users can curate.", + "userContentLimitsDescription": "These settings define the maximum number of countries, news sources, topics, and saved headlines a user can follow or save. Limits vary by user type (Guest, Standard, Premium) and directly impact what content users can curate.", "@userContentLimitsDescription": { "description": "Description for User Content Limits section" }, @@ -222,7 +222,7 @@ "@guestFollowedItemsLimitLabel": { "description": "Label for Guest Followed Items Limit" }, - "guestFollowedItemsLimitDescription": "Maximum number of countries, news sources, or categories a Guest user can follow (each type has its own limit).", + "guestFollowedItemsLimitDescription": "Maximum number of countries, news sources, or topics a Guest user can follow (each type has its own limit).", "@guestFollowedItemsLimitDescription": { "description": "Description for Guest Followed Items Limit" }, @@ -238,7 +238,7 @@ "@standardUserFollowedItemsLimitLabel": { "description": "Label for Standard User Followed Items Limit" }, - "standardUserFollowedItemsLimitDescription": "Maximum number of countries, news sources, or categories a Standard user can follow (each type has its own limit).", + "standardUserFollowedItemsLimitDescription": "Maximum number of countries, news sources, or topics a Standard user can follow (each type has its own limit).", "@standardUserFollowedItemsLimitDescription": { "description": "Description for Standard User Followed Items Limit" }, @@ -254,7 +254,7 @@ "@premiumFollowedItemsLimitLabel": { "description": "Label for Premium Followed Items Limit" }, - "premiumFollowedItemsLimitDescription": "Maximum number of countries, news sources, or categories a Premium user can follow (each type has its own limit).", + "premiumFollowedItemsLimitDescription": "Maximum number of countries, news sources, or topics a Premium user can follow (each type has its own limit).", "@premiumFollowedItemsLimitDescription": { "description": "Description for Premium Followed Items Limit" }, From 5d9fb53ea21ad333835b8a85a55776e45f5f4c53 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 12:20:37 +0100 Subject: [PATCH 45/69] refactor(l10n): migrate category localization to topic Updates the arabic localization file (`app_ar.arb`) to reflect the application-wide migration from "Category" to "Topic". This involves renaming all relevant keys and updating all user-facing strings to ensure the UI is consistent with the new data model. --- lib/l10n/arb/app_ar.arb | 78 ++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 66f79352..9c59c03a 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -93,7 +93,7 @@ "@contentManagement": { "description": "تسمية عنصر التنقل لإدارة المحتوى" }, - "contentManagementPageDescription": "إدارة العناوين الإخبارية والفئات والمصادر للوحة القيادة.", + "contentManagementPageDescription": "إدارة العناوين الإخبارية والمواضيع والمصادر للوحة القيادة.", "@contentManagementPageDescription": { "description": "وصف صفحة إدارة المحتوى" }, @@ -101,9 +101,9 @@ "@headlines": { "description": "تسمية الصفحة الفرعية للعناوين الرئيسية" }, - "categories": "الفئات", - "@categories": { - "description": "تسمية الصفحة الفرعية للفئات" + "topics": "المواضيع", + "@topics": { + "description": "تسمية الصفحة الفرعية للمواضيع" }, "sources": "المصادر", "@sources": { @@ -202,7 +202,7 @@ "@confirmSaveButton": { "description": "تسمية زر تأكيد الحفظ في مربع الحوار" }, - "userContentLimitsDescription": "تحدد هذه الإعدادات الحد الأقصى لعدد البلدان ومصادر الأخبار والفئات والعناوين المحفوظة التي يمكن للمستخدم متابعتها أو حفظها. تختلف الحدود حسب نوع المستخدم (ضيف، عادي، مميز) وتؤثر بشكل مباشر على المحتوى الذي يمكن للمستخدمين تنسيقه.", + "userContentLimitsDescription": "تحدد هذه الإعدادات الحد الأقصى لعدد البلدان ومصادر الأخبار والمواضيع والعناوين المحفوظة التي يمكن للمستخدم متابعتها أو حفظها. تختلف الحدود حسب نوع المستخدم (ضيف، عادي، مميز) وتؤثر بشكل مباشر على المحتوى الذي يمكن للمستخدمين تنسيقه.", "@userContentLimitsDescription": { "description": "وصف قسم حدود محتوى المستخدم" }, @@ -222,7 +222,7 @@ "@guestFollowedItemsLimitLabel": { "description": "تسمية حد العناصر المتابعة للضيف" }, - "guestFollowedItemsLimitDescription": "الحد الأقصى لعدد البلدان أو مصادر الأخبار أو الفئات التي يمكن للمستخدم الضيف متابعتها (لكل نوع حد خاص به).", + "guestFollowedItemsLimitDescription": "الحد الأقصى لعدد البلدان أو مصادر الأخبار أو المواضيع التي يمكن للمستخدم الضيف متابعتها (لكل نوع حد خاص به).", "@guestFollowedItemsLimitDescription": { "description": "وصف حد العناصر المتابعة للضيف" }, @@ -238,7 +238,7 @@ "@standardUserFollowedItemsLimitLabel": { "description": "تسمية حد العناصر المتابعة للمستخدم العادي" }, - "standardUserFollowedItemsLimitDescription": "الحد الأقصى لعدد البلدان أو مصادر الأخبار أو الفئات التي يمكن للمستخدم العادي متابعتها (لكل نوع حد خاص به).", + "standardUserFollowedItemsLimitDescription": "الحد الأقصى لعدد البلدان أو مصادر الأخبار أو المواضيع التي يمكن للمستخدم العادي متابعتها (لكل نوع حد خاص به).", "@standardUserFollowedItemsLimitDescription": { "description": "وصف حد العناصر المتابعة للمستخدم العادي" }, @@ -254,7 +254,7 @@ "@premiumFollowedItemsLimitLabel": { "description": "تسمية حد العناصر المتابعة للمستخدم المميز" }, - "premiumFollowedItemsLimitDescription": "الحد الأقصى لعدد البلدان أو مصادر الأخبار أو الفئات التي يمكن للمستخدم المميز متابعتها (لكل نوع حد خاص به).", + "premiumFollowedItemsLimitDescription": "الحد الأقصى لعدد البلدان أو مصادر الأخبار أو المواضيع التي يمكن للمستخدم المميز متابعتها (لكل نوع حد خاص به).", "@premiumFollowedItemsLimitDescription": { "description": "وصف حد العناصر المتابعة للمستخدم المميز" }, @@ -648,17 +648,17 @@ "@unknown": { "description": "نص احتياطي للقيم غير المعروفة" }, - "loadingCategories": "جاري تحميل الفئات", - "@loadingCategories": { - "description": "عنوان حالة تحميل الفئات" + "loadingTopics": "جاري تحميل المواضيع", + "@loadingTopics": { + "description": "عنوان حالة تحميل المواضيع" }, - "noCategoriesFound": "لم يتم العثور على فئات.", - "@noCategoriesFound": { - "description": "رسالة عند عدم العثور على فئات" + "noTopicsFound": "لم يتم العثور على مواضيع.", + "@noTopicsFound": { + "description": "رسالة عند عدم العثور على مواضيع" }, - "categoryName": "اسم الفئة", - "@categoryName": { - "description": "تسمية حقل اسم الفئة في النماذج والجداول." + "topicName": "اسم الموضوع", + "@topicName": { + "description": "تسمية حقل اسم الموضوع في النماذج والجداول." }, "description": "الوصف", "@description": { @@ -688,37 +688,37 @@ "@language": { "description": "رأس العمود للغة" }, - "editCategory": "تعديل الفئة", - "@editCategory": { - "description": "عنوان صفحة تعديل الفئة" + "editTopic": "تعديل الموضوع", + "@editTopic": { + "description": "عنوان صفحة تعديل الموضوع" }, "saveChanges": "حفظ التغييرات", "@saveChanges": { "description": "تلميح لزر حفظ التغييرات" }, - "loadingCategory": "جاري تحميل الفئة", - "@loadingCategory": { - "description": "رسالة تُعرض أثناء تحميل بيانات الفئة" + "loadingTopic": "جاري تحميل الموضوع", + "@loadingTopic": { + "description": "رسالة تُعرض أثناء تحميل بيانات الموضوع" }, "iconUrl": "رابط الأيقونة", "@iconUrl": { "description": "تسمية حقل إدخال رابط الأيقونة" }, - "categoryUpdatedSuccessfully": "تم تحديث الفئة بنجاح.", - "@categoryUpdatedSuccessfully": { - "description": "رسالة تُعرض عند تحديث الفئة بنجاح" + "topicUpdatedSuccessfully": "تم تحديث الموضوع بنجاح.", + "@topicUpdatedSuccessfully": { + "description": "رسالة تُعرض عند تحديث الموضوع بنجاح" }, - "cannotUpdateCategoryError": "لا يمكن التحديث: لم يتم تحميل بيانات الفئة الأصلية.", - "@cannotUpdateCategoryError": { - "description": "رسالة خطأ عند فشل تحديث الفئة بسبب عدم تحميل البيانات الأصلية" + "cannotUpdateTopicError": "لا يمكن التحديث: لم يتم تحميل بيانات الموضوع الأصلية.", + "@cannotUpdateTopicError": { + "description": "رسالة خطأ عند فشل تحديث الموضوع بسبب عدم تحميل البيانات الأصلية" }, - "createCategory": "إنشاء فئة", - "@createCategory": { - "description": "عنوان صفحة إنشاء فئة" + "createTopic": "إنشاء موضوع", + "@createTopic": { + "description": "عنوان صفحة إنشاء موضوع" }, - "categoryCreatedSuccessfully": "تم إنشاء الفئة بنجاح.", - "@categoryCreatedSuccessfully": { - "description": "رسالة تُعرض عند إنشاء الفئة بنجاح" + "topicCreatedSuccessfully": "تم إنشاء الموضوع بنجاح.", + "@topicCreatedSuccessfully": { + "description": "رسالة تُعرض عند إنشاء الموضوع بنجاح" }, "editSource": "تعديل المصدر", "@editSource": { @@ -838,9 +838,9 @@ "@totalHeadlines": { "description": "تسمية بطاقة ملخص إجمالي العناوين في لوحة القيادة" }, - "totalCategories": "إجمالي الفئات", - "@totalCategories": { - "description": "تسمية بطاقة ملخص إجمالي الفئات في لوحة القيادة" + "totalTopics": "إجمالي المواضيع", + "@totalTopics": { + "description": "تسمية بطاقة ملخص إجمالي المواضيع في لوحة القيادة" }, "totalSources": "إجمالي المصادر", "@totalSources": { @@ -922,4 +922,4 @@ } } } -} \ No newline at end of file +} From 3aadf1869645e3098034419990de1ca4135877ac Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 12:21:07 +0100 Subject: [PATCH 46/69] fix(l10n): update translations for 'category' to 'topic' - Updated English, Arabic translations - Changed "category" to "topic" in UI - Updated related descriptions and labels - Corrected loading and error messages - Reflected changes in dashboard summary --- lib/l10n/app_localizations.dart | 76 +++++++++++++++--------------- lib/l10n/app_localizations_ar.dart | 34 ++++++------- lib/l10n/app_localizations_en.dart | 34 ++++++------- 3 files changed, 72 insertions(+), 72 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8d322543..179048ef 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -227,7 +227,7 @@ abstract class AppLocalizations { /// Description for the Content Management page /// /// In en, this message translates to: - /// **'Manage news headlines, categories, and sources for the Dashboard.'** + /// **'Manage news headlines, topics, and sources for the Dashboard.'** String get contentManagementPageDescription; /// Label for the headlines subpage @@ -236,11 +236,11 @@ abstract class AppLocalizations { /// **'Headlines'** String get headlines; - /// Label for the categories subpage + /// Label for the topics subpage /// /// In en, this message translates to: - /// **'Categories'** - String get categories; + /// **'Topics'** + String get topics; /// Label for the sources subpage /// @@ -383,7 +383,7 @@ abstract class AppLocalizations { /// Description for User Content Limits section /// /// In en, this message translates to: - /// **'These settings define the maximum number of countries, news sources, categories, and saved headlines a user can follow or save. Limits vary by user type (Guest, Standard, Premium) and directly impact what content users can curate.'** + /// **'These settings define the maximum number of countries, news sources, topics, and saved headlines a user can follow or save. Limits vary by user type (Guest, Standard, Premium) and directly impact what content users can curate.'** String get userContentLimitsDescription; /// Tab title for Guest user role @@ -413,7 +413,7 @@ abstract class AppLocalizations { /// Description for Guest Followed Items Limit /// /// In en, this message translates to: - /// **'Maximum number of countries, news sources, or categories a Guest user can follow (each type has its own limit).'** + /// **'Maximum number of countries, news sources, or topics a Guest user can follow (each type has its own limit).'** String get guestFollowedItemsLimitDescription; /// Label for Guest Saved Headlines Limit @@ -437,7 +437,7 @@ abstract class AppLocalizations { /// Description for Standard User Followed Items Limit /// /// In en, this message translates to: - /// **'Maximum number of countries, news sources, or categories a Standard user can follow (each type has its own limit).'** + /// **'Maximum number of countries, news sources, or topics a Standard user can follow (each type has its own limit).'** String get standardUserFollowedItemsLimitDescription; /// Label for Standard User Saved Headlines Limit @@ -461,7 +461,7 @@ abstract class AppLocalizations { /// Description for Premium Followed Items Limit /// /// In en, this message translates to: - /// **'Maximum number of countries, news sources, or categories a Premium user can follow (each type has its own limit).'** + /// **'Maximum number of countries, news sources, or topics a Premium user can follow (each type has its own limit).'** String get premiumFollowedItemsLimitDescription; /// Label for Premium Saved Headlines Limit @@ -1034,23 +1034,23 @@ abstract class AppLocalizations { /// **'Unknown'** String get unknown; - /// Headline for loading state of categories + /// Headline for loading state of topics /// /// In en, this message translates to: - /// **'Loading Categories'** - String get loadingCategories; + /// **'Loading Topics'** + String get loadingTopics; - /// Message when no categories are found + /// Message when no topics are found /// /// In en, this message translates to: - /// **'No categories found.'** - String get noCategoriesFound; + /// **'No topics found.'** + String get noTopicsFound; - /// Label for the category name field in forms and tables. + /// Label for the topic name field in forms and tables. /// /// In en, this message translates to: - /// **'Category Name'** - String get categoryName; + /// **'Topic Name'** + String get topicName; /// Column header for description /// @@ -1094,11 +1094,11 @@ abstract class AppLocalizations { /// **'Language'** String get language; - /// Title for the Edit Category page + /// Title for the Edit Topic page /// /// In en, this message translates to: - /// **'Edit Category'** - String get editCategory; + /// **'Edit Topic'** + String get editTopic; /// Tooltip for the save changes button /// @@ -1106,11 +1106,11 @@ abstract class AppLocalizations { /// **'Save Changes'** String get saveChanges; - /// Message displayed while loading category data + /// Message displayed while loading topic data /// /// In en, this message translates to: - /// **'Loading Category'** - String get loadingCategory; + /// **'Loading Topic'** + String get loadingTopic; /// Label for the icon URL input field /// @@ -1118,29 +1118,29 @@ abstract class AppLocalizations { /// **'Icon URL'** String get iconUrl; - /// Message displayed when a category is updated successfully + /// Message displayed when a topic is updated successfully /// /// In en, this message translates to: - /// **'Category updated successfully.'** - String get categoryUpdatedSuccessfully; + /// **'Topic updated successfully.'** + String get topicUpdatedSuccessfully; - /// Error message when updating a category fails because the original data wasn't loaded + /// Error message when updating a topic fails because the original data wasn't loaded /// /// In en, this message translates to: - /// **'Cannot update: Original category data not loaded.'** - String get cannotUpdateCategoryError; + /// **'Cannot update: Original topic data not loaded.'** + String get cannotUpdateTopicError; - /// Title for the Create Category page + /// Title for the Create Topic page /// /// In en, this message translates to: - /// **'Create Category'** - String get createCategory; + /// **'Create Topic'** + String get createTopic; - /// Message displayed when a category is created successfully + /// Message displayed when a topic is created successfully /// /// In en, this message translates to: - /// **'Category created successfully.'** - String get categoryCreatedSuccessfully; + /// **'Topic created successfully.'** + String get topicCreatedSuccessfully; /// Title for the Edit Source page /// @@ -1334,11 +1334,11 @@ abstract class AppLocalizations { /// **'Total Headlines'** String get totalHeadlines; - /// Label for the total categories summary card on the dashboard + /// Label for the total topics summary card on the dashboard /// /// In en, this message translates to: - /// **'Total Categories'** - String get totalCategories; + /// **'Total Topics'** + String get totalTopics; /// Label for the total sources summary card on the dashboard /// diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index fdcbfbc0..40e9eff7 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -82,13 +82,13 @@ class AppLocalizationsAr extends AppLocalizations { @override String get contentManagementPageDescription => - 'إدارة العناوين الإخبارية والفئات والمصادر للوحة القيادة.'; + 'إدارة العناوين الإخبارية والمواضيع والمصادر للوحة القيادة.'; @override String get headlines => 'العناوين الرئيسية'; @override - String get categories => 'الفئات'; + String get topics => 'المواضيع'; @override String get sources => 'المصادر'; @@ -167,7 +167,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get userContentLimitsDescription => - 'تحدد هذه الإعدادات الحد الأقصى لعدد البلدان ومصادر الأخبار والفئات والعناوين المحفوظة التي يمكن للمستخدم متابعتها أو حفظها. تختلف الحدود حسب نوع المستخدم (ضيف، عادي، مميز) وتؤثر بشكل مباشر على المحتوى الذي يمكن للمستخدمين تنسيقه.'; + 'تحدد هذه الإعدادات الحد الأقصى لعدد البلدان ومصادر الأخبار والمواضيع والعناوين المحفوظة التي يمكن للمستخدم متابعتها أو حفظها. تختلف الحدود حسب نوع المستخدم (ضيف، عادي، مميز) وتؤثر بشكل مباشر على المحتوى الذي يمكن للمستخدمين تنسيقه.'; @override String get guestUserTab => 'ضيف'; @@ -183,7 +183,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get guestFollowedItemsLimitDescription => - 'الحد الأقصى لعدد البلدان أو مصادر الأخبار أو الفئات التي يمكن للمستخدم الضيف متابعتها (لكل نوع حد خاص به).'; + 'الحد الأقصى لعدد البلدان أو مصادر الأخبار أو المواضيع التي يمكن للمستخدم الضيف متابعتها (لكل نوع حد خاص به).'; @override String get guestSavedHeadlinesLimitLabel => 'حد العناوين المحفوظة للضيف'; @@ -198,7 +198,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get standardUserFollowedItemsLimitDescription => - 'الحد الأقصى لعدد البلدان أو مصادر الأخبار أو الفئات التي يمكن للمستخدم العادي متابعتها (لكل نوع حد خاص به).'; + 'الحد الأقصى لعدد البلدان أو مصادر الأخبار أو المواضيع التي يمكن للمستخدم العادي متابعتها (لكل نوع حد خاص به).'; @override String get standardUserSavedHeadlinesLimitLabel => @@ -214,7 +214,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get premiumFollowedItemsLimitDescription => - 'الحد الأقصى لعدد البلدان أو مصادر الأخبار أو الفئات التي يمكن للمستخدم المميز متابعتها (لكل نوع حد خاص به).'; + 'الحد الأقصى لعدد البلدان أو مصادر الأخبار أو المواضيع التي يمكن للمستخدم المميز متابعتها (لكل نوع حد خاص به).'; @override String get premiumSavedHeadlinesLimitLabel => @@ -545,13 +545,13 @@ class AppLocalizationsAr extends AppLocalizations { String get unknown => 'غير معروف'; @override - String get loadingCategories => 'جاري تحميل الفئات'; + String get loadingTopics => 'جاري تحميل المواضيع'; @override - String get noCategoriesFound => 'لم يتم العثور على فئات.'; + String get noTopicsFound => 'لم يتم العثور على مواضيع.'; @override - String get categoryName => 'اسم الفئة'; + String get topicName => 'اسم الموضوع'; @override String get description => 'الوصف'; @@ -575,29 +575,29 @@ class AppLocalizationsAr extends AppLocalizations { String get language => 'اللغة'; @override - String get editCategory => 'تعديل الفئة'; + String get editTopic => 'تعديل الموضوع'; @override String get saveChanges => 'حفظ التغييرات'; @override - String get loadingCategory => 'جاري تحميل الفئة'; + String get loadingTopic => 'جاري تحميل الموضوع'; @override String get iconUrl => 'رابط الأيقونة'; @override - String get categoryUpdatedSuccessfully => 'تم تحديث الفئة بنجاح.'; + String get topicUpdatedSuccessfully => 'تم تحديث الموضوع بنجاح.'; @override - String get cannotUpdateCategoryError => - 'لا يمكن التحديث: لم يتم تحميل بيانات الفئة الأصلية.'; + String get cannotUpdateTopicError => + 'لا يمكن التحديث: لم يتم تحميل بيانات الموضوع الأصلية.'; @override - String get createCategory => 'إنشاء فئة'; + String get createTopic => 'إنشاء موضوع'; @override - String get categoryCreatedSuccessfully => 'تم إنشاء الفئة بنجاح.'; + String get topicCreatedSuccessfully => 'تم إنشاء الموضوع بنجاح.'; @override String get editSource => 'تعديل المصدر'; @@ -698,7 +698,7 @@ class AppLocalizationsAr extends AppLocalizations { String get totalHeadlines => 'إجمالي العناوين'; @override - String get totalCategories => 'إجمالي الفئات'; + String get totalTopics => 'إجمالي المواضيع'; @override String get totalSources => 'إجمالي المصادر'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2bf1ee6f..ba2a7305 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -81,13 +81,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contentManagementPageDescription => - 'Manage news headlines, categories, and sources for the Dashboard.'; + 'Manage news headlines, topics, and sources for the Dashboard.'; @override String get headlines => 'Headlines'; @override - String get categories => 'Categories'; + String get topics => 'Topics'; @override String get sources => 'Sources'; @@ -168,7 +168,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get userContentLimitsDescription => - 'These settings define the maximum number of countries, news sources, categories, and saved headlines a user can follow or save. Limits vary by user type (Guest, Standard, Premium) and directly impact what content users can curate.'; + 'These settings define the maximum number of countries, news sources, topics, and saved headlines a user can follow or save. Limits vary by user type (Guest, Standard, Premium) and directly impact what content users can curate.'; @override String get guestUserTab => 'Guest'; @@ -184,7 +184,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get guestFollowedItemsLimitDescription => - 'Maximum number of countries, news sources, or categories a Guest user can follow (each type has its own limit).'; + 'Maximum number of countries, news sources, or topics a Guest user can follow (each type has its own limit).'; @override String get guestSavedHeadlinesLimitLabel => 'Guest Saved Headlines Limit'; @@ -199,7 +199,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get standardUserFollowedItemsLimitDescription => - 'Maximum number of countries, news sources, or categories a Standard user can follow (each type has its own limit).'; + 'Maximum number of countries, news sources, or topics a Standard user can follow (each type has its own limit).'; @override String get standardUserSavedHeadlinesLimitLabel => @@ -214,7 +214,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get premiumFollowedItemsLimitDescription => - 'Maximum number of countries, news sources, or categories a Premium user can follow (each type has its own limit).'; + 'Maximum number of countries, news sources, or topics a Premium user can follow (each type has its own limit).'; @override String get premiumSavedHeadlinesLimitLabel => 'Premium Saved Headlines Limit'; @@ -543,13 +543,13 @@ class AppLocalizationsEn extends AppLocalizations { String get unknown => 'Unknown'; @override - String get loadingCategories => 'Loading Categories'; + String get loadingTopics => 'Loading Topics'; @override - String get noCategoriesFound => 'No categories found.'; + String get noTopicsFound => 'No topics found.'; @override - String get categoryName => 'Category Name'; + String get topicName => 'Topic Name'; @override String get description => 'Description'; @@ -573,29 +573,29 @@ class AppLocalizationsEn extends AppLocalizations { String get language => 'Language'; @override - String get editCategory => 'Edit Category'; + String get editTopic => 'Edit Topic'; @override String get saveChanges => 'Save Changes'; @override - String get loadingCategory => 'Loading Category'; + String get loadingTopic => 'Loading Topic'; @override String get iconUrl => 'Icon URL'; @override - String get categoryUpdatedSuccessfully => 'Category updated successfully.'; + String get topicUpdatedSuccessfully => 'Topic updated successfully.'; @override - String get cannotUpdateCategoryError => - 'Cannot update: Original category data not loaded.'; + String get cannotUpdateTopicError => + 'Cannot update: Original topic data not loaded.'; @override - String get createCategory => 'Create Category'; + String get createTopic => 'Create Topic'; @override - String get categoryCreatedSuccessfully => 'Category created successfully.'; + String get topicCreatedSuccessfully => 'Topic created successfully.'; @override String get editSource => 'Edit Source'; @@ -696,7 +696,7 @@ class AppLocalizationsEn extends AppLocalizations { String get totalHeadlines => 'Total Headlines'; @override - String get totalCategories => 'Total Categories'; + String get totalTopics => 'Total Topics'; @override String get totalSources => 'Total Sources'; From 6ae462eeb7d44e23f142c5d427166de7e755702a Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 13:56:52 +0100 Subject: [PATCH 47/69] feat(l10n): add excerpt and country fields - Added 'excerpt' and 'countryName' fields - Updated translations for all locales - Updated ARB files with descriptions - Renamed category to topic in routes - Updated route paths and names --- lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_ar.dart | 6 ++++++ lib/l10n/app_localizations_en.dart | 6 ++++++ lib/l10n/arb/app_ar.arb | 8 ++++++++ lib/l10n/arb/app_en.arb | 10 +++++++++- lib/router/router.dart | 12 ++++++------ lib/router/routes.dart | 16 ++++++++-------- 7 files changed, 55 insertions(+), 15 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 179048ef..81cc77a5 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1016,6 +1016,18 @@ abstract class AppLocalizations { /// **'Title'** String get headlineTitle; + /// Label for the excerpt input field + /// + /// In en, this message translates to: + /// **'Excerpt'** + String get excerpt; + + /// Label for the country name dropdown field + /// + /// In en, this message translates to: + /// **'Country'** + String get countryName; + /// Column header for published date /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 40e9eff7..25a4b6ad 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -535,6 +535,12 @@ class AppLocalizationsAr extends AppLocalizations { @override String get headlineTitle => 'العنوان'; + @override + String get excerpt => 'المقتطف'; + + @override + String get countryName => 'البلد'; + @override String get publishedAt => 'تاريخ النشر'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ba2a7305..9fc16645 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -533,6 +533,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get headlineTitle => 'Title'; + @override + String get excerpt => 'Excerpt'; + + @override + String get countryName => 'Country'; + @override String get publishedAt => 'Published At'; diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 9c59c03a..6af647cc 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -636,6 +636,14 @@ "@headlineTitle": { "description": "رأس العمود لعنوان الخبر" }, + "excerpt": "المقتطف", + "@excerpt": { + "description": "تسمية حقل إدخال المقتطف" + }, + "countryName": "البلد", + "@countryName": { + "description": "تسمية حقل القائمة المنسدلة لاسم البلد" + }, "publishedAt": "تاريخ النشر", "@publishedAt": { "description": "رأس العمود لتاريخ النشر" diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 5c84913b..82482e91 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -636,6 +636,14 @@ "@headlineTitle": { "description": "Column header for headline title" }, + "excerpt": "Excerpt", + "@excerpt": { + "description": "Label for the excerpt input field" + }, + "countryName": "Country", + "@countryName": { + "description": "Label for the country name dropdown field" + }, "publishedAt": "Published At", "@publishedAt": { "description": "Column header for published date" @@ -923,4 +931,4 @@ } } -} \ No newline at end of file +} diff --git a/lib/router/router.dart b/lib/router/router.dart index 8e72f55f..67bc9d15 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -170,16 +170,16 @@ GoRouter createRouter({ }, ), GoRoute( - path: Routes.createCategory, - name: Routes.createCategoryName, - builder: (context, state) => const CreateCategoryPage(), + path: Routes.createTopic, + name: Routes.createTopicName, + builder: (context, state) => const CreateTopicPage(), ), GoRoute( - path: Routes.editCategory, - name: Routes.editCategoryName, + path: Routes.editTopic, + name: Routes.editTopicName, builder: (context, state) { final id = state.pathParameters['id']!; - return EditCategoryPage(categoryId: id); + return EditTopicPage(topicId: id); }, ), GoRoute( diff --git a/lib/router/routes.dart b/lib/router/routes.dart index 07696318..88346a03 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -56,17 +56,17 @@ abstract final class Routes { /// The name for the edit headline page route. static const String editHeadlineName = 'editHeadline'; - /// The path for creating a new category. - static const String createCategory = 'create-category'; + /// The path for creating a new topic. + static const String createTopic = 'create-topic'; - /// The name for the create category page route. - static const String createCategoryName = 'createCategory'; + /// The name for the create topic page route. + static const String createTopicName = 'createTopic'; - /// The path for editing an existing category. - static const String editCategory = 'edit-category/:id'; + /// The path for editing an existing topic. + static const String editTopic = 'edit-topic/:id'; - /// The name for the edit category page route. - static const String editCategoryName = 'editCategory'; + /// The name for the edit topic page route. + static const String editTopicName = 'editTopic'; /// The path for creating a new source. static const String createSource = 'create-source'; From c7364ec69dfd3c56f7c4f7d436ffb8b91458889c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:03:21 +0100 Subject: [PATCH 48/69] refactor(app_config): Rename AppConfig to RemoteConfig - Renamed `appConfig` to `remoteConfig` - Renamed `originalAppConfig` to `originalRemoteConfig` - Updated usage in `AppConfigurationState` --- .../bloc/app_configuration_state.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/app_configuration/bloc/app_configuration_state.dart b/lib/app_configuration/bloc/app_configuration_state.dart index 467d95ec..52f6dd6c 100644 --- a/lib/app_configuration/bloc/app_configuration_state.dart +++ b/lib/app_configuration/bloc/app_configuration_state.dart @@ -22,8 +22,8 @@ class AppConfigurationState extends Equatable { /// {@macro app_configuration_state} const AppConfigurationState({ this.status = AppConfigurationStatus.initial, - this.appConfig, - this.originalAppConfig, + this.remoteConfig, + this.originalRemoteConfig, this.errorMessage, this.isDirty = false, this.showSaveSuccess = false, @@ -33,10 +33,10 @@ class AppConfigurationState extends Equatable { final AppConfigurationStatus status; /// The loaded or updated application configuration. - final AppConfig? appConfig; + final RemoteConfig? remoteConfig; /// The original application configuration loaded from the backend. - final AppConfig? originalAppConfig; + final RemoteConfig? originalRemoteConfig; /// An error message if an operation failed. final String? errorMessage; @@ -50,8 +50,8 @@ class AppConfigurationState extends Equatable { /// Creates a copy of the current state with updated values. AppConfigurationState copyWith({ AppConfigurationStatus? status, - AppConfig? appConfig, - AppConfig? originalAppConfig, + RemoteConfig? remoteConfig, + RemoteConfig? originalRemoteConfig, String? errorMessage, bool? isDirty, bool clearErrorMessage = false, @@ -60,8 +60,8 @@ class AppConfigurationState extends Equatable { }) { return AppConfigurationState( status: status ?? this.status, - appConfig: appConfig ?? this.appConfig, - originalAppConfig: originalAppConfig ?? this.originalAppConfig, + remoteConfig: remoteConfig ?? this.remoteConfig, + originalRemoteConfig: originalRemoteConfig ?? this.originalRemoteConfig, errorMessage: clearErrorMessage ? null : errorMessage ?? this.errorMessage, @@ -75,8 +75,8 @@ class AppConfigurationState extends Equatable { @override List get props => [ status, - appConfig, - originalAppConfig, + remoteConfig, + originalRemoteConfig, errorMessage, isDirty, showSaveSuccess, From 3b70497c9b2e439d8b8e8b94426217431680c537 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:04:22 +0100 Subject: [PATCH 49/69] refactor(app_config): Rename AppConfig to RemoteConfig - Renamed `appConfig` to `remoteConfig` - Updated event props accordingly - Improved code clarity and consistency --- .../bloc/app_configuration_event.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/app_configuration/bloc/app_configuration_event.dart b/lib/app_configuration/bloc/app_configuration_event.dart index 478f21c4..0fc89bf0 100644 --- a/lib/app_configuration/bloc/app_configuration_event.dart +++ b/lib/app_configuration/bloc/app_configuration_event.dart @@ -24,13 +24,13 @@ class AppConfigurationLoaded extends AppConfigurationEvent { /// {@endtemplate} class AppConfigurationUpdated extends AppConfigurationEvent { /// {@macro app_configuration_updated} - const AppConfigurationUpdated(this.appConfig); + const AppConfigurationUpdated(this.remoteConfig); /// The updated application configuration. - final AppConfig appConfig; + final RemoteConfig remoteConfig; @override - List get props => [appConfig]; + List get props => [remoteConfig]; } /// {@template app_configuration_discarded} @@ -50,12 +50,12 @@ class AppConfigurationDiscarded extends AppConfigurationEvent { class AppConfigurationFieldChanged extends AppConfigurationEvent { /// {@macro app_configuration_field_changed} const AppConfigurationFieldChanged({ - this.appConfig, + this.remoteConfig, }); - /// The partially or fully updated AppConfig object. - final AppConfig? appConfig; + /// The partially or fully updated RemoteConfig object. + final RemoteConfig? remoteConfig; @override - List get props => [appConfig]; + List get props => [remoteConfig]; } From 4f588ca56d5ce2903342e9eaded9b97d2261280f Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:05:10 +0100 Subject: [PATCH 50/69] refactor(app_config): Rename AppConfig to RemoteConfig - Renamed `AppConfig` to `RemoteConfig` - Updated repository and bloc usage - Adjusted variable names accordingly - Ensured consistent naming throughout - Updated comments for clarity --- .../bloc/app_configuration_bloc.dart | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/app_configuration/bloc/app_configuration_bloc.dart b/lib/app_configuration/bloc/app_configuration_bloc.dart index 8085e182..7c443c2e 100644 --- a/lib/app_configuration/bloc/app_configuration_bloc.dart +++ b/lib/app_configuration/bloc/app_configuration_bloc.dart @@ -1,7 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; -import 'package:ht_shared/ht_shared.dart'; // Use AppConfig from ht_shared +import 'package:ht_shared/ht_shared.dart'; // Use RemoteConfig from ht_shared part 'app_configuration_event.dart'; part 'app_configuration_state.dart'; @@ -9,8 +9,8 @@ part 'app_configuration_state.dart'; class AppConfigurationBloc extends Bloc { AppConfigurationBloc({ - required HtDataRepository appConfigRepository, - }) : _appConfigRepository = appConfigRepository, + required HtDataRepository remoteConfigRepository, + }) : _remoteConfigRepository = remoteConfigRepository, super( const AppConfigurationState(), ) { @@ -20,7 +20,7 @@ class AppConfigurationBloc on(_onAppConfigurationDiscarded); } - final HtDataRepository _appConfigRepository; + final HtDataRepository _remoteConfigRepository; Future _onAppConfigurationLoaded( AppConfigurationLoaded event, @@ -28,12 +28,12 @@ class AppConfigurationBloc ) async { emit(state.copyWith(status: AppConfigurationStatus.loading)); try { - final appConfig = await _appConfigRepository.read(id: 'app_config'); + final remoteConfig = await _remoteConfigRepository.read(id: 'app_config'); emit( state.copyWith( status: AppConfigurationStatus.success, - appConfig: appConfig, - originalAppConfig: appConfig, // Store the original config + remoteConfig: remoteConfig, + originalRemoteConfig: remoteConfig, // Store the original config isDirty: false, clearShowSaveSuccess: true, // Clear any previous success snackbar flag @@ -62,15 +62,15 @@ class AppConfigurationBloc ) async { emit(state.copyWith(status: AppConfigurationStatus.loading)); try { - final updatedConfig = await _appConfigRepository.update( - id: event.appConfig.id, - item: event.appConfig, + final updatedConfig = await _remoteConfigRepository.update( + id: event.remoteConfig.id, + item: event.remoteConfig, ); emit( state.copyWith( status: AppConfigurationStatus.success, - appConfig: updatedConfig, - originalAppConfig: updatedConfig, // Update original config on save + remoteConfig: updatedConfig, + originalRemoteConfig: updatedConfig, // Update original config on save isDirty: false, showSaveSuccess: true, // Set flag to show success snackbar ), @@ -98,7 +98,7 @@ class AppConfigurationBloc ) { emit( state.copyWith( - appConfig: event.appConfig, + remoteConfig: event.remoteConfig, isDirty: true, clearErrorMessage: true, // Clear any previous error messages clearShowSaveSuccess: true, // Clear success snackbar on field change @@ -112,7 +112,7 @@ class AppConfigurationBloc ) { emit( state.copyWith( - appConfig: state.originalAppConfig, // Revert to original config + remoteConfig: state.originalRemoteConfig, // Revert to original config isDirty: false, clearErrorMessage: true, // Clear any previous error messages clearShowSaveSuccess: true, // Clear success snackbar From f8f0cdc28888c3953b3d27e07f1a5253b4a67543 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:08:37 +0100 Subject: [PATCH 51/69] refactor(app_config): Rename AppConfig to RemoteConfig - Renamed `AppConfig` to `RemoteConfig` - Updated variable names accordingly - Replaced `UserRoles` with `AppUserRole` - Removed Force Update section - Improved App Status section --- .../view/app_configuration_page.dart | 513 ++++++++---------- 1 file changed, 235 insertions(+), 278 deletions(-) diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index cf9cb902..be8b7bee 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -4,7 +4,8 @@ import 'package:ht_dashboard/app_configuration/bloc/app_configuration_bloc.dart' import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; import 'package:ht_dashboard/shared/widgets/widgets.dart'; -import 'package:ht_shared/ht_shared.dart'; // For AppConfig and its nested models +import 'package:ht_shared/ht_shared.dart'; +import 'package:ht_shared/src/enums/app_user_role.dart'; /// {@template app_configuration_page} /// A page for managing the application's remote configuration. @@ -117,8 +118,8 @@ class _AppConfigurationPageState extends State { }, ); } else if (state.status == AppConfigurationStatus.success && - state.appConfig != null) { - final appConfig = state.appConfig!; + state.remoteConfig != null) { + final remoteConfig = state.remoteConfig!; return ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ @@ -128,7 +129,7 @@ class _AppConfigurationPageState extends State { horizontal: AppSpacing.xxl, ), children: [ - _buildUserPreferenceLimitsSection(context, appConfig), + _buildUserPreferenceLimitsSection(context, remoteConfig), ], ), ExpansionTile( @@ -137,7 +138,7 @@ class _AppConfigurationPageState extends State { horizontal: AppSpacing.xxl, ), children: [ - _buildAdConfigSection(context, appConfig), + _buildAdConfigSection(context, remoteConfig), ], ), ExpansionTile( @@ -146,7 +147,7 @@ class _AppConfigurationPageState extends State { horizontal: AppSpacing.xxl, ), children: [ - _buildAccountActionConfigSection(context, appConfig), + _buildAccountActionConfigSection(context, remoteConfig), ], ), ExpansionTile( @@ -155,16 +156,7 @@ class _AppConfigurationPageState extends State { horizontal: AppSpacing.xxl, ), children: [ - _buildKillSwitchSection(context, appConfig), - ], - ), - ExpansionTile( - title: Text(l10n.forceUpdateTab), - childrenPadding: const EdgeInsets.symmetric( - horizontal: AppSpacing.xxl, - ), - children: [ - _buildForceUpdateSection(context, appConfig), + _buildAppStatusSection(context, remoteConfig), ], ), ], @@ -185,8 +177,8 @@ class _AppConfigurationPageState extends State { final isDirty = context.select( (AppConfigurationBloc bloc) => bloc.state.isDirty, ); - final appConfig = context.select( - (AppConfigurationBloc bloc) => bloc.state.appConfig, + final remoteConfig = context.select( + (AppConfigurationBloc bloc) => bloc.state.remoteConfig, ); return BottomAppBar( @@ -211,9 +203,9 @@ class _AppConfigurationPageState extends State { onPressed: isDirty ? () async { final confirmed = await _showConfirmationDialog(context); - if (context.mounted && confirmed && appConfig != null) { + if (context.mounted && confirmed && remoteConfig != null) { context.read().add( - AppConfigurationUpdated(appConfig), + AppConfigurationUpdated(remoteConfig), ); } } @@ -263,7 +255,7 @@ class _AppConfigurationPageState extends State { Widget _buildUserPreferenceLimitsSection( BuildContext context, - AppConfig appConfig, + RemoteConfig remoteConfig, ) { final l10n = context.l10n; return Column( @@ -283,12 +275,12 @@ class _AppConfigurationPageState extends State { ), children: [ _UserPreferenceLimitsForm( - userRole: UserRoles.guestUser, - appConfig: appConfig, + userRole: AppUserRole.guestUser.name, + remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( AppConfigurationFieldChanged( - appConfig: newConfig, + remoteConfig: newConfig, ), ); }, @@ -303,12 +295,12 @@ class _AppConfigurationPageState extends State { ), children: [ _UserPreferenceLimitsForm( - userRole: UserRoles.standardUser, - appConfig: appConfig, + userRole: AppUserRole.standardUser.name, + remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( AppConfigurationFieldChanged( - appConfig: newConfig, + remoteConfig: newConfig, ), ); }, @@ -323,12 +315,12 @@ class _AppConfigurationPageState extends State { ), children: [ _UserPreferenceLimitsForm( - userRole: UserRoles.premiumUser, - appConfig: appConfig, + userRole: AppUserRole.premiumUser.name, + remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( AppConfigurationFieldChanged( - appConfig: newConfig, + remoteConfig: newConfig, ), ); }, @@ -340,7 +332,7 @@ class _AppConfigurationPageState extends State { ); } - Widget _buildAdConfigSection(BuildContext context, AppConfig appConfig) { + Widget _buildAdConfigSection(BuildContext context, RemoteConfig remoteConfig) { final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -359,12 +351,12 @@ class _AppConfigurationPageState extends State { ), children: [ _AdConfigForm( - userRole: UserRoles.guestUser, - appConfig: appConfig, + userRole: AppUserRole.guestUser.name, + remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( AppConfigurationFieldChanged( - appConfig: newConfig, + remoteConfig: newConfig, ), ); }, @@ -379,12 +371,12 @@ class _AppConfigurationPageState extends State { ), children: [ _AdConfigForm( - userRole: UserRoles.standardUser, - appConfig: appConfig, + userRole: AppUserRole.standardUser.name, + remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( AppConfigurationFieldChanged( - appConfig: newConfig, + remoteConfig: newConfig, ), ); }, @@ -399,12 +391,12 @@ class _AppConfigurationPageState extends State { ), children: [ _AdConfigForm( - userRole: UserRoles.premiumUser, - appConfig: appConfig, + userRole: AppUserRole.premiumUser.name, + remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( AppConfigurationFieldChanged( - appConfig: newConfig, + remoteConfig: newConfig, ), ); }, @@ -418,7 +410,7 @@ class _AppConfigurationPageState extends State { Widget _buildAccountActionConfigSection( BuildContext context, - AppConfig appConfig, + RemoteConfig remoteConfig, ) { final l10n = context.l10n; return Column( @@ -438,12 +430,12 @@ class _AppConfigurationPageState extends State { ), children: [ _AccountActionConfigForm( - userRole: UserRoles.guestUser, - appConfig: appConfig, + userRole: AppUserRole.guestUser.name, + remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( AppConfigurationFieldChanged( - appConfig: newConfig, + remoteConfig: newConfig, ), ); }, @@ -458,12 +450,12 @@ class _AppConfigurationPageState extends State { ), children: [ _AccountActionConfigForm( - userRole: UserRoles.standardUser, - appConfig: appConfig, + userRole: AppUserRole.standardUser.name, + remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( AppConfigurationFieldChanged( - appConfig: newConfig, + remoteConfig: newConfig, ), ); }, @@ -475,7 +467,7 @@ class _AppConfigurationPageState extends State { ); } - Widget _buildKillSwitchSection(BuildContext context, AppConfig appConfig) { + Widget _buildAppStatusSection(BuildContext context, RemoteConfig remoteConfig) { final l10n = context.l10n; return SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.lg), @@ -490,79 +482,37 @@ class _AppConfigurationPageState extends State { ), ), const SizedBox(height: AppSpacing.lg), - _buildDropdownField( + _buildDropdownField( context, label: l10n.appOperationalStatusLabel, description: l10n.appOperationalStatusDescription, - value: appConfig.appOperationalStatus, - items: RemoteAppStatus.values, - itemLabelBuilder: (status) => status.name, + value: remoteConfig.appStatus, + items: const [], // AppStatus is a model, not an enum + itemLabelBuilder: (status) => status.isUnderMaintenance + ? l10n.appStatusMaintenance + : l10n.appStatusOperational, onChanged: (value) { if (value != null) { context.read().add( AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(appOperationalStatus: value), + remoteConfig: remoteConfig.copyWith(appStatus: value), ), ); } }, ), - if (appConfig.appOperationalStatus == RemoteAppStatus.maintenance) - _buildTextField( - context, - label: l10n.maintenanceMessageLabel, - description: l10n.maintenanceMessageDescription, - value: appConfig.maintenanceMessage, - onChanged: (value) { - context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(maintenanceMessage: value), - ), - ); - }, - ), - if (appConfig.appOperationalStatus == RemoteAppStatus.disabled) - _buildTextField( - context, - label: l10n.disabledMessageLabel, - description: l10n.disabledMessageDescription, - value: appConfig.disabledMessage, - onChanged: (value) { - context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(disabledMessage: value), - ), - ); - }, - ), - ], - ), - ); - } - - Widget _buildForceUpdateSection(BuildContext context, AppConfig appConfig) { - final l10n = context.l10n; - return SingleChildScrollView( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.forceUpdateDescription, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), - ), - const SizedBox(height: AppSpacing.lg), - _buildTextField( - context, - label: l10n.minAllowedAppVersionLabel, - description: l10n.minAllowedAppVersionDescription, - value: appConfig.minAllowedAppVersion, + SwitchListTile( + title: Text(l10n.isUnderMaintenanceLabel), + subtitle: Text(l10n.isUnderMaintenanceDescription), + value: remoteConfig.appStatus.isUnderMaintenance, onChanged: (value) { context.read().add( AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(minAllowedAppVersion: value), + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + isUnderMaintenance: value, + ), + ), ), ); }, @@ -571,63 +521,65 @@ class _AppConfigurationPageState extends State { context, label: l10n.latestAppVersionLabel, description: l10n.latestAppVersionDescription, - value: appConfig.latestAppVersion, - onChanged: (value) { - context.read().add( - AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(latestAppVersion: value), - ), - ); - }, - ), - _buildTextField( - context, - label: l10n.updateRequiredMessageLabel, - description: l10n.updateRequiredMessageDescription, - value: appConfig.updateRequiredMessage, + value: remoteConfig.appStatus.latestAppVersion, onChanged: (value) { context.read().add( AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(updateRequiredMessage: value), + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + latestAppVersion: value, + ), + ), ), ); }, ), - _buildTextField( - context, - label: l10n.updateOptionalMessageLabel, - description: l10n.updateOptionalMessageDescription, - value: appConfig.updateOptionalMessage, + SwitchListTile( + title: Text(l10n.isLatestVersionOnlyLabel), + subtitle: Text(l10n.isLatestVersionOnlyDescription), + value: remoteConfig.appStatus.isLatestVersionOnly, onChanged: (value) { context.read().add( AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(updateOptionalMessage: value), + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + isLatestVersionOnly: value, + ), + ), ), ); }, ), _buildTextField( context, - label: l10n.iosStoreUrlLabel, - description: l10n.iosStoreUrlDescription, - value: appConfig.iosStoreUrl, + label: l10n.iosUpdateUrlLabel, + description: l10n.iosUpdateUrlDescription, + value: remoteConfig.appStatus.iosUpdateUrl, onChanged: (value) { context.read().add( AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(iosStoreUrl: value), + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + iosUpdateUrl: value, + ), + ), ), ); }, ), _buildTextField( context, - label: l10n.androidStoreUrlLabel, - description: l10n.androidStoreUrlDescription, - value: appConfig.androidStoreUrl, + label: l10n.androidUpdateUrlLabel, + description: l10n.androidUpdateUrlDescription, + value: remoteConfig.appStatus.androidUpdateUrl, onChanged: (value) { context.read().add( AppConfigurationFieldChanged( - appConfig: appConfig.copyWith(androidStoreUrl: value), + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + androidUpdateUrl: value, + ), + ), ), ); }, @@ -643,7 +595,7 @@ class _AppConfigurationPageState extends State { required String description, required int value, required ValueChanged onChanged, - TextEditingController? controller, // Add controller parameter + TextEditingController? controller, }) { return Padding( padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), @@ -663,10 +615,10 @@ class _AppConfigurationPageState extends State { ), const SizedBox(height: AppSpacing.xs), TextFormField( - controller: controller, // Use controller + controller: controller, initialValue: controller == null ? value.toString() - : null, // Only use initialValue if no controller + : null, keyboardType: TextInputType.number, decoration: const InputDecoration( border: OutlineInputBorder(), @@ -690,7 +642,7 @@ class _AppConfigurationPageState extends State { required String description, required String? value, required ValueChanged onChanged, - TextEditingController? controller, // Add controller parameter + TextEditingController? controller, }) { return Padding( padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), @@ -710,10 +662,10 @@ class _AppConfigurationPageState extends State { ), const SizedBox(height: AppSpacing.xs), TextFormField( - controller: controller, // Use controller + controller: controller, initialValue: controller == null ? value - : null, // Only use initialValue if no controller + : null, decoration: const InputDecoration( border: OutlineInputBorder(), isDense: true, @@ -774,14 +726,14 @@ class _AppConfigurationPageState extends State { class _UserPreferenceLimitsForm extends StatefulWidget { const _UserPreferenceLimitsForm({ required this.userRole, - required this.appConfig, + required this.remoteConfig, required this.onConfigChanged, required this.buildIntField, }); final String userRole; - final AppConfig appConfig; - final ValueChanged onConfigChanged; + final RemoteConfig remoteConfig; + final ValueChanged onConfigChanged; final Widget Function( BuildContext context, { required String label, @@ -809,33 +761,33 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { void initState() { super.initState(); _guestFollowedItemsLimitController = TextEditingController( - text: widget.appConfig.userPreferenceLimits.guestFollowedItemsLimit + text: widget.remoteConfig.userPreferenceConfig.guestFollowedItemsLimit .toString(), ); _guestSavedHeadlinesLimitController = TextEditingController( - text: widget.appConfig.userPreferenceLimits.guestSavedHeadlinesLimit + text: widget.remoteConfig.userPreferenceConfig.guestSavedHeadlinesLimit .toString(), ); _authenticatedFollowedItemsLimitController = TextEditingController( text: widget - .appConfig - .userPreferenceLimits + .remoteConfig + .userPreferenceConfig .authenticatedFollowedItemsLimit .toString(), ); _authenticatedSavedHeadlinesLimitController = TextEditingController( text: widget - .appConfig - .userPreferenceLimits + .remoteConfig + .userPreferenceConfig .authenticatedSavedHeadlinesLimit .toString(), ); _premiumFollowedItemsLimitController = TextEditingController( - text: widget.appConfig.userPreferenceLimits.premiumFollowedItemsLimit + text: widget.remoteConfig.userPreferenceConfig.premiumFollowedItemsLimit .toString(), ); _premiumSavedHeadlinesLimitController = TextEditingController( - text: widget.appConfig.userPreferenceLimits.premiumSavedHeadlinesLimit + text: widget.remoteConfig.userPreferenceConfig.premiumSavedHeadlinesLimit .toString(), ); } @@ -843,36 +795,36 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { @override void didUpdateWidget(covariant _UserPreferenceLimitsForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.appConfig.userPreferenceLimits != - oldWidget.appConfig.userPreferenceLimits) { + if (widget.remoteConfig.userPreferenceConfig != + oldWidget.remoteConfig.userPreferenceConfig) { _guestFollowedItemsLimitController.value = TextEditingValue( - text: widget.appConfig.userPreferenceLimits.guestFollowedItemsLimit + text: widget.remoteConfig.userPreferenceConfig.guestFollowedItemsLimit .toString(), selection: TextSelection.collapsed( - offset: widget.appConfig.userPreferenceLimits.guestFollowedItemsLimit + offset: widget.remoteConfig.userPreferenceConfig.guestFollowedItemsLimit .toString() .length, ), ); _guestSavedHeadlinesLimitController.value = TextEditingValue( - text: widget.appConfig.userPreferenceLimits.guestSavedHeadlinesLimit + text: widget.remoteConfig.userPreferenceConfig.guestSavedHeadlinesLimit .toString(), selection: TextSelection.collapsed( - offset: widget.appConfig.userPreferenceLimits.guestSavedHeadlinesLimit + offset: widget.remoteConfig.userPreferenceConfig.guestSavedHeadlinesLimit .toString() .length, ), ); _authenticatedFollowedItemsLimitController.value = TextEditingValue( text: widget - .appConfig - .userPreferenceLimits + .remoteConfig + .userPreferenceConfig .authenticatedFollowedItemsLimit .toString(), selection: TextSelection.collapsed( offset: widget - .appConfig - .userPreferenceLimits + .remoteConfig + .userPreferenceConfig .authenticatedFollowedItemsLimit .toString() .length, @@ -880,38 +832,38 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { ); _authenticatedSavedHeadlinesLimitController.value = TextEditingValue( text: widget - .appConfig - .userPreferenceLimits + .remoteConfig + .userPreferenceConfig .authenticatedSavedHeadlinesLimit .toString(), selection: TextSelection.collapsed( offset: widget - .appConfig - .userPreferenceLimits + .remoteConfig + .userPreferenceConfig .authenticatedSavedHeadlinesLimit .toString() .length, ), ); _premiumFollowedItemsLimitController.value = TextEditingValue( - text: widget.appConfig.userPreferenceLimits.premiumFollowedItemsLimit + text: widget.remoteConfig.userPreferenceConfig.premiumFollowedItemsLimit .toString(), selection: TextSelection.collapsed( offset: widget - .appConfig - .userPreferenceLimits + .remoteConfig + .userPreferenceConfig .premiumFollowedItemsLimit .toString() .length, ), ); _premiumSavedHeadlinesLimitController.value = TextEditingValue( - text: widget.appConfig.userPreferenceLimits.premiumSavedHeadlinesLimit + text: widget.remoteConfig.userPreferenceConfig.premiumSavedHeadlinesLimit .toString(), selection: TextSelection.collapsed( offset: widget - .appConfig - .userPreferenceLimits + .remoteConfig + .userPreferenceConfig .premiumSavedHeadlinesLimit .toString() .length, @@ -933,10 +885,10 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { @override Widget build(BuildContext context) { - final userPreferenceLimits = widget.appConfig.userPreferenceLimits; + final userPreferenceConfig = widget.remoteConfig.userPreferenceConfig; - switch (widget.userRole) { - case UserRoles.guestUser: + switch (AppUserRole.values.byName(widget.userRole)) { + case AppUserRole.guestUser: return Column( children: [ widget.buildIntField( @@ -945,11 +897,11 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { description: 'Maximum number of countries, news sources, or categories a ' 'Guest user can follow (each type has its own limit).', - value: userPreferenceLimits.guestFollowedItemsLimit, + value: userPreferenceConfig.guestFollowedItemsLimit, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( + widget.remoteConfig.copyWith( + userPreferenceConfig: userPreferenceConfig.copyWith( guestFollowedItemsLimit: value, ), ), @@ -961,11 +913,11 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { context, label: 'Guest Saved Headlines Limit', description: 'Maximum number of headlines a Guest user can save.', - value: userPreferenceLimits.guestSavedHeadlinesLimit, + value: userPreferenceConfig.guestSavedHeadlinesLimit, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( + widget.remoteConfig.copyWith( + userPreferenceConfig: userPreferenceConfig.copyWith( guestSavedHeadlinesLimit: value, ), ), @@ -975,7 +927,7 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { ), ], ); - case UserRoles.standardUser: + case AppUserRole.standardUser: return Column( children: [ widget.buildIntField( @@ -984,11 +936,11 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { description: 'Maximum number of countries, news sources, or categories a ' 'Standard user can follow (each type has its own limit).', - value: userPreferenceLimits.authenticatedFollowedItemsLimit, + value: userPreferenceConfig.authenticatedFollowedItemsLimit, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( + widget.remoteConfig.copyWith( + userPreferenceConfig: userPreferenceConfig.copyWith( authenticatedFollowedItemsLimit: value, ), ), @@ -1001,11 +953,11 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { label: 'Standard User Saved Headlines Limit', description: 'Maximum number of headlines a Standard user can save.', - value: userPreferenceLimits.authenticatedSavedHeadlinesLimit, + value: userPreferenceConfig.authenticatedSavedHeadlinesLimit, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( + widget.remoteConfig.copyWith( + userPreferenceConfig: userPreferenceConfig.copyWith( authenticatedSavedHeadlinesLimit: value, ), ), @@ -1015,7 +967,7 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { ), ], ); - case UserRoles.premiumUser: + case AppUserRole.premiumUser: return Column( children: [ widget.buildIntField( @@ -1024,11 +976,11 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { description: 'Maximum number of countries, news sources, or categories a ' 'Premium user can follow (each type has its own limit).', - value: userPreferenceLimits.premiumFollowedItemsLimit, + value: userPreferenceConfig.premiumFollowedItemsLimit, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( + widget.remoteConfig.copyWith( + userPreferenceConfig: userPreferenceConfig.copyWith( premiumFollowedItemsLimit: value, ), ), @@ -1041,11 +993,11 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { label: 'Premium Saved Headlines Limit', description: 'Maximum number of headlines a Premium user can save.', - value: userPreferenceLimits.premiumSavedHeadlinesLimit, + value: userPreferenceConfig.premiumSavedHeadlinesLimit, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( - userPreferenceLimits: userPreferenceLimits.copyWith( + widget.remoteConfig.copyWith( + userPreferenceConfig: userPreferenceConfig.copyWith( premiumSavedHeadlinesLimit: value, ), ), @@ -1055,11 +1007,9 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { ), ], ); - case UserRoles.admin: - // Admin role might not have specific limits here, or could be - // a separate configuration. For now, return empty. - return const SizedBox.shrink(); - default: + case AppUserRole.none: + case AppUserRole.admin: // Assuming admin doesn't have specific limits here + case AppUserRole.publisher: // Assuming publisher doesn't have specific limits here return const SizedBox.shrink(); } } @@ -1068,14 +1018,14 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { class _AdConfigForm extends StatefulWidget { const _AdConfigForm({ required this.userRole, - required this.appConfig, + required this.remoteConfig, required this.onConfigChanged, required this.buildIntField, }); final String userRole; - final AppConfig appConfig; - final ValueChanged onConfigChanged; + final RemoteConfig remoteConfig; + final ValueChanged onConfigChanged; final Widget Function( BuildContext context, { required String label, @@ -1108,44 +1058,44 @@ class _AdConfigFormState extends State<_AdConfigForm> { void initState() { super.initState(); _guestAdFrequencyController = TextEditingController( - text: widget.appConfig.adConfig.guestAdFrequency.toString(), + text: widget.remoteConfig.adConfig.guestAdFrequency.toString(), ); _guestAdPlacementIntervalController = TextEditingController( - text: widget.appConfig.adConfig.guestAdPlacementInterval.toString(), + text: widget.remoteConfig.adConfig.guestAdPlacementInterval.toString(), ); _guestArticlesToReadBeforeShowingInterstitialAdsController = TextEditingController( text: widget - .appConfig + .remoteConfig .adConfig .guestArticlesToReadBeforeShowingInterstitialAds .toString(), ); _authenticatedAdFrequencyController = TextEditingController( - text: widget.appConfig.adConfig.authenticatedAdFrequency.toString(), + text: widget.remoteConfig.adConfig.authenticatedAdFrequency.toString(), ); _authenticatedAdPlacementIntervalController = TextEditingController( - text: widget.appConfig.adConfig.authenticatedAdPlacementInterval + text: widget.remoteConfig.adConfig.authenticatedAdPlacementInterval .toString(), ); _standardUserArticlesToReadBeforeShowingInterstitialAdsController = TextEditingController( text: widget - .appConfig + .remoteConfig .adConfig .standardUserArticlesToReadBeforeShowingInterstitialAds .toString(), ); _premiumAdFrequencyController = TextEditingController( - text: widget.appConfig.adConfig.premiumAdFrequency.toString(), + text: widget.remoteConfig.adConfig.premiumAdFrequency.toString(), ); _premiumAdPlacementIntervalController = TextEditingController( - text: widget.appConfig.adConfig.premiumAdPlacementInterval.toString(), + text: widget.remoteConfig.adConfig.premiumAdPlacementInterval.toString(), ); _premiumUserArticlesToReadBeforeShowingInterstitialAdsController = TextEditingController( text: widget - .appConfig + .remoteConfig .adConfig .premiumUserArticlesToReadBeforeShowingInterstitialAds .toString(), @@ -1155,17 +1105,17 @@ class _AdConfigFormState extends State<_AdConfigForm> { @override void didUpdateWidget(covariant _AdConfigForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.appConfig.adConfig != oldWidget.appConfig.adConfig) { + if (widget.remoteConfig.adConfig != oldWidget.remoteConfig.adConfig) { _guestAdFrequencyController.value = TextEditingValue( - text: widget.appConfig.adConfig.guestAdFrequency.toString(), + text: widget.remoteConfig.adConfig.guestAdFrequency.toString(), selection: TextSelection.collapsed( - offset: widget.appConfig.adConfig.guestAdFrequency.toString().length, + offset: widget.remoteConfig.adConfig.guestAdFrequency.toString().length, ), ); _guestAdPlacementIntervalController.value = TextEditingValue( - text: widget.appConfig.adConfig.guestAdPlacementInterval.toString(), + text: widget.remoteConfig.adConfig.guestAdPlacementInterval.toString(), selection: TextSelection.collapsed( - offset: widget.appConfig.adConfig.guestAdPlacementInterval + offset: widget.remoteConfig.adConfig.guestAdPlacementInterval .toString() .length, ), @@ -1173,13 +1123,13 @@ class _AdConfigFormState extends State<_AdConfigForm> { _guestArticlesToReadBeforeShowingInterstitialAdsController.value = TextEditingValue( text: widget - .appConfig + .remoteConfig .adConfig .guestArticlesToReadBeforeShowingInterstitialAds .toString(), selection: TextSelection.collapsed( offset: widget - .appConfig + .remoteConfig .adConfig .guestArticlesToReadBeforeShowingInterstitialAds .toString() @@ -1187,18 +1137,18 @@ class _AdConfigFormState extends State<_AdConfigForm> { ), ); _authenticatedAdFrequencyController.value = TextEditingValue( - text: widget.appConfig.adConfig.authenticatedAdFrequency.toString(), + text: widget.remoteConfig.adConfig.authenticatedAdFrequency.toString(), selection: TextSelection.collapsed( - offset: widget.appConfig.adConfig.authenticatedAdFrequency + offset: widget.remoteConfig.adConfig.authenticatedAdFrequency .toString() .length, ), ); _authenticatedAdPlacementIntervalController.value = TextEditingValue( - text: widget.appConfig.adConfig.authenticatedAdPlacementInterval + text: widget.remoteConfig.adConfig.authenticatedAdPlacementInterval .toString(), selection: TextSelection.collapsed( - offset: widget.appConfig.adConfig.authenticatedAdPlacementInterval + offset: widget.remoteConfig.adConfig.authenticatedAdPlacementInterval .toString() .length, ), @@ -1206,13 +1156,13 @@ class _AdConfigFormState extends State<_AdConfigForm> { _standardUserArticlesToReadBeforeShowingInterstitialAdsController.value = TextEditingValue( text: widget - .appConfig + .remoteConfig .adConfig .standardUserArticlesToReadBeforeShowingInterstitialAds .toString(), selection: TextSelection.collapsed( offset: widget - .appConfig + .remoteConfig .adConfig .standardUserArticlesToReadBeforeShowingInterstitialAds .toString() @@ -1220,17 +1170,17 @@ class _AdConfigFormState extends State<_AdConfigForm> { ), ); _premiumAdFrequencyController.value = TextEditingValue( - text: widget.appConfig.adConfig.premiumAdFrequency.toString(), + text: widget.remoteConfig.adConfig.premiumAdFrequency.toString(), selection: TextSelection.collapsed( - offset: widget.appConfig.adConfig.premiumAdFrequency + offset: widget.remoteConfig.adConfig.premiumAdFrequency .toString() .length, ), ); _premiumAdPlacementIntervalController.value = TextEditingValue( - text: widget.appConfig.adConfig.premiumAdPlacementInterval.toString(), + text: widget.remoteConfig.adConfig.premiumAdPlacementInterval.toString(), selection: TextSelection.collapsed( - offset: widget.appConfig.adConfig.premiumAdPlacementInterval + offset: widget.remoteConfig.adConfig.premiumAdPlacementInterval .toString() .length, ), @@ -1238,13 +1188,13 @@ class _AdConfigFormState extends State<_AdConfigForm> { _premiumUserArticlesToReadBeforeShowingInterstitialAdsController.value = TextEditingValue( text: widget - .appConfig + .remoteConfig .adConfig .premiumUserArticlesToReadBeforeShowingInterstitialAds .toString(), selection: TextSelection.collapsed( offset: widget - .appConfig + .remoteConfig .adConfig .premiumUserArticlesToReadBeforeShowingInterstitialAds .toString() @@ -1270,10 +1220,10 @@ class _AdConfigFormState extends State<_AdConfigForm> { @override Widget build(BuildContext context) { - final adConfig = widget.appConfig.adConfig; + final adConfig = widget.remoteConfig.adConfig; - switch (widget.userRole) { - case UserRoles.guestUser: + switch (AppUserRole.values.byName(widget.userRole)) { + case AppUserRole.guestUser: return Column( children: [ widget.buildIntField( @@ -1285,7 +1235,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { value: adConfig.guestAdFrequency, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( + widget.remoteConfig.copyWith( adConfig: adConfig.copyWith(guestAdFrequency: value), ), ); @@ -1301,7 +1251,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { value: adConfig.guestAdPlacementInterval, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( + widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( guestAdPlacementInterval: value, ), @@ -1319,7 +1269,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { value: adConfig.guestArticlesToReadBeforeShowingInterstitialAds, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( + widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( guestArticlesToReadBeforeShowingInterstitialAds: value, ), @@ -1331,7 +1281,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { ), ], ); - case UserRoles.standardUser: + case AppUserRole.standardUser: return Column( children: [ widget.buildIntField( @@ -1343,7 +1293,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { value: adConfig.authenticatedAdFrequency, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( + widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( authenticatedAdFrequency: value, ), @@ -1361,7 +1311,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { value: adConfig.authenticatedAdPlacementInterval, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( + widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( authenticatedAdPlacementInterval: value, ), @@ -1380,7 +1330,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { .standardUserArticlesToReadBeforeShowingInterstitialAds, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( + widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( standardUserArticlesToReadBeforeShowingInterstitialAds: value, @@ -1393,7 +1343,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { ), ], ); - case UserRoles.premiumUser: + case AppUserRole.premiumUser: return Column( children: [ widget.buildIntField( @@ -1404,7 +1354,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { value: adConfig.premiumAdFrequency, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( + widget.remoteConfig.copyWith( adConfig: adConfig.copyWith(premiumAdFrequency: value), ), ); @@ -1420,7 +1370,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { value: adConfig.premiumAdPlacementInterval, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( + widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( premiumAdPlacementInterval: value, ), @@ -1439,7 +1389,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { .premiumUserArticlesToReadBeforeShowingInterstitialAds, onChanged: (value) { widget.onConfigChanged( - widget.appConfig.copyWith( + widget.remoteConfig.copyWith( adConfig: adConfig.copyWith( premiumUserArticlesToReadBeforeShowingInterstitialAds: value, @@ -1452,9 +1402,9 @@ class _AdConfigFormState extends State<_AdConfigForm> { ), ], ); - case UserRoles.admin: - return const SizedBox.shrink(); - default: + case AppUserRole.none: + case AppUserRole.admin: + case AppUserRole.publisher: return const SizedBox.shrink(); } } @@ -1463,14 +1413,14 @@ class _AdConfigFormState extends State<_AdConfigForm> { class _AccountActionConfigForm extends StatefulWidget { const _AccountActionConfigForm({ required this.userRole, - required this.appConfig, + required this.remoteConfig, required this.onConfigChanged, required this.buildIntField, }); final String userRole; - final AppConfig appConfig; - final ValueChanged onConfigChanged; + final RemoteConfig remoteConfig; + final ValueChanged onConfigChanged; final Widget Function( BuildContext context, { required String label, @@ -1495,14 +1445,14 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { void initState() { super.initState(); _guestDaysBetweenAccountActionsController = TextEditingController( - text: widget.appConfig.accountActionConfig.guestDaysBetweenAccountActions + text: widget.remoteConfig.accountActionConfig.guestDaysBetweenActions .toString(), ); _standardUserDaysBetweenAccountActionsController = TextEditingController( text: widget - .appConfig + .remoteConfig .accountActionConfig - .standardUserDaysBetweenAccountActions + .standardUserDaysBetweenActions .toString(), ); } @@ -1510,34 +1460,34 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { @override void didUpdateWidget(covariant _AccountActionConfigForm oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.appConfig.accountActionConfig != - oldWidget.appConfig.accountActionConfig) { + if (widget.remoteConfig.accountActionConfig != + oldWidget.remoteConfig.accountActionConfig) { _guestDaysBetweenAccountActionsController.value = TextEditingValue( text: widget - .appConfig + .remoteConfig .accountActionConfig - .guestDaysBetweenAccountActions + .guestDaysBetweenActions .toString(), selection: TextSelection.collapsed( offset: widget - .appConfig + .remoteConfig .accountActionConfig - .guestDaysBetweenAccountActions + .guestDaysBetweenActions .toString() .length, ), ); _standardUserDaysBetweenAccountActionsController.value = TextEditingValue( text: widget - .appConfig + .remoteConfig .accountActionConfig - .standardUserDaysBetweenAccountActions + .standardUserDaysBetweenActions .toString(), selection: TextSelection.collapsed( offset: widget - .appConfig + .remoteConfig .accountActionConfig - .standardUserDaysBetweenAccountActions + .standardUserDaysBetweenActions .toString() .length, ), @@ -1554,10 +1504,10 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { @override Widget build(BuildContext context) { - final accountActionConfig = widget.appConfig.accountActionConfig; + final accountActionConfig = widget.remoteConfig.accountActionConfig; - switch (widget.userRole) { - case UserRoles.guestUser: + switch (AppUserRole.values.byName(widget.userRole)) { + case AppUserRole.guestUser: return Column( children: [ widget.buildIntField( @@ -1566,12 +1516,16 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { description: 'Minimum number of days that must pass before a Guest user ' 'sees another in-app prompt.', - value: accountActionConfig.guestDaysBetweenAccountActions, + value: accountActionConfig.guestDaysBetweenActions[FeedActionType.linkAccount] ?? 0, onChanged: (value) { + final updatedMap = Map.from( + accountActionConfig.guestDaysBetweenActions, + ); + updatedMap[FeedActionType.linkAccount] = value; widget.onConfigChanged( - widget.appConfig.copyWith( + widget.remoteConfig.copyWith( accountActionConfig: accountActionConfig.copyWith( - guestDaysBetweenAccountActions: value, + guestDaysBetweenActions: updatedMap, ), ), ); @@ -1580,7 +1534,7 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { ), ], ); - case UserRoles.standardUser: + case AppUserRole.standardUser: return Column( children: [ widget.buildIntField( @@ -1589,12 +1543,16 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { description: 'Minimum number of days that must pass before a Standard user ' 'sees another in-app prompt.', - value: accountActionConfig.standardUserDaysBetweenAccountActions, + value: accountActionConfig.standardUserDaysBetweenActions[FeedActionType.linkAccount] ?? 0, onChanged: (value) { + final updatedMap = Map.from( + accountActionConfig.standardUserDaysBetweenActions, + ); + updatedMap[FeedActionType.linkAccount] = value; widget.onConfigChanged( - widget.appConfig.copyWith( + widget.remoteConfig.copyWith( accountActionConfig: accountActionConfig.copyWith( - standardUserDaysBetweenAccountActions: value, + standardUserDaysBetweenActions: updatedMap, ), ), ); @@ -1603,10 +1561,9 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { ), ], ); - case UserRoles.premiumUser: - case UserRoles.admin: - return const SizedBox.shrink(); - default: + case AppUserRole.premiumUser: + case AppUserRole.admin: + case AppUserRole.publisher: return const SizedBox.shrink(); } } From 9d4f14bfd914a2d9e1abe8c60e624af8c9842689 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:19:25 +0100 Subject: [PATCH 52/69] feat(l10n): add app status and update URLs - Added translations for app statuses: Maintenance, Operational. - Added translations for update URLs: iOS and Android. - Improved app status descriptions. - Added labels and descriptions for update URL fields. - Removed obsolete `helloWorld` translation. --- .../view/app_configuration_page.dart | 18 ++--- lib/l10n/app_localizations.dart | 72 +++++++++++++++---- lib/l10n/app_localizations_ar.dart | 38 ++++++++-- lib/l10n/app_localizations_en.dart | 38 ++++++++-- lib/l10n/arb/app_ar.arb | 48 ++++++++++--- lib/l10n/arb/app_en.arb | 50 ++++++++++--- 6 files changed, 214 insertions(+), 50 deletions(-) diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index be8b7bee..0260d670 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -5,7 +5,6 @@ import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; import 'package:ht_dashboard/shared/widgets/widgets.dart'; import 'package:ht_shared/ht_shared.dart'; -import 'package:ht_shared/src/enums/app_user_role.dart'; /// {@template app_configuration_page} /// A page for managing the application's remote configuration. @@ -887,8 +886,8 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { Widget build(BuildContext context) { final userPreferenceConfig = widget.remoteConfig.userPreferenceConfig; - switch (AppUserRole.values.byName(widget.userRole)) { - case AppUserRole.guestUser: + switch (widget.userRole) { + case 'guestUser': return Column( children: [ widget.buildIntField( @@ -927,7 +926,7 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { ), ], ); - case AppUserRole.standardUser: + case 'standardUser': return Column( children: [ widget.buildIntField( @@ -967,7 +966,7 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { ), ], ); - case AppUserRole.premiumUser: + case 'premiumUser': return Column( children: [ widget.buildIntField( @@ -1007,11 +1006,12 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { ), ], ); - case AppUserRole.none: - case AppUserRole.admin: // Assuming admin doesn't have specific limits here - case AppUserRole.publisher: // Assuming publisher doesn't have specific limits here + case 'none': + case 'admin': // Assuming admin doesn't have specific limits here + case 'publisher': // Assuming publisher doesn't have specific limits here + return const SizedBox.shrink(); + default: return const SizedBox.shrink(); - } } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 81cc77a5..75ab7f39 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -98,12 +98,6 @@ abstract class AppLocalizations { Locale('en'), ]; - /// The conventional newborn programmer greeting - /// - /// In en, this message translates to: - /// **'Hello World!'** - String get helloWorld; - /// Headline for the main authentication page /// /// In en, this message translates to: @@ -1430,12 +1424,6 @@ abstract class AppLocalizations { /// **'Active'** String get appStatusActive; - /// Text for the 'Maintenance' app status - /// - /// In en, this message translates to: - /// **'Maintenance'** - String get appStatusMaintenance; - /// Text for the 'Disabled' app status /// /// In en, this message translates to: @@ -1453,6 +1441,66 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'For demo, use code: {code}'** String demoCodeHint(String code); + + /// Text for the 'Maintenance' app status + /// + /// In en, this message translates to: + /// **'Maintenance'** + String get appStatusMaintenance; + + /// Text for the 'Operational' app status + /// + /// In en, this message translates to: + /// **'Operational'** + String get appStatusOperational; + + /// Label for the 'is under maintenance' switch + /// + /// In en, this message translates to: + /// **'Under Maintenance'** + String get isUnderMaintenanceLabel; + + /// Description for the 'is under maintenance' switch + /// + /// In en, this message translates to: + /// **'Toggle to put the app in maintenance mode, preventing user access.'** + String get isUnderMaintenanceDescription; + + /// Label for the 'is latest version only' switch + /// + /// In en, this message translates to: + /// **'Force Latest Version Only'** + String get isLatestVersionOnlyLabel; + + /// Description for the 'is latest version only' switch + /// + /// In en, this message translates to: + /// **'If enabled, users must update to the latest app version to continue using the app.'** + String get isLatestVersionOnlyDescription; + + /// Label for iOS Update URL + /// + /// In en, this message translates to: + /// **'iOS Update URL'** + String get iosUpdateUrlLabel; + + /// Description for iOS Update URL + /// + /// In en, this message translates to: + /// **'URL for iOS app updates.'** + String get iosUpdateUrlDescription; + + /// Label for Android Update URL + /// + /// In en, this message translates to: + /// **'Android Update URL'** + String get androidUpdateUrlLabel; + + /// Description for Android Update URL + /// + /// In en, this message translates to: + /// **'URL for Android app updates.'** + String get androidUpdateUrlDescription; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 25a4b6ad..6f823816 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -8,9 +8,6 @@ import 'app_localizations.dart'; class AppLocalizationsAr extends AppLocalizations { AppLocalizationsAr([String locale = 'ar']) : super(locale); - @override - String get helloWorld => 'مرحبا بالعالم!'; - @override String get authenticationPageHeadline => 'الوصول إلى لوحة التحكم'; @@ -745,9 +742,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get appStatusActive => 'نشط'; - @override - String get appStatusMaintenance => 'صيانة'; - @override String get appStatusDisabled => 'معطل'; @@ -760,4 +754,36 @@ class AppLocalizationsAr extends AppLocalizations { String demoCodeHint(String code) { return 'للعرض التجريبي، استخدم الرمز: $code'; } + + @override + String get appStatusMaintenance => 'صيانة'; + + @override + String get appStatusOperational => 'تشغيلي'; + + @override + String get isUnderMaintenanceLabel => 'تحت الصيانة'; + + @override + String get isUnderMaintenanceDescription => + 'تبديل لوضع التطبيق في وضع الصيانة، مما يمنع وصول المستخدمين.'; + + @override + String get isLatestVersionOnlyLabel => 'فرض أحدث إصدار فقط'; + + @override + String get isLatestVersionOnlyDescription => + 'إذا تم التمكين، يجب على المستخدمين التحديث إلى أحدث إصدار من التطبيق لمواصلة استخدامه.'; + + @override + String get iosUpdateUrlLabel => 'رابط تحديث iOS'; + + @override + String get iosUpdateUrlDescription => 'رابط تحديثات تطبيق iOS.'; + + @override + String get androidUpdateUrlLabel => 'رابط تحديث Android'; + + @override + String get androidUpdateUrlDescription => 'رابط تحديثات تطبيق Android.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9fc16645..16f9b59c 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -8,9 +8,6 @@ import 'app_localizations.dart'; class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); - @override - String get helloWorld => 'Hello World!'; - @override String get authenticationPageHeadline => 'Dashboard Access'; @@ -743,9 +740,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appStatusActive => 'Active'; - @override - String get appStatusMaintenance => 'Maintenance'; - @override String get appStatusDisabled => 'Disabled'; @@ -758,4 +752,36 @@ class AppLocalizationsEn extends AppLocalizations { String demoCodeHint(String code) { return 'For demo, use code: $code'; } + + @override + String get appStatusMaintenance => 'Maintenance'; + + @override + String get appStatusOperational => 'Operational'; + + @override + String get isUnderMaintenanceLabel => 'Under Maintenance'; + + @override + String get isUnderMaintenanceDescription => + 'Toggle to put the app in maintenance mode, preventing user access.'; + + @override + String get isLatestVersionOnlyLabel => 'Force Latest Version Only'; + + @override + String get isLatestVersionOnlyDescription => + 'If enabled, users must update to the latest app version to continue using the app.'; + + @override + String get iosUpdateUrlLabel => 'iOS Update URL'; + + @override + String get iosUpdateUrlDescription => 'URL for iOS app updates.'; + + @override + String get androidUpdateUrlLabel => 'Android Update URL'; + + @override + String get androidUpdateUrlDescription => 'URL for Android app updates.'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 6af647cc..027f35a6 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1,8 +1,4 @@ { - "helloWorld": "مرحبا بالعالم!", - "@helloWorld": { - "description": "التحية التقليدية للمبرمج حديث الولادة" - }, "authenticationPageHeadline": "الوصول إلى لوحة التحكم", "@authenticationPageHeadline": { "description": "عنوان صفحة المصادقة الرئيسية" @@ -902,10 +898,6 @@ "@appStatusActive": { "description": "نص حالة التطبيق 'نشط'" }, - "appStatusMaintenance": "صيانة", - "@appStatusMaintenance": { - "description": "نص حالة التطبيق 'صيانة'" - }, "appStatusDisabled": "معطل", "@appStatusDisabled": { "description": "نص حالة التطبيق 'معطل'" @@ -929,5 +921,45 @@ "example": "123456" } } + }, + "appStatusMaintenance": "صيانة", + "@appStatusMaintenance": { + "description": "نص حالة التطبيق 'صيانة'" + }, + "appStatusOperational": "تشغيلي", + "@appStatusOperational": { + "description": "نص حالة التطبيق 'تشغيلي'" + }, + "isUnderMaintenanceLabel": "تحت الصيانة", + "@isUnderMaintenanceLabel": { + "description": "تسمية مفتاح 'تحت الصيانة'" + }, + "isUnderMaintenanceDescription": "تبديل لوضع التطبيق في وضع الصيانة، مما يمنع وصول المستخدمين.", + "@isUnderMaintenanceDescription": { + "description": "وصف مفتاح 'تحت الصيانة'" + }, + "isLatestVersionOnlyLabel": "فرض أحدث إصدار فقط", + "@isLatestVersionOnlyLabel": { + "description": "تسمية مفتاح 'فرض أحدث إصدار فقط'" + }, + "isLatestVersionOnlyDescription": "إذا تم التمكين، يجب على المستخدمين التحديث إلى أحدث إصدار من التطبيق لمواصلة استخدامه.", + "@isLatestVersionOnlyDescription": { + "description": "وصف مفتاح 'فرض أحدث إصدار فقط'" + }, + "iosUpdateUrlLabel": "رابط تحديث iOS", + "@iosUpdateUrlLabel": { + "description": "تسمية رابط تحديث iOS" + }, + "iosUpdateUrlDescription": "رابط تحديثات تطبيق iOS.", + "@iosUpdateUrlDescription": { + "description": "وصف رابط تحديث iOS" + }, + "androidUpdateUrlLabel": "رابط تحديث Android", + "@androidUpdateUrlLabel": { + "description": "تسمية رابط تحديث Android" + }, + "androidUpdateUrlDescription": "رابط تحديثات تطبيق Android.", + "@androidUpdateUrlDescription": { + "description": "وصف رابط تحديث Android" } } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 82482e91..4c913976 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,8 +1,4 @@ { - "helloWorld": "Hello World!", - "@helloWorld": { - "description": "The conventional newborn programmer greeting" - }, "authenticationPageHeadline": "Dashboard Access", "@authenticationPageHeadline": { "description": "Headline for the main authentication page" @@ -902,10 +898,7 @@ "@appStatusActive": { "description": "Text for the 'Active' app status" }, - "appStatusMaintenance": "Maintenance", - "@appStatusMaintenance": { - "description": "Text for the 'Maintenance' app status" - }, + "appStatusDisabled": "Disabled", "@appStatusDisabled": { "description": "Text for the 'Disabled' app status" @@ -929,6 +922,45 @@ "example": "123456" } } + }, + "appStatusMaintenance": "Maintenance", + "@appStatusMaintenance": { + "description": "Text for the 'Maintenance' app status" + }, + "appStatusOperational": "Operational", + "@appStatusOperational": { + "description": "Text for the 'Operational' app status" + }, + "isUnderMaintenanceLabel": "Under Maintenance", + "@isUnderMaintenanceLabel": { + "description": "Label for the 'is under maintenance' switch" + }, + "isUnderMaintenanceDescription": "Toggle to put the app in maintenance mode, preventing user access.", + "@isUnderMaintenanceDescription": { + "description": "Description for the 'is under maintenance' switch" + }, + "isLatestVersionOnlyLabel": "Force Latest Version Only", + "@isLatestVersionOnlyLabel": { + "description": "Label for the 'is latest version only' switch" + }, + "isLatestVersionOnlyDescription": "If enabled, users must update to the latest app version to continue using the app.", + "@isLatestVersionOnlyDescription": { + "description": "Description for the 'is latest version only' switch" + }, + "iosUpdateUrlLabel": "iOS Update URL", + "@iosUpdateUrlLabel": { + "description": "Label for iOS Update URL" + }, + "iosUpdateUrlDescription": "URL for iOS app updates.", + "@iosUpdateUrlDescription": { + "description": "Description for iOS Update URL" + }, + "androidUpdateUrlLabel": "Android Update URL", + "@androidUpdateUrlLabel": { + "description": "Label for Android Update URL" + }, + "androidUpdateUrlDescription": "URL for Android app updates.", + "@androidUpdateUrlDescription": { + "description": "Description for Android Update URL" } - } From 5e45dfb06d3a5ca811befe5725869a1ad6111619 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:21:16 +0100 Subject: [PATCH 53/69] refactor(app_config): Use string instead of enum for user roles - Replaced AppUserRole enum with string comparisons. - Improved code readability and maintainability. - Fixed potential issues with enum usage. - Simplified switch statement logic. - No functional changes. --- .../view/app_configuration_page.dart | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index 0260d670..72a5aba1 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -1012,6 +1012,7 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { return const SizedBox.shrink(); default: return const SizedBox.shrink(); + } } } @@ -1222,8 +1223,8 @@ class _AdConfigFormState extends State<_AdConfigForm> { Widget build(BuildContext context) { final adConfig = widget.remoteConfig.adConfig; - switch (AppUserRole.values.byName(widget.userRole)) { - case AppUserRole.guestUser: + switch (widget.userRole) { + case 'guestUser': return Column( children: [ widget.buildIntField( @@ -1281,7 +1282,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { ), ], ); - case AppUserRole.standardUser: + case 'standardUser': return Column( children: [ widget.buildIntField( @@ -1343,7 +1344,7 @@ class _AdConfigFormState extends State<_AdConfigForm> { ), ], ); - case AppUserRole.premiumUser: + case 'premiumUser': return Column( children: [ widget.buildIntField( @@ -1402,9 +1403,11 @@ class _AdConfigFormState extends State<_AdConfigForm> { ), ], ); - case AppUserRole.none: - case AppUserRole.admin: - case AppUserRole.publisher: + case 'none': + case 'admin': + case 'publisher': + return const SizedBox.shrink(); + default: return const SizedBox.shrink(); } } @@ -1506,8 +1509,8 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { Widget build(BuildContext context) { final accountActionConfig = widget.remoteConfig.accountActionConfig; - switch (AppUserRole.values.byName(widget.userRole)) { - case AppUserRole.guestUser: + switch (widget.userRole) { + case 'guestUser': return Column( children: [ widget.buildIntField( @@ -1534,7 +1537,7 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { ), ], ); - case AppUserRole.standardUser: + case 'standardUser': return Column( children: [ widget.buildIntField( @@ -1561,9 +1564,11 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { ), ], ); - case AppUserRole.premiumUser: - case AppUserRole.admin: - case AppUserRole.publisher: + case 'premiumUser': + case 'admin': + case 'publisher': + return const SizedBox.shrink(); + default: return const SizedBox.shrink(); } } From 3c8e47e90ec9a0cf38e9cd939c2115b43b89a494 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:29:54 +0100 Subject: [PATCH 54/69] refactor(app_config): remove redundant cases - Removed unnecessary 'none', 'admin', and 'publisher' cases. - Simplified conditional rendering logic. - Improved code readability. --- lib/app_configuration/view/app_configuration_page.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index 72a5aba1..9be4cd3a 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -1006,10 +1006,6 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { ), ], ); - case 'none': - case 'admin': // Assuming admin doesn't have specific limits here - case 'publisher': // Assuming publisher doesn't have specific limits here - return const SizedBox.shrink(); default: return const SizedBox.shrink(); } @@ -1403,10 +1399,6 @@ class _AdConfigFormState extends State<_AdConfigForm> { ), ], ); - case 'none': - case 'admin': - case 'publisher': - return const SizedBox.shrink(); default: return const SizedBox.shrink(); } From f39f6aa70a0b17b67a5897b97fe446b2870a2af2 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:32:58 +0100 Subject: [PATCH 55/69] refactor(app_config): improve code style and structure - Improved code readability and maintainability. - Removed unnecessary DropdownButtonFormField. - Simplified UserPreferenceLimitsForm and AdConfigForm. - Updated AppUserRole type to enum. - Consolidated similar code blocks. --- .../view/app_configuration_page.dart | 1365 ++++++----------- 1 file changed, 502 insertions(+), 863 deletions(-) diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index 9be4cd3a..c036b3a1 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -53,8 +53,8 @@ class _AppConfigurationPageState extends State { child: Text( l10n.appConfigurationPageDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), ), @@ -70,16 +70,16 @@ class _AppConfigurationPageState extends State { content: Text( l10n.appConfigSaveSuccessMessage, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onPrimary, - ), + color: Theme.of(context).colorScheme.onPrimary, + ), ), backgroundColor: Theme.of(context).colorScheme.primary, ), ); // Clear the showSaveSuccess flag after showing the snackbar context.read().add( - const AppConfigurationFieldChanged(), - ); + const AppConfigurationFieldChanged(), + ); } else if (state.status == AppConfigurationStatus.failure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() @@ -90,8 +90,8 @@ class _AppConfigurationPageState extends State { state.errorMessage ?? l10n.unknownError, ), style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onError, - ), + color: Theme.of(context).colorScheme.onError, + ), ), backgroundColor: Theme.of(context).colorScheme.error, ), @@ -112,8 +112,8 @@ class _AppConfigurationPageState extends State { state.errorMessage ?? l10n.failedToLoadConfigurationMessage, onRetry: () { context.read().add( - const AppConfigurationLoaded(), - ); + const AppConfigurationLoaded(), + ); }, ); } else if (state.status == AppConfigurationStatus.success && @@ -191,8 +191,8 @@ class _AppConfigurationPageState extends State { ? () { // Discard changes: revert to original config context.read().add( - const AppConfigurationDiscarded(), - ); + const AppConfigurationDiscarded(), + ); } : null, child: Text(context.l10n.discardChangesButton), @@ -204,8 +204,8 @@ class _AppConfigurationPageState extends State { final confirmed = await _showConfirmationDialog(context); if (context.mounted && confirmed && remoteConfig != null) { context.read().add( - AppConfigurationUpdated(remoteConfig), - ); + AppConfigurationUpdated(remoteConfig), + ); } } : null, @@ -263,8 +263,8 @@ class _AppConfigurationPageState extends State { Text( l10n.userContentLimitsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), ExpansionTile( @@ -274,14 +274,14 @@ class _AppConfigurationPageState extends State { ), children: [ _UserPreferenceLimitsForm( - userRole: AppUserRole.guestUser.name, + userRole: AppUserRole.guestUser, remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -294,14 +294,14 @@ class _AppConfigurationPageState extends State { ), children: [ _UserPreferenceLimitsForm( - userRole: AppUserRole.standardUser.name, + userRole: AppUserRole.standardUser, remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -314,14 +314,14 @@ class _AppConfigurationPageState extends State { ), children: [ _UserPreferenceLimitsForm( - userRole: AppUserRole.premiumUser.name, + userRole: AppUserRole.premiumUser, remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -339,8 +339,8 @@ class _AppConfigurationPageState extends State { Text( l10n.adSettingsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), ExpansionTile( @@ -350,14 +350,14 @@ class _AppConfigurationPageState extends State { ), children: [ _AdConfigForm( - userRole: AppUserRole.guestUser.name, + userRole: AppUserRole.guestUser, remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -370,14 +370,14 @@ class _AppConfigurationPageState extends State { ), children: [ _AdConfigForm( - userRole: AppUserRole.standardUser.name, + userRole: AppUserRole.standardUser, remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -390,14 +390,14 @@ class _AppConfigurationPageState extends State { ), children: [ _AdConfigForm( - userRole: AppUserRole.premiumUser.name, + userRole: AppUserRole.premiumUser, remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -418,8 +418,8 @@ class _AppConfigurationPageState extends State { Text( l10n.inAppPromptsDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.lg), ExpansionTile( @@ -429,14 +429,14 @@ class _AppConfigurationPageState extends State { ), children: [ _AccountActionConfigForm( - userRole: AppUserRole.guestUser.name, + userRole: AppUserRole.guestUser, remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -449,14 +449,14 @@ class _AppConfigurationPageState extends State { ), children: [ _AccountActionConfigForm( - userRole: AppUserRole.standardUser.name, + userRole: AppUserRole.standardUser, remoteConfig: remoteConfig, onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: newConfig, - ), - ); + AppConfigurationFieldChanged( + remoteConfig: newConfig, + ), + ); }, buildIntField: _buildIntField, ), @@ -466,7 +466,8 @@ class _AppConfigurationPageState extends State { ); } - Widget _buildAppStatusSection(BuildContext context, RemoteConfig remoteConfig) { + Widget _buildAppStatusSection( + BuildContext context, RemoteConfig remoteConfig) { final l10n = context.l10n; return SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.lg), @@ -476,44 +477,25 @@ class _AppConfigurationPageState extends State { Text( l10n.appOperationalStatusWarning, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.error, - fontWeight: FontWeight.bold, - ), + color: Theme.of(context).colorScheme.error, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: AppSpacing.lg), - _buildDropdownField( - context, - label: l10n.appOperationalStatusLabel, - description: l10n.appOperationalStatusDescription, - value: remoteConfig.appStatus, - items: const [], // AppStatus is a model, not an enum - itemLabelBuilder: (status) => status.isUnderMaintenance - ? l10n.appStatusMaintenance - : l10n.appStatusOperational, - onChanged: (value) { - if (value != null) { - context.read().add( - AppConfigurationFieldChanged( - remoteConfig: remoteConfig.copyWith(appStatus: value), - ), - ); - } - }, - ), SwitchListTile( title: Text(l10n.isUnderMaintenanceLabel), subtitle: Text(l10n.isUnderMaintenanceDescription), value: remoteConfig.appStatus.isUnderMaintenance, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: remoteConfig.copyWith( - appStatus: remoteConfig.appStatus.copyWith( - isUnderMaintenance: value, + AppConfigurationFieldChanged( + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + isUnderMaintenance: value, + ), + ), ), - ), - ), - ); + ); }, ), _buildTextField( @@ -523,14 +505,14 @@ class _AppConfigurationPageState extends State { value: remoteConfig.appStatus.latestAppVersion, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: remoteConfig.copyWith( - appStatus: remoteConfig.appStatus.copyWith( - latestAppVersion: value, + AppConfigurationFieldChanged( + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + latestAppVersion: value, + ), + ), ), - ), - ), - ); + ); }, ), SwitchListTile( @@ -539,14 +521,14 @@ class _AppConfigurationPageState extends State { value: remoteConfig.appStatus.isLatestVersionOnly, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: remoteConfig.copyWith( - appStatus: remoteConfig.appStatus.copyWith( - isLatestVersionOnly: value, + AppConfigurationFieldChanged( + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + isLatestVersionOnly: value, + ), + ), ), - ), - ), - ); + ); }, ), _buildTextField( @@ -556,14 +538,14 @@ class _AppConfigurationPageState extends State { value: remoteConfig.appStatus.iosUpdateUrl, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: remoteConfig.copyWith( - appStatus: remoteConfig.appStatus.copyWith( - iosUpdateUrl: value, + AppConfigurationFieldChanged( + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + iosUpdateUrl: value, + ), + ), ), - ), - ), - ); + ); }, ), _buildTextField( @@ -573,14 +555,14 @@ class _AppConfigurationPageState extends State { value: remoteConfig.appStatus.androidUpdateUrl, onChanged: (value) { context.read().add( - AppConfigurationFieldChanged( - remoteConfig: remoteConfig.copyWith( - appStatus: remoteConfig.appStatus.copyWith( - androidUpdateUrl: value, + AppConfigurationFieldChanged( + remoteConfig: remoteConfig.copyWith( + appStatus: remoteConfig.appStatus.copyWith( + androidUpdateUrl: value, + ), + ), ), - ), - ), - ); + ); }, ), ], @@ -609,15 +591,14 @@ class _AppConfigurationPageState extends State { Text( description, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: + Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.xs), TextFormField( controller: controller, - initialValue: controller == null - ? value.toString() - : null, + initialValue: controller == null ? value.toString() : null, keyboardType: TextInputType.number, decoration: const InputDecoration( border: OutlineInputBorder(), @@ -656,15 +637,14 @@ class _AppConfigurationPageState extends State { Text( description, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: + Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), ), const SizedBox(height: AppSpacing.xs), TextFormField( controller: controller, - initialValue: controller == null - ? value - : null, + initialValue: controller == null ? value : null, decoration: const InputDecoration( border: OutlineInputBorder(), isDense: true, @@ -675,51 +655,6 @@ class _AppConfigurationPageState extends State { ), ); } - - Widget _buildDropdownField( - BuildContext context, { - required String label, - required String description, - required T value, - required List items, - required String Function(T) itemLabelBuilder, - required ValueChanged onChanged, - }) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: AppSpacing.xs), - Text( - description, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), - ), - const SizedBox(height: AppSpacing.xs), - DropdownButtonFormField( - value: value, - decoration: const InputDecoration( - border: OutlineInputBorder(), - isDense: true, - ), - items: items.map((item) { - return DropdownMenuItem( - value: item, - child: Text(itemLabelBuilder(item)), - ); - }).toList(), - onChanged: onChanged, - ), - ], - ), - ); - } } class _UserPreferenceLimitsForm extends StatefulWidget { @@ -730,7 +665,7 @@ class _UserPreferenceLimitsForm extends StatefulWidget { required this.buildIntField, }); - final String userRole; + final AppUserRole userRole; final RemoteConfig remoteConfig; final ValueChanged onConfigChanged; final Widget Function( @@ -740,8 +675,7 @@ class _UserPreferenceLimitsForm extends StatefulWidget { required int value, required ValueChanged onChanged, TextEditingController? controller, - }) - buildIntField; + }) buildIntField; @override State<_UserPreferenceLimitsForm> createState() => @@ -749,46 +683,13 @@ class _UserPreferenceLimitsForm extends StatefulWidget { } class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { - late final TextEditingController _guestFollowedItemsLimitController; - late final TextEditingController _guestSavedHeadlinesLimitController; - late final TextEditingController _authenticatedFollowedItemsLimitController; - late final TextEditingController _authenticatedSavedHeadlinesLimitController; - late final TextEditingController _premiumFollowedItemsLimitController; - late final TextEditingController _premiumSavedHeadlinesLimitController; + late final TextEditingController _followedItemsLimitController; + late final TextEditingController _savedHeadlinesLimitController; @override void initState() { super.initState(); - _guestFollowedItemsLimitController = TextEditingController( - text: widget.remoteConfig.userPreferenceConfig.guestFollowedItemsLimit - .toString(), - ); - _guestSavedHeadlinesLimitController = TextEditingController( - text: widget.remoteConfig.userPreferenceConfig.guestSavedHeadlinesLimit - .toString(), - ); - _authenticatedFollowedItemsLimitController = TextEditingController( - text: widget - .remoteConfig - .userPreferenceConfig - .authenticatedFollowedItemsLimit - .toString(), - ); - _authenticatedSavedHeadlinesLimitController = TextEditingController( - text: widget - .remoteConfig - .userPreferenceConfig - .authenticatedSavedHeadlinesLimit - .toString(), - ); - _premiumFollowedItemsLimitController = TextEditingController( - text: widget.remoteConfig.userPreferenceConfig.premiumFollowedItemsLimit - .toString(), - ); - _premiumSavedHeadlinesLimitController = TextEditingController( - text: widget.remoteConfig.userPreferenceConfig.premiumSavedHeadlinesLimit - .toString(), - ); + _initializeControllers(); } @override @@ -796,89 +697,56 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { super.didUpdateWidget(oldWidget); if (widget.remoteConfig.userPreferenceConfig != oldWidget.remoteConfig.userPreferenceConfig) { - _guestFollowedItemsLimitController.value = TextEditingValue( - text: widget.remoteConfig.userPreferenceConfig.guestFollowedItemsLimit - .toString(), - selection: TextSelection.collapsed( - offset: widget.remoteConfig.userPreferenceConfig.guestFollowedItemsLimit - .toString() - .length, - ), - ); - _guestSavedHeadlinesLimitController.value = TextEditingValue( - text: widget.remoteConfig.userPreferenceConfig.guestSavedHeadlinesLimit - .toString(), - selection: TextSelection.collapsed( - offset: widget.remoteConfig.userPreferenceConfig.guestSavedHeadlinesLimit - .toString() - .length, - ), - ); - _authenticatedFollowedItemsLimitController.value = TextEditingValue( - text: widget - .remoteConfig - .userPreferenceConfig - .authenticatedFollowedItemsLimit - .toString(), - selection: TextSelection.collapsed( - offset: widget - .remoteConfig - .userPreferenceConfig - .authenticatedFollowedItemsLimit - .toString() - .length, - ), - ); - _authenticatedSavedHeadlinesLimitController.value = TextEditingValue( - text: widget - .remoteConfig - .userPreferenceConfig - .authenticatedSavedHeadlinesLimit - .toString(), - selection: TextSelection.collapsed( - offset: widget - .remoteConfig - .userPreferenceConfig - .authenticatedSavedHeadlinesLimit - .toString() - .length, - ), - ); - _premiumFollowedItemsLimitController.value = TextEditingValue( - text: widget.remoteConfig.userPreferenceConfig.premiumFollowedItemsLimit - .toString(), - selection: TextSelection.collapsed( - offset: widget - .remoteConfig - .userPreferenceConfig - .premiumFollowedItemsLimit - .toString() - .length, - ), - ); - _premiumSavedHeadlinesLimitController.value = TextEditingValue( - text: widget.remoteConfig.userPreferenceConfig.premiumSavedHeadlinesLimit - .toString(), - selection: TextSelection.collapsed( - offset: widget - .remoteConfig - .userPreferenceConfig - .premiumSavedHeadlinesLimit - .toString() - .length, - ), - ); + _updateControllers(); + } + } + + void _initializeControllers() { + final config = widget.remoteConfig.userPreferenceConfig; + switch (widget.userRole) { + case AppUserRole.guestUser: + _followedItemsLimitController = + TextEditingController(text: config.guestFollowedItemsLimit.toString()); + _savedHeadlinesLimitController = + TextEditingController(text: config.guestSavedHeadlinesLimit.toString()); + case AppUserRole.standardUser: + _followedItemsLimitController = TextEditingController( + text: config.authenticatedFollowedItemsLimit.toString()); + _savedHeadlinesLimitController = TextEditingController( + text: config.authenticatedSavedHeadlinesLimit.toString()); + case AppUserRole.premiumUser: + _followedItemsLimitController = TextEditingController( + text: config.premiumFollowedItemsLimit.toString()); + _savedHeadlinesLimitController = TextEditingController( + text: config.premiumSavedHeadlinesLimit.toString()); + } + } + + void _updateControllers() { + final config = widget.remoteConfig.userPreferenceConfig; + switch (widget.userRole) { + case AppUserRole.guestUser: + _followedItemsLimitController.text = + config.guestFollowedItemsLimit.toString(); + _savedHeadlinesLimitController.text = + config.guestSavedHeadlinesLimit.toString(); + case AppUserRole.standardUser: + _followedItemsLimitController.text = + config.authenticatedFollowedItemsLimit.toString(); + _savedHeadlinesLimitController.text = + config.authenticatedSavedHeadlinesLimit.toString(); + case AppUserRole.premiumUser: + _followedItemsLimitController.text = + config.premiumFollowedItemsLimit.toString(); + _savedHeadlinesLimitController.text = + config.premiumSavedHeadlinesLimit.toString(); } } @override void dispose() { - _guestFollowedItemsLimitController.dispose(); - _guestSavedHeadlinesLimitController.dispose(); - _authenticatedFollowedItemsLimitController.dispose(); - _authenticatedSavedHeadlinesLimitController.dispose(); - _premiumFollowedItemsLimitController.dispose(); - _premiumSavedHeadlinesLimitController.dispose(); + _followedItemsLimitController.dispose(); + _savedHeadlinesLimitController.dispose(); super.dispose(); } @@ -886,128 +754,88 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { Widget build(BuildContext context) { final userPreferenceConfig = widget.remoteConfig.userPreferenceConfig; + return Column( + children: [ + widget.buildIntField( + context, + label: 'Followed Items Limit', + description: + 'Maximum number of countries, news sources, or categories this ' + 'user role can follow (each type has its own limit).', + value: _getFollowedItemsLimit(userPreferenceConfig), + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + userPreferenceConfig: + _updateFollowedItemsLimit(userPreferenceConfig, value), + ), + ); + }, + controller: _followedItemsLimitController, + ), + widget.buildIntField( + context, + label: 'Saved Headlines Limit', + description: + 'Maximum number of headlines this user role can save.', + value: _getSavedHeadlinesLimit(userPreferenceConfig), + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + userPreferenceConfig: + _updateSavedHeadlinesLimit(userPreferenceConfig, value), + ), + ); + }, + controller: _savedHeadlinesLimitController, + ), + ], + ); + } + + int _getFollowedItemsLimit(UserPreferenceConfig config) { switch (widget.userRole) { - case 'guestUser': - return Column( - children: [ - widget.buildIntField( - context, - label: 'Guest Followed Items Limit', - description: - 'Maximum number of countries, news sources, or categories a ' - 'Guest user can follow (each type has its own limit).', - value: userPreferenceConfig.guestFollowedItemsLimit, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - userPreferenceConfig: userPreferenceConfig.copyWith( - guestFollowedItemsLimit: value, - ), - ), - ); - }, - controller: _guestFollowedItemsLimitController, - ), - widget.buildIntField( - context, - label: 'Guest Saved Headlines Limit', - description: 'Maximum number of headlines a Guest user can save.', - value: userPreferenceConfig.guestSavedHeadlinesLimit, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - userPreferenceConfig: userPreferenceConfig.copyWith( - guestSavedHeadlinesLimit: value, - ), - ), - ); - }, - controller: _guestSavedHeadlinesLimitController, - ), - ], - ); - case 'standardUser': - return Column( - children: [ - widget.buildIntField( - context, - label: 'Standard User Followed Items Limit', - description: - 'Maximum number of countries, news sources, or categories a ' - 'Standard user can follow (each type has its own limit).', - value: userPreferenceConfig.authenticatedFollowedItemsLimit, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - userPreferenceConfig: userPreferenceConfig.copyWith( - authenticatedFollowedItemsLimit: value, - ), - ), - ); - }, - controller: _authenticatedFollowedItemsLimitController, - ), - widget.buildIntField( - context, - label: 'Standard User Saved Headlines Limit', - description: - 'Maximum number of headlines a Standard user can save.', - value: userPreferenceConfig.authenticatedSavedHeadlinesLimit, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - userPreferenceConfig: userPreferenceConfig.copyWith( - authenticatedSavedHeadlinesLimit: value, - ), - ), - ); - }, - controller: _authenticatedSavedHeadlinesLimitController, - ), - ], - ); - case 'premiumUser': - return Column( - children: [ - widget.buildIntField( - context, - label: 'Premium Followed Items Limit', - description: - 'Maximum number of countries, news sources, or categories a ' - 'Premium user can follow (each type has its own limit).', - value: userPreferenceConfig.premiumFollowedItemsLimit, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - userPreferenceConfig: userPreferenceConfig.copyWith( - premiumFollowedItemsLimit: value, - ), - ), - ); - }, - controller: _premiumFollowedItemsLimitController, - ), - widget.buildIntField( - context, - label: 'Premium Saved Headlines Limit', - description: - 'Maximum number of headlines a Premium user can save.', - value: userPreferenceConfig.premiumSavedHeadlinesLimit, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - userPreferenceConfig: userPreferenceConfig.copyWith( - premiumSavedHeadlinesLimit: value, - ), - ), - ); - }, - controller: _premiumSavedHeadlinesLimitController, - ), - ], - ); - default: - return const SizedBox.shrink(); + case AppUserRole.guestUser: + return config.guestFollowedItemsLimit; + case AppUserRole.standardUser: + return config.authenticatedFollowedItemsLimit; + case AppUserRole.premiumUser: + return config.premiumFollowedItemsLimit; + } + } + + int _getSavedHeadlinesLimit(UserPreferenceConfig config) { + switch (widget.userRole) { + case AppUserRole.guestUser: + return config.guestSavedHeadlinesLimit; + case AppUserRole.standardUser: + return config.authenticatedSavedHeadlinesLimit; + case AppUserRole.premiumUser: + return config.premiumSavedHeadlinesLimit; + } + } + + UserPreferenceConfig _updateFollowedItemsLimit( + UserPreferenceConfig config, int value) { + switch (widget.userRole) { + case AppUserRole.guestUser: + return config.copyWith(guestFollowedItemsLimit: value); + case AppUserRole.standardUser: + return config.copyWith(authenticatedFollowedItemsLimit: value); + case AppUserRole.premiumUser: + return config.copyWith(premiumFollowedItemsLimit: value); + } + } + + UserPreferenceConfig _updateSavedHeadlinesLimit( + UserPreferenceConfig config, int value) { + switch (widget.userRole) { + case AppUserRole.guestUser: + return config.copyWith(guestSavedHeadlinesLimit: value); + case AppUserRole.standardUser: + return config.copyWith(authenticatedSavedHeadlinesLimit: value); + case AppUserRole.premiumUser: + return config.copyWith(premiumSavedHeadlinesLimit: value); } } } @@ -1020,7 +848,7 @@ class _AdConfigForm extends StatefulWidget { required this.buildIntField, }); - final String userRole; + final AppUserRole userRole; final RemoteConfig remoteConfig; final ValueChanged onConfigChanged; final Widget Function( @@ -1030,188 +858,100 @@ class _AdConfigForm extends StatefulWidget { required int value, required ValueChanged onChanged, TextEditingController? controller, - }) - buildIntField; + }) buildIntField; @override State<_AdConfigForm> createState() => _AdConfigFormState(); } class _AdConfigFormState extends State<_AdConfigForm> { - late final TextEditingController _guestAdFrequencyController; - late final TextEditingController _guestAdPlacementIntervalController; + late final TextEditingController _adFrequencyController; + late final TextEditingController _adPlacementIntervalController; late final TextEditingController - _guestArticlesToReadBeforeShowingInterstitialAdsController; - late final TextEditingController _authenticatedAdFrequencyController; - late final TextEditingController _authenticatedAdPlacementIntervalController; - late final TextEditingController - _standardUserArticlesToReadBeforeShowingInterstitialAdsController; - late final TextEditingController _premiumAdFrequencyController; - late final TextEditingController _premiumAdPlacementIntervalController; - late final TextEditingController - _premiumUserArticlesToReadBeforeShowingInterstitialAdsController; + _articlesToReadBeforeShowingInterstitialAdsController; @override void initState() { super.initState(); - _guestAdFrequencyController = TextEditingController( - text: widget.remoteConfig.adConfig.guestAdFrequency.toString(), - ); - _guestAdPlacementIntervalController = TextEditingController( - text: widget.remoteConfig.adConfig.guestAdPlacementInterval.toString(), - ); - _guestArticlesToReadBeforeShowingInterstitialAdsController = - TextEditingController( - text: widget - .remoteConfig - .adConfig - .guestArticlesToReadBeforeShowingInterstitialAds - .toString(), - ); - _authenticatedAdFrequencyController = TextEditingController( - text: widget.remoteConfig.adConfig.authenticatedAdFrequency.toString(), - ); - _authenticatedAdPlacementIntervalController = TextEditingController( - text: widget.remoteConfig.adConfig.authenticatedAdPlacementInterval - .toString(), - ); - _standardUserArticlesToReadBeforeShowingInterstitialAdsController = - TextEditingController( - text: widget - .remoteConfig - .adConfig - .standardUserArticlesToReadBeforeShowingInterstitialAds - .toString(), - ); - _premiumAdFrequencyController = TextEditingController( - text: widget.remoteConfig.adConfig.premiumAdFrequency.toString(), - ); - _premiumAdPlacementIntervalController = TextEditingController( - text: widget.remoteConfig.adConfig.premiumAdPlacementInterval.toString(), - ); - _premiumUserArticlesToReadBeforeShowingInterstitialAdsController = - TextEditingController( - text: widget - .remoteConfig - .adConfig - .premiumUserArticlesToReadBeforeShowingInterstitialAds - .toString(), - ); + _initializeControllers(); } @override void didUpdateWidget(covariant _AdConfigForm oldWidget) { super.didUpdateWidget(oldWidget); if (widget.remoteConfig.adConfig != oldWidget.remoteConfig.adConfig) { - _guestAdFrequencyController.value = TextEditingValue( - text: widget.remoteConfig.adConfig.guestAdFrequency.toString(), - selection: TextSelection.collapsed( - offset: widget.remoteConfig.adConfig.guestAdFrequency.toString().length, - ), - ); - _guestAdPlacementIntervalController.value = TextEditingValue( - text: widget.remoteConfig.adConfig.guestAdPlacementInterval.toString(), - selection: TextSelection.collapsed( - offset: widget.remoteConfig.adConfig.guestAdPlacementInterval - .toString() - .length, - ), - ); - _guestArticlesToReadBeforeShowingInterstitialAdsController.value = - TextEditingValue( - text: widget - .remoteConfig - .adConfig - .guestArticlesToReadBeforeShowingInterstitialAds - .toString(), - selection: TextSelection.collapsed( - offset: widget - .remoteConfig - .adConfig - .guestArticlesToReadBeforeShowingInterstitialAds - .toString() - .length, - ), - ); - _authenticatedAdFrequencyController.value = TextEditingValue( - text: widget.remoteConfig.adConfig.authenticatedAdFrequency.toString(), - selection: TextSelection.collapsed( - offset: widget.remoteConfig.adConfig.authenticatedAdFrequency - .toString() - .length, - ), - ); - _authenticatedAdPlacementIntervalController.value = TextEditingValue( - text: widget.remoteConfig.adConfig.authenticatedAdPlacementInterval - .toString(), - selection: TextSelection.collapsed( - offset: widget.remoteConfig.adConfig.authenticatedAdPlacementInterval - .toString() - .length, - ), - ); - _standardUserArticlesToReadBeforeShowingInterstitialAdsController.value = - TextEditingValue( - text: widget - .remoteConfig - .adConfig - .standardUserArticlesToReadBeforeShowingInterstitialAds - .toString(), - selection: TextSelection.collapsed( - offset: widget - .remoteConfig - .adConfig - .standardUserArticlesToReadBeforeShowingInterstitialAds - .toString() - .length, - ), - ); - _premiumAdFrequencyController.value = TextEditingValue( - text: widget.remoteConfig.adConfig.premiumAdFrequency.toString(), - selection: TextSelection.collapsed( - offset: widget.remoteConfig.adConfig.premiumAdFrequency - .toString() - .length, - ), - ); - _premiumAdPlacementIntervalController.value = TextEditingValue( - text: widget.remoteConfig.adConfig.premiumAdPlacementInterval.toString(), - selection: TextSelection.collapsed( - offset: widget.remoteConfig.adConfig.premiumAdPlacementInterval - .toString() - .length, - ), - ); - _premiumUserArticlesToReadBeforeShowingInterstitialAdsController.value = - TextEditingValue( - text: widget - .remoteConfig - .adConfig - .premiumUserArticlesToReadBeforeShowingInterstitialAds - .toString(), - selection: TextSelection.collapsed( - offset: widget - .remoteConfig - .adConfig - .premiumUserArticlesToReadBeforeShowingInterstitialAds - .toString() - .length, - ), - ); + _updateControllers(); + } + } + + void _initializeControllers() { + final adConfig = widget.remoteConfig.adConfig; + switch (widget.userRole) { + case AppUserRole.guestUser: + _adFrequencyController = + TextEditingController(text: adConfig.guestAdFrequency.toString()); + _adPlacementIntervalController = TextEditingController( + text: adConfig.guestAdPlacementInterval.toString()); + _articlesToReadBeforeShowingInterstitialAdsController = + TextEditingController( + text: adConfig.guestArticlesToReadBeforeShowingInterstitialAds + .toString()); + case AppUserRole.standardUser: + _adFrequencyController = TextEditingController( + text: adConfig.authenticatedAdFrequency.toString()); + _adPlacementIntervalController = TextEditingController( + text: adConfig.authenticatedAdPlacementInterval.toString()); + _articlesToReadBeforeShowingInterstitialAdsController = + TextEditingController( + text: adConfig + .standardUserArticlesToReadBeforeShowingInterstitialAds + .toString()); + case AppUserRole.premiumUser: + _adFrequencyController = + TextEditingController(text: adConfig.premiumAdFrequency.toString()); + _adPlacementIntervalController = TextEditingController( + text: adConfig.premiumAdPlacementInterval.toString()); + _articlesToReadBeforeShowingInterstitialAdsController = + TextEditingController( + text: adConfig + .premiumUserArticlesToReadBeforeShowingInterstitialAds + .toString()); + } + } + + void _updateControllers() { + final adConfig = widget.remoteConfig.adConfig; + switch (widget.userRole) { + case AppUserRole.guestUser: + _adFrequencyController.text = adConfig.guestAdFrequency.toString(); + _adPlacementIntervalController.text = + adConfig.guestAdPlacementInterval.toString(); + _articlesToReadBeforeShowingInterstitialAdsController.text = adConfig + .guestArticlesToReadBeforeShowingInterstitialAds + .toString(); + case AppUserRole.standardUser: + _adFrequencyController.text = + adConfig.authenticatedAdFrequency.toString(); + _adPlacementIntervalController.text = + adConfig.authenticatedAdPlacementInterval.toString(); + _articlesToReadBeforeShowingInterstitialAdsController.text = adConfig + .standardUserArticlesToReadBeforeShowingInterstitialAds + .toString(); + case AppUserRole.premiumUser: + _adFrequencyController.text = adConfig.premiumAdFrequency.toString(); + _adPlacementIntervalController.text = + adConfig.premiumAdPlacementInterval.toString(); + _articlesToReadBeforeShowingInterstitialAdsController.text = adConfig + .premiumUserArticlesToReadBeforeShowingInterstitialAds + .toString(); } } @override void dispose() { - _guestAdFrequencyController.dispose(); - _guestAdPlacementIntervalController.dispose(); - _guestArticlesToReadBeforeShowingInterstitialAdsController.dispose(); - _authenticatedAdFrequencyController.dispose(); - _authenticatedAdPlacementIntervalController.dispose(); - _standardUserArticlesToReadBeforeShowingInterstitialAdsController.dispose(); - _premiumAdFrequencyController.dispose(); - _premiumAdPlacementIntervalController.dispose(); - _premiumUserArticlesToReadBeforeShowingInterstitialAdsController.dispose(); + _adFrequencyController.dispose(); + _adPlacementIntervalController.dispose(); + _articlesToReadBeforeShowingInterstitialAdsController.dispose(); super.dispose(); } @@ -1219,188 +959,126 @@ class _AdConfigFormState extends State<_AdConfigForm> { Widget build(BuildContext context) { final adConfig = widget.remoteConfig.adConfig; + return Column( + children: [ + widget.buildIntField( + context, + label: 'Ad Frequency', + description: + 'How often an ad can appear for this user role (e.g., a value ' + 'of 5 means an ad could be placed after every 5 news items).', + value: _getAdFrequency(adConfig), + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: _updateAdFrequency(adConfig, value), + ), + ); + }, + controller: _adFrequencyController, + ), + widget.buildIntField( + context, + label: 'Ad Placement Interval', + description: + 'Minimum number of news items that must be shown before the ' + 'very first ad appears for this user role.', + value: _getAdPlacementInterval(adConfig), + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: _updateAdPlacementInterval(adConfig, value), + ), + ); + }, + controller: _adPlacementIntervalController, + ), + widget.buildIntField( + context, + label: 'Articles Before Interstitial Ads', + description: + 'Number of articles this user role needs to read before a ' + 'full-screen interstitial ad is shown.', + value: _getArticlesBeforeInterstitial(adConfig), + onChanged: (value) { + widget.onConfigChanged( + widget.remoteConfig.copyWith( + adConfig: _updateArticlesBeforeInterstitial(adConfig, value), + ), + ); + }, + controller: _articlesToReadBeforeShowingInterstitialAdsController, + ), + ], + ); + } + + int _getAdFrequency(AdConfig config) { switch (widget.userRole) { - case 'guestUser': - return Column( - children: [ - widget.buildIntField( - context, - label: 'Guest Ad Frequency', - description: - 'How often an ad can appear for Guest users (e.g., a value ' - 'of 5 means an ad could be placed after every 5 news items).', - value: adConfig.guestAdFrequency, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith(guestAdFrequency: value), - ), - ); - }, - controller: _guestAdFrequencyController, - ), - widget.buildIntField( - context, - label: 'Guest Ad Placement Interval', - description: - 'Minimum number of news items that must be shown before the ' - 'very first ad appears for Guest users.', - value: adConfig.guestAdPlacementInterval, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - guestAdPlacementInterval: value, - ), - ), - ); - }, - controller: _guestAdPlacementIntervalController, - ), - widget.buildIntField( - context, - label: 'Guest Articles Before Interstitial Ads', - description: - 'Number of articles a Guest user needs to read before a ' - 'full-screen interstitial ad is shown.', - value: adConfig.guestArticlesToReadBeforeShowingInterstitialAds, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - guestArticlesToReadBeforeShowingInterstitialAds: value, - ), - ), - ); - }, - controller: - _guestArticlesToReadBeforeShowingInterstitialAdsController, - ), - ], - ); - case 'standardUser': - return Column( - children: [ - widget.buildIntField( - context, - label: 'Standard User Ad Frequency', - description: - 'How often an ad can appear for Standard users (e.g., a value ' - 'of 10 means an ad could be placed after every 10 news items).', - value: adConfig.authenticatedAdFrequency, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - authenticatedAdFrequency: value, - ), - ), - ); - }, - controller: _authenticatedAdFrequencyController, - ), - widget.buildIntField( - context, - label: 'Standard User Ad Placement Interval', - description: - 'Minimum number of news items that must be shown before the ' - 'very first ad appears for Standard users.', - value: adConfig.authenticatedAdPlacementInterval, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - authenticatedAdPlacementInterval: value, - ), - ), - ); - }, - controller: _authenticatedAdPlacementIntervalController, - ), - widget.buildIntField( - context, - label: 'Standard User Articles Before Interstitial Ads', - description: - 'Number of articles a Standard user needs to read before a ' - 'full-screen interstitial ad is shown.', - value: adConfig - .standardUserArticlesToReadBeforeShowingInterstitialAds, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - standardUserArticlesToReadBeforeShowingInterstitialAds: - value, - ), - ), - ); - }, - controller: - _standardUserArticlesToReadBeforeShowingInterstitialAdsController, - ), - ], - ); - case 'premiumUser': - return Column( - children: [ - widget.buildIntField( - context, - label: 'Premium Ad Frequency', - description: - 'How often an ad can appear for Premium users (0 for no ads).', - value: adConfig.premiumAdFrequency, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith(premiumAdFrequency: value), - ), - ); - }, - controller: _premiumAdFrequencyController, - ), - widget.buildIntField( - context, - label: 'Premium Ad Placement Interval', - description: - 'Minimum number of news items that must be shown before the ' - 'very first ad appears for Premium users.', - value: adConfig.premiumAdPlacementInterval, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - premiumAdPlacementInterval: value, - ), - ), - ); - }, - controller: _premiumAdPlacementIntervalController, - ), - widget.buildIntField( - context, - label: 'Premium User Articles Before Interstitial Ads', - description: - 'Number of articles a Premium user needs to read before a ' - 'full-screen interstitial ad is shown.', - value: adConfig - .premiumUserArticlesToReadBeforeShowingInterstitialAds, - onChanged: (value) { - widget.onConfigChanged( - widget.remoteConfig.copyWith( - adConfig: adConfig.copyWith( - premiumUserArticlesToReadBeforeShowingInterstitialAds: - value, - ), - ), - ); - }, - controller: - _premiumUserArticlesToReadBeforeShowingInterstitialAdsController, - ), - ], - ); - default: - return const SizedBox.shrink(); + case AppUserRole.guestUser: + return config.guestAdFrequency; + case AppUserRole.standardUser: + return config.authenticatedAdFrequency; + case AppUserRole.premiumUser: + return config.premiumAdFrequency; + } + } + + int _getAdPlacementInterval(AdConfig config) { + switch (widget.userRole) { + case AppUserRole.guestUser: + return config.guestAdPlacementInterval; + case AppUserRole.standardUser: + return config.authenticatedAdPlacementInterval; + case AppUserRole.premiumUser: + return config.premiumAdPlacementInterval; + } + } + + int _getArticlesBeforeInterstitial(AdConfig config) { + switch (widget.userRole) { + case AppUserRole.guestUser: + return config.guestArticlesToReadBeforeShowingInterstitialAds; + case AppUserRole.standardUser: + return config.standardUserArticlesToReadBeforeShowingInterstitialAds; + case AppUserRole.premiumUser: + return config.premiumUserArticlesToReadBeforeShowingInterstitialAds; + } + } + + AdConfig _updateAdFrequency(AdConfig config, int value) { + switch (widget.userRole) { + case AppUserRole.guestUser: + return config.copyWith(guestAdFrequency: value); + case AppUserRole.standardUser: + return config.copyWith(authenticatedAdFrequency: value); + case AppUserRole.premiumUser: + return config.copyWith(premiumAdFrequency: value); + } + } + + AdConfig _updateAdPlacementInterval(AdConfig config, int value) { + switch (widget.userRole) { + case AppUserRole.guestUser: + return config.copyWith(guestAdPlacementInterval: value); + case AppUserRole.standardUser: + return config.copyWith(authenticatedAdPlacementInterval: value); + case AppUserRole.premiumUser: + return config.copyWith(premiumAdPlacementInterval: value); + } + } + + AdConfig _updateArticlesBeforeInterstitial(AdConfig config, int value) { + switch (widget.userRole) { + case AppUserRole.guestUser: + return config.copyWith( + guestArticlesToReadBeforeShowingInterstitialAds: value); + case AppUserRole.standardUser: + return config.copyWith( + standardUserArticlesToReadBeforeShowingInterstitialAds: value); + case AppUserRole.premiumUser: + return config.copyWith( + premiumUserArticlesToReadBeforeShowingInterstitialAds: value); } } } @@ -1413,7 +1091,7 @@ class _AccountActionConfigForm extends StatefulWidget { required this.buildIntField, }); - final String userRole; + final AppUserRole userRole; final RemoteConfig remoteConfig; final ValueChanged onConfigChanged; final Widget Function( @@ -1423,8 +1101,7 @@ class _AccountActionConfigForm extends StatefulWidget { required int value, required ValueChanged onChanged, TextEditingController? controller, - }) - buildIntField; + }) buildIntField; @override State<_AccountActionConfigForm> createState() => @@ -1432,24 +1109,12 @@ class _AccountActionConfigForm extends StatefulWidget { } class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { - late final TextEditingController _guestDaysBetweenAccountActionsController; - late final TextEditingController - _standardUserDaysBetweenAccountActionsController; + late final Map _controllers; @override void initState() { super.initState(); - _guestDaysBetweenAccountActionsController = TextEditingController( - text: widget.remoteConfig.accountActionConfig.guestDaysBetweenActions - .toString(), - ); - _standardUserDaysBetweenAccountActionsController = TextEditingController( - text: widget - .remoteConfig - .accountActionConfig - .standardUserDaysBetweenActions - .toString(), - ); + _controllers = _initializeControllers(); } @override @@ -1457,111 +1122,85 @@ class _AccountActionConfigFormState extends State<_AccountActionConfigForm> { super.didUpdateWidget(oldWidget); if (widget.remoteConfig.accountActionConfig != oldWidget.remoteConfig.accountActionConfig) { - _guestDaysBetweenAccountActionsController.value = TextEditingValue( - text: widget - .remoteConfig - .accountActionConfig - .guestDaysBetweenActions - .toString(), - selection: TextSelection.collapsed( - offset: widget - .remoteConfig - .accountActionConfig - .guestDaysBetweenActions - .toString() - .length, - ), - ); - _standardUserDaysBetweenAccountActionsController.value = TextEditingValue( - text: widget - .remoteConfig - .accountActionConfig - .standardUserDaysBetweenActions - .toString(), - selection: TextSelection.collapsed( - offset: widget - .remoteConfig - .accountActionConfig - .standardUserDaysBetweenActions - .toString() - .length, - ), - ); + _updateControllers(); + } + } + + Map _initializeControllers() { + final config = widget.remoteConfig.accountActionConfig; + final daysMap = _getDaysMap(config); + return { + for (final type in FeedActionType.values) + type: TextEditingController(text: (daysMap[type] ?? 0).toString()), + }; + } + + void _updateControllers() { + final config = widget.remoteConfig.accountActionConfig; + final daysMap = _getDaysMap(config); + for (final type in FeedActionType.values) { + _controllers[type]?.text = (daysMap[type] ?? 0).toString(); + } + } + + Map _getDaysMap(AccountActionConfig config) { + switch (widget.userRole) { + case AppUserRole.guestUser: + return config.guestDaysBetweenActions; + case AppUserRole.standardUser: + return config.standardUserDaysBetweenActions; + case AppUserRole.premiumUser: + return {}; } } @override void dispose() { - _guestDaysBetweenAccountActionsController.dispose(); - _standardUserDaysBetweenAccountActionsController.dispose(); + for (final controller in _controllers.values) { + controller.dispose(); + } super.dispose(); } + String _formatLabel(String enumName) { + // Converts camelCase to Title Case + final spaced = enumName.replaceAllMapped( + RegExp(r'([A-Z])'), (match) => ' ${match.group(1)}'); + return '${spaced[0].toUpperCase()}${spaced.substring(1)} Days'; + } + @override Widget build(BuildContext context) { final accountActionConfig = widget.remoteConfig.accountActionConfig; + final relevantActionTypes = + _getDaysMap(accountActionConfig).keys.toList(); - switch (widget.userRole) { - case 'guestUser': - return Column( - children: [ - widget.buildIntField( - context, - label: 'Guest Days Between In-App Prompts', - description: - 'Minimum number of days that must pass before a Guest user ' - 'sees another in-app prompt.', - value: accountActionConfig.guestDaysBetweenActions[FeedActionType.linkAccount] ?? 0, - onChanged: (value) { - final updatedMap = Map.from( - accountActionConfig.guestDaysBetweenActions, - ); - updatedMap[FeedActionType.linkAccount] = value; - widget.onConfigChanged( - widget.remoteConfig.copyWith( - accountActionConfig: accountActionConfig.copyWith( - guestDaysBetweenActions: updatedMap, - ), - ), - ); - }, - controller: _guestDaysBetweenAccountActionsController, - ), - ], - ); - case 'standardUser': - return Column( - children: [ - widget.buildIntField( - context, - label: 'Standard User Days Between In-App Prompts', - description: - 'Minimum number of days that must pass before a Standard user ' - 'sees another in-app prompt.', - value: accountActionConfig.standardUserDaysBetweenActions[FeedActionType.linkAccount] ?? 0, - onChanged: (value) { - final updatedMap = Map.from( - accountActionConfig.standardUserDaysBetweenActions, - ); - updatedMap[FeedActionType.linkAccount] = value; - widget.onConfigChanged( - widget.remoteConfig.copyWith( - accountActionConfig: accountActionConfig.copyWith( - standardUserDaysBetweenActions: updatedMap, - ), - ), - ); - }, - controller: _standardUserDaysBetweenAccountActionsController, - ), - ], + return Column( + children: relevantActionTypes.map((actionType) { + return widget.buildIntField( + context, + label: _formatLabel(actionType.name), + description: + 'Minimum number of days before showing the ${actionType.name} prompt.', + value: _getDaysMap(accountActionConfig)[actionType] ?? 0, + onChanged: (value) { + final currentMap = _getDaysMap(accountActionConfig); + final updatedMap = Map.from(currentMap) + ..[actionType] = value; + + final newConfig = widget.userRole == AppUserRole.guestUser + ? accountActionConfig.copyWith( + guestDaysBetweenActions: updatedMap) + : accountActionConfig.copyWith( + standardUserDaysBetweenActions: updatedMap); + + widget.onConfigChanged( + widget.remoteConfig.copyWith(accountActionConfig: newConfig), + ); + }, + controller: _controllers[actionType], ); - case 'premiumUser': - case 'admin': - case 'publisher': - return const SizedBox.shrink(); - default: - return const SizedBox.shrink(); - } + }).toList(), + ); } } From c3f4d946aa538a7d7a47f51add2d3c1765bd9b39 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:44:53 +0100 Subject: [PATCH 56/69] docs: add docstrings to AppState class --- lib/app/bloc/app_state.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/app/bloc/app_state.dart b/lib/app/bloc/app_state.dart index 1a6a2749..186e6f42 100644 --- a/lib/app/bloc/app_state.dart +++ b/lib/app/bloc/app_state.dart @@ -15,6 +15,10 @@ enum AppStatus { anonymous, } +/// {@template app_state} +/// Represents the overall state of the application, including authentication +/// status, current user, environment, and user-specific settings. +/// {@endtemplate} class AppState extends Equatable { /// {@macro app_state} const AppState({ From 44e68f87c31b00fe761d8ca6128e4ad4332673d7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:45:30 +0100 Subject: [PATCH 57/69] fix(app): Handle anonymous and guest users - Improved anonymous user handling. - Added logging for error handling. - Created default settings based on user ID. - Improved error handling during settings load. - Enhanced user app settings management. --- lib/app/bloc/app_bloc.dart | 46 +++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index e3b02733..b04b1371 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -8,6 +8,7 @@ import 'package:ht_auth_repository/ht_auth_repository.dart'; import 'package:ht_dashboard/app/config/config.dart' as local_config; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:logging/logging.dart'; part 'app_event.dart'; part 'app_state.dart'; @@ -18,9 +19,11 @@ class AppBloc extends Bloc { required HtDataRepository userAppSettingsRepository, required HtDataRepository appConfigRepository, required local_config.AppEnvironment environment, + Logger? logger, }) : _authenticationRepository = authenticationRepository, _userAppSettingsRepository = userAppSettingsRepository, _appConfigRepository = appConfigRepository, + _logger = logger ?? Logger('AppBloc'), super( AppState(environment: environment), ) { @@ -36,6 +39,7 @@ class AppBloc extends Bloc { final HtAuthRepository _authenticationRepository; final HtDataRepository _userAppSettingsRepository; final HtDataRepository _appConfigRepository; + final Logger _logger; late final StreamSubscription _userSubscription; /// Handles user changes and loads initial settings once user is available. @@ -46,10 +50,15 @@ class AppBloc extends Bloc { final user = event.user; final AppStatus status; - if (user != null && - (user.dashboardRole == DashboardUserRole.admin || - user.dashboardRole == DashboardUserRole.publisher)) { - status = AppStatus.authenticated; + if (user != null) { + if (user.dashboardRole == DashboardUserRole.admin || + user.dashboardRole == DashboardUserRole.publisher) { + status = AppStatus.authenticated; + } else if (user.appRole == AppUserRole.guestUser) { + status = AppStatus.anonymous; + } else { + status = AppStatus.unauthenticated; + } } else { status = AppStatus.unauthenticated; } @@ -66,9 +75,12 @@ class AppBloc extends Bloc { emit(state.copyWith(userAppSettings: userAppSettings)); } on NotFoundException { // If settings not found, create default ones - const defaultSettings = UserAppSettings( - id: 'default', - displaySettings: DisplaySettings( + _logger.info( + 'User app settings not found for user ${user.id}. Creating default.', + ); + final defaultSettings = UserAppSettings( + id: user.id, // Use actual user ID for default settings + displaySettings: const DisplaySettings( baseTheme: AppBaseTheme.system, accentTheme: AppAccentTheme.defaultBlue, fontFamily: 'SystemDefault', @@ -76,7 +88,7 @@ class AppBloc extends Bloc { fontWeight: AppFontWeight.regular, ), language: 'en', - feedPreferences: FeedDisplayPreferences( + feedPreferences: const FeedDisplayPreferences( headlineDensity: HeadlineDensity.standard, headlineImageStyle: HeadlineImageStyle.largeThumbnail, showSourceInHeadlineFeed: true, @@ -85,17 +97,25 @@ class AppBloc extends Bloc { ); await _userAppSettingsRepository.create(item: defaultSettings); emit(state.copyWith(userAppSettings: defaultSettings)); - } on HtHttpException catch (e) { + } on HtHttpException catch (e, s) { // Handle HTTP exceptions during settings load - print('Error loading user app settings: ${e.message}'); + _logger.severe( + 'Error loading user app settings for user ${user.id}: ${e.message}', + e, + s, + ); emit(state.copyWith(clearUserAppSettings: true)); - } catch (e) { + } catch (e, s) { // Handle any other unexpected errors - print('Unexpected error loading user app settings: $e'); + _logger.severe( + 'Unexpected error loading user app settings for user ${user.id}: $e', + e, + s, + ); emit(state.copyWith(clearUserAppSettings: true)); } } else { - // If user is unauthenticated, clear app settings + // If user is unauthenticated or anonymous, clear app settings emit(state.copyWith(clearUserAppSettings: true)); } } From 6dafaab3fbfb5190fa607b0c50782496cdc97029 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:45:39 +0100 Subject: [PATCH 58/69] refactor(app_shell): remove unused _goBranch method - Replaced direct method call with lambda. - Improved code readability and maintainability. --- lib/app/view/app_shell.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/app/view/app_shell.dart b/lib/app/view/app_shell.dart index f5abaf6c..da6d8e2e 100644 --- a/lib/app/view/app_shell.dart +++ b/lib/app/view/app_shell.dart @@ -22,13 +22,6 @@ class AppShell extends StatelessWidget { /// navigators in a stateful way. final StatefulNavigationShell navigationShell; - void _goBranch(int index) { - navigationShell.goBranch( - index, - initialLocation: index == navigationShell.currentIndex, - ); - } - @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -70,7 +63,12 @@ class AppShell extends StatelessWidget { ), body: AdaptiveScaffold( selectedIndex: navigationShell.currentIndex, - onSelectedIndexChange: _goBranch, + onSelectedIndexChange: (index) { + navigationShell.goBranch( + index, + initialLocation: index == navigationShell.currentIndex, + ); + }, destinations: [ NavigationDestination( icon: const Icon(Icons.dashboard_outlined), From baf7d9767397e725aa4bf5f3101994802bbbed08 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:45:48 +0100 Subject: [PATCH 59/69] feat(app): add logger to AppBloc - Added Logger to AppBloc - Improved repository names - Added doc comment to _AppView - Updated themeMode switch - Minor code style improvements --- lib/app/view/app.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 8442e8c9..af2ec3f8 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -19,6 +19,7 @@ import 'package:ht_dashboard/shared/theme/app_theme.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_kv_storage_service/ht_kv_storage_service.dart'; import 'package:ht_shared/ht_shared.dart' hide AppStatus; +import 'package:logging/logging.dart'; class App extends StatelessWidget { const App({ @@ -78,11 +79,12 @@ class App extends StatelessWidget { BlocProvider( create: (context) => AppBloc( authenticationRepository: context.read(), - userAppSettingsRepository: context - .read>(), + userAppSettingsRepository: + context.read>(), appConfigRepository: context.read>(), environment: _environment, + logger: Logger('AppBloc'), ), ), BlocProvider( @@ -92,7 +94,7 @@ class App extends StatelessWidget { ), BlocProvider( create: (context) => AppConfigurationBloc( - appConfigRepository: + remoteConfigRepository: context.read>(), ), ), @@ -123,6 +125,7 @@ class App extends StatelessWidget { } class _AppView extends StatefulWidget { + /// {@macro app_view} const _AppView({ required this.htAuthenticationRepository, required this.environment, @@ -216,7 +219,7 @@ class _AppViewState extends State<_AppView> { themeMode: switch (baseTheme) { AppBaseTheme.light => ThemeMode.light, AppBaseTheme.dark => ThemeMode.dark, - AppBaseTheme.system || null => ThemeMode.system, + _ => ThemeMode.system, }, locale: language != null ? Locale(language) : null, ), From 2c97bbde3907127cf643dcfcfd8253535ac6939c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:51:32 +0100 Subject: [PATCH 60/69] refactor(auth): improve authentication bloc - Replaced AuthenticationUserChanged with AuthenticationStatusChanged. - Updated state management to use AuthenticationStatus enum. - Improved error handling and feedback. - Enhanced code clarity and readability. - Standardized error message handling. --- .../bloc/authentication_bloc.dart | 201 ++++++++++++++---- .../bloc/authentication_event.dart | 10 +- .../bloc/authentication_state.dart | 107 +++++----- .../view/request_code_page.dart | 17 +- 4 files changed, 227 insertions(+), 108 deletions(-) diff --git a/lib/authentication/bloc/authentication_bloc.dart b/lib/authentication/bloc/authentication_bloc.dart index 9e3d551b..98f47bf6 100644 --- a/lib/authentication/bloc/authentication_bloc.dart +++ b/lib/authentication/bloc/authentication_bloc.dart @@ -27,13 +27,13 @@ class AuthenticationBloc /// {@macro authentication_bloc} AuthenticationBloc({required HtAuthRepository authenticationRepository}) : _authenticationRepository = authenticationRepository, - super(AuthenticationInitial()) { + super(const AuthenticationState()) { // Listen to authentication state changes from the repository _userAuthSubscription = _authenticationRepository.authStateChanges.listen( - (user) => add(_AuthenticationUserChanged(user: user)), + (user) => add(_AuthenticationStatusChanged(user: user)), ); - on<_AuthenticationUserChanged>(_onAuthenticationUserChanged); + on<_AuthenticationStatusChanged>(_onAuthenticationStatusChanged); on( _onAuthenticationRequestSignInCodeRequested, ); @@ -44,15 +44,25 @@ class AuthenticationBloc final HtAuthRepository _authenticationRepository; late final StreamSubscription _userAuthSubscription; - /// Handles [_AuthenticationUserChanged] events. - Future _onAuthenticationUserChanged( - _AuthenticationUserChanged event, + /// Handles [_AuthenticationStatusChanged] events. + Future _onAuthenticationStatusChanged( + _AuthenticationStatusChanged event, Emitter emit, ) async { if (event.user != null) { - emit(AuthenticationAuthenticated(user: event.user!)); + emit( + state.copyWith( + status: AuthenticationStatus.authenticated, + user: event.user, + ), + ); } else { - emit(AuthenticationUnauthenticated()); + emit( + state.copyWith( + status: AuthenticationStatus.unauthenticated, + user: null, + ), + ); } } @@ -63,37 +73,87 @@ class AuthenticationBloc ) async { // Validate email format (basic check) if (event.email.isEmpty || !event.email.contains('@')) { - emit(const AuthenticationFailure('Please enter a valid email address.')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'Please enter a valid email address.', + ), + ); return; } - emit(AuthenticationRequestCodeLoading()); + emit(state.copyWith(status: AuthenticationStatus.requestCodeLoading)); try { await _authenticationRepository.requestSignInCode( event.email, isDashboardLogin: true, ); - emit(AuthenticationCodeSentSuccess(email: event.email)); + emit( + state.copyWith( + status: AuthenticationStatus.codeSentSuccess, + email: event.email, + ), + ); } on InvalidInputException catch (e) { - emit(AuthenticationFailure('Invalid input: ${e.message}')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'Invalid input: ${e.message}', + ), + ); } on UnauthorizedException catch (e) { - emit(AuthenticationFailure(e.message)); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: e.message, + ), + ); } on ForbiddenException catch (e) { - emit(AuthenticationFailure(e.message)); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: e.message, + ), + ); } on NetworkException catch (_) { - emit(const AuthenticationFailure('Network error occurred.')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'Network error occurred.', + ), + ); } on ServerException catch (e) { - emit(AuthenticationFailure('Server error: ${e.message}')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'Server error: ${e.message}', + ), + ); } on OperationFailedException catch (e) { - emit(AuthenticationFailure('Operation failed: ${e.message}')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'Operation failed: ${e.message}', + ), + ); } on HtHttpException catch (e) { // Catch any other HtHttpException subtypes final message = e.message.isNotEmpty ? e.message : 'An unspecified HTTP error occurred.'; - emit(AuthenticationFailure('HTTP error: $message')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'HTTP error: $message', + ), + ); } catch (e) { // Catch any other unexpected errors - emit(AuthenticationFailure('An unexpected error occurred: $e')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'An unexpected error occurred: $e', + ), + ); // Optionally log the stackTrace here } } @@ -103,33 +163,73 @@ class AuthenticationBloc AuthenticationVerifyCodeRequested event, Emitter emit, ) async { - emit(AuthenticationLoading()); + emit(state.copyWith(status: AuthenticationStatus.loading)); try { await _authenticationRepository.verifySignInCode( event.email, event.code, isDashboardLogin: true, ); - // On success, the _AuthenticationUserChanged listener will handle + // On success, the _AuthenticationStatusChanged listener will handle // emitting AuthenticationAuthenticated. } on InvalidInputException catch (e) { - emit(AuthenticationFailure(e.message)); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: e.message, + ), + ); } on AuthenticationException catch (e) { - emit(AuthenticationFailure(e.message)); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: e.message, + ), + ); } on NotFoundException catch (e) { - emit(AuthenticationFailure(e.message)); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: e.message, + ), + ); } on NetworkException catch (_) { - emit(const AuthenticationFailure('Network error occurred.')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'Network error occurred.', + ), + ); } on ServerException catch (e) { - emit(AuthenticationFailure('Server error: ${e.message}')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'Server error: ${e.message}', + ), + ); } on OperationFailedException catch (e) { - emit(AuthenticationFailure('Operation failed: ${e.message}')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'Operation failed: ${e.message}', + ), + ); } on HtHttpException catch (e) { // Catch any other HtHttpException subtypes - emit(AuthenticationFailure('HTTP error: ${e.message}')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'HTTP error: ${e.message}', + ), + ); } catch (e) { // Catch any other unexpected errors - emit(AuthenticationFailure('An unexpected error occurred: $e')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'An unexpected error occurred: $e', + ), + ); // Optionally log the stackTrace here } } @@ -139,26 +239,47 @@ class AuthenticationBloc AuthenticationSignOutRequested event, Emitter emit, ) async { - emit(AuthenticationLoading()); + emit(state.copyWith(status: AuthenticationStatus.loading)); try { await _authenticationRepository.signOut(); - // On success, the _AuthenticationUserChanged listener will handle + // On success, the _AuthenticationStatusChanged listener will handle // emitting AuthenticationUnauthenticated. - // No need to emit AuthenticationLoading() before calling signOut if - // the authStateChanges listener handles the subsequent state update. - // However, if immediate feedback is desired, it can be kept. - // For now, let's assume the listener is sufficient. } on NetworkException catch (_) { - emit(const AuthenticationFailure('Network error occurred.')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'Network error occurred.', + ), + ); } on ServerException catch (e) { - emit(AuthenticationFailure('Server error: ${e.message}')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'Server error: ${e.message}', + ), + ); } on OperationFailedException catch (e) { - emit(AuthenticationFailure('Operation failed: ${e.message}')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'Operation failed: ${e.message}', + ), + ); } on HtHttpException catch (e) { // Catch any other HtHttpException subtypes - emit(AuthenticationFailure('HTTP error: ${e.message}')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'HTTP error: ${e.message}', + ), + ); } catch (e) { - emit(AuthenticationFailure('An unexpected error occurred: $e')); + emit( + state.copyWith( + status: AuthenticationStatus.failure, + errorMessage: 'An unexpected error occurred: $e', + ), + ); } } diff --git a/lib/authentication/bloc/authentication_event.dart b/lib/authentication/bloc/authentication_event.dart index 6034e484..c6001110 100644 --- a/lib/authentication/bloc/authentication_event.dart +++ b/lib/authentication/bloc/authentication_event.dart @@ -55,12 +55,12 @@ final class AuthenticationSignOutRequested extends AuthenticationEvent { const AuthenticationSignOutRequested(); } -/// {@template _authentication_user_changed} -/// Internal event triggered when the authentication state changes. +/// {@template _authentication_status_changed} +/// Internal event triggered when the authentication status changes. /// {@endtemplate} -final class _AuthenticationUserChanged extends AuthenticationEvent { - /// {@macro _authentication_user_changed} - const _AuthenticationUserChanged({required this.user}); +final class _AuthenticationStatusChanged extends AuthenticationEvent { + /// {@macro _authentication_status_changed} + const _AuthenticationStatusChanged({this.user}); /// The current authenticated user, or null if unauthenticated. final User? user; diff --git a/lib/authentication/bloc/authentication_state.dart b/lib/authentication/bloc/authentication_state.dart index 67c16504..f4ba12bb 100644 --- a/lib/authentication/bloc/authentication_state.dart +++ b/lib/authentication/bloc/authentication_state.dart @@ -1,74 +1,71 @@ part of 'authentication_bloc.dart'; -/// {@template authentication_state} -/// Base class for authentication states. +/// {@template authentication_status} +/// The status of the authentication process. /// {@endtemplate} -sealed class AuthenticationState extends Equatable { - /// {@macro authentication_state} - const AuthenticationState(); +enum AuthenticationStatus { + /// The initial state of the authentication bloc. + initial, - @override - List get props => []; -} + /// An authentication operation is in progress. + loading, -/// {@template authentication_initial} -/// The initial authentication state. -/// {@endtemplate} -final class AuthenticationInitial extends AuthenticationState {} + /// The user is authenticated. + authenticated, -/// {@template authentication_loading} -/// A state indicating that an authentication operation is in progress. -/// {@endtemplate} -final class AuthenticationLoading extends AuthenticationState {} + /// The user is unauthenticated. + unauthenticated, -/// {@template authentication_authenticated} -/// Represents a successful authentication. -/// {@endtemplate} -final class AuthenticationAuthenticated extends AuthenticationState { - /// {@macro authentication_authenticated} - const AuthenticationAuthenticated({required this.user}); + /// The sign-in code is being requested. + requestCodeLoading, - /// The authenticated [User] object. - final User user; + /// The sign-in code was sent successfully. + codeSentSuccess, - @override - List get props => [user]; + /// An authentication operation failed. + failure, } -/// {@template authentication_unauthenticated} -/// Represents an unauthenticated state. -/// {@endtemplate} -final class AuthenticationUnauthenticated extends AuthenticationState {} - -/// {@template authentication_request_code_loading} -/// State indicating that the sign-in code is being requested. +/// {@template authentication_state} +/// Represents the overall authentication state of the application. /// {@endtemplate} -final class AuthenticationRequestCodeLoading extends AuthenticationState {} +final class AuthenticationState extends Equatable { + /// {@macro authentication_state} + const AuthenticationState({ + this.status = AuthenticationStatus.initial, + this.user, + this.email, + this.errorMessage, + }); -/// {@template authentication_code_sent_success} -/// State indicating that the sign-in code was sent successfully. -/// {@endtemplate} -final class AuthenticationCodeSentSuccess extends AuthenticationState { - /// {@macro authentication_code_sent_success} - const AuthenticationCodeSentSuccess({required this.email}); + /// The current status of the authentication process. + final AuthenticationStatus status; - /// The email address the code was sent to. - final String email; + /// The authenticated [User] object, if available. + final User? user; - @override - List get props => [email]; -} - -/// {@template authentication_failure} -/// Represents an authentication failure. -/// {@endtemplate} -final class AuthenticationFailure extends AuthenticationState { - /// {@macro authentication_failure} - const AuthenticationFailure(this.errorMessage); + /// The email address involved in the current authentication flow. + final String? email; - /// The error message describing the authentication failure. - final String errorMessage; + /// The error message describing an authentication failure, if any. + final String? errorMessage; @override - List get props => [errorMessage]; + List get props => [status, user, email, errorMessage]; + + /// Creates a copy of this [AuthenticationState] with the given fields + /// replaced with the new values. + AuthenticationState copyWith({ + AuthenticationStatus? status, + User? user, + String? email, + String? errorMessage, + }) { + return AuthenticationState( + status: status ?? this.status, + user: user ?? this.user, + email: email ?? this.email, + errorMessage: errorMessage ?? this.errorMessage, + ); + } } diff --git a/lib/authentication/view/request_code_page.dart b/lib/authentication/view/request_code_page.dart index e5dde0ec..ca9116bd 100644 --- a/lib/authentication/view/request_code_page.dart +++ b/lib/authentication/view/request_code_page.dart @@ -70,32 +70,33 @@ class _RequestCodeView extends StatelessWidget { body: SafeArea( child: BlocConsumer( listener: (context, state) { - if (state is AuthenticationFailure) { + if (state.status == AuthenticationStatus.failure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.errorMessage), + content: Text(state.errorMessage!), backgroundColor: colorScheme.error, ), ); - } else if (state is AuthenticationCodeSentSuccess) { + } else if (state.status == AuthenticationStatus.codeSentSuccess) { // Navigate to the code verification page on success, passing the email context.goNamed( isLinkingContext ? Routes.linkingVerifyCodeName : Routes.verifyCodeName, - pathParameters: {'email': state.email}, + pathParameters: {'email': state.email!}, ); } }, // BuildWhen prevents unnecessary rebuilds if only listening buildWhen: (previous, current) => - current is AuthenticationInitial || - current is AuthenticationRequestCodeLoading || - current is AuthenticationFailure, + previous.status != current.status || + previous.errorMessage != current.errorMessage || + previous.email != current.email, builder: (context, state) { - final isLoading = state is AuthenticationRequestCodeLoading; + final isLoading = + state.status == AuthenticationStatus.requestCodeLoading; return Padding( padding: const EdgeInsets.all(AppSpacing.paddingLarge), From 67b91d4be1b4a466f619940015cdc68755de67c3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:58:38 +0100 Subject: [PATCH 61/69] refactor(auth): improve import formatting - Improved code readability. - Removed unnecessary line breaks. --- .../bloc/authentication_bloc.dart | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/authentication/bloc/authentication_bloc.dart b/lib/authentication/bloc/authentication_bloc.dart index 98f47bf6..586f1963 100644 --- a/lib/authentication/bloc/authentication_bloc.dart +++ b/lib/authentication/bloc/authentication_bloc.dart @@ -3,18 +3,17 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:ht_auth_repository/ht_auth_repository.dart'; -import 'package:ht_shared/ht_shared.dart' - show - AuthenticationException, - ForbiddenException, - HtHttpException, - InvalidInputException, - NetworkException, - NotFoundException, - OperationFailedException, - ServerException, - UnauthorizedException, - User; +import 'package:ht_shared/ht_shared.dart' show + AuthenticationException, + ForbiddenException, + HtHttpException, + InvalidInputException, + NetworkException, + NotFoundException, + OperationFailedException, + ServerException, + UnauthorizedException, + User; part 'authentication_event.dart'; part 'authentication_state.dart'; From d172c21c9cebc000c731ef525af24fec0798c590 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:58:44 +0100 Subject: [PATCH 62/69] fix(auth): Improve error handling and UI - Improved error message display. - Updated loading indicator logic. - Refined UI layout and spacing. - Enhanced code readability. - Used AppTheme for color consistency. --- .../view/authentication_page.dart | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/authentication/view/authentication_page.dart b/lib/authentication/view/authentication_page.dart index e7e26b7d..b508ef6e 100644 --- a/lib/authentication/view/authentication_page.dart +++ b/lib/authentication/view/authentication_page.dart @@ -1,6 +1,3 @@ -// -// ignore_for_file: lines_longer_than_80_chars - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -8,6 +5,7 @@ import 'package:ht_dashboard/authentication/bloc/authentication_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/router/routes.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; +import 'package:ht_dashboard/shared/theme/app_theme.dart'; /// {@template authentication_page} /// Displays authentication options for the dashboard. @@ -34,25 +32,27 @@ class AuthenticationPage extends StatelessWidget { child: BlocConsumer( // Listener remains crucial for feedback (errors) listener: (context, state) { - if (state is AuthenticationFailure) { + if (state.status == AuthenticationStatus.failure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text( // Provide a more user-friendly error message if possible - state.errorMessage, + state.errorMessage!, ), backgroundColor: colorScheme.error, ), ); } // Success states (Google/Anonymous) are typically handled by - // the AppBloc listening to repository changes and triggering redirects. - // Email link success is handled in the dedicated email flow pages. + // the AppBloc listening to repository changes and triggering + // redirects. Email link success is handled in the dedicated + // email flow pages. }, builder: (context, state) { - final isLoading = state is AuthenticationLoading; + final isLoading = state.status == AuthenticationStatus.loading || + state.status == AuthenticationStatus.requestCodeLoading; return Padding( padding: const EdgeInsets.all(AppSpacing.paddingLarge), @@ -64,14 +64,15 @@ class AuthenticationPage extends StatelessWidget { children: [ // --- Icon --- Padding( - padding: const EdgeInsets.only(bottom: AppSpacing.xl), + padding: const EdgeInsets.only( + bottom: AppSpacing.xl, + ), child: Icon( Icons.newspaper, size: AppSpacing.xxl * 2, color: colorScheme.primary, ), ), - // const SizedBox(height: AppSpacing.lg), // --- Headline and Subheadline --- Text( l10n.authenticationPageHeadline, @@ -111,13 +112,11 @@ class AuthenticationPage extends StatelessWidget { const SizedBox(height: AppSpacing.lg), // --- Loading Indicator --- - if (isLoading && - state is! AuthenticationRequestCodeLoading) ...[ + if (isLoading) const Padding( padding: EdgeInsets.only(top: AppSpacing.xl), child: Center(child: CircularProgressIndicator()), ), - ], ], ), ), From cb869342e5b8fbaa5d84fa371e9f52e69dbade80 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:58:51 +0100 Subject: [PATCH 63/69] fix(auth): handle authentication failure - Improved error handling in UI. - Updated state management for clarity. - Replaced direct state checks with enum. --- lib/authentication/view/email_code_verification_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/authentication/view/email_code_verification_page.dart b/lib/authentication/view/email_code_verification_page.dart index cf4f6684..1635a597 100644 --- a/lib/authentication/view/email_code_verification_page.dart +++ b/lib/authentication/view/email_code_verification_page.dart @@ -29,12 +29,12 @@ class EmailCodeVerificationPage extends StatelessWidget { body: SafeArea( child: BlocConsumer( listener: (context, state) { - if (state is AuthenticationFailure) { + if (state.status == AuthenticationStatus.failure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(state.errorMessage), + content: Text(state.errorMessage!), backgroundColor: colorScheme.error, ), ); @@ -42,7 +42,7 @@ class EmailCodeVerificationPage extends StatelessWidget { // Successful authentication is handled by AppBloc redirecting. }, builder: (context, state) { - final isLoading = state is AuthenticationLoading; + final isLoading = state.status == AuthenticationStatus.loading; return Padding( padding: const EdgeInsets.all(AppSpacing.paddingLarge), From 61af038f6f7f7d32459374cb43c7a3863359305d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 14:59:08 +0100 Subject: [PATCH 64/69] refactor(auth): remove unused import - Removed unnecessary import of `AppTheme`. - Improved code cleanliness. --- lib/authentication/view/authentication_page.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/authentication/view/authentication_page.dart b/lib/authentication/view/authentication_page.dart index b508ef6e..a3727e12 100644 --- a/lib/authentication/view/authentication_page.dart +++ b/lib/authentication/view/authentication_page.dart @@ -5,7 +5,6 @@ import 'package:ht_dashboard/authentication/bloc/authentication_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/router/routes.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; -import 'package:ht_dashboard/shared/theme/app_theme.dart'; /// {@template authentication_page} /// Displays authentication options for the dashboard. From c51b7126380790efdaf9f09a57164989142af885 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 15:14:24 +0100 Subject: [PATCH 65/69] refactor(dashboard): update appConfig type - Changed appConfig type from AppConfig to RemoteConfig --- lib/dashboard/bloc/dashboard_state.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dashboard/bloc/dashboard_state.dart b/lib/dashboard/bloc/dashboard_state.dart index 0bd718e0..7f371997 100644 --- a/lib/dashboard/bloc/dashboard_state.dart +++ b/lib/dashboard/bloc/dashboard_state.dart @@ -27,14 +27,14 @@ final class DashboardState extends Equatable { final DashboardStatus status; final DashboardSummary? summary; - final AppConfig? appConfig; + final RemoteConfig? appConfig; final List recentHeadlines; final String? errorMessage; DashboardState copyWith({ DashboardStatus? status, DashboardSummary? summary, - AppConfig? appConfig, + RemoteConfig? appConfig, List? recentHeadlines, String? errorMessage, }) { From 653ff60cf73421a49e545ee57e37396563026664 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 15:14:37 +0100 Subject: [PATCH 66/69] refactor(dashboard): Replace AppConfig with RemoteConfig - Updated AppConfig type to RemoteConfig - Adjusted repository type accordingly - Modified data fetching logic - Improved data handling consistency - Minor code cleanup --- lib/dashboard/bloc/dashboard_bloc.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/dashboard/bloc/dashboard_bloc.dart b/lib/dashboard/bloc/dashboard_bloc.dart index 5bfda30d..7a3e6cfb 100644 --- a/lib/dashboard/bloc/dashboard_bloc.dart +++ b/lib/dashboard/bloc/dashboard_bloc.dart @@ -11,7 +11,7 @@ class DashboardBloc extends Bloc { /// {@macro dashboard_bloc} DashboardBloc({ required HtDataRepository dashboardSummaryRepository, - required HtDataRepository appConfigRepository, + required HtDataRepository appConfigRepository, required HtDataRepository headlinesRepository, }) : _dashboardSummaryRepository = dashboardSummaryRepository, _appConfigRepository = appConfigRepository, @@ -21,7 +21,7 @@ class DashboardBloc extends Bloc { } final HtDataRepository _dashboardSummaryRepository; - final HtDataRepository _appConfigRepository; + final HtDataRepository _appConfigRepository; final HtDataRepository _headlinesRepository; Future _onDashboardSummaryLoaded( @@ -39,14 +39,13 @@ class DashboardBloc extends Bloc { _dashboardSummaryRepository.read(id: 'dashboard_summary'), _appConfigRepository.read(id: 'app_config'), _headlinesRepository.readAll( - sortBy: 'createdAt', - sortOrder: SortOrder.desc, - limit: 5, + pagination: const PaginationOptions(limit: 5), + sort: const [SortOption('createdAt', SortOrder.desc)], ), ]); final summary = summaryResponse as DashboardSummary; - final appConfig = appConfigResponse as AppConfig; + final appConfig = appConfigResponse as RemoteConfig; final recentHeadlines = (recentHeadlinesResponse as PaginatedResponse).items; emit( From 100326881c8905c59e50cbaf09d03784a5a9dd43 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 15:14:47 +0100 Subject: [PATCH 67/69] refactor(dashboard): improve dashboard UI - Renamed "Headlines" to "Topics" - Updated system status display logic - Improved quick actions button labels - Replaced `RemoteAppStatus` with `AppStatus` - Updated icons for better clarity --- lib/dashboard/view/dashboard_page.dart | 61 +++++++++++++------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/lib/dashboard/view/dashboard_page.dart b/lib/dashboard/view/dashboard_page.dart index 9215bf10..418b1f2d 100644 --- a/lib/dashboard/view/dashboard_page.dart +++ b/lib/dashboard/view/dashboard_page.dart @@ -65,14 +65,14 @@ class _DashboardPageState extends State { final summaryCards = [ _SummaryCard( - icon: Icons.article_outlined, - title: l10n.totalHeadlines, - value: summary.headlineCount.toString(), + icon: Icons.category_outlined, + title: l10n.totalTopics, + value: summary.topicCount.toString(), ), _SummaryCard( icon: Icons.category_outlined, - title: l10n.totalCategories, - value: summary.categoryCount.toString(), + title: l10n.totalTopics, + value: summary.topicCount.toString(), ), _SummaryCard( icon: Icons.source_outlined, @@ -116,7 +116,7 @@ class _DashboardPageState extends State { Column( children: [ _SystemStatusCard( - status: appConfig.appOperationalStatus, + appStatus: appConfig.appStatus, ), const SizedBox(height: AppSpacing.lg), const _QuickActionsCard(), @@ -157,16 +157,16 @@ class _DashboardPageState extends State { /// A card to display the current operational status of the application. class _SystemStatusCard extends StatelessWidget { - const _SystemStatusCard({required this.status}); + const _SystemStatusCard({required this.appStatus}); - final RemoteAppStatus status; + final AppStatus appStatus; @override Widget build(BuildContext context) { final l10n = context.l10n; final theme = Theme.of(context); - final (icon, color, text) = _getStatusDetails(status, l10n, theme); + final (icon, color, text) = _getStatusDetails(appStatus, l10n, theme); return Card( child: Padding( @@ -194,29 +194,28 @@ class _SystemStatusCard extends StatelessWidget { /// Returns the appropriate icon, color, and text for a given status. (IconData, Color, String) _getStatusDetails( - RemoteAppStatus status, + AppStatus appStatus, AppLocalizations l10n, ThemeData theme, ) { - switch (status) { - case RemoteAppStatus.active: - return ( - Icons.check_circle_outline, - theme.colorScheme.primary, - l10n.appStatusActive, - ); - case RemoteAppStatus.maintenance: - return ( - Icons.warning_amber_outlined, - theme.colorScheme.tertiary, - l10n.appStatusMaintenance, - ); - case RemoteAppStatus.disabled: - return ( - Icons.cancel_outlined, - theme.colorScheme.error, - l10n.appStatusDisabled, - ); + if (appStatus.isUnderMaintenance) { + return ( + Icons.warning_amber_outlined, + theme.colorScheme.tertiary, + l10n.appStatusMaintenance, + ); + } else if (appStatus.isLatestVersionOnly) { + return ( + Icons.cancel_outlined, + theme.colorScheme.error, + l10n.appStatusDisabled, + ); + } else { + return ( + Icons.check_circle_outline, + theme.colorScheme.primary, + l10n.appStatusActive, + ); } } } @@ -246,8 +245,8 @@ class _QuickActionsCard extends StatelessWidget { const SizedBox(height: AppSpacing.sm), OutlinedButton.icon( icon: const Icon(Icons.create_new_folder_outlined), - label: Text(l10n.createCategory), - onPressed: () => context.goNamed(Routes.createCategoryName), + label: Text(l10n.createTopic), + onPressed: () => context.goNamed(Routes.createTopicName), ), const SizedBox(height: AppSpacing.sm), OutlinedButton.icon( From ee066ed59423f9def0cd50d235557e900787718f Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 15:14:53 +0100 Subject: [PATCH 68/69] docs: remove unused translation string --- lib/l10n/arb/app_en.arb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 4c913976..5e72a003 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -898,7 +898,6 @@ "@appStatusActive": { "description": "Text for the 'Active' app status" }, - "appStatusDisabled": "Disabled", "@appStatusDisabled": { "description": "Text for the 'Disabled' app status" From 6e97d360aabf4fe1ec5e68bbd59affa5d59a3dc5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 15:19:01 +0100 Subject: [PATCH 69/69] fix(settings): handle missing user settings - Added default settings on creation - Improved error handling - Added DisplaySettings, FeedDisplayPreferences - Used default values for user settings - Updated UserAppSettings model --- lib/settings/bloc/settings_bloc.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/settings/bloc/settings_bloc.dart b/lib/settings/bloc/settings_bloc.dart index 973761dd..d6222a1e 100644 --- a/lib/settings/bloc/settings_bloc.dart +++ b/lib/settings/bloc/settings_bloc.dart @@ -35,7 +35,23 @@ class SettingsBloc extends Bloc { ); emit(SettingsLoadSuccess(userAppSettings: userAppSettings)); } on NotFoundException { - final defaultSettings = UserAppSettings(id: event.userId!); + final defaultSettings = UserAppSettings( + id: event.userId!, + displaySettings: const DisplaySettings( + baseTheme: AppBaseTheme.system, + accentTheme: AppAccentTheme.defaultBlue, + fontFamily: 'SystemDefault', + textScaleFactor: AppTextScaleFactor.medium, + fontWeight: AppFontWeight.regular, + ), + language: 'en', + feedPreferences: const FeedDisplayPreferences( + headlineDensity: HeadlineDensity.standard, + headlineImageStyle: HeadlineImageStyle.largeThumbnail, + showSourceInHeadlineFeed: true, + showPublishDateInHeadlineFeed: true, + ), + ); await _userAppSettingsRepository.create(item: defaultSettings); emit(SettingsLoadSuccess(userAppSettings: defaultSettings)); } on HtHttpException catch (e) {