WIP: android/ios-adaption feature, markdown, and augment #6

Draft
Juan wants to merge 61 commits from android-adaption into main
71 changed files with 4682 additions and 1542 deletions

View File

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
revision: "be698c48a6750c8cb8e61c740ca9991bb947aba2"
channel: "stable"
project_type: app
@ -13,26 +13,26 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
- platform: android
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
- platform: ios
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
- platform: linux
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
- platform: macos
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
- platform: web
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
- platform: windows
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
# User provided section

14
android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,44 @@
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")
}
android {
namespace = "com.example.crab_ui"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
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://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
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.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,7 @@
<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

@ -0,0 +1,45 @@
<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

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

View File

@ -0,0 +1,12 @@
<?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

@ -0,0 +1,12 @@
<?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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?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

@ -0,0 +1,18 @@
<?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

@ -0,0 +1,7 @@
<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>

21
android/build.gradle.kts Normal file
View File

@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

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

View File

@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
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 "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

13
android_old/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
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

@ -0,0 +1,58 @@
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.hym_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.hym_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

@ -0,0 +1,7 @@
<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

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="hym_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

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

View File

@ -0,0 +1,12 @@
<?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

@ -0,0 +1,12 @@
<?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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?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

@ -0,0 +1,18 @@
<?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

@ -0,0 +1,7 @@
<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>

32
android_old/build.gradle Normal file
View File

@ -0,0 +1,32 @@
buildscript {
ext.kotlin_version = '1.9.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
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

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

View File

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

View File

@ -0,0 +1,25 @@
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"

280
lib/Compose.dart Normal file
View File

@ -0,0 +1,280 @@
import 'package:crab_ui/api_service.dart';
import 'package:crab_ui/structs.dart';
import 'package:flutter/material.dart';
import 'package:super_editor/super_editor.dart';
import 'package:super_editor_markdown/super_editor_markdown.dart';
class ComposeEmail extends StatefulWidget {
final VoidCallback onClose;
final Function(String) onMinimize;
final Function(String) onSendMessage;
GetThreadResponse? emailDraftID;
ComposeEmail(
{Key? key,
required this.onMinimize,
required this.onClose,
required this.onSendMessage,
this.emailDraftID})
: super(key: key);
@override
_ComposeEmailState createState() => _ComposeEmailState();
}
class _ComposeEmailState extends State<ComposeEmail> {
// if one were to alter a mutableDocument, one should only alter the document through EditRequest to the Editor
late final Editor _editor;
late final MutableDocument _document;
late final MutableDocumentComposer _composer;
TextEditingController _emailRecipientController = TextEditingController();
TextEditingController _emailSubjectController = TextEditingController();
List<String>? contentOfDraft;
bool isInitialized = false;
@override
void initState() {
super.initState();
_loadDraftContent();
}
void _loadDraftContent() async {
if (widget.emailDraftID != null) {
String? drafted = widget.emailDraftID?.messages.last;
if (drafted != null) {
contentOfDraft =
await ApiService().fetchMarkdownContent([drafted!], "Drafts");
setState(() {
_document = MutableDocument(nodes: [
ParagraphNode(
id: Editor.createNodeId(),
text: AttributedText(contentOfDraft?[0] ??
""), // NOW THIS SHOULD BE WTV ITS IN DRAFTS
)
]);
_composer = MutableDocumentComposer();
_editor = createDefaultDocumentEditor(
document: _document, composer: _composer);
_emailRecipientController.text = widget.emailDraftID!.to[0].address;
_emailSubjectController.text = widget.emailDraftID!.subject;
isInitialized = true;
});
}
} else {
setState(() {
_document = MutableDocument(nodes: [
ParagraphNode(
id: Editor.createNodeId(),
text: AttributedText(""),
),
]);
_composer = MutableDocumentComposer();
_editor = createDefaultDocumentEditor(
document: _document, composer: _composer);
isInitialized = true;
});
}
}
@override
void dispose() {
_editor.dispose();
_emailRecipientController.dispose();
_emailSubjectController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!isInitialized) {
return Center(
child: CircularProgressIndicator(),
);
}
return Positioned(
bottom: 10.0,
right: 10.0,
child: Material(
elevation: 8.0,
child: Container(
width: 600.0,
height: 616.0,
decoration: BoxDecoration(
color: Colors.white,
),
child: Column(children: [
AppBar(
title: const Text("new message"),
actions: [
IconButton(
onPressed: () {
//TODO: implement minimize, and submit email to drafts
widget.onClose();
},
icon: Icon(Icons.minimize, color: Colors.grey[600])),
IconButton(
onPressed: () {
//TODO: implement maximizing the window or widget
},
icon: Icon(Icons.maximize, color: Colors.grey[600])),
IconButton(
onPressed: () {
widget.onClose();
},
icon: Icon(Icons.close, color: Colors.grey[600])),
],
),
Container(
// TODO: WHEN NOT CLICKED ITS ONLY A TEXTFIELD WITH A HINT, AND THEN WHEN CLICKED THIS
// width: 500.0,
// height: 40.0,
child: Row(
children: [
TextButton(onPressed: () {}, child: Text("To:")),
Expanded(
child: TextField(
controller: _emailRecipientController,
),
),
TextButton(onPressed: () {}, child: Text("Cc")),
SizedBox(
width: 4.0,
),
TextButton(onPressed: () {}, child: Text("Bcc")),
],
),
),
SizedBox(
height: 4,
),
Container(
// TODO: WHEN NOT CLICKED ITS ONLY A TEXTFIELD WITH A HINT, AND THEN WHEN CLICKED THIS
width: 500.0,
// height: 40.0,
child: Row(
children: [
Expanded(
child: TextField(
controller: _emailSubjectController,
decoration: InputDecoration(
hintText: "Subject",
),
),
)
],
),
),
Expanded(
//here the widget goes
child: SuperEditor(
//make this its own
editor: _editor,
plugins: {MarkdownInlineUpstreamSyntaxPlugin()},
// stylesheet: Stylesheet(
// rules: [StyleRule(BlockSelector.all, (doc, docNode) {
// return {
// Styles.maxWidth: 640.0,
// Styles.padding: const CascadingPadding.symmetric(horizontal: 24),
// Styles.textStyle: const TextStyle(
// color: Colors.black,
// // fontSize: 15,
// height: 1.4,
// ),
// };
// }),],
// inlineTextStyler: defaultInlineTextStyler)
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
ElevatedButton(
onPressed: () {
print('sent');
String markdown =
serializeDocumentToMarkdown(_editor.document);
print(_emailRecipientController.text);
print(_emailSubjectController.text);
print(markdown);
ApiService().sendEmail(_emailRecipientController.text,
_emailSubjectController.text, markdown);
},
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30)),
elevation: 4,
foregroundColor: Colors.white,
backgroundColor: Colors.blueAccent),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Send',
style: TextStyle(fontWeight: FontWeight.bold),
),
// const SizedBox(
// width: 8,
// ),
// Container(
// height: 30, width: 1.0, color: Colors.white),
// const SizedBox(
// width: 8,
// ),
// const Icon(
// Icons.arrow_drop_down,
// size: 24,
// )
],
),
),
],
),
)
])),
),
);
}
}
class OverlayService {
static final OverlayService _instance = OverlayService._internal();
factory OverlayService() => _instance;
OverlayService._internal();
OverlayEntry? _overlayEntry;
GetThreadResponse? draftID;
void showPersistentWidget(BuildContext context) {
if (_overlayEntry != null) {
print("overlay visible");
return;
}
_overlayEntry = OverlayEntry(
builder: (context) => ComposeEmail(
onClose: () {
removeComposeWidget();
},
onMinimize: (String content) {
minimizeComposeWidget(content);
},
onSendMessage: (message) {
print('msg senf form overlay $message');
},
emailDraftID: draftID));
Navigator.of(context).overlay?.insert(_overlayEntry!);
print("inserted into tree");
}
void removeComposeWidget() {
_overlayEntry?.remove();
_overlayEntry = null;
}
String minimizeComposeWidget(String content) {
//just hide the overlay but keep its info
return '';
}
}

View File

@ -0,0 +1,19 @@
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> {
@override
Widget build(BuildContext context) {
return Scaffold(body: Text("sonic email android"));
}
}

View File

@ -0,0 +1,22 @@
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> {
@override
Widget build(BuildContext context) {
return Scaffold(
body:Text("sonic email stub")
);
}
}

157
lib/SonicEmailViewWeb.dart Normal file
View File

@ -0,0 +1,157 @@
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);
}
void _handleViewspecs(String queryViewspecs) {
return;
}
void _handleFiltering(String query) {
return;
}
@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: () => {},
onJumpToNumbering: _scrollToNumber,
onViewspecs: _handleViewspecs,
onFiltering: _handleFiltering,
emails: [widget.email.name], subject: '', rootAugment: null,
),
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,26 +1,16 @@
// this file should handle most of the API calls
// it also builds some widgets, but it will be modulated later
// it also builds some widgets, but it will be modulated later // chat it did
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';
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 'package:web/web.dart' as web;
import 'dart:js_interop' as js;
class ApiService {
static String ip = "";
static String port = "";
static String ip = '127.0.0.1';
static String port = "3001";
static List<AttachmentResponse> threadAttachments =
[]; //holds attachments of the thread
static String currFolder = "";
@ -59,7 +49,39 @@ class ApiService {
return [];
}
}
Future<List<GetThreadResponse>> fetchEmailsFromFolderReversed(
String folder, int pagenitaion) async {
try {
var url = Uri.http('$ip:$port', 'sorted_threads_by_date_current', {
'folder': folder,
'limit': '50',
'offset': pagenitaion.toString(),
});
var response = await http.get(url);
List<GetThreadResponse> allEmails = [];
if (response.statusCode == 200) {
List json = jsonDecode(response.body);
for (var item in json) {
//each item in the json is a date
if (item.length > 1 && item[0] is String && item[1] is List) {
List<int> threadIDs = List<int>.from(item[1]);
for (var threadId in threadIDs) {
await fetchThreads(threadId, allEmails);
}
}
}
currFolder = folder;
return allEmails;
} else {
throw Exception('Failed to load threads');
}
} catch (e) {
print('_displayEmailsFromFolder caught error: $e');
return [];
}
}
Future<void> fetchThreads(
//populates allEmails, which is the List that contains all the emails in a thread
int threadId,
@ -148,7 +170,6 @@ class ApiService {
} catch (e) {
print('_getEmailContent caught error: $e');
}
// return content;
return HTMLofThread;
}
@ -180,7 +201,7 @@ class ApiService {
Future<bool> moveEmail(
//only moves the first email of the thread //or perhaps should do the last
String fromFolder,
String thread_id,
String thread_id, //uid
String toFolder) async {
var url = Uri.http('$ip:$port', 'move_email');
@ -191,16 +212,17 @@ class ApiService {
return false;
}
SerializableMessage firstMail = mailsInSerializable[0];
Map<String, String> requestBody = {
'from': fromFolder,
'uid': firstMail.uid.toString(),
'to': toFolder,
};
// SerializableMessage firstMail = mailsInSerializable[0];
try {
var response = await http.post(
for (SerializableMessage mail in mailsInSerializable) {
Map<String, String> requestBody = {
'from': fromFolder,
'uid': mail.uid.toString(),
'to': "Deleted Crabmail",
};
var response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
@ -212,7 +234,7 @@ class ApiService {
return true;
} else {
print('error ${response.statusCode} ${response.body}');
}
}}
} catch (e) {
print("failed trying to post move_email, with error: $e");
}
@ -351,308 +373,143 @@ class ApiService {
return AttachmentResponse(name: "error", data: Uint8List(0));
}
//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
Future<List<String>> fetchMarkdownContent(
List<String> IDsString, String emailFolder) async {
List<String> MDofThread = [];
threadAttachments = [];
int counter = 0;
// 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 {
//attaches email after email from a thread
for (var id in IDsString) {
var url = Uri.http('$ip:$port', 'email_md', {'id': id});
print(url);
var response = await http.get(url);
currThread.add(id);
if (response.statusCode == 200) {
counter += 1;
Map<String, dynamic> json = jsonDecode(response.body);
// 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 {
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,
required this.emailContent,
required this.from,
required this.name,
required this.to,
required this.subject,
required this.date,
required this.id,
required this.messages,
}) : super(key: key);
@override
_EmailViewState createState() => _EmailViewState();
}
class _EmailViewState extends State<EmailView> {
//html css rendering thing
late Key iframeKey;
late String currentContent;
late String viewTypeId; //make this a list too???
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},
];
@override
void initState() {
super.initState();
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);
MDofThread.add(json['md'] ?? '');
try {
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');
}
}
}
} catch (e) {
print('_getMDContent caught error: $e');
}
// print("IDS inside fetch md content $IDsString");
// print("inside apiservice $MDofThread");
return MDofThread;
}
// 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);
Future<void> markAsSeen(int thread_id) async {
try {
var url = Uri.http(
'$ip:$port', 'post_seen_thread', {'id': thread_id.toString()});
var response = await http.get(url);
if (response.statusCode == 200) {
var result = response.body;
print("data $result");
}
} catch (e) {
print("markAsSeen failed $e");
}
}
// TODO: void _invisibility(String ) //to make purple numbers not visible
Future<void> markAsUnseen(int thread_id) async {
try {
var url = Uri.http(
'$ip:$port', 'post_unseen_thread', {'id': thread_id.toString()});
var response = await http.get(url);
if (response.statusCode == 200) {
var result = response.body;
print("data $result");
}
} catch (e) {
print("markAsUnseen failed $e");
}
}
@override
Widget build(BuildContext context) {
// print("thread id ${widget.id}");
ApiService.currThreadID = widget.id;
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: Stack(
children: [
Column(
children: [
EmailToolbar(
onJumpToSpan: _scrollToNumber,
onButtonPressed: () => {},
// AugmentClasses.handleJump(viewTypeId, '1');
// print("button got pressed?");
Future<bool> deleteEmail(String from_folder, int thread_id) async {
// post
try {
List<SerializableMessage> mailsInSerializable =
await this.threadsInSerializable(thread_id.toString());
// _registerViewFactory(r"""
// <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>
// <h3>Here's a simple list:</h3>
// <ul>
// <li>HTML elements are the building blocks of HTML pages</li>
// <li>HTML uses tags like <code>&lt;tag&gt;</code> to organize and format content</li>
// <li>CSS is used with HTML to style pages</li>
// </ul>
// <p>Copyright © 2023</p>
// """);
// print("change");
// widget.emailContent = r"
if (mailsInSerializable.isEmpty) {
return false;
}
Map<String, String> requestBody = {
"from": from_folder,
"uid": mailsInSerializable.first.uid.toString(),
"to": "not used"
};
//
),
Row(
// title of email
children: [
Text(
widget.subject,
style: TextStyle(fontSize: 30),
),
],
),
Row(
children: [
Text(
'from ${widget.name}',
style: TextStyle(fontSize: 18),
),
Text(
'<${widget.from}>',
style: TextStyle(fontSize: 18),
),
Spacer(),
Text(
'${widget.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.to.toString()}',
style: TextStyle(fontSize: 15),
)
],
),
Expanded(
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,
// ),
// ),
],
),
//delete the email that is given to the
var url = Uri.http("$ip:$port", 'delete_email');
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("not 200: ${response.body}");
return false;
}
} catch (e) {
print("error in deleteEmail $e");
return false;
}
}
// 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(),
// );
// }
Future<bool> sendEmail(
String? to, String? subject, String? emailContent) async {
try {
var url = Uri.http('$ip:$port', 'send_email');
// 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),
// ),
// ),
// ),
// ),
// ),
],
));
Map<String, dynamic> requestBody = {
"to": [to ?? ""],
"cc": [],
"bcc": [],
"subject": subject ?? "Untitled",
"in_reply_to": "",
"messages": [
{"message": emailContent ?? "", "is_html": false}
],
"attachments": [],
"inline_images": [],
};
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}');
return false;
}
return true;
} catch (e) {
print("error in post send email $e");
return false;
}
}
}

View File

@ -0,0 +1,7 @@
import 'structs.dart';
class Attachmentdownload {
Future<void> saveFile(AttachmentResponse attachment) async {
print("stub attachment download");
}
}

View File

@ -1,15 +1,3 @@
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';
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)
);
}
}
export 'attachamentDownloadStub.dart'
if (dart.library.io) 'attachmentDownloadAndroid.dart';
// if (dart.library.js_interop) 'attachmentDownloadWeb.dart';

View File

@ -0,0 +1,7 @@
import 'structs.dart';
class Attachmentdownload {
Future<void> saveFile(AttachmentResponse attachment) async {
print("android attachment download");
}
}

View File

@ -0,0 +1,12 @@
// 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)
// );
// }
// }

View File

@ -1,103 +1,3 @@
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),
)
],
),
),
),
]));
}
}
export 'attachmentWidgetStub.dart'
if (dart.library.js_interop) 'attachmentWidgetWeb.dart'
if (dart.library.io) 'attachmentWidgetAndroid.dart';

View File

@ -0,0 +1,16 @@
import "package:crab_ui/structs.dart";
import "package:flutter/material.dart";
class AttachmentWidget extends StatelessWidget {
final AttachmentResponse attachment;
AttachmentWidget({required this.attachment});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text("PDF EVENTUALLY ANDROID")
);
}
}

View File

@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'structs.dart';
class AttachmentWidget extends StatelessWidget{
final AttachmentResponse attachment;
AttachmentWidget({required this.attachment});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text("PDF EVENTUALLY, STUB")
);
}
}

View File

