Compare commits

...

9 Commits

Author SHA1 Message Date
71707bd1c0 fix for composing widget 2025-07-23 10:30:07 -04:00
b961be3e8b routing, and link handling 2025-07-23 10:29:28 -04:00
c025fbe07a add the compose widget to sidebar 2025-07-23 10:29:13 -04:00
214a60ce1b change in the parameters 2025-07-23 10:28:50 -04:00
07091eb708 data structure for augment capabilities 2025-07-22 23:12:34 -04:00
dda581bda0 super editor and its md plugin 2025-07-22 23:11:05 -04:00
433394a74a compose widget where you can write the email 2025-07-22 23:10:45 -04:00
2465201b0b O(1) optimization 2025-07-18 10:57:50 -04:00
4d75674e8e instance rather than a new classs 2025-07-18 10:57:13 -04:00
12 changed files with 461 additions and 144 deletions

165
lib/Compose.dart Normal file
View File

@ -0,0 +1,165 @@
import 'package:flutter/material.dart';
import 'package:super_editor/super_editor.dart';
import 'package:super_editor_markdown/super_editor_markdown.dart';
class ComposeEmail extends StatefulWidget {
final VoidCallback onClose;
final Function(String) onMinimize;
final Function(String) onSendMessage;
const ComposeEmail({
Key? key,
required this.onMinimize,
required this.onClose,
required this.onSendMessage,
}) : super(key: key);
@override
_ComposeEmailState createState() => _ComposeEmailState();
}
class _ComposeEmailState extends State<ComposeEmail> {
// if one were to alter a mutableDocument, one should only alter the document through EditRequest to the Editor
late final Editor _editor;
late final MutableDocument _document;
late final MutableDocumentComposer _composer;
TextEditingController _emailRecipientController = TextEditingController();
@override
void initState() {
super.initState();
_document = MutableDocument(nodes: [
ParagraphNode(
id: Editor.createNodeId(),
text: AttributedText("hello world!"),
)
]);
_composer = MutableDocumentComposer();
_editor =
createDefaultDocumentEditor(document: _document!, composer: _composer!);
}
@override
void dispose() {
_editor.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Positioned(
bottom: 10.0,
right: 10.0,
child: Material(
elevation: 8.0,
child: Container(
width: 600.0,
height: 616.0,
decoration: BoxDecoration(
color: Colors.white,
),
child: Column(children: [
AppBar(
title: const Text("new message"),
actions: [
IconButton(
onPressed: () {
//TODO: implement minimize, and submit email to drafts
widget.onClose();
},
icon: Icon(Icons.minimize, color: Colors.grey[600])),
IconButton(
onPressed: () {
//TODO: implement that maximizing
},
icon: Icon(Icons.maximize, color: Colors.grey[600])),
IconButton(
onPressed: () {
widget.onClose();
},
icon: Icon(Icons.close, color: Colors.grey[600])),
],
),
Container(
// TODO: WHEN NOT CLICKED ITS ONLY A TEXTFIELD WITH A HINT, AND THEN WHEN CLICKED THIS
width: 500.0,
height: 40.0,
child: Row(
children: [
TextButton(onPressed: () {}, child: Text("To:")),
Expanded(
child: TextField(
controller: _emailRecipientController,
),
),
TextButton(onPressed: () {}, child: Text("Cc")),
SizedBox(
width: 4.0,
),
TextButton(onPressed: () {}, child: Text("Bcc")),
],
),
),
Expanded(
//here the widget goes
child: SuperEditor(
editor: _editor!,
plugins: {MarkdownInlineUpstreamSyntaxPlugin()},
stylesheet: Stylesheet(
rules: [StyleRule(BlockSelector.all, (doc, docNode) {
return {
Styles.maxWidth: 640.0,
Styles.padding: const CascadingPadding.symmetric(horizontal: 24),
Styles.textStyle: const TextStyle(
color: Colors.black,
fontSize: 15,
height: 1.4,
),
};
}),],
inlineTextStyler: defaultInlineTextStyler)
),
)
])),
),
);
}
}
class OverlayService {
static final OverlayService _instance = OverlayService._internal();
factory OverlayService() => _instance;
OverlayService._internal();
OverlayEntry? _overlayEntry;
void showPersistentWidget(BuildContext context) {
if (_overlayEntry != null) {
print("overlay visible");
return;
}
_overlayEntry = OverlayEntry(
builder: (context) => ComposeEmail(
onClose: () {
removeComposeWidget();
},
onMinimize: (String content) {
minimizeComposeWidget(content);
},
onSendMessage: (message) {
print('msg senf form overlay $message');
},
));
Navigator.of(context).overlay?.insert(_overlayEntry!);
print("inserted into tree");
}
void removeComposeWidget() {
_overlayEntry?.remove();
_overlayEntry = null;
}
String minimizeComposeWidget(String content) {
//just hide the overlay but keep its info
return '';
}
}

