import 'dart:collection'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; import 'package:web/web.dart' as web; import 'package:flutter/material.dart'; import 'dart:ui_web' as ui; 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 root = AugmentTree(); @override void initState() { super.initState(); _markdownConverter(); // _registerViewFactory(widget.threadHTML); // _serializableData(widget.threadIDs); // this _markdown2Tree(markdown); _keyListener(); _buildForZooms(level); } @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 { tree.children.add(newNode); } // } else{ // so the node should go high // _add2Tree(tree.children.last, node2add); // } //maybe? // } else { // } } 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; } print("> ${node2add.tag}"); _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"); int highest = 0; AugmentTree zoomTreeRoot = AugmentTree(); 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); } else if (node.tag == 'h3') { h3List.add(node.textContent); root.children.add(temp); _add2Tree(zoomTreeRoot, node); } else if (node.tag == 'h4') { h4List.add(node.textContent); //this broke it _add2Tree(zoomTreeRoot, node); // change to temp if (root.children.isNotEmpty) { root.children.last.children.add(temp); } } else if (node.tag == 'h5') { h5List.add(node.textContent); print(node.textContent); _add2Tree(zoomTreeRoot, node); if (root.children.last.children.isNotEmpty) { print("h5 index ${root.children.last.children.length}"); root.children.last.children.last.children.add(temp); print( "h5 after index length ${root.children.last.children.last.children.length}"); } } else if (node.tag == 'h6') { h6List.add(node.textContent); if (root.children.last.children.isNotEmpty) { print( "h6 index ${root.children.last.children.last.children.length}"); root.children.last.children.last.children.add(temp); print(node.textContent); _add2Tree(zoomTreeRoot, node); } // root.children.last.children.last.children.add(temp); } else if (node.tag == 'p' || node.tag == 'ul' || node.tag == 'li') { pList.add(node.textContent); _add2Tree(zoomTreeRoot, node); // fix this if (root.children.isEmpty) { root.children.add(temp); } else if (root.children.last.children.isNotEmpty) { //perhaps recursive root.children.last.children.last.children.add(temp); } } } } this.sentinel = [h1List, h2List, h3List, h4List, h5List, h6List, pList]; sentinel.removeWhere((hList) => hList.isEmpty); print('algorithm adding: '); print("first layer: ${zoomTreeRoot.children}"); print("first node: ${zoomTreeRoot.children[0].data}"); //good print("second layer: ${zoomTreeRoot.children.last.children}"); //good print( "first node: ${zoomTreeRoot.children.last.children.first.data}"); //good for (int n = 1; n < zoomTreeRoot.children.last.children.length - 1; n++) { //good print(zoomTreeRoot.children.last.children[n].data); } print("last node: ${zoomTreeRoot.children.last.children.last.data}"); //good print("third layer"); for (int thirdLayer = 0; thirdLayer < zoomTreeRoot.children.last.children.length; thirdLayer++) { print(zoomTreeRoot.children.last.children[thirdLayer].children); } print("third layer contents first"); //not sure print(zoomTreeRoot.children.last.children[5].children[0].data); //good for (int contentsOf4 = 1; contentsOf4 < zoomTreeRoot.children.last.children[5].children.length; contentsOf4++) { print(zoomTreeRoot.children.last.children[5].children[contentsOf4].data); } 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); }); // _buildForZooms(level + 1); //probably need a level? } else if (keyEvent.key == "b") { print("b"); setState(() { level = (level + 1).clamp(0, sentinel.length - 1); }); // _buildForZooms(level - 1); //probably need a level? } } void _registerViewFactory(List currentContent) async { // setState(() { //update to do item per item // each item to have itsviewtype ID // is this necessarey here?? //could just move to collapsable for (var emailHTML in widget.threadHTML) { String viewTypeId = 'email-${DateTime.now().millisecondsSinceEpoch}'; final ghost = web.document.createElement('div') as web.HTMLDivElement ..style.visibility = 'hidden' ..style.position = 'absolute' ..style.width = '100%' ..style.overflow = 'auto' ..innerHTML = emailHTML .toJS; // temporarily index because it has to do all of them web.document.body?.append(ghost); await Future.delayed(Duration(milliseconds: 10)); final heightOfEmail = ghost.scrollHeight; ghost.remove(); final HTMLsnippet = web.document.createElement('div') as web.HTMLDivElement ..id = viewTypeId ..innerHTML = emailHTML .toJS; // temporarily index because it has to do all of them HTMLsnippet.style ..width = '100%' ..height = '${heightOfEmail}px' ..overflow = 'auto' ..scrollBehavior = 'smooth'; ui.platformViewRegistry.registerViewFactory( viewTypeId, (int viewId) => HTMLsnippet, ); viewtypeIDs.add(viewTypeId); heightOfViewTypes.add(heightOfEmail); } } 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'"); // getTopLevel(); } } 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!); } // void getTopLevel() { // print("started top"); // int highest = 0; // AugmentTree zoomTreeRoot = AugmentTree(); // // zoomTreeRoot.data = emailsHTML[0]; // whole thing // while (highest < hirarchy.length - 1) { // var highestElement = web.document.querySelectorAll(hirarchy[highest]); // print("nodelist $highestElement"); // if (highestElement.isNull || highestElement.length == 0) { // //from h1, h2, h3, ..., p. // highest += 1; // print(hirarchy[highest]); // } else { // AugmentTree newLevel = AugmentTree(); // list of children of each level // for (int i = 0; i < highestElement.length; i++) { // print(highestElement.item(i)?.textContent); // tagsCollected // .add(highestElement.item(i)?.textContent ?? "nameless subtitle"); // newLevel.children // .add(highestElement.item(i)?.textContent ?? "nameless subtitle"); // } // // traverse to last node and add new level to it // // next // if (zoomTreeRoot.node == null) { // zoomTreeRoot.node = newLevel; // } else { // AugmentTree temp = zoomTreeRoot; // while (true) { // //get to the last and assign node = last node // if (temp.node != null) { // temp = temp.node!; // } else { // temp.node = newLevel; // break; // } // } // } // highest += 1; // } // } // print("out safely"); // print(tagsCollected); //instead of a list make a tree // print(zoomTreeRoot.children); // print(zoomTreeRoot.node?.children); // print(zoomTreeRoot.node?.node?.children); // print(zoomTreeRoot.node?.node?.node?.children); // } Widget _buildForZooms(int lvl) { this.level = lvl; if (lvl < 0 || lvl >= sentinel.length) { return Center(child: Text("No content at level $lvl")); } return ListView.builder( itemCount: this.sentinel[level].length, itemBuilder: (context, index) { 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.surfaceTint, child: Padding( padding: const EdgeInsets.all(16.0), child: MarkdownBlock( data: sentinel[level][index], // one string of markdown config: MarkdownConfig .darkConfig, // or lightConfig depending on theme ), ), ), ); }, ); } @override Widget build(BuildContext context) { return _isLoaded ? Column(children: [ Expanded( // child: MarkdownWidget(data: markdown), //hmmm child: _buildForZooms(level), ) // Expanded( // child: ListView.builder( // itemCount: widget.thread.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) // SizedBox( // height: heightOfViewTypes[index].toDouble(), // child: HtmlElementView( // key: UniqueKey(), viewType: viewtypeIDs[index]), // ), // Divider(), // ], // ); // }, // ), // ) ]) : const Center(child: CircularProgressIndicator()); } }