folders actions #5

Merged
Juan merged 20 commits from login into main 2025-05-21 18:05:05 +00:00
96 changed files with 1727 additions and 1377 deletions

118
.gitignore vendored
View File

@ -1,43 +1,75 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
.VSCodeCounter
*:Zone.Identifier
.packages
*.lock
*.tmp
*.temp
*.bak
*.old
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
.vscode/
/linux
/macos
/windows
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
# /android/app/debug
# /android/app/profile
# /android/app/release
# /android
# build
# android/.gradle/
# android/local.properties
ios/Flutter/Generated.xcconfig
ios/Flutter/App.framework
ios/Flutter/Flutter.framework
ios/Flutter/Flutter.podspec
ios/.symlinks/
ios/Pods/
ios/.generated/
macos/Flutter/GeneratedPluginRegistrant.swift
macos/Pods/
macos/.symlinks/
linux/flutter/
windows/flutter/

13
android/.gitignore vendored
View File

@ -1,13 +0,0 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View File

@ -1,58 +0,0 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader("UTF-8") { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
if (flutterVersionCode == null) {
flutterVersionCode = "1"
}
def flutterVersionName = localProperties.getProperty("flutter.versionName")
if (flutterVersionName == null) {
flutterVersionName = "1.0"
}
android {
namespace = "com.example.crab_ui"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.crab_ui"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}

View File

@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -1,45 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="crab_ui"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -1,5 +0,0 @@
package com.example.crab_ui
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -1,18 +0,0 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -1,3 +0,0 @@
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip

View File

@ -1,25 +0,0 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
}
include ":app"

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
HostUrl=https://onlinepngtools.com/

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
HostUrl=https://onlinepngtools.com/

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
HostUrl=https://onlinepngtools.com/

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
HostUrl=https://onlinepngtools.com/

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -5,6 +5,7 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'collapsableEmails.dart';
import 'structs.dart';
import 'package:flutter/material.dart';
@ -12,15 +13,19 @@ 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;
// import 'dart:html' as html;
// import 'dart:js' as js;
import 'package:web/web.dart' as web;
import 'dart:js_interop' as js;
class ApiService {
static String ip = "";
static String port = "";
static List<AttachmentResponse> threadAttachments = [];
static List<AttachmentResponse> threadAttachments =
[]; //holds attachments of the thread
static String currFolder = "";
static List<String> currThread = [];
static List<String> currThread = []; //holds the email ids of the thread
static String currThreadID = ""; //picked an email it prints the threadID
Future<List<GetThreadResponse>> fetchEmailsFromFolder(
String folder, int pagenitaion) async {
@ -31,7 +36,6 @@ class ApiService {
'offset': pagenitaion.toString(),
});
var response = await http.get(url);
// print(response);
List<GetThreadResponse> allEmails = [];
if (response.statusCode == 200) {
@ -45,6 +49,7 @@ class ApiService {
}
}
}
currFolder = folder;
return allEmails;
} else {
throw Exception('Failed to load threads');
@ -96,7 +101,6 @@ class ApiService {
List<dynamic> messagesJson = json.decode(response.body);
List<SerializableMessage> messages =
messagesJson.map((mj) => SerializableMessage.fromJson(mj)).toList();
return messages;
}
print(response.statusCode);
@ -107,11 +111,13 @@ class ApiService {
}
//returns the html for the email, it gets used in emailView
Future<String> fetchEmailContent(
Future<List<String>> fetchEmailContent(
List<String> IDsString, String emailFolder) async {
String content = r"""
""";
List<String> HTMLofThread = [];
threadAttachments = [];
int counter = 0;
try {
//attaches email after email from a thread
@ -120,10 +126,12 @@ class ApiService {
var response = await http.get(url);
currThread.add(id);
if (response.statusCode == 200) {
counter += 1;
content += response.body;
HTMLofThread.add(response.body);
try {
List<AttachmentInfo> attachments = await getAttachmentsInfo(
emailFolder, 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
@ -134,19 +142,82 @@ class ApiService {
}
content +=
"""<div id="JuanBedarramarker" style="width: 10px; height: 30px;"></div>""";
content += "<hr>";
content += "<hr><p>end of email</p>";
}
}
} catch (e) {
print('_getEmailContent caught error: $e');
}
return content;
// return content;
return HTMLofThread;
}
// void _addMailBox async(BuildContext context){
// //add email folder
// showDialog(context: context, builder: builder)
// }
Future<List<SerializableMessage>> threadsInSerializable(
String thread_id) async {
//actually a xyzwtv@gmail.com
// grab all of the emails in thread anyways, for the future it'll come in handy // maybe not
var url = Uri.http('$ip:$port', 'get_thread_messages', {'id': thread_id});
try {
var response = await http.get(url);
if (response.statusCode == 200) {
List json = jsonDecode(response.body);
List<SerializableMessage> serializableMessages = [];
for (var mail in json) {
serializableMessages.add(SerializableMessage.fromJson(mail));
}
return serializableMessages;
} else {
print(
"failed get request with status code ${response.statusCode}, and body ${response.body}");
}
} catch (e) {
print("caught in threadInSerializable method error: $e");
}
return [];
}
Future<bool> moveEmail(
//only moves the first email of the thread //or perhaps should do the last
String fromFolder,
String thread_id,
String toFolder) async {
var url = Uri.http('$ip:$port', 'move_email');
List<SerializableMessage> mailsInSerializable =
await this.threadsInSerializable(thread_id);
if (mailsInSerializable.isEmpty) {
return false;
}
SerializableMessage firstMail = mailsInSerializable[0];
Map<String, String> requestBody = {
'from': fromFolder,
'uid': firstMail.uid.toString(),
'to': toFolder,
};
try {
var response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode(requestBody),
);
if (response.statusCode == 200) {
print('response body ${response.body}');
return true;
} else {
print('error ${response.statusCode} ${response.body}');
}
} catch (e) {
print("failed trying to post move_email, with error: $e");
}
return false;
}
Future<List<String>> fetchFolders() async {
try {
@ -182,6 +253,31 @@ class ApiService {
}
}
Future<void> renameFolder(String oldFolder, String newFolder) async {
var url = Uri.http('$ip:$port', 'rename_folder');
Map<String, String> requestBody = {
'old_name': oldFolder,
'new_name': newFolder,
};
try {
var response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode(requestBody),
);
if (response.statusCode == 200) {
print('response body: ${response.body}');
} else {
print('Error: ${response.statusCode}, response body: ${response.body}');
}
} catch (e) {
print('error making post req: $e');
}
}
Future<void> deleteFolder(String folderName) async {
var url = Uri.http('$ip:$port', 'delete_folder');
@ -206,13 +302,6 @@ class ApiService {
}
Future<bool> logIn(String json) async {
// var url = Uri.https('')
// try{
// String response = await http.post(
// url
// );
// }
return false;
}
@ -251,7 +340,6 @@ class ApiService {
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");
@ -263,87 +351,88 @@ class ApiService {
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
//TODO: MOVE THIS INTO WEB
// 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
}
})();
''';
// 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]);
// 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");
// 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);
}
// // 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 [];
}
// return [];
// }
}
class EmailView extends StatefulWidget {
final String emailContent;
final List<String> emailContent;
final String from;
final String name;
final String to;
final String subject;
final String date;
final String id;
final List<String> messages;
const EmailView({
Key? key,
@ -354,6 +443,7 @@ class EmailView extends StatefulWidget {
required this.subject,
required this.date,
required this.id,
required this.messages,
}) : super(key: key);
@override
_EmailViewState createState() => _EmailViewState();
@ -363,7 +453,7 @@ class _EmailViewState extends State<EmailView> {
//html css rendering thing
late Key iframeKey;
late String currentContent;
late String viewTypeId;
late String viewTypeId; //make this a list too???
Future<List<Map<String, dynamic>>>? _markerPositionsFuture;
// TextEditingController _jumpController = TextEditingController();
final hardcodedMarkers = [
@ -371,29 +461,40 @@ class _EmailViewState extends State<EmailView> {
{'id': 'marker2', 'x': 150, 'y': 200},
{'id': 'marker3', 'x': 250, 'y': 300},
];
ApiService _apiService = ApiService();
@override
void initState() {
super.initState();
String currentContent = widget.emailContent;
viewTypeId = "iframe-${DateTime.now().millisecondsSinceEpoch}";
_registerViewFactory(currentContent);
_markerPositionsFuture = ApiService().getMarkerPosition();
print("thread id? ${widget.id}");
List<String> currentContent = widget
.emailContent; //html of the email/ actually entire thread, gives me little space to play in between
// i wonder if the other attributes change? because if so i have to add like some zooms in and out of the emails, as in collapse
// _registerViewFactory(currentContent);
}
void _registerViewFactory(String currentContent) {
setState(() {
viewTypeId = 'iframe-${DateTime.now().millisecondsSinceEpoch}';
ui.platformViewRegistry.registerViewFactory(
viewTypeId,
(int viewId) => html.IFrameElement()
..width = '100%'
..height = '100%'
..srcdoc = currentContent
..style.border = 'none');
});
}
// void _registerViewFactory(List<String> currentContent) { // i think this doesnt work anymore
// setState(() { //update to do item per item
// // each item to have itsviewtype ID
// // is this necessarey here??
// //could just move to collapsable
// viewTypeId = 'iframe-${DateTime.now().millisecondsSinceEpoch}';
// final emailHTML = web.document.createElement('div') as web.HTMLDivElement
// ..id = viewTypeId
// ..innerHTML = currentContent[0].toJS; // temporarily index because it has to do all of them
// emailHTML.style
// ..width = '100%'
// ..height = '100%'
// ..overflow = 'auto'
// ..scrollBehavior = 'smooth';
// ui.platformViewRegistry.registerViewFactory(
// viewTypeId,
// (int viewId) => emailHTML,
// );
// });
// }
void _scrollToNumber(String spanId) {
AugmentClasses.handleJump(spanId);
@ -403,7 +504,8 @@ class _EmailViewState extends State<EmailView> {
@override
Widget build(BuildContext context) {
// print(currentContent);
// print("thread id ${widget.id}");
ApiService.currThreadID = widget.id;
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
@ -422,7 +524,7 @@ class _EmailViewState extends State<EmailView> {
// <h1>Welcome to My Website</h1>
// <p>This is a simple HTML page.</p>
// <h2>What is HTML?</h2>
// <p>HTML (HyperText Markup Language) is the most basic building block of the Web. It defines the meaning and structure of web content. Other technologies besides HTML are generally used to describe a web page's appearance/presentation (CSS) or functionality/behavior (JavaScript).</p>
// <p>HTML (HyperText Markup Language) is the most basic building~ block of the Web. It defines the meaning and structure of web content. Other technologies besides HTML are generally used to describe a web page's appearance/presentation (CSS) or functionality/behavior (JavaScript).</p>
// <h3>Here's a simple list:</h3>
// <ul>
// <li>HTML elements are the building blocks of HTML pages</li>
@ -471,59 +573,66 @@ class _EmailViewState extends State<EmailView> {
)
],
),
Expanded(
child: HtmlElementView(
key: UniqueKey(),
viewType: viewTypeId,
child: CollapsableEmails(
//change here
thread: widget.messages, //this wont work in serializable
threadHTML: widget.emailContent,
threadIDs: widget.id,
),
),
// Expanded(
// child: HtmlElementView(
// key: UniqueKey(),
// viewType: viewTypeId,
// ),
// ),
],
),
// 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(),
);
}
// 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
},
),
// return SizedBox.shrink(); // No markers found
// },
// ),
// Red widget overlay
// Positioned(
// left: 8, // Adjust based on your desired position

View File

@ -1,4 +1,5 @@
import 'dart:html' as html;
import 'package:web/web.dart' as web;
import 'dart:io';
import 'structs.dart';
import 'package:file_saver/file_saver.dart';

View File

@ -1,10 +1,14 @@
// import 'dart:ffi';
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:pdfrx/pdfrx.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'dart:html' as html;
import 'dart:js' as js;
// import 'dart:html' as html;
// import 'dart:js' as js;
import 'package:web/web.dart' as web;
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'attachmentWidget.dart';
@ -88,6 +92,12 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
onPressed: AugmentClasses.handleStop,
child: Text('Stop'),
),
ElevatedButton(
onPressed: () {
AugmentClasses.handleMove(context);
},
child: Text('Move'),
),
Spacer(),
PopupMenuButton<String>(
onSelected: (String value) {
@ -212,8 +222,9 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
}
class AugmentClasses {
ApiService _apiService = ApiService();
static OverlayEntry? _overlayEntry;
static String? selectedFolder; // Manage selected folder at the class level
static void handleHome(BuildContext context) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
@ -222,11 +233,166 @@ class AugmentClasses {
print("reload");
}
Widget listOfFolders(BuildContext context) {
//list the emails and make some sort of selection box
return FutureBuilder<List<String>>(
future: ApiService().fetchFolders(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
String? selectedFolder; // Declare the selected folder state
return StatefulBuilder(
builder: (context, setState) {
return ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: snapshot.data!.map((folder) {
return RadioListTile<String>(
title: Text(folder),
value: folder,
groupValue: selectedFolder,
onChanged: (String? value) {
setState(() {
selectedFolder = value; // Update the selected folder
});
},
);
}).toList(),
);
},
);
} else {
return const Center(child: Text('No folders found.'));
}
},
);
}
static void handleMove(BuildContext context) async {
print("current folder: ${ApiService.currFolder}");
final overlay = Overlay.of(context);
String? selectedFolder; // Variable to store the selected folder
_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(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Move email from folder ${ApiService.currFolder} to:',
style: TextStyle(fontSize: 16),
),
Divider(height: 1),
Expanded(
child: FutureBuilder<List<String>>(
future: ApiService().fetchFolders(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
return StatefulBuilder(
builder: (context, setState) {
return ListView(
shrinkWrap: true,
children: snapshot.data!.map((folder) {
return RadioListTile<String>(
title: Text(folder),
value: folder,
groupValue: selectedFolder,
onChanged: (String? value) {
setState(() {
selectedFolder =
value; // Update the selected folder
});
},
);
}).toList(),
);
},
);
} else {
return const Center(
child: Text('No folders found.'));
}
},
),
),
Divider(height: 1),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
// Handle Accept button
if (selectedFolder != null) {
print("Selected folder: $selectedFolder");
// Store the selected folder or perform any action
// ApiService.currFolder = selectedFolder!;
ApiService().moveEmail(ApiService.currFolder,
ApiService.currThreadID, selectedFolder!);
_overlayEntry?.remove();
} else {
print("No folder selected");
}
},
child: Text('Accept'),
),
ElevatedButton(
onPressed: () {
// Handle Cancel button
_overlayEntry?.remove();
},
child: Text('Cancel'),
),
],
),
],
),
),
),
),
),
],
),
);
if (_overlayEntry != null) {
overlay.insert(_overlayEntry!);
}
}
static void handleImages(BuildContext context) {
//rename to handle attachments
print("Images button pressed");
final overlay = Overlay.of(context);
final renderBox = context.findRenderObject() as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
// final renderBox = context.findRenderObject() as RenderBox;
// final offset = renderBox.localToGlobal(Offset.zero);
_overlayEntry = OverlayEntry(
builder: (context) => Stack(
@ -295,11 +461,10 @@ class AugmentClasses {
static List<Widget> _buildMenuItem(BuildContext context) {
List<Widget> listOfFiles = [];
for (AttachmentResponse file in ApiService.threadAttachments) {
listOfFiles.add(
ListTile (
listOfFiles.add(ListTile(
leading: Icon(Icons.file_present),
title: Text(file.name.toString()),
trailing: GestureDetector(
trailing: GestureDetector(
child: Icon(Icons.download),
onTap: () => Attachmentdownload().saveFile(file),
),
@ -349,7 +514,7 @@ class AugmentClasses {
console.log('Iframe not found or not loaded.');
}
''';
js.context.callMethod('eval', [js_code]);
// js.context.callMethod('eval', [js_code]);
}
static void invisibility(String htmlClass) {}
@ -357,7 +522,7 @@ class AugmentClasses {
static Future<void> JumpButton(BuildContext context) async {
// FocusNode textFieldFocusNode = FocusNode();
AugmentClasses.disableIframePointerEvents();
// AugmentClasses.disableIframePointerEvents();
await showDialog(
barrierDismissible: true,
// barrierColor: Colors.yellow,
@ -439,7 +604,7 @@ class AugmentClasses {
],
),
).then((_) {
AugmentClasses.enableIframePointerEvents();
// AugmentClasses.enableIframePointerEvents();
});
}
@ -449,7 +614,7 @@ class AugmentClasses {
bool blankLines = false;
bool numbering = false;
bool statementSignatures = false;
AugmentClasses.disableIframePointerEvents();
// AugmentClasses.disableIframePointerEvents();
await showDialog(
context: context,
builder: (context) => Container(
@ -560,7 +725,7 @@ class AugmentClasses {
ElevatedButton(onPressed: () {}, child: Text('OK')),
ElevatedButton(
onPressed: () {
AugmentClasses.disableIframePointerEvents();
// AugmentClasses.disableIframePointerEvents();
Navigator.of(context).pop();
},
child: Text('Cancel')),
@ -574,7 +739,7 @@ class AugmentClasses {
),
),
)).then((_) {
AugmentClasses.enableIframePointerEvents(); // may be useless?
// AugmentClasses.enableIframePointerEvents(); // may be useless?
});
}
@ -584,7 +749,7 @@ class AugmentClasses {
//idea is to search in file, extract the <p> tags that contain these
//words and highlight, then when zoom, you just jump to that paragraph
AugmentClasses.disableIframePointerEvents();
// AugmentClasses.disableIframePointerEvents();
await showDialog(
context: context,
builder: (context) => Container(
@ -612,22 +777,27 @@ 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) {
iframe.style.pointerEvents = 'none'; // Disable pointer events
}
}
}
// static void disableIframePointerEvents() {
// //pretty sure these dont work
// // final iframes = html.document.getElementsByTagName('iframe');
// final iframes = web.document.getElementsByTagName('iframe');
// for (var iframe in iframes) {
// // if (iframe is html.Element) {
// // iframe.style.pointerEvents = 'none'; // Disable pointer events
// if (iframe is web.Element) {
// iframe.
// }
// }
// }
static void enableIframePointerEvents() {
final iframes = html.document.getElementsByTagName('iframe');
for (var iframe in iframes) {
if (iframe is html.Element) {
iframe.style.pointerEvents = 'auto'; // Re-enable pointer events
}
}
}
// static void enableIframePointerEvents() {
// // final iframes = html.document.getElementsByTagName('iframe');
// final iframes = html.document.getElementsByTagName('iframe');
// for (var iframe in iframes) {
// if (iframe is html.Element) {
// iframe.style.pointerEvents = 'auto'; // Re-enable pointer events
// }
// }
// }
}

132
lib/collapsableEmails.dart Normal file
View File

@ -0,0 +1,132 @@
import 'dart:js_interop';
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';
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;
@override
void initState() {
// TODO: implement initState
super.initState();
_registerViewFactory(widget.threadHTML);
_serializableData(widget.threadIDs);
}
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;
});
}
@override
Widget build(BuildContext context) {
return _isLoaded
?Column(children: [
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)
// if(viewtypeIDs[index] == null || heightOfViewTypes[index] == null)
// const SizedBox(height: 100, child: Center(child: CircularProgressIndicator())),
SizedBox(
height: heightOfViewTypes[index].toDouble(),
child: HtmlElementView(
key: UniqueKey(), viewType: viewtypeIDs[index]),
),
Divider(),
],
);
},
),
)
]): const Center(child:CircularProgressIndicator());
}
}

View File

@ -1,136 +1,148 @@
import 'package:flutter/material.dart';
import 'api_service.dart';
import 'structs.dart';
class EmailListScreen extends StatelessWidget {
final List<GetThreadResponse> emails;
final Future<String> Function(List<String>, String) getEmailContent;
final String folder;
EmailListScreen({required this.emails, required this.getEmailContent, required this.folder});
//fix the email list
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.separated(
itemCount: emails.length,
itemBuilder: (context, index) {
final email = emails[index];
return ListTile(
title: Text(email.from_name,
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(email.subject)],
),
trailing: Text(email.date.toString()),
onTap: () async {
String emailContent = await getEmailContent(email.messages, folder);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EmailView(
emailContent: emailContent,
from: email.from_address,
name: email.from_name,
to: email.to.toString(),
subject: email.subject,
date: email.date.toString(),
id: email.id.toString(),
),
),
);
},
);
},
separatorBuilder: (context, index) => Divider(),
),
);
}
}
// ignore: must_be_immutable
class EmailPage extends StatefulWidget {
EmailPage({Key? key}) : super(key: key);
String selectedFolder = "INBOX"; //starter
int offset = 0;
int page = 1;
@override
EmailPageState createState() => EmailPageState();
}
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 = page;
isBackDisabled = true;
}
void updateSelectedFolder(String folder) {
setState(() {
widget.selectedFolder = folder;
});
print(folder);
_fetchEmails();
}
String getPage() {
return widget.page.toString();
}
void updatePagenation(String option) {
if (option == "next") {
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);
_fetchEmails();
}
void _fetchEmails() async {
// print(selectedFolder)
try {
List<GetThreadResponse> fetchedEmails = await apiService
.fetchEmailsFromFolder(widget.selectedFolder, widget.offset);
if (!mounted) return;
setState(() {
emails = fetchedEmails; // Update the list of emails
});
} catch (e) {
print('Error fetching emails: $e');
}
}
@override
Widget build(BuildContext context) {
_fetchEmails();
return Scaffold(
body: EmailListScreen(
emails: emails,
getEmailContent: apiService.fetchEmailContent,
folder: widget.selectedFolder,//try to grab from it directly
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'api_service.dart';
import 'structs.dart';
class EmailListScreen extends StatelessWidget {
final List<GetThreadResponse> emails;
final Future<List<String>> Function(List<String>, String) getEmailContent;
final String folder;
EmailListScreen(
{required this.emails,
required this.getEmailContent,
required this.folder});
//fix the email list
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.separated(
itemCount: emails.length,
itemBuilder: (context, index) {
final email = emails[index];
return ListTile(
title: Text(email.from_name,
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(email.subject)],
),
trailing: Text(email.date.toString()),
onTap: () async {
List<String> emailContent = // list of the html
await getEmailContent(email.messages, folder);
// print("this is what email.messages look like in email.dart ${email.messages}");
// List<String> emailIds = email.messages;
print(email.messages); //email ids of the thread
Navigator.push(
context,
MaterialPageRoute(
// could call collapsable and inside collable each calls email view?
builder: (context) => EmailView(
emailContent: emailContent,
from: email.from_address,
name: email.from_name,
to: email.to.toString(),
subject: email.subject,
date: email.date.toString(),
id: email.id.toString(), //i think this is thread id?
messages: email.messages,
),
),
);
},
);
},
separatorBuilder: (context, index) => Divider(),
),
);
}
}
// ignore: must_be_immutable
class EmailPage extends StatefulWidget {
EmailPage({Key? key}) : super(key: key);
String selectedFolder = "INBOX"; //starter
int offset = 0;
int page = 1;
@override
EmailPageState createState() => EmailPageState();
}
class EmailPageState extends State<EmailPage> {
final ApiService apiService = ApiService();
List<GetThreadResponse> emails = [];
ValueNotifier<int> currentPageNotifier = ValueNotifier<int>(1);
int page = 1;
bool isBackDisabled = false;
@override
void initState() {
super.initState();
widget.page = page;
isBackDisabled = true;
_fetchEmails();
}
String getPage() => widget.page.toString();
bool get backDisabled => isBackDisabled;
void updateSelectedFolder(String folder) {
setState(() {
widget.selectedFolder = folder;
});
print(folder);
_fetchEmails();
}
void updatePagenation(String option) {
if (option == "next") {
setState(() {
widget.offset += 50;
widget.page += 1;
currentPageNotifier.value = widget.page;
isBackDisabled = false;
});
} else if (option == "back") {
setState(() {
widget.offset -= 50;
widget.page -= 1;
currentPageNotifier.value = widget.page;
if (widget.page == 1) {
isBackDisabled = true;
print("back disabled");
}
});
}
// print(currentPage);
print(widget.page);
_fetchEmails();
}
void _fetchEmails() async {
try {
List<GetThreadResponse> fetchedEmails = await apiService
.fetchEmailsFromFolder(widget.selectedFolder, widget.offset);
if (!mounted) return;
setState(() {
emails = fetchedEmails; // Update the list of emails
});
} catch (e) {
print('Error fetching emails: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: EmailListScreen(
emails: emails,
getEmailContent: apiService.fetchEmailContent,
folder: widget.selectedFolder, //try to grab from it directly
),
);
}
}

