diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index 5e57ab6..220f875 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -82,18 +82,18 @@ class AppBloc extends Bloc { displaySettings: const DisplaySettings( baseTheme: AppBaseTheme.system, accentTheme: AppAccentTheme.defaultBlue, - fontFamily: 'SystemDefault', - textScaleFactor: AppTextScaleFactor.medium, - fontWeight: AppFontWeight.regular, - ), - language: languagesFixturesData.firstWhere( - (l) => l.code == 'en', - orElse: () => throw StateError( - 'Default language "en" not found in language fixtures.', + fontFamily: 'SystemDefault', + textScaleFactor: AppTextScaleFactor.medium, + fontWeight: AppFontWeight.regular, ), - ), - feedPreferences: const FeedDisplayPreferences( - headlineDensity: HeadlineDensity.standard, + language: languagesFixturesData.firstWhere( + (l) => l.code == 'en', + orElse: () => throw StateError( + 'Default language "en" not found in language fixtures.', + ), + ), + feedPreferences: const FeedDisplayPreferences( + headlineDensity: HeadlineDensity.standard, headlineImageStyle: HeadlineImageStyle.largeThumbnail, showSourceInHeadlineFeed: true, showPublishDateInHeadlineFeed: true, diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 26109da..657a1fc 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -16,7 +16,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/content_manageme import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/bloc/dashboard_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/router.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/throttled_fetching_service.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; import 'package:go_router/go_router.dart'; import 'package:kv_storage_service/kv_storage_service.dart'; import 'package:logging/logging.dart'; @@ -80,7 +80,9 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _countriesRepository), RepositoryProvider.value(value: _languagesRepository), RepositoryProvider.value(value: _kvStorageService), - RepositoryProvider(create: (context) => const ThrottledFetchingService()), + RepositoryProvider( + create: (context) => const ThrottledFetchingService(), + ), ], child: MultiBlocProvider( providers: [ @@ -204,7 +206,6 @@ class _AppViewState extends State<_AppView> { fontFamily: fontFamily, ); - const double kMaxAppWidth = 1000; // Local constant for max width return Center( child: Card( margin: EdgeInsets.zero, // Remove default card margin @@ -215,7 +216,9 @@ class _AppViewState extends State<_AppView> { ), // Match cardRadius from theme ), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: kMaxAppWidth), + constraints: const BoxConstraints( + maxWidth: AppConstants.kMaxAppWidth, + ), child: MaterialApp.router( debugShowCheckedModeBanner: false, routerConfig: _router, diff --git a/lib/app_configuration/view/app_configuration_page.dart b/lib/app_configuration/view/app_configuration_page.dart index 5290168..4762ad8 100644 --- a/lib/app_configuration/view/app_configuration_page.dart +++ b/lib/app_configuration/view/app_configuration_page.dart @@ -4,6 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/bloc/app_configuration_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/feed_decorator_type_l10n.dart'; import 'package:ui_kit/ui_kit.dart'; /// {@template app_configuration_page} @@ -24,17 +26,28 @@ class AppConfigurationPage extends StatefulWidget { class _AppConfigurationPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; + // Controllers for the top-level ExpansionTiles to manage their expanded state. + late List _mainTileControllers; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); + // Initialize a controller for each of the 5 top-level ExpansionTiles. + _mainTileControllers = List.generate( + 5, + (index) => ExpansionTileController(), + ); context.read().add(const AppConfigurationLoaded()); } @override void dispose() { _tabController.dispose(); + // Dispose of all ExpansionTileControllers to prevent memory leaks. + for (final controller in _mainTileControllers) { + controller.dispose(); + } super.dispose(); } @@ -145,8 +158,24 @@ class _AppConfigurationPageState extends State ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ + // Top-level ExpansionTile for User Content Limits ExpansionTile( + controller: _mainTileControllers[0], title: Text(l10n.userContentLimitsTitle), + onExpansionChanged: (isExpanded) { + if (isExpanded) { + // Collapse other main tiles when this one expands + for ( + var i = 0; + i < _mainTileControllers.length; + i++ + ) { + if (i != 0) { + _mainTileControllers[i].collapse(); + } + } + } + }, childrenPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.xxl, ), @@ -157,8 +186,24 @@ class _AppConfigurationPageState extends State ), ], ), + // Top-level ExpansionTile for Feed Decorators ExpansionTile( + controller: _mainTileControllers[1], title: Text(l10n.feedDecoratorsTitle), + onExpansionChanged: (isExpanded) { + if (isExpanded) { + // Collapse other main tiles when this one expands + for ( + var i = 0; + i < _mainTileControllers.length; + i++ + ) { + if (i != 1) { + _mainTileControllers[i].collapse(); + } + } + } + }, childrenPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.xxl, ), @@ -171,8 +216,24 @@ class _AppConfigurationPageState extends State ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ + // Top-level ExpansionTile for Ad Settings ExpansionTile( + controller: _mainTileControllers[2], title: Text(l10n.adSettingsTitle), + onExpansionChanged: (isExpanded) { + if (isExpanded) { + // Collapse other main tiles when this one expands + for ( + var i = 0; + i < _mainTileControllers.length; + i++ + ) { + if (i != 2) { + _mainTileControllers[i].collapse(); + } + } + } + }, childrenPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.xxl, ), @@ -183,9 +244,59 @@ class _AppConfigurationPageState extends State ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ - _buildMaintenanceSection(context, remoteConfig), + // Top-level ExpansionTile for Maintenance Section + ExpansionTile( + controller: _mainTileControllers[3], + title: Text(l10n.maintenanceModeTitle), + onExpansionChanged: (isExpanded) { + if (isExpanded) { + // Collapse other main tiles when this one expands + for ( + var i = 0; + i < _mainTileControllers.length; + i++ + ) { + if (i != 3) { + _mainTileControllers[i].collapse(); + } + } + } + }, + childrenPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xxl, + vertical: AppSpacing.md, + ), + children: [ + _buildMaintenanceSection(context, remoteConfig), + ], + ), const SizedBox(height: AppSpacing.lg), - _buildForceUpdateSection(context, remoteConfig), + // Top-level ExpansionTile for Force Update Section + ExpansionTile( + controller: _mainTileControllers[4], + title: Text(l10n.forceUpdateTitle), + onExpansionChanged: (isExpanded) { + if (isExpanded) { + // Collapse other main tiles when this one expands + for ( + var i = 0; + i < _mainTileControllers.length; + i++ + ) { + if (i != 4) { + _mainTileControllers[i].collapse(); + } + } + } + }, + childrenPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xxl, + vertical: AppSpacing.md, + ), + children: [ + _buildForceUpdateSection(context, remoteConfig), + ], + ), ], ), ], @@ -366,7 +477,6 @@ class _AppConfigurationPageState extends State RemoteConfig remoteConfig, ) { final l10n = AppLocalizationsX(context).l10n; - final decoratorConfigs = remoteConfig.feedDecoratorConfig.entries.toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -374,26 +484,49 @@ class _AppConfigurationPageState extends State Text( l10n.feedDecoratorsDescription, 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), - for (final decoratorEntry in decoratorConfigs) + for (final decoratorType in FeedDecoratorType.values) ExpansionTile( title: Text( - decoratorEntry.key.name.toUpperCase(), + decoratorType.l10n(context), ), childrenPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.xxl, ), children: [ _FeedDecoratorForm( - decoratorType: decoratorEntry.key, - remoteConfig: remoteConfig, + decoratorType: decoratorType, + remoteConfig: remoteConfig.copyWith( + feedDecoratorConfig: + Map.from(remoteConfig.feedDecoratorConfig)..putIfAbsent( + decoratorType, + () => FeedDecoratorConfig( + category: + decoratorType == + FeedDecoratorType.suggestedTopics || + decoratorType == + FeedDecoratorType.suggestedSources + ? FeedDecoratorCategory.contentCollection + : FeedDecoratorCategory.callToAction, + enabled: false, + visibleTo: const {}, + itemsToDisplay: + decoratorType == + FeedDecoratorType.suggestedTopics || + decoratorType == + FeedDecoratorType.suggestedSources + ? 0 + : null, + ), + ), + ), onConfigChanged: (newConfig) { context.read().add( - AppConfigurationFieldChanged(remoteConfig: newConfig), - ); + AppConfigurationFieldChanged(remoteConfig: newConfig), + ); }, buildIntField: _buildIntField, ), @@ -743,26 +876,54 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { final config = widget.remoteConfig.userPreferenceConfig; switch (widget.userRole) { case AppUserRole.guestUser: - _followedItemsLimitController = TextEditingController( - text: config.guestFollowedItemsLimit.toString(), - ); - _savedHeadlinesLimitController = TextEditingController( - text: config.guestSavedHeadlinesLimit.toString(), - ); + _followedItemsLimitController = + TextEditingController( + text: config.guestFollowedItemsLimit.toString(), + ) + ..selection = TextSelection.collapsed( + offset: config.guestFollowedItemsLimit.toString().length, + ); + _savedHeadlinesLimitController = + TextEditingController( + text: config.guestSavedHeadlinesLimit.toString(), + ) + ..selection = TextSelection.collapsed( + offset: config.guestSavedHeadlinesLimit.toString().length, + ); case AppUserRole.standardUser: - _followedItemsLimitController = TextEditingController( - text: config.authenticatedFollowedItemsLimit.toString(), - ); - _savedHeadlinesLimitController = TextEditingController( - text: config.authenticatedSavedHeadlinesLimit.toString(), - ); + _followedItemsLimitController = + TextEditingController( + text: config.authenticatedFollowedItemsLimit.toString(), + ) + ..selection = TextSelection.collapsed( + offset: config.authenticatedFollowedItemsLimit + .toString() + .length, + ); + _savedHeadlinesLimitController = + TextEditingController( + text: config.authenticatedSavedHeadlinesLimit.toString(), + ) + ..selection = TextSelection.collapsed( + offset: config.authenticatedSavedHeadlinesLimit + .toString() + .length, + ); case AppUserRole.premiumUser: - _followedItemsLimitController = TextEditingController( - text: config.premiumFollowedItemsLimit.toString(), - ); - _savedHeadlinesLimitController = TextEditingController( - text: config.premiumSavedHeadlinesLimit.toString(), - ); + _followedItemsLimitController = + TextEditingController( + text: config.premiumFollowedItemsLimit.toString(), + ) + ..selection = TextSelection.collapsed( + offset: config.premiumFollowedItemsLimit.toString().length, + ); + _savedHeadlinesLimitController = + TextEditingController( + text: config.premiumSavedHeadlinesLimit.toString(), + ) + ..selection = TextSelection.collapsed( + offset: config.premiumSavedHeadlinesLimit.toString().length, + ); } } @@ -772,20 +933,38 @@ class _UserPreferenceLimitsFormState extends State<_UserPreferenceLimitsForm> { case AppUserRole.guestUser: _followedItemsLimitController.text = config.guestFollowedItemsLimit .toString(); + _followedItemsLimitController.selection = TextSelection.collapsed( + offset: _followedItemsLimitController.text.length, + ); _savedHeadlinesLimitController.text = config.guestSavedHeadlinesLimit .toString(); + _savedHeadlinesLimitController.selection = TextSelection.collapsed( + offset: _savedHeadlinesLimitController.text.length, + ); case AppUserRole.standardUser: _followedItemsLimitController.text = config .authenticatedFollowedItemsLimit .toString(); + _followedItemsLimitController.selection = TextSelection.collapsed( + offset: _followedItemsLimitController.text.length, + ); _savedHeadlinesLimitController.text = config .authenticatedSavedHeadlinesLimit .toString(); + _savedHeadlinesLimitController.selection = TextSelection.collapsed( + offset: _savedHeadlinesLimitController.text.length, + ); case AppUserRole.premiumUser: _followedItemsLimitController.text = config.premiumFollowedItemsLimit .toString(); + _followedItemsLimitController.selection = TextSelection.collapsed( + offset: _followedItemsLimitController.text.length, + ); _savedHeadlinesLimitController.text = config.premiumSavedHeadlinesLimit .toString(); + _savedHeadlinesLimitController.selection = TextSelection.collapsed( + offset: _savedHeadlinesLimitController.text.length, + ); } } @@ -954,7 +1133,8 @@ class _FeedDecoratorForm extends StatefulWidget { required int value, required ValueChanged onChanged, TextEditingController? controller, - }) buildIntField; + }) + buildIntField; @override State<_FeedDecoratorForm> createState() => _FeedDecoratorFormState(); @@ -982,16 +1162,30 @@ class _FeedDecoratorFormState extends State<_FeedDecoratorForm> { void _initializeControllers() { final decoratorConfig = widget.remoteConfig.feedDecoratorConfig[widget.decoratorType]!; - _itemsToDisplayController = TextEditingController( - text: decoratorConfig.itemsToDisplay?.toString() ?? '', - ); + _itemsToDisplayController = + TextEditingController( + text: decoratorConfig.itemsToDisplay?.toString() ?? '', + ) + ..selection = TextSelection.collapsed( + offset: decoratorConfig.itemsToDisplay?.toString().length ?? 0, + ); _roleControllers = { for (final role in AppUserRole.values) - role: TextEditingController( - text: decoratorConfig.visibleTo[role]?.daysBetweenViews.toString() ?? - '', - ), + role: + TextEditingController( + text: + decoratorConfig.visibleTo[role]?.daysBetweenViews + .toString() ?? + '', + ) + ..selection = TextSelection.collapsed( + offset: + decoratorConfig.visibleTo[role]?.daysBetweenViews + .toString() + .length ?? + 0, + ), }; } @@ -1000,9 +1194,15 @@ class _FeedDecoratorFormState extends State<_FeedDecoratorForm> { widget.remoteConfig.feedDecoratorConfig[widget.decoratorType]!; _itemsToDisplayController.text = decoratorConfig.itemsToDisplay?.toString() ?? ''; + _itemsToDisplayController.selection = TextSelection.collapsed( + offset: _itemsToDisplayController.text.length, + ); for (final role in AppUserRole.values) { _roleControllers[role]?.text = decoratorConfig.visibleTo[role]?.daysBetweenViews.toString() ?? ''; + _roleControllers[role]?.selection = TextSelection.collapsed( + offset: _roleControllers[role]?.text.length ?? 0, + ); } } @@ -1030,8 +1230,8 @@ class _FeedDecoratorFormState extends State<_FeedDecoratorForm> { final newDecoratorConfig = decoratorConfig.copyWith(enabled: value); final newFeedDecoratorConfig = Map.from( - widget.remoteConfig.feedDecoratorConfig, - )..[widget.decoratorType] = newDecoratorConfig; + widget.remoteConfig.feedDecoratorConfig, + )..[widget.decoratorType] = newDecoratorConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( feedDecoratorConfig: newFeedDecoratorConfig, @@ -1039,20 +1239,20 @@ class _FeedDecoratorFormState extends State<_FeedDecoratorForm> { ); }, ), - if (decoratorConfig.category == - FeedDecoratorCategory.contentCollection) + if (decoratorConfig.category == FeedDecoratorCategory.contentCollection) widget.buildIntField( context, label: l10n.itemsToDisplayLabel, description: l10n.itemsToDisplayDescription, value: decoratorConfig.itemsToDisplay ?? 0, onChanged: (value) { - final newDecoratorConfig = - decoratorConfig.copyWith(itemsToDisplay: value); + final newDecoratorConfig = decoratorConfig.copyWith( + itemsToDisplay: value, + ); final newFeedDecoratorConfig = Map.from( - widget.remoteConfig.feedDecoratorConfig, - )..[widget.decoratorType] = newDecoratorConfig; + widget.remoteConfig.feedDecoratorConfig, + )..[widget.decoratorType] = newDecoratorConfig; widget.onConfigChanged( widget.remoteConfig.copyWith( feedDecoratorConfig: newFeedDecoratorConfig, @@ -1065,63 +1265,78 @@ class _FeedDecoratorFormState extends State<_FeedDecoratorForm> { title: Text(l10n.roleSpecificSettingsTitle), children: AppUserRole.values.map((role) { final roleConfig = decoratorConfig.visibleTo[role]; - return CheckboxListTile( - title: Text(role.name), - value: roleConfig != null, - onChanged: (value) { - final newVisibleTo = - Map.from( - decoratorConfig.visibleTo, - ); - if (value == true) { - newVisibleTo[role] = - const FeedDecoratorRoleConfig(daysBetweenViews: 7); - } else { - newVisibleTo.remove(role); - } - final newDecoratorConfig = - decoratorConfig.copyWith(visibleTo: newVisibleTo); - final newFeedDecoratorConfig = - Map.from( - widget.remoteConfig.feedDecoratorConfig, - )..[widget.decoratorType] = newDecoratorConfig; - widget.onConfigChanged( - widget.remoteConfig.copyWith( - feedDecoratorConfig: newFeedDecoratorConfig, - ), - ); - }, - secondary: SizedBox( - width: 100, - child: widget.buildIntField( - context, - label: l10n.daysBetweenViewsLabel, - description: '', - value: roleConfig?.daysBetweenViews ?? 0, - onChanged: (value) { - if (roleConfig != null) { - final newRoleConfig = - roleConfig.copyWith(daysBetweenViews: value); - final newVisibleTo = - Map.from( - decoratorConfig.visibleTo, - )..[role] = newRoleConfig; - final newDecoratorConfig = - decoratorConfig.copyWith(visibleTo: newVisibleTo); - final newFeedDecoratorConfig = - Map.from( - widget.remoteConfig.feedDecoratorConfig, - )..[widget.decoratorType] = newDecoratorConfig; - widget.onConfigChanged( - widget.remoteConfig.copyWith( - feedDecoratorConfig: newFeedDecoratorConfig, - ), - ); - } - }, - controller: _roleControllers[role], + return Column( + children: [ + CheckboxListTile( + title: Text(role.l10n(context)), + value: roleConfig != null, + onChanged: + widget.decoratorType == FeedDecoratorType.linkAccount && + (role == AppUserRole.standardUser || + role == AppUserRole.premiumUser) + ? null // Disable for standard and premium users for linkAccount + : (value) { + final newVisibleTo = + Map.from( + decoratorConfig.visibleTo, + ); + if (value == true) { + newVisibleTo[role] = const FeedDecoratorRoleConfig( + daysBetweenViews: 7, + ); + } else { + newVisibleTo.remove(role); + } + final newDecoratorConfig = decoratorConfig.copyWith( + visibleTo: newVisibleTo, + ); + final newFeedDecoratorConfig = + Map.from( + widget.remoteConfig.feedDecoratorConfig, + )..[widget.decoratorType] = newDecoratorConfig; + widget.onConfigChanged( + widget.remoteConfig.copyWith( + feedDecoratorConfig: newFeedDecoratorConfig, + ), + ); + }, ), - ), + if (roleConfig != null) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.sm, + ), + child: widget.buildIntField( + context, + label: l10n.daysBetweenViewsLabel, + description: l10n.daysBetweenViewsDescription, + value: roleConfig.daysBetweenViews, + onChanged: (value) { + final newRoleConfig = roleConfig.copyWith( + daysBetweenViews: value, + ); + final newVisibleTo = + Map.from( + decoratorConfig.visibleTo, + )..[role] = newRoleConfig; + final newDecoratorConfig = decoratorConfig.copyWith( + visibleTo: newVisibleTo, + ); + final newFeedDecoratorConfig = + Map.from( + widget.remoteConfig.feedDecoratorConfig, + )..[widget.decoratorType] = newDecoratorConfig; + widget.onConfigChanged( + widget.remoteConfig.copyWith( + feedDecoratorConfig: newFeedDecoratorConfig, + ), + ); + }, + controller: _roleControllers[role], + ), + ), + ], ); }).toList(), ), @@ -1179,43 +1394,86 @@ class _AdConfigFormState extends State<_AdConfigForm> { final adConfig = widget.remoteConfig.adConfig; switch (widget.userRole) { case AppUserRole.guestUser: - _adFrequencyController = TextEditingController( - text: adConfig.guestAdFrequency.toString(), - ); - _adPlacementIntervalController = TextEditingController( - text: adConfig.guestAdPlacementInterval.toString(), - ); + _adFrequencyController = + TextEditingController( + text: adConfig.guestAdFrequency.toString(), + ) + ..selection = TextSelection.collapsed( + offset: adConfig.guestAdFrequency.toString().length, + ); + _adPlacementIntervalController = + TextEditingController( + text: adConfig.guestAdPlacementInterval.toString(), + ) + ..selection = TextSelection.collapsed( + offset: adConfig.guestAdPlacementInterval.toString().length, + ); _articlesToReadBeforeShowingInterstitialAdsController = TextEditingController( - text: adConfig.guestArticlesToReadBeforeShowingInterstitialAds - .toString(), - ); + text: adConfig.guestArticlesToReadBeforeShowingInterstitialAds + .toString(), + ) + ..selection = TextSelection.collapsed( + offset: adConfig.guestArticlesToReadBeforeShowingInterstitialAds + .toString() + .length, + ); case AppUserRole.standardUser: - _adFrequencyController = TextEditingController( - text: adConfig.authenticatedAdFrequency.toString(), - ); - _adPlacementIntervalController = TextEditingController( - text: adConfig.authenticatedAdPlacementInterval.toString(), - ); + _adFrequencyController = + TextEditingController( + text: adConfig.authenticatedAdFrequency.toString(), + ) + ..selection = TextSelection.collapsed( + offset: adConfig.authenticatedAdFrequency.toString().length, + ); + _adPlacementIntervalController = + TextEditingController( + text: adConfig.authenticatedAdPlacementInterval.toString(), + ) + ..selection = TextSelection.collapsed( + offset: adConfig.authenticatedAdPlacementInterval + .toString() + .length, + ); _articlesToReadBeforeShowingInterstitialAdsController = TextEditingController( - text: adConfig - .standardUserArticlesToReadBeforeShowingInterstitialAds - .toString(), - ); + text: adConfig + .standardUserArticlesToReadBeforeShowingInterstitialAds + .toString(), + ) + ..selection = TextSelection.collapsed( + offset: adConfig + .standardUserArticlesToReadBeforeShowingInterstitialAds + .toString() + .length, + ); case AppUserRole.premiumUser: - _adFrequencyController = TextEditingController( - text: adConfig.premiumAdFrequency.toString(), - ); - _adPlacementIntervalController = TextEditingController( - text: adConfig.premiumAdPlacementInterval.toString(), - ); + _adFrequencyController = + TextEditingController( + text: adConfig.premiumAdFrequency.toString(), + ) + ..selection = TextSelection.collapsed( + offset: adConfig.premiumAdFrequency.toString().length, + ); + _adPlacementIntervalController = + TextEditingController( + text: adConfig.premiumAdPlacementInterval.toString(), + ) + ..selection = TextSelection.collapsed( + offset: adConfig.premiumAdPlacementInterval.toString().length, + ); _articlesToReadBeforeShowingInterstitialAdsController = TextEditingController( - text: adConfig - .premiumUserArticlesToReadBeforeShowingInterstitialAds - .toString(), - ); + text: adConfig + .premiumUserArticlesToReadBeforeShowingInterstitialAds + .toString(), + ) + ..selection = TextSelection.collapsed( + offset: adConfig + .premiumUserArticlesToReadBeforeShowingInterstitialAds + .toString() + .length, + ); } } @@ -1224,28 +1482,61 @@ class _AdConfigFormState extends State<_AdConfigForm> { switch (widget.userRole) { case AppUserRole.guestUser: _adFrequencyController.text = adConfig.guestAdFrequency.toString(); + _adFrequencyController.selection = TextSelection.collapsed( + offset: _adFrequencyController.text.length, + ); _adPlacementIntervalController.text = adConfig.guestAdPlacementInterval .toString(); + _adPlacementIntervalController.selection = TextSelection.collapsed( + offset: _adPlacementIntervalController.text.length, + ); _articlesToReadBeforeShowingInterstitialAdsController.text = adConfig .guestArticlesToReadBeforeShowingInterstitialAds .toString(); + _articlesToReadBeforeShowingInterstitialAdsController + .selection = TextSelection.collapsed( + offset: + _articlesToReadBeforeShowingInterstitialAdsController.text.length, + ); case AppUserRole.standardUser: _adFrequencyController.text = adConfig.authenticatedAdFrequency .toString(); + _adFrequencyController.selection = TextSelection.collapsed( + offset: _adFrequencyController.text.length, + ); _adPlacementIntervalController.text = adConfig .authenticatedAdPlacementInterval .toString(); + _adPlacementIntervalController.selection = TextSelection.collapsed( + offset: _adPlacementIntervalController.text.length, + ); _articlesToReadBeforeShowingInterstitialAdsController.text = adConfig .standardUserArticlesToReadBeforeShowingInterstitialAds .toString(); + _articlesToReadBeforeShowingInterstitialAdsController + .selection = TextSelection.collapsed( + offset: + _articlesToReadBeforeShowingInterstitialAdsController.text.length, + ); case AppUserRole.premiumUser: _adFrequencyController.text = adConfig.premiumAdFrequency.toString(); + _adFrequencyController.selection = TextSelection.collapsed( + offset: _adFrequencyController.text.length, + ); _adPlacementIntervalController.text = adConfig .premiumAdPlacementInterval .toString(); + _adPlacementIntervalController.selection = TextSelection.collapsed( + offset: _adPlacementIntervalController.text.length, + ); _articlesToReadBeforeShowingInterstitialAdsController.text = adConfig .premiumUserArticlesToReadBeforeShowingInterstitialAds .toString(); + _articlesToReadBeforeShowingInterstitialAdsController + .selection = TextSelection.collapsed( + offset: + _articlesToReadBeforeShowingInterstitialAdsController.text.length, + ); } } diff --git a/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart b/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart index 63a7ab3..a778bd5 100644 --- a/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart +++ b/lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart @@ -169,13 +169,15 @@ class ArchivedHeadlinesBloc final updatedHeadlines = List.from(state.headlines) ..insert( state.headlines.indexWhere( - (h) => - h.updatedAt.isBefore(state.lastDeletedHeadline!.updatedAt), - ) != - -1 + (h) => h.updatedAt.isBefore( + state.lastDeletedHeadline!.updatedAt, + ), + ) != + -1 ? state.headlines.indexWhere( - (h) => - h.updatedAt.isBefore(state.lastDeletedHeadline!.updatedAt), + (h) => h.updatedAt.isBefore( + state.lastDeletedHeadline!.updatedAt, + ), ) : state.headlines.length, state.lastDeletedHeadline!, diff --git a/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart b/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart index b91404a..3ad27f9 100644 --- a/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart +++ b/lib/content_management/bloc/archived_headlines/archived_headlines_state.dart @@ -50,12 +50,12 @@ class ArchivedHeadlinesState extends Equatable { @override List get props => [ - status, - headlines, - cursor, - hasMore, - exception, - restoredHeadline, - lastDeletedHeadline, - ]; + status, + headlines, + cursor, + hasMore, + exception, + restoredHeadline, + lastDeletedHeadline, + ]; } diff --git a/lib/content_management/bloc/content_management_bloc.dart b/lib/content_management/bloc/content_management_bloc.dart index d6217ef..3de7e0e 100644 --- a/lib/content_management/bloc/content_management_bloc.dart +++ b/lib/content_management/bloc/content_management_bloc.dart @@ -28,13 +28,13 @@ class ContentManagementBloc required DataRepository countriesRepository, required DataRepository languagesRepository, required ThrottledFetchingService fetchingService, - }) : _headlinesRepository = headlinesRepository, - _topicsRepository = topicsRepository, - _sourcesRepository = sourcesRepository, - _countriesRepository = countriesRepository, - _languagesRepository = languagesRepository, - _fetchingService = fetchingService, - super(const ContentManagementState()) { + }) : _headlinesRepository = headlinesRepository, + _topicsRepository = topicsRepository, + _sourcesRepository = sourcesRepository, + _countriesRepository = countriesRepository, + _languagesRepository = languagesRepository, + _fetchingService = fetchingService, + super(const ContentManagementState()) { on(_onSharedDataRequested); on(_onContentManagementTabChanged); on(_onLoadHeadlinesRequested); 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 1af9167..53f6944 100644 --- a/lib/content_management/bloc/create_source/create_source_bloc.dart +++ b/lib/content_management/bloc/create_source/create_source_bloc.dart @@ -136,6 +136,8 @@ class CreateSourceBloc extends Bloc { CreateSourceDataUpdated event, Emitter emit, ) { - emit(state.copyWith(countries: event.countries, languages: event.languages)); + emit( + state.copyWith(countries: event.countries, languages: event.languages), + ); } } 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 961e095..d6126b0 100644 --- a/lib/content_management/bloc/edit_source/edit_source_bloc.dart +++ b/lib/content_management/bloc/edit_source/edit_source_bloc.dart @@ -201,6 +201,8 @@ class EditSourceBloc extends Bloc { EditSourceDataUpdated event, Emitter emit, ) { - emit(state.copyWith(countries: event.countries, languages: event.languages)); + emit( + state.copyWith(countries: event.countries, languages: event.languages), + ); } } diff --git a/lib/content_management/view/archived_headlines_page.dart b/lib/content_management/view/archived_headlines_page.dart index 1e8ae36..c10e4ef 100644 --- a/lib/content_management/view/archived_headlines_page.dart +++ b/lib/content_management/view/archived_headlines_page.dart @@ -48,8 +48,9 @@ class _ArchivedHeadlinesView extends StatelessWidget { ); } if (state.lastDeletedHeadline != null) { - final truncatedTitle = - state.lastDeletedHeadline!.title.truncate(30); + final truncatedTitle = state.lastDeletedHeadline!.title.truncate( + 30, + ); ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( @@ -60,9 +61,9 @@ class _ArchivedHeadlinesView extends StatelessWidget { action: SnackBarAction( label: l10n.undo, onPressed: () { - context - .read() - .add(const UndoDeleteHeadlineRequested()); + context.read().add( + const UndoDeleteHeadlineRequested(), + ); }, ), ), diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d237ee0..00ef42c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1807,6 +1807,66 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Days Between Views'** String get daysBetweenViewsLabel; + + /// Description for days between views input for feed decorators + /// + /// In en, this message translates to: + /// **'This setting determines the minimum number of days that must pass before a decorator can be shown again to a user, provided the associated task has not yet been completed.'** + String get daysBetweenViewsDescription; + + /// Localized name for FeedDecoratorType.linkAccount + /// + /// In en, this message translates to: + /// **'Link Account'** + String get feedDecoratorTypeLinkAccount; + + /// Localized name for FeedDecoratorType.upgrade + /// + /// In en, this message translates to: + /// **'Upgrade to Premium'** + String get feedDecoratorTypeUpgrade; + + /// Localized name for FeedDecoratorType.rateApp + /// + /// In en, this message translates to: + /// **'Rate App'** + String get feedDecoratorTypeRateApp; + + /// Localized name for FeedDecoratorType.enableNotifications + /// + /// In en, this message translates to: + /// **'Enable Notifications'** + String get feedDecoratorTypeEnableNotifications; + + /// Localized name for FeedDecoratorType.suggestedTopics + /// + /// In en, this message translates to: + /// **'Suggested Topics'** + String get feedDecoratorTypeSuggestedTopics; + + /// Localized name for FeedDecoratorType.suggestedSources + /// + /// In en, this message translates to: + /// **'Suggested Sources'** + String get feedDecoratorTypeSuggestedSources; + + /// Localized name for AppUserRole.guestUser + /// + /// In en, this message translates to: + /// **'Guest User'** + String get guestUserRole; + + /// Localized name for AppUserRole.standardUser + /// + /// In en, this message translates to: + /// **'Standard User'** + String get standardUserRole; + + /// Localized name for AppUserRole.premiumUser + /// + /// In en, this message translates to: + /// **'Premium User'** + String get premiumUserRole; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index c99af05..8a9c60d 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -953,4 +953,35 @@ class AppLocalizationsAr extends AppLocalizations { @override String get daysBetweenViewsLabel => 'الأيام بين المشاهدات'; + + @override + String get daysBetweenViewsDescription => + 'يحدد هذا الإعداد الحد الأدنى لعدد الأيام التي يجب أن تمر قبل أن يمكن عرض هذه الزينة مرة أخرى للمستخدم، شريطة ألا يكون قد أكمل المهمة المرتبطة بها بعد.'; + + @override + String get feedDecoratorTypeLinkAccount => 'ربط الحساب'; + + @override + String get feedDecoratorTypeUpgrade => 'الترقية إلى مميز'; + + @override + String get feedDecoratorTypeRateApp => 'تقييم التطبيق'; + + @override + String get feedDecoratorTypeEnableNotifications => 'تفعيل الإشعارات'; + + @override + String get feedDecoratorTypeSuggestedTopics => 'مواضيع مقترحة'; + + @override + String get feedDecoratorTypeSuggestedSources => 'مصادر مقترحة'; + + @override + String get guestUserRole => 'مستخدم ضيف'; + + @override + String get standardUserRole => 'مستخدم عادي'; + + @override + String get premiumUserRole => 'مستخدم مميز'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4b2bc8c..ca2f782 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -952,4 +952,35 @@ class AppLocalizationsEn extends AppLocalizations { @override String get daysBetweenViewsLabel => 'Days Between Views'; + + @override + String get daysBetweenViewsDescription => + 'This setting determines the minimum number of days that must pass before a decorator can be shown again to a user, provided the associated task has not yet been completed.'; + + @override + String get feedDecoratorTypeLinkAccount => 'Link Account'; + + @override + String get feedDecoratorTypeUpgrade => 'Upgrade to Premium'; + + @override + String get feedDecoratorTypeRateApp => 'Rate App'; + + @override + String get feedDecoratorTypeEnableNotifications => 'Enable Notifications'; + + @override + String get feedDecoratorTypeSuggestedTopics => 'Suggested Topics'; + + @override + String get feedDecoratorTypeSuggestedSources => 'Suggested Sources'; + + @override + String get guestUserRole => 'Guest User'; + + @override + String get standardUserRole => 'Standard User'; + + @override + String get premiumUserRole => 'Premium User'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index ce7f73c..22c1ab8 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1187,5 +1187,45 @@ "daysBetweenViewsLabel": "الأيام بين المشاهدات", "@daysBetweenViewsLabel": { "description": "تسمية حقل الأيام بين المشاهدات" + }, + "daysBetweenViewsDescription": "يحدد هذا الإعداد الحد الأدنى لعدد الأيام التي يجب أن تمر قبل أن يمكن عرض هذه الزينة مرة أخرى للمستخدم، شريطة ألا يكون قد أكمل المهمة المرتبطة بها بعد.", + "@daysBetweenViewsDescription": { + "description": "وصف حقل الأيام بين المشاهدات لزينة الموجز" + }, + "feedDecoratorTypeLinkAccount": "ربط الحساب", + "@feedDecoratorTypeLinkAccount": { + "description": "الاسم المترجم لـ FeedDecoratorType.linkAccount" + }, + "feedDecoratorTypeUpgrade": "الترقية إلى مميز", + "@feedDecoratorTypeUpgrade": { + "description": "الاسم المترجم لـ FeedDecoratorType.upgrade" + }, + "feedDecoratorTypeRateApp": "تقييم التطبيق", + "@feedDecoratorTypeRateApp": { + "description": "الاسم المترجم لـ FeedDecoratorType.rateApp" + }, + "feedDecoratorTypeEnableNotifications": "تفعيل الإشعارات", + "@feedDecoratorTypeEnableNotifications": { + "description": "الاسم المترجم لـ FeedDecoratorType.enableNotifications" + }, + "feedDecoratorTypeSuggestedTopics": "مواضيع مقترحة", + "@feedDecoratorTypeSuggestedTopics": { + "description": "الاسم المترجم لـ FeedDecoratorType.suggestedTopics" + }, + "feedDecoratorTypeSuggestedSources": "مصادر مقترحة", + "@feedDecoratorTypeSuggestedSources": { + "description": "الاسم المترجم لـ FeedDecoratorType.suggestedSources" + }, + "guestUserRole": "مستخدم ضيف", + "@guestUserRole": { + "description": "الاسم المترجم لـ AppUserRole.guestUser" + }, + "standardUserRole": "مستخدم عادي", + "@standardUserRole": { + "description": "الاسم المترجم لـ AppUserRole.standardUser" + }, + "premiumUserRole": "مستخدم مميز", + "@premiumUserRole": { + "description": "الاسم المترجم لـ AppUserRole.premiumUser" } } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7635d49..2655a55 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1182,5 +1182,45 @@ "daysBetweenViewsLabel": "Days Between Views", "@daysBetweenViewsLabel": { "description": "Label for days between views input" + }, + "daysBetweenViewsDescription": "This setting determines the minimum number of days that must pass before a decorator can be shown again to a user, provided the associated task has not yet been completed.", + "@daysBetweenViewsDescription": { + "description": "Description for days between views input for feed decorators" + }, + "feedDecoratorTypeLinkAccount": "Link Account", + "@feedDecoratorTypeLinkAccount": { + "description": "Localized name for FeedDecoratorType.linkAccount" + }, + "feedDecoratorTypeUpgrade": "Upgrade to Premium", + "@feedDecoratorTypeUpgrade": { + "description": "Localized name for FeedDecoratorType.upgrade" + }, + "feedDecoratorTypeRateApp": "Rate App", + "@feedDecoratorTypeRateApp": { + "description": "Localized name for FeedDecoratorType.rateApp" + }, + "feedDecoratorTypeEnableNotifications": "Enable Notifications", + "@feedDecoratorTypeEnableNotifications": { + "description": "Localized name for FeedDecoratorType.enableNotifications" + }, + "feedDecoratorTypeSuggestedTopics": "Suggested Topics", + "@feedDecoratorTypeSuggestedTopics": { + "description": "Localized name for FeedDecoratorType.suggestedTopics" + }, + "feedDecoratorTypeSuggestedSources": "Suggested Sources", + "@feedDecoratorTypeSuggestedSources": { + "description": "Localized name for FeedDecoratorType.suggestedSources" + }, + "guestUserRole": "Guest User", + "@guestUserRole": { + "description": "Localized name for AppUserRole.guestUser" + }, + "standardUserRole": "Standard User", + "@standardUserRole": { + "description": "Localized name for AppUserRole.standardUser" + }, + "premiumUserRole": "Premium User", + "@premiumUserRole": { + "description": "Localized name for AppUserRole.premiumUser" } } diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index d5ce7a4..b1982be 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -27,9 +27,33 @@ class SettingsPage extends StatelessWidget { } } -class _SettingsView extends StatelessWidget { +class _SettingsView extends StatefulWidget { const _SettingsView(); + @override + State<_SettingsView> createState() => _SettingsViewState(); +} + +class _SettingsViewState extends State<_SettingsView> { + late List _mainTileControllers; + + @override + void initState() { + super.initState(); + _mainTileControllers = List.generate( + 2, + (index) => ExpansionTileController(), + ); + } + + @override + void dispose() { + for (final controller in _mainTileControllers) { + controller.dispose(); + } + super.dispose(); + } + @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -112,7 +136,17 @@ class _SettingsView extends StatelessWidget { padding: const EdgeInsets.all(AppSpacing.lg), children: [ ExpansionTile( + controller: _mainTileControllers[0], title: Text(l10n.appearanceSettingsLabel), + onExpansionChanged: (isExpanded) { + if (isExpanded) { + for (var i = 0; i < _mainTileControllers.length; i++) { + if (i != 0) { + _mainTileControllers[i].collapse(); + } + } + } + }, childrenPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.xxl, ), @@ -263,7 +297,17 @@ class _SettingsView extends StatelessWidget { ], ), ExpansionTile( + controller: _mainTileControllers[1], title: Text(l10n.languageSettingsLabel), + onExpansionChanged: (isExpanded) { + if (isExpanded) { + for (var i = 0; i < _mainTileControllers.length; i++) { + if (i != 1) { + _mainTileControllers[i].collapse(); + } + } + } + }, childrenPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.xxl, ), @@ -418,12 +462,17 @@ class _LanguageSelectionList extends StatelessWidget { /// The localized strings for the application. final AppLocalizations l10n; + /// The list of supported languages for the application. + static final List _supportedLanguages = languagesFixturesData + .where((lang) => lang.code == 'en' || lang.code == 'ar') + .toList(); + @override Widget build(BuildContext context) { return ListView.builder( - itemCount: languagesFixturesData.length, + itemCount: _supportedLanguages.length, itemBuilder: (context, index) { - final language = languagesFixturesData[index]; + final language = _supportedLanguages[index]; final isSelected = language == currentLanguage; return ListTile( title: Text( @@ -436,8 +485,8 @@ class _LanguageSelectionList extends StatelessWidget { onTap: () { if (!isSelected) { context.read().add( - SettingsLanguageChanged(language), - ); + SettingsLanguageChanged(language), + ); } }, ); diff --git a/lib/shared/constants/app_constants.dart b/lib/shared/constants/app_constants.dart new file mode 100644 index 0000000..a43a1cd --- /dev/null +++ b/lib/shared/constants/app_constants.dart @@ -0,0 +1,5 @@ +/// Defines application-wide constants. +abstract final class AppConstants { + /// The maximum width the application should occupy on large screens. + static const double kMaxAppWidth = 1000; +} diff --git a/lib/shared/constants/constants.dart b/lib/shared/constants/constants.dart new file mode 100644 index 0000000..399560b --- /dev/null +++ b/lib/shared/constants/constants.dart @@ -0,0 +1 @@ +export 'app_constants.dart'; diff --git a/lib/shared/extensions/app_user_role_l10n.dart b/lib/shared/extensions/app_user_role_l10n.dart new file mode 100644 index 0000000..a4815e2 --- /dev/null +++ b/lib/shared/extensions/app_user_role_l10n.dart @@ -0,0 +1,22 @@ +import 'package:core/core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +/// {@template app_user_role_l10n} +/// Extension on [AppUserRole] to provide localized string representations. +/// {@endtemplate} +extension AppUserRoleL10n on AppUserRole { + /// Returns the localized name for an [AppUserRole]. + String l10n(BuildContext context) { + final l10n = context.l10n; + switch (this) { + case AppUserRole.guestUser: + return l10n.guestUserTab; + case AppUserRole.standardUser: + return l10n + .authenticatedUserTab; // Using authenticatedUserTab for standardUser + case AppUserRole.premiumUser: + return l10n.premiumUserTab; + } + } +} diff --git a/lib/shared/extensions/extensions.dart b/lib/shared/extensions/extensions.dart index 32086ea..7f8f84e 100644 --- a/lib/shared/extensions/extensions.dart +++ b/lib/shared/extensions/extensions.dart @@ -1,3 +1,5 @@ +export 'app_user_role_l10n.dart'; export 'content_status_l10n.dart'; +export 'feed_decorator_type_l10n.dart'; export 'source_type_l10n.dart'; export 'string_truncate.dart'; diff --git a/lib/shared/extensions/feed_decorator_type_l10n.dart b/lib/shared/extensions/feed_decorator_type_l10n.dart new file mode 100644 index 0000000..ce5d56b --- /dev/null +++ b/lib/shared/extensions/feed_decorator_type_l10n.dart @@ -0,0 +1,27 @@ +import 'package:core/core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +/// {@template feed_decorator_type_l10n} +/// Extension on [FeedDecoratorType] to provide localized string representations. +/// {@endtemplate} +extension FeedDecoratorTypeL10n on FeedDecoratorType { + /// Returns the localized name for a [FeedDecoratorType]. + String l10n(BuildContext context) { + final l10n = context.l10n; + switch (this) { + case FeedDecoratorType.linkAccount: + return l10n.feedDecoratorTypeLinkAccount; + case FeedDecoratorType.upgrade: + return l10n.feedDecoratorTypeUpgrade; + case FeedDecoratorType.rateApp: + return l10n.feedDecoratorTypeRateApp; + case FeedDecoratorType.enableNotifications: + return l10n.feedDecoratorTypeEnableNotifications; + case FeedDecoratorType.suggestedTopics: + return l10n.feedDecoratorTypeSuggestedTopics; + case FeedDecoratorType.suggestedSources: + return l10n.feedDecoratorTypeSuggestedSources; + } + } +} diff --git a/lib/shared/services/services.dart b/lib/shared/services/services.dart new file mode 100644 index 0000000..05d9ad0 --- /dev/null +++ b/lib/shared/services/services.dart @@ -0,0 +1 @@ +export 'throttled_fetching_service.dart'; diff --git a/lib/shared/shared.dart b/lib/shared/shared.dart index 6ddc72e..1ab43f9 100644 --- a/lib/shared/shared.dart +++ b/lib/shared/shared.dart @@ -1 +1,3 @@ +export 'constants/constants.dart'; export 'extensions/extensions.dart'; +export 'services/services.dart'; diff --git a/pubspec.lock b/pubspec.lock index 882ffcf..c864f2d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -32,7 +32,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "9c25c6bff76c554e5220368536fcb9370758f48f" + resolved-ref: b47d995b24b8cfd9f47b7d0bc4d12d551777531c url: "https://github.com/flutter-news-app-full-source-code/auth-inmemory" source: git version: "0.0.0"