import 'dart:js_interop'; import 'package:web/web.dart' as web; 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; final String threadIDs; CollapsableEmails( {required this.thread, required this.threadHTML, required this.threadIDs}); @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 viewtypeIDs = []; //IDs of the viewtypes, order matters List heightOfViewTypes = []; //the height of each viewtype List emailsInThread = []; bool _isLoaded = false; static bool _isListenerRegistered = false; static bool left = true; static bool right = true; web.EventListener? _listener; List hirarchy = ["h1", "h2", "h3", "h4", "h5", "h6", "p"]; Map hirarchyDict = { "h1": 1, "h2": 2, "h3": 3, "h4": 4, "h5": 6, "h6": 7, "p": 8, "ul": 8, "li": 8, }; List tagsCollected = []; String markdown = ''; List> sentinel = []; int level = 0; AugmentTree zoomTreeRoot = AugmentTree(); late AugmentTree currentZoomNode; bool zoomOut = false; bool zoomIn = true; @override void initState() { super.initState(); _markdownConverter(); // _registerViewFactory(widget.threadHTML); _serializableData(widget.threadIDs); // this _markdown2Tree(markdown); _keyListener(); _buildForZooms(); } @override void dispose() { if (_listener != null) { web.window.document.removeEventListener('keydown', _listener!); _listener = null; _isListenerRegistered = false; } super.dispose(); } void _markdownConverter() async { markdown = html2md.convert(widget.threadHTML[0]); } 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 print('is 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)) { // print("equals??"); tree.children.add(newNode); newNode.parent = tree; } } } void _markdown2Tree(String text) { print("started markdown2tree"); final List nakedList = md.Document().parseLines(text.split('\n')); List pList = []; List h1List = []; List h2List = []; List h3List = []; List h4List = []; List h5List = []; List h6List = []; for (var node in nakedList) { //maybe do an add function, but isn't this it? if (node is md.Element) { // print(node.textContent); AugmentTree temp = AugmentTree(); temp.data = node.textContent; temp.ogTag = node.tag; if (node.tag == 'h1') { h1List.add(node.textContent); _add2Tree(zoomTreeRoot, node); } else if (node.tag == 'h2') { // i dont add any since i dont have it, maybe the function makes sense h2List.add(node.textContent); _add2Tree(zoomTreeRoot, node); // fix this } else if (node.tag == 'h3') { h3List.add(node.textContent); _add2Tree(zoomTreeRoot, node); } else if (node.tag == 'h4') { h4List.add(node.textContent); //this broke it _add2Tree(zoomTreeRoot, node); // change to temp } else if (node.tag == 'h5') { h5List.add(node.textContent); print(node.textContent); _add2Tree(zoomTreeRoot, node); } else if (node.tag == 'h6') { h6List.add(node.textContent); _add2Tree(zoomTreeRoot, node); // fix this } else if (node.tag == 'p' || node.tag == 'ul' || node.tag == 'li') { pList.add(node.textContent); _add2Tree(zoomTreeRoot, node); // fix this } } } this.sentinel = [h1List, h2List, h3List, h4List, h5List, h6List, pList]; sentinel.removeWhere((hList) => hList.isEmpty); currentZoomNode = zoomTreeRoot; if (!mounted) return; setState(() { _isLoaded = true; }); } void handleKeyDownMD(web.Event event) async { final keyEvent = event as web.KeyboardEvent; if (!mounted) return; if (keyEvent.key == 'a') { print("key a"); setState(() { level = (level - 1).clamp(0, sentinel.length - 1); }); } else if (keyEvent.key == "b") { print("b"); setState(() { level = (level + 1).clamp(0, sentinel.length - 1); }); } } void _goToChildren(int index) async { final target = currentZoomNode.children[index]; if (target.children.isNotEmpty) { setState(() { currentZoomNode = target; }); } else { print("This child has no further children."); } } void _goToParent() async { if (currentZoomNode.parent != null) { setState(() { currentZoomNode = currentZoomNode.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; }); } void handleKeyDownHTML(web.Event event) { final keyEvent = event as web.KeyboardEvent; if (keyEvent.key == 'G') { print('You pressed the "G" key!'); final rightPurpleNums = web.document.getElementsByClassName("right"); _CollapsableEmailsState.right = !_CollapsableEmailsState.right; final newOpacity = _CollapsableEmailsState.right ? '1.0' : '0.0'; for (int i = 0; i < rightPurpleNums.length; i++) { final currentElement = rightPurpleNums.item(i) as web.HTMLElement; currentElement.style.opacity = newOpacity; } } else if (keyEvent.key == 'H') { print('You pressed the "H" key!'); final leftPurpleNums = web.document.getElementsByClassName("left"); _CollapsableEmailsState.left = !_CollapsableEmailsState.left; final newOpacity = _CollapsableEmailsState.left ? '1.0' : '0.0'; for (int i = 0; i < leftPurpleNums.length; i++) { final currentElement = leftPurpleNums.item(i) as web.HTMLElement; currentElement.style.opacity = newOpacity; } } else if (keyEvent.key == 'm') { print("you pressed 'm'"); final purpleNums = web.document.getElementsByClassName("purplenumber"); _CollapsableEmailsState.left = true; _CollapsableEmailsState.right = true; for (int i = 0; i < purpleNums.length; i++) { final currentElement = purpleNums.item(i) as web.HTMLElement; currentElement.style.opacity = '1.0'; } } else if (keyEvent.key == 'n') { print("you pressed 'n'"); final purpleNums = web.document.getElementsByClassName("purplenumber"); _CollapsableEmailsState.left = false; _CollapsableEmailsState.right = false; for (int i = 0; i < purpleNums.length; i++) { final currentElement = purpleNums.item(i) as web.HTMLElement; currentElement.style.opacity = '0.0'; } } else if (keyEvent.key == 'w') { print("you pressed 'w'"); } } void _keyListener() { if (_isListenerRegistered) return; _isListenerRegistered = true; // Convert the top-level function to JS-compatible // _listener = handleKeyDownHTML.toJS; _listener = handleKeyDownMD.toJS; web.window.document.addEventListener('keydown', _listener!); } Widget _buildForZooms({Key? key}) { if (!_isLoaded) { return const Center(child: CircularProgressIndicator()); // loading screen } final canZoomOut = currentZoomNode.parent != null; return ListView.builder( key: key, itemCount: currentZoomNode.children.length, itemBuilder: (context, index) { final childNode = currentZoomNode.children[index]; final canZoomIn = childNode.children.isNotEmpty; 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() : null, child: Icon(Icons.north_west_sharp), ), OutlinedButton( onPressed: canZoomIn ? () => _goToChildren(index) : null, child: Icon(Icons.south_east_sharp), ), ], ), SizedBox(width: 12.0), Expanded( child: MarkdownBlock( data: currentZoomNode .children[index].data, // one string of markdown config: MarkdownConfig .darkConfig, // or lightConfig depending on theme ), ), ], ), ), ), ); }, ); } @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(key: ValueKey(currentZoomNode)), ), Divider(), ], ); }, ), ), ]) : const Center(child: CircularProgressIndicator()); } }