View File

@ -16,6 +16,7 @@ class FolderDrawer extends StatefulWidget {
class _FolderDrawerState extends State<FolderDrawer> {
List<String> folders = [];
final TextEditingController _renameController = TextEditingController();
@override
void initState() {
@ -48,7 +49,7 @@ class _FolderDrawerState extends State<FolderDrawer> {
icon: Icon(Icons.more_vert),
onPressed: () => {
///show options
_showOptions(context)
_showOptions(context, folder)
},
),
onTap: () {
@ -65,7 +66,7 @@ class _FolderDrawerState extends State<FolderDrawer> {
showDialog(
context: context,
builder: (BuildContext context) {
return NewMailbox(apiService: widget.apiService);
return NewMailbox(apiService: widget.apiService);
},
);
// Navigator.of(context).pop();
@ -76,46 +77,114 @@ class _FolderDrawerState extends State<FolderDrawer> {
title: Text('Refresh Folders'),
onTap: () {
_fetchFolders(); // Refresh folders when this tile is tapped
Navigator.pop(context); // Close the drawer after refresh
// rebuild
// Navigator.pop(context); // Close the drawer after refresh
},
),
],
),
);
}
void _showOptions(BuildContext context) async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
await showMenu<String>(
context: context,
position: RelativeRect.fromLTRB(100, 100, overlay.size.width, overlay.size.height),
items: <PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'Rename',
child: Text('Rename Folder'),
),
PopupMenuItem<String>(
value: 'Delete',
child: Text('Delete Folder'),
),
],
).then((value) {
// Handle the action based on the selected menu item
if (value == 'Rename') {
// Logic for renaming the folder
print('Rename folder');
} else if (value == 'Delete') {
// Logic for deleting the folder
print('Delete folder');
}
});
}
Future<bool> _renameDialog(String oldFolder) async {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Rename Mailbox"),
content: TextField(
controller: _renameController,
decoration: const InputDecoration(
hintText: "New Name",
),
),
actions: <Widget>[
TextButton(
onPressed: () {
String newfolderName = _renameController.text;
if (newfolderName.isNotEmpty) {
//make an and to make sure there's two folders with the same name
ApiService().renameFolder(oldFolder, newfolderName);
}
Navigator.of(context).pop();
},
child: const Text("Rename"),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Cancel"),
)
],
);
});
return false;
}
Future<void> doubleCheckDelete(String folderTobeDeleted) async {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Confirm delete of: $folderTobeDeleted"),
actions: [
TextButton(
onPressed: () {
ApiService().deleteFolder(folderTobeDeleted);
Navigator.of(context).pop();
},
child: Text("Yes")),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("No")),
],
);
});
}
void _showOptions(BuildContext context, String folderName) async {
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
print(folderName);
await showMenu<String>(
context: context,
position: RelativeRect.fromLTRB(
100, 100, overlay.size.width, overlay.size.height),
items: <PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'Rename',
child: Text('Rename Folder'),
),
PopupMenuItem<String>(
value: 'Delete',
child: Text('Delete Folder'),
),
],
).then((value) {
// Handle the action based on the selected menu item
if (value == 'Rename') {
// Logic for renaming the folder
print('Rename folder $folderName');
_renameDialog(folderName);
} else if (value == 'Delete') {
// Logic for deleting the folder
print("Deleting $folderName");
doubleCheckDelete(folderName);
// ApiService().deleteFolder(folderName);
print('Deleted folder');
}
});
}
}
class NewMailbox extends StatelessWidget {
final ApiService apiService;
// final Function(String) onFolderCreated;
final TextEditingController _textFieldController = TextEditingController();
NewMailbox({required this.apiService});
@ -127,7 +196,7 @@ class NewMailbox extends StatelessWidget {
content: TextField(
controller: _textFieldController,
decoration: const InputDecoration(
hintText: "EPIC FOLDER", // Your custom hint text here
hintText: "New Folder",
),
),
actions: <Widget>[
@ -138,13 +207,17 @@ class NewMailbox extends StatelessWidget {
if (folderName.isNotEmpty) {
apiService.createFolder(folderName);
// onFolderCreated(folderName);
}
// apiService.createFolder(_textFieldController.text);
Navigator.of(context).pop();
},
child: const Text("Approve"),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Cancel"),
)
],
);
}

