WIP: android/ios-adaption feature, markdown, and augment #6
30
.metadata
@ -4,7 +4,7 @@
|
|||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
|
revision: "be698c48a6750c8cb8e61c740ca9991bb947aba2"
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
@ -13,26 +13,26 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: android
|
- platform: android
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: ios
|
- platform: ios
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: linux
|
- platform: linux
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: macos
|
- platform: macos
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: web
|
- platform: web
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
- platform: windows
|
- platform: windows
|
||||||
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
|
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
14
android/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
44
android/app/build.gradle.kts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.crab_ui"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.example.crab_ui"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
7
android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
45
android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="crab_ui"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.example.crab_ui
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
18
android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
7
android/app/src/profile/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
21
android/build.gradle.kts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
3
android/gradle.properties
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
25
android/settings.gradle.kts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath = run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.7.3" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
13
android_old/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
58
android_old/app/build.gradle
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
def localProperties = new Properties()
|
||||||
|
def localPropertiesFile = rootProject.file("local.properties")
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
localPropertiesFile.withReader("UTF-8") { reader ->
|
||||||
|
localProperties.load(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
|
||||||
|
if (flutterVersionCode == null) {
|
||||||
|
flutterVersionCode = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionName = localProperties.getProperty("flutter.versionName")
|
||||||
|
if (flutterVersionName == null) {
|
||||||
|
flutterVersionName = "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.hym_ui"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.example.hym_ui"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutterVersionCode.toInteger()
|
||||||
|
versionName = flutterVersionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
7
android_old/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
45
android_old/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="hym_ui"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.example.hym_ui
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity: FlutterActivity()
|
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
12
android_old/app/src/main/res/drawable/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
BIN
android_old/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 544 B |
BIN
android_old/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 442 B |
BIN
android_old/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 721 B |
BIN
android_old/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
android_old/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
18
android_old/app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
18
android_old/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
7
android_old/app/src/profile/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
32
android_old/build.gradle
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.9.10'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:8.3.0'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.buildDir = "../build"
|
||||||
|
subprojects {
|
||||||
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("clean", Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
3
android_old/gradle.properties
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
5
android_old/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
|
25
android_old/settings.gradle
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}()
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
|
id "com.android.application" version "7.3.0" apply false
|
||||||
|
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include ":app"
|
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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
150
lib/SonicEmailViewWeb.dart
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
),
|
||||||
|
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,22 +1,12 @@
|
|||||||
// this file should handle most of the API calls
|
// this file should handle most of the API calls
|
||||||
// it also builds some widgets, but it will be modulated later
|
// it also builds some widgets, but it will be modulated later // chat it did
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
|
||||||
import 'collapsableEmails.dart';
|
|
||||||
|
|
||||||
import 'structs.dart';
|
import 'structs.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:ui_web' as ui;
|
|
||||||
import 'augment.dart';
|
|
||||||
// import 'dart:html' as html;
|
|
||||||
// import 'dart:js' as js;
|
|
||||||
import 'package:web/web.dart' as web;
|
|
||||||
import 'dart:js_interop' as js;
|
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
static String ip = "";
|
static String ip = "";
|
||||||
@ -148,7 +138,6 @@ class ApiService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('_getEmailContent caught error: $e');
|
print('_getEmailContent caught error: $e');
|
||||||
}
|
}
|
||||||
// return content;
|
|
||||||
return HTMLofThread;
|
return HTMLofThread;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,308 +340,41 @@ class ApiService {
|
|||||||
return AttachmentResponse(name: "error", data: Uint8List(0));
|
return AttachmentResponse(name: "error", data: Uint8List(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: MOVE THIS INTO WEB
|
Future<List<String>> fetchMarkdownContent(
|
||||||
// Future<List<Map<String, dynamic>>> getMarkerPosition() async {
|
List<String> IDsString, String emailFolder) async {
|
||||||
// //this is so we can put a widget right below each email, but the way how the email content is generated
|
List<String> MDofThread = [];
|
||||||
// //leads to problems as for a) the html is added one right after the other in one iframe, b)
|
threadAttachments = [];
|
||||||
// // if it was multiple iframes then the scrolling to jump would not work as expected
|
int counter = 0;
|
||||||
|
|
||||||
// print("marker called");
|
try {
|
||||||
// // JavaScript code embedded as a string
|
//attaches email after email from a thread
|
||||||
// String jsCode = '''
|
for (var id in IDsString) {
|
||||||
// (async function waitForIframeAndMarkers() {
|
var url = Uri.http('$ip:$port', 'email_md', {'id': id});
|
||||||
// try {
|
print(url);
|
||||||
// return await new Promise((resolve) => {
|
var response = await http.get(url);
|
||||||
// const interval = setInterval(() => {
|
currThread.add(id);
|
||||||
// console.log("⏳ Checking for iframe...");
|
if (response.statusCode == 200) {
|
||||||
// var iframe = document.getElementsByTagName('iframe')[0];
|
counter += 1;
|
||||||
// if (iframe && iframe.contentDocument) {
|
Map<String, dynamic> json = jsonDecode(response.body);
|
||||||
// console.log("✅ Iframe found!");
|
|
||||||
// var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
MDofThread.add(json['md'] ?? '');
|
||||||
// var markers = iframeDoc.querySelectorAll('[id^="JuanBedarramarker"]');
|
try {
|
||||||
// if (markers.length > 0) {
|
List<AttachmentInfo> attachments =
|
||||||
// console.log(`✅ Found markers in the iframe.`);
|
await getAttachmentsInfo(emailFolder, id);
|
||||||
// var positions = [];
|
for (var attachment in attachments) {
|
||||||
// markers.forEach((marker) => {
|
//TODO: for each attachment creaate at the bottom a widget for each individual one
|
||||||
// var rect = marker.getBoundingClientRect();
|
threadAttachments
|
||||||
// positions.push({
|
.add(await getAttachment(emailFolder, id, attachment.name));
|
||||||
// id: marker.id,
|
}
|
||||||
// x: rect.left + window.scrollX,
|
} catch (innerError) {
|
||||||
// y: rect.top + window.scrollY,
|
print('_getAttachment info caught error $innerError');
|
||||||
// });
|
}
|
||||||
// });
|
}
|
||||||
// console.log("📌 Marker positions:", positions);
|
}
|
||||||
// clearInterval(interval);
|
} catch (e) {
|
||||||
// resolve(JSON.stringify(positions)); // Ensure proper JSON string
|
print('_getMDContent caught error: $e');
|
||||||
// } else {
|
}
|
||||||
// console.log("❌ No markers found yet.");
|
|
||||||
// }
|
return MDofThread;
|
||||||
// } else {
|
|
||||||
// console.log("❌ Iframe not found or not loaded yet.");
|
|
||||||
// }
|
|
||||||
// }, 200);
|
|
||||||
// });
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error("JS Error:", error);
|
|
||||||
// throw error; // Propagate error to Dart
|
|
||||||
// }
|
|
||||||
// })();
|
|
||||||
// ''';
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// // Execute the JavaScript code using eval
|
|
||||||
// // final result = await js.context.callMethod('eval', [jsCode]);
|
|
||||||
|
|
||||||
// if (result != null && result is String) {
|
|
||||||
// print("Result received: $result");
|
|
||||||
|
|
||||||
// // Parse the JSON string returned by JavaScript into a Dart list of maps
|
|
||||||
// final List<dynamic> parsedResult = jsonDecode(result);
|
|
||||||
// var positions = List<Map<String, dynamic>>.from(parsedResult);
|
|
||||||
// print("positions put on");
|
|
||||||
// print(positions);
|
|
||||||
// return positions;
|
|
||||||
// } else {
|
|
||||||
// print("result is null or not a string");
|
|
||||||
// }
|
|
||||||
// } catch (e, stackTrace) {
|
|
||||||
// print("Error executing JavaScript: $e");
|
|
||||||
// print(stackTrace);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return [];
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
class EmailView extends StatefulWidget {
|
|
||||||
final List<String> emailContent;
|
|
||||||
final String from;
|
|
||||||
final String name;
|
|
||||||
final String to;
|
|
||||||
final String subject;
|
|
||||||
final String date;
|
|
||||||
final String id;
|
|
||||||
final List<String> messages;
|
|
||||||
|
|
||||||
const EmailView({
|
|
||||||
Key? key,
|
|
||||||
required this.emailContent,
|
|
||||||
required this.from,
|
|
||||||
required this.name,
|
|
||||||
required this.to,
|
|
||||||
required this.subject,
|
|
||||||
required this.date,
|
|
||||||
required this.id,
|
|
||||||
required this.messages,
|
|
||||||
}) : super(key: key);
|
|
||||||
@override
|
|
||||||
_EmailViewState createState() => _EmailViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EmailViewState extends State<EmailView> {
|
|
||||||
//html css rendering thing
|
|
||||||
late Key iframeKey;
|
|
||||||
late String currentContent;
|
|
||||||
late String viewTypeId; //make this a list too???
|
|
||||||
Future<List<Map<String, dynamic>>>? _markerPositionsFuture;
|
|
||||||
// TextEditingController _jumpController = TextEditingController();
|
|
||||||
final hardcodedMarkers = [
|
|
||||||
{'id': 'marker1', 'x': 50, 'y': 100},
|
|
||||||
{'id': 'marker2', 'x': 150, 'y': 200},
|
|
||||||
{'id': 'marker3', 'x': 250, 'y': 300},
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
print("thread id? ${widget.id}");
|
|
||||||
List<String> currentContent = widget
|
|
||||||
.emailContent; //html of the email/ actually entire thread, gives me little space to play in between
|
|
||||||
// i wonder if the other attributes change? because if so i have to add like some zooms in and out of the emails, as in collapse
|
|
||||||
// _registerViewFactory(currentContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// void _registerViewFactory(List<String> currentContent) { // i think this doesnt work anymore
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: void _invisibility(String ) //to make purple numbers not visible
|
|
||||||
|
|
||||||
@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?");
|
|
||||||
|
|
||||||
// _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"
|
|
||||||
|
|
||||||
//
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// Overlay widgets dynamically based on marker positions
|
|
||||||
// FutureBuilder<List<Map<String, dynamic>>>(
|
|
||||||
// future: _markerPositionsFuture,
|
|
||||||
// builder: (context, snapshot) {
|
|
||||||
// print("FutureBuilder state: ${snapshot.connectionState}");
|
|
||||||
// if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
// return Center(child: CircularProgressIndicator());
|
|
||||||
// }
|
|
||||||
// if (snapshot.hasError) {
|
|
||||||
// print("Error in FutureBuilder: ${snapshot.error}");
|
|
||||||
// return Center(child: Text('error loading markers'));
|
|
||||||
// }
|
|
||||||
// if (snapshot.hasData && snapshot.data != null) {
|
|
||||||
// final markers = snapshot.data!;
|
|
||||||
// return Stack(
|
|
||||||
// children: markers.map((marker) {
|
|
||||||
// return Positioned(
|
|
||||||
// left: marker['x'].toDouble(),
|
|
||||||
// top: marker['y'].toDouble(),
|
|
||||||
// child: GestureDetector(
|
|
||||||
// onTap: () {
|
|
||||||
// print('Tapped on ${marker['id']}');
|
|
||||||
// },
|
|
||||||
// child: Container(
|
|
||||||
// width: 50,
|
|
||||||
// height: 50,
|
|
||||||
// color: Colors.red,
|
|
||||||
// child: Center(
|
|
||||||
// child: Text(
|
|
||||||
// marker['id'],
|
|
||||||
// style: TextStyle(color: Colors.white),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }).toList(),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return SizedBox.shrink(); // No markers found
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// Red widget overlay
|
|
||||||
// Positioned(
|
|
||||||
// left: 8, // Adjust based on your desired position
|
|
||||||
// top: 100 + 44 + 5, // Adjust based on your desired position
|
|
||||||
// child: IgnorePointer(
|
|
||||||
// ignoring: true, // Ensures the iframe remains interactive
|
|
||||||
// child: Container(
|
|
||||||
// color: Colors.red,
|
|
||||||
// width: 100,
|
|
||||||
// height: 50,
|
|
||||||
// child: Center(
|
|
||||||
// child: Text(
|
|
||||||
// 'Overlay',
|
|
||||||
// style: TextStyle(color: Colors.white),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
lib/attachamentDownloadStub.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import 'structs.dart';
|
||||||
|
|
||||||
|
class Attachmentdownload {
|
||||||
|
Future<void> saveFile(AttachmentResponse attachment) async {
|
||||||
|
print("stub attachment download");
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,3 @@
|
|||||||
import 'dart:html' as html;
|
export 'attachamentDownloadStub.dart'
|
||||||
import 'package:web/web.dart' as web;
|
if (dart.library.io) 'attachmentDownloadAndroid.dart';
|
||||||
import 'dart:io';
|
// if (dart.library.js_interop) 'attachmentDownloadWeb.dart';
|
||||||
import 'structs.dart';
|
|
||||||
import 'package:file_saver/file_saver.dart';
|
|
||||||
|
|
||||||
class Attachmentdownload {
|
|
||||||
Future<void> saveFile(AttachmentResponse attachment) async {
|
|
||||||
await FileSaver.instance.saveFile(
|
|
||||||
name: attachment.name.toString().substring(0, attachment.name.toString().lastIndexOf('.')),
|
|
||||||
bytes: attachment.data,
|
|
||||||
ext: attachment.name.toString().substring(attachment.name.toString().lastIndexOf('.')+1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
7
lib/attachmentDownloadAndroid.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import 'structs.dart';
|
||||||
|
|
||||||
|
class Attachmentdownload {
|
||||||
|
Future<void> saveFile(AttachmentResponse attachment) async {
|
||||||
|
print("android attachment download");
|
||||||
|
}
|
||||||
|
}
|
12
lib/attachmentDownloadWeb.dart
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// import 'structs.dart';
|
||||||
|
// import 'package:file_saver/file_saver.dart';
|
||||||
|
|
||||||
|
// class Attachmentdownload {
|
||||||
|
// Future<void> saveFile(AttachmentResponse attachment) async {
|
||||||
|
// await FileSaver.instance.saveFile(
|
||||||
|
// name: attachment.name.toString().substring(0, attachment.name.toString().lastIndexOf('.')),
|
||||||
|
// bytes: attachment.data,
|
||||||
|
// ext: attachment.name.toString().substring(attachment.name.toString().lastIndexOf('.')+1)
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
@ -1,103 +1,3 @@
|
|||||||
import "dart:typed_data";
|
export 'attachmentWidgetStub.dart'
|
||||||
|
if (dart.library.js_interop) 'attachmentWidgetWeb.dart'
|
||||||
import "package:crab_ui/attachmentDownload.dart";
|
if (dart.library.io) 'attachmentWidgetAndroid.dart';
|
||||||
import "package:crab_ui/structs.dart";
|
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import 'package:pdfrx/pdfrx.dart';
|
|
||||||
import 'package:photo_view/photo_view.dart';
|
|
||||||
|
|
||||||
class AttachmentWidget extends StatelessWidget {
|
|
||||||
final AttachmentResponse attachment;
|
|
||||||
AttachmentWidget({required this.attachment});
|
|
||||||
|
|
||||||
Widget attachmentViewer(AttachmentResponse att) {
|
|
||||||
String extension = att.name
|
|
||||||
.toString()
|
|
||||||
.substring(att.name.toString().indexOf(".") + 1)
|
|
||||||
.toLowerCase();
|
|
||||||
if (extension == "jpg" || extension == "png") {
|
|
||||||
return Image.memory(att.data);
|
|
||||||
} else if (extension == "pdf") {
|
|
||||||
return PdfViewer.data(Uint8List.fromList(att.data),
|
|
||||||
sourceName: att.name,
|
|
||||||
params: PdfViewerParams(
|
|
||||||
enableTextSelection: true,
|
|
||||||
scrollByMouseWheel: 0.5,
|
|
||||||
annotationRenderingMode:
|
|
||||||
PdfAnnotationRenderingMode.annotationAndForms,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return Center(
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xff6C63FF),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black26,
|
|
||||||
blurRadius: 10,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"No preview available",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white, fontSize: 18, decoration: TextDecoration.none),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 5,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
child: ElevatedButton(
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text("Download", style: TextStyle(color: Color(0xff2c3e50)),),
|
|
||||||
Icon(Icons.download,
|
|
||||||
color: Color(0xffb0b0b0),),
|
|
||||||
]),
|
|
||||||
onPressed: () => Attachmentdownload().saveFile(att),
|
|
||||||
)),
|
|
||||||
]),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
color: Colors.black38,
|
|
||||||
child: Stack(children: <Widget>[
|
|
||||||
Container(
|
|
||||||
color: Colors.white,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(10, 20, 0, 10),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
CloseButton(onPressed: () => {Navigator.pop(context)}),
|
|
||||||
Text(
|
|
||||||
attachment.name
|
|
||||||
.toString(), //its alr a string but incase ¯\(ツ)/¯ //update: i did that everywhere lol
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.black,
|
|
||||||
fontSize: 20,
|
|
||||||
decoration: TextDecoration
|
|
||||||
.none), //TODO: personalize your fonts
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: attachmentViewer(attachment),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
16
lib/attachmentWidgetAndroid.dart
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import "package:crab_ui/structs.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentWidget extends StatelessWidget {
|
||||||
|
final AttachmentResponse attachment;
|
||||||
|
AttachmentWidget({required this.attachment});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Text("PDF EVENTUALLY ANDROID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
14
lib/attachmentWidgetStub.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'structs.dart';
|
||||||
|
|
||||||
|
class AttachmentWidget extends StatelessWidget{
|
||||||
|
final AttachmentResponse attachment;
|
||||||
|
AttachmentWidget({required this.attachment});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Text("PDF EVENTUALLY, STUB")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
101
lib/attachmentWidgetWeb.dart
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import "dart:typed_data";
|
||||||
|
import "package:crab_ui/attachmentDownload.dart";
|
||||||
|
import "package:crab_ui/structs.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
|
|
||||||
|
class AttachmentWidget extends StatelessWidget {
|
||||||
|
final AttachmentResponse attachment;
|
||||||
|
AttachmentWidget({required this.attachment});
|
||||||
|
|
||||||
|
Widget attachmentViewer(AttachmentResponse att) {
|
||||||
|
String extension = att.name
|
||||||
|
.toString()
|
||||||
|
.substring(att.name.toString().indexOf(".") + 1)
|
||||||
|
.toLowerCase();
|
||||||
|
if (extension == "jpg" || extension == "png") {
|
||||||
|
return Image.memory(att.data);
|
||||||
|
} else if (extension == "pdf") {
|
||||||
|
return PdfViewer.data(Uint8List.fromList(att.data),
|
||||||
|
sourceName: att.name,
|
||||||
|
params: PdfViewerParams(
|
||||||
|
enableTextSelection: true,
|
||||||
|
scrollByMouseWheel: 0.5,
|
||||||
|
annotationRenderingMode:
|
||||||
|
PdfAnnotationRenderingMode.annotationAndForms,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Center(
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xff6C63FF),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black26,
|
||||||
|
blurRadius: 10,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"No preview available",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white, fontSize: 18, decoration: TextDecoration.none),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 5,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
child: ElevatedButton(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text("Download", style: TextStyle(color: Color(0xff2c3e50)),),
|
||||||
|
Icon(Icons.download,
|
||||||
|
color: Color(0xffb0b0b0),),
|
||||||
|
]),
|
||||||
|
onPressed: () => Attachmentdownload().saveFile(att),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.black38,
|
||||||
|
child: Stack(children: <Widget>[
|
||||||
|
Container(
|
||||||
|
color: Colors.white,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(10, 20, 0, 10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
CloseButton(onPressed: () => {Navigator.pop(context)}),
|
||||||
|
Text(
|
||||||
|
attachment.name
|
||||||
|
.toString(), //its alr a string but incase ¯\(ツ)/¯ //update: i did that everywhere lol
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 20,
|
||||||
|
decoration: TextDecoration
|
||||||
|
.none), //TODO: personalize your fonts
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: attachmentViewer(attachment),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,17 @@
|
|||||||
// import 'dart:ffi';
|
|
||||||
|
|
||||||
import 'package:crab_ui/api_service.dart';
|
import 'package:crab_ui/api_service.dart';
|
||||||
import 'package:crab_ui/attachmentDownload.dart';
|
import 'package:crab_ui/attachmentDownload.dart';
|
||||||
import 'package:crab_ui/structs.dart';
|
import 'package:crab_ui/structs.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:pdfrx/pdfrx.dart';
|
|
||||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
|
||||||
// import 'dart:html' as html;
|
|
||||||
// import 'dart:js' as js;
|
|
||||||
import 'package:web/web.dart' as web;
|
|
||||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||||
import 'attachmentWidget.dart';
|
import 'attachmentWidget.dart';
|
||||||
|
|
||||||
class EmailToolbar extends StatefulWidget {
|
class EmailToolbar extends StatefulWidget {
|
||||||
final Function(String) onJumpToSpan;
|
final Function(String) onJumpToNumbering;
|
||||||
|
final Function(String) onViewspecs;
|
||||||
final VoidCallback onButtonPressed;
|
final VoidCallback onButtonPressed;
|
||||||
|
|
||||||
EmailToolbar(
|
EmailToolbar(
|
||||||
{Key? key, required this.onButtonPressed, required this.onJumpToSpan})
|
{Key? key, required this.onButtonPressed, required this.onJumpToNumbering, required this.onViewspecs})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -26,7 +20,8 @@ class EmailToolbar extends StatefulWidget {
|
|||||||
|
|
||||||
class _DynamicClassesAugment extends State<EmailToolbar> {
|
class _DynamicClassesAugment extends State<EmailToolbar> {
|
||||||
String selectedClass = 'Class 1';
|
String selectedClass = 'Class 1';
|
||||||
// TextEditingController _jumpController = TextEditingController();
|
TextEditingController _jumpController = TextEditingController();
|
||||||
|
TextEditingController _viewspecsController = TextEditingController();
|
||||||
|
|
||||||
// late final FocusNode _JumpItemfocusNode;
|
// late final FocusNode _JumpItemfocusNode;
|
||||||
// late final FocusNode _viewSpecsfocusNode;
|
// late final FocusNode _viewSpecsfocusNode;
|
||||||
@ -53,7 +48,7 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
// _JumpItemfocusNode.dispose();
|
// _JumpItemfocusNode.dispose();
|
||||||
// _viewSpecsfocusNode.dispose();
|
// _viewSpecsfocusNode.dispose();
|
||||||
// _jumpController.dispose();
|
_jumpController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,20 +73,20 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
child: Text('Attachments'),
|
child: Text('Attachments'),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
ElevatedButton(
|
// ElevatedButton(
|
||||||
onPressed: AugmentClasses.handleOpen,
|
// onPressed: AugmentClasses.handleOpen,
|
||||||
child: Text('Open'),
|
// child: Text('Open'),
|
||||||
),
|
// ),
|
||||||
// SizedBox(width: 8),
|
// SizedBox(width: 8),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: AugmentClasses.handleFind,
|
onPressed: AugmentClasses.handleFind,
|
||||||
child: Text('Find'),
|
child: Text('Find'),
|
||||||
),
|
),
|
||||||
// SizedBox(width: 8),
|
// SizedBox(width: 8),
|
||||||
ElevatedButton(
|
// ElevatedButton(
|
||||||
onPressed: AugmentClasses.handleStop,
|
// onPressed: AugmentClasses.handleStop,
|
||||||
child: Text('Stop'),
|
// child: Text('Stop'),
|
||||||
),
|
// ),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AugmentClasses.handleMove(context);
|
AugmentClasses.handleMove(context);
|
||||||
@ -138,10 +133,10 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
// width: 8,
|
// width: 8,
|
||||||
// ),
|
// ),
|
||||||
Container(
|
Container(
|
||||||
width: 50,
|
width: 100,
|
||||||
height: 30,
|
height: 30,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
// controller: _jumpController,
|
controller: _jumpController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
// suffixIcon: Icon(Icons.search)
|
// suffixIcon: Icon(Icons.search)
|
||||||
@ -149,7 +144,7 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
onSubmitted: (value) {
|
onSubmitted: (value) {
|
||||||
print("onSubmitted");
|
print("onSubmitted");
|
||||||
if (value.isNotEmpty) {
|
if (value.isNotEmpty) {
|
||||||
widget.onJumpToSpan(value);
|
widget.onJumpToNumbering(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -186,14 +181,18 @@ class _DynamicClassesAugment extends State<EmailToolbar> {
|
|||||||
onPressed: () => AugmentClasses.ViewSpecsButton(context),
|
onPressed: () => AugmentClasses.ViewSpecsButton(context),
|
||||||
child: Text('ViewSpecs:')),
|
child: Text('ViewSpecs:')),
|
||||||
Container(
|
Container(
|
||||||
width: 50,
|
width: 100,
|
||||||
height: 30,
|
height: 30,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
controller: _viewspecsController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '',
|
labelText: '',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
// suffixIcon: Icon(Icons.style_rounded)
|
// suffixIcon: Icon(Icons.style_rounded)
|
||||||
),
|
),
|
||||||
|
onSubmitted: (value) {
|
||||||
|
widget.onViewspecs(value);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
@ -491,30 +490,8 @@ class AugmentClasses {
|
|||||||
print("Stop button pressed");
|
print("Stop button pressed");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleJump(String spanId) {
|
static void handleJump(String value) {
|
||||||
String js_code = '''
|
print(value);
|
||||||
var iframe = document.getElementsByTagName('iframe')[0]; // 0 for the first iframe, 1 for the second, etc.
|
|
||||||
|
|
||||||
// Check if the iframe is loaded and has content
|
|
||||||
if (iframe && iframe.contentDocument) {
|
|
||||||
// Access the document inside the iframe
|
|
||||||
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
|
||||||
|
|
||||||
// Find the element with the specific id inside the iframe
|
|
||||||
var targetElement = iframeDoc.getElementById("$spanId"); // Replace '36 ' with the actual id of the target element
|
|
||||||
|
|
||||||
// If the element exists, scroll to it
|
|
||||||
if (targetElement) {
|
|
||||||
targetElement.scrollIntoView();
|
|
||||||
console.log('Scrolled to element with id "$spanId" inside the iframe.');
|
|
||||||
} else {
|
|
||||||
console.log('Element with id "$spanId" not found inside the iframe.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Iframe not found or not loaded.');
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
// js.context.callMethod('eval', [js_code]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void invisibility(String htmlClass) {}
|
static void invisibility(String htmlClass) {}
|
||||||
|
@ -1,132 +1,3 @@
|
|||||||
import 'dart:js_interop';
|
export 'collapsableEmailsStub.dart'
|
||||||
import 'package:web/web.dart' as web;
|
if (dart.library.io) 'collapsableEmailsAndroid.dart'
|
||||||
import 'package:flutter/material.dart';
|
if (dart.library.js_interop) 'collapsableEmailsWeb.dart';
|
||||||
import 'dart:ui_web' as ui;
|
|
||||||
import 'api_service.dart';
|
|
||||||
import 'structs.dart';
|
|
||||||
|
|
||||||
class CollapsableEmails extends StatefulWidget {
|
|
||||||
final List<String> thread; // email id's in the form xyz@gmail.com
|
|
||||||
final List<String> threadHTML;
|
|
||||||
final String threadIDs;
|
|
||||||
|
|
||||||
CollapsableEmails(
|
|
||||||
{required this.thread,
|
|
||||||
required this.threadHTML,
|
|
||||||
required this.threadIDs});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CollapsableEmails> createState() => _CollapsableEmailsState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CollapsableEmailsState extends State<CollapsableEmails> {
|
|
||||||
List<String> emailsHTML = []; //html of the emails in the thread
|
|
||||||
// build attachments with the forldar name and id
|
|
||||||
Set<int> _expandedEmails = {}; //open emails
|
|
||||||
List viewtypeIDs = []; //IDs of the viewtypes, order matters
|
|
||||||
List heightOfViewTypes = []; //the height of each viewtype
|
|
||||||
List<SerializableMessage> emailsInThread = [];
|
|
||||||
bool _isLoaded = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
// TODO: implement initState
|
|
||||||
super.initState();
|
|
||||||
_registerViewFactory(widget.threadHTML);
|
|
||||||
_serializableData(widget.threadIDs);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _registerViewFactory(List<String> currentContent) async {
|
|
||||||
// setState(() { //update to do item per item
|
|
||||||
// each item to have itsviewtype ID
|
|
||||||
// is this necessarey here??
|
|
||||||
|
|
||||||
//could just move to collapsable
|
|
||||||
|
|
||||||
for (var emailHTML in widget.threadHTML) {
|
|
||||||
String viewTypeId = 'email-${DateTime.now().millisecondsSinceEpoch}';
|
|
||||||
|
|
||||||
final ghost = web.document.createElement('div') as web.HTMLDivElement
|
|
||||||
..style.visibility = 'hidden'
|
|
||||||
..style.position = 'absolute'
|
|
||||||
..style.width = '100%'
|
|
||||||
..style.overflow = 'auto'
|
|
||||||
..innerHTML = emailHTML
|
|
||||||
.toJS; // temporarily index because it has to do all of them
|
|
||||||
web.document.body?.append(ghost);
|
|
||||||
await Future.delayed(Duration(milliseconds: 10));
|
|
||||||
|
|
||||||
final heightOfEmail = ghost.scrollHeight;
|
|
||||||
ghost.remove();
|
|
||||||
|
|
||||||
final HTMLsnippet = web.document.createElement('div')
|
|
||||||
as web.HTMLDivElement
|
|
||||||
..id = viewTypeId
|
|
||||||
..innerHTML = emailHTML
|
|
||||||
.toJS; // temporarily index because it has to do all of them
|
|
||||||
HTMLsnippet.style
|
|
||||||
..width = '100%'
|
|
||||||
..height = '${heightOfEmail}px'
|
|
||||||
..overflow = 'auto'
|
|
||||||
..scrollBehavior = 'smooth';
|
|
||||||
|
|
||||||
ui.platformViewRegistry.registerViewFactory(
|
|
||||||
viewTypeId,
|
|
||||||
(int viewId) => HTMLsnippet,
|
|
||||||
);
|
|
||||||
viewtypeIDs.add(viewTypeId);
|
|
||||||
heightOfViewTypes.add(heightOfEmail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _serializableData(String threadID) async {
|
|
||||||
emailsInThread = await ApiService().threadsInSerializable(threadID);
|
|
||||||
print("done thread serializable");
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_isLoaded = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return _isLoaded
|
|
||||||
?Column(children: [
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: widget.thread.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final isExpanded =
|
|
||||||
_expandedEmails.contains(index); //check if email is expanded
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
title: Text(emailsInThread[index].from),
|
|
||||||
trailing: Text(emailsInThread[index].date),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
if (isExpanded) {
|
|
||||||
_expandedEmails.remove(index);
|
|
||||||
} else {
|
|
||||||
_expandedEmails.add(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (isExpanded)
|
|
||||||
// if(viewtypeIDs[index] == null || heightOfViewTypes[index] == null)
|
|
||||||
// const SizedBox(height: 100, child: Center(child: CircularProgressIndicator())),
|
|
||||||
SizedBox(
|
|
||||||
height: heightOfViewTypes[index].toDouble(),
|
|
||||||
child: HtmlElementView(
|
|
||||||
key: UniqueKey(), viewType: viewtypeIDs[index]),
|
|
||||||
),
|
|
||||||
Divider(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]): const Center(child:CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
}
|
|
435
lib/collapsableEmailsAndroid.dart
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
CollapsableEmails(
|
||||||
|
{required this.thread,
|
||||||
|
required this.threadMarkdown,
|
||||||
|
required this.threadIDs,
|
||||||
|
this.targetJumpNumbering,
|
||||||
|
this.targetViewspecs,
|
||||||
|
});
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
23
lib/collapsableEmailsStub.dart
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CollapsableEmails> createState() => _CollapsableEmailsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CollapsableEmailsState extends State<CollapsableEmails> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(body: Text("collapsable stud"));
|
||||||
|
}
|
||||||
|
}
|
450
lib/collapsableEmailsWeb.dart
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
import 'package:english_words/english_words.dart';
|
||||||
|
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; to be replaced with the MD
|
||||||
|
final List<String> threadMarkdown;
|
||||||
|
final String threadIDs;
|
||||||
|
final String? targetJumpNumbering;
|
||||||
|
final String? targetViewspecs;
|
||||||
|
|
||||||
|
const CollapsableEmails({
|
||||||
|
required this.thread,
|
||||||
|
// required this.threadHTML,
|
||||||
|
required this.threadMarkdown,
|
||||||
|
required this.threadIDs,
|
||||||
|
this.targetJumpNumbering,
|
||||||
|
this.targetViewspecs,
|
||||||
|
});
|
||||||
|
|
||||||
|
@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": 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 = [];
|
||||||
|
bool zoomOut = false;
|
||||||
|
bool zoomIn = true;
|
||||||
|
late List<AugmentTree> threadNodes = [];
|
||||||
|
static bool leftNumbering = true;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// void _markdownConverter() async {
|
||||||
|
// // to list of markdown
|
||||||
|
// // for (int email = 0; email < widget.threadHTML.length; email++) {
|
||||||
|
// // String markdown = html2md.convert(widget.threadHTML[email]);
|
||||||
|
// // allMarkdown.add(markdown);
|
||||||
|
// // }
|
||||||
|
// for (int email = 0; email < widget.threadMarkdown.length; email++) {
|
||||||
|
// allMarkdown.add(email);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
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), //show the tree
|
||||||
|
// child: _buildForZooms(key: ValueKey(currentZoomNode)),
|
||||||
|
),
|
||||||
|
Divider(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
: const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart' as http;
|
// import 'package:http/http.dart' as http;
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
// import 'package:flutter_html/flutter_html.dart';
|
||||||
|
|
||||||
class ContactsPage extends StatefulWidget {
|
class ContactsPage extends StatefulWidget {
|
||||||
const ContactsPage({super.key});
|
const ContactsPage({super.key});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'api_service.dart';
|
import 'api_service.dart';
|
||||||
import 'structs.dart';
|
import 'structs.dart';
|
||||||
|
import 'emailView.dart';
|
||||||
|
|
||||||
class EmailListScreen extends StatelessWidget {
|
class EmailListScreen extends StatelessWidget {
|
||||||
final List<GetThreadResponse> emails;
|
final List<GetThreadResponse> emails;
|
||||||
@ -140,7 +140,8 @@ class EmailPageState extends State<EmailPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: EmailListScreen(
|
body: EmailListScreen(
|
||||||
emails: emails,
|
emails: emails,
|
||||||
getEmailContent: apiService.fetchEmailContent,
|
// getEmailContent: apiService.fetchEmailContent,
|
||||||
|
getEmailContent: apiService.fetchMarkdownContent,
|
||||||
folder: widget.selectedFolder, //try to grab from it directly
|
folder: widget.selectedFolder, //try to grab from it directly
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
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';
|
115
lib/emailViewAndroid.dart
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
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){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,
|
||||||
|
|
||||||
|
),
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
143
lib/emailViewWeb.dart
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
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 '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???
|
||||||
|
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},
|
||||||
|
];
|
||||||
|
String? _targetJumpNumbering;
|
||||||
|
String? _targetViewspecs;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: void _invisibility(String ) //to make purple numbers not visible
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
ApiService.currThreadID = widget.id;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(widget.name),
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
EmailToolbar(
|
||||||
|
onJumpToNumbering: _handleJumpRequest,
|
||||||
|
onViewspecs: _handleViewspecsRequest,
|
||||||
|
onButtonPressed: () => {print("email tool bar pressed")},
|
||||||
|
),
|
||||||
|
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, // old html
|
||||||
|
threadMarkdown: widget.emailContent,
|
||||||
|
threadIDs: widget.id,
|
||||||
|
targetJumpNumbering: _targetJumpNumbering,
|
||||||
|
targetViewspecs: _targetViewspecs,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import 'package:crab_ui/sonicEmailView.dart';
|
|||||||
|
|
||||||
import 'folder_drawer.dart';
|
import 'folder_drawer.dart';
|
||||||
import 'structs.dart';
|
import 'structs.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
// import 'package:flutter/widgets.dart';
|
||||||
import 'api_service.dart';
|
import 'api_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'email.dart';
|
import 'email.dart';
|
||||||
@ -148,16 +148,6 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
builder: (context) =>SonicEmailView(
|
builder: (context) =>SonicEmailView(
|
||||||
email: email,
|
email: email,
|
||||||
emailHTML: emailContent[0])
|
emailHTML: emailContent[0])
|
||||||
// builder: (context) => EmailView(
|
|
||||||
// emailContent: emailContent,
|
|
||||||
// from: email.from,
|
|
||||||
// name: email.name,
|
|
||||||
// to: email.to.toString(),
|
|
||||||
// subject: email.subject,
|
|
||||||
// date: email.date.toString(),
|
|
||||||
// id: email.id.toString(),
|
|
||||||
// messages: [email.id],
|
|
||||||
// ),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -165,28 +155,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => Divider(),
|
separatorBuilder: (context, index) => Divider(),
|
||||||
),
|
),
|
||||||
// child: Column(
|
|
||||||
// mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
// children: [
|
|
||||||
// Text("Results for: $query", style: TextStyle(fontSize: 24)),
|
|
||||||
// // Display the actual data
|
|
||||||
// Text(result[0].name), // Accessing the first result safely
|
|
||||||
// Text(result[0].from), // Displaying the 'from' field as an example
|
|
||||||
// Text(result[0].hash),
|
|
||||||
// Text(result[0].subject),
|
|
||||||
// Text(result[0].uid.toString()),
|
|
||||||
// Text(result[0].list),
|
|
||||||
// Text(result[0].id),
|
|
||||||
|
|
||||||
// // Add more fields or customize the display
|
|
||||||
// // SerializableEmailListScreen(emails: result, getEmailContent: getEmailContent)
|
|
||||||
// // Expanded(
|
|
||||||
|
|
||||||
// // child:
|
|
||||||
// // ),
|
|
||||||
// ],
|
|
||||||
);
|
);
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -201,265 +171,253 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: _scaffoldKey,
|
backgroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
drawer: FolderDrawer(
|
body: Padding(
|
||||||
apiService: apiService,
|
padding: const EdgeInsets.fromLTRB(0, 20, 0 , 20),
|
||||||
onFolderTap: (folder) {
|
child: Scaffold(
|
||||||
_emailPageKey.currentState?.updateSelectedFolder(folder);
|
key: _scaffoldKey,
|
||||||
},
|
drawer: FolderDrawer(
|
||||||
),
|
apiService: apiService,
|
||||||
body: Stack(
|
onFolderTap: (folder) {
|
||||||
children: [
|
_emailPageKey.currentState?.updateSelectedFolder(folder);
|
||||||
Row(
|
},
|
||||||
children: [
|
),
|
||||||
// Sidebar
|
body: Scaffold(
|
||||||
if (_isSidebarOpen)
|
backgroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
Container(
|
body: Padding(
|
||||||
width: 70,
|
padding: const EdgeInsets.fromLTRB(0, 20, 0, 0),
|
||||||
color: Color.fromARGB(17, 96, 122, 135),
|
child: Stack(
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Row(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
// Sidebar
|
||||||
leading: Icon(Icons.home),
|
if (_isSidebarOpen)
|
||||||
onTap: () {
|
Container(
|
||||||
// Navigate to Home
|
width: 70,
|
||||||
},
|
color: Color.fromARGB(17, 96, 122, 135),
|
||||||
),
|
child: Column(
|
||||||
ListTile(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
leading: Icon(Icons.settings),
|
children: [
|
||||||
onTap: () {
|
ListTile(
|
||||||
// Navigate to Settings
|
leading: Icon(Icons.home),
|
||||||
},
|
onTap: () {
|
||||||
),
|
// Navigate to Home
|
||||||
ListTile(
|
},
|
||||||
leading: Icon(Icons.email),
|
),
|
||||||
onTap: () {
|
ListTile(
|
||||||
_scaffoldKey.currentState?.openDrawer();
|
leading: Icon(Icons.settings),
|
||||||
},
|
onTap: () {
|
||||||
),
|
// Navigate to Settings
|
||||||
Spacer(),
|
},
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.all(8.0),
|
ListTile(
|
||||||
child: Align(
|
leading: Icon(Icons.email),
|
||||||
alignment: Alignment.bottomLeft,
|
onTap: () {
|
||||||
child: IconButton(
|
_scaffoldKey.currentState?.openDrawer();
|
||||||
icon: Icon(Icons.close, color: Colors.white),
|
},
|
||||||
onPressed: () {
|
),
|
||||||
setState(() {
|
Spacer(),
|
||||||
_isSidebarOpen = false;
|
Padding(
|
||||||
});
|
padding: const EdgeInsets.all(8.0),
|
||||||
},
|
child: Align(
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(Icons.close, color: Colors.white),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isSidebarOpen = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Main content
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 4.0),
|
||||||
|
color: Color.fromARGB(42, 36, 102, 132),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: 800,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search...',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
),
|
||||||
|
onSubmitted: (value) {
|
||||||
|
if (value.isNotEmpty) {
|
||||||
|
_performSearch(value, _selectedOption);
|
||||||
|
}
|
||||||
|
//this is the input box i mentioned
|
||||||
|
// if (value == '') {
|
||||||
|
// setState(() {
|
||||||
|
// querySearches = false;
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// Future<List<String>> results = apiService
|
||||||
|
// .sonicSearch('INBOX', 20, 0, value);
|
||||||
|
// // print(value);
|
||||||
|
// print(results);
|
||||||
|
// setState(() {
|
||||||
|
// querySearches = true;
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 40,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _showOptionsSearchDialog,
|
||||||
|
child: Icon(Icons.manage_search),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
color: Color.fromARGB(42, 36, 102, 132),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 2,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: Color.fromARGB(255, 131, 110, 143),
|
||||||
|
child: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
isScrollable: true,
|
||||||
|
tabs: _tabs
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((entry) => Tab(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(entry.value),
|
||||||
|
if (entry.value != 'Emails')
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _removeTab(entry.key),
|
||||||
|
child: Icon(Icons.close, size: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
labelColor: Colors.white,
|
||||||
|
indicatorColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
// alignment: Alignment.topLeft,
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
color: Colors.white,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
_emailPageKey.currentState!.isBackDisabled ? null: _emailPageKey.currentState
|
||||||
|
?.updatePagenation('back');
|
||||||
|
},
|
||||||
|
child: Icon(Icons.navigate_before),
|
||||||
|
),
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final emailState = _emailPageKey.currentState;
|
||||||
|
if (emailState == null) {
|
||||||
|
// Schedule a rebuild once the state is available
|
||||||
|
Future.microtask(() => setState(() {}));
|
||||||
|
return Text('Loading...');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueListenableBuilder<int>(
|
||||||
|
valueListenable: emailState.currentPageNotifier,
|
||||||
|
builder: (context, value, _) => Text('$value'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
_emailPageKey.currentState
|
||||||
|
?.updatePagenation('next');
|
||||||
|
},
|
||||||
|
child: Icon(Icons.navigate_next),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: _tabs.map((tab) {
|
||||||
|
return _tabWidgets[tab] ??
|
||||||
|
Center(child: Text("No content found"));
|
||||||
|
// return Center(
|
||||||
|
// child: EmailPage(
|
||||||
|
// key: _emailPageKey,
|
||||||
|
// ));
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// if (_tabs.isEmpty)
|
||||||
|
// Expanded(
|
||||||
|
// child: EmailPage(key: _emailPageKey),
|
||||||
|
// ),
|
||||||
|
// if (_tabs.isNotEmpty)
|
||||||
|
// Expanded(
|
||||||
|
// // child: Text('supposed to be mails'),
|
||||||
|
// child: TabBarView(
|
||||||
|
// controller: _tabController,
|
||||||
|
// children: _tabs
|
||||||
|
// .map((tab) => Center(child: Text('Results for $tab')))
|
||||||
|
// .toList(),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
if (!_isSidebarOpen)
|
||||||
// Main content
|
Positioned(
|
||||||
Expanded(
|
bottom: 16,
|
||||||
child: Column(
|
left: 16,
|
||||||
children: [
|
child: FloatingActionButton(
|
||||||
Container(
|
child: Icon(Icons.menu),
|
||||||
padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 4.0),
|
onPressed: () {
|
||||||
color: Color.fromARGB(42, 36, 102, 132),
|
setState(() {
|
||||||
child: Row(
|
_isSidebarOpen = true;
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
});
|
||||||
children: [
|
},
|
||||||
Container(
|
|
||||||
width: 800,
|
|
||||||
height: 40,
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Search...',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
prefixIcon: Icon(Icons.search),
|
|
||||||
),
|
|
||||||
onSubmitted: (value) {
|
|
||||||
if (value.isNotEmpty) {
|
|
||||||
_performSearch(value, _selectedOption);
|
|
||||||
}
|
|
||||||
//this is the input box i mentioned
|
|
||||||
// if (value == '') {
|
|
||||||
// setState(() {
|
|
||||||
// querySearches = false;
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// Future<List<String>> results = apiService
|
|
||||||
// .sonicSearch('INBOX', 20, 0, value);
|
|
||||||
// // print(value);
|
|
||||||
// print(results);
|
|
||||||
// setState(() {
|
|
||||||
// querySearches = true;
|
|
||||||
// });
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 80,
|
|
||||||
height: 40,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: _showOptionsSearchDialog,
|
|
||||||
child: Icon(Icons.manage_search),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
],
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
color: Color.fromARGB(42, 36, 102, 132),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
height: 2,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
color: Color.fromARGB(255, 131, 110, 143),
|
|
||||||
child: TabBar(
|
|
||||||
controller: _tabController,
|
|
||||||
isScrollable: true,
|
|
||||||
tabs: _tabs
|
|
||||||
.asMap()
|
|
||||||
.entries
|
|
||||||
.map((entry) => Tab(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(entry.value),
|
|
||||||
if (entry.value != 'Emails')
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _removeTab(entry.key),
|
|
||||||
child: Icon(Icons.close, size: 16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
labelColor: Colors.white,
|
|
||||||
indicatorColor: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
// alignment: Alignment.topLeft,
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
color: Colors.white,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
_emailPageKey.currentState!.isBackDisabled ? null: _emailPageKey.currentState
|
|
||||||
?.updatePagenation('back');
|
|
||||||
},
|
|
||||||
child: Icon(Icons.navigate_before),
|
|
||||||
),
|
|
||||||
Builder(
|
|
||||||
builder: (context) {
|
|
||||||
final emailState = _emailPageKey.currentState;
|
|
||||||
if (emailState == null) {
|
|
||||||
// Schedule a rebuild once the state is available
|
|
||||||
Future.microtask(() => setState(() {}));
|
|
||||||
return Text('Loading...');
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValueListenableBuilder<int>(
|
|
||||||
valueListenable: emailState.currentPageNotifier,
|
|
||||||
builder: (context, value, _) => Text('$value'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
_emailPageKey.currentState
|
|
||||||
?.updatePagenation('next');
|
|
||||||
},
|
|
||||||
child: Icon(Icons.navigate_next),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TabBarView(
|
|
||||||
controller: _tabController,
|
|
||||||
children: _tabs.map((tab) {
|
|
||||||
return _tabWidgets[tab] ??
|
|
||||||
Center(child: Text("No content found"));
|
|
||||||
// return Center(
|
|
||||||
// child: EmailPage(
|
|
||||||
// key: _emailPageKey,
|
|
||||||
// ));
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// if (_tabs.isEmpty)
|
|
||||||
// Expanded(
|
|
||||||
// child: EmailPage(key: _emailPageKey),
|
|
||||||
// ),
|
|
||||||
// if (_tabs.isNotEmpty)
|
|
||||||
// Expanded(
|
|
||||||
// // child: Text('supposed to be mails'),
|
|
||||||
// child: TabBarView(
|
|
||||||
// controller: _tabController,
|
|
||||||
// children: _tabs
|
|
||||||
// .map((tab) => Center(child: Text('Results for $tab')))
|
|
||||||
// .toList(),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (!_isSidebarOpen)
|
|
||||||
Positioned(
|
|
||||||
bottom: 16,
|
|
||||||
left: 16,
|
|
||||||
child: FloatingActionButton(
|
|
||||||
child: Icon(Icons.menu),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_isSidebarOpen = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// void _showPopupMenu(BuildContext context, Offset position) async {
|
|
||||||
// final RenderBox overlay =
|
|
||||||
// Overlay.of(context).context.findRenderObject() as RenderBox;
|
|
||||||
|
|
||||||
// await showMenu<String>(
|
|
||||||
// context: context,
|
|
||||||
// position: RelativeRect.fromLTRB(
|
|
||||||
// position.dx,
|
|
||||||
// position.dy,
|
|
||||||
// overlay.size.width - position.dx,
|
|
||||||
// overlay.size.height - position.dy,
|
|
||||||
// ),
|
|
||||||
// items: <PopupMenuEntry<String>>[
|
|
||||||
// PopupMenuItem<String>(
|
|
||||||
// value: 'Open',
|
|
||||||
// child: Text('Open'),
|
|
||||||
// ),
|
|
||||||
// PopupMenuItem<String>(
|
|
||||||
// value: 'Reply',
|
|
||||||
// child: Text('Reply'),
|
|
||||||
// ),
|
|
||||||
// PopupMenuItem<String>(
|
|
||||||
// value: 'Delete',
|
|
||||||
// child: Text('Delete'),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
@ -17,7 +17,10 @@ class HyM extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData.light(),
|
theme: ThemeData(
|
||||||
|
colorScheme: ColorScheme.light(),
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
title: 'HyM',
|
title: 'HyM',
|
||||||
// home: HomeScreen(),
|
// home: HomeScreen(),
|
||||||
initialRoute: "/",
|
initialRoute: "/",
|
||||||
|
@ -1,145 +1,3 @@
|
|||||||
import 'package:crab_ui/augment.dart';
|
export 'SonicEmailViewStub.dart'
|
||||||
import 'package:web/web.dart' as web;
|
if (dart.library.js_interop) 'SonicEmailViewWeb.dart'
|
||||||
import 'dart:ui_web' as ui;
|
if (dart.library.io) 'SonicEmailViewAndroid.dart';
|
||||||
import 'dart:js_interop';
|
|
||||||
import 'structs.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class SonicEmailView extends StatefulWidget {
|
|
||||||
SerializableMessage email;
|
|
||||||
String emailHTML;
|
|
||||||
|
|
||||||
SonicEmailView({required this.email, required this.emailHTML});
|
|
||||||
|
|
||||||
@override
|
|
||||||
_SonicEmailViewState createState() => _SonicEmailViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SonicEmailViewState extends State<SonicEmailView> {
|
|
||||||
String viewTypeIDs = "";
|
|
||||||
int heightOFViewtype = 0;
|
|
||||||
bool _isLoaded = false;
|
|
||||||
|
|
||||||
void _scrollToNumber(String spanId) {
|
|
||||||
AugmentClasses.handleJump(spanId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_init();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _init() async {
|
|
||||||
await _registerViewFactory(widget.emailHTML);
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_isLoaded = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _registerViewFactory(String currentContent) async {
|
|
||||||
// setState(() { //update to do item per item
|
|
||||||
// each item to have itsviewtype ID
|
|
||||||
// is this necessarey here??
|
|
||||||
|
|
||||||
//could just move to collapsable
|
|
||||||
|
|
||||||
// for (var emailHTML in widget.threadHTML) {
|
|
||||||
String viewTypeId = 'email-${DateTime.now().millisecondsSinceEpoch}';
|
|
||||||
|
|
||||||
final ghost = web.document.createElement('div') as web.HTMLDivElement
|
|
||||||
..style.visibility = 'hidden'
|
|
||||||
..style.position = 'absolute'
|
|
||||||
..style.width = '100%'
|
|
||||||
..style.overflow = 'auto'
|
|
||||||
..innerHTML = currentContent.toJS;
|
|
||||||
web.document.body?.append(ghost);
|
|
||||||
await Future.delayed(Duration(milliseconds: 10));
|
|
||||||
|
|
||||||
final heightOfEmail = ghost.scrollHeight;
|
|
||||||
ghost.remove();
|
|
||||||
|
|
||||||
final HTMLsnippet = web.document.createElement('div') as web.HTMLDivElement
|
|
||||||
..id = viewTypeId
|
|
||||||
..innerHTML = widget
|
|
||||||
.emailHTML.toJS; // temporarily index because it has to do all of them
|
|
||||||
HTMLsnippet.style
|
|
||||||
..width = '100%'
|
|
||||||
..height = '${heightOfEmail}px'
|
|
||||||
..overflow = 'auto'
|
|
||||||
..scrollBehavior = 'smooth';
|
|
||||||
|
|
||||||
ui.platformViewRegistry.registerViewFactory(
|
|
||||||
viewTypeId,
|
|
||||||
(int viewId) => HTMLsnippet,
|
|
||||||
);
|
|
||||||
this.viewTypeIDs = viewTypeId;
|
|
||||||
this.heightOFViewtype = heightOfEmail;
|
|
||||||
print(viewTypeIDs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return _isLoaded
|
|
||||||
? Scaffold(
|
|
||||||
appBar: AppBar(title: Text(widget.email.subject)),
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
EmailToolbar(
|
|
||||||
onButtonPressed: () => {},
|
|
||||||
onJumpToSpan: _scrollToNumber),
|
|
||||||
Row(
|
|
||||||
// title of email
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.email.subject,
|
|
||||||
style: TextStyle(fontSize: 30),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'from ${widget.email.name}',
|
|
||||||
style: TextStyle(fontSize: 18),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'<${widget.email.from}>',
|
|
||||||
style: TextStyle(fontSize: 18),
|
|
||||||
),
|
|
||||||
Spacer(),
|
|
||||||
Text(
|
|
||||||
'${widget.email.date}',
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// TODO: make a case where if one of these is the user's email it just says me :)))))
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'to ${widget.email.to.toString()}',
|
|
||||||
style: TextStyle(fontSize: 15),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
// child: SizedBox(
|
|
||||||
// height: heightOFViewtype.toDouble(),
|
|
||||||
child: HtmlElementView(
|
|
||||||
key: UniqueKey(), viewType: this.viewTypeIDs,
|
|
||||||
// ),
|
|
||||||
))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -127,7 +127,8 @@ class AttachmentInfoList extends Iterable<AttachmentInfo> {
|
|||||||
AttachmentInfoList(this._attachments);
|
AttachmentInfoList(this._attachments);
|
||||||
|
|
||||||
factory AttachmentInfoList.fromJsonList(List<Map<String, dynamic>> jsonList) {
|
factory AttachmentInfoList.fromJsonList(List<Map<String, dynamic>> jsonList) {
|
||||||
return AttachmentInfoList(jsonList.map((json) => AttachmentInfo.fromJson(json)).toList());
|
return AttachmentInfoList(
|
||||||
|
jsonList.map((json) => AttachmentInfo.fromJson(json)).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -143,6 +144,67 @@ class AttachmentResponse {
|
|||||||
AttachmentResponse({required this.name, required this.data});
|
AttachmentResponse({required this.name, required this.data});
|
||||||
|
|
||||||
factory AttachmentResponse.fromJson(Map<String, dynamic> json) {
|
factory AttachmentResponse.fromJson(Map<String, dynamic> json) {
|
||||||
return AttachmentResponse(name: json["name"], data: Uint8List.fromList(List<int>.from(json["data"])));
|
return AttachmentResponse(
|
||||||
|
name: json["name"],
|
||||||
|
data: Uint8List.fromList(List<int>.from(json["data"])));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AugmentTree {
|
||||||
|
List<AugmentTree> children = [];
|
||||||
|
|
||||||
|
String data = '';
|
||||||
|
AugmentTree? parent;
|
||||||
|
String ogTag = '';
|
||||||
|
String numbering = '';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownParsed{
|
||||||
|
final String text;
|
||||||
|
MarkdownParsed({required this.text});
|
||||||
|
factory MarkdownParsed.fromJson(Map<String, String> json){
|
||||||
|
return MarkdownParsed(
|
||||||
|
text: json['md'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
522
pubspec.lock
@ -12,14 +12,12 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
http: 1.2.2
|
http: 1.2.2
|
||||||
flutter_html_all: 3.0.0-beta.2
|
|
||||||
flutter_widget_from_html: ^0.10.0
|
|
||||||
shared_preferences: ^2.0.6
|
shared_preferences: ^2.0.6
|
||||||
encrypt: ^5.0.0
|
encrypt: ^5.0.0
|
||||||
pointycastle: ^3.4.0
|
pointycastle: ^3.4.0
|
||||||
mime: ^1.0.3
|
mime: ^1.0.3
|
||||||
pointer_interceptor: ^0.10.1+2
|
pointer_interceptor: ^0.10.1+2
|
||||||
file_saver: ^0.2.14
|
|
||||||
|
|
||||||
|
|
||||||
english_words: ^4.0.0
|
english_words: ^4.0.0
|
||||||
@ -28,6 +26,10 @@ dependencies:
|
|||||||
pdfrx: ^1.0.94
|
pdfrx: ^1.0.94
|
||||||
photo_view: ^0.15.0
|
photo_view: ^0.15.0
|
||||||
web: ^1.1.1
|
web: ^1.1.1
|
||||||
|
flutter_widget_from_html: ^0.16.0
|
||||||
|
html2md: ^1.3.2
|
||||||
|
markdown_widget: ^2.3.2+8
|
||||||
|
markdown: ^7.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -39,6 +41,7 @@ dependency_overrides:
|
|||||||
flutter_layout_grid: 2.0.7
|
flutter_layout_grid: 2.0.7
|
||||||
flutter_math_fork: 0.7.2
|
flutter_math_fork: 0.7.2
|
||||||
|
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
assets:
|
assets:
|
||||||
|