From a65c973dc74be72457fb4422f37971362045922c Mon Sep 17 00:00:00 2001 From: juan Date: Thu, 24 Apr 2025 17:01:19 -0400 Subject: [PATCH 1/7] rename and delete api calls --- lib/api_service.dart | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/api_service.dart b/lib/api_service.dart index 1777c9c..37b6736 100644 --- a/lib/api_service.dart +++ b/lib/api_service.dart @@ -122,8 +122,8 @@ class ApiService { if (response.statusCode == 200) { content += response.body; try { - List attachments = await getAttachmentsInfo( - emailFolder, id); + List attachments = + await getAttachmentsInfo(emailFolder, id); for (var attachment in attachments) { //TODO: for each attachment creaate at the bottom a widget for each individual one threadAttachments @@ -164,6 +164,31 @@ class ApiService { Map requestBody = {'name': folderName}; + try { + var response = await http.post( + url, + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode(requestBody), + ); + if (response.statusCode == 200) { + print('response body: ${response.body}'); + } else { + print('Error: ${response.statusCode}, response body: ${response.body}'); + } + } catch (e) { + print('error making post req: $e'); + } + } + + Future renameFolder(String oldFolder, String newFolder) async { + var url = Uri.http('$ip:$port', 'rename_folder'); + Map requestBody = { + 'old_name': oldFolder, + 'new_name': newFolder, + }; + try { var response = await http.post( url, @@ -268,7 +293,6 @@ class ApiService { //leads to problems as for a) the html is added one right after the other in one iframe, b) // if it was multiple iframes then the scrolling to jump would not work as expected - print("marker called"); // JavaScript code embedded as a string String jsCode = ''' @@ -379,7 +403,7 @@ class _EmailViewState extends State { String currentContent = widget.emailContent; viewTypeId = "iframe-${DateTime.now().millisecondsSinceEpoch}"; _registerViewFactory(currentContent); - _markerPositionsFuture = ApiService().getMarkerPosition(); + _markerPositionsFuture = ApiService().getMarkerPosition(); } void _registerViewFactory(String currentContent) { -- 2.34.1 From a7c30732f47fe4a6ff775ac83565bf4bbdc57a50 Mon Sep 17 00:00:00 2001 From: juan Date: Thu, 24 Apr 2025 17:01:46 -0400 Subject: [PATCH 2/7] alerDialog widget for renaming --- lib/folder_drawer.dart | 100 +++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 33 deletions(-) diff --git a/lib/folder_drawer.dart b/lib/folder_drawer.dart index 62b0a42..257a98d 100644 --- a/lib/folder_drawer.dart +++ b/lib/folder_drawer.dart @@ -16,6 +16,7 @@ class FolderDrawer extends StatefulWidget { class _FolderDrawerState extends State { List folders = []; + final TextEditingController _renameController = TextEditingController(); @override void initState() { @@ -48,7 +49,7 @@ class _FolderDrawerState extends State { icon: Icon(Icons.more_vert), onPressed: () => { ///show options - _showOptions(context) + _showOptions(context, folder) }, ), onTap: () { @@ -65,7 +66,7 @@ class _FolderDrawerState extends State { showDialog( context: context, builder: (BuildContext context) { - return NewMailbox(apiService: widget.apiService); + return NewMailbox(apiService: widget.apiService); }, ); // Navigator.of(context).pop(); @@ -83,39 +84,74 @@ class _FolderDrawerState extends State { ), ); } - void _showOptions(BuildContext context) async { - final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; - - await showMenu( - context: context, - position: RelativeRect.fromLTRB(100, 100, overlay.size.width, overlay.size.height), - items: >[ - PopupMenuItem( - value: 'Rename', - child: Text('Rename Folder'), - ), - PopupMenuItem( - value: 'Delete', - child: Text('Delete Folder'), - ), - ], - ).then((value) { - // Handle the action based on the selected menu item - if (value == 'Rename') { - // Logic for renaming the folder - print('Rename folder'); - } else if (value == 'Delete') { - // Logic for deleting the folder - print('Delete folder'); - } - }); -} + Future _renameDialog(String oldFolder) async { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Rename Mailbox"), + content: TextField( + controller: _renameController, + decoration: const InputDecoration( + hintText: "New Name", + ), + ), + actions: [ + TextButton( + onPressed: () { + String newfolderName = _renameController.text; + if (newfolderName.isNotEmpty) { + //make an and to make sure there's two folders with the same name + ApiService().renameFolder(oldFolder, newfolderName); + } + Navigator.of(context).pop(); + }, + child: const Text("Rename"), + ) + ], + ); + }); + + return false; + } + + void _showOptions(BuildContext context, String folderName) async { + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; + print(folderName); + await showMenu( + context: context, + position: RelativeRect.fromLTRB( + 100, 100, overlay.size.width, overlay.size.height), + items: >[ + PopupMenuItem( + value: 'Rename', + child: Text('Rename Folder'), + ), + PopupMenuItem( + value: 'Delete', + child: Text('Delete Folder'), + ), + ], + ).then((value) { + // Handle the action based on the selected menu item + if (value == 'Rename') { + // Logic for renaming the folder + print('Rename folder $folderName'); + _renameDialog(folderName); + } else if (value == 'Delete') { + // Logic for deleting the folder + print("Deleting $folderName"); + ApiService().deleteFolder(folderName); + print('Deleted folder'); + } + }); + } } class NewMailbox extends StatelessWidget { final ApiService apiService; - // final Function(String) onFolderCreated; final TextEditingController _textFieldController = TextEditingController(); NewMailbox({required this.apiService}); @@ -127,7 +163,7 @@ class NewMailbox extends StatelessWidget { content: TextField( controller: _textFieldController, decoration: const InputDecoration( - hintText: "EPIC FOLDER", // Your custom hint text here + hintText: "New Folder", ), ), actions: [ @@ -138,9 +174,7 @@ class NewMailbox extends StatelessWidget { if (folderName.isNotEmpty) { apiService.createFolder(folderName); - // onFolderCreated(folderName); } - // apiService.createFolder(_textFieldController.text); Navigator.of(context).pop(); }, child: const Text("Approve"), -- 2.34.1 From e104f445ab1c6ee1b25957c6ee3da349aa10f1ef Mon Sep 17 00:00:00 2001 From: juan Date: Mon, 28 Apr 2025 20:08:35 -0400 Subject: [PATCH 3/7] confirm dialog when deleting a folder --- lib/folder_drawer.dart | 81 ++++++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/lib/folder_drawer.dart b/lib/folder_drawer.dart index 257a98d..edc7eca 100644 --- a/lib/folder_drawer.dart +++ b/lib/folder_drawer.dart @@ -90,32 +90,62 @@ class _FolderDrawerState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text("Rename Mailbox"), - content: TextField( - controller: _renameController, - decoration: const InputDecoration( - hintText: "New Name", - ), - ), - actions: [ - TextButton( - onPressed: () { - String newfolderName = _renameController.text; - if (newfolderName.isNotEmpty) { - //make an and to make sure there's two folders with the same name - ApiService().renameFolder(oldFolder, newfolderName); - } - Navigator.of(context).pop(); - }, - child: const Text("Rename"), - ) - ], - ); + title: Text("Rename Mailbox"), + content: TextField( + controller: _renameController, + decoration: const InputDecoration( + hintText: "New Name", + ), + ), + actions: [ + TextButton( + onPressed: () { + String newfolderName = _renameController.text; + if (newfolderName.isNotEmpty) { + //make an and to make sure there's two folders with the same name + ApiService().renameFolder(oldFolder, newfolderName); + } + Navigator.of(context).pop(); + }, + child: const Text("Rename"), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("Cancel"), + ) + ], + ); }); return false; } + Future doubleCheckDelete(String folderTobeDeleted) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Confirm delete of: $folderTobeDeleted"), + actions: [ + TextButton( + onPressed: () { + ApiService().deleteFolder(folderTobeDeleted); + Navigator.of(context).pop(); + }, + child: Text("Yes")), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text("No")), + ], + ); + }); + } + void _showOptions(BuildContext context, String folderName) async { final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; @@ -143,7 +173,8 @@ class _FolderDrawerState extends State { } else if (value == 'Delete') { // Logic for deleting the folder print("Deleting $folderName"); - ApiService().deleteFolder(folderName); + doubleCheckDelete(folderName); + // ApiService().deleteFolder(folderName); print('Deleted folder'); } }); @@ -179,6 +210,12 @@ class NewMailbox extends StatelessWidget { }, child: const Text("Approve"), ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("Cancel"), + ) ], ); } -- 2.34.1 From b5cb5b99fbe30133dc75cd25df4108de6424236b Mon Sep 17 00:00:00 2001 From: juan Date: Fri, 2 May 2025 19:35:51 -0400 Subject: [PATCH 4/7] fixed problem of pagenation not updating --- lib/email.dart | 282 +++++++------- lib/home_page.dart | 894 +++++++++++++++++++++++---------------------- 2 files changed, 600 insertions(+), 576 deletions(-) diff --git a/lib/email.dart b/lib/email.dart index e289526..5a4c396 100644 --- a/lib/email.dart +++ b/lib/email.dart @@ -1,136 +1,146 @@ -import 'package:flutter/material.dart'; -import 'api_service.dart'; -import 'structs.dart'; - -class EmailListScreen extends StatelessWidget { - final List emails; - final Future Function(List, String) getEmailContent; - final String folder; - - - EmailListScreen({required this.emails, required this.getEmailContent, required this.folder}); -//fix the email list - @override - Widget build(BuildContext context) { - return Scaffold( - body: ListView.separated( - itemCount: emails.length, - itemBuilder: (context, index) { - final email = emails[index]; - return ListTile( - title: Text(email.from_name, - style: TextStyle(fontWeight: FontWeight.bold)), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text(email.subject)], - ), - trailing: Text(email.date.toString()), - onTap: () async { - String emailContent = await getEmailContent(email.messages, folder); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EmailView( - emailContent: emailContent, - from: email.from_address, - name: email.from_name, - to: email.to.toString(), - subject: email.subject, - date: email.date.toString(), - id: email.id.toString(), - ), - ), - ); - }, - ); - }, - separatorBuilder: (context, index) => Divider(), - ), - ); - } -} - -// ignore: must_be_immutable -class EmailPage extends StatefulWidget { - EmailPage({Key? key}) : super(key: key); - String selectedFolder = "INBOX"; //starter - int offset = 0; - int page = 1; - - @override - EmailPageState createState() => EmailPageState(); -} - -class EmailPageState extends State { - final ApiService apiService = ApiService(); - List emails = []; - int page = 1; - bool isBackDisabled = false; - - @override - void initState() { - super.initState(); - widget.page = page; - isBackDisabled = true; - } - - void updateSelectedFolder(String folder) { - setState(() { - widget.selectedFolder = folder; - }); - print(folder); - _fetchEmails(); - } - - String getPage() { - return widget.page.toString(); - } - - void updatePagenation(String option) { - if (option == "next") { - setState(() { - widget.offset += 50; - widget.page += 1; - isBackDisabled = false; - }); - } else if (option == "back") { - setState(() { - widget.offset -= 50; - widget.page -= 1; - if (widget.page == 1) { - isBackDisabled = true; - print("back dis"); - } - }); - } - // print(currentPage); - _fetchEmails(); - } - - void _fetchEmails() async { - // print(selectedFolder) - try { - List fetchedEmails = await apiService - .fetchEmailsFromFolder(widget.selectedFolder, widget.offset); - if (!mounted) return; - - setState(() { - emails = fetchedEmails; // Update the list of emails - }); - } catch (e) { - print('Error fetching emails: $e'); - } - } - - @override - Widget build(BuildContext context) { - _fetchEmails(); - return Scaffold( - body: EmailListScreen( - emails: emails, - getEmailContent: apiService.fetchEmailContent, - folder: widget.selectedFolder,//try to grab from it directly - ), - ); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'api_service.dart'; +import 'structs.dart'; + +class EmailListScreen extends StatelessWidget { + final List emails; + final Future Function(List, String) getEmailContent; + final String folder; + + EmailListScreen( + {required this.emails, + required this.getEmailContent, + required this.folder}); +//fix the email list + @override + Widget build(BuildContext context) { + return Scaffold( + body: ListView.separated( + itemCount: emails.length, + itemBuilder: (context, index) { + final email = emails[index]; + return ListTile( + title: Text(email.from_name, + style: TextStyle(fontWeight: FontWeight.bold)), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text(email.subject)], + ), + trailing: Text(email.date.toString()), + onTap: () async { + String emailContent = + await getEmailContent(email.messages, folder); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EmailView( + emailContent: emailContent, + from: email.from_address, + name: email.from_name, + to: email.to.toString(), + subject: email.subject, + date: email.date.toString(), + id: email.id.toString(), + ), + ), + ); + }, + ); + }, + separatorBuilder: (context, index) => Divider(), + ), + ); + } +} + +// ignore: must_be_immutable +class EmailPage extends StatefulWidget { + EmailPage({Key? key}) : super(key: key); + String selectedFolder = "INBOX"; //starter + int offset = 0; + int page = 1; + + @override + EmailPageState createState() => EmailPageState(); +} + +class EmailPageState extends State { + final ApiService apiService = ApiService(); + List emails = []; + ValueNotifier currentPageNotifier = ValueNotifier(1); + int page = 1; + bool isBackDisabled = false; + + @override + void initState() { + super.initState(); + widget.page = page; + isBackDisabled = true; + } + + String getPage() => widget.page.toString(); + bool get backDisabled => isBackDisabled; + + void updateSelectedFolder(String folder) { + setState(() { + widget.selectedFolder = folder; + }); + print(folder); + _fetchEmails(); + } + + // String getPage() => widget.page.toString(); + + + void updatePagenation(String option) { + if (option == "next") { + setState(() { + widget.offset += 50; + widget.page += 1; + currentPageNotifier.value = widget.page; + isBackDisabled = false; + }); + } else if (option == "back") { + setState(() { + widget.offset -= 50; + widget.page -= 1; + currentPageNotifier.value = widget.page; + if (widget.page == 1) { + isBackDisabled = true; + print("back dis"); + } + }); + } + // print(currentPage); + print(widget.page); + _fetchEmails(); + } + + void _fetchEmails() async { + // print(selectedFolder) + try { + List fetchedEmails = await apiService + .fetchEmailsFromFolder(widget.selectedFolder, widget.offset); + if (!mounted) return; + + setState(() { + emails = fetchedEmails; // Update the list of emails + }); + } catch (e) { + print('Error fetching emails: $e'); + } + } + + @override + Widget build(BuildContext context) { + _fetchEmails(); + return Scaffold( + body: EmailListScreen( + emails: emails, + getEmailContent: apiService.fetchEmailContent, + folder: widget.selectedFolder, //try to grab from it directly + ), + ); + } +} diff --git a/lib/home_page.dart b/lib/home_page.dart index 5badb9f..03ae481 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -1,440 +1,454 @@ -import 'folder_drawer.dart'; -import 'structs.dart'; -import 'package:flutter/widgets.dart'; -import 'api_service.dart'; -import 'package:flutter/material.dart'; -import 'email.dart'; -// import 'package:shared_preferences/shared_preferences.dart'; -// import 'serialize.dart'; - -class HomeScreen extends StatefulWidget { - @override - _HomeScreenState createState() => _HomeScreenState(); -} - -class _HomeScreenState extends State with TickerProviderStateMixin { - final GlobalKey _scaffoldKey = GlobalKey(); - final GlobalKey _emailPageKey = GlobalKey(); - ApiService apiService = ApiService(); - bool _isSidebarOpen = true; - bool querySearches = false; - String? _selectedOption = "INBOX"; - - List _tabs = ['Emails']; - Map _tabWidgets = {}; - TabController? _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: _tabs.length, vsync: this); - _tabWidgets['Emails'] = EmailPage( - key: _emailPageKey, - ); - } - - // Add a new tab based on the search - void _performSearch(String query, String? list) { - setState(() { - if (!_tabs.contains(query)) { - _tabs.add(query); - _tabWidgets[query] = _buildSearchResultsWidget( - query, list); // Store a different widget for this tab - _tabController = TabController(length: _tabs.length, vsync: this); - } - }); - } - - void _showOptionsSearchDialog () async { - List folders = await apiService.fetchFolders(); - - if (mounted) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Choose an Option'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: folders.map((option) { - return ListTile( - title: Text(option), - leading: Radio( - value: option, - groupValue: _selectedOption, // Bind with _selectedOption - onChanged: (String? value) { - setState(() { - _selectedOption = value; - }); - Navigator.of(context).pop(); // Close the dialog on selection - }, - ), - ); - }).toList(), - ), - actions: [ - ElevatedButton( - child: Text('Submit'), - onPressed: () { - Navigator.of(context).pop(); // Close the dialog - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('You selected: $_selectedOption'), - )); - }, - ), - ], - ); - }, - );} - } - - - // Remove a tab - void _removeTab(int index) { - if (_tabs[index] != 'Emails') { - setState(() { - String tabToRemove = _tabs[index]; - _tabs.removeAt(index); - _tabWidgets - .remove(tabToRemove); // Remove widget associated with the tab - _tabController = TabController(length: _tabs.length, vsync: this); - }); - } - } - - // Build a custom widget for each search query - Widget _buildSearchResultsWidget(String query, String? list) { - return FutureBuilder>( - future: apiService.sonicSearch(list ?? "INBOX", 50, 0, query), - builder: (BuildContext context, - AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Error: ${snapshot.error}')); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Center(child: Text('No results found for: $query')); - } else { - List result = snapshot.data!; - return Scaffold( - body: ListView.separated( - itemCount: result.length, - itemBuilder: (context, index) { - final SerializableMessage email = result[index]; - return ListTile( - title: Text(email.from, - style: TextStyle(fontWeight: FontWeight.bold)), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text(email.subject)], - ), - trailing: Text(email.date.toString()), - onTap: () async { - // print('tapped'); - String emailContent = - await apiService.fetchEmailContent([email.id], email.list); - // print('content below'); - // print(emailContent); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EmailView( - emailContent: emailContent, - from: email.from, - name: email.name, - to: email.to.toString(), - subject: email.subject, - date: email.date.toString(), - id: email.id.toString(), - ), - ), - ); - }, - ); - }, - separatorBuilder: (context, index) => Divider(), - ), - // child: Column( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Text("Results for: $query", style: TextStyle(fontSize: 24)), - // // Display the actual data - // Text(result[0].name), // Accessing the first result safely - // Text(result[0].from), // Displaying the 'from' field as an example - // Text(result[0].hash), - // Text(result[0].subject), - // Text(result[0].uid.toString()), - // Text(result[0].list), - // Text(result[0].id), - - // // Add more fields or customize the display - // // SerializableEmailListScreen(emails: result, getEmailContent: getEmailContent) - // // Expanded( - - // // child: - // // ), - // ], - ); - // ); - } - }, - ); - } - - @override - void dispose() { - _tabController?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - key: _scaffoldKey, - drawer: FolderDrawer( - apiService: apiService, - onFolderTap: (folder) { - _emailPageKey.currentState?.updateSelectedFolder(folder); - }, - ), - body: Stack( - children: [ - Row( - children: [ - // Sidebar - if (_isSidebarOpen) - Container( - width: 70, - color: Color.fromARGB(17, 96, 122, 135), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - leading: Icon(Icons.home), - onTap: () { - // Navigate to Home - }, - ), - ListTile( - leading: Icon(Icons.settings), - onTap: () { - // Navigate to Settings - }, - ), - ListTile( - leading: Icon(Icons.email), - onTap: () { - _scaffoldKey.currentState?.openDrawer(); - }, - ), - Spacer(), - Padding( - padding: const EdgeInsets.all(8.0), - child: Align( - alignment: Alignment.bottomLeft, - child: IconButton( - icon: Icon(Icons.close, color: Colors.white), - onPressed: () { - setState(() { - _isSidebarOpen = false; - }); - }, - ), - ), - ), - ], - ), - ), - // Main content - Expanded( - child: Column( - children: [ - Container( - padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 4.0), - color: Color.fromARGB(42, 36, 102, 132), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 800, - height: 40, - child: TextField( - decoration: InputDecoration( - hintText: 'Search...', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.search), - ), - onSubmitted: (value) { - if (value.isNotEmpty) { - _performSearch(value, _selectedOption); - } - //this is the input box i mentioned - // if (value == '') { - // setState(() { - // querySearches = false; - // }); - // } - // Future> results = apiService - // .sonicSearch('INBOX', 20, 0, value); - // // print(value); - // print(results); - // setState(() { - // querySearches = true; - // }); - }, - ), - ), - SizedBox( - width: 16, - ), - Container( - width: 80, - height: 40, - child: ElevatedButton( - onPressed: _showOptionsSearchDialog, - child: Icon(Icons.manage_search), - ), - ) - ], - ), - ), - Container( - padding: EdgeInsets.all(0.0), - color: Color.fromARGB(42, 36, 102, 132), - child: Row( - children: [ - Container( - height: 2, - ) - ], - ), - ), - Container( - color: Color.fromARGB(255, 131, 110, 143), - child: TabBar( - controller: _tabController, - isScrollable: true, - tabs: _tabs - .asMap() - .entries - .map((entry) => Tab( - child: Row( - children: [ - Text(entry.value), - if (entry.value != 'Emails') - GestureDetector( - onTap: () => _removeTab(entry.key), - child: Icon(Icons.close, size: 16), - ), - ], - ), - )) - .toList(), - labelColor: Colors.white, - indicatorColor: Colors.white, - ), - ), - Container( - // alignment: Alignment.topLeft, - padding: EdgeInsets.all(8.0), - color: Colors.white, - child: Row( - children: [ - ElevatedButton( - onPressed: () { - _emailPageKey.currentState!.isBackDisabled ? null: _emailPageKey.currentState - ?.updatePagenation('back'); - }, - child: Icon(Icons.navigate_before), - ), - Text(_emailPageKey.currentState?.getPage() ?? '1'), - ElevatedButton( - onPressed: () { - _emailPageKey.currentState - ?.updatePagenation('next'); - }, - child: Icon(Icons.navigate_next), - ), - ], - ), - ), - Expanded( - child: TabBarView( - controller: _tabController, - children: _tabs.map((tab) { - return _tabWidgets[tab] ?? - Center(child: Text("No content found")); - // return Center( - // child: EmailPage( - // key: _emailPageKey, - // )); - }).toList(), - ), - ), - - // if (_tabs.isEmpty) - // Expanded( - // child: EmailPage(key: _emailPageKey), - // ), - // if (_tabs.isNotEmpty) - // Expanded( - // // child: Text('supposed to be mails'), - // child: TabBarView( - // controller: _tabController, - // children: _tabs - // .map((tab) => Center(child: Text('Results for $tab'))) - // .toList(), - // ), - // ), - ], - ), - ), - ], - ), - if (!_isSidebarOpen) - Positioned( - bottom: 16, - left: 16, - child: FloatingActionButton( - child: Icon(Icons.menu), - onPressed: () { - setState(() { - _isSidebarOpen = true; - }); - }, - ), - ), - ], - ), - ); - } -} -// void _showPopupMenu(BuildContext context, Offset position) async { -// final RenderBox overlay = -// Overlay.of(context).context.findRenderObject() as RenderBox; - -// await showMenu( -// context: context, -// position: RelativeRect.fromLTRB( -// position.dx, -// position.dy, -// overlay.size.width - position.dx, -// overlay.size.height - position.dy, -// ), -// items: >[ -// PopupMenuItem( -// value: 'Open', -// child: Text('Open'), -// ), -// PopupMenuItem( -// value: 'Reply', -// child: Text('Reply'), -// ), -// PopupMenuItem( -// value: 'Delete', -// child: Text('Delete'), -// ), -// ], -// ); -// } -// } +import 'folder_drawer.dart'; +import 'structs.dart'; +import 'package:flutter/widgets.dart'; +import 'api_service.dart'; +import 'package:flutter/material.dart'; +import 'email.dart'; +// import 'package:shared_preferences/shared_preferences.dart'; +// import 'serialize.dart'; + +class HomeScreen extends StatefulWidget { + @override + _HomeScreenState createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State with TickerProviderStateMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _emailPageKey = GlobalKey(); + ApiService apiService = ApiService(); + bool _isSidebarOpen = true; + bool querySearches = false; + String? _selectedOption = "INBOX"; + + List _tabs = ['Emails']; + Map _tabWidgets = {}; + TabController? _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: _tabs.length, vsync: this); + _tabWidgets['Emails'] = EmailPage( + key: _emailPageKey, + ); + } + + // Add a new tab based on the search + void _performSearch(String query, String? list) { + setState(() { + if (!_tabs.contains(query)) { + _tabs.add(query); + _tabWidgets[query] = _buildSearchResultsWidget( + query, list); // Store a different widget for this tab + _tabController = TabController(length: _tabs.length, vsync: this); + } + }); + } + + void _showOptionsSearchDialog () async { + List folders = await apiService.fetchFolders(); + + if (mounted) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Choose an Option'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: folders.map((option) { + return ListTile( + title: Text(option), + leading: Radio( + value: option, + groupValue: _selectedOption, // Bind with _selectedOption + onChanged: (String? value) { + setState(() { + _selectedOption = value; + }); + Navigator.of(context).pop(); // Close the dialog on selection + }, + ), + ); + }).toList(), + ), + actions: [ + ElevatedButton( + child: Text('Submit'), + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('You selected: $_selectedOption'), + )); + }, + ), + ], + ); + }, + );} + } + + + // Remove a tab + void _removeTab(int index) { + if (_tabs[index] != 'Emails') { + setState(() { + String tabToRemove = _tabs[index]; + _tabs.removeAt(index); + _tabWidgets + .remove(tabToRemove); // Remove widget associated with the tab + _tabController = TabController(length: _tabs.length, vsync: this); + }); + } + } + + // Build a custom widget for each search query + Widget _buildSearchResultsWidget(String query, String? list) { + return FutureBuilder>( + future: apiService.sonicSearch(list ?? "INBOX", 50, 0, query), + builder: (BuildContext context, + AsyncSnapshot> snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Center(child: Text('No results found for: $query')); + } else { + List result = snapshot.data!; + return Scaffold( + body: ListView.separated( + itemCount: result.length, + itemBuilder: (context, index) { + final SerializableMessage email = result[index]; + return ListTile( + title: Text(email.from, + style: TextStyle(fontWeight: FontWeight.bold)), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text(email.subject)], + ), + trailing: Text(email.date.toString()), + onTap: () async { + // print('tapped'); + String emailContent = + await apiService.fetchEmailContent([email.id], email.list); + // print('content below'); + // print(emailContent); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EmailView( + emailContent: emailContent, + from: email.from, + name: email.name, + to: email.to.toString(), + subject: email.subject, + date: email.date.toString(), + id: email.id.toString(), + ), + ), + ); + }, + ); + }, + separatorBuilder: (context, index) => Divider(), + ), + // child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // Text("Results for: $query", style: TextStyle(fontSize: 24)), + // // Display the actual data + // Text(result[0].name), // Accessing the first result safely + // Text(result[0].from), // Displaying the 'from' field as an example + // Text(result[0].hash), + // Text(result[0].subject), + // Text(result[0].uid.toString()), + // Text(result[0].list), + // Text(result[0].id), + + // // Add more fields or customize the display + // // SerializableEmailListScreen(emails: result, getEmailContent: getEmailContent) + // // Expanded( + + // // child: + // // ), + // ], + ); + // ); + } + }, + ); + } + + @override + void dispose() { + _tabController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + drawer: FolderDrawer( + apiService: apiService, + onFolderTap: (folder) { + _emailPageKey.currentState?.updateSelectedFolder(folder); + }, + ), + body: Stack( + children: [ + Row( + children: [ + // Sidebar + if (_isSidebarOpen) + Container( + width: 70, + color: Color.fromARGB(17, 96, 122, 135), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + leading: Icon(Icons.home), + onTap: () { + // Navigate to Home + }, + ), + ListTile( + leading: Icon(Icons.settings), + onTap: () { + // Navigate to Settings + }, + ), + ListTile( + leading: Icon(Icons.email), + onTap: () { + _scaffoldKey.currentState?.openDrawer(); + }, + ), + Spacer(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Align( + alignment: Alignment.bottomLeft, + child: IconButton( + icon: Icon(Icons.close, color: Colors.white), + onPressed: () { + setState(() { + _isSidebarOpen = false; + }); + }, + ), + ), + ), + ], + ), + ), + // Main content + Expanded( + child: Column( + children: [ + Container( + padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 4.0), + color: Color.fromARGB(42, 36, 102, 132), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 800, + height: 40, + child: TextField( + decoration: InputDecoration( + hintText: 'Search...', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.search), + ), + onSubmitted: (value) { + if (value.isNotEmpty) { + _performSearch(value, _selectedOption); + } + //this is the input box i mentioned + // if (value == '') { + // setState(() { + // querySearches = false; + // }); + // } + // Future> results = apiService + // .sonicSearch('INBOX', 20, 0, value); + // // print(value); + // print(results); + // setState(() { + // querySearches = true; + // }); + }, + ), + ), + SizedBox( + width: 16, + ), + Container( + width: 80, + height: 40, + child: ElevatedButton( + onPressed: _showOptionsSearchDialog, + child: Icon(Icons.manage_search), + ), + ) + ], + ), + ), + Container( + padding: EdgeInsets.all(0.0), + color: Color.fromARGB(42, 36, 102, 132), + child: Row( + children: [ + Container( + height: 2, + ) + ], + ), + ), + Container( + color: Color.fromARGB(255, 131, 110, 143), + child: TabBar( + controller: _tabController, + isScrollable: true, + tabs: _tabs + .asMap() + .entries + .map((entry) => Tab( + child: Row( + children: [ + Text(entry.value), + if (entry.value != 'Emails') + GestureDetector( + onTap: () => _removeTab(entry.key), + child: Icon(Icons.close, size: 16), + ), + ], + ), + )) + .toList(), + labelColor: Colors.white, + indicatorColor: Colors.white, + ), + ), + Container( + // alignment: Alignment.topLeft, + padding: EdgeInsets.all(8.0), + color: Colors.white, + child: Row( + children: [ + ElevatedButton( + onPressed: () { + _emailPageKey.currentState!.isBackDisabled ? null: _emailPageKey.currentState + ?.updatePagenation('back'); + }, + child: Icon(Icons.navigate_before), + ), + Builder( + builder: (context) { + final emailState = _emailPageKey.currentState; + if (emailState == null) { + // Schedule a rebuild once the state is available + Future.microtask(() => setState(() {})); + return Text('Loading...'); + } + + return ValueListenableBuilder( + valueListenable: emailState.currentPageNotifier, + builder: (context, value, _) => Text('$value'), + ); + }, + ), + ElevatedButton( + onPressed: () { + _emailPageKey.currentState + ?.updatePagenation('next'); + }, + child: Icon(Icons.navigate_next), + ), + ], + ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: _tabs.map((tab) { + return _tabWidgets[tab] ?? + Center(child: Text("No content found")); + // return Center( + // child: EmailPage( + // key: _emailPageKey, + // )); + }).toList(), + ), + ), + + // if (_tabs.isEmpty) + // Expanded( + // child: EmailPage(key: _emailPageKey), + // ), + // if (_tabs.isNotEmpty) + // Expanded( + // // child: Text('supposed to be mails'), + // child: TabBarView( + // controller: _tabController, + // children: _tabs + // .map((tab) => Center(child: Text('Results for $tab'))) + // .toList(), + // ), + // ), + ], + ), + ), + ], + ), + if (!_isSidebarOpen) + Positioned( + bottom: 16, + left: 16, + child: FloatingActionButton( + child: Icon(Icons.menu), + onPressed: () { + setState(() { + _isSidebarOpen = true; + }); + }, + ), + ), + ], + ), + ); + } +} +// void _showPopupMenu(BuildContext context, Offset position) async { +// final RenderBox overlay = +// Overlay.of(context).context.findRenderObject() as RenderBox; + +// await showMenu( +// context: context, +// position: RelativeRect.fromLTRB( +// position.dx, +// position.dy, +// overlay.size.width - position.dx, +// overlay.size.height - position.dy, +// ), +// items: >[ +// PopupMenuItem( +// value: 'Open', +// child: Text('Open'), +// ), +// PopupMenuItem( +// value: 'Reply', +// child: Text('Reply'), +// ), +// PopupMenuItem( +// value: 'Delete', +// child: Text('Delete'), +// ), +// ], +// ); +// } +// } -- 2.34.1 From b2fd9d16cc0ea310f9632e476fdc3f989beb9815 Mon Sep 17 00:00:00 2001 From: juan Date: Wed, 7 May 2025 16:17:43 -0400 Subject: [PATCH 5/7] move_email endpoint --- lib/api_service.dart | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/api_service.dart b/lib/api_service.dart index 37b6736..75f21a8 100644 --- a/lib/api_service.dart +++ b/lib/api_service.dart @@ -143,10 +143,32 @@ class ApiService { return content; } - // void _addMailBox async(BuildContext context){ - // //add email folder - // showDialog(context: context, builder: builder) - // } + Future moveEmail(String fromFolder, String uID, String toFolder) async { + var url = Uri.http('$ip:$port', 'move_email'); + Map requestBody = { + 'from': fromFolder, + 'uid': uID, + 'to': toFolder, + }; + try { + var response = await http.post( + url, + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode(requestBody), + ); + if (response.statusCode == 200) { + print('response body ${response.body}'); + return true; + } else { + print('error '); + } + } catch (e) { + print(e); + } + return false; + } Future> fetchFolders() async { try { @@ -176,7 +198,7 @@ class ApiService { print('response body: ${response.body}'); } else { print('Error: ${response.statusCode}, response body: ${response.body}'); - } + } } catch (e) { print('error making post req: $e'); } -- 2.34.1 From 2677625b5461d8ed718e59d5bd805da4d6017250 Mon Sep 17 00:00:00 2001 From: juan Date: Thu, 8 May 2025 01:06:53 -0400 Subject: [PATCH 6/7] fixed issue that at rebuild it would go in a roulete of fetching emails every time --- lib/email.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/email.dart b/lib/email.dart index 5a4c396..12b9b83 100644 --- a/lib/email.dart +++ b/lib/email.dart @@ -41,7 +41,7 @@ class EmailListScreen extends StatelessWidget { to: email.to.toString(), subject: email.subject, date: email.date.toString(), - id: email.id.toString(), + id: email.id.toString(), //i think this is thread id? ), ), ); @@ -77,6 +77,7 @@ class EmailPageState extends State { super.initState(); widget.page = page; isBackDisabled = true; + _fetchEmails(); } String getPage() => widget.page.toString(); @@ -91,7 +92,6 @@ class EmailPageState extends State { } // String getPage() => widget.page.toString(); - void updatePagenation(String option) { if (option == "next") { @@ -108,7 +108,7 @@ class EmailPageState extends State { currentPageNotifier.value = widget.page; if (widget.page == 1) { isBackDisabled = true; - print("back dis"); + print("back disabled"); } }); } @@ -118,7 +118,6 @@ class EmailPageState extends State { } void _fetchEmails() async { - // print(selectedFolder) try { List fetchedEmails = await apiService .fetchEmailsFromFolder(widget.selectedFolder, widget.offset); @@ -134,7 +133,6 @@ class EmailPageState extends State { @override Widget build(BuildContext context) { - _fetchEmails(); return Scaffold( body: EmailListScreen( emails: emails, -- 2.34.1 From 9297468f6fc086031d08a704283e79b8eeb69f1c Mon Sep 17 00:00:00 2001 From: juan Date: Thu, 8 May 2025 16:00:46 -0400 Subject: [PATCH 7/7] finished api calls for moving an email, cleaned, and made clearer logs for errors in api calls --- lib/api_service.dart | 65 +++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/lib/api_service.dart b/lib/api_service.dart index 75f21a8..b9ffdf8 100644 --- a/lib/api_service.dart +++ b/lib/api_service.dart @@ -18,9 +18,11 @@ import 'dart:js' as js; class ApiService { static String ip = ""; static String port = ""; - static List threadAttachments = []; + static List threadAttachments = + []; //holds attachments of the thread static String currFolder = ""; - static List currThread = []; + static List currThread = []; //holds the email ids of the thread + static String currThreadID = ""; //picked an email it prints the threadID Future> fetchEmailsFromFolder( String folder, int pagenitaion) async { @@ -31,7 +33,6 @@ class ApiService { 'offset': pagenitaion.toString(), }); var response = await http.get(url); - // print(response); List allEmails = []; if (response.statusCode == 200) { @@ -143,13 +144,52 @@ class ApiService { return content; } - Future moveEmail(String fromFolder, String uID, String toFolder) async { + Future> threadsInSerializable( + String thread_id) async { + // grab all of the emails in thread anyways, for the future it'll come in handy + var url = Uri.http('$ip:$port', 'get_thread_messages', {'id': thread_id}); + try { + var response = await http.get(url); + if (response.statusCode == 200) { + List json = jsonDecode(response.body); + List serializableMessages = []; + for (var mail in json) { + serializableMessages.add(SerializableMessage.fromJson(mail)); + } + return serializableMessages; + } else { + print( + "failed get request with status code ${response.statusCode}, and body ${response.body}"); + } + } catch (e) { + print("caught in threadInSerializable method error: $e"); + } + + return []; + } + + Future moveEmail( + //only moves the first email of the thread //or perhaps should do the last + String fromFolder, + String thread_id, + String toFolder) async { var url = Uri.http('$ip:$port', 'move_email'); + + List mailsInSerializable = + await this.threadsInSerializable(thread_id); + + if (mailsInSerializable.isEmpty) { + return false; + } + + SerializableMessage firstMail = mailsInSerializable[0]; + Map requestBody = { 'from': fromFolder, - 'uid': uID, + 'uid': firstMail.uid.toString(), 'to': toFolder, }; + try { var response = await http.post( url, @@ -162,10 +202,10 @@ class ApiService { print('response body ${response.body}'); return true; } else { - print('error '); + print('error ${response.statusCode} ${response.body}'); } } catch (e) { - print(e); + print("failed trying to post move_email, with error: $e"); } return false; } @@ -253,13 +293,6 @@ class ApiService { } Future logIn(String json) async { - // var url = Uri.https('') - // try{ - // String response = await http.post( - // url - // ); - // } - return false; } @@ -298,7 +331,6 @@ class ApiService { var response = await http.get(url); if (response.statusCode == 200) { var result = response.body; - // print(result); Map attachmentData = json.decode(result); AttachmentResponse data = AttachmentResponse.fromJson(attachmentData); print("data $data"); @@ -449,7 +481,8 @@ class _EmailViewState extends State { @override Widget build(BuildContext context) { - // print(currentContent); + // print("thread id ${widget.id}"); + ApiService.currThreadID = widget.id; return Scaffold( appBar: AppBar( title: Text(widget.name), -- 2.34.1