@ -0,0 +1,101 @@
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';
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,24 +1,33 @@
// import 'dart:ffi';
import 'package:crab_ui/api_service.dart';
import 'package:crab_ui/attachmentDownload.dart';
import 'package:crab_ui/collapsableEmails.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 'package:web/web.dart' as web;
import 'package:go_router/go_router.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'attachmentWidget.dart';
import 'package:flutter/services.dart';
import 'routingHandler.dart';
class EmailToolbar extends StatefulWidget {
final Function(String) onJumpToSpan;
final Function(String) onJumpToNumbering;
final Function(String) onViewspecs;
final VoidCallback onButtonPressed;
final Function(String) onFiltering;
final List<String> emails;
final String subject;
late AugmentTree? rootAugment;
EmailToolbar(
{Key? key, required this.onButtonPressed, required this.onJumpToSpan})
: super(key: key);
EmailToolbar({
Key? key,
required this.onButtonPressed,
required this.onJumpToNumbering,
required this.onViewspecs,
required this.onFiltering,
required this.emails,
required this.subject,
required this.rootAugment,
}) : super(key: key);
@override
_DynamicClassesAugment createState() => _DynamicClassesAugment();
@ -26,7 +35,10 @@ class EmailToolbar extends StatefulWidget {
class _DynamicClassesAugment extends State<EmailToolbar> {
String selectedClass = 'Class 1';
// TextEditingController _jumpController = TextEditingController();
TextEditingController _jumpController = TextEditingController();
TextEditingController _viewspecsController = TextEditingController();
AugmentTree? localAugment;
List<SerializableMessage>? emailsInThread;
// late final FocusNode _JumpItemfocusNode;
// late final FocusNode _viewSpecsfocusNode;
@ -47,13 +59,25 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
// _viewSpecsfocusNode.addListener(() {
// setState(() => _viewSpecsHasFocus = _viewSpecsfocusNode.hasFocus);
// });
localAugment = widget.rootAugment;
_serializableData(widget.emails);
}
void _serializableData(List<String> threadID) async {
// emailsInThread = await ApiService().threadsInSerializable();
print("done thread serializable");
if (!mounted) return;
// setState(() {
// _isLoaded = true;
// });
}
@override
void dispose() {
// _JumpItemfocusNode.dispose();
// _viewSpecsfocusNode.dispose();
// _jumpController.dispose();
_jumpController.dispose();
super.dispose();
}
@ -78,20 +102,20 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
child: Text('Attachments'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: AugmentClasses.handleOpen,
child: Text('Open'),
),
// ElevatedButton(
// onPressed: AugmentClasses.handleOpen,
// child: Text('Open'),
// ),
// SizedBox(width: 8),
ElevatedButton(
onPressed: AugmentClasses.handleFind,
child: Text('Find'),
),
// SizedBox(width: 8),
ElevatedButton(
onPressed: AugmentClasses.handleStop,
child: Text('Stop'),
),
// ElevatedButton(
// onPressed: AugmentClasses.handleStop,
// child: Text('Stop'),
// ),
ElevatedButton(
onPressed: () {
AugmentClasses.handleMove(context);
@ -138,10 +162,10 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
// width: 8,
// ),
Container(
width: 50,
width: 100,
height: 30,
child: TextField(
// controller: _jumpController,
controller: _jumpController,
decoration: InputDecoration(
border: OutlineInputBorder(),
// suffixIcon: Icon(Icons.search)
@ -149,7 +173,7 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
onSubmitted: (value) {
print("onSubmitted");
if (value.isNotEmpty) {
widget.onJumpToSpan(value);
widget.onJumpToNumbering(value);
}
},
),
@ -186,18 +210,23 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
onPressed: () => AugmentClasses.ViewSpecsButton(context),
child: Text('ViewSpecs:')),
Container(
width: 50,
width: 100,
height: 30,
child: TextField(
controller: _viewspecsController,
decoration: InputDecoration(
labelText: '',
border: OutlineInputBorder(),
// suffixIcon: Icon(Icons.style_rounded)
),
onSubmitted: (value) {
widget.onViewspecs(value);
},
),
),
ElevatedButton(
onPressed: () => AugmentClasses.FilterButton(context),
onPressed: () =>
AugmentClasses().filterButton(context, widget.onFiltering),
child: Text('Filter'),
),
SizedBox(width: 8),
@ -207,11 +236,14 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
),
// SizedBox(width: 8),
ElevatedButton(
onPressed: AugmentClasses.handleFind,
onPressed: () => AugmentClasses()
.handleCreateLink(context, widget.emails, widget.subject, widget.emails[0]), //need to add the email ids
child: Text('Create Link'),
),
ElevatedButton(
onPressed: AugmentClasses.handleFind,
// onPressed: () => localAugment!.handlePaste(context),
onPressed: () =>
AugmentClasses().handlePaste(context),
child: Text('Paste Link'),
),
],
@ -222,6 +254,18 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
}
class AugmentClasses {
CollapsableEmails? localCollapsable;
String? nameOfDocument;
AugmentTree? rootTree;
void setRootTree(AugmentTree aTree) {
rootTree = aTree;
}
// AugmentClasses(CollapsableEmails localCollapsable) {
// localCollapsable = localCollapsable;
// }
static OverlayEntry? _overlayEntry;
static String? selectedFolder; // Manage selected folder at the class level
@ -487,45 +531,236 @@ class AugmentClasses {
print("Find button pressed");
}
AugmentTree? _findAugmentNode(String target, AugmentTree node, int index) {
// so the ideqa is that since the numbering its quite linear, meaning that it tells you where it goes,
// thus i've thought the amount of max moves are only the length of the string of the target
//e.g. if we have a target of 1e9, its steps are the same or time complexity as if it were 1a1, or 99z99
// since each number or letter tells us which is the index in this array, genius ik
// thus first one needs another function from converting from alphabetical to numbers
if (node.numbering[index] == target[index]) {
_findAugmentNode(target, node.children[index++], index++);
}
}
bool _checkValidTarget(String target) {
target = target.trim();
//find if the target exists,
//recursive?
_findAugmentNode(target, rootTree!, 0);
return false;
}
_copyLink(String anchor, String target, String format, String viewspecs,
String nameOfDocument, emailID) {
String form = "$anchor < $nameOfDocument, $target :$viewspecs > $emailID";
final link = ClipboardData(text: form);
Clipboard.setData(link);
}
Future<void> handleCreateLink(BuildContext context,
List<String> emailsInThread, String nameOfDocument, String emailID) async {
print("create link button pressed");
final TextEditingController targetController = TextEditingController();
final TextEditingController anchorController = TextEditingController();
final TextEditingController viewspecsController = TextEditingController();
final TextEditingController formatController = TextEditingController();
// String anchorPhrase = '';
String format = 'augment';
// String target = '';
// String viewspecs = '';
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Create URL Link'),
content: SizedBox(
height: 400,
child: Column(
children: [
Row(
children: [
Text("Which email? "),
SizedBox(
width: 350.0,
child: Text(nameOfDocument),
// child: ListView.builder(
// itemCount: emailsInThread.length,
// itemBuilder: (context, index) {
// // var item = emailsInThread[index];
// // ApiService().
// return ListTile(
// title: Text(nameOfDocument),
// );
// }),
)
],
),
Row(
children: [
Text("Link to target item at: "),
SizedBox(
width: 350.0,
child: TextField(
controller: targetController,
autofocus: true,
maxLines: 1,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
],
),
SizedBox(height: 8),
Row(
children: [
ElevatedButton(
onPressed: () => ViewSpecsButton(context),
child: Text("Viewspecs:")),
SizedBox(
width: 150.0,
child: TextField(
controller: viewspecsController,
maxLines: 1,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
],
),
SizedBox(height: 8),
Row(
children: [
Text("Using anchor phrase: "),
SizedBox(
width: 150.0,
child: TextField(
controller: anchorController,
maxLines: 1,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
],
),
SizedBox(height: 8),
Row(
children: [
Text("Using link format: "),
SizedBox(
width: 250.0,
child: Text("Augment"),
),
],
)
],
),
),
actions: [
ElevatedButton(
onPressed: () => {
// _checkValidTarget(targetController.text),
_copyLink(
anchorController.text,
targetController.text,
format,
viewspecsController.text,
nameOfDocument,
emailID,
),
Navigator.of(context).pop()
},
child: Text("OK")),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: Text("Cancel")),
ElevatedButton(onPressed: null, child: Text("Help")),
],
));
}
Future<void> handlePaste(BuildContext context) async {
final TextEditingController gotoLink = TextEditingController();
Routinghandler localRouting;
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("GOTO Link"),
content: SizedBox(
height: 400,
child: Column(
children: [
Row(
children: [
Text("Paste link to go: "),
SizedBox(
width: 350,
child: TextField(
controller: gotoLink,
maxLines: 1,
autofocus: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
))
],
)
],
),
),
actions: [
ElevatedButton(
onPressed: () {
print('pressed');
Navigator.of(context).pop();
final localRouting =
Routinghandler(gotoLink.text);
final String subject =
localRouting.docName; // This is your :subject
final String target =
localRouting.target; // This is your :target
final String viewspecs =
localRouting.viewspecs; // This is your :viewspecs
final String finalEmailID = localRouting.emailID;
final encodedSubject = Uri.encodeComponent(subject);
final encodedTarget = Uri.encodeComponent(target);
final encodedViewspecs = Uri.encodeComponent(viewspecs);
final encodedEmailID = Uri.encodeComponent(finalEmailID);
print("emailID $encodedEmailID");
String link =
"/email/$encodedSubject/$encodedTarget/$encodedViewspecs/$encodedEmailID";
GoRouter.of(context).go(link);
},
child: Text("OK")),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("Cancel"))
],
));
}
static void handleStop() {
print("Stop button pressed");
}
static void handleJump(String spanId) {
String js_code = '''
var iframe = document.getElementsByTagName('iframe')[0]; // 0 for the first iframe, 1 for the second, etc.
// Check if the iframe is loaded and has content
if (iframe && iframe.contentDocument) {
// Access the document inside the iframe
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Find the element with the specific id inside the iframe
var targetElement = iframeDoc.getElementById("$spanId"); // Replace '36 ' with the actual id of the target element
// If the element exists, scroll to it
if (targetElement) {
targetElement.scrollIntoView();
console.log('Scrolled to element with id "$spanId" inside the iframe.');
} else {
console.log('Element with id "$spanId" not found inside the iframe.');
}
} else {
console.log('Iframe not found or not loaded.');
}
''';
// js.context.callMethod('eval', [js_code]);
static void handleJump(String value) {
print(value);
}
static void invisibility(String htmlClass) {}
static Future<void> JumpButton(BuildContext context) async {
// FocusNode textFieldFocusNode = FocusNode();
// AugmentClasses.disableIframePointerEvents();
await showDialog(
barrierDismissible: true,
// barrierColor: Colors.yellow,
context: context,
builder: (context) => AlertDialog(
title: Text('Jump Item:'),
@ -725,7 +960,6 @@ class AugmentClasses {
ElevatedButton(onPressed: () {}, child: Text('OK')),
ElevatedButton(
onPressed: () {
// AugmentClasses.disableIframePointerEvents();
Navigator.of(context).pop();
},
child: Text('Cancel')),
@ -743,61 +977,90 @@ class AugmentClasses {
});
}
void handleFilter() {}
static Future<void> FilterButton(context) async {
Future<List<AugmentTree>> searchFilter(String query) async {
return [];
}
Future<void> filterButton(
context, Function(String) onFilteringCallback) async {
//this is literally ctrl+F :skull:
//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
bool? numbering = false;
String filterQueue = '';
// AugmentClasses.disableIframePointerEvents();
await showDialog(
context: context,
builder: (context) => Container(
height: 150,
width: 300,
child: AlertDialog(
title: Text('Filter'),
content: Container(
width: 400, // Set the width to simulate the Windows style
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Set filter:'),
SizedBox(
width: 175,
child: TextField(
maxLines: 1,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
context: context,
builder: (BuildContext dialogContext) {
return StatefulBuilder(builder:
(BuildContext statefulBuilderContext, StateSetter setState) {
return AlertDialog(
title: const Text('Filter'),
content: SizedBox(
width: 400, // Set the width to simulate the Windows style
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Set filter:'),
SizedBox(
width: 175,
child: TextField(
autofocus: true,
maxLines: 1,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
)
],
)))));
onChanged: (value) {
print(value);
filterQueue = value;
},
),
),
SizedBox(
height: 10,
),
Column(children: [
Row(children: [
Checkbox(
value: numbering,
activeColor:
Theme.of(context).colorScheme.tertiary,
onChanged: (newBool) {
setState(() {
numbering = newBool;
});
}),
Text("Start at top of file")
]),
]),
])),
actions: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("Cancel")),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop({
'filterQueue': filterQueue,
'numbering': numbering,
});
},
child: Text("Apply")),
],
);
});
},
).then((result) {
if (result != null) {
print("filter done $result");
final String query = result['filterQueue'];
onFilteringCallback(query);
} else {
print('cancelled');
}
});
}
// 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');
// final iframes = html.document.getElementsByTagName('iframe');
// for (var iframe in iframes) {
// if (iframe is html.Element) {
// iframe.style.pointerEvents = 'auto'; // Re-enable pointer events
// }
// }
// }
}

View File

@ -1,132 +1,3 @@
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());
}
}
export 'collapsableEmailsStub.dart'
if (dart.library.io) 'collapsableEmailsAndroid.dart'
if (dart.library.js_interop) 'collapsableEmailsWeb.dart';

View File

@ -0,0 +1,442 @@
import 'package:flutter/material.dart';
import 'api_service.dart';
import 'structs.dart';
import 'package:html2md/html2md.dart' as html2md;
import 'package:markdown_widget/markdown_widget.dart';
import 'package:markdown/markdown.dart' as md;
class CollapsableEmails extends StatefulWidget {
final List<String> thread; // email id's in the form xyz@gmail.com
// final List<String> threadHTML;
final List<String> threadMarkdown;
final String threadIDs;
final String? targetJumpNumbering;
final String? targetViewspecs;
final String? targetFiltering;
CollapsableEmails({
required this.thread,
required this.threadMarkdown,
required this.threadIDs,
this.targetJumpNumbering,
this.targetViewspecs,
this.targetFiltering,
});
@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<SerializableMessage> emailsInThread = [];
bool _isLoaded = false;
List<String> hirarchy = ["h1", "h2", "h3", "h4", "h5", "h6", "p"];
Map<String, int> hirarchyDict = {
"h1": 1,
"h2": 2,
"h3": 3,
"h4": 4,
"h5": 6,
"h6": 7,
"p": 8,
"ul": 8,
"li": 8,
};
List<String> tagsCollected = [];
List<String> allMarkdown = [];
List<List<String>> sentinel = [];
int level = 0;
AugmentTree zoomTreeRoot = AugmentTree();
// late AugmentTree currentZoomNode;
late List<AugmentTree> currentZoomTree = [];
bool zoomOut = false;
bool zoomIn = true;
late List<AugmentTree> threadNodes = [];
static bool leftNumbering = false;
static bool rightNumbering = true;
bool showWhole = false;
@override
void initState() {
super.initState();
threadNodes = [];
currentZoomTree = [];
// _markdownConverter();
_serializableData(widget.threadIDs); // this
_markdown2Tree(widget.threadMarkdown);
}
@override
void didUpdateWidget(covariant CollapsableEmails oldWidget) {
// TODO: implement didUpdateWidget
super.didUpdateWidget(oldWidget);
if (widget.targetJumpNumbering != null &&
widget.targetJumpNumbering != oldWidget.targetJumpNumbering) {
_handleJump(widget.targetJumpNumbering!);
}
if (widget.targetViewspecs != null &&
widget.targetViewspecs != oldWidget.targetViewspecs) {
_handleViewspecs(widget.targetViewspecs!);
}
}
@override
void dispose() {
super.dispose();
}
List<String> getThreads() {
return widget.thread;
}
void _add2Tree(AugmentTree tree, md.Element node2add) {
// adds node to its corresponding place
AugmentTree newNode = AugmentTree();
newNode.setData(node2add.textContent);
newNode.ogTag = node2add.tag;
// cases,
//1. a node that comes is lower than the root.children last, if so it goes beneath it
if (tree.children.isEmpty) {
// new level to be created when totally empty
tree.children.add(newNode);
newNode.parent = tree;
} else if (tree.children.isNotEmpty &&
tree.children.last.ogTag.isNotEmpty) {
if ((hirarchyDict[node2add.tag] ??
-1) < // e.g. new node is h1 and old is h2, heapify
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
//have to figure out the borthers
//assuming it all goes right
if ((hirarchyDict[node2add.tag] ?? -1) == -1 ||
(hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) {
print(
'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} < ${hirarchyDict[tree.children.last.ogTag] ?? -1}');
return;
} else if (tree.children.last.parent == null) {
// becomes the new top level
for (AugmentTree brother in tree.children) {
brother.parent = newNode;
}
tree.children = [newNode];
} else {
newNode.parent = tree;
tree.children.add(newNode);
}
} else if ((hirarchyDict[node2add.tag] ??
-1) > // go down e.g. new node is h3 and old is h2 or something
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
if ((hirarchyDict[node2add.tag] ?? -1) == -1 ||
(hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) {
print(
'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} > ${hirarchyDict[tree.children.last.ogTag] ?? -1}');
print("-1 ${tree.children.last.ogTag}");
return;
}
_add2Tree(tree.children.last, node2add);
} else if ((hirarchyDict[node2add.tag] ?? -1) ==
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
tree.children.add(newNode);
newNode.parent = tree;
}
}
}
void _markdown2Tree(List<String> text) {
print("started markdown2tree");
for (int emailsMD = 0; emailsMD < text.length; emailsMD++) {
final List<md.Node> nakedList =
md.Document().parseLines(text[emailsMD].split('\n'));
zoomTreeRoot = AugmentTree();
for (var node in nakedList) {
//maybe do an add function, but isn't this it?
if (node is md.Element) {
AugmentTree temp = AugmentTree();
temp.data = node.textContent;
temp.ogTag = node.tag;
if (node.tag == 'h1') {
// make this O(1)
_add2Tree(zoomTreeRoot, node);
} else if (node.tag == 'h2') {
// i dont add any since i dont have it, maybe the function makes sense
_add2Tree(zoomTreeRoot, node); // fix this
} else if (node.tag == 'h3') {
_add2Tree(zoomTreeRoot, node);
} else if (node.tag == 'h4') {
_add2Tree(zoomTreeRoot, node); // change to temp
} else if (node.tag == 'h5') {
_add2Tree(zoomTreeRoot, node);
} else if (node.tag == 'h6') {
_add2Tree(zoomTreeRoot, node); // fix this
} else if (node.tag == 'p' || node.tag == 'ul' || node.tag == 'li') {
_add2Tree(zoomTreeRoot, node); // fix this
}
}
}
zoomTreeRoot.addNumbering();
threadNodes.add(zoomTreeRoot);
currentZoomTree.add(zoomTreeRoot);
}
if (!mounted) return;
setState(() {
_isLoaded = true;
});
}
void _goToChildren(int indexThread, int index) async {
final target = currentZoomTree[indexThread].children[index];
if (target.children.isNotEmpty) {
setState(() {
currentZoomTree[indexThread] = target;
});
} else {
print("This child has no further children.");
}
}
void _goToParent(int indexThread) async {
if (currentZoomTree[indexThread].parent != null) {
setState(() {
currentZoomTree[indexThread] = currentZoomTree[indexThread].parent!;
});
} else {
print("Already at root.");
}
}
void _serializableData(String threadID) async {
emailsInThread = await ApiService().threadsInSerializable(threadID);
print("done thread serializable");
if (!mounted) return;
setState(() {
_isLoaded = true;
});
}
Widget _buildForZooms(int indexThread) {
// IF I GIVE IT THE INDEX????
if (!_isLoaded) {
return const Center(child: CircularProgressIndicator()); // loading screen
}
final AugmentTree currentZoomNodeForThisEmail =
currentZoomTree[indexThread];
final canZoomOut = currentZoomNodeForThisEmail.parent != null;
return ListView.builder(
itemCount: currentZoomNodeForThisEmail.children.length,
itemBuilder: (context, index) {
final childNode = currentZoomNodeForThisEmail.children[index];
final canZoomIn = childNode.children.isNotEmpty;
// currentZoomNodeForThisEmail.addNumbering();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
child: Material(
elevation: 1,
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceBright,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 4.0,
children: [
OutlinedButton(
onPressed:
canZoomOut ? () => _goToParent(indexThread) : null,
child: Icon(Icons.north_west_sharp),
),
OutlinedButton(
onPressed: canZoomIn
? () => _goToChildren(indexThread, index)
: null,
child: Icon(Icons.south_east_sharp),
),
],
),
SizedBox(width: 12.0),
if (leftNumbering)
Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 5, 0),
child: Text(
childNode.numbering,
style:
TextStyle(color: Color(Colors.purple[400]!.value)),
),
),
Expanded(
child: MarkdownBlock(
data: childNode.data,
// data: currentZoomNode
// .children[index].data, // one string of markdown
config: MarkdownConfig
.darkConfig, // or lightConfig depending on theme
),
),
if (rightNumbering)
Padding(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 0),
child: Text(
childNode.numbering,
style:
TextStyle(color: Color(Colors.purple[400]!.value)),
),
),
],
),
),
),
);
},
);
}
void _handleJump(String queryNumbering) {
print(queryNumbering);
if (queryNumbering.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Please enter a numbering to jump to.')),
);
return;
}
final int targetEmailIndex = _expandedEmails.first;
if (targetEmailIndex >= threadNodes.length) {
// Error handling
return;
}
final AugmentTree rootOfCurrentEmail = threadNodes[targetEmailIndex];
final AugmentTree? foundNode =
_findNodeByNumbering(rootOfCurrentEmail, queryNumbering);
if (foundNode != null) {
setState(() {
currentZoomTree[targetEmailIndex] = foundNode; // Update the state
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Numbering "$queryNumbering" not found.')),
);
}
}
void _handleViewspecs(String viewspecsQuery) {
print(viewspecsQuery);
if (viewspecsQuery.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Please enter the viewspecs.')),
);
return;
}
final int targetEmailIndex = _expandedEmails.first;
if (targetEmailIndex >= threadNodes.length) {
// Error handling
return;
}
if (viewspecsQuery.contains('n')) {
setState(() {
leftNumbering = false; // Update the state
rightNumbering = false;
});
}
if (viewspecsQuery.contains('m')) {
setState(() {
rightNumbering = true;
leftNumbering = true;
});
}
if (viewspecsQuery.contains('H')) {
setState(() {
leftNumbering = !leftNumbering;
});
}
if (viewspecsQuery.contains('G')) {
setState(() {
rightNumbering = !rightNumbering;
});
}
if (viewspecsQuery.contains('w')) {
setState(() {
showWhole = true;
});
}
// else {
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(content: Text('Numbering "$viewspecsQuery" not found.')),
// );
// }
}
AugmentTree? _findNodeByNumbering(AugmentTree root, String numbering) {
//recursively finds the node you mentioned
// to find the AugmentTree node corresponding to the `numbering`.
if (root.numbering == numbering) {
return root;
}
for (var child in root.children) {
final found = _findNodeByNumbering(child, numbering);
if (found != null) {
return found;
}
}
return null;
}
@override
Widget build(BuildContext context) {
return _isLoaded
? Column(children: [
Expanded(
child: ListView.builder(
itemCount: emailsInThread.length,
itemBuilder: (context, index) {
final isExpanded = _expandedEmails
.contains(index); //check if email is expanded
return Column(
children: [
ListTile(
title: Text(emailsInThread[index].from),
trailing: Text(emailsInThread[index].date),
onTap: () {
setState(() {
if (isExpanded) {
_expandedEmails.remove(index);
} else {
_expandedEmails.add(index);
}
});
},
),
if (isExpanded)
ConstrainedBox(
constraints: BoxConstraints(
minHeight: 100,
maxHeight: MediaQuery.of(context).size.height * 0.6,
),
child: _buildForZooms(index),
),
Divider(),
],
);
},
),
),
])
: const Center(child: CircularProgressIndicator());
}
}

View File

@ -0,0 +1,32 @@
import 'structs.dart';
import 'package:flutter/material.dart';
class CollapsableEmails extends StatefulWidget {
final List<String> thread; // email id's in the form xyz@gmail.com
final List<String> threadMarkdown;
final String threadIDs;
CollapsableEmails(
{required this.thread,
required this.threadMarkdown,
required this.threadIDs, String? targetJumpNumbering, String? targetViewspecs, String? targetFiltering, required String nameOfDocument});
get getThreads => null;
get getAugmentRoot => null;
@override
State<CollapsableEmails> createState() => _CollapsableEmailsState();
}
class _CollapsableEmailsState extends State<CollapsableEmails> {
List<String> getThreads() {
return widget.thread;
}
@override
Widget build(BuildContext context) {
return Scaffold(body: Text("collapsable stud"));
}
}

View File

@ -0,0 +1,582 @@
import 'package:flutter/material.dart';
import 'api_service.dart';
import 'structs.dart';
import 'package:markdown_widget/markdown_widget.dart';
import 'package:markdown/markdown.dart' as md;
class CollapsableEmails extends StatefulWidget {
final List<String> thread; // email id's in the form xyz@gmail.com
// final List<String> threadHTML; to be replaced with the MD
final List<String> threadMarkdown;
final String threadIDs;
final String? targetJumpNumbering;
final String? targetViewspecs;
final String? targetFiltering;
final String? nameOfDocument;
const CollapsableEmails({
required this.thread,
// required this.threadHTML,
required this.threadMarkdown,
required this.threadIDs,
this.targetJumpNumbering,
this.targetViewspecs,
this.targetFiltering,
this.nameOfDocument,
});
@override
State<CollapsableEmails> createState() => _CollapsableEmailsState();
AugmentTree? getAugmentRoot() {
return _CollapsableEmailsState().getAugmentRoot();
}
}
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<SerializableMessage> emailsInThread = [];
bool _isLoaded = false;
List<String> hirarchy = ["h1", "h2", "h3", "h4", "h5", "h6", "p"];
Map<String, int> hirarchyDict = {
"h1": 1,
"h2": 2,
"h3": 3,
"h4": 4,
"h5": 5,
"h6": 6,
"p": 8,
"ul": 8,
"li": 8,
};
List<String> tagsCollected = [];
List<String> allMarkdown = [];
List<List<String>> sentinel = [];
int level = 0;
AugmentTree zoomTreeRoot = AugmentTree();
// late AugmentTree currentZoomNode;
late List<AugmentTree> currentZoomTree =
[]; // holds a list of list that holds the list of nodes on the currentzoom
bool zoomOut = false;
bool zoomIn = true;
late List<AugmentTree> threadNodes = [];
static bool leftNumbering = true;
static bool rightNumbering = true;
bool showWhole = false;
List<AugmentTree> queryResults = []; //results of conducting filtering
bool _isFilteringActive = false;
@override
void initState() {
super.initState();
threadNodes = [];
currentZoomTree = [];
// _markdownConverter();
_serializableData(widget.threadIDs); // this
_markdown2Tree(widget.threadMarkdown);
}
@override
void didUpdateWidget(covariant CollapsableEmails oldWidget) {
// TODO: implement didUpdateWidget
super.didUpdateWidget(oldWidget);
if (widget.targetJumpNumbering != null &&
widget.targetJumpNumbering != oldWidget.targetJumpNumbering) {
_handleJump(widget.targetJumpNumbering!);
}
if (widget.targetViewspecs != null &&
widget.targetViewspecs != oldWidget.targetViewspecs) {
_handleViewspecs(widget.targetViewspecs!);
}
if (widget.targetFiltering != null &&
widget.targetFiltering != oldWidget.targetFiltering) {
_handleFilterQuery(zoomTreeRoot, widget.targetFiltering!);
}
}
@override
void dispose() {
super.dispose();
}
List<SerializableMessage> getThreads() {
return emailsInThread;
}
AugmentTree getAugmentRoot() {
return zoomTreeRoot;
}
void _add2Tree(AugmentTree tree, md.Element node2add) {
// adds node to its corresponding place
AugmentTree newNode = AugmentTree();
newNode.setData(node2add.textContent);
newNode.ogTag = node2add.tag;
// cases,
//1. a node that comes is lower than the root.children last, if so it goes beneath it
if (tree.children.isEmpty) {
// new level to be created when totally empty
tree.children.add(newNode);
newNode.parent = tree;
} else if (tree.children.isNotEmpty &&
tree.children.last.ogTag.isNotEmpty) {
if ((hirarchyDict[node2add.tag] ??
-1) < // e.g. new node is h1 and old is h2, heapify
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
//have to figure out the borthers
//assuming it all goes right
if ((hirarchyDict[node2add.tag] ?? -1) == -1 ||
(hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) {
print(
'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} < ${hirarchyDict[tree.children.last.ogTag] ?? -1}');
return;
} else if (tree.children.last.parent == null) {
// becomes the new top level
for (AugmentTree brother in tree.children) {
brother.parent = newNode;
}
tree.children = [newNode];
} else {
newNode.parent = tree;
tree.children.add(newNode);
}
} else if ((hirarchyDict[node2add.tag] ??
-1) > // go down e.g. new node is h3 and old is h2 or something
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
if ((hirarchyDict[node2add.tag] ?? -1) == -1 ||
(hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) {
print(
'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} > ${hirarchyDict[tree.children.last.ogTag] ?? -1}');
print("-1 ${tree.children.last.ogTag}");
return;
}
_add2Tree(tree.children.last, node2add);
} else if ((hirarchyDict[node2add.tag] ?? -1) ==
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
tree.children.add(newNode);
newNode.parent = tree;
}
}
}
void _markdown2Tree(List<String> text) {
print("started markdown2tree");
for (int emailsMD = 0; emailsMD < text.length; emailsMD++) {
final List<md.Node> nakedList =
md.Document().parseLines(text[emailsMD].split('\n'));
zoomTreeRoot = AugmentTree();
for (var node in nakedList) {
//maybe do an add function, but isn't this it?
if (node is md.Element) {
AugmentTree temp = AugmentTree();
temp.data = node.textContent;
temp.ogTag = node.tag;
//why did i do this???
if ( hirarchyDict.containsKey(node.tag)) {
_add2Tree(zoomTreeRoot, node);
}
}
}
zoomTreeRoot.addNumbering();
threadNodes.add(zoomTreeRoot);
currentZoomTree.add(zoomTreeRoot);
}
if (!mounted) return;
setState(() {
_isLoaded = true;
});
}
void _goToChildren(int indexThread, int index) async {
final target = currentZoomTree[indexThread].children[index];
if (target.children.isNotEmpty) {
setState(() {
currentZoomTree[indexThread] = target;
});
} else {
print("This child has no further children.");
}
}
void _goToChildrenFiltering(
int indexThread, int index, AugmentTree node) async {
final target = node;
if (target.children.isNotEmpty) {
setState(() {
currentZoomTree[indexThread] = target;
_isFilteringActive = false;
});
for (var child in target.children) {
print(child.data);
}
} else {
print("This child has no further children.");
}
}
void _goToParent(int indexThread) async {
if (currentZoomTree[indexThread].parent != null) {
setState(() {
currentZoomTree[indexThread] = currentZoomTree[indexThread].parent!;
});
} else {
print("Already at root.");
}
}
void _goToParentFiltering(int indexThread, AugmentTree node) async {
if (node.parent != null) {
setState(() {
currentZoomTree[indexThread] = node.parent!;
});
} else {
print("Already at root.");
}
}
void _serializableData(String threadID) async {
emailsInThread = await ApiService().threadsInSerializable(threadID);
print("done thread serializable");
if (!mounted) return;
setState(() {
_isLoaded = true;
});
}
Widget _buildForZooms(int indexThread) {
// index of email in thread, currentZoomTree,
//
if (!_isLoaded) {
return const Center(child: CircularProgressIndicator()); // loading screen
}
AugmentTree
currentZoomNodeForThisEmail = //each index is an email in the thread
currentZoomTree[indexThread];
print(currentZoomNodeForThisEmail.data);
print(currentZoomNodeForThisEmail.children);
print(currentZoomNodeForThisEmail.parent);
// if (_isFilteringActive) {
// nodesToDisplay = queryResults;
// } else {
// nodesToDisplay = currentZoomNodeForThisEmail.children;
// }
final canZoomOut = currentZoomNodeForThisEmail.parent != null;
if (_isFilteringActive) {
return ListView.builder(
itemCount: queryResults.length,
itemBuilder: (context, index) {
AugmentTree childNode = queryResults[index];
bool canZoomIn = childNode.children.isNotEmpty;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Material(
elevation: 1,
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceBright,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 4.0,
children: [
OutlinedButton(
onPressed: () => {
setState(() {
_goToParentFiltering(indexThread, childNode);
_isFilteringActive = false;
})
},
child: Icon(Icons.north_west_sharp),
),
OutlinedButton(
onPressed: canZoomIn
? () => _goToChildrenFiltering(
indexThread, index, childNode)
: null,
child: Icon(Icons.south_east_sharp),
),
],
),
SizedBox(width: 12.0),
if (leftNumbering)
Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 5, 0),
child: Text(
childNode.numbering,
style: TextStyle(
color: Color(Colors.purple[400]!.value)),
),
),
Expanded(
child: MarkdownBlock(
data: childNode.data,
config: MarkdownConfig
.darkConfig, // or lightConfig depending on theme
),
),
if (rightNumbering)
Padding(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 0),
child: Text(
childNode.numbering,
style: TextStyle(
color: Color(Colors.purple[400]!.value)),
),
),
],
),
),
),
);
});
}
return ListView.builder(
itemCount: currentZoomNodeForThisEmail.children.length,
itemBuilder: (context, index) {
final childNode = currentZoomNodeForThisEmail.children[index];
final canZoomIn = childNode.children.isNotEmpty;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
child: Material(
elevation: 1,
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceBright,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 4.0,
children: [
OutlinedButton(
onPressed:
canZoomOut ? () => _goToParent(indexThread) : null,
child: Icon(Icons.north_west_sharp),
),
OutlinedButton(
onPressed: canZoomIn
? () => _goToChildren(indexThread, index)
: null,
child: Icon(Icons.south_east_sharp),
),
],
),
SizedBox(width: 12.0),
if (leftNumbering)
Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 5, 0),
child: Text(
childNode.numbering,
style:
TextStyle(color: Color(Colors.purple[400]!.value)),
),
),
Expanded(
child: MarkdownBlock(
data: childNode.data,
config: MarkdownConfig
.darkConfig, // or lightConfig depending on theme
),
),
if (rightNumbering)
Padding(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 0),
child: Text(
childNode.numbering,
style:
TextStyle(color: Color(Colors.purple[400]!.value)),
),
),
],
),
),
),
);
},
);
}
void _handleJump(String queryNumbering) {
print(queryNumbering);
if (queryNumbering.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Please enter a numbering to jump to.')),
);
return;
}
final int targetEmailIndex = _expandedEmails.first;
if (targetEmailIndex >= threadNodes.length) {
// Error handling
return;
}
final AugmentTree rootOfCurrentEmail = threadNodes[targetEmailIndex];
final AugmentTree? foundNode =
_findNodeByNumbering(rootOfCurrentEmail, queryNumbering);
if (foundNode != null) {
setState(() {
currentZoomTree[targetEmailIndex] = foundNode; // Update the state
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Numbering "$queryNumbering" not found.')),
);
}
}
void _handleViewspecs(String viewspecsQuery) {
print(viewspecsQuery);
if (viewspecsQuery.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Please enter the viewspecs.')),
);
return;
}
final int targetEmailIndex = _expandedEmails.first;
if (targetEmailIndex >= threadNodes.length) {
// Error handling
return;
}
if (viewspecsQuery.contains('n')) {
setState(() {
leftNumbering = false; // Update the state
rightNumbering = false;
});
}
if (viewspecsQuery.contains('m')) {
setState(() {
rightNumbering = true;
leftNumbering = true;
});
}
if (viewspecsQuery.contains('H')) {
setState(() {
leftNumbering = !leftNumbering;
});
}
if (viewspecsQuery.contains('G')) {
setState(() {
rightNumbering = !rightNumbering;
});
}
if (viewspecsQuery.contains('w')) {
setState(() {
showWhole = true;
});
}
// else {
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(content: Text('Numbering "$viewspecsQuery" not found.')),
// );
// }
}
void _findNodesContainingStrDFS(
AugmentTree node, String query, List<AugmentTree> results) {
if (node.data.contains(query)) {
results.add(node);
}
for (var child in node.children) {
_findNodesContainingStrDFS(child, query, results);
}
}
List<AugmentTree> _handleFilterQuery(AugmentTree root, String query) {
List<AugmentTree> results = [];
final int targetEmailIndex = _expandedEmails.first;
_findNodesContainingStrDFS(root, query, results);
print(results);
for (var res in results) {
print(res.data);
}
if (results.isNotEmpty) {
setState(() {
queryResults = results;
// currentZoomTree[targetEmailIndex] = results.first; // Update the state
_isFilteringActive = true;
currentZoomTree[targetEmailIndex] = root;
});
print(currentZoomTree);
}
return results;
}
AugmentTree? _findNodeByNumbering(AugmentTree root, String numbering) {
//recursively finds the node you mentioned
// to find the AugmentTree node corresponding to the `numbering`.
if (root.numbering == numbering) {
return root;
}
for (var child in root.children) {
final found = _findNodeByNumbering(child, numbering);
if (found != null) {
return found;
}
}
return null;
}
@override
Widget build(BuildContext context) {
return _isLoaded
? Column(children: [
Expanded(
child: ListView.builder(
itemCount: emailsInThread.length,
itemBuilder: (context, index) {
final isExpanded = _expandedEmails
.contains(index); //check if email is expanded
return Column(
children: [
ListTile(
title: Text(emailsInThread[index].from),
trailing: Text(emailsInThread[index].date),
onTap: () {
setState(() {
if (isExpanded) {
_expandedEmails.remove(index);
} else {
_expandedEmails.add(index);
}
});
},
),
if (isExpanded)
ConstrainedBox(
constraints: BoxConstraints(
minHeight: 100,
maxHeight: MediaQuery.of(context).size.height * 0.6,
),
child: _buildForZooms(index), //show the tree
),
Divider(),
],
);
},
),
),
])
: const Center(child: CircularProgressIndicator());
}
}

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_html/flutter_html.dart';
// import 'package:http/http.dart' as http;
// import 'package:flutter_html/flutter_html.dart';
class ContactsPage extends StatefulWidget {
const ContactsPage({super.key});

View File

@ -1,57 +1,225 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:markdown/markdown.dart' as md;
import 'api_service.dart';
import 'structs.dart';
import 'emailView.dart';
import 'Compose.dart';
class EmailListScreen extends StatelessWidget {
class EmailListScreen extends StatefulWidget {
final List<GetThreadResponse> emails;
final Future<List<String>> Function(List<String>, String) getEmailContent;
final String folder;
final GlobalKey<_EmailListScreenState> key;
final Function(List<GetThreadResponse>)? onSelectionChanged;
EmailListScreen({
required this.key,
required this.emails,
required this.getEmailContent,
required this.folder,
this.onSelectionChanged,
}) : super(key: key);
@override
_EmailListScreenState createState() => _EmailListScreenState();
}
class _EmailListScreenState extends State<EmailListScreen>
with TickerProviderStateMixin {
late List<bool> selectStates; // for checkboxes if its selected or not
late List<GetThreadResponse> selectedEmails =
[]; // holds the emails that are selected i.e. the emails that got the checkbox on
final Set<int> _hoveredRows = {}; //the row that is being hovered over atm
bool bulkSelectMenu = false;
final GlobalKey<EmailPageState> _emailPageKey = GlobalKey<EmailPageState>();
@override
void initState() {
super.initState();
selectStates = List<bool>.filled(widget.emails.length, false);
}
@override
void didUpdateWidget(covariant EmailListScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.emails.length != widget.emails.length) {
selectStates = List<bool>.filled(widget.emails.length, false);
}
}
bool selectAllChecks(bool selectionType) {
//perhaps it should return a list of the selected
setState(() {
selectedEmails = [];
if (selectionType) {
bulkSelectMenu = true;
for (int email = 0; email < selectStates.length; email++) {
selectStates[email] = selectionType;
selectedEmails.add(widget.emails[email]);
}
} else {
for (int email = 0; email < selectStates.length; email++) {
selectStates[email] = selectionType;
}
selectedEmails = [];
}
});
widget.onSelectionChanged?.call(selectedEmails);
printTheSelected();
return false;
}
bool markAsRead(bool read) {
print("markasread $read");
setState(() {
if (read) {
//read
for (int email = 0; email < selectedEmails.length; email++) {
selectedEmails[email].seen = read;
ApiService()
.markAsSeen(selectedEmails[email].id); //the remote or .json
}
} else {
//unread
for (int email = 0; email < selectedEmails.length; email++) {
selectedEmails[email].seen = read;
ApiService()
.markAsUnseen(selectedEmails[email].id); //the remote or .json
print(selectedEmails[email].subject);
}
}
});
return false;
}
bool moveOfSelected(String destinyFolder) {
//this should be called from a widget
print("move of folder");
setState(() {
for (int email = 0; email < selectedEmails.length; email++) {
ApiService().moveEmail(
widget.folder, selectedEmails[email].id.toString(), destinyFolder);
}
});
return false;
}
// Widget moveOfFolderWidget()
List<GetThreadResponse> listOfSelectedThreads() {
return selectedEmails;
}
void printTheSelected() {
for (int i = 0; i < selectedEmails.length; i++) {
print(selectedEmails[i].subject);
}
}
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,
itemCount: widget.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;
Color seenColour;
final email = widget.emails[index];
if (email.seen) {
seenColour = ThemeData().highlightColor;
} else {
seenColour = Colors.transparent;
}
return MouseRegion(
onEnter: (_) => setState(() => _hoveredRows.add(index)),
onExit: (_) => setState(() => _hoveredRows.remove(index)),
child: ListTile(
leading: Checkbox(
value: selectStates[index],
onChanged: (bool? value) {
setState(() {
//works great
selectStates[index] = value ?? false;
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,
),
),
);
},
setState(() {
if (value!) {
selectedEmails.add(widget.emails[index]);
//here i must update the other side
_emailPageKey.currentState?.getListOfSelected();
} else {
selectedEmails.remove(widget.emails[index]);
_emailPageKey.currentState?.getListOfSelected();
}
widget.onSelectionChanged?.call(selectedEmails);
print(selectedEmails);
});
});
},
),
title: Text(email.from_name,
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(email.subject)],
),
trailing: _hoveredRows.contains(index)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.mark_email_read_outlined),
onPressed: () {
// mark email as read
setState(() {
widget.emails[index].seen = true;
ApiService().markAsSeen(email.id);
});
},
),
IconButton(
icon: Icon(Icons.delete_outline),
onPressed: () {
// delete email
ApiService().deleteEmail(widget.folder, email.id);
},
),
],
)
: Text(email.date.toString()),
hoverColor: Colors.transparent,
tileColor: seenColour,
onTap: () async {
List<String> emailContent = // list of the html
await widget.getEmailContent(email.messages, widget.folder);
// print("thread id? $email.id"); yes
print(email.messages); //email ids of the thread
if (widget.folder == "Drafts") {
print("IN DRAFTS MOVE THE CONTENT TO THE WRITING THING");
//open the compose
OverlayService _thisInstance = OverlayService();
_thisInstance.draftID = email;
_thisInstance.showPersistentWidget(context);
} else {
// print(email)
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,
),
),
);
ApiService().markAsSeen(email.id);
}
},
),
);
},
separatorBuilder: (context, index) => Divider(),
@ -62,10 +230,12 @@ class EmailListScreen extends StatelessWidget {
// ignore: must_be_immutable
class EmailPage extends StatefulWidget {
EmailPage({Key? key}) : super(key: key);
String selectedFolder = "INBOX"; //starter
int offset = 0;
int page = 1;
final Function(List<GetThreadResponse>)? onSelectionChanged;
EmailPage({Key? key, this.onSelectionChanged}) : super(key: key);
@override
EmailPageState createState() => EmailPageState();
@ -78,6 +248,9 @@ class EmailPageState extends State<EmailPage> {
int page = 1;
bool isBackDisabled = false;
final GlobalKey<_EmailListScreenState> emailListKey =
GlobalKey<_EmailListScreenState>();
@override
void initState() {
super.initState();
@ -86,6 +259,7 @@ class EmailPageState extends State<EmailPage> {
_fetchEmails();
}
List<GetThreadResponse> get getEmails => emails;
String getPage() => widget.page.toString();
bool get backDisabled => isBackDisabled;
@ -116,7 +290,6 @@ class EmailPageState extends State<EmailPage> {
}
});
}
// print(currentPage);
print(widget.page);
_fetchEmails();
}
@ -124,7 +297,7 @@ class EmailPageState extends State<EmailPage> {
void _fetchEmails() async {
try {
List<GetThreadResponse> fetchedEmails = await apiService
.fetchEmailsFromFolder(widget.selectedFolder, widget.offset);
.fetchEmailsFromFolderReversed(widget.selectedFolder, widget.offset);
if (!mounted) return;
setState(() {
@ -135,14 +308,36 @@ class EmailPageState extends State<EmailPage> {
}
}
bool selectAllEmails(bool selectionType) {
emailListKey.currentState?.selectAllChecks(selectionType);
return selectionType;
}
bool markSelectedAsRead(bool selectionType) {
emailListKey.currentState?.markAsRead(selectionType);
return selectionType;
}
bool moveSelectedOfFolder(String folder) {
emailListKey.currentState?.moveOfSelected(folder);
return false;
}
List<GetThreadResponse> getListOfSelected() {
return emailListKey.currentState!.listOfSelectedThreads();
}
// return [GetThreadResponse(id: 1, messages: [], subject: "subject", date: DateTime(2025), from_name: "from_name", from_address: "from_address", to: [], seen: false)];
@override
Widget build(BuildContext context) {
return Scaffold(
body: EmailListScreen(
emails: emails,
getEmailContent: apiService.fetchEmailContent,
folder: widget.selectedFolder, //try to grab from it directly
),
);
body: EmailListScreen(
key: emailListKey,
emails: emails,
// getEmailContent: apiService.fetchEmailContent,
getEmailContent: apiService.fetchMarkdownContent,
folder: widget.selectedFolder, //try to grab from it directly
onSelectionChanged: widget.onSelectionChanged,
));
}
}

3
lib/emailView.dart Normal file
View File

@ -0,0 +1,3 @@
export 'emailViewStub.dart'
if (dart.library.io) 'emailViewAndroid.dart'
if (dart.library.js_interop) 'emailViewWeb.dart';

120
lib/emailViewAndroid.dart Normal file
View File

@ -0,0 +1,120 @@
import 'package:crab_ui/augment.dart';
import 'package:crab_ui/collapsableEmailsAndroid.dart';
import 'package:flutter/material.dart';
// import 'dart:ui_web' as ui;
// import 'augment.dart';
// // import 'dart:js_interop' as js; //eventually for manipulating css
// import 'package:pointer_interceptor/pointer_interceptor.dart';
// import 'collapsableEmails.dart';
// import 'api_service.dart';
class EmailView extends StatefulWidget {
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,
required this.emailContent,
required this.from,
required this.name, // tf is name
required this.to,
required this.subject,
required this.date,
required this.id,
required this.messages,
}) : super(key: key);
@override
_EmailViewState createState() => _EmailViewState();
}
class _EmailViewState extends State<EmailView> {
@override
void initState() {
super.initState();
}
void _scrollToNumber(String spanId) {
// AugmentClasses.handleJump(spanId);
}
void _viewSpecs(String command){
}
void _filteringQuery(String query){
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
EmailToolbar(
onButtonPressed: () => {},
onJumpToNumbering: _scrollToNumber,
onViewspecs: _viewSpecs,
onFiltering: _filteringQuery,
emails: widget.messages, subject: '', rootAugment: null,
),
Row(
children: [
Expanded(
child: Text(
widget.subject,
style: TextStyle(fontSize: 15),
overflow: TextOverflow.visible,
softWrap: true,
),
),
],
),
Row(
children: [
Text(
'from ${widget.name}',
style: TextStyle(fontSize: 8),
),
Text(
'<${widget.from}>',
style: TextStyle(fontSize: 8),
),
Spacer(),
Text(
widget.date,
textAlign: TextAlign.right,
)
],
),
Row(
children: [
Text(
'to ${widget.to.toString()}',
style: TextStyle(fontSize: 8),
)
],
),
Expanded(
child: CollapsableEmails(
thread: widget.messages,
threadMarkdown: widget.emailContent,
threadIDs: widget.id,
),
),
],
),
)
);
}
}

39
lib/emailViewStub.dart Normal file
View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class EmailView extends StatefulWidget {
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,
required this.emailContent,
required this.from,
required this.name,
required this.to,
required this.subject,
required this.date,
required this.id,
required this.messages,
}) : super(key: key);
@override
_EmailViewState createState() => _EmailViewState();
}
class _EmailViewState extends State<EmailView> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(" emailview stub, not supported")
)
);
}
}

159
lib/emailViewWeb.dart Normal file
View File

@ -0,0 +1,159 @@
import 'package:flutter/material.dart';
import 'dart:ui_web' as ui;
import 'augment.dart';
import 'collapsableEmails.dart';
import 'api_service.dart';
class EmailView extends StatefulWidget {
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,
required this.emailContent,
required this.from,
required this.name,
required this.to,
required this.subject,
required this.date,
required this.id,
required this.messages,
}) : super(key: key);
@override
_EmailViewState createState() => _EmailViewState();
}
class _EmailViewState extends State<EmailView> {
//html css rendering thing
late Key iframeKey;
late String currentContent;
late String viewTypeId; //make this a list too???
// TextEditingController _jumpController = TextEditingController();
late EmailToolbar toolbarInstance = EmailToolbar(
onJumpToNumbering: _handleJumpRequest,
onViewspecs: _handleViewspecsRequest,
onButtonPressed: () => {print("email tool bar pressed")},
onFiltering: _handleFiltering,
emails: widget.messages,
subject: widget.subject,
rootAugment: localCollapsable.getAugmentRoot(),
);
late CollapsableEmails localCollapsable = CollapsableEmails(
//change here
thread: widget.messages, //this wont work in serializable
// threadHTML: widget.emailContent, // old html
threadMarkdown: widget.emailContent,
threadIDs: widget.id,
targetJumpNumbering: _targetJumpNumbering,
targetViewspecs: _targetViewspecs,
targetFiltering: _queryFiltering,
nameOfDocument: widget.subject,
);
final hardcodedMarkers = [
{'id': 'marker1', 'x': 50, 'y': 100},
{'id': 'marker2', 'x': 150, 'y': 200},
{'id': 'marker3', 'x': 250, 'y': 300},
];
String? _targetJumpNumbering;
String? _targetViewspecs;
String? _queryFiltering;
@override
void initState() {
super.initState();
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 _scrollToNumber(String spanId) {
AugmentClasses.handleJump(spanId);
}
void _handleJumpRequest(String numbering) {
setState(() {
_targetJumpNumbering = numbering;
});
}
void _handleViewspecsRequest(String viewspecsCommand) {
setState(() {
_targetViewspecs = viewspecsCommand;
});
}
void _handleFiltering(String query) {
setState(() {
_queryFiltering = query;
});
}
@override
Widget build(BuildContext context) {
ApiService.currThreadID = widget.id;
// AugmentClasses localAugment = AugmentClasses(localCollapsable);
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: Stack(
children: [
Column(
children: [
toolbarInstance,
Row(
// title of email
children: [
Text(
widget.subject,
style: TextStyle(fontSize: 30),
),
],
),
Row(
children: [
Text(
'from ${widget.name}',
style: TextStyle(fontSize: 18),
),
Text(
'<${widget.from}>',
style: TextStyle(fontSize: 18),
),
Spacer(),
Text(
'${widget.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.to.toString()}',
style: TextStyle(fontSize: 15),
)
],
),
Expanded(
child: localCollapsable,
),
],
),
],
));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,22 @@
import 'dart:convert';
// import 'package:crab_ui/api_service.dart';
import 'package:go_router/go_router.dart';
import 'api_service.dart';
import 'home_page.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;
// import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart' show rootBundle;
class AuthService {
class AuthService extends ChangeNotifier {
Future<bool> isUserLoggedIn() async {
ApiService.ip = '192.168.2.38';
ApiService.port = '3001';
print("setted up");
return true;
try {
final response =
await http.get(Uri.http('localhost:6823', 'read-config'));
@ -83,6 +90,7 @@ class LoginPage extends StatefulWidget {
}
class SplashScreen extends StatefulWidget {
//entry point
@override
_SplashScreenState createState() => _SplashScreenState();
}
@ -92,19 +100,24 @@ class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
_checkLoginStatus();
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkLoginStatus();
});
}
Future<void> _checkLoginStatus() async {
// SharedPreferences prefs = await SharedPreferences.getInstance();
// print(prefs);
// bool isLoggedIn = prefs.getBool('isLoggedIn') ?? false;
await Future.delayed(const Duration(seconds: 1));
bool isLoggedIn = await _authService.isUserLoggedIn();
print("is loogeed in $isLoggedIn");
print("is logged in $isLoggedIn");
if (isLoggedIn) {
Navigator.pushReplacementNamed(context, '/home');
context.go("/home");
// Navigator.pushReplacementNamed(context, '/home');
} else {
Navigator.pushReplacementNamed(context, '/login');
context.go("/login");
// Navigator.pushReplacementNamed(context, '/login');
}
}
@ -112,7 +125,7 @@ class _SplashScreenState extends State<SplashScreen> {
Widget build(BuildContext context) {
return Center(
child: Scaffold(
body: Center(child: CircularProgressIndicator()),
body: Center(child: CircularProgressIndicator()), //nothing happens
),
);
}
@ -132,6 +145,7 @@ class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
Future<bool> setIp(String ip) async {
//this is not done :sob: :skull:
// _configManager.setField("api_addr", ip);
return false;
}

View File

@ -1,12 +1,18 @@
import 'package:crab_ui/contact.dart';
import 'package:crab_ui/email.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'home_page.dart';
import 'login.dart';
import 'package:go_router/go_router.dart';
import 'routingHandler.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(HyM());
runApp(ChangeNotifierProvider(
create: (context) => AuthService(),
child: HyM(),
));
}
class HyM extends StatelessWidget {
@ -15,19 +21,56 @@ class HyM extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
final GoRouter _router = GoRouter(
// refreshListenable: ,
initialLocation: '/',
routes: [
GoRoute(
path: "/",
builder: (context, state) => SplashScreen(),
),
GoRoute(
path: "/login",
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: "/home",
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: "/contacts",
builder: (context, state) => ContactsPage(),
),
GoRoute(
path: "/email/:subject/:target/:viewspecs/:emailID",
builder: (context, state) {
final subject = state.pathParameters['subject']!;
final target = state.pathParameters['target']!;
final viewspecs = state.pathParameters['viewspecs']!;
final emailId = state.pathParameters['emailID']!;
return Routinghandler.fromParameters("main anchor", subject, target, viewspecs, emailId);
}),
]);
return MaterialApp.router(
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
theme: ThemeData(
colorScheme: ColorScheme.light(),
useMaterial3: true,
),
title: 'HyM',
routerConfig: _router,
// home: HomeScreen(),
initialRoute: "/",
routes: {
"/": (context) => SplashScreen(),
"/login": (context) => const LoginPage(),
"/home": (context) => HomeScreen(),
"/contacts": (context) => ContactsPage(),
},
// routes: {
// "/": (context) => SplashScreen(),
// "/login": (context) => const LoginPage(),
// "/home": (context) => HomeScreen(),
// "/contacts": (context) => ContactsPage(),
// GoRoute(
// path:
// )
// "/email": (context) => EmailListScreen(),
// },
);
}
}

229
lib/routingHandler.dart Normal file
View File

@ -0,0 +1,229 @@
import 'package:crab_ui/collapsableEmails.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:markdown_widget/markdown_widget.dart';
import 'api_service.dart';
import 'structs.dart';
class Routinghandler extends StatefulWidget {
Routinghandler(String link) {
bool anchorDone = false;
bool docNameDone = false;
bool viewspecsDone = false;
bool targetDone = false;
bool emailIdDone = false;
for (int letter = 0; letter < link.length; letter++) {
if (!anchorDone) {
if (link[letter] != '<') {
//when the anchor hasnt been dissected
anchor += link[letter];
} else {
anchorDone = true;
}
} else if (!docNameDone) {
if (link[letter] != ',') {
//when the docName hasnt been dissected
docName += link[letter];
} else {
docNameDone = true;
}
} else if (!targetDone) {
if (link[letter] != ':') {
target += link[letter];
} else {
targetDone = true;
}
} else if (!viewspecsDone) {
if (link[letter] != '>') {
//when the docName hasnt been dissected
viewspecs += link[letter];
} else {
viewspecsDone = true;
}
} else if (!emailIdDone) {
emailID += link[letter];
}
}
anchor = anchor.trim();
docName = docName.trim();
target = target.trim();
viewspecs = viewspecs.trim();
print("inside constructor uwu $emailID");
emailID = emailID.trim();
}
Routinghandler.fromParameters(String anchor, String docName, String target,
String viewspecs, String emailID) {
this.anchor = anchor;
this.docName = docName;
this.viewspecs = viewspecs;
this.target = target;
this.emailID = emailID;
}
Routinghandler.copyConstructor(Routinghandler other) {
anchor = other.anchor;
docName = other.docName;
viewspecs = other.viewspecs;
target = other.target;
emailID = other.emailID;
}
String anchor = '';
String docName = '';
String viewspecs = '';
String target = '';
String emailID = '';
void goToLink() {
// bool anchorDone = false;
// bool docNameDone = false;
// bool viewspecsDone = false;
// bool targetDone = false;
// for (int letter = 0; letter < link.length; letter++) {
// if (!anchorDone) {
// if (link[letter] != '<') {
// //when the anchor hasnt been dissected
// anchor += link[letter];
// } else {
// anchorDone = true;
// }
// } else if (!docNameDone) {
// if (link[letter] != ',') {
// //when the docName hasnt been dissected
// docName += link[letter];
// } else {
// docNameDone = true;
// }
// } else if (!targetDone) {
// if (link[letter] != ':') {
// target += link[letter];
// } else {
// targetDone = true;
// }
// } else if (!viewspecsDone) {
// if (link[letter] != '>') {
// //when the docName hasnt been dissected
// viewspecs += link[letter];
// } else {
// viewspecsDone = true;
// }
// }
// }
print("anchor $anchor");
print("docName $docName");
print("target $target");
print("viewspecs $viewspecs");
print("emailID $emailID");
//now it should open a widget in that part
//maybe i need a rewrite
}
String getEmailID() {
return emailID;
}
@override
State<StatefulWidget> createState() => _RoutingHandlerState();
}
class _RoutingHandlerState extends State<Routinghandler> {
List<String> markdownContent = [];
bool _isLoaded = false;
AugmentTree? aug;
@override
void initState() {
// TODO: implement initState
super.initState();
_loadMarkdown();
}
Future<void> _loadMarkdown() async {
String folder = ApiService.currFolder;
print(widget.getEmailID());
String emailID = widget.emailID;
print("inside _loadMarkdown in routinghandler $emailID");
markdownContent =
await ApiService().fetchMarkdownContent([emailID], folder);
// print(markdownContent);
aug = AugmentTree.fromMD(markdownContent[0]);
aug!.addNumbering();
setState(() {
_isLoaded = true;
});
}
@override
Widget build(BuildContext context) {
if (!_isLoaded) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Scaffold(
appBar: AppBar(
title: Text("Routing Handler"),
leading: IconButton(
onPressed: () {
GoRouter.of(context).go('/home');
},
icon: const Icon(Icons.arrow_back_ios)),
),
body: ConstrainedBox(
constraints: BoxConstraints(
minHeight: 100,
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
child: SingleChildScrollView(
//inside here put the bunch rows
//make rows of markdownBlocks, but firstly i need to conveert the content into a tree
// child:MarkdownBlock(data: markdownContent[0])
child: Column(children: [
for (int i = 0; i < this.aug!.children![0]!.children.length; i++)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// if (leftNumbering)
Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 5, 0),
child: Text(
aug!.children![0]!.children![i]!.numbering,
style: TextStyle(
color: Color(Colors.purple[400]!.value)),
),
),
Expanded(
child: Align(
alignment: Alignment.topLeft,
child: Wrap(
children: [
MarkdownBlock(
data: aug!.children![0]!.children![i]!
.data ??
''),
],
))),
Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 5, 0),
child: Text(
aug!.children![0]!.children![i]!.numbering,
style: TextStyle(
color: Color(Colors.purple[400]!.value)),
),
),
]),
]))));
}
}

View File

@ -1,145 +1,3 @@
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(),
);
}
}
export 'SonicEmailViewStub.dart'
if (dart.library.js_interop) 'SonicEmailViewWeb.dart'
if (dart.library.io) 'SonicEmailViewAndroid.dart';

View File

@ -1,6 +1,7 @@
//data structures
import 'dart:typed_data';
import 'package:markdown/markdown.dart' as md;
class GetThreadResponse {
final int id;
@ -10,6 +11,7 @@ class GetThreadResponse {
final String from_name;
final String from_address;
final List<MailAddress> to;
late bool seen;
GetThreadResponse({
required this.id,
@ -19,19 +21,20 @@ class GetThreadResponse {
required this.from_name,
required this.from_address,
required this.to,
required this.seen,
});
factory GetThreadResponse.fromJson(Map<String, dynamic> json) {
var toList = json['to'] as List<dynamic>;
return GetThreadResponse(
id: json['id'],
messages: List<String>.from(json['messages']),
subject: json['subject'],
date: DateTime.parse(json['date']),
from_name: json['from_name'],
from_address: json['from_address'],
to: toList.map((i) => MailAddress.fromJson(i)).toList(),
);
id: json['id'],
messages: List<String>.from(json['messages']),
subject: json['subject'],
date: DateTime.parse(json['date']),
from_name: json['from_name'],
from_address: json['from_address'],
to: toList.map((i) => MailAddress.fromJson(i)).toList(),
seen: json['seen']);
}
}
@ -127,7 +130,8 @@ class AttachmentInfoList extends Iterable<AttachmentInfo> {
AttachmentInfoList(this._attachments);
factory AttachmentInfoList.fromJsonList(List<Map<String, dynamic>> jsonList) {
return AttachmentInfoList(jsonList.map((json) => AttachmentInfo.fromJson(json)).toList());
return AttachmentInfoList(
jsonList.map((json) => AttachmentInfo.fromJson(json)).toList());
}
@override
@ -143,6 +147,160 @@ class AttachmentResponse {
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"])));
return AttachmentResponse(
name: json["name"],
data: Uint8List.fromList(List<int>.from(json["data"])));
}
}
class AugmentTree {
List<AugmentTree> children = [];
String data = '';
AugmentTree? parent;
String ogTag = '';
String numbering = '';
Map<String, int> hirarchyDict = {
"h1": 1,
"h2": 2,
"h3": 3,
"h4": 4,
"h5": 5,
"h6": 6,
"p": 8,
"ul": 8,
"li": 8,
};
AugmentTree();
AugmentTree.fromMD(String rawMD) {
//makes raw MD into an augmentTree
print("started markdown2tree");
final List<md.Node> nakedList = md.Document().parseLines(rawMD.split(
'\n')); //emails md is the index of the email in the thread, since this only handles one thus it shall be removed
// AugmentTree zoomTreeRoot = AugmentTree();
for (var node in nakedList) {
//maybe do an add function, but isn't this it?
if (node is md.Element) {
AugmentTree temp = AugmentTree();
temp.data = node.textContent;
temp.ogTag = node.tag;
if (hirarchyDict.containsKey(node.tag)) {
// make this O(1)
_add2Tree(this, node);
// print(node);
}
}
}
this.addNumbering();
}
void _add2Tree(AugmentTree tree, md.Element node2add) {
// adds node to its corresponding place
AugmentTree newNode = AugmentTree();
newNode.setData(node2add.textContent);
newNode.ogTag = node2add.tag;
// cases,
//1. a node that comes is lower than the root.children last, if so it goes beneath it
if (tree.children.isEmpty) {
// new level to be created when totally empty
tree.children.add(newNode);
newNode.parent = tree;
} else if (tree.children.isNotEmpty &&
tree.children.last.ogTag.isNotEmpty) {
if ((hirarchyDict[node2add.tag] ??
-1) < // e.g. new node is h1 and old is h2, heapify
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
//have to figure out the borthers
//assuming it all goes right
if ((hirarchyDict[node2add.tag] ?? -1) == -1 ||
(hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) {
print(
'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} < ${hirarchyDict[tree.children.last.ogTag] ?? -1}');
return;
} else if (tree.children.last.parent == null) {
// becomes the new top level
for (AugmentTree brother in tree.children) {
brother.parent = newNode;
}
tree.children = [newNode];
} else {
newNode.parent = tree;
tree.children.add(newNode);
}
} else if ((hirarchyDict[node2add.tag] ??
-1) > // go down e.g. new node is h3 and old is h2 or something
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
if ((hirarchyDict[node2add.tag] ?? -1) == -1 ||
(hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) {
print(
'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} > ${hirarchyDict[tree.children.last.ogTag] ?? -1}');
print("-1 ${tree.children.last.ogTag}");
return;
}
_add2Tree(tree.children.last, node2add);
} else if ((hirarchyDict[node2add.tag] ?? -1) ==
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
tree.children.add(newNode);
newNode.parent = tree;
}
}
}
void setData(String data) {
this.data = data;
}
static String _intToLetter(int index) {
return String.fromCharCode('a'.runes.first + index);
}
void addNumbering({String prefix = ''}) {
//if called in root, numbers them all
for (int i = 0; i < children.length; i++) {
final child = children[i];
String childNumbering;
bool parentIsLettered = prefix.contains(RegExp(r'[a-z]'));
if (prefix.isEmpty) {
parentIsLettered = false;
} else {
parentIsLettered = prefix.runes.last >= 'a'.runes.first &&
prefix.runes.last <= 'z'.runes.first;
}
if (prefix.isEmpty) {
// Top-level children (direct children of the original root being numbered) get 1, 2, 3...
childNumbering = (i + 1).toString();
} else if (parentIsLettered) {
// Deeper children get '1a', '1b', '2a', '2b', etc.
childNumbering = '$prefix${i + 1}';
} else {
childNumbering = '$prefix${_intToLetter(i)}';
}
child.numbering = childNumbering;
// Recursively call for children
child.addNumbering(prefix: childNumbering);
}
}
}
//perhaps make a struct that builds augment tree, since its so complex and needs to be like recursive
class MarkdownParsed {
//struct for holding the MD given in endpoint //not used
final String text;
MarkdownParsed({required this.text});
factory MarkdownParsed.fromJson(Map<String, String> json) {
return MarkdownParsed(
text: json['md'] ?? '',
);
}
}
//should make an md to tree class/struct
// make a for loop of rows with markdown

File diff suppressed because it is too large Load Diff

View File

@ -12,14 +12,12 @@ dependencies:
flutter:
sdk: flutter
http: 1.2.2
flutter_html_all: 3.0.0-beta.2
flutter_widget_from_html: ^0.10.0
shared_preferences: ^2.0.6
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
@ -28,6 +26,13 @@ dependencies:
pdfrx: ^1.0.94
photo_view: ^0.15.0
web: ^1.1.1
flutter_widget_from_html: ^0.16.0
html2md: ^1.3.2
markdown_widget: ^2.3.2+8
markdown: ^7.3.0
go_router: ^16.0.0
super_editor: ^0.3.0-dev.27
super_editor_markdown: 0.1.8
dev_dependencies:
flutter_test:
@ -39,6 +44,7 @@ dependency_overrides:
flutter_layout_grid: 2.0.7
flutter_math_fork: 0.7.2
flutter:
uses-material-design: true
assets: