Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
65c9f29
refactor(content_management): enhance ArchivedHeadlinesState with pen…
fulleni Sep 21, 2025
744a055
refactor(content_management): enhance UndoDeleteHeadlineRequested eve…
fulleni Sep 21, 2025
5401662
refactor(content_management): improve undo functionality in archived …
fulleni Sep 21, 2025
955003a
refactor(content_management): update undo delete functionality
fulleni Sep 21, 2025
88ae002
style: format
fulleni Sep 21, 2025
bf00b0f
feat(shared): add PendingDeletionsService for managing undoable delet…
fulleni Sep 21, 2025
13de787
refactor(content_management): clarify archived headlines state comments
fulleni Sep 21, 2025
60c1743
refactor(content_management): update and add archived headlines events
fulleni Sep 21, 2025
b9d2f78
refactor(archived_headlines_bloc): integrate PendingDeletionsService …
fulleni Sep 21, 2025
9ec0bd7
refactor(content_management): improve headline restoration and undo f…
fulleni Sep 21, 2025
5547775
feat(shared): add pending deletions service
fulleni Sep 21, 2025
9f268e9
refactor(bloc_observer): enhance state change logging and error handling
fulleni Sep 21, 2025
3aa8067
feat(pending_deletions_service): add item to DeletionEvent and improv…
fulleni Sep 21, 2025
0eac06a
refactor(content_management): replace pendingDeletions map with snack…
fulleni Sep 21, 2025
b89bc08
refactor(content_management): add type to DeletionEvent in archived h…
fulleni Sep 21, 2025
d61a85d
refactor(content_management): optimize archived headlines state manag…
fulleni Sep 21, 2025
a8d2b3a
refactor(content_management): improve snackbar logic and pending dele…
fulleni Sep 21, 2025
165db41
fix(archived headlines ): undo UI refresh
fulleni Sep 21, 2025
eadffe3
style: cleanup
fulleni Sep 21, 2025
2e4a0dc
refactor: remove pending deletions service initialization
fulleni Sep 21, 2025
f46afd5
refactor(content_management): improve headline restoration logic
fulleni Sep 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/app/bloc/app_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
unawaited(_authenticationRepository.signOut());
emit(
state.copyWith(clearUserAppSettings: true),
); // Clear settings on logout
);
}

@override
Expand Down
11 changes: 10 additions & 1 deletion lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/content_manageme
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/overview/bloc/overview_bloc.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/pending_deletions_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';
Expand All @@ -37,6 +38,7 @@ class App extends StatelessWidget {
required DataRepository<LocalAd> localAdsRepository,
required KVStorageService storageService,
required AppEnvironment environment,
required PendingDeletionsService pendingDeletionsService,
super.key,
}) : _authenticationRepository = authenticationRepository,
_headlinesRepository = headlinesRepository,
Expand All @@ -50,7 +52,8 @@ class App extends StatelessWidget {
_countriesRepository = countriesRepository,
_languagesRepository = languagesRepository,
_localAdsRepository = localAdsRepository,
_environment = environment;
_environment = environment,
_pendingDeletionsService = pendingDeletionsService;

final AuthRepository _authenticationRepository;
final DataRepository<Headline> _headlinesRepository;
Expand All @@ -67,6 +70,9 @@ class App extends StatelessWidget {
final KVStorageService _kvStorageService;
final AppEnvironment _environment;

/// The service for managing pending deletions with an undo period.
final PendingDeletionsService _pendingDeletionsService;

@override
Widget build(BuildContext context) {
return MultiRepositoryProvider(
Expand All @@ -86,6 +92,9 @@ class App extends StatelessWidget {
RepositoryProvider(
create: (context) => const ThrottledFetchingService(),
),
RepositoryProvider.value(
value: _pendingDeletionsService,
),
],
child: MultiBlocProvider(
providers: [
Expand Down
2 changes: 1 addition & 1 deletion lib/app_configuration/view/app_configuration_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class _AppConfigurationPageState extends State<AppConfigurationPage>
icon: Icons.settings_applications_outlined,
headline: l10n.appConfigurationPageTitle,
subheadline: l10n.loadAppSettingsSubheadline,
); // Fallback
);
},
),
bottomNavigationBar: _buildBottomAppBar(context),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class _AdvertisementsConfigurationTabState
}
: null,
initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled,
enabled: adConfig.enabled, // Disable the tile itself
enabled: adConfig.enabled,
children: [
AdPlatformConfigForm(
remoteConfig: widget.remoteConfig,
Expand Down Expand Up @@ -110,7 +110,7 @@ class _AdvertisementsConfigurationTabState
}
: null,
initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled,
enabled: adConfig.enabled, // Disable the tile itself
enabled: adConfig.enabled,
children: [
FeedAdSettingsForm(
remoteConfig: widget.remoteConfig,
Expand Down Expand Up @@ -141,7 +141,7 @@ class _AdvertisementsConfigurationTabState
}
: null,
initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled,
enabled: adConfig.enabled, // Disable the tile itself
enabled: adConfig.enabled,
children: [
ArticleAdSettingsForm(
remoteConfig: widget.remoteConfig,
Expand Down Expand Up @@ -172,7 +172,7 @@ class _AdvertisementsConfigurationTabState
}
: null,
initiallyExpanded: expandedIndex == tileIndex && adConfig.enabled,
enabled: adConfig.enabled, // Disable the tile itself
enabled: adConfig.enabled,
children: [
InterstitialAdSettingsForm(
remoteConfig: widget.remoteConfig,
Expand Down
26 changes: 10 additions & 16 deletions lib/app_configuration/widgets/ad_platform_config_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,11 @@ class _AdPlatformConfigFormState extends State<AdPlatformConfigForm> {
ExpansionTile(
title: Text(l10n.primaryAdPlatformTitle),
childrenPadding: const EdgeInsetsDirectional.only(
start: AppSpacing.lg, // Adjusted padding for hierarchy
start: AppSpacing.lg,
top: AppSpacing.md,
bottom: AppSpacing.md,
),
expandedCrossAxisAlignment:
CrossAxisAlignment.start, // Align content to start
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.primaryAdPlatformDescription,
Expand All @@ -233,7 +232,7 @@ class _AdPlatformConfigFormState extends State<AdPlatformConfigForm> {
context,
).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.start, // Ensure text aligns to start
textAlign: TextAlign.start,
),
const SizedBox(height: AppSpacing.lg),
Align(
Expand All @@ -246,10 +245,7 @@ class _AdPlatformConfigFormState extends State<AdPlatformConfigForm> {
),
segments: AdPlatformType.values
.where(
(type) =>
type !=
AdPlatformType
.demo, // Ignore demo ad platform for dashboard
(type) => type != AdPlatformType.demo,
)
.map(
(type) => ButtonSegment<AdPlatformType>(
Expand Down Expand Up @@ -281,12 +277,11 @@ class _AdPlatformConfigFormState extends State<AdPlatformConfigForm> {
ExpansionTile(
title: Text(l10n.adUnitIdentifiersTitle),
childrenPadding: const EdgeInsetsDirectional.only(
start: AppSpacing.lg, // Adjusted padding for hierarchy
start: AppSpacing.lg,
top: AppSpacing.md,
bottom: AppSpacing.md,
),
expandedCrossAxisAlignment:
CrossAxisAlignment.start, // Align content to start
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.adUnitIdentifiersDescription,
Expand All @@ -295,7 +290,7 @@ class _AdPlatformConfigFormState extends State<AdPlatformConfigForm> {
context,
).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.start, // Ensure text aligns to start
textAlign: TextAlign.start,
),
const SizedBox(height: AppSpacing.lg),
_buildAdUnitIdentifierFields(
Expand All @@ -313,12 +308,11 @@ class _AdPlatformConfigFormState extends State<AdPlatformConfigForm> {
ExpansionTile(
title: Text(l10n.localAdManagementTitle),
childrenPadding: const EdgeInsetsDirectional.only(
start: AppSpacing.lg, // Adjusted padding for hierarchy
start: AppSpacing.lg,
top: AppSpacing.md,
bottom: AppSpacing.md,
),
expandedCrossAxisAlignment:
CrossAxisAlignment.start, // Align content to start
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.localAdManagementDescription,
Expand All @@ -327,7 +321,7 @@ class _AdPlatformConfigFormState extends State<AdPlatformConfigForm> {
context,
).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.start, // Ensure text aligns to start
textAlign: TextAlign.start,
),
const SizedBox(height: AppSpacing.lg),
Center(
Expand Down
10 changes: 4 additions & 6 deletions lib/app_configuration/widgets/app_config_form_fields.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ class AppConfigIntField extends StatelessWidget {
return Padding(
padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start, // Ensure alignment to start
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: theme.textTheme.titleMedium),
const SizedBox(height: AppSpacing.xs),
Expand All @@ -47,7 +46,7 @@ class AppConfigIntField extends StatelessWidget {
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.start, // Ensure text aligns to start
textAlign: TextAlign.start,
),
const SizedBox(height: AppSpacing.xs),
TextFormField(
Expand Down Expand Up @@ -107,8 +106,7 @@ class AppConfigTextField extends StatelessWidget {
return Padding(
padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start, // Ensure alignment to start
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: theme.textTheme.titleMedium),
const SizedBox(height: AppSpacing.xs),
Expand All @@ -117,7 +115,7 @@ class AppConfigTextField extends StatelessWidget {
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.start, // Ensure text aligns to start
textAlign: TextAlign.start,
),
const SizedBox(height: AppSpacing.xs),
TextFormField(
Expand Down
16 changes: 7 additions & 9 deletions lib/app_configuration/widgets/feed_ad_settings_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,19 +145,18 @@ class _FeedAdSettingsFormState extends State<FeedAdSettingsForm>
ExpansionTile(
title: Text(l10n.feedAdTypeSelectionTitle),
childrenPadding: const EdgeInsetsDirectional.only(
start: AppSpacing.lg, // Adjusted padding for hierarchy
start: AppSpacing.lg,
top: AppSpacing.md,
bottom: AppSpacing.md,
),
expandedCrossAxisAlignment:
CrossAxisAlignment.start, // Align content to start
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.feedAdTypeSelectionDescription,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.start, // Ensure text aligns to start
textAlign: TextAlign.start,
),
const SizedBox(height: AppSpacing.lg),
Align(
Expand Down Expand Up @@ -199,19 +198,18 @@ class _FeedAdSettingsFormState extends State<FeedAdSettingsForm>
ExpansionTile(
title: Text(l10n.userRoleFrequencySettingsTitle),
childrenPadding: const EdgeInsetsDirectional.only(
start: AppSpacing.lg, // Adjusted padding for hierarchy
start: AppSpacing.lg,
top: AppSpacing.md,
bottom: AppSpacing.md,
),
expandedCrossAxisAlignment:
CrossAxisAlignment.start, // Align content to start
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.userRoleFrequencySettingsDescription,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.start, // Ensure text aligns to start
textAlign: TextAlign.start,
),
const SizedBox(height: AppSpacing.lg),
// Replaced SegmentedButton with TabBar for role selection
Expand All @@ -232,7 +230,7 @@ class _FeedAdSettingsFormState extends State<FeedAdSettingsForm>
const SizedBox(height: AppSpacing.lg),
// TabBarView to display role-specific fields
SizedBox(
height: 250, // Fixed height for TabBarView within a ListView
height: 250,
child: TabBarView(
controller: _tabController,
children: AppUserRole.values
Expand Down
2 changes: 1 addition & 1 deletion lib/app_configuration/widgets/feed_decorator_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class _FeedDecoratorFormState extends State<FeedDecoratorForm>
const SizedBox(height: AppSpacing.lg),
// TabBarView to display role-specific fields
SizedBox(
height: 250, // Fixed height for TabBarView within a ListView
height: 250,
child: TabBarView(
controller: _tabController,
children: AppUserRole.values
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ class _InterstitialAdSettingsFormState extends State<InterstitialAdSettingsForm>
),
const SizedBox(height: AppSpacing.lg),
SizedBox(
height: 250, // Fixed height for TabBarView within a ListView
height: 250,
child: TabBarView(
controller: _tabController,
children: AppUserRole.values
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ class _UserPreferenceLimitsFormState extends State<UserPreferenceLimitsForm>
const SizedBox(height: AppSpacing.lg),
// TabBarView to display role-specific fields
SizedBox(
height: 250, // Fixed height for TabBarView within a ListView
height: 250,
child: TabBarView(
controller: _tabController,
children: AppUserRole.values
Expand Down
49 changes: 45 additions & 4 deletions lib/bloc_observer.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// ignore_for_file: avoid_dynamic_calls

import 'dart:developer';

import 'package:bloc/bloc.dart';

class AppBlocObserver extends BlocObserver {
Expand All @@ -6,14 +10,51 @@ class AppBlocObserver extends BlocObserver {
@override
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
super.onChange(bloc, change);
// log('onChange(${bloc.runtimeType}, $change)');
print('onChange(${bloc.runtimeType}, $change)');
final dynamic oldState = change.currentState;
final dynamic newState = change.nextState;

// Initialize state information strings.
// By default, truncate the full string representation of the state
// to the first 250 characters to prevent excessively long logs.
var oldStateInfo = oldState.toString().substring(
0,
oldState.toString().length > 250 ? 250 : oldState.toString().length,
);
var newStateInfo = newState.toString().substring(
0,
newState.toString().length > 250 ? 250 : newState.toString().length,
);

try {
// Attempt to access a 'status' property on the state objects.
// Many BLoC states use a 'status' property (e.g., Loading, Success, Failure)
// to represent their current lifecycle phase. If this property exists
// and is not null, prioritize logging its value for conciseness.
if (oldState.status != null) {
oldStateInfo = 'status: ${oldState.status}';
}
if (newState.status != null) {
newStateInfo = 'status: ${newState.status}';
}
} catch (e) {
// This catch block handles cases where:
// 1. The 'status' property does not exist on the state object (NoSuchMethodError).
// 2. Accessing 'status' throws any other runtime error.
// In such scenarios, the `oldStateInfo` and `newStateInfo` variables
// will retain their initially truncated string representations,
// providing a fallback for states without a 'status' property.
// Log the error for debugging purposes, but do not rethrow to avoid
// crashing the observer.
log('Error accessing status property for ${bloc.runtimeType}: $e');
}

// Log the state change, including the BLoC type and the old and new state information.
log('onChange(${bloc.runtimeType}, $oldStateInfo -> $newStateInfo)');
}

@override
void onError(BlocBase<dynamic> bloc, Object error, StackTrace stackTrace) {
// log('onError(${bloc.runtimeType}, $error, $stackTrace)');
print('onError(${bloc.runtimeType}, $error, $stackTrace)');
log('onError(${bloc.runtimeType}, $error, $stackTrace)');
super.onError(bloc, error, stackTrace);
}
}
Loading
Loading