Merge pull request 'HyM attachments resolve: #2' (#3) from login into main

Reviewed-on: #3
This commit is contained in:
Juan Marulanda De Los Rios 2025-04-24 16:52:58 +00:00
commit 4fa8e5b6fe
10 changed files with 874 additions and 420 deletions

View File

@ -1,20 +1,29 @@
// this file should handle most of the API calls
// it also builds some widgets, but it will be modulated later
import 'package:crab_ui/structs.dart';
import 'dart:async';
import 'dart:typed_data';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'structs.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:ui_web' as ui;
import 'augment.dart';
import 'dart:html' as html;
import 'dart:js' as js;
class ApiService {
static String ip = "";
static String port = "";
static List<AttachmentResponse> threadAttachments = [];
static String currFolder = "";
static List<String> currThread = [];
Future<List<GetThreadResponse>> fetchEmailsFromFolder(
String folder, int pagenitaion) async {
// print(ip + " " + port);
try {
var url = Uri.http('$ip:$port', 'sorted_threads_by_date', {
'folder': folder,
@ -51,8 +60,8 @@ class ApiService {
int threadId,
List<GetThreadResponse> allEmails) async {
try {
var url =
Uri.http('$ip:$port', 'get_thread', {'id': threadId.toString()});
var url = Uri.http('${ApiService.ip}:${ApiService.port}', 'get_thread',
{'id': threadId.toString()});
var response = await http.get(url);
if (response.statusCode == 200) {
@ -97,24 +106,34 @@ class ApiService {
return [];
}
Future<String> fetchEmailContent(List<String> IDs) async {
//returns the html for the email, it gets used in emailView
Future<String> fetchEmailContent(
List<String> IDsString, String emailFolder) async {
String content = r"""
""";
threadAttachments = [];
try {
//attaches email after email from a thread
for (var id in IDs) {
for (var id in IDsString) {
var url = Uri.http('$ip:$port', 'email', {'id': id});
var response = await http.get(url);
currThread.add(id);
if (response.statusCode == 200) {
content += response.body;
try {
getAttachmentsInfo("INBOX", id);
List<AttachmentInfo> attachments = await getAttachmentsInfo(
emailFolder, id);
for (var attachment in attachments) {
//TODO: for each attachment creaate at the bottom a widget for each individual one
threadAttachments
.add(await getAttachment(emailFolder, id, attachment.name));
}
} catch (innerError) {
print('_getAttachment info caught error $innerError');
}
content +=
"""<div id="JuanBedarramarker" style="width: 10px; height: 30px;"></div>""";
content += "<hr>";
}
}
@ -200,18 +219,21 @@ class ApiService {
Future<List<AttachmentInfo>> getAttachmentsInfo(
String folder, String email_id) async {
try {
var url = Uri.http('127.0.0.1:3001', 'get_attachments_info',
{'folder': folder, 'email_id': email_id});
print(url);
var url = Uri.http('$ip:$port', 'get_attachments_info',
{'folder': folder, 'id': email_id});
// print(url);
var response = await http.get(url);
print("response $response");
// print("response $response");
if (response.statusCode == 200) {
var result = response.body;
// print(result);
List<dynamic> attachmentList = json.decode(result);
print("attachment list $attachmentList");
// Map<String, dynamic> attachmentList = json.decode(result);
// print("attachment list $attachmentList");
List<AttachmentInfo> attachments =
attachmentList.map((al) => AttachmentInfo.fromJson(al)).toList();
print("attachments $attachments");
// print("attachments $attachments");
return attachments;
}
@ -220,6 +242,98 @@ class ApiService {
}
return [];
}
Future<AttachmentResponse> getAttachment(
String folder, String email_id, String name) async {
try {
var url = Uri.http('$ip:$port', 'get_attachment',
{'folder': folder, 'id': email_id, 'name': name});
var response = await http.get(url);
if (response.statusCode == 200) {
var result = response.body;
// print(result);
Map<String, dynamic> attachmentData = json.decode(result);
AttachmentResponse data = AttachmentResponse.fromJson(attachmentData);
print("data $data");
return data;
}
} catch (e) {
print("getAttachment failed $e");
}
return AttachmentResponse(name: "error", data: Uint8List(0));
}
Future<List<Map<String, dynamic>>> getMarkerPosition() async {
//this is so we can put a widget right below each email, but the way how the email content is generated
//leads to problems as for a) the html is added one right after the other in one iframe, b)
// if it was multiple iframes then the scrolling to jump would not work as expected
print("marker called");
// JavaScript code embedded as a string
String jsCode = '''
(async function waitForIframeAndMarkers() {
try {
return await new Promise((resolve) => {
const interval = setInterval(() => {
console.log("⏳ Checking for iframe...");
var iframe = document.getElementsByTagName('iframe')[0];
if (iframe && iframe.contentDocument) {
console.log("✅ Iframe found!");
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
var markers = iframeDoc.querySelectorAll('[id^="JuanBedarramarker"]');
if (markers.length > 0) {
console.log(` Found markers in the iframe.`);
var positions = [];
markers.forEach((marker) => {
var rect = marker.getBoundingClientRect();
positions.push({
id: marker.id,
x: rect.left + window.scrollX,
y: rect.top + window.scrollY,
});
});
console.log("📌 Marker positions:", positions);
clearInterval(interval);
resolve(JSON.stringify(positions)); // Ensure proper JSON string
} else {
console.log("❌ No markers found yet.");
}
} else {
console.log("❌ Iframe not found or not loaded yet.");
}
}, 200);
});
} catch (error) {
console.error("JS Error:", error);
throw error; // Propagate error to Dart
}
})();
''';
try {
// Execute the JavaScript code using eval
final result = await js.context.callMethod('eval', [jsCode]);
if (result != null && result is String) {
print("Result received: $result");
// Parse the JSON string returned by JavaScript into a Dart list of maps
final List<dynamic> parsedResult = jsonDecode(result);
var positions = List<Map<String, dynamic>>.from(parsedResult);
print("positions put on");
print(positions);
return positions;
} else {
print("result is null or not a string");
}
} catch (e, stackTrace) {
print("Error executing JavaScript: $e");
print(stackTrace);
}
return [];
}
}
class EmailView extends StatefulWidget {
@ -246,10 +360,18 @@ class EmailView extends StatefulWidget {
}
class _EmailViewState extends State<EmailView> {
//html css rendering thing
late Key iframeKey;
late String currentContent;
late String viewTypeId;
Future<List<Map<String, dynamic>>>? _markerPositionsFuture;
// TextEditingController _jumpController = TextEditingController();
final hardcodedMarkers = [
{'id': 'marker1', 'x': 50, 'y': 100},
{'id': 'marker2', 'x': 150, 'y': 200},
{'id': 'marker3', 'x': 250, 'y': 300},
];
ApiService _apiService = ApiService();
@override
void initState() {
@ -257,6 +379,7 @@ class _EmailViewState extends State<EmailView> {
String currentContent = widget.emailContent;
viewTypeId = "iframe-${DateTime.now().millisecondsSinceEpoch}";
_registerViewFactory(currentContent);
_markerPositionsFuture = ApiService().getMarkerPosition();
}
void _registerViewFactory(String currentContent) {
@ -276,7 +399,7 @@ class _EmailViewState extends State<EmailView> {
AugmentClasses.handleJump(spanId);
}
// TODO: void _invisibility(String )
// TODO: void _invisibility(String ) //to make purple numbers not visible
@override
Widget build(BuildContext context) {
@ -285,7 +408,9 @@ class _EmailViewState extends State<EmailView> {
appBar: AppBar(
title: Text(widget.name),
),
body: Column(
body: Stack(
children: [
Column(
children: [
EmailToolbar(
onJumpToSpan: _scrollToNumber,
@ -309,8 +434,7 @@ class _EmailViewState extends State<EmailView> {
// print("change");
// widget.emailContent = r"
// "
// },
//
),
Row(
// title of email
@ -355,6 +479,71 @@ class _EmailViewState extends State<EmailView> {
),
),
],
),
// Overlay widgets dynamically based on marker positions
FutureBuilder<List<Map<String, dynamic>>>(
future: _markerPositionsFuture,
builder: (context, snapshot) {
print("FutureBuilder state: ${snapshot.connectionState}");
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
print("Error in FutureBuilder: ${snapshot.error}");
return Center(child: Text('error loading markers'));
}
if (snapshot.hasData && snapshot.data != null) {
final markers = snapshot.data!;
return Stack(
children: markers.map((marker) {
return Positioned(
left: marker['x'].toDouble(),
top: marker['y'].toDouble(),
child: GestureDetector(
onTap: () {
print('Tapped on ${marker['id']}');
},
child: Container(
width: 50,
height: 50,
color: Colors.red,
child: Center(
child: Text(
marker['id'],
style: TextStyle(color: Colors.white),
),
),
),
),
);
}).toList(),
);
}
return SizedBox.shrink(); // No markers found
},
),
// Red widget overlay
// Positioned(
// left: 8, // Adjust based on your desired position
// top: 100 + 44 + 5, // Adjust based on your desired position
// child: IgnorePointer(
// ignoring: true, // Ensures the iframe remains interactive
// child: Container(
// color: Colors.red,
// width: 100,
// height: 50,
// child: Center(
// child: Text(
// 'Overlay',
// style: TextStyle(color: Colors.white),
// ),
// ),
// ),
// ),
// ),
],
));
}
}

View File

@ -0,0 +1,14 @@
import 'dart:html' as html;
import 'dart:io';
import 'structs.dart';
import 'package:file_saver/file_saver.dart';
class Attachmentdownload {
Future<void> saveFile(AttachmentResponse attachment) async {
await FileSaver.instance.saveFile(
name: attachment.name.toString().substring(0, attachment.name.toString().lastIndexOf('.')),
bytes: attachment.data,
ext: attachment.name.toString().substring(attachment.name.toString().lastIndexOf('.')+1)
);
}
}

103
lib/attachmentWidget.dart Normal file
View File

@ -0,0 +1,103 @@
import "dart:typed_data";
import "package:crab_ui/attachmentDownload.dart";
import "package:crab_ui/structs.dart";
import "package:flutter/material.dart";
import 'package:pdfrx/pdfrx.dart';
import 'package:photo_view/photo_view.dart';
class AttachmentWidget extends StatelessWidget {
final AttachmentResponse attachment;
AttachmentWidget({required this.attachment});
Widget attachmentViewer(AttachmentResponse att) {
String extension = att.name
.toString()
.substring(att.name.toString().indexOf(".") + 1)
.toLowerCase();
if (extension == "jpg" || extension == "png") {
return Image.memory(att.data);
} else if (extension == "pdf") {
return PdfViewer.data(Uint8List.fromList(att.data),
sourceName: att.name,
params: PdfViewerParams(
enableTextSelection: true,
scrollByMouseWheel: 0.5,
annotationRenderingMode:
PdfAnnotationRenderingMode.annotationAndForms,
));
}
return Center(
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Color(0xff6C63FF),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"No preview available",
style: TextStyle(
color: Colors.white, fontSize: 18, decoration: TextDecoration.none),
),
SizedBox(
height: 5,
),
GestureDetector(
child: ElevatedButton(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("Download", style: TextStyle(color: Color(0xff2c3e50)),),
Icon(Icons.download,
color: Color(0xffb0b0b0),),
]),
onPressed: () => Attachmentdownload().saveFile(att),
)),
]),
));
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black38,
child: Stack(children: <Widget>[
Container(
color: Colors.white,
child: Padding(
padding: EdgeInsets.fromLTRB(10, 20, 0, 10),
child: Column(
children: [
Row(
children: [
CloseButton(onPressed: () => {Navigator.pop(context)}),
Text(
attachment.name
.toString(), //its alr a string but incase ¯\()/¯ //update: i did that everywhere lol
style: TextStyle(
color: Colors.black,
fontSize: 20,
decoration: TextDecoration
.none), //TODO: personalize your fonts
),
],
),
Expanded(
child: attachmentViewer(attachment),
)
],
),
),
),
]));
}
}

View File

@ -1,6 +1,12 @@
import 'package:crab_ui/api_service.dart';
import 'package:crab_ui/attachmentDownload.dart';
import 'package:crab_ui/structs.dart';
import 'package:flutter/material.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'dart:html' as html;
import 'dart:js' as js;
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'attachmentWidget.dart';
class EmailToolbar extends StatefulWidget {
final Function(String) onJumpToSpan;
@ -64,8 +70,8 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
child: Text('Reload'),
),
ElevatedButton(
onPressed: AugmentClasses.handleImages,
child: Text('Images'),
onPressed: () => AugmentClasses.handleImages(context),
child: Text('Attachments'),
),
SizedBox(width: 8),
ElevatedButton(
@ -206,6 +212,8 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
}
class AugmentClasses {
ApiService _apiService = ApiService();
static OverlayEntry? _overlayEntry;
static void handleHome(BuildContext context) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
@ -214,8 +222,96 @@ class AugmentClasses {
print("reload");
}
static void handleImages() {
static void handleImages(BuildContext context) {
print("Images button pressed");
final overlay = Overlay.of(context);
final renderBox = context.findRenderObject() as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
_overlayEntry = OverlayEntry(
builder: (context) => Stack(
children: [
// Dimmed background
Container(
color: Colors.black54,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
),
// Focused content window
PointerInterceptor(
child: Center(
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(12),
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400,
maxHeight: 500,
),
child: Column(
children: [
_buildHeader(context),
const Divider(height: 1),
Expanded(
child: ListView(
children: _buildMenuItem(context),
),
),
],
),
),
),
),
),
],
),
);
if (_overlayEntry != null) {
overlay.insert(_overlayEntry!);
}
}
// Add missing widget builder methods
static Widget _buildHeader(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16.0),
child:
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text(
'Thread Attachments',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
CloseButton(
onPressed: () {
_overlayEntry?.remove();
},
),
]));
}
static List<Widget> _buildMenuItem(BuildContext context) {
List<Widget> listOfFiles = [];
for (AttachmentResponse file in ApiService.threadAttachments) {
listOfFiles.add(
ListTile (
leading: Icon(Icons.file_present),
title: Text(file.name.toString()),
trailing: GestureDetector(
child: Icon(Icons.download),
onTap: () => Attachmentdownload().saveFile(file),
),
onTap: () {
_overlayEntry?.remove();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttachmentWidget(attachment: file)));
}));
}
return listOfFiles;
}
static void handleOpen() {
@ -517,6 +613,7 @@ class AugmentClasses {
}
static void disableIframePointerEvents() {
//pretty sure these dont work
final iframes = html.document.getElementsByTagName('iframe');
for (var iframe in iframes) {
if (iframe is html.Element) {

View File

@ -4,10 +4,12 @@ import 'structs.dart';
class EmailListScreen extends StatelessWidget {
final List<GetThreadResponse> emails;
final Future<String> Function(List<String>) getEmailContent;
final Future<String> Function(List<String>, String) getEmailContent;
final String folder;
EmailListScreen({required this.emails, required this.getEmailContent});
EmailListScreen({required this.emails, required this.getEmailContent, required this.folder});
//fix the email list
@override
Widget build(BuildContext context) {
return Scaffold(
@ -24,7 +26,7 @@ class EmailListScreen extends StatelessWidget {
),
trailing: Text(email.date.toString()),
onTap: () async {
String emailContent = await getEmailContent(email.messages);
String emailContent = await getEmailContent(email.messages, folder);
Navigator.push(
context,
MaterialPageRoute(
@ -51,7 +53,7 @@ class EmailListScreen extends StatelessWidget {
// ignore: must_be_immutable
class EmailPage extends StatefulWidget {
EmailPage({Key? key}) : super(key: key);
String selectedFolder = "INBOX";
String selectedFolder = "INBOX"; //starter
int offset = 0;
int page = 1;
@ -62,11 +64,14 @@ class EmailPage extends StatefulWidget {
class EmailPageState extends State<EmailPage> {
final ApiService apiService = ApiService();
List<GetThreadResponse> emails = [];
int page = 1;
bool isBackDisabled = false;
@override
void initState() {
super.initState();
widget.page = widget.page;
widget.page = page;
isBackDisabled = true;
}
void updateSelectedFolder(String folder) {
@ -86,11 +91,16 @@ class EmailPageState extends State<EmailPage> {
setState(() {
widget.offset += 50;
widget.page += 1;
isBackDisabled = false;
});
} else if (option == "back") {
setState(() {
widget.offset -= 50;
widget.page -= 1;
if (widget.page == 1) {
isBackDisabled = true;
print("back dis");
}
});
}
// print(currentPage);
@ -119,6 +129,7 @@ class EmailPageState extends State<EmailPage> {
body: EmailListScreen(
emails: emails,
getEmailContent: apiService.fetchEmailContent,
folder: widget.selectedFolder,//try to grab from it directly
),
);
}

View File

@ -1,9 +1,10 @@
import 'package:crab_ui/folder_drawer.dart';
import 'package:crab_ui/structs.dart';
import 'folder_drawer.dart';
import 'structs.dart';
import 'package:flutter/widgets.dart';
import 'api_service.dart';
import 'package:flutter/material.dart';
import 'email.dart';
// import 'package:shared_preferences/shared_preferences.dart';
// import 'serialize.dart';
class HomeScreen extends StatefulWidget {
@ -119,7 +120,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
body: ListView.separated(
itemCount: result.length,
itemBuilder: (context, index) {
final email = result[index];
final SerializableMessage email = result[index];
return ListTile(
title: Text(email.from,
style: TextStyle(fontWeight: FontWeight.bold)),
@ -131,7 +132,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
onTap: () async {
// print('tapped');
String emailContent =
await apiService.fetchEmailContent([email.id]);
await apiService.fetchEmailContent([email.id], email.list);
// print('content below');
// print(emailContent);
Navigator.push(
@ -341,7 +342,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
children: [
ElevatedButton(
onPressed: () {
_emailPageKey.currentState
_emailPageKey.currentState!.isBackDisabled ? null: _emailPageKey.currentState
?.updatePagenation('back');
},
child: Icon(Icons.navigate_before),

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:crab_ui/api_service.dart';
import 'package:crab_ui/home_page.dart';
// import 'package:crab_ui/api_service.dart';
import 'api_service.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;
@ -11,7 +12,9 @@ class AuthService {
Future<bool> isUserLoggedIn() async {
try {
final response =
await http.get(Uri.parse('http://localhost:6823/read-config'));
await http.get(Uri.http('localhost:6823', 'read-config'));
// await http.get(Uri.parse('http://localhost:6823/read-config'));
print(response.statusCode);
print(response.body);
if (response.statusCode == 200) {
@ -27,7 +30,9 @@ class AuthService {
Map<String, dynamic> json = jsonDecode(jsonOuter);
// print(json["is_logged_in"]);
ApiService.ip = data['ip'];
print("setting ip " + ApiService.ip);
ApiService.port = data['port'];
print("setting port " + data['port']);
return json["is_logged_in"];
}
} catch (er) {
@ -35,6 +40,7 @@ class AuthService {
}
}
} catch (e) {
print('caughtther');
print(e);
}
return false;

View File

@ -61,7 +61,7 @@ class _SerializableMessageListScreenState extends State<SerializableMessageListS
),
trailing: Text(message.date),
onTap: () async {
String emailContent = await apiService.fetchEmailContent([message.id]);
String emailContent = await apiService.fetchEmailContent([message.id], message.list);
Navigator.push(
context,
MaterialPageRoute(

View File

@ -1,5 +1,7 @@
//data structures
import 'dart:typed_data';
class GetThreadResponse {
final int id;
final List<String> messages;
@ -76,7 +78,7 @@ class SerializableMessage {
required this.subject,
required this.date,
required this.uid,
required this.list,
required this.list, //email list???
required this.id,
required this.in_reply_to,
});
@ -118,3 +120,29 @@ class AttachmentInfo {
);
}
}
class AttachmentInfoList extends Iterable<AttachmentInfo> {
final List<AttachmentInfo> _attachments;
AttachmentInfoList(this._attachments);
factory AttachmentInfoList.fromJsonList(List<Map<String, dynamic>> jsonList) {
return AttachmentInfoList(jsonList.map((json) => AttachmentInfo.fromJson(json)).toList());
}
@override
Iterator<AttachmentInfo> get iterator => _attachments.iterator;
@override
String toString() => _attachments.toString();
}
class AttachmentResponse {
final name;
final Uint8List data;
AttachmentResponse({required this.name, required this.data});
factory AttachmentResponse.fromJson(Map<String, dynamic> json) {
return AttachmentResponse(name: json["name"], data: Uint8List.fromList(List<int>.from(json["data"])));
}
}

View File

@ -18,10 +18,15 @@ dependencies:
encrypt: ^5.0.0
pointycastle: ^3.4.0
mime: ^1.0.3
pointer_interceptor: ^0.10.1+2
file_saver: ^0.2.14
english_words: ^4.0.0
provider: ^6.0.0
intl: ^0.19.0
pdfrx: ^1.0.94
photo_view: ^0.15.0
dev_dependencies:
flutter_test: