WIP: android/ios-adaption feature, markdown, and augment #6
30
.metadata
@ -4,7 +4,7 @@
|
|||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
|
revision: "be698c48a6750c8cb8e61c740ca9991bb947aba2"
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
@ -13,26 +13,26 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: android
|
- platform: android
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: ios
|
- platform: ios
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: linux
|
- platform: linux
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: macos
|
- platform: macos
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: web
|
- platform: web
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: windows
|
- platform: windows
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
14
android/.gitignore
vendored
Normal 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
|
44
android/app/build.gradle.kts
Normal 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 = "../.."
|
||||||
|
}
|
7
android/app/src/debug/AndroidManifest.xml
Normal 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>
|
45
android/app/src/main/AndroidManifest.xml
Normal 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>
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.example.crab_ui
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal 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>
|
12
android/app/src/main/res/drawable/launch_background.xml
Normal 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>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal 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>
|
18
android/app/src/main/res/values/styles.xml
Normal 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>
|
7
android/app/src/profile/AndroidManifest.xml
Normal 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
@ -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)
|
||||||
|
}
|
3
android/gradle.properties
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
25
android/settings.gradle.kts
Normal 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
@ -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
|
58
android_old/app/build.gradle
Normal 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 = "../.."
|
||||||
|
}
|
7
android_old/app/src/debug/AndroidManifest.xml
Normal 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>
|
45
android_old/app/src/main/AndroidManifest.xml
Normal 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>
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.example.hym_ui
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity: FlutterActivity()
|
@ -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>
|
12
android_old/app/src/main/res/drawable/launch_background.xml
Normal 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>
|
BIN
android_old/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 544 B |
BIN
android_old/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 442 B |
BIN
android_old/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 721 B |
BIN
android_old/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
android_old/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
18
android_old/app/src/main/res/values-night/styles.xml
Normal 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>
|
18
android_old/app/src/main/res/values/styles.xml
Normal 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>
|
7
android_old/app/src/profile/AndroidManifest.xml
Normal 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
@ -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
|
||||||
|
}
|
3
android_old/gradle.properties
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
5
android_old/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
25
android_old/settings.gradle
Normal 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
@ -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 '';
|
||||||
|
}
|
||||||
|
}
|
19
lib/SonicEmailViewAndroid.dart
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
22
lib/SonicEmailViewStub.dart
Normal 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
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +1,16 @@
|
|||||||
// this file should handle most of the API calls
|
// 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:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
|
||||||
import 'collapsableEmails.dart';
|
|
||||||
|
|
||||||
import 'structs.dart';
|
import 'structs.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:convert';
|
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 {
|
class ApiService {
|
||||||
static String ip = "";
|
static String ip = '127.0.0.1';
|
||||||
static String port = "";
|
static String port = "3001";
|
||||||
static List<AttachmentResponse> threadAttachments =
|
static List<AttachmentResponse> threadAttachments =
|
||||||
[]; //holds attachments of the thread
|
[]; //holds attachments of the thread
|
||||||
static String currFolder = "";
|
static String currFolder = "";
|
||||||
@ -59,7 +49,39 @@ class ApiService {
|
|||||||
return [];
|
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(
|
Future<void> fetchThreads(
|
||||||
//populates allEmails, which is the List that contains all the emails in a thread
|
//populates allEmails, which is the List that contains all the emails in a thread
|
||||||
int threadId,
|
int threadId,
|
||||||
@ -148,7 +170,6 @@ class ApiService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('_getEmailContent caught error: $e');
|
print('_getEmailContent caught error: $e');
|
||||||
}
|
}
|
||||||
// return content;
|
|
||||||
return HTMLofThread;
|
return HTMLofThread;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +201,7 @@ class ApiService {
|
|||||||
Future<bool> moveEmail(
|
Future<bool> moveEmail(
|
||||||
//only moves the first email of the thread //or perhaps should do the last
|
//only moves the first email of the thread //or perhaps should do the last
|
||||||
String fromFolder,
|
String fromFolder,
|
||||||
String thread_id,
|
String thread_id, //uid
|
||||||
String toFolder) async {
|
String toFolder) async {
|
||||||
var url = Uri.http('$ip:$port', 'move_email');
|
var url = Uri.http('$ip:$port', 'move_email');
|
||||||
|
|
||||||
@ -191,16 +212,17 @@ class ApiService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SerializableMessage firstMail = mailsInSerializable[0];
|
// SerializableMessage firstMail = mailsInSerializable[0];
|
||||||
|
|
||||||
Map<String, String> requestBody = {
|
|
||||||
'from': fromFolder,
|
|
||||||
'uid': firstMail.uid.toString(),
|
|
||||||
'to': toFolder,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
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,
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -212,7 +234,7 @@ class ApiService {
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
print('error ${response.statusCode} ${response.body}');
|
print('error ${response.statusCode} ${response.body}');
|
||||||
}
|
}}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("failed trying to post move_email, with error: $e");
|
print("failed trying to post move_email, with error: $e");
|
||||||
}
|
}
|
||||||
@ -351,308 +373,143 @@ class ApiService {
|
|||||||
return AttachmentResponse(name: "error", data: Uint8List(0));
|
return AttachmentResponse(name: "error", data: Uint8List(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: MOVE THIS INTO WEB
|
Future<List<String>> fetchMarkdownContent(
|
||||||
// Future<List<Map<String, dynamic>>> getMarkerPosition() async {
|
List<String> IDsString, String emailFolder) async {
|
||||||
// //this is so we can put a widget right below each email, but the way how the email content is generated
|
List<String> MDofThread = [];
|
||||||
// //leads to problems as for a) the html is added one right after the other in one iframe, b)
|
threadAttachments = [];
|
||||||
// // if it was multiple iframes then the scrolling to jump would not work as expected
|
int counter = 0;
|
||||||
|
|
||||||
// print("marker called");
|
try {
|
||||||
// // JavaScript code embedded as a string
|
//attaches email after email from a thread
|
||||||
// String jsCode = '''
|
for (var id in IDsString) {
|
||||||
// (async function waitForIframeAndMarkers() {
|
var url = Uri.http('$ip:$port', 'email_md', {'id': id});
|
||||||
// try {
|
print(url);
|
||||||
// return await new Promise((resolve) => {
|
var response = await http.get(url);
|
||||||
// const interval = setInterval(() => {
|
currThread.add(id);
|
||||||
// console.log("⏳ Checking for iframe...");
|
if (response.statusCode == 200) {
|
||||||
// var iframe = document.getElementsByTagName('iframe')[0];
|
counter += 1;
|
||||||
// if (iframe && iframe.contentDocument) {
|
Map<String, dynamic> json = jsonDecode(response.body);
|
||||||
// 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 {
|
MDofThread.add(json['md'] ?? '');
|
||||||
// // Execute the JavaScript code using eval
|
try {
|
||||||
// // final result = await js.context.callMethod('eval', [jsCode]);
|
List<AttachmentInfo> attachments =
|
||||||
|
await getAttachmentsInfo(emailFolder, id);
|
||||||
// if (result != null && result is String) {
|
for (var attachment in attachments) {
|
||||||
// print("Result received: $result");
|
//TODO: for each attachment creaate at the bottom a widget for each individual one
|
||||||
|
threadAttachments
|
||||||
// // Parse the JSON string returned by JavaScript into a Dart list of maps
|
.add(await getAttachment(emailFolder, id, attachment.name));
|
||||||
// final List<dynamic> parsedResult = jsonDecode(result);
|
}
|
||||||
// var positions = List<Map<String, dynamic>>.from(parsedResult);
|
} catch (innerError) {
|
||||||
// print("positions put on");
|
print('_getAttachment info caught error $innerError');
|
||||||
// print(positions);
|
}
|
||||||
// return positions;
|
}
|
||||||
// } else {
|
}
|
||||||
// print("result is null or not a string");
|
} catch (e) {
|
||||||
// }
|
print('_getMDContent caught error: $e');
|
||||||
// } catch (e, stackTrace) {
|
}
|
||||||
// print("Error executing JavaScript: $e");
|
// print("IDS inside fetch md content $IDsString");
|
||||||
// print(stackTrace);
|
// print("inside apiservice $MDofThread");
|
||||||
// }
|
return MDofThread;
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// void _registerViewFactory(List<String> currentContent) { // i think this doesnt work anymore
|
Future<void> markAsSeen(int thread_id) async {
|
||||||
// setState(() { //update to do item per item
|
try {
|
||||||
// // each item to have itsviewtype ID
|
var url = Uri.http(
|
||||||
// // is this necessarey here??
|
'$ip:$port', 'post_seen_thread', {'id': thread_id.toString()});
|
||||||
|
var response = await http.get(url);
|
||||||
// //could just move to collapsable
|
if (response.statusCode == 200) {
|
||||||
|
var result = response.body;
|
||||||
// viewTypeId = 'iframe-${DateTime.now().millisecondsSinceEpoch}';
|
print("data $result");
|
||||||
// final emailHTML = web.document.createElement('div') as web.HTMLDivElement
|
}
|
||||||
// ..id = viewTypeId
|
} catch (e) {
|
||||||
// ..innerHTML = currentContent[0].toJS; // temporarily index because it has to do all of them
|
print("markAsSeen failed $e");
|
||||||
// emailHTML.style
|
}
|
||||||
// ..width = '100%'
|
|
||||||
// ..height = '100%'
|
|
||||||
// ..overflow = 'auto'
|
|
||||||
// ..scrollBehavior = 'smooth';
|
|
||||||
|
|
||||||
// ui.platformViewRegistry.registerViewFactory(
|
|
||||||
// viewTypeId,
|
|
||||||
// (int viewId) => emailHTML,
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
void _scrollToNumber(String spanId) {
|
|
||||||
AugmentClasses.handleJump(spanId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
Future<bool> deleteEmail(String from_folder, int thread_id) async {
|
||||||
Widget build(BuildContext context) {
|
// post
|
||||||
// print("thread id ${widget.id}");
|
try {
|
||||||
ApiService.currThreadID = widget.id;
|
List<SerializableMessage> mailsInSerializable =
|
||||||
return Scaffold(
|
await this.threadsInSerializable(thread_id.toString());
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(widget.name),
|
|
||||||
),
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
EmailToolbar(
|
|
||||||
onJumpToSpan: _scrollToNumber,
|
|
||||||
onButtonPressed: () => {},
|
|
||||||
// AugmentClasses.handleJump(viewTypeId, '1');
|
|
||||||
// print("button got pressed?");
|
|
||||||
|
|
||||||
// _registerViewFactory(r"""
|
if (mailsInSerializable.isEmpty) {
|
||||||
// <h1>Welcome to My Website</h1>
|
return false;
|
||||||
// <p>This is a simple HTML page.</p>
|
}
|
||||||
// <h2>What is HTML?</h2>
|
Map<String, String> requestBody = {
|
||||||
// <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>
|
"from": from_folder,
|
||||||
// <h3>Here's a simple list:</h3>
|
"uid": mailsInSerializable.first.uid.toString(),
|
||||||
// <ul>
|
"to": "not used"
|
||||||
// <li>HTML elements are the building blocks of HTML pages</li>
|
};
|
||||||
// <li>HTML uses tags like <code><tag></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"
|
|
||||||
|
|
||||||
//
|
//delete the email that is given to the
|
||||||
),
|
var url = Uri.http("$ip:$port", 'delete_email');
|
||||||
Row(
|
var response = await http.post(url,
|
||||||
// title of email
|
headers: {
|
||||||
children: [
|
"Content-Type": "application/json",
|
||||||
Text(
|
},
|
||||||
widget.subject,
|
body: jsonEncode(requestBody));
|
||||||
style: TextStyle(fontSize: 30),
|
if (response.statusCode == 200) {
|
||||||
),
|
print("response body: ${response.body}");
|
||||||
],
|
return true;
|
||||||
),
|
} else {
|
||||||
Row(
|
print("not 200: ${response.body}");
|
||||||
children: [
|
return false;
|
||||||
Text(
|
}
|
||||||
'from ${widget.name}',
|
} catch (e) {
|
||||||
style: TextStyle(fontSize: 18),
|
print("error in deleteEmail $e");
|
||||||
),
|
return false;
|
||||||
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,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// Overlay widgets dynamically based on marker positions
|
Future<bool> sendEmail(
|
||||||
// FutureBuilder<List<Map<String, dynamic>>>(
|
String? to, String? subject, String? emailContent) async {
|
||||||
// future: _markerPositionsFuture,
|
try {
|
||||||
// builder: (context, snapshot) {
|
var url = Uri.http('$ip:$port', 'send_email');
|
||||||
// print("FutureBuilder state: ${snapshot.connectionState}");
|
|
||||||
// if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
// return Center(child: CircularProgressIndicator());
|
|
||||||
// }
|
|
||||||
// if (snapshot.hasError) {
|
|
||||||
// print("Error in FutureBuilder: ${snapshot.error}");
|
|
||||||
// return Center(child: Text('error loading markers'));
|
|
||||||
// }
|
|
||||||
// if (snapshot.hasData && snapshot.data != null) {
|
|
||||||
// final markers = snapshot.data!;
|
|
||||||
// return Stack(
|
|
||||||
// children: markers.map((marker) {
|
|
||||||
// return Positioned(
|
|
||||||
// left: marker['x'].toDouble(),
|
|
||||||
// top: marker['y'].toDouble(),
|
|
||||||
// child: GestureDetector(
|
|
||||||
// onTap: () {
|
|
||||||
// print('Tapped on ${marker['id']}');
|
|
||||||
// },
|
|
||||||
// child: Container(
|
|
||||||
// width: 50,
|
|
||||||
// height: 50,
|
|
||||||
// color: Colors.red,
|
|
||||||
// child: Center(
|
|
||||||
// child: Text(
|
|
||||||
// marker['id'],
|
|
||||||
// style: TextStyle(color: Colors.white),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }).toList(),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return SizedBox.shrink(); // No markers found
|
Map<String, dynamic> requestBody = {
|
||||||
// },
|
"to": [to ?? ""],
|
||||||
// ),
|
"cc": [],
|
||||||
// Red widget overlay
|
"bcc": [],
|
||||||
// Positioned(
|
"subject": subject ?? "Untitled",
|
||||||
// left: 8, // Adjust based on your desired position
|
"in_reply_to": "",
|
||||||
// top: 100 + 44 + 5, // Adjust based on your desired position
|
"messages": [
|
||||||
// child: IgnorePointer(
|
{"message": emailContent ?? "", "is_html": false}
|
||||||
// ignoring: true, // Ensures the iframe remains interactive
|
],
|
||||||
// child: Container(
|
"attachments": [],
|
||||||
// color: Colors.red,
|
"inline_images": [],
|
||||||
// width: 100,
|
};
|
||||||
// height: 50,
|
var response = await http.post(
|
||||||
// child: Center(
|
url,
|
||||||
// child: Text(
|
headers: {
|
||||||
// 'Overlay',
|
'Content-Type': 'application/json',
|
||||||
// style: TextStyle(color: Colors.white),
|
},
|
||||||
// ),
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
lib/attachamentDownloadStub.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import 'structs.dart';
|
||||||
|
|
||||||
|
class Attachmentdownload {
|
||||||
|
Future<void> saveFile(AttachmentResponse attachment) async {
|
||||||
|
print("stub attachment download");
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,3 @@
|
|||||||
import 'dart:html' as html;
|
export 'attachamentDownloadStub.dart'
|
||||||
import 'package:web/web.dart' as web;
|
if (dart.library.io) 'attachmentDownloadAndroid.dart';
|
||||||
import 'dart:io';
|
// if (dart.library.js_interop) 'attachmentDownloadWeb.dart';
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
7
lib/attachmentDownloadAndroid.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import 'structs.dart';
|
||||||
|
|
||||||
|
class Attachmentdownload {
|
||||||
|
Future<void> saveFile(AttachmentResponse attachment) async {
|
||||||
|
print("android attachment download");
|
||||||
|
}
|
||||||
|
}
|
12
lib/attachmentDownloadWeb.dart
Normal 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)
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
@ -1,103 +1,3 @@
|
|||||||
import "dart:typed_data";
|
export 'attachmentWidgetStub.dart'
|
||||||
|
if (dart.library.js_interop) 'attachmentWidgetWeb.dart'
|
||||||
import "package:crab_ui/attachmentDownload.dart";
|
if (dart.library.io) 'attachmentWidgetAndroid.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),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
16
lib/attachmentWidgetAndroid.dart
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
14
lib/attachmentWidgetStub.dart
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
101
lib/attachmentWidgetWeb.dart
Normal 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),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
477
lib/augment.dart
@ -1,24 +1,33 @@
|
|||||||
// import 'dart:ffi';
|
|
||||||
|
|
||||||
import 'package:crab_ui/api_service.dart';
|
import 'package:crab_ui/api_service.dart';
|
||||||
import 'package:crab_ui/attachmentDownload.dart';
|
import 'package:crab_ui/attachmentDownload.dart';
|
||||||
|
import 'package:crab_ui/collapsableEmails.dart';
|
||||||
import 'package:crab_ui/structs.dart';
|
import 'package:crab_ui/structs.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:pdfrx/pdfrx.dart';
|
import 'package:go_router/go_router.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:pointer_interceptor/pointer_interceptor.dart';
|
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||||
import 'attachmentWidget.dart';
|
import 'attachmentWidget.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'routingHandler.dart';
|
||||||
|
|
||||||
class EmailToolbar extends StatefulWidget {
|
class EmailToolbar extends StatefulWidget {
|
||||||
final Function(String) onJumpToSpan;
|
final Function(String) onJumpToNumbering;
|
||||||
|
final Function(String) onViewspecs;
|
||||||
final VoidCallback onButtonPressed;
|
final VoidCallback onButtonPressed;
|
||||||
|
final Function(String) onFiltering;
|
||||||
|
final List<String> emails;
|
||||||
|
final String subject;
|
||||||
|
late AugmentTree? rootAugment;
|
||||||
|
|
||||||
EmailToolbar(
|
EmailToolbar({
|
||||||
{Key? key, required this.onButtonPressed, required this.onJumpToSpan})
|
Key? key,
|
||||||
: super(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
|
@override
|
||||||
_DynamicClassesAugment createState() => _DynamicClassesAugment();
|
_DynamicClassesAugment createState() => _DynamicClassesAugment();
|
||||||
@ -26,7 +35,10 @@ class EmailToolbar extends StatefulWidget {
|
|||||||
|
|
||||||
class _DynamicClassesAugment extends State<EmailToolbar> {
|
class _DynamicClassesAugment extends State<EmailToolbar> {
|
||||||
String selectedClass = 'Class 1';
|
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 _JumpItemfocusNode;
|
||||||
// late final FocusNode _viewSpecsfocusNode;
|
// late final FocusNode _viewSpecsfocusNode;
|
||||||
@ -47,13 +59,25 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
// _viewSpecsfocusNode.addListener(() {
|
// _viewSpecsfocusNode.addListener(() {
|
||||||
// setState(() => _viewSpecsHasFocus = _viewSpecsfocusNode.hasFocus);
|
// 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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// _JumpItemfocusNode.dispose();
|
// _JumpItemfocusNode.dispose();
|
||||||
// _viewSpecsfocusNode.dispose();
|
// _viewSpecsfocusNode.dispose();
|
||||||
// _jumpController.dispose();
|
_jumpController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,20 +102,20 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
child: Text('Attachments'),
|
child: Text('Attachments'),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
ElevatedButton(
|
// ElevatedButton(
|
||||||
onPressed: AugmentClasses.handleOpen,
|
// onPressed: AugmentClasses.handleOpen,
|
||||||
child: Text('Open'),
|
// child: Text('Open'),
|
||||||
),
|
// ),
|
||||||
// SizedBox(width: 8),
|
// SizedBox(width: 8),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: AugmentClasses.handleFind,
|
onPressed: AugmentClasses.handleFind,
|
||||||
child: Text('Find'),
|
child: Text('Find'),
|
||||||
),
|
),
|
||||||
// SizedBox(width: 8),
|
// SizedBox(width: 8),
|
||||||
ElevatedButton(
|
// ElevatedButton(
|
||||||
onPressed: AugmentClasses.handleStop,
|
// onPressed: AugmentClasses.handleStop,
|
||||||
child: Text('Stop'),
|
// child: Text('Stop'),
|
||||||
),
|
// ),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AugmentClasses.handleMove(context);
|
AugmentClasses.handleMove(context);
|
||||||
@ -138,10 +162,10 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
// width: 8,
|
// width: 8,
|
||||||
// ),
|
// ),
|
||||||
Container(
|
Container(
|
||||||
width: 50,
|
width: 100,
|
||||||
height: 30,
|
height: 30,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
// controller: _jumpController,
|
controller: _jumpController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
// suffixIcon: Icon(Icons.search)
|
// suffixIcon: Icon(Icons.search)
|
||||||
@ -149,7 +173,7 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
onSubmitted: (value) {
|
onSubmitted: (value) {
|
||||||
print("onSubmitted");
|
print("onSubmitted");
|
||||||
if (value.isNotEmpty) {
|
if (value.isNotEmpty) {
|
||||||
widget.onJumpToSpan(value);
|
widget.onJumpToNumbering(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -186,18 +210,23 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
onPressed: () => AugmentClasses.ViewSpecsButton(context),
|
onPressed: () => AugmentClasses.ViewSpecsButton(context),
|
||||||
child: Text('ViewSpecs:')),
|
child: Text('ViewSpecs:')),
|
||||||
Container(
|
Container(
|
||||||
width: 50,
|
width: 100,
|
||||||
height: 30,
|
height: 30,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
controller: _viewspecsController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '',
|
labelText: '',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
// suffixIcon: Icon(Icons.style_rounded)
|
// suffixIcon: Icon(Icons.style_rounded)
|
||||||
),
|
),
|
||||||
|
onSubmitted: (value) {
|
||||||
|
widget.onViewspecs(value);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => AugmentClasses.FilterButton(context),
|
onPressed: () =>
|
||||||
|
AugmentClasses().filterButton(context, widget.onFiltering),
|
||||||
child: Text('Filter'),
|
child: Text('Filter'),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
@ -207,11 +236,14 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
),
|
),
|
||||||
// SizedBox(width: 8),
|
// SizedBox(width: 8),
|
||||||
ElevatedButton(
|
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'),
|
child: Text('Create Link'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: AugmentClasses.handleFind,
|
// onPressed: () => localAugment!.handlePaste(context),
|
||||||
|
onPressed: () =>
|
||||||
|
AugmentClasses().handlePaste(context),
|
||||||
child: Text('Paste Link'),
|
child: Text('Paste Link'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -222,6 +254,18 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AugmentClasses {
|
class AugmentClasses {
|
||||||
|
CollapsableEmails? localCollapsable;
|
||||||
|
String? nameOfDocument;
|
||||||
|
AugmentTree? rootTree;
|
||||||
|
|
||||||
|
void setRootTree(AugmentTree aTree) {
|
||||||
|
rootTree = aTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AugmentClasses(CollapsableEmails localCollapsable) {
|
||||||
|
// localCollapsable = localCollapsable;
|
||||||
|
// }
|
||||||
|
|
||||||
static OverlayEntry? _overlayEntry;
|
static OverlayEntry? _overlayEntry;
|
||||||
static String? selectedFolder; // Manage selected folder at the class level
|
static String? selectedFolder; // Manage selected folder at the class level
|
||||||
|
|
||||||
@ -487,45 +531,236 @@ class AugmentClasses {
|
|||||||
print("Find button pressed");
|
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() {
|
static void handleStop() {
|
||||||
print("Stop button pressed");
|
print("Stop button pressed");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleJump(String spanId) {
|
static void handleJump(String value) {
|
||||||
String js_code = '''
|
print(value);
|
||||||
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 invisibility(String htmlClass) {}
|
static void invisibility(String htmlClass) {}
|
||||||
|
|
||||||
static Future<void> JumpButton(BuildContext context) async {
|
static Future<void> JumpButton(BuildContext context) async {
|
||||||
// FocusNode textFieldFocusNode = FocusNode();
|
|
||||||
|
|
||||||
// AugmentClasses.disableIframePointerEvents();
|
|
||||||
await showDialog(
|
await showDialog(
|
||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
// barrierColor: Colors.yellow,
|
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text('Jump Item:'),
|
title: Text('Jump Item:'),
|
||||||
@ -725,7 +960,6 @@ class AugmentClasses {
|
|||||||
ElevatedButton(onPressed: () {}, child: Text('OK')),
|
ElevatedButton(onPressed: () {}, child: Text('OK')),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// AugmentClasses.disableIframePointerEvents();
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text('Cancel')),
|
child: Text('Cancel')),
|
||||||
@ -743,61 +977,90 @@ class AugmentClasses {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleFilter() {}
|
Future<List<AugmentTree>> searchFilter(String query) async {
|
||||||
static Future<void> FilterButton(context) async {
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> filterButton(
|
||||||
|
context, Function(String) onFilteringCallback) async {
|
||||||
//this is literally ctrl+F :skull:
|
//this is literally ctrl+F :skull:
|
||||||
//idea is to search in file, extract the <p> tags that contain these
|
//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
|
//words and highlight, then when zoom, you just jump to that paragraph
|
||||||
|
bool? numbering = false;
|
||||||
|
String filterQueue = '';
|
||||||
|
|
||||||
// AugmentClasses.disableIframePointerEvents();
|
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Container(
|
builder: (BuildContext dialogContext) {
|
||||||
height: 150,
|
return StatefulBuilder(builder:
|
||||||
width: 300,
|
(BuildContext statefulBuilderContext, StateSetter setState) {
|
||||||
child: AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Filter'),
|
title: const Text('Filter'),
|
||||||
content: Container(
|
content: SizedBox(
|
||||||
width: 400, // Set the width to simulate the Windows style
|
width: 400, // Set the width to simulate the Windows style
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('Set filter:'),
|
const Text('Set filter:'),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 175,
|
width: 175,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
maxLines: 1,
|
autofocus: true,
|
||||||
decoration: InputDecoration(
|
maxLines: 1,
|
||||||
border: OutlineInputBorder(),
|
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
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
@ -1,132 +1,3 @@
|
|||||||
import 'dart:js_interop';
|
export 'collapsableEmailsStub.dart'
|
||||||
import 'package:web/web.dart' as web;
|
if (dart.library.io) 'collapsableEmailsAndroid.dart'
|
||||||
import 'package:flutter/material.dart';
|
if (dart.library.js_interop) 'collapsableEmailsWeb.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());
|
|
||||||
}
|
|
||||||
}
|
|
442
lib/collapsableEmailsAndroid.dart
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
32
lib/collapsableEmailsStub.dart
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
582
lib/collapsableEmailsWeb.dart
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart' as http;
|
// import 'package:http/http.dart' as http;
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
// import 'package:flutter_html/flutter_html.dart';
|
||||||
|
|
||||||
class ContactsPage extends StatefulWidget {
|
class ContactsPage extends StatefulWidget {
|
||||||
const ContactsPage({super.key});
|
const ContactsPage({super.key});
|
||||||
|
293
lib/email.dart
@ -1,57 +1,225 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:markdown/markdown.dart' as md;
|
||||||
import 'api_service.dart';
|
import 'api_service.dart';
|
||||||
import 'structs.dart';
|
import 'structs.dart';
|
||||||
|
import 'emailView.dart';
|
||||||
|
import 'Compose.dart';
|
||||||
|
|
||||||
class EmailListScreen extends StatelessWidget {
|
class EmailListScreen extends StatefulWidget {
|
||||||
final List<GetThreadResponse> emails;
|
final List<GetThreadResponse> emails;
|
||||||
final Future<List<String>> Function(List<String>, String) getEmailContent;
|
final Future<List<String>> Function(List<String>, String) getEmailContent;
|
||||||
final String folder;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: ListView.separated(
|
body: ListView.separated(
|
||||||
itemCount: emails.length,
|
itemCount: widget.emails.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final email = emails[index];
|
Color seenColour;
|
||||||
return ListTile(
|
final email = widget.emails[index];
|
||||||
title: Text(email.from_name,
|
if (email.seen) {
|
||||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
seenColour = ThemeData().highlightColor;
|
||||||
subtitle: Column(
|
} else {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
seenColour = Colors.transparent;
|
||||||
children: [Text(email.subject)],
|
}
|
||||||
),
|
return MouseRegion(
|
||||||
trailing: Text(email.date.toString()),
|
onEnter: (_) => setState(() => _hoveredRows.add(index)),
|
||||||
onTap: () async {
|
onExit: (_) => setState(() => _hoveredRows.remove(index)),
|
||||||
List<String> emailContent = // list of the html
|
child: ListTile(
|
||||||
await getEmailContent(email.messages, folder);
|
leading: Checkbox(
|
||||||
// print("this is what email.messages look like in email.dart ${email.messages}");
|
value: selectStates[index],
|
||||||
// List<String> emailIds = email.messages;
|
onChanged: (bool? value) {
|
||||||
|
setState(() {
|
||||||
|
//works great
|
||||||
|
selectStates[index] = value ?? false;
|
||||||
|
|
||||||
print(email.messages); //email ids of the thread
|
setState(() {
|
||||||
Navigator.push(
|
if (value!) {
|
||||||
context,
|
selectedEmails.add(widget.emails[index]);
|
||||||
MaterialPageRoute(
|
//here i must update the other side
|
||||||
// could call collapsable and inside collable each calls email view?
|
_emailPageKey.currentState?.getListOfSelected();
|
||||||
builder: (context) => EmailView(
|
} else {
|
||||||
emailContent: emailContent,
|
selectedEmails.remove(widget.emails[index]);
|
||||||
from: email.from_address,
|
_emailPageKey.currentState?.getListOfSelected();
|
||||||
name: email.from_name,
|
}
|
||||||
to: email.to.toString(),
|
widget.onSelectionChanged?.call(selectedEmails);
|
||||||
subject: email.subject,
|
print(selectedEmails);
|
||||||
date: email.date.toString(),
|
});
|
||||||
id: email.id.toString(), //i think this is thread id?
|
});
|
||||||
messages: email.messages,
|
},
|
||||||
),
|
),
|
||||||
),
|
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(),
|
separatorBuilder: (context, index) => Divider(),
|
||||||
@ -62,10 +230,12 @@ class EmailListScreen extends StatelessWidget {
|
|||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class EmailPage extends StatefulWidget {
|
class EmailPage extends StatefulWidget {
|
||||||
EmailPage({Key? key}) : super(key: key);
|
|
||||||
String selectedFolder = "INBOX"; //starter
|
String selectedFolder = "INBOX"; //starter
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
int page = 1;
|
int page = 1;
|
||||||
|
final Function(List<GetThreadResponse>)? onSelectionChanged;
|
||||||
|
|
||||||
|
EmailPage({Key? key, this.onSelectionChanged}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EmailPageState createState() => EmailPageState();
|
EmailPageState createState() => EmailPageState();
|
||||||
@ -78,6 +248,9 @@ class EmailPageState extends State<EmailPage> {
|
|||||||
int page = 1;
|
int page = 1;
|
||||||
bool isBackDisabled = false;
|
bool isBackDisabled = false;
|
||||||
|
|
||||||
|
final GlobalKey<_EmailListScreenState> emailListKey =
|
||||||
|
GlobalKey<_EmailListScreenState>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -86,6 +259,7 @@ class EmailPageState extends State<EmailPage> {
|
|||||||
_fetchEmails();
|
_fetchEmails();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<GetThreadResponse> get getEmails => emails;
|
||||||
String getPage() => widget.page.toString();
|
String getPage() => widget.page.toString();
|
||||||
bool get backDisabled => isBackDisabled;
|
bool get backDisabled => isBackDisabled;
|
||||||
|
|
||||||
@ -116,7 +290,6 @@ class EmailPageState extends State<EmailPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// print(currentPage);
|
|
||||||
print(widget.page);
|
print(widget.page);
|
||||||
_fetchEmails();
|
_fetchEmails();
|
||||||
}
|
}
|
||||||
@ -124,7 +297,7 @@ class EmailPageState extends State<EmailPage> {
|
|||||||
void _fetchEmails() async {
|
void _fetchEmails() async {
|
||||||
try {
|
try {
|
||||||
List<GetThreadResponse> fetchedEmails = await apiService
|
List<GetThreadResponse> fetchedEmails = await apiService
|
||||||
.fetchEmailsFromFolder(widget.selectedFolder, widget.offset);
|
.fetchEmailsFromFolderReversed(widget.selectedFolder, widget.offset);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
setState(() {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: EmailListScreen(
|
body: EmailListScreen(
|
||||||
emails: emails,
|
key: emailListKey,
|
||||||
getEmailContent: apiService.fetchEmailContent,
|
emails: emails,
|
||||||
folder: widget.selectedFolder, //try to grab from it directly
|
// getEmailContent: apiService.fetchEmailContent,
|
||||||
),
|
getEmailContent: apiService.fetchMarkdownContent,
|
||||||
);
|
folder: widget.selectedFolder, //try to grab from it directly
|
||||||
|
onSelectionChanged: widget.onSelectionChanged,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
lib/emailView.dart
Normal 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
@ -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
@ -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
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,22 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
// import 'package:crab_ui/api_service.dart';
|
// import 'package:crab_ui/api_service.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import 'api_service.dart';
|
import 'api_service.dart';
|
||||||
import 'home_page.dart';
|
// import 'home_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
// import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
// import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
// import 'package:shared_preferences/shared_preferences.dart';
|
// import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
|
||||||
class AuthService {
|
class AuthService extends ChangeNotifier {
|
||||||
Future<bool> isUserLoggedIn() async {
|
Future<bool> isUserLoggedIn() async {
|
||||||
|
ApiService.ip = '192.168.2.38';
|
||||||
|
ApiService.port = '3001';
|
||||||
|
print("setted up");
|
||||||
|
|
||||||
|
return true;
|
||||||
try {
|
try {
|
||||||
final response =
|
final response =
|
||||||
await http.get(Uri.http('localhost:6823', 'read-config'));
|
await http.get(Uri.http('localhost:6823', 'read-config'));
|
||||||
@ -83,6 +90,7 @@ class LoginPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SplashScreen extends StatefulWidget {
|
class SplashScreen extends StatefulWidget {
|
||||||
|
//entry point
|
||||||
@override
|
@override
|
||||||
_SplashScreenState createState() => _SplashScreenState();
|
_SplashScreenState createState() => _SplashScreenState();
|
||||||
}
|
}
|
||||||
@ -92,19 +100,24 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_checkLoginStatus();
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_checkLoginStatus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkLoginStatus() async {
|
Future<void> _checkLoginStatus() async {
|
||||||
// SharedPreferences prefs = await SharedPreferences.getInstance();
|
// SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
// print(prefs);
|
// print(prefs);
|
||||||
// bool isLoggedIn = prefs.getBool('isLoggedIn') ?? false;
|
// bool isLoggedIn = prefs.getBool('isLoggedIn') ?? false;
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
bool isLoggedIn = await _authService.isUserLoggedIn();
|
bool isLoggedIn = await _authService.isUserLoggedIn();
|
||||||
print("is loogeed in $isLoggedIn");
|
print("is logged in $isLoggedIn");
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
Navigator.pushReplacementNamed(context, '/home');
|
context.go("/home");
|
||||||
|
// Navigator.pushReplacementNamed(context, '/home');
|
||||||
} else {
|
} 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) {
|
Widget build(BuildContext context) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Scaffold(
|
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>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
Future<bool> setIp(String ip) async {
|
Future<bool> setIp(String ip) async {
|
||||||
|
//this is not done :sob: :skull:
|
||||||
// _configManager.setField("api_addr", ip);
|
// _configManager.setField("api_addr", ip);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
import 'package:crab_ui/contact.dart';
|
import 'package:crab_ui/contact.dart';
|
||||||
|
import 'package:crab_ui/email.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
import 'login.dart';
|
import 'login.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'routingHandler.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
runApp(HyM());
|
runApp(ChangeNotifierProvider(
|
||||||
|
create: (context) => AuthService(),
|
||||||
|
child: HyM(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
class HyM extends StatelessWidget {
|
class HyM extends StatelessWidget {
|
||||||
@ -15,19 +21,56 @@ class HyM extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData.light(),
|
theme: ThemeData(
|
||||||
|
colorScheme: ColorScheme.light(),
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
title: 'HyM',
|
title: 'HyM',
|
||||||
|
routerConfig: _router,
|
||||||
// home: HomeScreen(),
|
// home: HomeScreen(),
|
||||||
initialRoute: "/",
|
|
||||||
|
|
||||||
routes: {
|
// routes: {
|
||||||
"/": (context) => SplashScreen(),
|
// "/": (context) => SplashScreen(),
|
||||||
"/login": (context) => const LoginPage(),
|
// "/login": (context) => const LoginPage(),
|
||||||
"/home": (context) => HomeScreen(),
|
// "/home": (context) => HomeScreen(),
|
||||||
"/contacts": (context) => ContactsPage(),
|
// "/contacts": (context) => ContactsPage(),
|
||||||
},
|
// GoRoute(
|
||||||
|
// path:
|
||||||
|
// )
|
||||||
|
// "/email": (context) => EmailListScreen(),
|
||||||
|
// },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
229
lib/routingHandler.dart
Normal 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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]))));
|
||||||
|
}
|
||||||
|
}
|
@ -1,145 +1,3 @@
|
|||||||
import 'package:crab_ui/augment.dart';
|
export 'SonicEmailViewStub.dart'
|
||||||
import 'package:web/web.dart' as web;
|
if (dart.library.js_interop) 'SonicEmailViewWeb.dart'
|
||||||
import 'dart:ui_web' as ui;
|
if (dart.library.io) 'SonicEmailViewAndroid.dart';
|
||||||
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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
178
lib/structs.dart
@ -1,6 +1,7 @@
|
|||||||
//data structures
|
//data structures
|
||||||
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
class GetThreadResponse {
|
class GetThreadResponse {
|
||||||
final int id;
|
final int id;
|
||||||
@ -10,6 +11,7 @@ class GetThreadResponse {
|
|||||||
final String from_name;
|
final String from_name;
|
||||||
final String from_address;
|
final String from_address;
|
||||||
final List<MailAddress> to;
|
final List<MailAddress> to;
|
||||||
|
late bool seen;
|
||||||
|
|
||||||
GetThreadResponse({
|
GetThreadResponse({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -19,19 +21,20 @@ class GetThreadResponse {
|
|||||||
required this.from_name,
|
required this.from_name,
|
||||||
required this.from_address,
|
required this.from_address,
|
||||||
required this.to,
|
required this.to,
|
||||||
|
required this.seen,
|
||||||
});
|
});
|
||||||
factory GetThreadResponse.fromJson(Map<String, dynamic> json) {
|
factory GetThreadResponse.fromJson(Map<String, dynamic> json) {
|
||||||
var toList = json['to'] as List<dynamic>;
|
var toList = json['to'] as List<dynamic>;
|
||||||
|
|
||||||
return GetThreadResponse(
|
return GetThreadResponse(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
messages: List<String>.from(json['messages']),
|
messages: List<String>.from(json['messages']),
|
||||||
subject: json['subject'],
|
subject: json['subject'],
|
||||||
date: DateTime.parse(json['date']),
|
date: DateTime.parse(json['date']),
|
||||||
from_name: json['from_name'],
|
from_name: json['from_name'],
|
||||||
from_address: json['from_address'],
|
from_address: json['from_address'],
|
||||||
to: toList.map((i) => MailAddress.fromJson(i)).toList(),
|
to: toList.map((i) => MailAddress.fromJson(i)).toList(),
|
||||||
);
|
seen: json['seen']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +130,8 @@ class AttachmentInfoList extends Iterable<AttachmentInfo> {
|
|||||||
AttachmentInfoList(this._attachments);
|
AttachmentInfoList(this._attachments);
|
||||||
|
|
||||||
factory AttachmentInfoList.fromJsonList(List<Map<String, dynamic>> jsonList) {
|
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
|
@override
|
||||||
@ -143,6 +147,160 @@ class AttachmentResponse {
|
|||||||
AttachmentResponse({required this.name, required this.data});
|
AttachmentResponse({required this.name, required this.data});
|
||||||
|
|
||||||
factory AttachmentResponse.fromJson(Map<String, dynamic> json) {
|
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
|
||||||
|
522
pubspec.lock
12
pubspec.yaml
@ -12,14 +12,12 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
http: 1.2.2
|
http: 1.2.2
|
||||||
flutter_html_all: 3.0.0-beta.2
|
|
||||||
flutter_widget_from_html: ^0.10.0
|
|
||||||
shared_preferences: ^2.0.6
|
shared_preferences: ^2.0.6
|
||||||
encrypt: ^5.0.0
|
encrypt: ^5.0.0
|
||||||
pointycastle: ^3.4.0
|
pointycastle: ^3.4.0
|
||||||
mime: ^1.0.3
|
mime: ^1.0.3
|
||||||
pointer_interceptor: ^0.10.1+2
|
pointer_interceptor: ^0.10.1+2
|
||||||
file_saver: ^0.2.14
|
|
||||||
|
|
||||||
|
|
||||||
english_words: ^4.0.0
|
english_words: ^4.0.0
|
||||||
@ -28,6 +26,13 @@ dependencies:
|
|||||||
pdfrx: ^1.0.94
|
pdfrx: ^1.0.94
|
||||||
photo_view: ^0.15.0
|
photo_view: ^0.15.0
|
||||||
web: ^1.1.1
|
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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -39,6 +44,7 @@ dependency_overrides:
|
|||||||
flutter_layout_grid: 2.0.7
|
flutter_layout_grid: 2.0.7
|
||||||
flutter_math_fork: 0.7.2
|
flutter_math_fork: 0.7.2
|
||||||
|
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
assets:
|
assets:
|
||||||
|