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

Draft
Juan wants to merge 28 commits from android-adaption into main
68 changed files with 2681 additions and 1358 deletions

View File

@ -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
View File

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

View File

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.crab_ui"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.crab_ui"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

13
android_old/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

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

View File

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

32
android_old/build.gradle Normal file
View File

@ -0,0 +1,32 @@
buildscript {
ext.kotlin_version = '1.9.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
import 'structs.dart';
import 'package:flutter/material.dart';
class SonicEmailView extends StatefulWidget {
SerializableMessage email;
String emailHTML;
SonicEmailView({required this.email, required this.emailHTML});
@override
_SonicEmailViewState createState() => _SonicEmailViewState();
}
class _SonicEmailViewState extends State<SonicEmailView> {
@override
Widget build(BuildContext context) {
return Scaffold(body: Text("sonic email android"));
}
}

View File

@ -0,0 +1,22 @@
import 'structs.dart';
import 'package:flutter/material.dart';
class SonicEmailView extends StatefulWidget {
SerializableMessage email;
String emailHTML;
SonicEmailView({required this.email, required this.emailHTML});
@override
_SonicEmailViewState createState() => _SonicEmailViewState();
}
class _SonicEmailViewState extends State<SonicEmailView> {
@override
Widget build(BuildContext context) {
return Scaffold(
body:Text("sonic email stub")
);
}
}

150
lib/SonicEmailViewWeb.dart Normal file
View 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(),
);
}
}

View File

@ -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>&lt;tag&gt;</code> to organize and format content</li>
// <li>CSS is used with HTML to style pages</li>
// </ul>
// <p>Copyright © 2023</p>
// """);
// print("change");
// widget.emailContent = r"
//
),
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),
// ),
// ),
// ),
// ),
// ),
],
));
} }
} }

View File

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

View File

@ -1,15 +1,3 @@
import 'dart:html' as html; 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)
);
}
}

View File

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

View File

@ -0,0 +1,12 @@
// import 'structs.dart';
// import 'package:file_saver/file_saver.dart';
// class Attachmentdownload {
// Future<void> saveFile(AttachmentResponse attachment) async {
// await FileSaver.instance.saveFile(
// name: attachment.name.toString().substring(0, attachment.name.toString().lastIndexOf('.')),
// bytes: attachment.data,
// ext: attachment.name.toString().substring(attachment.name.toString().lastIndexOf('.')+1)
// );
// }
// }

View File

@ -1,103 +1,3 @@
import "dart:typed_data"; 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),
)
],
),
),
),
]));
}
}

View File

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

View File

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

View File

@ -0,0 +1,101 @@
import "dart:typed_data";
import "package:crab_ui/attachmentDownload.dart";
import "package:crab_ui/structs.dart";
import "package:flutter/material.dart";
import 'package:pdfrx/pdfrx.dart';
class AttachmentWidget extends StatelessWidget {
final AttachmentResponse attachment;
AttachmentWidget({required this.attachment});
Widget attachmentViewer(AttachmentResponse att) {
String extension = att.name
.toString()
.substring(att.name.toString().indexOf(".") + 1)
.toLowerCase();
if (extension == "jpg" || extension == "png") {
return Image.memory(att.data);
} else if (extension == "pdf") {
return PdfViewer.data(Uint8List.fromList(att.data),
sourceName: att.name,
params: PdfViewerParams(
enableTextSelection: true,
scrollByMouseWheel: 0.5,
annotationRenderingMode:
PdfAnnotationRenderingMode.annotationAndForms,
));
}
return Center(
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Color(0xff6C63FF),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"No preview available",
style: TextStyle(
color: Colors.white, fontSize: 18, decoration: TextDecoration.none),
),
SizedBox(
height: 5,
),
GestureDetector(
child: ElevatedButton(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("Download", style: TextStyle(color: Color(0xff2c3e50)),),
Icon(Icons.download,
color: Color(0xffb0b0b0),),
]),
onPressed: () => Attachmentdownload().saveFile(att),
)),
]),
));
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black38,
child: Stack(children: <Widget>[
Container(
color: Colors.white,
child: Padding(
padding: EdgeInsets.fromLTRB(10, 20, 0, 10),
child: Column(
children: [
Row(
children: [
CloseButton(onPressed: () => {Navigator.pop(context)}),
Text(
attachment.name
.toString(), //its alr a string but incase ¯\()/¯ //update: i did that everywhere lol
style: TextStyle(
color: Colors.black,
fontSize: 20,
decoration: TextDecoration
.none), //TODO: personalize your fonts
),
],
),
Expanded(
child: attachmentViewer(attachment),
)
],
),
),
),
]));
}
}

View File

@ -1,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) {}

View File

@ -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());
}
}

View 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());
}
}

View 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"));
}
}

View 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());
}
}

View File

@ -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});

View File

@ -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
View 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
View 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
View File

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

143
lib/emailViewWeb.dart Normal file
View 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,
),
),
],
),
],
));
}
}

View File

@ -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'),
// ),
// ],
// );
// }
// }

View File

@ -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: "/",

View File

@ -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(),
);
}
}

View File

@ -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'] ?? '',
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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: