hym_ui/lib/collapsableEmailsWeb.dart
2025-06-09 16:37:38 -04:00

506 lines
18 KiB
Dart

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<String> thread; // email id's in the form xyz@gmail.com
final List<String> threadHTML;
final String threadIDs;
CollapsableEmails(
{required this.thread,
required this.threadHTML,
required this.threadIDs});
@override
State<CollapsableEmails> createState() => _CollapsableEmailsState();
}
class _CollapsableEmailsState extends State<CollapsableEmails> {
List<String> emailsHTML = []; //html of the emails in the thread
// build attachments with the forldar name and id
Set<int> _expandedEmails = {}; //open emails
List viewtypeIDs = []; //IDs of the viewtypes, order matters
List heightOfViewTypes = []; //the height of each viewtype
List<SerializableMessage> emailsInThread = [];
bool _isLoaded = false;
static bool _isListenerRegistered = false;
static bool left = true;
static bool right = true;
web.EventListener? _listener;
List<String> hirarchy = ["h1", "h2", "h3", "h4", "h5", "h6", "p"];
Map<String, int> hirarchyDict = {
"h1": 1,
"h2": 2,
"h3": 3,
"h4": 4,
"h5": 6,
"h6": 7,
"p": 8,
"ul": 8,
"li": 8,
};
List<String> tagsCollected = [];
String markdown = '';
List<List<String>> 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<md.Node> nakedList = md.Document().parseLines(text.split('\n'));
List<String> pList = [];
List<String> h1List = [];
List<String> h2List = [];
List<String> h3List = [];
List<String> h4List = [];
List<String> h5List = [];
List<String> 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<String> 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());
}
}