Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
44 changes: 38 additions & 6 deletions lib/core/injection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,20 @@ import 'package:campus_app/utils/dio_utils.dart';
import 'package:campus_app/utils/constants.dart';
import 'package:native_dio_adapter/native_dio_adapter.dart';

// Email-related imports
import 'package:campus_app/pages/email_client/services/email_auth_service.dart';
import 'package:campus_app/pages/email_client/services/imap_email_service.dart';
import 'package:campus_app/pages/email_client/repositories/email_repository.dart';
import 'package:campus_app/pages/email_client/repositories/imap_email_repository.dart';
import 'package:campus_app/pages/email_client/services/email_service.dart';

final sl = GetIt.instance; // service locator

Future<void> init() async {
//!
//! Datasources
//!

//! Datasources
sl.registerSingletonAsync(() async {
final client = Dio();
client.httpClientAdapter = NativeAdapter();
Expand All @@ -63,10 +69,13 @@ Future<void> init() async {
sl.registerLazySingleton(() => NavigationDatasource(appwriteClient: sl()));

//!
//! Repositories
//! Repositories (non-email)
//!

sl.registerLazySingleton(() => BackendRepository(client: sl()));
sl.registerLazySingleton(() {
final Client client = Client().setEndpoint(appwrite).setProject('campus_app');
return BackendRepository(client: client);
});

sl.registerSingletonWithDependencies(
() => NewsRepository(newsDatasource: sl()),
Expand All @@ -87,6 +96,32 @@ Future<void> init() async {
() => TicketRepository(ticketDataSource: sl(), secureStorage: sl()),
);

//!
//! Email dependencies (reordered)
//!

// 1. FlutterSecureStorage is already registered below in “External”

// 2. EmailAuthService (needs secure storage)
sl.registerLazySingleton<EmailAuthService>(
() => EmailAuthService(),
);

// 3. ImapEmailService (low-level IMAP/SMTP)
sl.registerLazySingleton<ImapEmailService>(
() => ImapEmailService(),
);

// 4. EmailRepository (depends on ImapEmailService)
sl.registerLazySingleton<EmailRepository>(
() => ImapEmailRepository(sl<ImapEmailService>()),
);

// 5. EmailService (business logic, depends on EmailRepository)
sl.registerLazySingleton<EmailService>(
() => EmailService(sl<EmailRepository>()),
);

//!
//! Usecases
//!
Expand All @@ -95,17 +130,14 @@ Future<void> init() async {
() => NewsUsecases(newsRepository: sl()),
dependsOn: [NewsRepository],
);

sl.registerSingletonWithDependencies(
() => CalendarUsecases(calendarRepository: sl()),
dependsOn: [CalendarRepository],
);

sl.registerSingletonWithDependencies(
() => MensaUsecases(mensaRepository: sl()),
dependsOn: [MensaRepository],
);

sl.registerLazySingleton(
() => TicketUsecases(ticketRepository: sl()),
);
Expand Down
12 changes: 12 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ import 'package:campus_app/pages/calendar/entities/venue_entity.dart';
import 'package:campus_app/utils/pages/main_utils.dart';
import 'package:campus_app/utils/pages/mensa_utils.dart';

import 'package:campus_app/pages/email_client/services/email_service.dart';
import 'package:campus_app/pages/email_client/services/imap_email_service.dart';
import 'package:campus_app/pages/email_client/services/email_auth_service.dart';
import 'package:campus_app/pages/email_client/repositories/email_repository.dart';
import 'package:campus_app/pages/email_client/repositories/imap_email_repository.dart';

Future<void> main() async {
final WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
// Keeps the native splash screen onscreen until all loading is done
Expand Down Expand Up @@ -66,6 +72,9 @@ Future<void> main() async {
// Initializes the provider that handles the app-theme, authentication and other things
ChangeNotifierProvider<SettingsHandler>(create: (_) => SettingsHandler()),
ChangeNotifierProvider<ThemesNotifier>(create: (_) => ThemesNotifier()),
ChangeNotifierProvider<EmailAuthService>(create: (_) => EmailAuthService()),
Provider<EmailRepository>(create: (_) => ImapEmailRepository(ImapEmailService())),
ChangeNotifierProvider<EmailService>(create: (ctx) => EmailService(ctx.read<EmailRepository>()))
],
child: CampusApp(
key: campusAppKey,
Expand All @@ -80,6 +89,9 @@ Future<void> main() async {
// Initializes the provider that handles the app-theme, authentication and other things
ChangeNotifierProvider<SettingsHandler>(create: (_) => SettingsHandler()),
ChangeNotifierProvider<ThemesNotifier>(create: (_) => ThemesNotifier()),
ChangeNotifierProvider<EmailAuthService>(create: (_) => EmailAuthService()),
Provider<EmailRepository>(create: (_) => ImapEmailRepository(ImapEmailService())),
ChangeNotifierProvider<EmailService>(create: (ctx) => EmailService(ctx.read<EmailRepository>()))
],
child: CampusApp(
key: campusAppKey,
Expand Down
37 changes: 37 additions & 0 deletions lib/pages/email_client/email_drawer/archives.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:campus_app/pages/email_client/models/email.dart';
import 'package:campus_app/pages/email_client/services/email_service.dart';
import 'package:campus_app/pages/email_client/widgets/email_tile.dart';
import 'package:campus_app/pages/email_client/email_pages/email_view.dart';

class ArchivesPage extends StatelessWidget {
const ArchivesPage({super.key});

@override
Widget build(BuildContext context) {
final emailService = Provider.of<EmailService>(context, listen: false);
final archivedEmails = emailService.filterEmails('', EmailFolder.archives);

return Scaffold(
appBar: AppBar(title: const Text('Archives')),
body: archivedEmails.isEmpty
? const Center(child: Text('No archived emails'))
: ListView.builder(
itemCount: archivedEmails.length,
itemBuilder: (context, index) {
final email = archivedEmails[index];
return EmailTile(
email: email,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EmailView(email: email),
),
),
);
},
),
);
}
}
146 changes: 146 additions & 0 deletions lib/pages/email_client/email_drawer/drafts.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:campus_app/pages/email_client/models/email.dart';
import 'package:campus_app/pages/email_client/services/email_service.dart';
import 'package:campus_app/pages/email_client/widgets/email_tile.dart';
import 'package:campus_app/pages/email_client/email_pages/compose_email_screen.dart';

// UI screen to display and manage email drafts
class DraftsPage extends StatefulWidget {
const DraftsPage({super.key});

@override
State<DraftsPage> createState() => _DraftsPageState();
}

class _DraftsPageState extends State<DraftsPage> {
@override
Widget build(BuildContext context) {
final emailService = Provider.of<EmailService>(context); // Access the email service
final selectionController = emailService.selectionController; // For managing multi-selection
final drafts = emailService.allEmails.where((e) => e.folder == EmailFolder.drafts).toList()
..sort((a, b) => b.date.compareTo(a.date)); // Sort drafts by newest first

return Scaffold(
appBar: _buildAppBar(selectionController, drafts, emailService), // Show toolbar with actions
body: drafts.isEmpty
? _buildEmptyState() // Show message if no drafts
: ListView.separated(
itemCount: drafts.length,
separatorBuilder: (_, __) => Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
itemBuilder: (_, index) {
final draft = drafts[index];
return EmailTile(
email: draft,
isSelected: selectionController.isSelected(draft),
onTap: () => _handleEmailTap(draft, selectionController), // Tap to edit
onLongPress: () => _handleEmailLongPress(draft, selectionController), // Long press to select
);
},
),
);
}

// Builds AppBar depending on whether selection mode is active
PreferredSizeWidget _buildAppBar(selectionController, List<Email> drafts, EmailService emailService) {
if (selectionController.isSelecting) {
return AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => selectionController.clearSelection(), // Exit selection mode
),
title: Text('${selectionController.selectionCount} selected'),
actions: [
IconButton(
icon: const Icon(Icons.select_all),
onPressed: () => selectionController.selectAll(drafts), // Select all drafts
),
IconButton(
icon: const Icon(Icons.delete_forever),
onPressed: () => _showDeleteConfirmation(selectionController, emailService), // Confirm before deletion
),
],
);
}

// Default AppBar when not selecting
return AppBar(
title: const Text('Drafts'),
);
}

// Widget shown when there are no drafts
Widget _buildEmptyState() {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.drafts_outlined,
size: 64,
color: Colors.grey,
),
SizedBox(height: 16),
Text(
'No drafts',
style: TextStyle(
fontSize: 18,
color: Colors.grey,
),
),
],
),
);
}

// Handles tapping a draft: open for editing or toggle selection
void _handleEmailTap(Email draft, selectionController) {
if (selectionController.isSelecting) {
selectionController.toggleSelection(draft); // Toggle selected state
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ComposeEmailScreen(draft: draft), // Navigate to compose screen with the draft
),
);
}
}

// Handles long press to enter selection mode
void _handleEmailLongPress(Email draft, selectionController) {
if (!selectionController.isSelecting) {
selectionController.toggleSelection(draft); // Start selecting
}
}

// Show confirmation dialog before permanently deleting selected drafts
void _showDeleteConfirmation(selectionController, EmailService emailService) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Drafts'),
content: Text(
'Are you sure you want to permanently delete ${selectionController.selectionCount} draft(s)?\n\nThis action cannot be undone.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context), // Cancel deletion
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
emailService.deleteEmailsPermanently(selectionController.selectedEmails); // Delete selected drafts
Navigator.pop(context); // Close dialog
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Delete'),
),
],
),
);
}
}
Empty file.
37 changes: 37 additions & 0 deletions lib/pages/email_client/email_drawer/sent.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:campus_app/pages/email_client/models/email.dart';
import 'package:campus_app/pages/email_client/services/email_service.dart';
import 'package:campus_app/pages/email_client/widgets/email_tile.dart';
import 'package:campus_app/pages/email_client/email_pages/email_view.dart';

class SentPage extends StatelessWidget {
const SentPage({super.key});

@override
Widget build(BuildContext context) {
final emailService = Provider.of<EmailService>(context, listen: false);
final sentEmails = emailService.filterEmails('', EmailFolder.sent);

return Scaffold(
appBar: AppBar(title: const Text('Sent Emails')),
body: sentEmails.isEmpty
? const Center(child: Text('No sent emails'))
: ListView.builder(
itemCount: sentEmails.length,
itemBuilder: (context, index) {
final email = sentEmails[index];
return EmailTile(
email: email,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EmailView(email: email),
),
),
);
},
),
);
}
}
Loading