View File

@ -1,440 +1,465 @@
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 {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<EmailPageState> _emailPageKey = GlobalKey<EmailPageState>();
ApiService apiService = ApiService();
bool _isSidebarOpen = true;
bool querySearches = false;
String? _selectedOption = "INBOX";
List<String> _tabs = ['Emails'];
Map<String, Widget> _tabWidgets = {};
TabController? _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: _tabs.length, vsync: this);
_tabWidgets['Emails'] = EmailPage(
key: _emailPageKey,
);
}
// Add a new tab based on the search
void _performSearch(String query, String? list) {
setState(() {
if (!_tabs.contains(query)) {
_tabs.add(query);
_tabWidgets[query] = _buildSearchResultsWidget(
query, list); // Store a different widget for this tab
_tabController = TabController(length: _tabs.length, vsync: this);
}
});
}
void _showOptionsSearchDialog () async {
List<String> folders = await apiService.fetchFolders();
if (mounted) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Choose an Option'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: folders.map((option) {
return ListTile(
title: Text(option),
leading: Radio<String>(
value: option,
groupValue: _selectedOption, // Bind with _selectedOption
onChanged: (String? value) {
setState(() {
_selectedOption = value;
});
Navigator.of(context).pop(); // Close the dialog on selection
},
),
);
}).toList(),
),
actions: <Widget>[
ElevatedButton(
child: Text('Submit'),
onPressed: () {
Navigator.of(context).pop(); // Close the dialog
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('You selected: $_selectedOption'),
));
},
),
],
);
},
);}
}
// Remove a tab
void _removeTab(int index) {
if (_tabs[index] != 'Emails') {
setState(() {
String tabToRemove = _tabs[index];
_tabs.removeAt(index);
_tabWidgets
.remove(tabToRemove); // Remove widget associated with the tab
_tabController = TabController(length: _tabs.length, vsync: this);
});
}
}
// Build a custom widget for each search query
Widget _buildSearchResultsWidget(String query, String? list) {
return FutureBuilder<List<SerializableMessage>>(
future: apiService.sonicSearch(list ?? "INBOX", 50, 0, query),
builder: (BuildContext context,
AsyncSnapshot<List<SerializableMessage>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No results found for: $query'));
} else {
List<SerializableMessage> result = snapshot.data!;
return Scaffold(
body: ListView.separated(
itemCount: result.length,
itemBuilder: (context, index) {
final SerializableMessage email = result[index];
return ListTile(
title: Text(email.from,
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(email.subject)],
),
trailing: Text(email.date.toString()),
onTap: () async {
// print('tapped');
String emailContent =
await apiService.fetchEmailContent([email.id], email.list);
// print('content below');
// print(emailContent);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EmailView(
emailContent: emailContent,
from: email.from,
name: email.name,
to: email.to.toString(),
subject: email.subject,
date: email.date.toString(),
id: email.id.toString(),
),
),
);
},
);
},
separatorBuilder: (context, index) => Divider(),
),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text("Results for: $query", style: TextStyle(fontSize: 24)),
// // Display the actual data
// Text(result[0].name), // Accessing the first result safely
// Text(result[0].from), // Displaying the 'from' field as an example
// Text(result[0].hash),
// Text(result[0].subject),
// Text(result[0].uid.toString()),
// Text(result[0].list),
// Text(result[0].id),
// // Add more fields or customize the display
// // SerializableEmailListScreen(emails: result, getEmailContent: getEmailContent)
// // Expanded(
// // child:
// // ),
// ],
);
// );
}
},
);
}
@override
void dispose() {
_tabController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
drawer: FolderDrawer(
apiService: apiService,
onFolderTap: (folder) {
_emailPageKey.currentState?.updateSelectedFolder(folder);
},
),
body: Stack(
children: [
Row(
children: [
// Sidebar
if (_isSidebarOpen)
Container(
width: 70,
color: Color.fromARGB(17, 96, 122, 135),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: Icon(Icons.home),
onTap: () {
// Navigate to Home
},
),
ListTile(
leading: Icon(Icons.settings),
onTap: () {
// Navigate to Settings
},
),
ListTile(
leading: Icon(Icons.email),
onTap: () {
_scaffoldKey.currentState?.openDrawer();
},
),
Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: Alignment.bottomLeft,
child: IconButton(
icon: Icon(Icons.close, color: Colors.white),
onPressed: () {
setState(() {
_isSidebarOpen = false;
});
},
),
),
),
],
),
),
// Main content
Expanded(
child: Column(
children: [
Container(
padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 4.0),
color: Color.fromARGB(42, 36, 102, 132),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 800,
height: 40,
child: TextField(
decoration: InputDecoration(
hintText: 'Search...',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.search),
),
onSubmitted: (value) {
if (value.isNotEmpty) {
_performSearch(value, _selectedOption);
}
//this is the input box i mentioned
// if (value == '') {
// setState(() {
// querySearches = false;
// });
// }
// Future<List<String>> results = apiService
// .sonicSearch('INBOX', 20, 0, value);
// // print(value);
// print(results);
// setState(() {
// querySearches = true;
// });
},
),
),
SizedBox(
width: 16,
),
Container(
width: 80,
height: 40,
child: ElevatedButton(
onPressed: _showOptionsSearchDialog,
child: Icon(Icons.manage_search),
),
)
],
),
),
Container(
padding: EdgeInsets.all(0.0),
color: Color.fromARGB(42, 36, 102, 132),
child: Row(
children: [
Container(
height: 2,
)
],
),
),
Container(
color: Color.fromARGB(255, 131, 110, 143),
child: TabBar(
controller: _tabController,
isScrollable: true,
tabs: _tabs
.asMap()
.entries
.map((entry) => Tab(
child: Row(
children: [
Text(entry.value),
if (entry.value != 'Emails')
GestureDetector(
onTap: () => _removeTab(entry.key),
child: Icon(Icons.close, size: 16),
),
],
),
))
.toList(),
labelColor: Colors.white,
indicatorColor: Colors.white,
),
),
Container(
// alignment: Alignment.topLeft,
padding: EdgeInsets.all(8.0),
color: Colors.white,
child: Row(
children: [
ElevatedButton(
onPressed: () {
_emailPageKey.currentState!.isBackDisabled ? null: _emailPageKey.currentState
?.updatePagenation('back');
},
child: Icon(Icons.navigate_before),
),
Text(_emailPageKey.currentState?.getPage() ?? '1'),
ElevatedButton(
onPressed: () {
_emailPageKey.currentState
?.updatePagenation('next');
},
child: Icon(Icons.navigate_next),
),
],
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: _tabs.map((tab) {
return _tabWidgets[tab] ??
Center(child: Text("No content found"));
// return Center(
// child: EmailPage(
// key: _emailPageKey,
// ));
}).toList(),
),
),
// if (_tabs.isEmpty)
// Expanded(
// child: EmailPage(key: _emailPageKey),
// ),
// if (_tabs.isNotEmpty)
// Expanded(
// // child: Text('supposed to be mails'),
// child: TabBarView(
// controller: _tabController,
// children: _tabs
// .map((tab) => Center(child: Text('Results for $tab')))
// .toList(),
// ),
// ),
],
),
),
],
),
if (!_isSidebarOpen)
Positioned(
bottom: 16,
left: 16,
child: FloatingActionButton(
child: Icon(Icons.menu),
onPressed: () {
setState(() {
_isSidebarOpen = true;
});
},
),
),
],
),
);
}
}
// void _showPopupMenu(BuildContext context, Offset position) async {
// final RenderBox overlay =
// Overlay.of(context).context.findRenderObject() as RenderBox;
// await showMenu<String>(
// context: context,
// position: RelativeRect.fromLTRB(
// position.dx,
// position.dy,
// overlay.size.width - position.dx,
// overlay.size.height - position.dy,
// ),
// items: <PopupMenuEntry<String>>[
// PopupMenuItem<String>(
// value: 'Open',
// child: Text('Open'),
// ),
// PopupMenuItem<String>(
// value: 'Reply',
// child: Text('Reply'),
// ),
// PopupMenuItem<String>(
// value: 'Delete',
// child: Text('Delete'),
// ),
// ],
// );
// }
// }
import 'package:crab_ui/sonicEmailView.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 {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<EmailPageState> _emailPageKey = GlobalKey<EmailPageState>();
ApiService apiService = ApiService();
bool _isSidebarOpen = true;
bool querySearches = false;
String? _selectedOption = "INBOX";
List<String> _tabs = ['Emails'];
Map<String, Widget> _tabWidgets = {};
TabController? _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: _tabs.length, vsync: this);
_tabWidgets['Emails'] = EmailPage(
key: _emailPageKey,
);
}
// Add a new tab based on the search
void _performSearch(String query, String? list) {
setState(() {
if (!_tabs.contains(query)) {
_tabs.add(query);
_tabWidgets[query] = _buildSearchResultsWidget(
query, list); // Store a different widget for this tab
_tabController = TabController(length: _tabs.length, vsync: this);
}
});
}
void _showOptionsSearchDialog () async {
List<String> folders = await apiService.fetchFolders();
if (mounted) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Choose an Option'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: folders.map((option) {
return ListTile(
title: Text(option),
leading: Radio<String>(
value: option,
groupValue: _selectedOption, // Bind with _selectedOption
onChanged: (String? value) {
setState(() {
_selectedOption = value;
});
Navigator.of(context).pop(); // Close the dialog on selection
},
),
);
}).toList(),
),
actions: <Widget>[
ElevatedButton(
child: Text('Submit'),
onPressed: () {
Navigator.of(context).pop(); // Close the dialog
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('You selected: $_selectedOption'),
));
},
),
],
);
},
);}
}
// Remove a tab
void _removeTab(int index) {
if (_tabs[index] != 'Emails') {
setState(() {
String tabToRemove = _tabs[index];
_tabs.removeAt(index);
_tabWidgets
.remove(tabToRemove); // Remove widget associated with the tab
_tabController = TabController(length: _tabs.length, vsync: this);
});
}
}
// Build a custom widget for each search query
Widget _buildSearchResultsWidget(String query, String? list) {
return FutureBuilder<List<SerializableMessage>>(
future: apiService.sonicSearch(list ?? "INBOX", 50, 0, query),
builder: (BuildContext context,
AsyncSnapshot<List<SerializableMessage>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No results found for: $query'));
} else {
List<SerializableMessage> result = snapshot.data!;
return Scaffold(
body: ListView.separated(
itemCount: result.length,
itemBuilder: (context, index) {
final SerializableMessage email = result[index];
return ListTile(
title: Text(email.from,
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(email.subject)],
),
trailing: Text(email.date.toString()),
onTap: () async {
// print('tapped');
// List<String> emailContent =
// await apiService.fetchEmailContent([email.id], email.list);
//call the foldable
List<String> emailContent = // list of the html
await apiService.fetchEmailContent([email.id], email.list);
// List<String> emailIds = email.messages;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>SonicEmailView(
email: email,
emailHTML: emailContent[0])
// builder: (context) => EmailView(
// emailContent: emailContent,
// from: email.from,
// name: email.name,
// to: email.to.toString(),
// subject: email.subject,
// date: email.date.toString(),
// id: email.id.toString(),
// messages: [email.id],
// ),
),
);
},
);
},
separatorBuilder: (context, index) => Divider(),
),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text("Results for: $query", style: TextStyle(fontSize: 24)),
// // Display the actual data
// Text(result[0].name), // Accessing the first result safely
// Text(result[0].from), // Displaying the 'from' field as an example
// Text(result[0].hash),
// Text(result[0].subject),
// Text(result[0].uid.toString()),
// Text(result[0].list),
// Text(result[0].id),
// // Add more fields or customize the display
// // SerializableEmailListScreen(emails: result, getEmailContent: getEmailContent)
// // Expanded(
// // child:
// // ),
// ],
);
// );
}
},
);
}
@override
void dispose() {
_tabController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
drawer: FolderDrawer(
apiService: apiService,
onFolderTap: (folder) {
_emailPageKey.currentState?.updateSelectedFolder(folder);
},
),
body: Stack(
children: [
Row(
children: [
// Sidebar
if (_isSidebarOpen)
Container(
width: 70,
color: Color.fromARGB(17, 96, 122, 135),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: Icon(Icons.home),
onTap: () {
// Navigate to Home
},
),
ListTile(
leading: Icon(Icons.settings),
onTap: () {
// Navigate to Settings
},
),
ListTile(
leading: Icon(Icons.email),
onTap: () {
_scaffoldKey.currentState?.openDrawer();
},
),
Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: Alignment.bottomLeft,
child: IconButton(
icon: Icon(Icons.close, color: Colors.white),
onPressed: () {
setState(() {
_isSidebarOpen = false;
});
},
),
),
),
],
),
),
// Main content
Expanded(
child: Column(
children: [
Container(
padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 4.0),
color: Color.fromARGB(42, 36, 102, 132),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 800,
height: 40,
child: TextField(
decoration: InputDecoration(
hintText: 'Search...',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.search),
),
onSubmitted: (value) {
if (value.isNotEmpty) {
_performSearch(value, _selectedOption);
}
//this is the input box i mentioned
// if (value == '') {
// setState(() {
// querySearches = false;
// });
// }
// Future<List<String>> results = apiService
// .sonicSearch('INBOX', 20, 0, value);
// // print(value);
// print(results);
// setState(() {
// querySearches = true;
// });
},
),
),
SizedBox(
width: 16,
),
Container(
width: 80,
height: 40,
child: ElevatedButton(
onPressed: _showOptionsSearchDialog,
child: Icon(Icons.manage_search),
),
)
],
),
),
Container(
padding: EdgeInsets.all(0.0),
color: Color.fromARGB(42, 36, 102, 132),
child: Row(
children: [
Container(
height: 2,
)
],
),
),
Container(
color: Color.fromARGB(255, 131, 110, 143),
child: TabBar(
controller: _tabController,
isScrollable: true,
tabs: _tabs
.asMap()
.entries
.map((entry) => Tab(
child: Row(
children: [
Text(entry.value),
if (entry.value != 'Emails')
GestureDetector(
onTap: () => _removeTab(entry.key),
child: Icon(Icons.close, size: 16),
),
],
),
))
.toList(),
labelColor: Colors.white,
indicatorColor: Colors.white,
),
),
Container(
// alignment: Alignment.topLeft,
padding: EdgeInsets.all(8.0),
color: Colors.white,
child: Row(
children: [
ElevatedButton(
onPressed: () {
_emailPageKey.currentState!.isBackDisabled ? null: _emailPageKey.currentState
?.updatePagenation('back');
},
child: Icon(Icons.navigate_before),
),
Builder(
builder: (context) {
final emailState = _emailPageKey.currentState;
if (emailState == null) {
// Schedule a rebuild once the state is available
Future.microtask(() => setState(() {}));
return Text('Loading...');
}
return ValueListenableBuilder<int>(
valueListenable: emailState.currentPageNotifier,
builder: (context, value, _) => Text('$value'),
);
},
),
ElevatedButton(
onPressed: () {
_emailPageKey.currentState
?.updatePagenation('next');
},
child: Icon(Icons.navigate_next),
),
],
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: _tabs.map((tab) {
return _tabWidgets[tab] ??
Center(child: Text("No content found"));
// return Center(
// child: EmailPage(
// key: _emailPageKey,
// ));
}).toList(),
),
),
// if (_tabs.isEmpty)
// Expanded(
// child: EmailPage(key: _emailPageKey),
// ),
// if (_tabs.isNotEmpty)
// Expanded(
// // child: Text('supposed to be mails'),
// child: TabBarView(
// controller: _tabController,
// children: _tabs
// .map((tab) => Center(child: Text('Results for $tab')))
// .toList(),
// ),
// ),
],
),
),
],
),
if (!_isSidebarOpen)
Positioned(
bottom: 16,
left: 16,
child: FloatingActionButton(
child: Icon(Icons.menu),
onPressed: () {
setState(() {
_isSidebarOpen = true;
});
},
),
),
],
),
);
}
}
// void _showPopupMenu(BuildContext context, Offset position) async {
// final RenderBox overlay =
// Overlay.of(context).context.findRenderObject() as RenderBox;
// await showMenu<String>(
// context: context,
// position: RelativeRect.fromLTRB(
// position.dx,
// position.dy,
// overlay.size.width - position.dx,
// overlay.size.height - position.dy,
// ),
// items: <PopupMenuEntry<String>>[
// PopupMenuItem<String>(
// value: 'Open',
// child: Text('Open'),
// ),
// PopupMenuItem<String>(
// value: 'Reply',
// child: Text('Reply'),
// ),
// PopupMenuItem<String>(
// value: 'Delete',
// child: Text('Delete'),
// ),
// ],
// );
// }
// }

View File

@ -1,85 +0,0 @@
import 'package:flutter/material.dart';
import 'api_service.dart';
import 'structs.dart';
class SerializableMessageListScreen extends StatefulWidget {
@override
_SerializableMessageListScreenState createState() => _SerializableMessageListScreenState();
}
class _SerializableMessageListScreenState extends State<SerializableMessageListScreen> {
List<SerializableMessage>? messages;
bool isLoading = true;
bool hasError = false;
final ApiService apiService = ApiService();
@override
void initState() {
super.initState();
_fetchMessages();
}
Future<void> _fetchMessages() async {
try {
List<SerializableMessage> fetchedMessages = await apiService.sonicSearch("INBOX", 10, 0, "searchQuery");
setState(() {
messages = fetchedMessages;
isLoading = false;
});
} catch (e) {
setState(() {
hasError = true;
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
if (hasError) {
return Center(child: Text("Error fetching messages."));
}
if (messages == null || messages!.isEmpty) {
return Center(child: Text("No messages found."));
}
return ListView.separated(
itemCount: messages!.length,
itemBuilder: (context, index) {
final message = messages![index];
return ListTile(
title: Text(message.name, style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(message.subject)],
),
trailing: Text(message.date),
onTap: () async {
String emailContent = await apiService.fetchEmailContent([message.id], message.list);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EmailView(
emailContent: emailContent,
from: message.from,
name: message.name,
to: message.to.toString(),
subject: message.subject,
date: message.date,
id: message.id,
),
),
);
},
);
},
separatorBuilder: (context, index) => Divider(),
);
}
}

