import 'package:english_words/english_words.dart'; import 'package:flutter/material.dart'; import 'api_service.dart'; import 'structs.dart'; import 'package:html2md/html2md.dart' as html2md; import 'package:markdown_widget/markdown_widget.dart'; import 'package:markdown/markdown.dart' as md; class CollapsableEmails extends StatefulWidget { final List thread; // email id's in the form xyz@gmail.com // final List threadHTML; to be replaced with the MD final List threadMarkdown; final String threadIDs; final String? targetJumpNumbering; final String? targetViewspecs; const CollapsableEmails({ required this.thread, // required this.threadHTML, required this.threadMarkdown, required this.threadIDs, this.targetJumpNumbering, this.targetViewspecs, }); @override State createState() => _CollapsableEmailsState(); } class _CollapsableEmailsState extends State { List emailsHTML = []; //html of the emails in the thread // build attachments with the forldar name and id Set _expandedEmails = {}; //open emails List emailsInThread = []; bool _isLoaded = false; List hirarchy = ["h1", "h2", "h3", "h4", "h5", "h6", "p"]; Map hirarchyDict = { "h1": 1, "h2": 2, "h3": 3, "h4": 4, "h5": 5, "h6": 6, "p": 8, "ul": 8, "li": 8, }; List tagsCollected = []; List allMarkdown = []; List> sentinel = []; int level = 0; AugmentTree zoomTreeRoot = AugmentTree(); // late AugmentTree currentZoomNode; late List currentZoomTree = []; bool zoomOut = false; bool zoomIn = true; late List threadNodes = []; static bool leftNumbering = true; static bool rightNumbering = true; bool showWhole = false; @override void initState() { super.initState(); threadNodes = []; currentZoomTree = []; // _markdownConverter(); _serializableData(widget.threadIDs); // this _markdown2Tree(widget.threadMarkdown); } @override void didUpdateWidget(covariant CollapsableEmails oldWidget) { // TODO: implement didUpdateWidget super.didUpdateWidget(oldWidget); if (widget.targetJumpNumbering != null && widget.targetJumpNumbering != oldWidget.targetJumpNumbering) { _handleJump(widget.targetJumpNumbering!); } if (widget.targetViewspecs != null && widget.targetViewspecs != oldWidget.targetViewspecs) { _handleViewspecs(widget.targetViewspecs!); } } @override void dispose() { super.dispose(); } // void _markdownConverter() async { // // to list of markdown // // for (int email = 0; email < widget.threadHTML.length; email++) { // // String markdown = html2md.convert(widget.threadHTML[email]); // // allMarkdown.add(markdown); // // } // for (int email = 0; email < widget.threadMarkdown.length; email++) { // allMarkdown.add(email); // } // } 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 _markdown2Tree(List text) { print("started markdown2tree"); for (int emailsMD = 0; emailsMD < text.length; emailsMD++) { final List nakedList = md.Document().parseLines(text[emailsMD].split('\n')); 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 (node.tag == 'h1') { // make this O(1) _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 } } } zoomTreeRoot.addNumbering(); threadNodes.add(zoomTreeRoot); currentZoomTree.add(zoomTreeRoot); } if (!mounted) return; setState(() { _isLoaded = true; }); } void _goToChildren(int indexThread, int index) async { final target = currentZoomTree[indexThread].children[index]; if (target.children.isNotEmpty) { setState(() { currentZoomTree[indexThread] = target; }); } else { print("This child has no further children."); } } void _goToParent(int indexThread) async { if (currentZoomTree[indexThread].parent != null) { setState(() { currentZoomTree[indexThread] = currentZoomTree[indexThread].parent!; }); } else { print("Already at root."); } } void _serializableData(String threadID) async { emailsInThread = await ApiService().threadsInSerializable(threadID); print("done thread serializable"); if (!mounted) return; setState(() { _isLoaded = true; }); } Widget _buildForZooms(int indexThread) { // IF I GIVE IT THE INDEX???? if (!_isLoaded) { return const Center(child: CircularProgressIndicator()); // loading screen } final AugmentTree currentZoomNodeForThisEmail = currentZoomTree[indexThread]; final canZoomOut = currentZoomNodeForThisEmail.parent != null; return ListView.builder( itemCount: currentZoomNodeForThisEmail.children.length, itemBuilder: (context, index) { final childNode = currentZoomNodeForThisEmail.children[index]; final canZoomIn = childNode.children.isNotEmpty; // currentZoomNodeForThisEmail.addNumbering(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), child: Material( elevation: 1, borderRadius: BorderRadius.circular(12), color: Theme.of(context).colorScheme.surface, surfaceTintColor: Theme.of(context).colorScheme.surfaceBright, child: Padding( padding: const EdgeInsets.all(16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 4.0, children: [ OutlinedButton( onPressed: canZoomOut ? () => _goToParent(indexThread) : null, child: Icon(Icons.north_west_sharp), ), OutlinedButton( onPressed: canZoomIn ? () => _goToChildren(indexThread, index) : null, child: Icon(Icons.south_east_sharp), ), ], ), SizedBox(width: 12.0), if (leftNumbering) Padding( padding: const EdgeInsets.fromLTRB(0, 10, 5, 0), child: Text( childNode.numbering, style: TextStyle(color: Color(Colors.purple[400]!.value)), ), ), Expanded( child: MarkdownBlock( data: childNode.data, // data: currentZoomNode // .children[index].data, // one string of markdown config: MarkdownConfig .darkConfig, // or lightConfig depending on theme ), ), if (rightNumbering) Padding( padding: const EdgeInsets.fromLTRB(5, 10, 5, 0), child: Text( childNode.numbering, style: TextStyle(color: Color(Colors.purple[400]!.value)), ), ), ], ), ), ), ); }, ); } void _handleJump(String queryNumbering) { print(queryNumbering); if (queryNumbering.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Please enter a numbering to jump to.')), ); return; } final int targetEmailIndex = _expandedEmails.first; if (targetEmailIndex >= threadNodes.length) { // Error handling return; } final AugmentTree rootOfCurrentEmail = threadNodes[targetEmailIndex]; final AugmentTree? foundNode = _findNodeByNumbering(rootOfCurrentEmail, queryNumbering); if (foundNode != null) { setState(() { currentZoomTree[targetEmailIndex] = foundNode; // Update the state }); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Numbering "$queryNumbering" not found.')), ); } } void _handleViewspecs(String viewspecsQuery) { print(viewspecsQuery); if (viewspecsQuery.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Please enter the viewspecs.')), ); return; } final int targetEmailIndex = _expandedEmails.first; if (targetEmailIndex >= threadNodes.length) { // Error handling return; } if (viewspecsQuery.contains('n')) { setState(() { leftNumbering = false; // Update the state rightNumbering = false; }); } if (viewspecsQuery.contains('m')) { setState(() { rightNumbering = true; leftNumbering = true; }); } if (viewspecsQuery.contains('H')) { setState(() { leftNumbering = !leftNumbering; }); } if (viewspecsQuery.contains('G')) { setState(() { rightNumbering = !rightNumbering; }); } if (viewspecsQuery.contains('w')) { setState(() { showWhole = true; }); } // else { // ScaffoldMessenger.of(context).showSnackBar( // SnackBar(content: Text('Numbering "$viewspecsQuery" not found.')), // ); // } } AugmentTree? _findNodeByNumbering(AugmentTree root, String numbering) { //recursively finds the node you mentioned // to find the AugmentTree node corresponding to the `numbering`. if (root.numbering == numbering) { return root; } for (var child in root.children) { final found = _findNodeByNumbering(child, numbering); if (found != null) { return found; } } return null; } @override Widget build(BuildContext context) { return _isLoaded ? Column(children: [ Expanded( child: ListView.builder( itemCount: emailsInThread.length, itemBuilder: (context, index) { final isExpanded = _expandedEmails .contains(index); //check if email is expanded return Column( children: [ ListTile( title: Text(emailsInThread[index].from), trailing: Text(emailsInThread[index].date), onTap: () { setState(() { if (isExpanded) { _expandedEmails.remove(index); } else { _expandedEmails.add(index); } }); }, ), if (isExpanded) ConstrainedBox( constraints: BoxConstraints( minHeight: 100, maxHeight: MediaQuery.of(context).size.height * 0.6, ), child: _buildForZooms(index), //show the tree // child: _buildForZooms(key: ValueKey(currentZoomNode)), ), Divider(), ], ); }, ), ), ]) : const Center(child: CircularProgressIndicator()); } }