View File

@ -101,6 +101,7 @@ class _SonicEmailViewState extends State<SonicEmailView> {
onJumpToNumbering: _scrollToNumber,
onViewspecs: _handleViewspecs,
onFiltering: _handleFiltering,
emails: [widget.email.name], subject: '', rootAugment: null,
),
Row(
// title of email

View File

@ -90,6 +90,11 @@ class _CollapsableEmailsState extends State<CollapsableEmails> {
super.dispose();
}
List<String> getThreads() {
return widget.thread;
}
void _add2Tree(AugmentTree tree, md.Element node2add) {
// adds node to its corresponding place
AugmentTree newNode = AugmentTree();

View File

@ -9,13 +9,22 @@ class CollapsableEmails extends StatefulWidget {
CollapsableEmails(
{required this.thread,
required this.threadMarkdown,
required this.threadIDs, String? targetJumpNumbering, String? targetViewspecs, String? targetFiltering});
required this.threadIDs, String? targetJumpNumbering, String? targetViewspecs, String? targetFiltering, required String nameOfDocument});
get getThreads => null;
get getAugmentRoot => null;
@override
State<CollapsableEmails> createState() => _CollapsableEmailsState();
}
class _CollapsableEmailsState extends State<CollapsableEmails> {
List<String> getThreads() {
return widget.thread;
}
@override
Widget build(BuildContext context) {
return Scaffold(body: Text("collapsable stud"));

View File

@ -176,22 +176,9 @@ class _CollapsableEmailsState extends State<CollapsableEmails> {
AugmentTree temp = AugmentTree();
temp.data = node.textContent;
temp.ogTag = node.tag;
if (node.tag == 'h1') {
// make this O(1)
//why did i do this???
if ( hirarchyDict.containsKey(node.tag)) {
_add2Tree(zoomTreeRoot, node);
} else if (node.tag == 'h2') {
// i dont add any since i dont have it, maybe the function makes sense
_add2Tree(zoomTreeRoot, node); // fix this
} else if (node.tag == 'h3') {
_add2Tree(zoomTreeRoot, node);
} else if (node.tag == 'h4') {
_add2Tree(zoomTreeRoot, node); // change to temp
} else if (node.tag == 'h5') {
_add2Tree(zoomTreeRoot, node);
} else if (node.tag == 'h6') {
_add2Tree(zoomTreeRoot, node); // fix this
} else if (node.tag == 'p' || node.tag == 'ul' || node.tag == 'li') {
_add2Tree(zoomTreeRoot, node); // fix this
}
}
}

View File

@ -66,6 +66,7 @@ class _EmailViewState extends State<EmailView> {
onJumpToNumbering: _scrollToNumber,
onViewspecs: _viewSpecs,
onFiltering: _filteringQuery,
emails: widget.messages, subject: '', rootAugment: null,
),
Row(
children: [

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'dart:ui_web' as ui;
import 'augment.dart';
// import 'dart:js_interop' as js; //eventually for manipulating css
import 'collapsableEmails.dart';
import 'api_service.dart';
@ -35,8 +34,30 @@ class _EmailViewState extends State<EmailView> {
late Key iframeKey;
late String currentContent;
late String viewTypeId; //make this a list too???
Future<List<Map<String, dynamic>>>? _markerPositionsFuture;
// TextEditingController _jumpController = TextEditingController();
late EmailToolbar toolbarInstance = EmailToolbar(
onJumpToNumbering: _handleJumpRequest,
onViewspecs: _handleViewspecsRequest,
onButtonPressed: () => {print("email tool bar pressed")},
onFiltering: _handleFiltering,
emails: widget.messages,
subject: widget.subject,
rootAugment: localCollapsable.getAugmentRoot(),
);
late CollapsableEmails localCollapsable = CollapsableEmails(
//change here
thread: widget.messages, //this wont work in serializable
// threadHTML: widget.emailContent, // old html
threadMarkdown: widget.emailContent,
threadIDs: widget.id,
targetJumpNumbering: _targetJumpNumbering,
targetViewspecs: _targetViewspecs,
targetFiltering: _queryFiltering,
nameOfDocument: widget.subject,
);
final hardcodedMarkers = [
{'id': 'marker1', 'x': 50, 'y': 100},
{'id': 'marker2', 'x': 150, 'y': 200},
@ -78,11 +99,11 @@ class _EmailViewState extends State<EmailView> {
});
}
// TODO: void _invisibility(String ) //to make purple numbers not visible
@override
Widget build(BuildContext context) {
ApiService.currThreadID = widget.id;
// AugmentClasses localAugment = AugmentClasses(localCollapsable);
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
@ -91,12 +112,7 @@ class _EmailViewState extends State<EmailView> {
children: [
Column(
children: [
EmailToolbar(
onJumpToNumbering: _handleJumpRequest,
onViewspecs: _handleViewspecsRequest,
onButtonPressed: () => {print("email tool bar pressed")},
onFiltering: _handleFiltering,
),
toolbarInstance,
Row(
// title of email
children: [
@ -133,16 +149,7 @@ class _EmailViewState extends State<EmailView> {
],
),
Expanded(
child: CollapsableEmails(
//change here
thread: widget.messages, //this wont work in serializable
// threadHTML: widget.emailContent, // old html
threadMarkdown: widget.emailContent,
threadIDs: widget.id,
targetJumpNumbering: _targetJumpNumbering,
targetViewspecs: _targetViewspecs,
targetFiltering: _queryFiltering,
),
child: localCollapsable,
),
],
),

View File

@ -5,6 +5,7 @@ import 'structs.dart';
import 'api_service.dart';
import 'package:flutter/material.dart';
import 'email.dart';
import 'Compose.dart';
class HomeScreen extends StatefulWidget {
@override
@ -44,7 +45,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
});
}
void _showOptionsSearchDialog () async {
void _showOptionsSearchDialog() async {
List<String> folders = await apiService.fetchFolders();
if (mounted) {
@ -65,7 +66,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
setState(() {
_selectedOption = value;
});
Navigator.of(context).pop(); // Close the dialog on selection
Navigator.of(context)
.pop(); // Close the dialog on selection
},
),
);
@ -84,9 +86,9 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
],
);
},
);}
);
}
}
// Remove a tab
void _removeTab(int index) {
@ -135,24 +137,21 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
//call the foldable
List<String> emailContent = // list of the html
await apiService.fetchEmailContent([email.id], email.list);
await apiService
.fetchEmailContent([email.id], email.list);
// List<String> emailIds = email.messages;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>SonicEmailView(
email: email,
emailHTML: emailContent[0])
),
builder: (context) => SonicEmailView(
email: email, emailHTML: emailContent[0])),
);
},
);
},
separatorBuilder: (context, index) => Divider(),
),
);
}
},
@ -170,7 +169,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.onPrimary,
body: Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0 , 20),
padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: Scaffold(
key: _scaffoldKey,
drawer: FolderDrawer(
@ -195,6 +194,12 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: Icon(Icons.edit_note_sharp),
onTap: () {
OverlayService()
.showPersistentWidget(context);
}),
ListTile(
leading: Icon(Icons.home),
onTap: () {
@ -219,7 +224,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
child: Align(
alignment: Alignment.bottomLeft,
child: IconButton(
icon: Icon(Icons.close, color: Colors.white),
icon:
Icon(Icons.close, color: Colors.white),
onPressed: () {
setState(() {
_isSidebarOpen = false;
@ -256,7 +262,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
),
onSubmitted: (value) {
if (value.isNotEmpty) {
_performSearch(value, _selectedOption);
_performSearch(
value, _selectedOption);
}
//this is the input box i mentioned
// if (value == '') {
@ -314,8 +321,10 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
Text(entry.value),
if (entry.value != 'Emails')
GestureDetector(
onTap: () => _removeTab(entry.key),
child: Icon(Icons.close, size: 16),
onTap: () =>
_removeTab(entry.key),
child: Icon(Icons.close,
size: 16),
),
],
),
@ -333,14 +342,17 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
children: [
ElevatedButton(
onPressed: () {
_emailPageKey.currentState!.isBackDisabled ? null: _emailPageKey.currentState
_emailPageKey.currentState!.isBackDisabled
? null
: _emailPageKey.currentState
?.updatePagenation('back');
},
child: Icon(Icons.navigate_before),
),
Builder(
builder: (context) {
final emailState = _emailPageKey.currentState;
final emailState =
_emailPageKey.currentState;
if (emailState == null) {
// Schedule a rebuild once the state is available
Future.microtask(() => setState(() {}));
@ -348,8 +360,10 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
}
return ValueListenableBuilder<int>(
valueListenable: emailState.currentPageNotifier,
builder: (context, value, _) => Text('$value'),
valueListenable:
emailState.currentPageNotifier,
builder: (context, value, _) =>
Text('$value'),
);
},
),

View File

@ -1,15 +1,22 @@
import 'dart:convert';
// import 'package:crab_ui/api_service.dart';
import 'package:go_router/go_router.dart';
import 'api_service.dart';
import 'home_page.dart';
// import 'home_page.dart';
import 'package:flutter/material.dart';
// import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:http/http.dart' as http;
// import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart' show rootBundle;
class AuthService {
class AuthService extends ChangeNotifier {
Future<bool> isUserLoggedIn() async {
ApiService.ip = '192.168.2.38';
ApiService.port = '3001';
print("setted up");
return true;
try {
final response =
await http.get(Uri.http('localhost:6823', 'read-config'));
@ -83,6 +90,7 @@ class LoginPage extends StatefulWidget {
}
class SplashScreen extends StatefulWidget {
//entry point
@override
_SplashScreenState createState() => _SplashScreenState();
}
@ -92,19 +100,24 @@ class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkLoginStatus();
});
}
Future<void> _checkLoginStatus() async {
// SharedPreferences prefs = await SharedPreferences.getInstance();
// print(prefs);
// bool isLoggedIn = prefs.getBool('isLoggedIn') ?? false;
await Future.delayed(const Duration(seconds: 1));
bool isLoggedIn = await _authService.isUserLoggedIn();
print("is loogeed in $isLoggedIn");
print("is logged in $isLoggedIn");
if (isLoggedIn) {
Navigator.pushReplacementNamed(context, '/home');
context.go("/home");
// Navigator.pushReplacementNamed(context, '/home');
} else {
Navigator.pushReplacementNamed(context, '/login');
context.go("/login");
// Navigator.pushReplacementNamed(context, '/login');
}
}
@ -112,7 +125,7 @@ class _SplashScreenState extends State<SplashScreen> {
Widget build(BuildContext context) {
return Center(
child: Scaffold(
body: Center(child: CircularProgressIndicator()),
body: Center(child: CircularProgressIndicator()), //nothing happens
),
);
}
@ -132,6 +145,7 @@ class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
Future<bool> setIp(String ip) async {
//this is not done :sob: :skull:
// _configManager.setField("api_addr", ip);
return false;
}

View File

@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:markdown_widget/markdown_widget.dart';
import 'api_service.dart';
import 'structs.dart';
class Routinghandler extends StatefulWidget {
Routinghandler(String link, emailID) {
@ -133,6 +134,7 @@ class Routinghandler extends StatefulWidget {
class _RoutingHandlerState extends State<Routinghandler> {
List<String> markdownContent = [];
bool _isLoaded = false;
AugmentTree? aug;
@override
void initState() {
@ -143,13 +145,15 @@ class _RoutingHandlerState extends State<Routinghandler> {
Future<void> _loadMarkdown() async {
String folder = ApiService.currFolder;
// print(folder);
print(widget.getEmailID());
String emailID = widget.emailID;
print("inside _loadMarkdown in routinghandler $emailID");
markdownContent =
await ApiService().fetchMarkdownContent([emailID], "INBOX");
await ApiService().fetchMarkdownContent([emailID], folder);
// print(markdownContent);
aug = AugmentTree.fromMD(markdownContent[0]);
aug!.addNumbering();
setState(() {
_isLoaded = true;
});
@ -177,29 +181,43 @@ class _RoutingHandlerState extends State<Routinghandler> {
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
child: SingleChildScrollView(
child:MarkdownBlock(data: markdownContent[0]))),
);
}
}
//inside here put the bunch rows
//make rows of markdownBlocks, but firstly i need to conveert the content into a tree
// child:MarkdownBlock(data: markdownContent[0])
class LinkViewer extends StatefulWidget {
const LinkViewer({super.key});
@override
State<StatefulWidget> createState() => _LinkViewerState();
}
class _LinkViewerState extends State<LinkViewer> {
@override
Widget build(BuildContext context) {
// this should be a class that opens a popup of the email on the view it wants
return Scaffold(
appBar: AppBar(
title: Text('url viewer'),
child: Column(children: [
for (int i = 0; i < this.aug!.children![0]!.children.length; i++)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// if (leftNumbering)
Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 5, 0),
child: Text(
aug!.children![0]!.children![i]!.numbering,
style: TextStyle(
color: Color(Colors.purple[400]!.value)),
),
body: Column(
children: [],
));
),
Expanded(
child: Align(
alignment: Alignment.topLeft,
child: Wrap(children: [
MarkdownBlock(
data: aug!.children![0]!.children![i]!.data ?? ''),
],)
)
),
Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 5, 0),
child: Text(
aug!.children![0]!.children![i]!.numbering,
style: TextStyle(
color: Color(Colors.purple[400]!.value)),
),
),
]),
]))));
}
}

View File

@ -1,6 +1,7 @@
//data structures
import 'dart:typed_data';
import 'package:markdown/markdown.dart' as md;
class GetThreadResponse {
final int id;
@ -157,6 +158,94 @@ class AugmentTree {
AugmentTree? parent;
String ogTag = '';
String numbering = '';
Map<String, int> hirarchyDict = {
"h1": 1,
"h2": 2,
"h3": 3,
"h4": 4,
"h5": 5,
"h6": 6,
"p": 8,
"ul": 8,
"li": 8,
};
AugmentTree();
AugmentTree.fromMD(String rawMD) {
//makes raw MD into an augmentTree
print("started markdown2tree");
final List<md.Node> nakedList = md.Document().parseLines(rawMD.split(
'\n')); //emails md is the index of the email in the thread, since this only handles one thus it shall be removed
// AugmentTree zoomTreeRoot = AugmentTree();
for (var node in nakedList) {
//maybe do an add function, but isn't this it?
if (node is md.Element) {
AugmentTree temp = AugmentTree();
temp.data = node.textContent;
temp.ogTag = node.tag;
if (hirarchyDict.containsKey(node.tag)) {
// make this O(1)
_add2Tree(this, node);
// print(node);
}
}
}
this.addNumbering();
}
void _add2Tree(AugmentTree tree, md.Element node2add) {
// adds node to its corresponding place
AugmentTree newNode = AugmentTree();
newNode.setData(node2add.textContent);
newNode.ogTag = node2add.tag;
// cases,
//1. a node that comes is lower than the root.children last, if so it goes beneath it
if (tree.children.isEmpty) {
// new level to be created when totally empty
tree.children.add(newNode);
newNode.parent = tree;
} else if (tree.children.isNotEmpty &&
tree.children.last.ogTag.isNotEmpty) {
if ((hirarchyDict[node2add.tag] ??
-1) < // e.g. new node is h1 and old is h2, heapify
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
//have to figure out the borthers
//assuming it all goes right
if ((hirarchyDict[node2add.tag] ?? -1) == -1 ||
(hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) {
print(
'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} < ${hirarchyDict[tree.children.last.ogTag] ?? -1}');
return;
} else if (tree.children.last.parent == null) {
// becomes the new top level
for (AugmentTree brother in tree.children) {
brother.parent = newNode;
}
tree.children = [newNode];
} else {
newNode.parent = tree;
tree.children.add(newNode);
}
} else if ((hirarchyDict[node2add.tag] ??
-1) > // go down e.g. new node is h3 and old is h2 or something
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
if ((hirarchyDict[node2add.tag] ?? -1) == -1 ||
(hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) {
print(
'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} > ${hirarchyDict[tree.children.last.ogTag] ?? -1}');
print("-1 ${tree.children.last.ogTag}");
return;
}
_add2Tree(tree.children.last, node2add);
} else if ((hirarchyDict[node2add.tag] ?? -1) ==
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
tree.children.add(newNode);
newNode.parent = tree;
}
}
}
void setData(String data) {
this.data = data;
@ -197,14 +286,19 @@ class AugmentTree {
}
}
//perhaps make a struct that builds augment tree, since its so complex and needs to be like recursive
class MarkdownParsed{
class MarkdownParsed {
//struct for holding the MD given in endpoint //not used
final String text;
MarkdownParsed({required this.text});
factory MarkdownParsed.fromJson(Map<String, String> json){
factory MarkdownParsed.fromJson(Map<String, String> json) {
return MarkdownParsed(
text: json['md'] ?? '',
);
}
}
//should make an md to tree class/struct
// make a for loop of rows with markdown

View File

@ -31,6 +31,8 @@ dependencies:
markdown_widget: ^2.3.2+8
markdown: ^7.3.0
go_router: ^16.0.0
super_editor: ^0.3.0-dev.27
super_editor_markdown: 0.1.8
dev_dependencies:
flutter_test: