Skip to content

Commit 2e475a5

Browse files
authored
Merge pull request #7 from headlines-toolkit/feature_settings_page
Feature settings page
2 parents ddfd609 + 105e551 commit 2e475a5

17 files changed

+1643
-76
lines changed

analysis_options.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
analyzer:
22
errors:
3+
avoid_catches_without_on_clauses: ignore
34
avoid_print: ignore
5+
deprecated_member_use: ignore
46
document_ignores: ignore
57
lines_longer_than_80_chars: ignore
68
include: package:very_good_analysis/analysis_options.9.0.0.yaml

lib/app/bloc/app_bloc.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
2525
) {
2626
on<AppUserChanged>(_onAppUserChanged);
2727
on<AppLogoutRequested>(_onLogoutRequested);
28+
on<AppUserAppSettingsChanged>(_onAppUserAppSettingsChanged);
2829

2930
_userSubscription = _authenticationRepository.authStateChanges.listen(
3031
(User? user) => add(AppUserChanged(user)),
@@ -58,10 +59,46 @@ class AppBloc extends Bloc<AppEvent, AppState> {
5859

5960
// Emit user and status update
6061
emit(state.copyWith(status: status, user: event.user));
62+
63+
// If user is authenticated, load their app settings
64+
if (event.user != null) {
65+
try {
66+
final userAppSettings = await _userAppSettingsRepository.read(
67+
id: event.user!.id,
68+
);
69+
emit(state.copyWith(userAppSettings: userAppSettings));
70+
} on NotFoundException {
71+
// If settings not found, create default ones
72+
final defaultSettings = UserAppSettings(id: event.user!.id);
73+
await _userAppSettingsRepository.create(item: defaultSettings);
74+
emit(state.copyWith(userAppSettings: defaultSettings));
75+
} on HtHttpException catch (e) {
76+
// Handle HTTP exceptions during settings load
77+
print('Error loading user app settings: ${e.message}');
78+
emit(state.copyWith()); // Clear settings on error
79+
} catch (e) {
80+
// Handle any other unexpected errors
81+
print('Unexpected error loading user app settings: $e');
82+
emit(state.copyWith()); // Clear settings on error
83+
}
84+
} else {
85+
// If user is unauthenticated, clear app settings
86+
emit(state.copyWith(clearUserAppSettings: true));
87+
}
88+
}
89+
90+
void _onAppUserAppSettingsChanged(
91+
AppUserAppSettingsChanged event,
92+
Emitter<AppState> emit,
93+
) {
94+
emit(state.copyWith(userAppSettings: event.userAppSettings));
6195
}
6296

6397
void _onLogoutRequested(AppLogoutRequested event, Emitter<AppState> emit) {
6498
unawaited(_authenticationRepository.signOut());
99+
emit(
100+
state.copyWith(clearUserAppSettings: true),
101+
); // Clear settings on logout
65102
}
66103

67104
@override

lib/app/bloc/app_event.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,17 @@ class AppLogoutRequested extends AppEvent {
2323
/// {@macro app_logout_requested}
2424
const AppLogoutRequested();
2525
}
26+
27+
/// {@template app_user_app_settings_changed}
28+
/// Event to notify that user application settings have changed.
29+
/// {@endtemplate}
30+
final class AppUserAppSettingsChanged extends AppEvent {
31+
/// {@macro app_user_app_settings_changed}
32+
const AppUserAppSettingsChanged(this.userAppSettings);
33+
34+
/// The updated user application settings.
35+
final UserAppSettings userAppSettings;
36+
37+
@override
38+
List<Object?> get props => [userAppSettings];
39+
}

lib/app/bloc/app_state.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class AppState extends Equatable {
2121
this.status = AppStatus.initial,
2222
this.user,
2323
this.environment,
24+
this.userAppSettings,
2425
});
2526

2627
/// The current authentication status of the application.
@@ -32,17 +33,25 @@ class AppState extends Equatable {
3233
/// The current application environment (e.g., production, development, demo).
3334
final local_config.AppEnvironment? environment;
3435

36+
/// The current user application settings. Null if not loaded or unauthenticated.
37+
final UserAppSettings? userAppSettings;
38+
3539
/// Creates a copy of the current state with updated values.
3640
AppState copyWith({
3741
AppStatus? status,
3842
User? user,
3943
local_config.AppEnvironment? environment,
44+
UserAppSettings? userAppSettings,
4045
bool clearEnvironment = false,
46+
bool clearUserAppSettings = false,
4147
}) {
4248
return AppState(
4349
status: status ?? this.status,
4450
user: user ?? this.user,
4551
environment: clearEnvironment ? null : environment ?? this.environment,
52+
userAppSettings: clearUserAppSettings
53+
? null
54+
: userAppSettings ?? this.userAppSettings,
4655
);
4756
}
4857

@@ -51,5 +60,6 @@ class AppState extends Equatable {
5160
status,
5261
user,
5362
environment,
63+
userAppSettings,
5464
];
5565
}

lib/app/view/app.dart

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//
22
// ignore_for_file: deprecated_member_use
33

4+
import 'package:flex_color_scheme/flex_color_scheme.dart';
45
import 'package:flutter/material.dart';
56
import 'package:flutter_bloc/flutter_bloc.dart';
67
import 'package:go_router/go_router.dart';
@@ -11,6 +12,9 @@ import 'package:ht_dashboard/app_configuration/bloc/app_configuration_bloc.dart'
1112
import 'package:ht_dashboard/authentication/bloc/authentication_bloc.dart';
1213
import 'package:ht_dashboard/l10n/app_localizations.dart';
1314
import 'package:ht_dashboard/router/router.dart';
15+
import 'package:ht_dashboard/shared/theme/app_theme.dart'
16+
as app_theme_extension; // Import for app_theme.dart
17+
import 'package:ht_dashboard/shared/theme/app_theme.dart';
1418
import 'package:ht_data_repository/ht_data_repository.dart';
1519
import 'package:ht_kv_storage_service/ht_kv_storage_service.dart';
1620
import 'package:ht_shared/ht_shared.dart';
@@ -135,12 +139,37 @@ class _AppViewState extends State<_AppView> {
135139
@override
136140
Widget build(BuildContext context) {
137141
return BlocListener<AppBloc, AppState>(
138-
listenWhen: (previous, current) => previous.status != current.status,
142+
listenWhen: (previous, current) =>
143+
previous.status != current.status ||
144+
previous.userAppSettings != current.userAppSettings,
139145
listener: (context, state) {
140146
_statusNotifier.value = state.status;
141147
},
142148
child: BlocBuilder<AppBloc, AppState>(
143149
builder: (context, state) {
150+
final userAppSettings = state.userAppSettings;
151+
final baseTheme = userAppSettings?.displaySettings.baseTheme;
152+
final accentTheme = userAppSettings?.displaySettings.accentTheme;
153+
final fontFamily = userAppSettings?.displaySettings.fontFamily;
154+
final textScaleFactor =
155+
userAppSettings?.displaySettings.textScaleFactor;
156+
final fontWeight = userAppSettings?.displaySettings.fontWeight;
157+
final language = userAppSettings?.language;
158+
159+
final lightThemeData = lightTheme(
160+
scheme: accentTheme?.toFlexScheme ?? FlexScheme.materialHc,
161+
appTextScaleFactor: textScaleFactor ?? AppTextScaleFactor.medium,
162+
appFontWeight: fontWeight ?? AppFontWeight.regular,
163+
fontFamily: fontFamily,
164+
);
165+
166+
final darkThemeData = darkTheme(
167+
scheme: accentTheme?.toFlexScheme ?? FlexScheme.materialHc,
168+
appTextScaleFactor: textScaleFactor ?? AppTextScaleFactor.medium,
169+
appFontWeight: fontWeight ?? AppFontWeight.regular,
170+
fontFamily: fontFamily,
171+
);
172+
144173
const double kMaxAppWidth = 1000; // Local constant for max width
145174
return Center(
146175
child: Card(
@@ -159,6 +188,16 @@ class _AppViewState extends State<_AppView> {
159188
localizationsDelegates:
160189
AppLocalizations.localizationsDelegates,
161190
supportedLocales: AppLocalizations.supportedLocales,
191+
theme: baseTheme == AppBaseTheme.dark
192+
? darkThemeData
193+
: lightThemeData,
194+
darkTheme: darkThemeData,
195+
themeMode: switch (baseTheme) {
196+
AppBaseTheme.light => ThemeMode.light,
197+
AppBaseTheme.dark => ThemeMode.dark,
198+
AppBaseTheme.system || null => ThemeMode.system,
199+
},
200+
locale: language != null ? Locale(language) : null,
162201
),
163202
),
164203
),
@@ -168,3 +207,16 @@ class _AppViewState extends State<_AppView> {
168207
);
169208
}
170209
}
210+
211+
extension AppAccentThemeExtension on AppAccentTheme {
212+
FlexScheme get toFlexScheme {
213+
switch (this) {
214+
case AppAccentTheme.defaultBlue:
215+
return FlexScheme.materialHc;
216+
case AppAccentTheme.newsRed:
217+
return FlexScheme.redWine;
218+
case AppAccentTheme.graphiteGray:
219+
return FlexScheme.outerSpace;
220+
}
221+
}
222+
}

lib/app/view/app_shell.dart

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
3+
import 'package:flutter_bloc/flutter_bloc.dart';
34
import 'package:go_router/go_router.dart';
5+
import 'package:ht_dashboard/app/bloc/app_bloc.dart';
46
import 'package:ht_dashboard/l10n/l10n.dart';
7+
import 'package:ht_dashboard/router/routes.dart';
8+
import 'package:ht_dashboard/shared/constants/app_spacing.dart';
59

610
/// A responsive scaffold shell for the main application sections.
711
///
@@ -27,32 +31,65 @@ class AppShell extends StatelessWidget {
2731

2832
@override
2933
Widget build(BuildContext context) {
30-
return AdaptiveScaffold(
31-
selectedIndex: navigationShell.currentIndex,
32-
onSelectedIndexChange: _goBranch,
33-
destinations: [
34-
NavigationDestination(
35-
icon: const Icon(Icons.dashboard_outlined),
36-
selectedIcon: const Icon(Icons.dashboard),
37-
label: context.l10n.dashboard,
38-
),
39-
NavigationDestination(
40-
icon: const Icon(Icons.folder_open_outlined),
41-
selectedIcon: const Icon(Icons.folder),
42-
label: context.l10n.contentManagement,
43-
),
44-
NavigationDestination(
45-
icon: const Icon(Icons.settings_applications_outlined),
46-
selectedIcon: const Icon(Icons.settings_applications),
47-
label: context.l10n.appConfiguration,
48-
),
49-
NavigationDestination(
50-
icon: const Icon(Icons.settings_outlined),
51-
selectedIcon: const Icon(Icons.settings),
52-
label: context.l10n.settings,
53-
),
54-
],
55-
body: (_) => navigationShell,
34+
final l10n = context.l10n;
35+
return Scaffold(
36+
appBar: AppBar(
37+
title: Text(l10n.dashboard),
38+
actions: [
39+
PopupMenuButton<String>(
40+
onSelected: (value) {
41+
if (value == 'settings') {
42+
context.goNamed(Routes.settingsName);
43+
} else if (value == 'signOut') {
44+
context.read<AppBloc>().add(const AppLogoutRequested());
45+
}
46+
},
47+
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
48+
PopupMenuItem<String>(
49+
value: 'settings',
50+
child: Text(l10n.settings),
51+
),
52+
PopupMenuItem<String>(
53+
value: 'signOut',
54+
child: Text(l10n.signOut),
55+
),
56+
],
57+
child: Padding(
58+
padding: const EdgeInsets.all(AppSpacing.sm),
59+
child: CircleAvatar(
60+
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
61+
child: Icon(
62+
Icons.person,
63+
color: Theme.of(context).colorScheme.onPrimaryContainer,
64+
),
65+
),
66+
),
67+
),
68+
const SizedBox(width: AppSpacing.sm),
69+
],
70+
),
71+
body: AdaptiveScaffold(
72+
selectedIndex: navigationShell.currentIndex,
73+
onSelectedIndexChange: _goBranch,
74+
destinations: [
75+
NavigationDestination(
76+
icon: const Icon(Icons.dashboard_outlined),
77+
selectedIcon: const Icon(Icons.dashboard),
78+
label: l10n.dashboard,
79+
),
80+
NavigationDestination(
81+
icon: const Icon(Icons.folder_open_outlined),
82+
selectedIcon: const Icon(Icons.folder),
83+
label: l10n.contentManagement,
84+
),
85+
NavigationDestination(
86+
icon: const Icon(Icons.settings_applications_outlined),
87+
selectedIcon: const Icon(Icons.settings_applications),
88+
label: l10n.appConfiguration,
89+
),
90+
],
91+
body: (_) => navigationShell,
92+
),
5693
);
5794
}
5895
}

lib/app_configuration/view/app_configuration_page.dart

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -721,40 +721,6 @@ class _AppConfigurationPageState extends State<AppConfigurationPage>
721721
);
722722
}
723723

724-
Widget _buildSwitchField(
725-
BuildContext context, {
726-
required String label,
727-
required String description,
728-
required bool value,
729-
required ValueChanged<bool> onChanged,
730-
}) {
731-
return Padding(
732-
padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
733-
child: Column(
734-
crossAxisAlignment: CrossAxisAlignment.start,
735-
children: [
736-
Text(
737-
label,
738-
style: Theme.of(context).textTheme.titleMedium,
739-
),
740-
const SizedBox(height: AppSpacing.xs),
741-
Text(
742-
description,
743-
style: Theme.of(context).textTheme.bodySmall?.copyWith(
744-
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
745-
),
746-
),
747-
SwitchListTile(
748-
title: Text(label),
749-
value: value,
750-
onChanged: onChanged,
751-
contentPadding: EdgeInsets.zero,
752-
),
753-
],
754-
),
755-
);
756-
}
757-
758724
Widget _buildDropdownField<T>(
759725
BuildContext context, {
760726
required String label,

0 commit comments

Comments
 (0)