145
lib/sonicEmailView.dart Normal file
View File

@ -0,0 +1,145 @@
import 'package:crab_ui/augment.dart';
import 'package:web/web.dart' as web;
import 'dart:ui_web' as ui;
import 'dart:js_interop';
import 'structs.dart';
import 'package:flutter/material.dart';
class SonicEmailView extends StatefulWidget {
SerializableMessage email;
String emailHTML;
SonicEmailView({required this.email, required this.emailHTML});
@override
_SonicEmailViewState createState() => _SonicEmailViewState();
}
class _SonicEmailViewState extends State<SonicEmailView> {
String viewTypeIDs = "";
int heightOFViewtype = 0;
bool _isLoaded = false;
void _scrollToNumber(String spanId) {
AugmentClasses.handleJump(spanId);
}
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
await _registerViewFactory(widget.emailHTML);
if (!mounted) return;
setState(() {
_isLoaded = true;
});
}
Future<void> _registerViewFactory(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 = currentContent.toJS;
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 = widget
.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,
);
this.viewTypeIDs = viewTypeId;
this.heightOFViewtype = heightOfEmail;
print(viewTypeIDs);
}
@override
Widget build(BuildContext context) {
return _isLoaded
? Scaffold(
appBar: AppBar(title: Text(widget.email.subject)),
body: Stack(
children: [
Column(
children: [
EmailToolbar(
onButtonPressed: () => {},
onJumpToSpan: _scrollToNumber),
Row(
// title of email
children: [
Text(
widget.email.subject,
style: TextStyle(fontSize: 30),
),
],
),
Row(
children: [
Text(
'from ${widget.email.name}',
style: TextStyle(fontSize: 18),
),
Text(
'<${widget.email.from}>',
style: TextStyle(fontSize: 18),
),
Spacer(),
Text(
'${widget.email.date}',
textAlign: TextAlign.right,
)
],
),
// TODO: make a case where if one of these is the user's email it just says me :)))))
Row(
children: [
Text(
'to ${widget.email.to.toString()}',
style: TextStyle(fontSize: 15),
)
],
),
Expanded(
// child: SizedBox(
// height: heightOFViewtype.toDouble(),
child: HtmlElementView(
key: UniqueKey(), viewType: this.viewTypeIDs,
// ),
))
],
),
],
),
)
: const Center(
child: CircularProgressIndicator(),
);
}
}

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.5.0"
asn1lib:
dependency: transitive
description:
name: asn1lib
sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5"
url: "https://pub.dev"
source: hosted
version: "1.5.8"
async:
dependency: transitive
description:
@ -97,14 +105,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.18.0"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.6"
csslib:
dependency: transitive
description:
@ -121,6 +137,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dio:
dependency: transitive
description:
name: dio
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
url: "https://pub.dev"
source: hosted
version: "5.8.0+1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
encrypt:
dependency: "direct main"
description:
name: encrypt
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
english_words:
dependency: "direct main"
description:
@ -141,10 +181,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
file:
dependency: transitive
description:
@ -153,6 +193,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
file_saver:
dependency: "direct main"
description:
name: file_saver
sha256: "017a127de686af2d2fbbd64afea97052d95f2a0f87d19d25b87e097407bf9c1e"
url: "https://pub.dev"
source: hosted
version: "0.2.14"
fixnum:
dependency: transitive
description:
@ -368,6 +416,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
js:
dependency: transitive
description:
@ -464,6 +520,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.12.0"
mime:
dependency: "direct main"
description:
name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
nested:
dependency: transitive
description:
@ -500,10 +564,10 @@ packages:
dependency: transitive
description:
name: path_provider
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
@ -544,6 +608,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
pdfrx:
dependency: "direct main"
description:
name: pdfrx
sha256: "29c7b03d27d647c80da8cc08bd1256c74df90e5640fdd676646e4bd04f90553a"
url: "https://pub.dev"
source: hosted
version: "1.0.94"
petitparser:
dependency: transitive
description:
@ -552,6 +624,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.2"
photo_view:
dependency: "direct main"
description:
name: photo_view
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
url: "https://pub.dev"
source: hosted
version: "0.15.0"
platform:
dependency: transitive
description:
@ -568,6 +648,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointer_interceptor:
dependency: "direct main"
description:
name: pointer_interceptor
sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523"
url: "https://pub.dev"
source: hosted
version: "0.10.1+2"
pointer_interceptor_ios:
dependency: transitive
description:
name: pointer_interceptor_ios
sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917
url: "https://pub.dev"
source: hosted
version: "0.10.1"
pointer_interceptor_platform_interface:
dependency: transitive
description:
name: pointer_interceptor_platform_interface
sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506"
url: "https://pub.dev"
source: hosted
version: "0.10.0+1"
pointer_interceptor_web:
dependency: transitive
description:
name: pointer_interceptor_web
sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044"
url: "https://pub.dev"
source: hosted
version: "0.10.2+1"
pointycastle:
dependency: "direct main"
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
provider:
dependency: "direct main"
description:
@ -592,6 +712,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.27.7"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
@ -697,10 +873,10 @@ packages:
dependency: transitive
description:
name: url_launcher
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
@ -886,13 +1062,13 @@ packages:
source: hosted
version: "0.2.1"
web:
dependency: transitive
dependency: "direct main"
description:
name: web
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.1.1"
webview_flutter:
dependency: transitive
description:

View File

@ -27,6 +27,7 @@ dependencies:
intl: ^0.19.0
pdfrx: ^1.0.94
photo_view: ^0.15.0
web: ^1.1.1
dev_dependencies:
flutter_test:

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip

View File

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\juana\Flutter\flutter_windows_3.22.2-stable.zip