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.
|
||||
|
||||
version:
|
||||
revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
|
||||
revision: "be698c48a6750c8cb8e61c740ca9991bb947aba2"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@ -13,26 +13,26 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
- platform: android
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
- platform: ios
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
- platform: linux
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
- platform: macos
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
- platform: web
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
- platform: windows
|
||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
||||
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
|
||||
# User provided section
|
||||
|
||||
|
14
android/.gitignore
vendored
Normal file
@ -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
|
||||
// it also builds some widgets, but it will be modulated later
|
||||
// it also builds some widgets, but it will be modulated later // chat it did
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||
import 'collapsableEmails.dart';
|
||||
|
||||
import 'structs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'dart:ui_web' as ui;
|
||||
import 'augment.dart';
|
||||
// import 'dart:html' as html;
|
||||
// import 'dart:js' as js;
|
||||
import 'package:web/web.dart' as web;
|
||||
import 'dart:js_interop' as js;
|
||||
|
||||
class ApiService {
|
||||
static String ip = "";
|
||||
static String port = "";
|
||||
static String ip = '127.0.0.1';
|
||||
static String port = "3001";
|
||||
static List<AttachmentResponse> threadAttachments =
|
||||
[]; //holds attachments of the thread
|
||||
static String currFolder = "";
|
||||
@ -59,7 +49,39 @@ class ApiService {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Future<List<GetThreadResponse>> fetchEmailsFromFolderReversed(
|
||||
String folder, int pagenitaion) async {
|
||||
try {
|
||||
var url = Uri.http('$ip:$port', 'sorted_threads_by_date_current', {
|
||||
'folder': folder,
|
||||
'limit': '50',
|
||||
'offset': pagenitaion.toString(),
|
||||
});
|
||||
|
||||
var response = await http.get(url);
|
||||
List<GetThreadResponse> allEmails = [];
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
List json = jsonDecode(response.body);
|
||||
for (var item in json) {
|
||||
//each item in the json is a date
|
||||
if (item.length > 1 && item[0] is String && item[1] is List) {
|
||||
List<int> threadIDs = List<int>.from(item[1]);
|
||||
for (var threadId in threadIDs) {
|
||||
await fetchThreads(threadId, allEmails);
|
||||
}
|
||||
}
|
||||
}
|
||||
currFolder = folder;
|
||||
return allEmails;
|
||||
} else {
|
||||
throw Exception('Failed to load threads');
|
||||
}
|
||||
} catch (e) {
|
||||
print('_displayEmailsFromFolder caught error: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Future<void> fetchThreads(
|
||||
//populates allEmails, which is the List that contains all the emails in a thread
|
||||
int threadId,
|
||||
@ -148,7 +170,6 @@ class ApiService {
|
||||
} catch (e) {
|
||||
print('_getEmailContent caught error: $e');
|
||||
}
|
||||
// return content;
|
||||
return HTMLofThread;
|
||||
}
|
||||
|
||||
@ -180,7 +201,7 @@ class ApiService {
|
||||
Future<bool> moveEmail(
|
||||
//only moves the first email of the thread //or perhaps should do the last
|
||||
String fromFolder,
|
||||
String thread_id,
|
||||
String thread_id, //uid
|
||||
String toFolder) async {
|
||||
var url = Uri.http('$ip:$port', 'move_email');
|
||||
|
||||
@ -191,16 +212,17 @@ class ApiService {
|
||||
return false;
|
||||
}
|
||||
|
||||
SerializableMessage firstMail = mailsInSerializable[0];
|
||||
|
||||
Map<String, String> requestBody = {
|
||||
'from': fromFolder,
|
||||
'uid': firstMail.uid.toString(),
|
||||
'to': toFolder,
|
||||
};
|
||||
// SerializableMessage firstMail = mailsInSerializable[0];
|
||||
|
||||
|
||||
try {
|
||||
var response = await http.post(
|
||||
for (SerializableMessage mail in mailsInSerializable) {
|
||||
Map<String, String> requestBody = {
|
||||
'from': fromFolder,
|
||||
'uid': mail.uid.toString(),
|
||||
'to': "Deleted Crabmail",
|
||||
};
|
||||
var response = await http.post(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -212,7 +234,7 @@ class ApiService {
|
||||
return true;
|
||||
} else {
|
||||
print('error ${response.statusCode} ${response.body}');
|
||||
}
|
||||
}}
|
||||
} catch (e) {
|
||||
print("failed trying to post move_email, with error: $e");
|
||||
}
|
||||
@ -351,308 +373,143 @@ class ApiService {
|
||||
return AttachmentResponse(name: "error", data: Uint8List(0));
|
||||
}
|
||||
|
||||
//TODO: MOVE THIS INTO WEB
|
||||
// Future<List<Map<String, dynamic>>> getMarkerPosition() async {
|
||||
// //this is so we can put a widget right below each email, but the way how the email content is generated
|
||||
// //leads to problems as for a) the html is added one right after the other in one iframe, b)
|
||||
// // if it was multiple iframes then the scrolling to jump would not work as expected
|
||||
Future<List<String>> fetchMarkdownContent(
|
||||
List<String> IDsString, String emailFolder) async {
|
||||
List<String> MDofThread = [];
|
||||
threadAttachments = [];
|
||||
int counter = 0;
|
||||
|
||||
// print("marker called");
|
||||
// // JavaScript code embedded as a string
|
||||
// String jsCode = '''
|
||||
// (async function waitForIframeAndMarkers() {
|
||||
// try {
|
||||
// return await new Promise((resolve) => {
|
||||
// const interval = setInterval(() => {
|
||||
// console.log("⏳ Checking for iframe...");
|
||||
// var iframe = document.getElementsByTagName('iframe')[0];
|
||||
// if (iframe && iframe.contentDocument) {
|
||||
// console.log("✅ Iframe found!");
|
||||
// var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
// var markers = iframeDoc.querySelectorAll('[id^="JuanBedarramarker"]');
|
||||
// if (markers.length > 0) {
|
||||
// console.log(`✅ Found markers in the iframe.`);
|
||||
// var positions = [];
|
||||
// markers.forEach((marker) => {
|
||||
// var rect = marker.getBoundingClientRect();
|
||||
// positions.push({
|
||||
// id: marker.id,
|
||||
// x: rect.left + window.scrollX,
|
||||
// y: rect.top + window.scrollY,
|
||||
// });
|
||||
// });
|
||||
// console.log("📌 Marker positions:", positions);
|
||||
// clearInterval(interval);
|
||||
// resolve(JSON.stringify(positions)); // Ensure proper JSON string
|
||||
// } else {
|
||||
// console.log("❌ No markers found yet.");
|
||||
// }
|
||||
// } else {
|
||||
// console.log("❌ Iframe not found or not loaded yet.");
|
||||
// }
|
||||
// }, 200);
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.error("JS Error:", error);
|
||||
// throw error; // Propagate error to Dart
|
||||
// }
|
||||
// })();
|
||||
// ''';
|
||||
try {
|
||||
//attaches email after email from a thread
|
||||
for (var id in IDsString) {
|
||||
var url = Uri.http('$ip:$port', 'email_md', {'id': id});
|
||||
print(url);
|
||||
var response = await http.get(url);
|
||||
currThread.add(id);
|
||||
if (response.statusCode == 200) {
|
||||
counter += 1;
|
||||
Map<String, dynamic> json = jsonDecode(response.body);
|
||||
|
||||
// try {
|
||||
// // Execute the JavaScript code using eval
|
||||
// // final result = await js.context.callMethod('eval', [jsCode]);
|
||||
|
||||
// if (result != null && result is String) {
|
||||
// print("Result received: $result");
|
||||
|
||||
// // Parse the JSON string returned by JavaScript into a Dart list of maps
|
||||
// final List<dynamic> parsedResult = jsonDecode(result);
|
||||
// var positions = List<Map<String, dynamic>>.from(parsedResult);
|
||||
// print("positions put on");
|
||||
// print(positions);
|
||||
// return positions;
|
||||
// } else {
|
||||
// print("result is null or not a string");
|
||||
// }
|
||||
// } catch (e, stackTrace) {
|
||||
// print("Error executing JavaScript: $e");
|
||||
// print(stackTrace);
|
||||
// }
|
||||
|
||||
// return [];
|
||||
// }
|
||||
}
|
||||
|
||||
class EmailView extends StatefulWidget {
|
||||
final List<String> emailContent;
|
||||
final String from;
|
||||
final String name;
|
||||
final String to;
|
||||
final String subject;
|
||||
final String date;
|
||||
final String id;
|
||||
final List<String> messages;
|
||||
|
||||
const EmailView({
|
||||
Key? key,
|
||||
required this.emailContent,
|
||||
required this.from,
|
||||
required this.name,
|
||||
required this.to,
|
||||
required this.subject,
|
||||
required this.date,
|
||||
required this.id,
|
||||
required this.messages,
|
||||
}) : super(key: key);
|
||||
@override
|
||||
_EmailViewState createState() => _EmailViewState();
|
||||
}
|
||||
|
||||
class _EmailViewState extends State<EmailView> {
|
||||
//html css rendering thing
|
||||
late Key iframeKey;
|
||||
late String currentContent;
|
||||
late String viewTypeId; //make this a list too???
|
||||
Future<List<Map<String, dynamic>>>? _markerPositionsFuture;
|
||||
// TextEditingController _jumpController = TextEditingController();
|
||||
final hardcodedMarkers = [
|
||||
{'id': 'marker1', 'x': 50, 'y': 100},
|
||||
{'id': 'marker2', 'x': 150, 'y': 200},
|
||||
{'id': 'marker3', 'x': 250, 'y': 300},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
print("thread id? ${widget.id}");
|
||||
List<String> currentContent = widget
|
||||
.emailContent; //html of the email/ actually entire thread, gives me little space to play in between
|
||||
// i wonder if the other attributes change? because if so i have to add like some zooms in and out of the emails, as in collapse
|
||||
// _registerViewFactory(currentContent);
|
||||
MDofThread.add(json['md'] ?? '');
|
||||
try {
|
||||
List<AttachmentInfo> attachments =
|
||||
await getAttachmentsInfo(emailFolder, id);
|
||||
for (var attachment in attachments) {
|
||||
//TODO: for each attachment creaate at the bottom a widget for each individual one
|
||||
threadAttachments
|
||||
.add(await getAttachment(emailFolder, id, attachment.name));
|
||||
}
|
||||
} catch (innerError) {
|
||||
print('_getAttachment info caught error $innerError');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('_getMDContent caught error: $e');
|
||||
}
|
||||
// print("IDS inside fetch md content $IDsString");
|
||||
// print("inside apiservice $MDofThread");
|
||||
return MDofThread;
|
||||
}
|
||||
|
||||
// void _registerViewFactory(List<String> currentContent) { // i think this doesnt work anymore
|
||||
// setState(() { //update to do item per item
|
||||
// // each item to have itsviewtype ID
|
||||
// // is this necessarey here??
|
||||
|
||||
// //could just move to collapsable
|
||||
|
||||
// viewTypeId = 'iframe-${DateTime.now().millisecondsSinceEpoch}';
|
||||
// final emailHTML = web.document.createElement('div') as web.HTMLDivElement
|
||||
// ..id = viewTypeId
|
||||
// ..innerHTML = currentContent[0].toJS; // temporarily index because it has to do all of them
|
||||
// emailHTML.style
|
||||
// ..width = '100%'
|
||||
// ..height = '100%'
|
||||
// ..overflow = 'auto'
|
||||
// ..scrollBehavior = 'smooth';
|
||||
|
||||
// ui.platformViewRegistry.registerViewFactory(
|
||||
// viewTypeId,
|
||||
// (int viewId) => emailHTML,
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
void _scrollToNumber(String spanId) {
|
||||
AugmentClasses.handleJump(spanId);
|
||||
Future<void> markAsSeen(int thread_id) async {
|
||||
try {
|
||||
var url = Uri.http(
|
||||
'$ip:$port', 'post_seen_thread', {'id': thread_id.toString()});
|
||||
var response = await http.get(url);
|
||||
if (response.statusCode == 200) {
|
||||
var result = response.body;
|
||||
print("data $result");
|
||||
}
|
||||
} catch (e) {
|
||||
print("markAsSeen failed $e");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: void _invisibility(String ) //to make purple numbers not visible
|
||||
Future<void> markAsUnseen(int thread_id) async {
|
||||
try {
|
||||
var url = Uri.http(
|
||||
'$ip:$port', 'post_unseen_thread', {'id': thread_id.toString()});
|
||||
var response = await http.get(url);
|
||||
if (response.statusCode == 200) {
|
||||
var result = response.body;
|
||||
print("data $result");
|
||||
}
|
||||
} catch (e) {
|
||||
print("markAsUnseen failed $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// print("thread id ${widget.id}");
|
||||
ApiService.currThreadID = widget.id;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.name),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
EmailToolbar(
|
||||
onJumpToSpan: _scrollToNumber,
|
||||
onButtonPressed: () => {},
|
||||
// AugmentClasses.handleJump(viewTypeId, '1');
|
||||
// print("button got pressed?");
|
||||
Future<bool> deleteEmail(String from_folder, int thread_id) async {
|
||||
// post
|
||||
try {
|
||||
List<SerializableMessage> mailsInSerializable =
|
||||
await this.threadsInSerializable(thread_id.toString());
|
||||
|
||||
// _registerViewFactory(r"""
|
||||
// <h1>Welcome to My Website</h1>
|
||||
// <p>This is a simple HTML page.</p>
|
||||
// <h2>What is HTML?</h2>
|
||||
// <p>HTML (HyperText Markup Language) is the most basic building~ block of the Web. It defines the meaning and structure of web content. Other technologies besides HTML are generally used to describe a web page's appearance/presentation (CSS) or functionality/behavior (JavaScript).</p>
|
||||
// <h3>Here's a simple list:</h3>
|
||||
// <ul>
|
||||
// <li>HTML elements are the building blocks of HTML pages</li>
|
||||
// <li>HTML uses tags like <code><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"
|
||||
if (mailsInSerializable.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
Map<String, String> requestBody = {
|
||||
"from": from_folder,
|
||||
"uid": mailsInSerializable.first.uid.toString(),
|
||||
"to": "not used"
|
||||
};
|
||||
|
||||
//
|
||||
),
|
||||
Row(
|
||||
// title of email
|
||||
children: [
|
||||
Text(
|
||||
widget.subject,
|
||||
style: TextStyle(fontSize: 30),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'from ${widget.name}',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
Text(
|
||||
'<${widget.from}>',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
'${widget.date}',
|
||||
textAlign: TextAlign.right,
|
||||
)
|
||||
],
|
||||
),
|
||||
// TODO: make a case where if one of these is the user's email it just says me :)))))
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'to ${widget.to.toString()}',
|
||||
style: TextStyle(fontSize: 15),
|
||||
)
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: CollapsableEmails(
|
||||
//change here
|
||||
thread: widget.messages, //this wont work in serializable
|
||||
threadHTML: widget.emailContent,
|
||||
threadIDs: widget.id,
|
||||
),
|
||||
),
|
||||
// Expanded(
|
||||
// child: HtmlElementView(
|
||||
// key: UniqueKey(),
|
||||
// viewType: viewTypeId,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
//delete the email that is given to the
|
||||
var url = Uri.http("$ip:$port", 'delete_email');
|
||||
var response = await http.post(url,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: jsonEncode(requestBody));
|
||||
if (response.statusCode == 200) {
|
||||
print("response body: ${response.body}");
|
||||
return true;
|
||||
} else {
|
||||
print("not 200: ${response.body}");
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
print("error in deleteEmail $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay widgets dynamically based on marker positions
|
||||
// FutureBuilder<List<Map<String, dynamic>>>(
|
||||
// future: _markerPositionsFuture,
|
||||
// builder: (context, snapshot) {
|
||||
// print("FutureBuilder state: ${snapshot.connectionState}");
|
||||
// if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
// return Center(child: CircularProgressIndicator());
|
||||
// }
|
||||
// if (snapshot.hasError) {
|
||||
// print("Error in FutureBuilder: ${snapshot.error}");
|
||||
// return Center(child: Text('error loading markers'));
|
||||
// }
|
||||
// if (snapshot.hasData && snapshot.data != null) {
|
||||
// final markers = snapshot.data!;
|
||||
// return Stack(
|
||||
// children: markers.map((marker) {
|
||||
// return Positioned(
|
||||
// left: marker['x'].toDouble(),
|
||||
// top: marker['y'].toDouble(),
|
||||
// child: GestureDetector(
|
||||
// onTap: () {
|
||||
// print('Tapped on ${marker['id']}');
|
||||
// },
|
||||
// child: Container(
|
||||
// width: 50,
|
||||
// height: 50,
|
||||
// color: Colors.red,
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// marker['id'],
|
||||
// style: TextStyle(color: Colors.white),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }).toList(),
|
||||
// );
|
||||
// }
|
||||
Future<bool> sendEmail(
|
||||
String? to, String? subject, String? emailContent) async {
|
||||
try {
|
||||
var url = Uri.http('$ip:$port', 'send_email');
|
||||
|
||||
// return SizedBox.shrink(); // No markers found
|
||||
// },
|
||||
// ),
|
||||
// Red widget overlay
|
||||
// Positioned(
|
||||
// left: 8, // Adjust based on your desired position
|
||||
// top: 100 + 44 + 5, // Adjust based on your desired position
|
||||
// child: IgnorePointer(
|
||||
// ignoring: true, // Ensures the iframe remains interactive
|
||||
// child: Container(
|
||||
// color: Colors.red,
|
||||
// width: 100,
|
||||
// height: 50,
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// 'Overlay',
|
||||
// style: TextStyle(color: Colors.white),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
));
|
||||
Map<String, dynamic> requestBody = {
|
||||
"to": [to ?? ""],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": subject ?? "Untitled",
|
||||
"in_reply_to": "",
|
||||
"messages": [
|
||||
{"message": emailContent ?? "", "is_html": false}
|
||||
],
|
||||
"attachments": [],
|
||||
"inline_images": [],
|
||||
};
|
||||
var response = await http.post(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(requestBody),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
print("response body: ${response.body}");
|
||||
} else {
|
||||
print('error: ${response.statusCode}, response body: ${response.body}');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
print("error in post send email $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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;
|
||||
import 'package:web/web.dart' as web;
|
||||
import 'dart:io';
|
||||
import 'structs.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
|
||||
class Attachmentdownload {
|
||||
Future<void> saveFile(AttachmentResponse attachment) async {
|
||||
await FileSaver.instance.saveFile(
|
||||
name: attachment.name.toString().substring(0, attachment.name.toString().lastIndexOf('.')),
|
||||
bytes: attachment.data,
|
||||
ext: attachment.name.toString().substring(attachment.name.toString().lastIndexOf('.')+1)
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'attachamentDownloadStub.dart'
|
||||
if (dart.library.io) 'attachmentDownloadAndroid.dart';
|
||||
// if (dart.library.js_interop) 'attachmentDownloadWeb.dart';
|
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";
|
||||
|
||||
import "package:crab_ui/attachmentDownload.dart";
|
||||
import "package:crab_ui/structs.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:pdfrx/pdfrx.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
class AttachmentWidget extends StatelessWidget {
|
||||
final AttachmentResponse attachment;
|
||||
AttachmentWidget({required this.attachment});
|
||||
|
||||
Widget attachmentViewer(AttachmentResponse att) {
|
||||
String extension = att.name
|
||||
.toString()
|
||||
.substring(att.name.toString().indexOf(".") + 1)
|
||||
.toLowerCase();
|
||||
if (extension == "jpg" || extension == "png") {
|
||||
return Image.memory(att.data);
|
||||
} else if (extension == "pdf") {
|
||||
return PdfViewer.data(Uint8List.fromList(att.data),
|
||||
sourceName: att.name,
|
||||
params: PdfViewerParams(
|
||||
enableTextSelection: true,
|
||||
scrollByMouseWheel: 0.5,
|
||||
annotationRenderingMode:
|
||||
PdfAnnotationRenderingMode.annotationAndForms,
|
||||
));
|
||||
}
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff6C63FF),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"No preview available",
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 18, decoration: TextDecoration.none),
|
||||
),
|
||||
SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
GestureDetector(
|
||||
child: ElevatedButton(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Download", style: TextStyle(color: Color(0xff2c3e50)),),
|
||||
Icon(Icons.download,
|
||||
color: Color(0xffb0b0b0),),
|
||||
]),
|
||||
onPressed: () => Attachmentdownload().saveFile(att),
|
||||
)),
|
||||
]),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black38,
|
||||
child: Stack(children: <Widget>[
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(10, 20, 0, 10),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CloseButton(onPressed: () => {Navigator.pop(context)}),
|
||||
Text(
|
||||
attachment.name
|
||||
.toString(), //its alr a string but incase ¯\(ツ)/¯ //update: i did that everywhere lol
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 20,
|
||||
decoration: TextDecoration
|
||||
.none), //TODO: personalize your fonts
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: attachmentViewer(attachment),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]));
|
||||
}
|
||||
}
|
||||
export 'attachmentWidgetStub.dart'
|
||||
if (dart.library.js_interop) 'attachmentWidgetWeb.dart'
|
||||
if (dart.library.io) 'attachmentWidgetAndroid.dart';
|
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/attachmentDownload.dart';
|
||||
import 'package:crab_ui/collapsableEmails.dart';
|
||||
import 'package:crab_ui/structs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pdfrx/pdfrx.dart';
|
||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||
// import 'dart:html' as html;
|
||||
// import 'dart:js' as js;
|
||||
import 'package:web/web.dart' as web;
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||
import 'attachmentWidget.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'routingHandler.dart';
|
||||
|
||||
class EmailToolbar extends StatefulWidget {
|
||||
final Function(String) onJumpToSpan;
|
||||
final Function(String) onJumpToNumbering;
|
||||
final Function(String) onViewspecs;
|
||||
final VoidCallback onButtonPressed;
|
||||
final Function(String) onFiltering;
|
||||
final List<String> emails;
|
||||
final String subject;
|
||||
late AugmentTree? rootAugment;
|
||||
|
||||
EmailToolbar(
|
||||
{Key? key, required this.onButtonPressed, required this.onJumpToSpan})
|
||||
: super(key: key);
|
||||
EmailToolbar({
|
||||
Key? key,
|
||||
required this.onButtonPressed,
|
||||
required this.onJumpToNumbering,
|
||||
required this.onViewspecs,
|
||||
required this.onFiltering,
|
||||
required this.emails,
|
||||
required this.subject,
|
||||
required this.rootAugment,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_DynamicClassesAugment createState() => _DynamicClassesAugment();
|
||||
@ -26,7 +35,10 @@ class EmailToolbar extends StatefulWidget {
|
||||
|
||||
class _DynamicClassesAugment extends State<EmailToolbar> {
|
||||
String selectedClass = 'Class 1';
|
||||
// TextEditingController _jumpController = TextEditingController();
|
||||
TextEditingController _jumpController = TextEditingController();
|
||||
TextEditingController _viewspecsController = TextEditingController();
|
||||
AugmentTree? localAugment;
|
||||
List<SerializableMessage>? emailsInThread;
|
||||
|
||||
// late final FocusNode _JumpItemfocusNode;
|
||||
// late final FocusNode _viewSpecsfocusNode;
|
||||
@ -47,13 +59,25 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
||||
// _viewSpecsfocusNode.addListener(() {
|
||||
// setState(() => _viewSpecsHasFocus = _viewSpecsfocusNode.hasFocus);
|
||||
// });
|
||||
localAugment = widget.rootAugment;
|
||||
_serializableData(widget.emails);
|
||||
}
|
||||
|
||||
void _serializableData(List<String> threadID) async {
|
||||
// emailsInThread = await ApiService().threadsInSerializable();
|
||||
print("done thread serializable");
|
||||
|
||||
if (!mounted) return;
|
||||
// setState(() {
|
||||
// _isLoaded = true;
|
||||
// });
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// _JumpItemfocusNode.dispose();
|
||||
// _viewSpecsfocusNode.dispose();
|
||||
// _jumpController.dispose();
|
||||
_jumpController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -78,20 +102,20 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
||||
child: Text('Attachments'),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: AugmentClasses.handleOpen,
|
||||
child: Text('Open'),
|
||||
),
|
||||
// ElevatedButton(
|
||||
// onPressed: AugmentClasses.handleOpen,
|
||||
// child: Text('Open'),
|
||||
// ),
|
||||
// SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: AugmentClasses.handleFind,
|
||||
child: Text('Find'),
|
||||
),
|
||||
// SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: AugmentClasses.handleStop,
|
||||
child: Text('Stop'),
|
||||
),
|
||||
// ElevatedButton(
|
||||
// onPressed: AugmentClasses.handleStop,
|
||||
// child: Text('Stop'),
|
||||
// ),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
AugmentClasses.handleMove(context);
|
||||
@ -138,10 +162,10 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
||||
// width: 8,
|
||||
// ),
|
||||
Container(
|
||||
width: 50,
|
||||
width: 100,
|
||||
height: 30,
|
||||
child: TextField(
|
||||
// controller: _jumpController,
|
||||
controller: _jumpController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
// suffixIcon: Icon(Icons.search)
|
||||
@ -149,7 +173,7 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
||||
onSubmitted: (value) {
|
||||
print("onSubmitted");
|
||||
if (value.isNotEmpty) {
|
||||
widget.onJumpToSpan(value);
|
||||
widget.onJumpToNumbering(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -186,18 +210,23 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
||||
onPressed: () => AugmentClasses.ViewSpecsButton(context),
|
||||
child: Text('ViewSpecs:')),
|
||||
Container(
|
||||
width: 50,
|
||||
width: 100,
|
||||
height: 30,
|
||||
child: TextField(
|
||||
controller: _viewspecsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '',
|
||||
border: OutlineInputBorder(),
|
||||
// suffixIcon: Icon(Icons.style_rounded)
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
widget.onViewspecs(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => AugmentClasses.FilterButton(context),
|
||||
onPressed: () =>
|
||||
AugmentClasses().filterButton(context, widget.onFiltering),
|
||||
child: Text('Filter'),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
@ -207,11 +236,14 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
||||
),
|
||||
// SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: AugmentClasses.handleFind,
|
||||
onPressed: () => AugmentClasses()
|
||||
.handleCreateLink(context, widget.emails, widget.subject, widget.emails[0]), //need to add the email ids
|
||||
child: Text('Create Link'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: AugmentClasses.handleFind,
|
||||
// onPressed: () => localAugment!.handlePaste(context),
|
||||
onPressed: () =>
|
||||
AugmentClasses().handlePaste(context),
|
||||
child: Text('Paste Link'),
|
||||
),
|
||||
],
|
||||
@ -222,6 +254,18 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
||||
}
|
||||
|
||||
class AugmentClasses {
|
||||
CollapsableEmails? localCollapsable;
|
||||
String? nameOfDocument;
|
||||
AugmentTree? rootTree;
|
||||
|
||||
void setRootTree(AugmentTree aTree) {
|
||||
rootTree = aTree;
|
||||
}
|
||||
|
||||
// AugmentClasses(CollapsableEmails localCollapsable) {
|
||||
// localCollapsable = localCollapsable;
|
||||
// }
|
||||
|
||||
static OverlayEntry? _overlayEntry;
|
||||
static String? selectedFolder; // Manage selected folder at the class level
|
||||
|
||||
@ -487,45 +531,236 @@ class AugmentClasses {
|
||||
print("Find button pressed");
|
||||
}
|
||||
|
||||
AugmentTree? _findAugmentNode(String target, AugmentTree node, int index) {
|
||||
// so the ideqa is that since the numbering its quite linear, meaning that it tells you where it goes,
|
||||
// thus i've thought the amount of max moves are only the length of the string of the target
|
||||
//e.g. if we have a target of 1e9, its steps are the same or time complexity as if it were 1a1, or 99z99
|
||||
// since each number or letter tells us which is the index in this array, genius ik
|
||||
// thus first one needs another function from converting from alphabetical to numbers
|
||||
if (node.numbering[index] == target[index]) {
|
||||
_findAugmentNode(target, node.children[index++], index++);
|
||||
}
|
||||
}
|
||||
|
||||
bool _checkValidTarget(String target) {
|
||||
target = target.trim();
|
||||
//find if the target exists,
|
||||
//recursive?
|
||||
_findAugmentNode(target, rootTree!, 0);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_copyLink(String anchor, String target, String format, String viewspecs,
|
||||
String nameOfDocument, emailID) {
|
||||
String form = "$anchor < $nameOfDocument, $target :$viewspecs > $emailID";
|
||||
final link = ClipboardData(text: form);
|
||||
Clipboard.setData(link);
|
||||
}
|
||||
|
||||
Future<void> handleCreateLink(BuildContext context,
|
||||
List<String> emailsInThread, String nameOfDocument, String emailID) async {
|
||||
print("create link button pressed");
|
||||
final TextEditingController targetController = TextEditingController();
|
||||
final TextEditingController anchorController = TextEditingController();
|
||||
final TextEditingController viewspecsController = TextEditingController();
|
||||
final TextEditingController formatController = TextEditingController();
|
||||
|
||||
// String anchorPhrase = '';
|
||||
String format = 'augment';
|
||||
// String target = '';
|
||||
// String viewspecs = '';
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Create URL Link'),
|
||||
content: SizedBox(
|
||||
height: 400,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text("Which email? "),
|
||||
SizedBox(
|
||||
width: 350.0,
|
||||
child: Text(nameOfDocument),
|
||||
// child: ListView.builder(
|
||||
// itemCount: emailsInThread.length,
|
||||
// itemBuilder: (context, index) {
|
||||
// // var item = emailsInThread[index];
|
||||
// // ApiService().
|
||||
// return ListTile(
|
||||
// title: Text(nameOfDocument),
|
||||
// );
|
||||
// }),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text("Link to target item at: "),
|
||||
SizedBox(
|
||||
width: 350.0,
|
||||
child: TextField(
|
||||
controller: targetController,
|
||||
autofocus: true,
|
||||
maxLines: 1,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => ViewSpecsButton(context),
|
||||
child: Text("Viewspecs:")),
|
||||
SizedBox(
|
||||
width: 150.0,
|
||||
child: TextField(
|
||||
controller: viewspecsController,
|
||||
maxLines: 1,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Text("Using anchor phrase: "),
|
||||
SizedBox(
|
||||
width: 150.0,
|
||||
child: TextField(
|
||||
controller: anchorController,
|
||||
maxLines: 1,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Text("Using link format: "),
|
||||
SizedBox(
|
||||
width: 250.0,
|
||||
child: Text("Augment"),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => {
|
||||
// _checkValidTarget(targetController.text),
|
||||
|
||||
_copyLink(
|
||||
anchorController.text,
|
||||
targetController.text,
|
||||
format,
|
||||
viewspecsController.text,
|
||||
nameOfDocument,
|
||||
emailID,
|
||||
),
|
||||
Navigator.of(context).pop()
|
||||
},
|
||||
child: Text("OK")),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text("Cancel")),
|
||||
ElevatedButton(onPressed: null, child: Text("Help")),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> handlePaste(BuildContext context) async {
|
||||
final TextEditingController gotoLink = TextEditingController();
|
||||
|
||||
Routinghandler localRouting;
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text("GOTO Link"),
|
||||
content: SizedBox(
|
||||
height: 400,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text("Paste link to go: "),
|
||||
SizedBox(
|
||||
width: 350,
|
||||
child: TextField(
|
||||
controller: gotoLink,
|
||||
maxLines: 1,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
print('pressed');
|
||||
Navigator.of(context).pop();
|
||||
final localRouting =
|
||||
Routinghandler(gotoLink.text);
|
||||
final String subject =
|
||||
localRouting.docName; // This is your :subject
|
||||
final String target =
|
||||
localRouting.target; // This is your :target
|
||||
final String viewspecs =
|
||||
localRouting.viewspecs; // This is your :viewspecs
|
||||
final String finalEmailID = localRouting.emailID;
|
||||
|
||||
final encodedSubject = Uri.encodeComponent(subject);
|
||||
final encodedTarget = Uri.encodeComponent(target);
|
||||
final encodedViewspecs = Uri.encodeComponent(viewspecs);
|
||||
final encodedEmailID = Uri.encodeComponent(finalEmailID);
|
||||
print("emailID $encodedEmailID");
|
||||
String link =
|
||||
"/email/$encodedSubject/$encodedTarget/$encodedViewspecs/$encodedEmailID";
|
||||
GoRouter.of(context).go(link);
|
||||
},
|
||||
child: Text("OK")),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text("Cancel"))
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
static void handleStop() {
|
||||
print("Stop button pressed");
|
||||
}
|
||||
|
||||
static void handleJump(String spanId) {
|
||||
String js_code = '''
|
||||
var iframe = document.getElementsByTagName('iframe')[0]; // 0 for the first iframe, 1 for the second, etc.
|
||||
|
||||
// Check if the iframe is loaded and has content
|
||||
if (iframe && iframe.contentDocument) {
|
||||
// Access the document inside the iframe
|
||||
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
|
||||
// Find the element with the specific id inside the iframe
|
||||
var targetElement = iframeDoc.getElementById("$spanId"); // Replace '36 ' with the actual id of the target element
|
||||
|
||||
// If the element exists, scroll to it
|
||||
if (targetElement) {
|
||||
targetElement.scrollIntoView();
|
||||
console.log('Scrolled to element with id "$spanId" inside the iframe.');
|
||||
} else {
|
||||
console.log('Element with id "$spanId" not found inside the iframe.');
|
||||
}
|
||||
} else {
|
||||
console.log('Iframe not found or not loaded.');
|
||||
}
|
||||
''';
|
||||
// js.context.callMethod('eval', [js_code]);
|
||||
static void handleJump(String value) {
|
||||
print(value);
|
||||
}
|
||||
|
||||
static void invisibility(String htmlClass) {}
|
||||
|
||||
static Future<void> JumpButton(BuildContext context) async {
|
||||
// FocusNode textFieldFocusNode = FocusNode();
|
||||
|
||||
// AugmentClasses.disableIframePointerEvents();
|
||||
await showDialog(
|
||||
barrierDismissible: true,
|
||||
// barrierColor: Colors.yellow,
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Jump Item:'),
|
||||
@ -725,7 +960,6 @@ class AugmentClasses {
|
||||
ElevatedButton(onPressed: () {}, child: Text('OK')),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// AugmentClasses.disableIframePointerEvents();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text('Cancel')),
|
||||
@ -743,61 +977,90 @@ class AugmentClasses {
|
||||
});
|
||||
}
|
||||
|
||||
void handleFilter() {}
|
||||
static Future<void> FilterButton(context) async {
|
||||
Future<List<AugmentTree>> searchFilter(String query) async {
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<void> filterButton(
|
||||
context, Function(String) onFilteringCallback) async {
|
||||
//this is literally ctrl+F :skull:
|
||||
//idea is to search in file, extract the <p> tags that contain these
|
||||
//words and highlight, then when zoom, you just jump to that paragraph
|
||||
bool? numbering = false;
|
||||
String filterQueue = '';
|
||||
|
||||
// AugmentClasses.disableIframePointerEvents();
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
height: 150,
|
||||
width: 300,
|
||||
child: AlertDialog(
|
||||
title: Text('Filter'),
|
||||
content: Container(
|
||||
width: 400, // Set the width to simulate the Windows style
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Set filter:'),
|
||||
SizedBox(
|
||||
width: 175,
|
||||
child: TextField(
|
||||
maxLines: 1,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return StatefulBuilder(builder:
|
||||
(BuildContext statefulBuilderContext, StateSetter setState) {
|
||||
return AlertDialog(
|
||||
title: const Text('Filter'),
|
||||
content: SizedBox(
|
||||
width: 400, // Set the width to simulate the Windows style
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Set filter:'),
|
||||
SizedBox(
|
||||
width: 175,
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
maxLines: 1,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
)
|
||||
],
|
||||
)))));
|
||||
onChanged: (value) {
|
||||
print(value);
|
||||
filterQueue = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Column(children: [
|
||||
Row(children: [
|
||||
Checkbox(
|
||||
value: numbering,
|
||||
activeColor:
|
||||
Theme.of(context).colorScheme.tertiary,
|
||||
onChanged: (newBool) {
|
||||
setState(() {
|
||||
numbering = newBool;
|
||||
});
|
||||
}),
|
||||
Text("Start at top of file")
|
||||
]),
|
||||
]),
|
||||
])),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text("Cancel")),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop({
|
||||
'filterQueue': filterQueue,
|
||||
'numbering': numbering,
|
||||
});
|
||||
},
|
||||
child: Text("Apply")),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
).then((result) {
|
||||
if (result != null) {
|
||||
print("filter done $result");
|
||||
final String query = result['filterQueue'];
|
||||
onFilteringCallback(query);
|
||||
} else {
|
||||
print('cancelled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// static void disableIframePointerEvents() {
|
||||
// //pretty sure these dont work
|
||||
// // final iframes = html.document.getElementsByTagName('iframe');
|
||||
// final iframes = web.document.getElementsByTagName('iframe');
|
||||
// for (var iframe in iframes) {
|
||||
// // if (iframe is html.Element) {
|
||||
// // iframe.style.pointerEvents = 'none'; // Disable pointer events
|
||||
// if (iframe is web.Element) {
|
||||
// iframe.
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// static void enableIframePointerEvents() {
|
||||
// // final iframes = html.document.getElementsByTagName('iframe');
|
||||
// final iframes = html.document.getElementsByTagName('iframe');
|
||||
|
||||
// for (var iframe in iframes) {
|
||||
// if (iframe is html.Element) {
|
||||
// iframe.style.pointerEvents = 'auto'; // Re-enable pointer events
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
@ -1,132 +1,3 @@
|
||||
import 'dart:js_interop';
|
||||
import 'package:web/web.dart' as web;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:ui_web' as ui;
|
||||
import 'api_service.dart';
|
||||
import 'structs.dart';
|
||||
|
||||
class CollapsableEmails extends StatefulWidget {
|
||||
final List<String> thread; // email id's in the form xyz@gmail.com
|
||||
final List<String> threadHTML;
|
||||
final String threadIDs;
|
||||
|
||||
CollapsableEmails(
|
||||
{required this.thread,
|
||||
required this.threadHTML,
|
||||
required this.threadIDs});
|
||||
|
||||
@override
|
||||
State<CollapsableEmails> createState() => _CollapsableEmailsState();
|
||||
}
|
||||
|
||||
class _CollapsableEmailsState extends State<CollapsableEmails> {
|
||||
List<String> emailsHTML = []; //html of the emails in the thread
|
||||
// build attachments with the forldar name and id
|
||||
Set<int> _expandedEmails = {}; //open emails
|
||||
List viewtypeIDs = []; //IDs of the viewtypes, order matters
|
||||
List heightOfViewTypes = []; //the height of each viewtype
|
||||
List<SerializableMessage> emailsInThread = [];
|
||||
bool _isLoaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
_registerViewFactory(widget.threadHTML);
|
||||
_serializableData(widget.threadIDs);
|
||||
}
|
||||
|
||||
void _registerViewFactory(List<String> currentContent) async {
|
||||
// setState(() { //update to do item per item
|
||||
// each item to have itsviewtype ID
|
||||
// is this necessarey here??
|
||||
|
||||
//could just move to collapsable
|
||||
|
||||
for (var emailHTML in widget.threadHTML) {
|
||||
String viewTypeId = 'email-${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
final ghost = web.document.createElement('div') as web.HTMLDivElement
|
||||
..style.visibility = 'hidden'
|
||||
..style.position = 'absolute'
|
||||
..style.width = '100%'
|
||||
..style.overflow = 'auto'
|
||||
..innerHTML = emailHTML
|
||||
.toJS; // temporarily index because it has to do all of them
|
||||
web.document.body?.append(ghost);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
|
||||
final heightOfEmail = ghost.scrollHeight;
|
||||
ghost.remove();
|
||||
|
||||
final HTMLsnippet = web.document.createElement('div')
|
||||
as web.HTMLDivElement
|
||||
..id = viewTypeId
|
||||
..innerHTML = emailHTML
|
||||
.toJS; // temporarily index because it has to do all of them
|
||||
HTMLsnippet.style
|
||||
..width = '100%'
|
||||
..height = '${heightOfEmail}px'
|
||||
..overflow = 'auto'
|
||||
..scrollBehavior = 'smooth';
|
||||
|
||||
ui.platformViewRegistry.registerViewFactory(
|
||||
viewTypeId,
|
||||
(int viewId) => HTMLsnippet,
|
||||
);
|
||||
viewtypeIDs.add(viewTypeId);
|
||||
heightOfViewTypes.add(heightOfEmail);
|
||||
}
|
||||
}
|
||||
|
||||
void _serializableData(String threadID) async {
|
||||
emailsInThread = await ApiService().threadsInSerializable(threadID);
|
||||
print("done thread serializable");
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _isLoaded
|
||||
?Column(children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: widget.thread.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isExpanded =
|
||||
_expandedEmails.contains(index); //check if email is expanded
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(emailsInThread[index].from),
|
||||
trailing: Text(emailsInThread[index].date),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (isExpanded) {
|
||||
_expandedEmails.remove(index);
|
||||
} else {
|
||||
_expandedEmails.add(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
if (isExpanded)
|
||||
// if(viewtypeIDs[index] == null || heightOfViewTypes[index] == null)
|
||||
// const SizedBox(height: 100, child: Center(child: CircularProgressIndicator())),
|
||||
SizedBox(
|
||||
height: heightOfViewTypes[index].toDouble(),
|
||||
child: HtmlElementView(
|
||||
key: UniqueKey(), viewType: viewtypeIDs[index]),
|
||||
),
|
||||
Divider(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
]): const Center(child:CircularProgressIndicator());
|
||||
}
|
||||
}
|
||||
export 'collapsableEmailsStub.dart'
|
||||
if (dart.library.io) 'collapsableEmailsAndroid.dart'
|
||||
if (dart.library.js_interop) 'collapsableEmailsWeb.dart';
|
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:http/http.dart' as http;
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
// import 'package:http/http.dart' as http;
|
||||
// import 'package:flutter_html/flutter_html.dart';
|
||||
|
||||
class ContactsPage extends StatefulWidget {
|
||||
const ContactsPage({super.key});
|
||||
|
293
lib/email.dart
@ -1,57 +1,225 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'api_service.dart';
|
||||
import 'structs.dart';
|
||||
import 'emailView.dart';
|
||||
import 'Compose.dart';
|
||||
|
||||
class EmailListScreen extends StatelessWidget {
|
||||
class EmailListScreen extends StatefulWidget {
|
||||
final List<GetThreadResponse> emails;
|
||||
final Future<List<String>> Function(List<String>, String) getEmailContent;
|
||||
final String folder;
|
||||
final GlobalKey<_EmailListScreenState> key;
|
||||
final Function(List<GetThreadResponse>)? onSelectionChanged;
|
||||
|
||||
EmailListScreen({
|
||||
required this.key,
|
||||
required this.emails,
|
||||
required this.getEmailContent,
|
||||
required this.folder,
|
||||
this.onSelectionChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_EmailListScreenState createState() => _EmailListScreenState();
|
||||
}
|
||||
|
||||
class _EmailListScreenState extends State<EmailListScreen>
|
||||
with TickerProviderStateMixin {
|
||||
late List<bool> selectStates; // for checkboxes if its selected or not
|
||||
late List<GetThreadResponse> selectedEmails =
|
||||
[]; // holds the emails that are selected i.e. the emails that got the checkbox on
|
||||
final Set<int> _hoveredRows = {}; //the row that is being hovered over atm
|
||||
bool bulkSelectMenu = false;
|
||||
final GlobalKey<EmailPageState> _emailPageKey = GlobalKey<EmailPageState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectStates = List<bool>.filled(widget.emails.length, false);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant EmailListScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.emails.length != widget.emails.length) {
|
||||
selectStates = List<bool>.filled(widget.emails.length, false);
|
||||
}
|
||||
}
|
||||
|
||||
bool selectAllChecks(bool selectionType) {
|
||||
//perhaps it should return a list of the selected
|
||||
setState(() {
|
||||
selectedEmails = [];
|
||||
if (selectionType) {
|
||||
bulkSelectMenu = true;
|
||||
for (int email = 0; email < selectStates.length; email++) {
|
||||
selectStates[email] = selectionType;
|
||||
selectedEmails.add(widget.emails[email]);
|
||||
}
|
||||
} else {
|
||||
for (int email = 0; email < selectStates.length; email++) {
|
||||
selectStates[email] = selectionType;
|
||||
}
|
||||
selectedEmails = [];
|
||||
}
|
||||
});
|
||||
widget.onSelectionChanged?.call(selectedEmails);
|
||||
printTheSelected();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool markAsRead(bool read) {
|
||||
print("markasread $read");
|
||||
setState(() {
|
||||
if (read) {
|
||||
//read
|
||||
for (int email = 0; email < selectedEmails.length; email++) {
|
||||
selectedEmails[email].seen = read;
|
||||
ApiService()
|
||||
.markAsSeen(selectedEmails[email].id); //the remote or .json
|
||||
}
|
||||
} else {
|
||||
//unread
|
||||
for (int email = 0; email < selectedEmails.length; email++) {
|
||||
selectedEmails[email].seen = read;
|
||||
ApiService()
|
||||
.markAsUnseen(selectedEmails[email].id); //the remote or .json
|
||||
print(selectedEmails[email].subject);
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
bool moveOfSelected(String destinyFolder) {
|
||||
//this should be called from a widget
|
||||
print("move of folder");
|
||||
setState(() {
|
||||
for (int email = 0; email < selectedEmails.length; email++) {
|
||||
ApiService().moveEmail(
|
||||
widget.folder, selectedEmails[email].id.toString(), destinyFolder);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Widget moveOfFolderWidget()
|
||||
|
||||
List<GetThreadResponse> listOfSelectedThreads() {
|
||||
return selectedEmails;
|
||||
}
|
||||
|
||||
void printTheSelected() {
|
||||
for (int i = 0; i < selectedEmails.length; i++) {
|
||||
print(selectedEmails[i].subject);
|
||||
}
|
||||
}
|
||||
|
||||
EmailListScreen(
|
||||
{required this.emails,
|
||||
required this.getEmailContent,
|
||||
required this.folder});
|
||||
//fix the email list
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ListView.separated(
|
||||
itemCount: emails.length,
|
||||
itemCount: widget.emails.length,
|
||||
itemBuilder: (context, index) {
|
||||
final email = emails[index];
|
||||
return ListTile(
|
||||
title: Text(email.from_name,
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [Text(email.subject)],
|
||||
),
|
||||
trailing: Text(email.date.toString()),
|
||||
onTap: () async {
|
||||
List<String> emailContent = // list of the html
|
||||
await getEmailContent(email.messages, folder);
|
||||
// print("this is what email.messages look like in email.dart ${email.messages}");
|
||||
// List<String> emailIds = email.messages;
|
||||
Color seenColour;
|
||||
final email = widget.emails[index];
|
||||
if (email.seen) {
|
||||
seenColour = ThemeData().highlightColor;
|
||||
} else {
|
||||
seenColour = Colors.transparent;
|
||||
}
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _hoveredRows.add(index)),
|
||||
onExit: (_) => setState(() => _hoveredRows.remove(index)),
|
||||
child: ListTile(
|
||||
leading: Checkbox(
|
||||
value: selectStates[index],
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
//works great
|
||||
selectStates[index] = value ?? false;
|
||||
|
||||
print(email.messages); //email ids of the thread
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
// could call collapsable and inside collable each calls email view?
|
||||
builder: (context) => EmailView(
|
||||
emailContent: emailContent,
|
||||
from: email.from_address,
|
||||
name: email.from_name,
|
||||
to: email.to.toString(),
|
||||
subject: email.subject,
|
||||
date: email.date.toString(),
|
||||
id: email.id.toString(), //i think this is thread id?
|
||||
messages: email.messages,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
setState(() {
|
||||
if (value!) {
|
||||
selectedEmails.add(widget.emails[index]);
|
||||
//here i must update the other side
|
||||
_emailPageKey.currentState?.getListOfSelected();
|
||||
} else {
|
||||
selectedEmails.remove(widget.emails[index]);
|
||||
_emailPageKey.currentState?.getListOfSelected();
|
||||
}
|
||||
widget.onSelectionChanged?.call(selectedEmails);
|
||||
print(selectedEmails);
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
title: Text(email.from_name,
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [Text(email.subject)],
|
||||
),
|
||||
trailing: _hoveredRows.contains(index)
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.mark_email_read_outlined),
|
||||
onPressed: () {
|
||||
// mark email as read
|
||||
setState(() {
|
||||
widget.emails[index].seen = true;
|
||||
ApiService().markAsSeen(email.id);
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_outline),
|
||||
onPressed: () {
|
||||
// delete email
|
||||
ApiService().deleteEmail(widget.folder, email.id);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(email.date.toString()),
|
||||
hoverColor: Colors.transparent,
|
||||
tileColor: seenColour,
|
||||
onTap: () async {
|
||||
List<String> emailContent = // list of the html
|
||||
await widget.getEmailContent(email.messages, widget.folder);
|
||||
// print("thread id? $email.id"); yes
|
||||
print(email.messages); //email ids of the thread
|
||||
if (widget.folder == "Drafts") {
|
||||
print("IN DRAFTS MOVE THE CONTENT TO THE WRITING THING");
|
||||
//open the compose
|
||||
OverlayService _thisInstance = OverlayService();
|
||||
_thisInstance.draftID = email;
|
||||
_thisInstance.showPersistentWidget(context);
|
||||
} else {
|
||||
// print(email)
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
// could call collapsable and inside collable each calls email view?
|
||||
builder: (context) => EmailView(
|
||||
emailContent: emailContent,
|
||||
from: email.from_address,
|
||||
name: email.from_name,
|
||||
to: email.to.toString(),
|
||||
subject: email.subject,
|
||||
date: email.date.toString(),
|
||||
id: email.id.toString(), //i think this is thread id?
|
||||
messages: email.messages,
|
||||
),
|
||||
),
|
||||
);
|
||||
ApiService().markAsSeen(email.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Divider(),
|
||||
@ -62,10 +230,12 @@ class EmailListScreen extends StatelessWidget {
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class EmailPage extends StatefulWidget {
|
||||
EmailPage({Key? key}) : super(key: key);
|
||||
String selectedFolder = "INBOX"; //starter
|
||||
int offset = 0;
|
||||
int page = 1;
|
||||
final Function(List<GetThreadResponse>)? onSelectionChanged;
|
||||
|
||||
EmailPage({Key? key, this.onSelectionChanged}) : super(key: key);
|
||||
|
||||
@override
|
||||
EmailPageState createState() => EmailPageState();
|
||||
@ -78,6 +248,9 @@ class EmailPageState extends State<EmailPage> {
|
||||
int page = 1;
|
||||
bool isBackDisabled = false;
|
||||
|
||||
final GlobalKey<_EmailListScreenState> emailListKey =
|
||||
GlobalKey<_EmailListScreenState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -86,6 +259,7 @@ class EmailPageState extends State<EmailPage> {
|
||||
_fetchEmails();
|
||||
}
|
||||
|
||||
List<GetThreadResponse> get getEmails => emails;
|
||||
String getPage() => widget.page.toString();
|
||||
bool get backDisabled => isBackDisabled;
|
||||
|
||||
@ -116,7 +290,6 @@ class EmailPageState extends State<EmailPage> {
|
||||
}
|
||||
});
|
||||
}
|
||||
// print(currentPage);
|
||||
print(widget.page);
|
||||
_fetchEmails();
|
||||
}
|
||||
@ -124,7 +297,7 @@ class EmailPageState extends State<EmailPage> {
|
||||
void _fetchEmails() async {
|
||||
try {
|
||||
List<GetThreadResponse> fetchedEmails = await apiService
|
||||
.fetchEmailsFromFolder(widget.selectedFolder, widget.offset);
|
||||
.fetchEmailsFromFolderReversed(widget.selectedFolder, widget.offset);
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
@ -135,14 +308,36 @@ class EmailPageState extends State<EmailPage> {
|
||||
}
|
||||
}
|
||||
|
||||
bool selectAllEmails(bool selectionType) {
|
||||
emailListKey.currentState?.selectAllChecks(selectionType);
|
||||
return selectionType;
|
||||
}
|
||||
|
||||
bool markSelectedAsRead(bool selectionType) {
|
||||
emailListKey.currentState?.markAsRead(selectionType);
|
||||
return selectionType;
|
||||
}
|
||||
|
||||
bool moveSelectedOfFolder(String folder) {
|
||||
emailListKey.currentState?.moveOfSelected(folder);
|
||||
return false;
|
||||
}
|
||||
|
||||
List<GetThreadResponse> getListOfSelected() {
|
||||
return emailListKey.currentState!.listOfSelectedThreads();
|
||||
}
|
||||
// return [GetThreadResponse(id: 1, messages: [], subject: "subject", date: DateTime(2025), from_name: "from_name", from_address: "from_address", to: [], seen: false)];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: EmailListScreen(
|
||||
emails: emails,
|
||||
getEmailContent: apiService.fetchEmailContent,
|
||||
folder: widget.selectedFolder, //try to grab from it directly
|
||||
),
|
||||
);
|
||||
body: EmailListScreen(
|
||||
key: emailListKey,
|
||||
emails: emails,
|
||||
// getEmailContent: apiService.fetchEmailContent,
|
||||
getEmailContent: apiService.fetchMarkdownContent,
|
||||
folder: widget.selectedFolder, //try to grab from it directly
|
||||
onSelectionChanged: widget.onSelectionChanged,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
3
lib/emailView.dart
Normal file
@ -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 'package:crab_ui/api_service.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'api_service.dart';
|
||||
import 'home_page.dart';
|
||||
// import 'home_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
// import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
class AuthService {
|
||||
class AuthService extends ChangeNotifier {
|
||||
Future<bool> isUserLoggedIn() async {
|
||||
ApiService.ip = '192.168.2.38';
|
||||
ApiService.port = '3001';
|
||||
print("setted up");
|
||||
|
||||
return true;
|
||||
try {
|
||||
final response =
|
||||
await http.get(Uri.http('localhost:6823', 'read-config'));
|
||||
@ -83,6 +90,7 @@ class LoginPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
//entry point
|
||||
@override
|
||||
_SplashScreenState createState() => _SplashScreenState();
|
||||
}
|
||||
@ -92,19 +100,24 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkLoginStatus();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkLoginStatus();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkLoginStatus() async {
|
||||
// SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
// print(prefs);
|
||||
// bool isLoggedIn = prefs.getBool('isLoggedIn') ?? false;
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
bool isLoggedIn = await _authService.isUserLoggedIn();
|
||||
print("is loogeed in $isLoggedIn");
|
||||
print("is logged in $isLoggedIn");
|
||||
if (isLoggedIn) {
|
||||
Navigator.pushReplacementNamed(context, '/home');
|
||||
context.go("/home");
|
||||
// Navigator.pushReplacementNamed(context, '/home');
|
||||
} else {
|
||||
Navigator.pushReplacementNamed(context, '/login');
|
||||
context.go("/login");
|
||||
// Navigator.pushReplacementNamed(context, '/login');
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +125,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
body: Center(child: CircularProgressIndicator()), //nothing happens
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -132,6 +145,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
Future<bool> setIp(String ip) async {
|
||||
//this is not done :sob: :skull:
|
||||
// _configManager.setField("api_addr", ip);
|
||||
return false;
|
||||
}
|
||||
|
@ -1,12 +1,18 @@
|
||||
import 'package:crab_ui/contact.dart';
|
||||
import 'package:crab_ui/email.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'home_page.dart';
|
||||
import 'login.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'routingHandler.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(HyM());
|
||||
runApp(ChangeNotifierProvider(
|
||||
create: (context) => AuthService(),
|
||||
child: HyM(),
|
||||
));
|
||||
}
|
||||
|
||||
class HyM extends StatelessWidget {
|
||||
@ -15,19 +21,56 @@ class HyM extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
final GoRouter _router = GoRouter(
|
||||
// refreshListenable: ,
|
||||
initialLocation: '/',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "/",
|
||||
builder: (context, state) => SplashScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/login",
|
||||
builder: (context, state) => const LoginPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/home",
|
||||
builder: (context, state) => HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/contacts",
|
||||
builder: (context, state) => ContactsPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/email/:subject/:target/:viewspecs/:emailID",
|
||||
builder: (context, state) {
|
||||
final subject = state.pathParameters['subject']!;
|
||||
final target = state.pathParameters['target']!;
|
||||
final viewspecs = state.pathParameters['viewspecs']!;
|
||||
final emailId = state.pathParameters['emailID']!;
|
||||
return Routinghandler.fromParameters("main anchor", subject, target, viewspecs, emailId);
|
||||
}),
|
||||
]);
|
||||
return MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData.light(),
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.light(),
|
||||
useMaterial3: true,
|
||||
),
|
||||
title: 'HyM',
|
||||
routerConfig: _router,
|
||||
// home: HomeScreen(),
|
||||
initialRoute: "/",
|
||||
|
||||
routes: {
|
||||
"/": (context) => SplashScreen(),
|
||||
"/login": (context) => const LoginPage(),
|
||||
"/home": (context) => HomeScreen(),
|
||||
"/contacts": (context) => ContactsPage(),
|
||||
},
|
||||
// routes: {
|
||||
// "/": (context) => SplashScreen(),
|
||||
// "/login": (context) => const LoginPage(),
|
||||
// "/home": (context) => HomeScreen(),
|
||||
// "/contacts": (context) => ContactsPage(),
|
||||
// GoRoute(
|
||||
// path:
|
||||
// )
|
||||
// "/email": (context) => EmailListScreen(),
|
||||
// },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
229
lib/routingHandler.dart
Normal file
@ -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';
|
||||
import 'package:web/web.dart' as web;
|
||||
import 'dart:ui_web' as ui;
|
||||
import 'dart:js_interop';
|
||||
import 'structs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SonicEmailView extends StatefulWidget {
|
||||
SerializableMessage email;
|
||||
String emailHTML;
|
||||
|
||||
SonicEmailView({required this.email, required this.emailHTML});
|
||||
|
||||
@override
|
||||
_SonicEmailViewState createState() => _SonicEmailViewState();
|
||||
}
|
||||
|
||||
class _SonicEmailViewState extends State<SonicEmailView> {
|
||||
String viewTypeIDs = "";
|
||||
int heightOFViewtype = 0;
|
||||
bool _isLoaded = false;
|
||||
|
||||
void _scrollToNumber(String spanId) {
|
||||
AugmentClasses.handleJump(spanId);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
await _registerViewFactory(widget.emailHTML);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _registerViewFactory(String currentContent) async {
|
||||
// setState(() { //update to do item per item
|
||||
// each item to have itsviewtype ID
|
||||
// is this necessarey here??
|
||||
|
||||
//could just move to collapsable
|
||||
|
||||
// for (var emailHTML in widget.threadHTML) {
|
||||
String viewTypeId = 'email-${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
final ghost = web.document.createElement('div') as web.HTMLDivElement
|
||||
..style.visibility = 'hidden'
|
||||
..style.position = 'absolute'
|
||||
..style.width = '100%'
|
||||
..style.overflow = 'auto'
|
||||
..innerHTML = currentContent.toJS;
|
||||
web.document.body?.append(ghost);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
|
||||
final heightOfEmail = ghost.scrollHeight;
|
||||
ghost.remove();
|
||||
|
||||
final HTMLsnippet = web.document.createElement('div') as web.HTMLDivElement
|
||||
..id = viewTypeId
|
||||
..innerHTML = widget
|
||||
.emailHTML.toJS; // temporarily index because it has to do all of them
|
||||
HTMLsnippet.style
|
||||
..width = '100%'
|
||||
..height = '${heightOfEmail}px'
|
||||
..overflow = 'auto'
|
||||
..scrollBehavior = 'smooth';
|
||||
|
||||
ui.platformViewRegistry.registerViewFactory(
|
||||
viewTypeId,
|
||||
(int viewId) => HTMLsnippet,
|
||||
);
|
||||
this.viewTypeIDs = viewTypeId;
|
||||
this.heightOFViewtype = heightOfEmail;
|
||||
print(viewTypeIDs);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _isLoaded
|
||||
? Scaffold(
|
||||
appBar: AppBar(title: Text(widget.email.subject)),
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
EmailToolbar(
|
||||
onButtonPressed: () => {},
|
||||
onJumpToSpan: _scrollToNumber),
|
||||
Row(
|
||||
// title of email
|
||||
children: [
|
||||
Text(
|
||||
widget.email.subject,
|
||||
style: TextStyle(fontSize: 30),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'from ${widget.email.name}',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
Text(
|
||||
'<${widget.email.from}>',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
'${widget.email.date}',
|
||||
textAlign: TextAlign.right,
|
||||
)
|
||||
],
|
||||
),
|
||||
// TODO: make a case where if one of these is the user's email it just says me :)))))
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'to ${widget.email.to.toString()}',
|
||||
style: TextStyle(fontSize: 15),
|
||||
)
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
// child: SizedBox(
|
||||
// height: heightOFViewtype.toDouble(),
|
||||
child: HtmlElementView(
|
||||
key: UniqueKey(), viewType: this.viewTypeIDs,
|
||||
// ),
|
||||
))
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'SonicEmailViewStub.dart'
|
||||
if (dart.library.js_interop) 'SonicEmailViewWeb.dart'
|
||||
if (dart.library.io) 'SonicEmailViewAndroid.dart';
|
178
lib/structs.dart
@ -1,6 +1,7 @@
|
||||
//data structures
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
class GetThreadResponse {
|
||||
final int id;
|
||||
@ -10,6 +11,7 @@ class GetThreadResponse {
|
||||
final String from_name;
|
||||
final String from_address;
|
||||
final List<MailAddress> to;
|
||||
late bool seen;
|
||||
|
||||
GetThreadResponse({
|
||||
required this.id,
|
||||
@ -19,19 +21,20 @@ class GetThreadResponse {
|
||||
required this.from_name,
|
||||
required this.from_address,
|
||||
required this.to,
|
||||
required this.seen,
|
||||
});
|
||||
factory GetThreadResponse.fromJson(Map<String, dynamic> json) {
|
||||
var toList = json['to'] as List<dynamic>;
|
||||
|
||||
return GetThreadResponse(
|
||||
id: json['id'],
|
||||
messages: List<String>.from(json['messages']),
|
||||
subject: json['subject'],
|
||||
date: DateTime.parse(json['date']),
|
||||
from_name: json['from_name'],
|
||||
from_address: json['from_address'],
|
||||
to: toList.map((i) => MailAddress.fromJson(i)).toList(),
|
||||
);
|
||||
id: json['id'],
|
||||
messages: List<String>.from(json['messages']),
|
||||
subject: json['subject'],
|
||||
date: DateTime.parse(json['date']),
|
||||
from_name: json['from_name'],
|
||||
from_address: json['from_address'],
|
||||
to: toList.map((i) => MailAddress.fromJson(i)).toList(),
|
||||
seen: json['seen']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,7 +130,8 @@ class AttachmentInfoList extends Iterable<AttachmentInfo> {
|
||||
AttachmentInfoList(this._attachments);
|
||||
|
||||
factory AttachmentInfoList.fromJsonList(List<Map<String, dynamic>> jsonList) {
|
||||
return AttachmentInfoList(jsonList.map((json) => AttachmentInfo.fromJson(json)).toList());
|
||||
return AttachmentInfoList(
|
||||
jsonList.map((json) => AttachmentInfo.fromJson(json)).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
@ -143,6 +147,160 @@ class AttachmentResponse {
|
||||
AttachmentResponse({required this.name, required this.data});
|
||||
|
||||
factory AttachmentResponse.fromJson(Map<String, dynamic> json) {
|
||||
return AttachmentResponse(name: json["name"], data: Uint8List.fromList(List<int>.from(json["data"])));
|
||||
return AttachmentResponse(
|
||||
name: json["name"],
|
||||
data: Uint8List.fromList(List<int>.from(json["data"])));
|
||||
}
|
||||
}
|
||||
|
||||
class AugmentTree {
|
||||
List<AugmentTree> children = [];
|
||||
|
||||
String data = '';
|
||||
AugmentTree? parent;
|
||||
String ogTag = '';
|
||||
String numbering = '';
|
||||
Map<String, int> hirarchyDict = {
|
||||
"h1": 1,
|
||||
"h2": 2,
|
||||
"h3": 3,
|
||||
"h4": 4,
|
||||
"h5": 5,
|
||||
"h6": 6,
|
||||
"p": 8,
|
||||
"ul": 8,
|
||||
"li": 8,
|
||||
};
|
||||
|
||||
AugmentTree();
|
||||
|
||||
AugmentTree.fromMD(String rawMD) {
|
||||
//makes raw MD into an augmentTree
|
||||
print("started markdown2tree");
|
||||
final List<md.Node> nakedList = md.Document().parseLines(rawMD.split(
|
||||
'\n')); //emails md is the index of the email in the thread, since this only handles one thus it shall be removed
|
||||
// AugmentTree zoomTreeRoot = AugmentTree();
|
||||
for (var node in nakedList) {
|
||||
//maybe do an add function, but isn't this it?
|
||||
if (node is md.Element) {
|
||||
AugmentTree temp = AugmentTree();
|
||||
temp.data = node.textContent;
|
||||
temp.ogTag = node.tag;
|
||||
if (hirarchyDict.containsKey(node.tag)) {
|
||||
// make this O(1)
|
||||
_add2Tree(this, node);
|
||||
// print(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.addNumbering();
|
||||
}
|
||||
|
||||
void _add2Tree(AugmentTree tree, md.Element node2add) {
|
||||
// adds node to its corresponding place
|
||||
AugmentTree newNode = AugmentTree();
|
||||
newNode.setData(node2add.textContent);
|
||||
newNode.ogTag = node2add.tag;
|
||||
// cases,
|
||||
//1. a node that comes is lower than the root.children last, if so it goes beneath it
|
||||
if (tree.children.isEmpty) {
|
||||
// new level to be created when totally empty
|
||||
tree.children.add(newNode);
|
||||
newNode.parent = tree;
|
||||
} else if (tree.children.isNotEmpty &&
|
||||
tree.children.last.ogTag.isNotEmpty) {
|
||||
if ((hirarchyDict[node2add.tag] ??
|
||||
-1) < // e.g. new node is h1 and old is h2, heapify
|
||||
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
|
||||
//have to figure out the borthers
|
||||
//assuming it all goes right
|
||||
if ((hirarchyDict[node2add.tag] ?? -1) == -1 ||
|
||||
(hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) {
|
||||
print(
|
||||
'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} < ${hirarchyDict[tree.children.last.ogTag] ?? -1}');
|
||||
return;
|
||||
} else if (tree.children.last.parent == null) {
|
||||
// becomes the new top level
|
||||
for (AugmentTree brother in tree.children) {
|
||||
brother.parent = newNode;
|
||||
}
|
||||
tree.children = [newNode];
|
||||
} else {
|
||||
newNode.parent = tree;
|
||||
tree.children.add(newNode);
|
||||
}
|
||||
} else if ((hirarchyDict[node2add.tag] ??
|
||||
-1) > // go down e.g. new node is h3 and old is h2 or something
|
||||
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
|
||||
if ((hirarchyDict[node2add.tag] ?? -1) == -1 ||
|
||||
(hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) {
|
||||
print(
|
||||
'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} > ${hirarchyDict[tree.children.last.ogTag] ?? -1}');
|
||||
print("-1 ${tree.children.last.ogTag}");
|
||||
return;
|
||||
}
|
||||
|
||||
_add2Tree(tree.children.last, node2add);
|
||||
} else if ((hirarchyDict[node2add.tag] ?? -1) ==
|
||||
(hirarchyDict[tree.children.last.ogTag] ?? -1)) {
|
||||
tree.children.add(newNode);
|
||||
newNode.parent = tree;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setData(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static String _intToLetter(int index) {
|
||||
return String.fromCharCode('a'.runes.first + index);
|
||||
}
|
||||
|
||||
void addNumbering({String prefix = ''}) {
|
||||
//if called in root, numbers them all
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
final child = children[i];
|
||||
String childNumbering;
|
||||
bool parentIsLettered = prefix.contains(RegExp(r'[a-z]'));
|
||||
if (prefix.isEmpty) {
|
||||
parentIsLettered = false;
|
||||
} else {
|
||||
parentIsLettered = prefix.runes.last >= 'a'.runes.first &&
|
||||
prefix.runes.last <= 'z'.runes.first;
|
||||
}
|
||||
|
||||
if (prefix.isEmpty) {
|
||||
// Top-level children (direct children of the original root being numbered) get 1, 2, 3...
|
||||
childNumbering = (i + 1).toString();
|
||||
} else if (parentIsLettered) {
|
||||
// Deeper children get '1a', '1b', '2a', '2b', etc.
|
||||
childNumbering = '$prefix${i + 1}';
|
||||
} else {
|
||||
childNumbering = '$prefix${_intToLetter(i)}';
|
||||
}
|
||||
|
||||
child.numbering = childNumbering;
|
||||
|
||||
// Recursively call for children
|
||||
child.addNumbering(prefix: childNumbering);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//perhaps make a struct that builds augment tree, since its so complex and needs to be like recursive
|
||||
|
||||
class MarkdownParsed {
|
||||
//struct for holding the MD given in endpoint //not used
|
||||
final String text;
|
||||
MarkdownParsed({required this.text});
|
||||
factory MarkdownParsed.fromJson(Map<String, String> json) {
|
||||
return MarkdownParsed(
|
||||
text: json['md'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//should make an md to tree class/struct
|
||||
|
||||
// make a for loop of rows with markdown
|
||||
|
522
pubspec.lock
12
pubspec.yaml
@ -12,14 +12,12 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
http: 1.2.2
|
||||
flutter_html_all: 3.0.0-beta.2
|
||||
flutter_widget_from_html: ^0.10.0
|
||||
shared_preferences: ^2.0.6
|
||||
encrypt: ^5.0.0
|
||||
pointycastle: ^3.4.0
|
||||
mime: ^1.0.3
|
||||
pointer_interceptor: ^0.10.1+2
|
||||
file_saver: ^0.2.14
|
||||
|
||||
|
||||
|
||||
english_words: ^4.0.0
|
||||
@ -28,6 +26,13 @@ dependencies:
|
||||
pdfrx: ^1.0.94
|
||||
photo_view: ^0.15.0
|
||||
web: ^1.1.1
|
||||
flutter_widget_from_html: ^0.16.0
|
||||
html2md: ^1.3.2
|
||||
markdown_widget: ^2.3.2+8
|
||||
markdown: ^7.3.0
|
||||
go_router: ^16.0.0
|
||||
super_editor: ^0.3.0-dev.27
|
||||
super_editor_markdown: 0.1.8
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -39,6 +44,7 @@ dependency_overrides:
|
||||
flutter_layout_grid: 2.0.7
|
||||
flutter_math_fork: 0.7.2
|
||||
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
|