Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0a500df
chore: init
fulleni Jun 27, 2025
232fb34
refactor: Simplify main.dart and remove DevicePreview
fulleni Jun 27, 2025
659b43b
refactor: streamline data client instantiation
fulleni Jun 27, 2025
8791c5a
refactor: simplify AppBloc auth state handling
fulleni Jun 27, 2025
9bed72f
refactor(app): simplify AppShell widget
fulleni Jun 27, 2025
8f0db75
refactor: migrate to ht_dashboard package
fulleni Jun 27, 2025
361a8da
refactor(config): Simplify AppConfig factories
fulleni Jun 27, 2025
d281f8e
refactor(auth): remove anonymous sign-in
fulleni Jun 27, 2025
7efe223
refactor(auth): Remove anonymous sign-in button
fulleni Jun 27, 2025
d39a2a0
refactor(router): simplify routing logic
fulleni Jun 27, 2025
787d6ee
feat: add dashboard page
fulleni Jun 27, 2025
cf1814e
feat(l10n): add email auth localization
fulleni Jun 27, 2025
0e77aaf
feat: Remove splash screen on web after load
fulleni Jun 27, 2025
bcc6486
refactor(router): Remove unused context property
fulleni Jun 27, 2025
17c8a55
feat(l10n): add email auth localizations
fulleni Jun 27, 2025
0008729
refactor(auth): Remove isLinkingContext from page
fulleni Jun 27, 2025
da05f0b
feat: add new labels for navigation and content
fulleni Jun 27, 2025
7203758
feat(app): localize navigation labels
fulleni Jun 27, 2025
a948bd8
feat(router): add new routes and pages
fulleni Jun 27, 2025
5667548
feat: add content management page
fulleni Jun 27, 2025
b31405a
feat: add app configuration page
fulleni Jun 27, 2025
bda6034
feat: add settings page
fulleni Jun 27, 2025
ba03899
refactor: remove unused useDrawer property
fulleni Jun 27, 2025
54668e4
feat(content): implement tabbed content management
fulleni Jun 27, 2025
1a02b19
refactor: Remove Scaffold from page bodies
fulleni Jun 27, 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
3 changes: 3 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
analyzer:
errors:
avoid_print: ignore
include: package:very_good_analysis/analysis_options.9.0.0.yaml
linter:
rules:
Expand Down
1 change: 1 addition & 0 deletions lib/app/app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'view/app.dart';
75 changes: 75 additions & 0 deletions lib/app/bloc/app_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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_data_repository/ht_data_repository.dart';
import 'package:ht_dashboard/app/config/config.dart' as local_config;
import 'package:ht_shared/ht_shared.dart';

part 'app_event.dart';
part 'app_state.dart';

class AppBloc extends Bloc<AppEvent, AppState> {
AppBloc({
required HtAuthRepository authenticationRepository,
required HtDataRepository<UserAppSettings> userAppSettingsRepository,
required HtDataRepository<AppConfig> appConfigRepository,
required local_config.AppEnvironment environment,
}) : _authenticationRepository = authenticationRepository,
_userAppSettingsRepository = userAppSettingsRepository,
_appConfigRepository = appConfigRepository,
_environment = environment,
super(
const AppState(
user: null,
status: AppStatus.initial,
environment: null,
),
) {
on<AppUserChanged>(_onAppUserChanged);
on<AppLogoutRequested>(_onLogoutRequested);

_userSubscription = _authenticationRepository.authStateChanges.listen(
(User? user) => add(AppUserChanged(user)),
);
}

final HtAuthRepository _authenticationRepository;
final HtDataRepository<UserAppSettings> _userAppSettingsRepository;
final HtDataRepository<AppConfig> _appConfigRepository;
final local_config.AppEnvironment _environment;
late final StreamSubscription<User?> _userSubscription;

/// Handles user changes and loads initial settings once user is available.
Future<void> _onAppUserChanged(
AppUserChanged event,
Emitter<AppState> emit,
) async {
// Determine the AppStatus based on the user object and its role
final AppStatus status;

switch (event.user?.role) {
case null:
status = AppStatus.unauthenticated;
case UserRole.standardUser:
status = AppStatus.authenticated;
// ignore: no_default_cases
default: // Fallback for any other roles not explicitly handled
status = AppStatus.unauthenticated; // Treat other roles as unauthenticated for dashboard
}

// Emit user and status update
emit(state.copyWith(status: status, user: event.user));
}

void _onLogoutRequested(AppLogoutRequested event, Emitter<AppState> emit) {
unawaited(_authenticationRepository.signOut());
}

@override
Future<void> close() {
_userSubscription.cancel();
return super.close();
}
}
25 changes: 25 additions & 0 deletions lib/app/bloc/app_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
part of 'app_bloc.dart';

abstract class AppEvent extends Equatable {
const AppEvent();

@override
List<Object?> get props => [];
}

class AppUserChanged extends AppEvent {
const AppUserChanged(this.user);

final User? user;

@override
List<Object?> get props => [user];
}

/// {@template app_logout_requested}
/// Event to request user logout.
/// {@endtemplate}
class AppLogoutRequested extends AppEvent {
/// {@macro app_logout_requested}
const AppLogoutRequested();
}
55 changes: 55 additions & 0 deletions lib/app/bloc/app_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
part of 'app_bloc.dart';

/// Represents the application's authentication status.
enum AppStatus {
/// The application is initializing and the status is unknown.
initial,

/// The user is authenticated.
authenticated,

/// The user is unauthenticated.
unauthenticated,

/// The user is anonymous (signed in using an anonymous provider).
anonymous,
}

class AppState extends Equatable {
/// {@macro app_state}
const AppState({
this.status = AppStatus.initial,
this.user,
this.environment,
});

/// The current authentication status of the application.
final AppStatus status;

/// The current user details. Null if unauthenticated.
final User? user;

/// The current application environment (e.g., production, development, demo).
final local_config.AppEnvironment? environment;

/// Creates a copy of the current state with updated values.
AppState copyWith({
AppStatus? status,
User? user,
local_config.AppEnvironment? environment,
bool clearEnvironment = false,
}) {
return AppState(
status: status ?? this.status,
user: user ?? this.user,
environment: clearEnvironment ? null : environment ?? this.environment,
);
}

@override
List<Object?> get props => [
status,
user,
environment,
];
}
26 changes: 26 additions & 0 deletions lib/app/config/app_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:ht_dashboard/app/config/app_environment.dart';

class AppConfig {
const AppConfig({
required this.environment,
required this.baseUrl,
});

factory AppConfig.production() => const AppConfig(
environment: AppEnvironment.production,
baseUrl: 'http://api.yourproductiondomain.com',
);

factory AppConfig.demo() => const AppConfig(
environment: AppEnvironment.demo,
baseUrl: '',
);

factory AppConfig.development() => const AppConfig(
environment: AppEnvironment.development,
baseUrl: 'http://localhost:8080',
);

final AppEnvironment environment;
final String baseUrl;
}
24 changes: 24 additions & 0 deletions lib/app/config/app_environment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// Defines the different application environments.
enum AppEnvironment {
/// Represents the production environment.
///
/// This environment is used for the live application, connecting to
/// production backend services and configurations.
production,

/// Represents a development environment connecting to a local API.
///
/// This environment is used during local development, typically connecting
/// to a locally running Dart Frog backend API.
development,

/// Represents a demonstration environment with in-memory data.
///
/// This environment is designed for showcasing the application's user
/// interface and features without requiring any external backend services.
/// All data operations are handled by mock data stored directly in memory,
/// eliminating the need for API access.
demo,

// Add other environments like staging, etc. as needed
}
2 changes: 2 additions & 0 deletions lib/app/config/config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'app_config.dart';
export 'app_environment.dart';
149 changes: 149 additions & 0 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//
// ignore_for_file: deprecated_member_use

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:ht_auth_repository/ht_auth_repository.dart';
import 'package:ht_data_repository/ht_data_repository.dart';
import 'package:ht_kv_storage_service/ht_kv_storage_service.dart';
import 'package:ht_dashboard/app/bloc/app_bloc.dart';
import 'package:ht_dashboard/app/config/app_environment.dart';
import 'package:ht_dashboard/authentication/bloc/authentication_bloc.dart';
import 'package:ht_dashboard/l10n/app_localizations.dart';
import 'package:ht_dashboard/l10n/l10n.dart';
import 'package:ht_dashboard/router/router.dart';
import 'package:ht_shared/ht_shared.dart';

class App extends StatelessWidget {
const App({
required HtAuthRepository htAuthenticationRepository,
required HtDataRepository<Headline> htHeadlinesRepository,
required HtDataRepository<Category> htCategoriesRepository,
required HtDataRepository<Country> htCountriesRepository,
required HtDataRepository<Source> htSourcesRepository,
required HtDataRepository<UserAppSettings> htUserAppSettingsRepository,
required HtDataRepository<UserContentPreferences>
htUserContentPreferencesRepository,
required HtDataRepository<AppConfig> htAppConfigRepository,
required HtKVStorageService kvStorageService,
required AppEnvironment environment,
super.key,
}) : _htAuthenticationRepository = htAuthenticationRepository,
_htHeadlinesRepository = htHeadlinesRepository,
_htCategoriesRepository = htCategoriesRepository,
_htCountriesRepository = htCountriesRepository,
_htSourcesRepository = htSourcesRepository,
_htUserAppSettingsRepository = htUserAppSettingsRepository,
_htUserContentPreferencesRepository = htUserContentPreferencesRepository,
_htAppConfigRepository = htAppConfigRepository,
_kvStorageService = kvStorageService,
_environment = environment;

final HtAuthRepository _htAuthenticationRepository;
final HtDataRepository<Headline> _htHeadlinesRepository;
final HtDataRepository<Category> _htCategoriesRepository;
final HtDataRepository<Country> _htCountriesRepository;
final HtDataRepository<Source> _htSourcesRepository;
final HtDataRepository<UserAppSettings> _htUserAppSettingsRepository;
final HtDataRepository<UserContentPreferences>
_htUserContentPreferencesRepository;
final HtDataRepository<AppConfig> _htAppConfigRepository;
final HtKVStorageService _kvStorageService;
final AppEnvironment _environment;

@override
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: [
RepositoryProvider.value(value: _htAuthenticationRepository),
RepositoryProvider.value(value: _htHeadlinesRepository),
RepositoryProvider.value(value: _htCategoriesRepository),
RepositoryProvider.value(value: _htCountriesRepository),
RepositoryProvider.value(value: _htSourcesRepository),
RepositoryProvider.value(value: _htUserAppSettingsRepository),
RepositoryProvider.value(value: _htUserContentPreferencesRepository),
RepositoryProvider.value(value: _htAppConfigRepository),
RepositoryProvider.value(value: _kvStorageService),
],
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => AppBloc(
authenticationRepository: context.read<HtAuthRepository>(),
userAppSettingsRepository:
context.read<HtDataRepository<UserAppSettings>>(),
appConfigRepository: context.read<HtDataRepository<AppConfig>>(),
environment: _environment,
),
),
BlocProvider(
create: (context) => AuthenticationBloc(
authenticationRepository: context.read<HtAuthRepository>(),
),
),
],
child: _AppView(
htAuthenticationRepository: _htAuthenticationRepository,
environment: _environment,
),
),
);
}
}

class _AppView extends StatefulWidget {
const _AppView({
required this.htAuthenticationRepository,
required this.environment,
});

final HtAuthRepository htAuthenticationRepository;
final AppEnvironment environment;

@override
State<_AppView> createState() => _AppViewState();
}

class _AppViewState extends State<_AppView> {
late final GoRouter _router;
late final ValueNotifier<AppStatus> _statusNotifier;

@override
void initState() {
super.initState();
final appBloc = context.read<AppBloc>();
_statusNotifier = ValueNotifier<AppStatus>(appBloc.state.status);
_router = createRouter(
authStatusNotifier: _statusNotifier,
htAuthenticationRepository: widget.htAuthenticationRepository,
environment: widget.environment,
);
}

@override
void dispose() {
_statusNotifier.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return BlocListener<AppBloc, AppState>(
listenWhen: (previous, current) => previous.status != current.status,
listener: (context, state) {
_statusNotifier.value = state.status;
},
child: BlocBuilder<AppBloc, AppState>(
builder: (context, state) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
routerConfig: _router,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
);
},
),
);
}
}
Loading