diff --git a/.metadata b/.metadata index 6eb54a1..c9704a8 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49" + revision: "be698c48a6750c8cb8e61c740ca9991bb947aba2" channel: "stable" project_type: app @@ -13,26 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 - platform: android - create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 - platform: ios - create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 - platform: linux - create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 - platform: macos - create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 - platform: web - create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 - platform: windows - create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 # User provided section diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..c908258 --- /dev/null +++ b/android/.gitignore @@ -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 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..fe27d63 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,52 @@ +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 + ndk { + abiFilters.addAll(listOf("arm64-v8a", "x86_64")) + } + sourceSets { + getByName("main") { + jniLibs.srcDirs("src/main/jniLibs") + } + } + } + + 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 = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..8ffe024 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6023d1e --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/crab_ui/MainActivity.kt b/android/app/src/main/kotlin/com/example/crab_ui/MainActivity.kt new file mode 100644 index 0000000..7ea6dde --- /dev/null +++ b/android/app/src/main/kotlin/com/example/crab_ui/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.crab_ui + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..1cb7aa2 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..8403758 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..360a160 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..5fac679 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..8ffe024 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..2f2f3f0 --- /dev/null +++ b/android/build.gradle.kts @@ -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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..b7cda7b --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..dcc7e10 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..8ddb35d --- /dev/null +++ b/android/settings.gradle.kts @@ -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") diff --git a/android_old/.gitignore b/android_old/.gitignore new file mode 100644 index 0000000..5d99765 --- /dev/null +++ b/android_old/.gitignore @@ -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 diff --git a/android_old/app/build.gradle b/android_old/app/build.gradle new file mode 100644 index 0000000..02e20bc --- /dev/null +++ b/android_old/app/build.gradle @@ -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 = "../.." +} diff --git a/android_old/app/src/debug/AndroidManifest.xml b/android_old/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..8ffe024 --- /dev/null +++ b/android_old/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android_old/app/src/main/AndroidManifest.xml b/android_old/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d2914b1 --- /dev/null +++ b/android_old/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android_old/app/src/main/kotlin/com/example/hym_ui/MainActivity.kt b/android_old/app/src/main/kotlin/com/example/hym_ui/MainActivity.kt new file mode 100644 index 0000000..4a7c91b --- /dev/null +++ b/android_old/app/src/main/kotlin/com/example/hym_ui/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.hym_ui + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android_old/app/src/main/res/drawable-v21/launch_background.xml b/android_old/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..1cb7aa2 --- /dev/null +++ b/android_old/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android_old/app/src/main/res/drawable/launch_background.xml b/android_old/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..8403758 --- /dev/null +++ b/android_old/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android_old/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android_old/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android_old/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android_old/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android_old/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android_old/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android_old/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android_old/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android_old/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android_old/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android_old/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android_old/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android_old/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android_old/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android_old/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android_old/app/src/main/res/values-night/styles.xml b/android_old/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..360a160 --- /dev/null +++ b/android_old/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android_old/app/src/main/res/values/styles.xml b/android_old/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..5fac679 --- /dev/null +++ b/android_old/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android_old/app/src/profile/AndroidManifest.xml b/android_old/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..8ffe024 --- /dev/null +++ b/android_old/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android_old/build.gradle b/android_old/build.gradle new file mode 100644 index 0000000..fefa38b --- /dev/null +++ b/android_old/build.gradle @@ -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 +} diff --git a/android_old/gradle.properties b/android_old/gradle.properties new file mode 100644 index 0000000..0d7fbd5 --- /dev/null +++ b/android_old/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android_old/gradle/wrapper/gradle-wrapper.properties b/android_old/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..4abf038 --- /dev/null +++ b/android_old/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/android_old/settings.gradle b/android_old/settings.gradle new file mode 100644 index 0000000..0f10e04 --- /dev/null +++ b/android_old/settings.gradle @@ -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" diff --git a/lib/Compose.dart b/lib/Compose.dart new file mode 100644 index 0000000..331bed7 --- /dev/null +++ b/lib/Compose.dart @@ -0,0 +1,280 @@ +import 'package:crab_ui/api_service.dart'; +import 'package:crab_ui/structs.dart'; +import 'package:flutter/material.dart'; +import 'package:super_editor/super_editor.dart'; +import 'package:super_editor_markdown/super_editor_markdown.dart'; + +class ComposeEmail extends StatefulWidget { + final VoidCallback onClose; + final Function(String) onMinimize; + final Function(String) onSendMessage; + GetThreadResponse? emailDraftID; + ComposeEmail( + {Key? key, + required this.onMinimize, + required this.onClose, + required this.onSendMessage, + this.emailDraftID}) + : super(key: key); + + @override + _ComposeEmailState createState() => _ComposeEmailState(); +} + +class _ComposeEmailState extends State { + // if one were to alter a mutableDocument, one should only alter the document through EditRequest to the Editor + late final Editor _editor; + late final MutableDocument _document; + late final MutableDocumentComposer _composer; + TextEditingController _emailRecipientController = TextEditingController(); + TextEditingController _emailSubjectController = TextEditingController(); + List? contentOfDraft; + bool isInitialized = false; + + @override + void initState() { + super.initState(); + _loadDraftContent(); + } + + void _loadDraftContent() async { + if (widget.emailDraftID != null) { + String? drafted = widget.emailDraftID?.messages.last; + if (drafted != null) { + contentOfDraft = + await ApiService().fetchMarkdownContent([drafted!], "Drafts"); + setState(() { + _document = MutableDocument(nodes: [ + ParagraphNode( + id: Editor.createNodeId(), + text: AttributedText(contentOfDraft?[0] ?? + ""), // NOW THIS SHOULD BE WTV ITS IN DRAFTS + ) + ]); + _composer = MutableDocumentComposer(); + _editor = createDefaultDocumentEditor( + document: _document, composer: _composer); + _emailRecipientController.text = widget.emailDraftID!.to[0].address; + _emailSubjectController.text = widget.emailDraftID!.subject; + isInitialized = true; + + }); + } + } else { + setState(() { + _document = MutableDocument(nodes: [ + ParagraphNode( + id: Editor.createNodeId(), + text: AttributedText(""), + ), + ]); + _composer = MutableDocumentComposer(); + _editor = createDefaultDocumentEditor( + document: _document, composer: _composer); + isInitialized = true; + }); + } + } + + @override + void dispose() { + _editor.dispose(); + _emailRecipientController.dispose(); + _emailSubjectController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!isInitialized) { + return Center( + child: CircularProgressIndicator(), + ); + } + return Positioned( + bottom: 10.0, + right: 10.0, + child: Material( + elevation: 8.0, + child: Container( + width: 600.0, + height: 616.0, + decoration: BoxDecoration( + color: Colors.white, + ), + child: Column(children: [ + AppBar( + title: const Text("new message"), + actions: [ + IconButton( + onPressed: () { + //TODO: implement minimize, and submit email to drafts + widget.onClose(); + }, + icon: Icon(Icons.minimize, color: Colors.grey[600])), + IconButton( + onPressed: () { + //TODO: implement maximizing the window or widget + }, + icon: Icon(Icons.maximize, color: Colors.grey[600])), + IconButton( + onPressed: () { + widget.onClose(); + }, + icon: Icon(Icons.close, color: Colors.grey[600])), + ], + ), + Container( + // TODO: WHEN NOT CLICKED ITS ONLY A TEXTFIELD WITH A HINT, AND THEN WHEN CLICKED THIS + // width: 500.0, + // height: 40.0, + child: Row( + children: [ + TextButton(onPressed: () {}, child: Text("To:")), + Expanded( + child: TextField( + controller: _emailRecipientController, + ), + ), + TextButton(onPressed: () {}, child: Text("Cc")), + SizedBox( + width: 4.0, + ), + TextButton(onPressed: () {}, child: Text("Bcc")), + ], + ), + ), + SizedBox( + height: 4, + ), + Container( + // TODO: WHEN NOT CLICKED ITS ONLY A TEXTFIELD WITH A HINT, AND THEN WHEN CLICKED THIS + width: 500.0, + // height: 40.0, + child: Row( + children: [ + Expanded( + child: TextField( + controller: _emailSubjectController, + decoration: InputDecoration( + hintText: "Subject", + ), + ), + ) + ], + ), + ), + Expanded( + //here the widget goes + child: SuperEditor( + //make this its own + editor: _editor, + plugins: {MarkdownInlineUpstreamSyntaxPlugin()}, + + // stylesheet: Stylesheet( + // rules: [StyleRule(BlockSelector.all, (doc, docNode) { + // return { + // Styles.maxWidth: 640.0, + // Styles.padding: const CascadingPadding.symmetric(horizontal: 24), + // Styles.textStyle: const TextStyle( + // color: Colors.black, + // // fontSize: 15, + // height: 1.4, + // ), + // }; + // }),], + // inlineTextStyler: defaultInlineTextStyler) + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: () { + print('sent'); + String markdown = + serializeDocumentToMarkdown(_editor.document); + + print(_emailRecipientController.text); + print(_emailSubjectController.text); + print(markdown); + ApiService().sendEmail(_emailRecipientController.text, + _emailSubjectController.text, markdown); + }, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30)), + elevation: 4, + foregroundColor: Colors.white, + backgroundColor: Colors.blueAccent), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Send', + style: TextStyle(fontWeight: FontWeight.bold), + ), + // const SizedBox( + // width: 8, + // ), + // Container( + // height: 30, width: 1.0, color: Colors.white), + // const SizedBox( + // width: 8, + // ), + // const Icon( + // Icons.arrow_drop_down, + // size: 24, + // ) + ], + ), + ), + ], + ), + ) + ])), + ), + ); + } +} + +class OverlayService { + static final OverlayService _instance = OverlayService._internal(); + factory OverlayService() => _instance; + OverlayService._internal(); + OverlayEntry? _overlayEntry; + GetThreadResponse? draftID; + + void showPersistentWidget(BuildContext context) { + if (_overlayEntry != null) { + print("overlay visible"); + return; + } + _overlayEntry = OverlayEntry( + builder: (context) => ComposeEmail( + onClose: () { + removeComposeWidget(); + }, + onMinimize: (String content) { + minimizeComposeWidget(content); + }, + onSendMessage: (message) { + print('msg senf form overlay $message'); + }, + emailDraftID: draftID)); + Navigator.of(context).overlay?.insert(_overlayEntry!); + print("inserted into tree"); + } + + void removeComposeWidget() { + _overlayEntry?.remove(); + _overlayEntry = null; + } + + String minimizeComposeWidget(String content) { + //just hide the overlay but keep its info + return ''; + } +} diff --git a/lib/SonicEmailViewAndroid.dart b/lib/SonicEmailViewAndroid.dart new file mode 100644 index 0000000..fc2b67c --- /dev/null +++ b/lib/SonicEmailViewAndroid.dart @@ -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 { + @override + Widget build(BuildContext context) { + return Scaffold(body: Text("sonic email android")); + } +} diff --git a/lib/SonicEmailViewStub.dart b/lib/SonicEmailViewStub.dart new file mode 100644 index 0000000..0307a15 --- /dev/null +++ b/lib/SonicEmailViewStub.dart @@ -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 { + @override + Widget build(BuildContext context) { + return Scaffold( + body:Text("sonic email stub") + ); + } + +} diff --git a/lib/SonicEmailViewWeb.dart b/lib/SonicEmailViewWeb.dart new file mode 100644 index 0000000..53ab936 --- /dev/null +++ b/lib/SonicEmailViewWeb.dart @@ -0,0 +1,157 @@ +import 'package:crab_ui/augment.dart'; +import 'package:web/web.dart' as web; +import 'dart:ui_web' as ui; +import 'dart:js_interop'; +import 'structs.dart'; +import 'package:flutter/material.dart'; + +class SonicEmailView extends StatefulWidget { + SerializableMessage email; + String emailHTML; + + SonicEmailView({required this.email, required this.emailHTML}); + + @override + _SonicEmailViewState createState() => _SonicEmailViewState(); +} + +class _SonicEmailViewState extends State { + String viewTypeIDs = ""; + int heightOFViewtype = 0; + bool _isLoaded = false; + + void _scrollToNumber(String spanId) { + AugmentClasses.handleJump(spanId); + } + + void _handleViewspecs(String queryViewspecs) { + return; + } + + void _handleFiltering(String query) { + return; + } + + @override + void initState() { + super.initState(); + _init(); + } + + Future _init() async { + await _registerViewFactory(widget.emailHTML); + if (!mounted) return; + setState(() { + _isLoaded = true; + }); + } + + Future _registerViewFactory(String currentContent) async { + // setState(() { //update to do item per item + // each item to have itsviewtype ID + // is this necessarey here?? + + //could just move to collapsable + + // for (var emailHTML in widget.threadHTML) { + String viewTypeId = 'email-${DateTime.now().millisecondsSinceEpoch}'; + + final ghost = web.document.createElement('div') as web.HTMLDivElement + ..style.visibility = 'hidden' + ..style.position = 'absolute' + ..style.width = '100%' + ..style.overflow = 'auto' + ..innerHTML = currentContent.toJS; + web.document.body?.append(ghost); + await Future.delayed(Duration(milliseconds: 10)); + + final heightOfEmail = ghost.scrollHeight; + ghost.remove(); + + final HTMLsnippet = web.document.createElement('div') as web.HTMLDivElement + ..id = viewTypeId + ..innerHTML = widget + .emailHTML.toJS; // temporarily index because it has to do all of them + HTMLsnippet.style + ..width = '100%' + ..height = '${heightOfEmail}px' + ..overflow = 'auto' + ..scrollBehavior = 'smooth'; + + ui.platformViewRegistry.registerViewFactory( + viewTypeId, + (int viewId) => HTMLsnippet, + ); + this.viewTypeIDs = viewTypeId; + this.heightOFViewtype = heightOfEmail; + print(viewTypeIDs); + } + + @override + Widget build(BuildContext context) { + return _isLoaded + ? Scaffold( + appBar: AppBar(title: Text(widget.email.subject)), + body: Stack( + children: [ + Column( + children: [ + EmailToolbar( + onButtonPressed: () => {}, + onJumpToNumbering: _scrollToNumber, + onViewspecs: _handleViewspecs, + onFiltering: _handleFiltering, + emails: [widget.email.name], subject: '', rootAugment: null, + ), + Row( + // title of email + children: [ + Text( + widget.email.subject, + style: TextStyle(fontSize: 30), + ), + ], + ), + Row( + children: [ + Text( + 'from ${widget.email.name}', + style: TextStyle(fontSize: 18), + ), + Text( + '<${widget.email.from}>', + style: TextStyle(fontSize: 18), + ), + Spacer(), + Text( + '${widget.email.date}', + textAlign: TextAlign.right, + ) + ], + ), + // TODO: make a case where if one of these is the user's email it just says me :))))) + Row( + children: [ + Text( + 'to ${widget.email.to.toString()}', + style: TextStyle(fontSize: 15), + ) + ], + ), + Expanded( + // child: SizedBox( + // height: heightOFViewtype.toDouble(), + child: HtmlElementView( + key: UniqueKey(), viewType: this.viewTypeIDs, + // ), + )) + ], + ), + ], + ), + ) + : const Center( + child: CircularProgressIndicator(), + ); + } +} diff --git a/lib/api_service.dart b/lib/api_service.dart index b0b92ea..ecc433e 100644 --- a/lib/api_service.dart +++ b/lib/api_service.dart @@ -1,26 +1,16 @@ // this file should handle most of the API calls -// it also builds some widgets, but it will be modulated later +// it also builds some widgets, but it will be modulated later // chat it did import 'dart:async'; import 'dart:typed_data'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'collapsableEmails.dart'; - import 'structs.dart'; -import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; -import 'dart:ui_web' as ui; -import 'augment.dart'; -// import 'dart:html' as html; -// import 'dart:js' as js; -import 'package:web/web.dart' as web; -import 'dart:js_interop' as js; class ApiService { - static String ip = ""; - static String port = ""; + static String ip = '127.0.0.1'; + static String port = "3001"; static List threadAttachments = []; //holds attachments of the thread static String currFolder = ""; @@ -60,6 +50,40 @@ class ApiService { } } + Future> fetchEmailsFromFolderReversed( + String folder, int pagenitaion) async { + try { + var url = Uri.http('$ip:$port', 'sorted_threads_by_date_current', { + 'folder': folder, + 'limit': '50', + 'offset': pagenitaion.toString(), + }); + + var response = await http.get(url); + List allEmails = []; + + if (response.statusCode == 200) { + List json = jsonDecode(response.body); + for (var item in json) { + //each item in the json is a date + if (item.length > 1 && item[0] is String && item[1] is List) { + List threadIDs = List.from(item[1]); + for (var threadId in threadIDs) { + await fetchThreads(threadId, allEmails); + } + } + } + currFolder = folder; + return allEmails; + } else { + throw Exception('Failed to load threads'); + } + } catch (e) { + print('_displayEmailsFromFolder caught error: $e'); + return []; + } + } + Future fetchThreads( //populates allEmails, which is the List that contains all the emails in a thread int threadId, @@ -148,7 +172,6 @@ class ApiService { } catch (e) { print('_getEmailContent caught error: $e'); } - // return content; return HTMLofThread; } @@ -180,7 +203,7 @@ class ApiService { Future moveEmail( //only moves the first email of the thread //or perhaps should do the last String fromFolder, - String thread_id, + String thread_id, //uid String toFolder) async { var url = Uri.http('$ip:$port', 'move_email'); @@ -191,27 +214,28 @@ class ApiService { return false; } - SerializableMessage firstMail = mailsInSerializable[0]; - - Map requestBody = { - 'from': fromFolder, - 'uid': firstMail.uid.toString(), - 'to': toFolder, - }; + // SerializableMessage firstMail = mailsInSerializable[0]; try { - var response = await http.post( - url, - headers: { - 'Content-Type': 'application/json', - }, - body: jsonEncode(requestBody), - ); - if (response.statusCode == 200) { - print('response body ${response.body}'); - return true; - } else { - print('error ${response.statusCode} ${response.body}'); + for (SerializableMessage mail in mailsInSerializable) { + Map requestBody = { + 'from': fromFolder, + 'uid': mail.uid.toString(), + 'to': "Deleted Crabmail", + }; + var response = await http.post( + url, + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode(requestBody), + ); + if (response.statusCode == 200) { + print('response body ${response.body}'); + return true; + } else { + print('error ${response.statusCode} ${response.body}'); + } } } catch (e) { print("failed trying to post move_email, with error: $e"); @@ -229,6 +253,17 @@ class ApiService { return []; } } + + Future> fetchContacts() async { + try { + var url = Uri.http('$ip:$port', 'get_contacts'); + var response = await http.get(url); + return List.from(json.decode(response.body)); + } catch (e) { + print('fetchFolders caught error: $e'); + return []; + } + } Future createFolder(String folderName) async { var url = Uri.http('$ip:$port', 'create_folder'); @@ -351,308 +386,145 @@ class ApiService { return AttachmentResponse(name: "error", data: Uint8List(0)); } - //TODO: MOVE THIS INTO WEB -// Future>> getMarkerPosition() async { -// //this is so we can put a widget right below each email, but the way how the email content is generated -// //leads to problems as for a) the html is added one right after the other in one iframe, b) -// // if it was multiple iframes then the scrolling to jump would not work as expected + Future> fetchMarkdownContent( + List IDsString, String emailFolder) async { + List MDofThread = []; + threadAttachments = []; + int counter = 0; -// print("marker called"); -// // JavaScript code embedded as a string -// String jsCode = ''' -// (async function waitForIframeAndMarkers() { -// try { -// return await new Promise((resolve) => { -// const interval = setInterval(() => { -// console.log("⏳ Checking for iframe..."); -// var iframe = document.getElementsByTagName('iframe')[0]; -// if (iframe && iframe.contentDocument) { -// console.log("✅ Iframe found!"); -// var iframeDoc = iframe.contentDocument || iframe.contentWindow.document; -// var markers = iframeDoc.querySelectorAll('[id^="JuanBedarramarker"]'); -// if (markers.length > 0) { -// console.log(`✅ Found markers in the iframe.`); -// var positions = []; -// markers.forEach((marker) => { -// var rect = marker.getBoundingClientRect(); -// positions.push({ -// id: marker.id, -// x: rect.left + window.scrollX, -// y: rect.top + window.scrollY, -// }); -// }); -// console.log("📌 Marker positions:", positions); -// clearInterval(interval); -// resolve(JSON.stringify(positions)); // Ensure proper JSON string -// } else { -// console.log("❌ No markers found yet."); -// } -// } else { -// console.log("❌ Iframe not found or not loaded yet."); -// } -// }, 200); -// }); -// } catch (error) { -// console.error("JS Error:", error); -// throw error; // Propagate error to Dart -// } -// })(); -// '''; + try { + //attaches email after email from a thread + for (var id in IDsString) { + var url = Uri.http('$ip:$port', 'email_md', {'id': id}); + print(url); + var response = await http.get(url); + currThread.add(id); + if (response.statusCode == 200) { + counter += 1; + Map json = jsonDecode(response.body); -// try { -// // Execute the JavaScript code using eval -// // final result = await js.context.callMethod('eval', [jsCode]); - -// if (result != null && result is String) { -// print("Result received: $result"); - -// // Parse the JSON string returned by JavaScript into a Dart list of maps -// final List parsedResult = jsonDecode(result); -// var positions = List>.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 emailContent; - final String from; - final String name; - final String to; - final String subject; - final String date; - final String id; - final List 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 { - //html css rendering thing - late Key iframeKey; - late String currentContent; - late String viewTypeId; //make this a list too??? - Future>>? _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 currentContent = widget - .emailContent; //html of the email/ actually entire thread, gives me little space to play in between - // i wonder if the other attributes change? because if so i have to add like some zooms in and out of the emails, as in collapse - // _registerViewFactory(currentContent); + MDofThread.add(json['md'] ?? ''); + try { + List attachments = + await getAttachmentsInfo(emailFolder, id); + for (var attachment in attachments) { + //TODO: for each attachment creaate at the bottom a widget for each individual one + threadAttachments + .add(await getAttachment(emailFolder, id, attachment.name)); + } + } catch (innerError) { + print('_getAttachment info caught error $innerError'); + } + } + } + } catch (e) { + print('_getMDContent caught error: $e'); + } + // print("IDS inside fetch md content $IDsString"); + // print("inside apiservice $MDofThread"); + return MDofThread; } - // void _registerViewFactory(List currentContent) { // i think this doesnt work anymore - // setState(() { //update to do item per item - // // each item to have itsviewtype ID - // // is this necessarey here?? - - // //could just move to collapsable - - // viewTypeId = 'iframe-${DateTime.now().millisecondsSinceEpoch}'; - // final emailHTML = web.document.createElement('div') as web.HTMLDivElement - // ..id = viewTypeId - // ..innerHTML = currentContent[0].toJS; // temporarily index because it has to do all of them - // emailHTML.style - // ..width = '100%' - // ..height = '100%' - // ..overflow = 'auto' - // ..scrollBehavior = 'smooth'; - - // ui.platformViewRegistry.registerViewFactory( - // viewTypeId, - // (int viewId) => emailHTML, - // ); - // }); - // } - - void _scrollToNumber(String spanId) { - AugmentClasses.handleJump(spanId); + Future markAsSeen(int thread_id) async { + try { + var url = Uri.http( + '$ip:$port', 'post_seen_thread', {'id': thread_id.toString()}); + print("url: $url"); + var response = await http.get(url); + if (response.statusCode == 200) { + var result = response.body; + print("data $result"); + } + } catch (e) { + print("markAsSeen failed $e"); + } } - // TODO: void _invisibility(String ) //to make purple numbers not visible + Future markAsUnseen(int thread_id) async { + try { + var url = Uri.http( + '$ip:$port', 'post_unseen_thread', {'id': thread_id.toString()}); + print("url: $url"); + var response = await http.get(url); + if (response.statusCode == 200) { + var result = response.body; + print("data $result"); + } + } catch (e) { + print("markAsUnseen failed $e"); + } + } - @override - Widget build(BuildContext context) { - // print("thread id ${widget.id}"); - ApiService.currThreadID = widget.id; - return Scaffold( - appBar: AppBar( - title: Text(widget.name), - ), - body: Stack( - children: [ - Column( - children: [ - EmailToolbar( - onJumpToSpan: _scrollToNumber, - onButtonPressed: () => {}, - // AugmentClasses.handleJump(viewTypeId, '1'); - // print("button got pressed?"); + Future deleteEmail(String from_folder, int thread_id) async { + // post + try { + List mailsInSerializable = + await this.threadsInSerializable(thread_id.toString()); - // _registerViewFactory(r""" - //

Welcome to My Website

- //

This is a simple HTML page.

- //

What is HTML?

- //

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).

- //

Here's a simple list:

- //
    - //
  • HTML elements are the building blocks of HTML pages
  • - //
  • HTML uses tags like <tag> to organize and format content
  • - //
  • CSS is used with HTML to style pages
  • - //
- //

Copyright © 2023

- // """); - // print("change"); - // widget.emailContent = r" + if (mailsInSerializable.isEmpty) { + return false; + } + Map requestBody = { + "from": from_folder, + "uid": mailsInSerializable.first.uid.toString(), + "to": "not used" + }; - // - ), - Row( - // title of email - children: [ - Text( - widget.subject, - style: TextStyle(fontSize: 30), - ), - ], - ), - Row( - children: [ - Text( - 'from ${widget.name}', - style: TextStyle(fontSize: 18), - ), - Text( - '<${widget.from}>', - style: TextStyle(fontSize: 18), - ), - Spacer(), - Text( - '${widget.date}', - textAlign: TextAlign.right, - ) - ], - ), - // TODO: make a case where if one of these is the user's email it just says me :))))) - Row( - children: [ - Text( - 'to ${widget.to.toString()}', - style: TextStyle(fontSize: 15), - ) - ], - ), - Expanded( - child: CollapsableEmails( - //change here - thread: widget.messages, //this wont work in serializable - threadHTML: widget.emailContent, - threadIDs: widget.id, - ), - ), - // Expanded( - // child: HtmlElementView( - // key: UniqueKey(), - // viewType: viewTypeId, - // ), - // ), - ], - ), + //delete the email that is given to the + var url = Uri.http("$ip:$port", 'delete_email'); + var response = await http.post(url, + headers: { + "Content-Type": "application/json", + }, + body: jsonEncode(requestBody)); + if (response.statusCode == 200) { + print("response body: ${response.body}"); + return true; + } else { + print("not 200: ${response.body}"); + return false; + } + } catch (e) { + print("error in deleteEmail $e"); + return false; + } + } - // Overlay widgets dynamically based on marker positions - // FutureBuilder>>( - // future: _markerPositionsFuture, - // builder: (context, snapshot) { - // print("FutureBuilder state: ${snapshot.connectionState}"); - // if (snapshot.connectionState == ConnectionState.waiting) { - // return Center(child: CircularProgressIndicator()); - // } - // if (snapshot.hasError) { - // print("Error in FutureBuilder: ${snapshot.error}"); - // return Center(child: Text('error loading markers')); - // } - // if (snapshot.hasData && snapshot.data != null) { - // final markers = snapshot.data!; - // return Stack( - // children: markers.map((marker) { - // return Positioned( - // left: marker['x'].toDouble(), - // top: marker['y'].toDouble(), - // child: GestureDetector( - // onTap: () { - // print('Tapped on ${marker['id']}'); - // }, - // child: Container( - // width: 50, - // height: 50, - // color: Colors.red, - // child: Center( - // child: Text( - // marker['id'], - // style: TextStyle(color: Colors.white), - // ), - // ), - // ), - // ), - // ); - // }).toList(), - // ); - // } + Future sendEmail( + String? to, String? subject, String? emailContent) async { + try { + var url = Uri.http('$ip:$port', 'send_email'); - // return SizedBox.shrink(); // No markers found - // }, - // ), - // Red widget overlay - // Positioned( - // left: 8, // Adjust based on your desired position - // top: 100 + 44 + 5, // Adjust based on your desired position - // child: IgnorePointer( - // ignoring: true, // Ensures the iframe remains interactive - // child: Container( - // color: Colors.red, - // width: 100, - // height: 50, - // child: Center( - // child: Text( - // 'Overlay', - // style: TextStyle(color: Colors.white), - // ), - // ), - // ), - // ), - // ), - ], - )); + Map requestBody = { + "to": [to ?? ""], + "cc": [], + "bcc": [], + "subject": subject ?? "Untitled", + "in_reply_to": "", + "messages": [ + {"message": emailContent ?? "", "is_html": false} + ], + "attachments": [], + "inline_images": [], + }; + var response = await http.post( + url, + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode(requestBody), + ); + if (response.statusCode == 200) { + print("response body: ${response.body}"); + } else { + print('error: ${response.statusCode}, response body: ${response.body}'); + return false; + } + + return true; + } catch (e) { + print("error in post send email $e"); + return false; + } } } diff --git a/lib/attachamentDownloadStub.dart b/lib/attachamentDownloadStub.dart new file mode 100644 index 0000000..5daf854 --- /dev/null +++ b/lib/attachamentDownloadStub.dart @@ -0,0 +1,7 @@ +import 'structs.dart'; + +class Attachmentdownload { + Future saveFile(AttachmentResponse attachment) async { + print("stub attachment download"); + } +} diff --git a/lib/attachmentDownload.dart b/lib/attachmentDownload.dart index 63364ba..f1f8f86 100644 --- a/lib/attachmentDownload.dart +++ b/lib/attachmentDownload.dart @@ -1,15 +1,3 @@ -import 'dart:html' as html; -import 'package:web/web.dart' as web; -import 'dart:io'; -import 'structs.dart'; -import 'package:file_saver/file_saver.dart'; - -class Attachmentdownload { - Future saveFile(AttachmentResponse attachment) async { - await FileSaver.instance.saveFile( - name: attachment.name.toString().substring(0, attachment.name.toString().lastIndexOf('.')), - bytes: attachment.data, - ext: attachment.name.toString().substring(attachment.name.toString().lastIndexOf('.')+1) - ); - } -} +export 'attachamentDownloadStub.dart' + if (dart.library.io) 'attachmentDownloadAndroid.dart'; + // if (dart.library.js_interop) 'attachmentDownloadWeb.dart'; \ No newline at end of file diff --git a/lib/attachmentDownloadAndroid.dart b/lib/attachmentDownloadAndroid.dart new file mode 100644 index 0000000..603253c --- /dev/null +++ b/lib/attachmentDownloadAndroid.dart @@ -0,0 +1,7 @@ +import 'structs.dart'; + +class Attachmentdownload { + Future saveFile(AttachmentResponse attachment) async { + print("android attachment download"); + } +} diff --git a/lib/attachmentDownloadWeb.dart b/lib/attachmentDownloadWeb.dart new file mode 100644 index 0000000..cb4954c --- /dev/null +++ b/lib/attachmentDownloadWeb.dart @@ -0,0 +1,12 @@ +// import 'structs.dart'; +// import 'package:file_saver/file_saver.dart'; + +// class Attachmentdownload { +// Future 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) +// ); +// } +// } diff --git a/lib/attachmentWidget.dart b/lib/attachmentWidget.dart index 5146d9c..c40eae1 100644 --- a/lib/attachmentWidget.dart +++ b/lib/attachmentWidget.dart @@ -1,103 +1,3 @@ -import "dart:typed_data"; - -import "package:crab_ui/attachmentDownload.dart"; -import "package:crab_ui/structs.dart"; -import "package:flutter/material.dart"; -import 'package:pdfrx/pdfrx.dart'; -import 'package:photo_view/photo_view.dart'; - -class AttachmentWidget extends StatelessWidget { - final AttachmentResponse attachment; - AttachmentWidget({required this.attachment}); - - Widget attachmentViewer(AttachmentResponse att) { - String extension = att.name - .toString() - .substring(att.name.toString().indexOf(".") + 1) - .toLowerCase(); - if (extension == "jpg" || extension == "png") { - return Image.memory(att.data); - } else if (extension == "pdf") { - return PdfViewer.data(Uint8List.fromList(att.data), - sourceName: att.name, - params: PdfViewerParams( - enableTextSelection: true, - scrollByMouseWheel: 0.5, - annotationRenderingMode: - PdfAnnotationRenderingMode.annotationAndForms, - )); - } - return Center( - child: Container( - padding: EdgeInsets.all(20), - decoration: BoxDecoration( - color: Color(0xff6C63FF), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black26, - blurRadius: 10, - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "No preview available", - style: TextStyle( - color: Colors.white, fontSize: 18, decoration: TextDecoration.none), - ), - SizedBox( - height: 5, - ), - GestureDetector( - child: ElevatedButton( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("Download", style: TextStyle(color: Color(0xff2c3e50)),), - Icon(Icons.download, - color: Color(0xffb0b0b0),), - ]), - onPressed: () => Attachmentdownload().saveFile(att), - )), - ]), - )); - } - - @override - Widget build(BuildContext context) { - return Container( - color: Colors.black38, - child: Stack(children: [ - Container( - color: Colors.white, - child: Padding( - padding: EdgeInsets.fromLTRB(10, 20, 0, 10), - child: Column( - children: [ - Row( - children: [ - CloseButton(onPressed: () => {Navigator.pop(context)}), - Text( - attachment.name - .toString(), //its alr a string but incase ¯\(ツ)/¯ //update: i did that everywhere lol - style: TextStyle( - color: Colors.black, - fontSize: 20, - decoration: TextDecoration - .none), //TODO: personalize your fonts - ), - ], - ), - Expanded( - child: attachmentViewer(attachment), - ) - ], - ), - ), - ), - ])); - } -} +export 'attachmentWidgetStub.dart' + if (dart.library.js_interop) 'attachmentWidgetWeb.dart' + if (dart.library.io) 'attachmentWidgetAndroid.dart'; \ No newline at end of file diff --git a/lib/attachmentWidgetAndroid.dart b/lib/attachmentWidgetAndroid.dart new file mode 100644 index 0000000..72c3cd8 --- /dev/null +++ b/lib/attachmentWidgetAndroid.dart @@ -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") + ); + } +} + diff --git a/lib/attachmentWidgetStub.dart b/lib/attachmentWidgetStub.dart new file mode 100644 index 0000000..5bb6c14 --- /dev/null +++ b/lib/attachmentWidgetStub.dart @@ -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") + ); + } +} \ No newline at end of file diff --git a/lib/attachmentWidgetWeb.dart b/lib/attachmentWidgetWeb.dart new file mode 100644 index 0000000..b8b260b --- /dev/null +++ b/lib/attachmentWidgetWeb.dart @@ -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: [ + 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), + ) + ], + ), + ), + ), + ])); + } +} diff --git a/lib/augment.dart b/lib/augment.dart index b957740..dc35805 100644 --- a/lib/augment.dart +++ b/lib/augment.dart @@ -1,24 +1,33 @@ -// import 'dart:ffi'; - import 'package:crab_ui/api_service.dart'; import 'package:crab_ui/attachmentDownload.dart'; +import 'package:crab_ui/collapsableEmails.dart'; import 'package:crab_ui/structs.dart'; import 'package:flutter/material.dart'; -import 'package:pdfrx/pdfrx.dart'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; -// import 'dart:html' as html; -// import 'dart:js' as js; -import 'package:web/web.dart' as web; +import 'package:go_router/go_router.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'attachmentWidget.dart'; +import 'package:flutter/services.dart'; +import 'routingHandler.dart'; class EmailToolbar extends StatefulWidget { - final Function(String) onJumpToSpan; + final Function(String) onJumpToNumbering; + final Function(String) onViewspecs; final VoidCallback onButtonPressed; + final Function(String) onFiltering; + final List emails; + final String subject; + late AugmentTree? rootAugment; - EmailToolbar( - {Key? key, required this.onButtonPressed, required this.onJumpToSpan}) - : super(key: key); + EmailToolbar({ + Key? key, + required this.onButtonPressed, + required this.onJumpToNumbering, + required this.onViewspecs, + required this.onFiltering, + required this.emails, + required this.subject, + required this.rootAugment, + }) : super(key: key); @override _DynamicClassesAugment createState() => _DynamicClassesAugment(); @@ -26,7 +35,10 @@ class EmailToolbar extends StatefulWidget { class _DynamicClassesAugment extends State { String selectedClass = 'Class 1'; - // TextEditingController _jumpController = TextEditingController(); + TextEditingController _jumpController = TextEditingController(); + TextEditingController _viewspecsController = TextEditingController(); + AugmentTree? localAugment; + List? emailsInThread; // late final FocusNode _JumpItemfocusNode; // late final FocusNode _viewSpecsfocusNode; @@ -47,13 +59,25 @@ class _DynamicClassesAugment extends State { // _viewSpecsfocusNode.addListener(() { // setState(() => _viewSpecsHasFocus = _viewSpecsfocusNode.hasFocus); // }); + localAugment = widget.rootAugment; + _serializableData(widget.emails); + } + + void _serializableData(List threadID) async { + // emailsInThread = await ApiService().threadsInSerializable(); + print("done thread serializable"); + + if (!mounted) return; + // setState(() { + // _isLoaded = true; + // }); } @override void dispose() { // _JumpItemfocusNode.dispose(); // _viewSpecsfocusNode.dispose(); - // _jumpController.dispose(); + _jumpController.dispose(); super.dispose(); } @@ -78,20 +102,20 @@ class _DynamicClassesAugment extends State { child: Text('Attachments'), ), SizedBox(width: 8), - ElevatedButton( - onPressed: AugmentClasses.handleOpen, - child: Text('Open'), - ), + // ElevatedButton( + // onPressed: AugmentClasses.handleOpen, + // child: Text('Open'), + // ), // SizedBox(width: 8), ElevatedButton( onPressed: AugmentClasses.handleFind, child: Text('Find'), ), // SizedBox(width: 8), - ElevatedButton( - onPressed: AugmentClasses.handleStop, - child: Text('Stop'), - ), + // ElevatedButton( + // onPressed: AugmentClasses.handleStop, + // child: Text('Stop'), + // ), ElevatedButton( onPressed: () { AugmentClasses.handleMove(context); @@ -138,10 +162,10 @@ class _DynamicClassesAugment extends State { // width: 8, // ), Container( - width: 50, + width: 100, height: 30, child: TextField( - // controller: _jumpController, + controller: _jumpController, decoration: InputDecoration( border: OutlineInputBorder(), // suffixIcon: Icon(Icons.search) @@ -149,7 +173,7 @@ class _DynamicClassesAugment extends State { onSubmitted: (value) { print("onSubmitted"); if (value.isNotEmpty) { - widget.onJumpToSpan(value); + widget.onJumpToNumbering(value); } }, ), @@ -186,18 +210,23 @@ class _DynamicClassesAugment extends State { onPressed: () => AugmentClasses.ViewSpecsButton(context), child: Text('ViewSpecs:')), Container( - width: 50, + width: 100, height: 30, child: TextField( + controller: _viewspecsController, decoration: InputDecoration( labelText: '', border: OutlineInputBorder(), // suffixIcon: Icon(Icons.style_rounded) ), + onSubmitted: (value) { + widget.onViewspecs(value); + }, ), ), ElevatedButton( - onPressed: () => AugmentClasses.FilterButton(context), + onPressed: () => + AugmentClasses().filterButton(context, widget.onFiltering), child: Text('Filter'), ), SizedBox(width: 8), @@ -207,11 +236,14 @@ class _DynamicClassesAugment extends State { ), // SizedBox(width: 8), ElevatedButton( - onPressed: AugmentClasses.handleFind, + onPressed: () => AugmentClasses() + .handleCreateLink(context, widget.emails, widget.subject, widget.emails[0]), //need to add the email ids child: Text('Create Link'), ), ElevatedButton( - onPressed: AugmentClasses.handleFind, + // onPressed: () => localAugment!.handlePaste(context), + onPressed: () => + AugmentClasses().handlePaste(context), child: Text('Paste Link'), ), ], @@ -222,6 +254,18 @@ class _DynamicClassesAugment extends State { } class AugmentClasses { + CollapsableEmails? localCollapsable; + String? nameOfDocument; + AugmentTree? rootTree; + + void setRootTree(AugmentTree aTree) { + rootTree = aTree; + } + + // AugmentClasses(CollapsableEmails localCollapsable) { + // localCollapsable = localCollapsable; + // } + static OverlayEntry? _overlayEntry; static String? selectedFolder; // Manage selected folder at the class level @@ -487,45 +531,236 @@ class AugmentClasses { print("Find button pressed"); } + AugmentTree? _findAugmentNode(String target, AugmentTree node, int index) { + // so the ideqa is that since the numbering its quite linear, meaning that it tells you where it goes, + // thus i've thought the amount of max moves are only the length of the string of the target + //e.g. if we have a target of 1e9, its steps are the same or time complexity as if it were 1a1, or 99z99 + // since each number or letter tells us which is the index in this array, genius ik + // thus first one needs another function from converting from alphabetical to numbers + if (node.numbering[index] == target[index]) { + _findAugmentNode(target, node.children[index++], index++); + } + } + + bool _checkValidTarget(String target) { + target = target.trim(); + //find if the target exists, + //recursive? + _findAugmentNode(target, rootTree!, 0); + + return false; + } + + _copyLink(String anchor, String target, String format, String viewspecs, + String nameOfDocument, emailID) { + String form = "$anchor < $nameOfDocument, $target :$viewspecs > $emailID"; + final link = ClipboardData(text: form); + Clipboard.setData(link); + } + + Future handleCreateLink(BuildContext context, + List emailsInThread, String nameOfDocument, String emailID) async { + print("create link button pressed"); + final TextEditingController targetController = TextEditingController(); + final TextEditingController anchorController = TextEditingController(); + final TextEditingController viewspecsController = TextEditingController(); + final TextEditingController formatController = TextEditingController(); + + // String anchorPhrase = ''; + String format = 'augment'; + // String target = ''; + // String viewspecs = ''; + + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Create URL Link'), + content: SizedBox( + height: 400, + child: Column( + children: [ + Row( + children: [ + Text("Which email? "), + SizedBox( + width: 350.0, + child: Text(nameOfDocument), + // child: ListView.builder( + // itemCount: emailsInThread.length, + // itemBuilder: (context, index) { + // // var item = emailsInThread[index]; + // // ApiService(). + // return ListTile( + // title: Text(nameOfDocument), + // ); + // }), + ) + ], + ), + Row( + children: [ + Text("Link to target item at: "), + SizedBox( + width: 350.0, + child: TextField( + controller: targetController, + autofocus: true, + maxLines: 1, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ), + ], + ), + SizedBox(height: 8), + Row( + children: [ + ElevatedButton( + onPressed: () => ViewSpecsButton(context), + child: Text("Viewspecs:")), + SizedBox( + width: 150.0, + child: TextField( + controller: viewspecsController, + maxLines: 1, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ), + ], + ), + SizedBox(height: 8), + Row( + children: [ + Text("Using anchor phrase: "), + SizedBox( + width: 150.0, + child: TextField( + controller: anchorController, + maxLines: 1, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ), + ], + ), + SizedBox(height: 8), + Row( + children: [ + Text("Using link format: "), + SizedBox( + width: 250.0, + child: Text("Augment"), + ), + ], + ) + ], + ), + ), + actions: [ + ElevatedButton( + onPressed: () => { + // _checkValidTarget(targetController.text), + + _copyLink( + anchorController.text, + targetController.text, + format, + viewspecsController.text, + nameOfDocument, + emailID, + ), + Navigator.of(context).pop() + }, + child: Text("OK")), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text("Cancel")), + ElevatedButton(onPressed: null, child: Text("Help")), + ], + )); + } + + Future handlePaste(BuildContext context) async { + final TextEditingController gotoLink = TextEditingController(); + + Routinghandler localRouting; + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("GOTO Link"), + content: SizedBox( + height: 400, + child: Column( + children: [ + Row( + children: [ + Text("Paste link to go: "), + SizedBox( + width: 350, + child: TextField( + controller: gotoLink, + maxLines: 1, + autofocus: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + )) + ], + ) + ], + ), + ), + actions: [ + ElevatedButton( + onPressed: () { + print('pressed'); + Navigator.of(context).pop(); + final localRouting = + Routinghandler(gotoLink.text); + final String subject = + localRouting.docName; // This is your :subject + final String target = + localRouting.target; // This is your :target + final String viewspecs = + localRouting.viewspecs; // This is your :viewspecs + final String finalEmailID = localRouting.emailID; + + final encodedSubject = Uri.encodeComponent(subject); + final encodedTarget = Uri.encodeComponent(target); + final encodedViewspecs = Uri.encodeComponent(viewspecs); + final encodedEmailID = Uri.encodeComponent(finalEmailID); + print("emailID $encodedEmailID"); + String link = + "/email/$encodedSubject/$encodedTarget/$encodedViewspecs/$encodedEmailID"; + GoRouter.of(context).go(link); + }, + child: Text("OK")), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text("Cancel")) + ], + )); + } + static void handleStop() { print("Stop button pressed"); } - static void handleJump(String spanId) { - String js_code = ''' - var iframe = document.getElementsByTagName('iframe')[0]; // 0 for the first iframe, 1 for the second, etc. - - // Check if the iframe is loaded and has content - if (iframe && iframe.contentDocument) { - // Access the document inside the iframe - var iframeDoc = iframe.contentDocument || iframe.contentWindow.document; - - // Find the element with the specific id inside the iframe - var targetElement = iframeDoc.getElementById("$spanId"); // Replace '36 ' with the actual id of the target element - - // If the element exists, scroll to it - if (targetElement) { - targetElement.scrollIntoView(); - console.log('Scrolled to element with id "$spanId" inside the iframe.'); - } else { - console.log('Element with id "$spanId" not found inside the iframe.'); - } - } else { - console.log('Iframe not found or not loaded.'); - } - '''; - // js.context.callMethod('eval', [js_code]); + static void handleJump(String value) { + print(value); } static void invisibility(String htmlClass) {} static Future JumpButton(BuildContext context) async { - // FocusNode textFieldFocusNode = FocusNode(); - - // AugmentClasses.disableIframePointerEvents(); await showDialog( barrierDismissible: true, - // barrierColor: Colors.yellow, context: context, builder: (context) => AlertDialog( title: Text('Jump Item:'), @@ -725,7 +960,6 @@ class AugmentClasses { ElevatedButton(onPressed: () {}, child: Text('OK')), ElevatedButton( onPressed: () { - // AugmentClasses.disableIframePointerEvents(); Navigator.of(context).pop(); }, child: Text('Cancel')), @@ -743,61 +977,90 @@ class AugmentClasses { }); } - void handleFilter() {} - static Future FilterButton(context) async { + Future> searchFilter(String query) async { + return []; + } + + Future filterButton( + context, Function(String) onFilteringCallback) async { //this is literally ctrl+F :skull: //idea is to search in file, extract the

tags that contain these //words and highlight, then when zoom, you just jump to that paragraph + bool? numbering = false; + String filterQueue = ''; - // AugmentClasses.disableIframePointerEvents(); await showDialog( - context: context, - builder: (context) => Container( - height: 150, - width: 300, - child: AlertDialog( - title: Text('Filter'), - content: Container( - width: 400, // Set the width to simulate the Windows style - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Set filter:'), - SizedBox( - width: 175, - child: TextField( - maxLines: 1, - decoration: InputDecoration( - border: OutlineInputBorder(), - ), + context: context, + builder: (BuildContext dialogContext) { + return StatefulBuilder(builder: + (BuildContext statefulBuilderContext, StateSetter setState) { + return AlertDialog( + title: const Text('Filter'), + content: SizedBox( + width: 400, // Set the width to simulate the Windows style + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Set filter:'), + SizedBox( + width: 175, + child: TextField( + autofocus: true, + maxLines: 1, + decoration: const InputDecoration( + border: OutlineInputBorder(), ), - ) - ], - ))))); + onChanged: (value) { + print(value); + filterQueue = value; + }, + ), + ), + SizedBox( + height: 10, + ), + Column(children: [ + Row(children: [ + Checkbox( + value: numbering, + activeColor: + Theme.of(context).colorScheme.tertiary, + onChanged: (newBool) { + setState(() { + numbering = newBool; + }); + }), + Text("Start at top of file") + ]), + ]), + ])), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text("Cancel")), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop({ + 'filterQueue': filterQueue, + 'numbering': numbering, + }); + }, + child: Text("Apply")), + ], + ); + }); + }, + ).then((result) { + if (result != null) { + print("filter done $result"); + final String query = result['filterQueue']; + onFilteringCallback(query); + } else { + print('cancelled'); + } + }); } - - // static void disableIframePointerEvents() { - // //pretty sure these dont work - // // final iframes = html.document.getElementsByTagName('iframe'); - // final iframes = web.document.getElementsByTagName('iframe'); - // for (var iframe in iframes) { - // // if (iframe is html.Element) { - // // iframe.style.pointerEvents = 'none'; // Disable pointer events - // if (iframe is web.Element) { - // iframe. - // } - // } - // } - - // static void enableIframePointerEvents() { - // // final iframes = html.document.getElementsByTagName('iframe'); - // final iframes = html.document.getElementsByTagName('iframe'); - - // for (var iframe in iframes) { - // if (iframe is html.Element) { - // iframe.style.pointerEvents = 'auto'; // Re-enable pointer events - // } - // } - // } } diff --git a/lib/collapsableEmails.dart b/lib/collapsableEmails.dart index ceed92a..e8b144d 100644 --- a/lib/collapsableEmails.dart +++ b/lib/collapsableEmails.dart @@ -1,132 +1,3 @@ -import 'dart:js_interop'; -import 'package:web/web.dart' as web; -import 'package:flutter/material.dart'; -import 'dart:ui_web' as ui; -import 'api_service.dart'; -import 'structs.dart'; - -class CollapsableEmails extends StatefulWidget { - final List thread; // email id's in the form xyz@gmail.com - final List threadHTML; - final String threadIDs; - - CollapsableEmails( - {required this.thread, - required this.threadHTML, - required this.threadIDs}); - - @override - State createState() => _CollapsableEmailsState(); -} - -class _CollapsableEmailsState extends State { - List emailsHTML = []; //html of the emails in the thread - // build attachments with the forldar name and id - Set _expandedEmails = {}; //open emails - List viewtypeIDs = []; //IDs of the viewtypes, order matters - List heightOfViewTypes = []; //the height of each viewtype - List emailsInThread = []; - bool _isLoaded = false; - - @override - void initState() { - // TODO: implement initState - super.initState(); - _registerViewFactory(widget.threadHTML); - _serializableData(widget.threadIDs); - } - - void _registerViewFactory(List currentContent) async { - // setState(() { //update to do item per item - // each item to have itsviewtype ID - // is this necessarey here?? - - //could just move to collapsable - - for (var emailHTML in widget.threadHTML) { - String viewTypeId = 'email-${DateTime.now().millisecondsSinceEpoch}'; - - final ghost = web.document.createElement('div') as web.HTMLDivElement - ..style.visibility = 'hidden' - ..style.position = 'absolute' - ..style.width = '100%' - ..style.overflow = 'auto' - ..innerHTML = emailHTML - .toJS; // temporarily index because it has to do all of them - web.document.body?.append(ghost); - await Future.delayed(Duration(milliseconds: 10)); - - final heightOfEmail = ghost.scrollHeight; - ghost.remove(); - - final HTMLsnippet = web.document.createElement('div') - as web.HTMLDivElement - ..id = viewTypeId - ..innerHTML = emailHTML - .toJS; // temporarily index because it has to do all of them - HTMLsnippet.style - ..width = '100%' - ..height = '${heightOfEmail}px' - ..overflow = 'auto' - ..scrollBehavior = 'smooth'; - - ui.platformViewRegistry.registerViewFactory( - viewTypeId, - (int viewId) => HTMLsnippet, - ); - viewtypeIDs.add(viewTypeId); - heightOfViewTypes.add(heightOfEmail); - } - } - - void _serializableData(String threadID) async { - emailsInThread = await ApiService().threadsInSerializable(threadID); - print("done thread serializable"); - if (!mounted) return; - setState(() { - _isLoaded = true; - }); - } - - @override - Widget build(BuildContext context) { - return _isLoaded - ?Column(children: [ - Expanded( - child: ListView.builder( - itemCount: widget.thread.length, - itemBuilder: (context, index) { - final isExpanded = - _expandedEmails.contains(index); //check if email is expanded - return Column( - children: [ - ListTile( - title: Text(emailsInThread[index].from), - trailing: Text(emailsInThread[index].date), - onTap: () { - setState(() { - if (isExpanded) { - _expandedEmails.remove(index); - } else { - _expandedEmails.add(index); - } - }); - }, - ), - if (isExpanded) - // if(viewtypeIDs[index] == null || heightOfViewTypes[index] == null) - // const SizedBox(height: 100, child: Center(child: CircularProgressIndicator())), - SizedBox( - height: heightOfViewTypes[index].toDouble(), - child: HtmlElementView( - key: UniqueKey(), viewType: viewtypeIDs[index]), - ), - Divider(), - ], - ); - }, - ), - ) - ]): const Center(child:CircularProgressIndicator()); - } -} +export 'collapsableEmailsStub.dart' + if (dart.library.io) 'collapsableEmailsAndroid.dart' + if (dart.library.js_interop) 'collapsableEmailsWeb.dart'; \ No newline at end of file diff --git a/lib/collapsableEmailsAndroid.dart b/lib/collapsableEmailsAndroid.dart new file mode 100644 index 0000000..275eb6f --- /dev/null +++ b/lib/collapsableEmailsAndroid.dart @@ -0,0 +1,442 @@ +import 'package:flutter/material.dart'; +import 'api_service.dart'; +import 'structs.dart'; +import 'package:html2md/html2md.dart' as html2md; +import 'package:markdown_widget/markdown_widget.dart'; +import 'package:markdown/markdown.dart' as md; + +class CollapsableEmails extends StatefulWidget { + final List thread; // email id's in the form xyz@gmail.com + // final List threadHTML; + final List threadMarkdown; + final String threadIDs; + final String? targetJumpNumbering; + final String? targetViewspecs; + final String? targetFiltering; + + CollapsableEmails({ + required this.thread, + required this.threadMarkdown, + required this.threadIDs, + this.targetJumpNumbering, + this.targetViewspecs, + this.targetFiltering, + }); + + @override + State createState() => _CollapsableEmailsState(); +} + +class _CollapsableEmailsState extends State { + List emailsHTML = []; //html of the emails in the thread + // build attachments with the forldar name and id + Set _expandedEmails = {}; //open emails + List emailsInThread = []; + bool _isLoaded = false; + + List hirarchy = ["h1", "h2", "h3", "h4", "h5", "h6", "p"]; + Map hirarchyDict = { + "h1": 1, + "h2": 2, + "h3": 3, + "h4": 4, + "h5": 6, + "h6": 7, + "p": 8, + "ul": 8, + "li": 8, + }; + + List tagsCollected = []; + List allMarkdown = []; + List> sentinel = []; + int level = 0; + AugmentTree zoomTreeRoot = AugmentTree(); + // late AugmentTree currentZoomNode; + late List currentZoomTree = []; + bool zoomOut = false; + bool zoomIn = true; + late List threadNodes = []; + static bool leftNumbering = false; + static bool rightNumbering = true; + bool showWhole = false; + + @override + void initState() { + super.initState(); + threadNodes = []; + currentZoomTree = []; + // _markdownConverter(); + _serializableData(widget.threadIDs); // this + _markdown2Tree(widget.threadMarkdown); + } + + @override + void didUpdateWidget(covariant CollapsableEmails oldWidget) { + // TODO: implement didUpdateWidget + super.didUpdateWidget(oldWidget); + if (widget.targetJumpNumbering != null && + widget.targetJumpNumbering != oldWidget.targetJumpNumbering) { + _handleJump(widget.targetJumpNumbering!); + } + if (widget.targetViewspecs != null && + widget.targetViewspecs != oldWidget.targetViewspecs) { + _handleViewspecs(widget.targetViewspecs!); + } + } + + @override + void dispose() { + super.dispose(); + } + + + List getThreads() { + return widget.thread; + } + + void _add2Tree(AugmentTree tree, md.Element node2add) { + // adds node to its corresponding place + AugmentTree newNode = AugmentTree(); + newNode.setData(node2add.textContent); + newNode.ogTag = node2add.tag; + // cases, + //1. a node that comes is lower than the root.children last, if so it goes beneath it + if (tree.children.isEmpty) { + // new level to be created when totally empty + tree.children.add(newNode); + newNode.parent = tree; + } else if (tree.children.isNotEmpty && + tree.children.last.ogTag.isNotEmpty) { + if ((hirarchyDict[node2add.tag] ?? + -1) < // e.g. new node is h1 and old is h2, heapify + (hirarchyDict[tree.children.last.ogTag] ?? -1)) { + //have to figure out the borthers + //assuming it all goes right + if ((hirarchyDict[node2add.tag] ?? -1) == -1 || + (hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) { + print( + 'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} < ${hirarchyDict[tree.children.last.ogTag] ?? -1}'); + return; + } else if (tree.children.last.parent == null) { + // becomes the new top level + for (AugmentTree brother in tree.children) { + brother.parent = newNode; + } + tree.children = [newNode]; + } else { + newNode.parent = tree; + tree.children.add(newNode); + } + } else if ((hirarchyDict[node2add.tag] ?? + -1) > // go down e.g. new node is h3 and old is h2 or something + (hirarchyDict[tree.children.last.ogTag] ?? -1)) { + if ((hirarchyDict[node2add.tag] ?? -1) == -1 || + (hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) { + print( + 'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} > ${hirarchyDict[tree.children.last.ogTag] ?? -1}'); + print("-1 ${tree.children.last.ogTag}"); + return; + } + + _add2Tree(tree.children.last, node2add); + } else if ((hirarchyDict[node2add.tag] ?? -1) == + (hirarchyDict[tree.children.last.ogTag] ?? -1)) { + tree.children.add(newNode); + newNode.parent = tree; + } + } + } + + void _markdown2Tree(List text) { + print("started markdown2tree"); + for (int emailsMD = 0; emailsMD < text.length; emailsMD++) { + final List 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()); + } +} diff --git a/lib/collapsableEmailsStub.dart b/lib/collapsableEmailsStub.dart new file mode 100644 index 0000000..e4ec443 --- /dev/null +++ b/lib/collapsableEmailsStub.dart @@ -0,0 +1,32 @@ +import 'structs.dart'; +import 'package:flutter/material.dart'; + +class CollapsableEmails extends StatefulWidget { + final List thread; // email id's in the form xyz@gmail.com + final List threadMarkdown; + final String threadIDs; + + CollapsableEmails( + {required this.thread, + required this.threadMarkdown, + required this.threadIDs, String? targetJumpNumbering, String? targetViewspecs, String? targetFiltering, required String nameOfDocument}); + + get getThreads => null; + + get getAugmentRoot => null; + + @override + State createState() => _CollapsableEmailsState(); +} + +class _CollapsableEmailsState extends State { + + List getThreads() { + return widget.thread; + } + + @override + Widget build(BuildContext context) { + return Scaffold(body: Text("collapsable stud")); + } +} diff --git a/lib/collapsableEmailsWeb.dart b/lib/collapsableEmailsWeb.dart new file mode 100644 index 0000000..b758783 --- /dev/null +++ b/lib/collapsableEmailsWeb.dart @@ -0,0 +1,582 @@ +import 'package:flutter/material.dart'; +import 'api_service.dart'; +import 'structs.dart'; +import 'package:markdown_widget/markdown_widget.dart'; +import 'package:markdown/markdown.dart' as md; + +class CollapsableEmails extends StatefulWidget { + final List thread; // email id's in the form xyz@gmail.com + // final List threadHTML; to be replaced with the MD + final List threadMarkdown; + final String threadIDs; + final String? targetJumpNumbering; + final String? targetViewspecs; + final String? targetFiltering; + final String? nameOfDocument; + + const CollapsableEmails({ + required this.thread, + // required this.threadHTML, + required this.threadMarkdown, + required this.threadIDs, + this.targetJumpNumbering, + this.targetViewspecs, + this.targetFiltering, + this.nameOfDocument, + }); + + @override + State createState() => _CollapsableEmailsState(); + + AugmentTree? getAugmentRoot() { + return _CollapsableEmailsState().getAugmentRoot(); + } +} + +class _CollapsableEmailsState extends State { + List emailsHTML = []; //html of the emails in the thread + // build attachments with the forldar name and id + Set _expandedEmails = {}; //open emails + + List emailsInThread = []; + bool _isLoaded = false; + List hirarchy = ["h1", "h2", "h3", "h4", "h5", "h6", "p"]; + Map hirarchyDict = { + "h1": 1, + "h2": 2, + "h3": 3, + "h4": 4, + "h5": 5, + "h6": 6, + "p": 8, + "ul": 8, + "li": 8, + }; + + List tagsCollected = []; + List allMarkdown = []; + List> sentinel = []; + int level = 0; + AugmentTree zoomTreeRoot = AugmentTree(); + // late AugmentTree currentZoomNode; + late List currentZoomTree = + []; // holds a list of list that holds the list of nodes on the currentzoom + bool zoomOut = false; + bool zoomIn = true; + late List threadNodes = []; + static bool leftNumbering = true; + static bool rightNumbering = true; + bool showWhole = false; + List queryResults = []; //results of conducting filtering + bool _isFilteringActive = false; + + @override + void initState() { + super.initState(); + threadNodes = []; + currentZoomTree = []; + // _markdownConverter(); + _serializableData(widget.threadIDs); // this + _markdown2Tree(widget.threadMarkdown); + } + + @override + void didUpdateWidget(covariant CollapsableEmails oldWidget) { + // TODO: implement didUpdateWidget + super.didUpdateWidget(oldWidget); + if (widget.targetJumpNumbering != null && + widget.targetJumpNumbering != oldWidget.targetJumpNumbering) { + _handleJump(widget.targetJumpNumbering!); + } + if (widget.targetViewspecs != null && + widget.targetViewspecs != oldWidget.targetViewspecs) { + _handleViewspecs(widget.targetViewspecs!); + } + if (widget.targetFiltering != null && + widget.targetFiltering != oldWidget.targetFiltering) { + _handleFilterQuery(zoomTreeRoot, widget.targetFiltering!); + } + } + + @override + void dispose() { + super.dispose(); + } + + List getThreads() { + return emailsInThread; + } + + AugmentTree getAugmentRoot() { + return zoomTreeRoot; + } + + void _add2Tree(AugmentTree tree, md.Element node2add) { + // adds node to its corresponding place + AugmentTree newNode = AugmentTree(); + newNode.setData(node2add.textContent); + newNode.ogTag = node2add.tag; + // cases, + //1. a node that comes is lower than the root.children last, if so it goes beneath it + if (tree.children.isEmpty) { + // new level to be created when totally empty + tree.children.add(newNode); + newNode.parent = tree; + } else if (tree.children.isNotEmpty && + tree.children.last.ogTag.isNotEmpty) { + if ((hirarchyDict[node2add.tag] ?? + -1) < // e.g. new node is h1 and old is h2, heapify + (hirarchyDict[tree.children.last.ogTag] ?? -1)) { + //have to figure out the borthers + //assuming it all goes right + if ((hirarchyDict[node2add.tag] ?? -1) == -1 || + (hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) { + print( + 'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} < ${hirarchyDict[tree.children.last.ogTag] ?? -1}'); + return; + } else if (tree.children.last.parent == null) { + // becomes the new top level + for (AugmentTree brother in tree.children) { + brother.parent = newNode; + } + tree.children = [newNode]; + } else { + newNode.parent = tree; + tree.children.add(newNode); + } + } else if ((hirarchyDict[node2add.tag] ?? + -1) > // go down e.g. new node is h3 and old is h2 or something + (hirarchyDict[tree.children.last.ogTag] ?? -1)) { + if ((hirarchyDict[node2add.tag] ?? -1) == -1 || + (hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) { + print( + 'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} > ${hirarchyDict[tree.children.last.ogTag] ?? -1}'); + print("-1 ${tree.children.last.ogTag}"); + return; + } + + _add2Tree(tree.children.last, node2add); + } else if ((hirarchyDict[node2add.tag] ?? -1) == + (hirarchyDict[tree.children.last.ogTag] ?? -1)) { + tree.children.add(newNode); + newNode.parent = tree; + } + } + } + + void _markdown2Tree(List text) { + print("started markdown2tree"); + for (int emailsMD = 0; emailsMD < text.length; emailsMD++) { + final List nakedList = + md.Document().parseLines(text[emailsMD].split('\n')); + zoomTreeRoot = AugmentTree(); + for (var node in nakedList) { + //maybe do an add function, but isn't this it? + if (node is md.Element) { + AugmentTree temp = AugmentTree(); + temp.data = node.textContent; + temp.ogTag = node.tag; + //why did i do this??? + if ( hirarchyDict.containsKey(node.tag)) { + _add2Tree(zoomTreeRoot, node); + } + } + } + zoomTreeRoot.addNumbering(); + threadNodes.add(zoomTreeRoot); + currentZoomTree.add(zoomTreeRoot); + } + + if (!mounted) return; + setState(() { + _isLoaded = true; + }); + } + + void _goToChildren(int indexThread, int index) async { + final target = currentZoomTree[indexThread].children[index]; + if (target.children.isNotEmpty) { + setState(() { + currentZoomTree[indexThread] = target; + }); + } else { + print("This child has no further children."); + } + } + + void _goToChildrenFiltering( + int indexThread, int index, AugmentTree node) async { + final target = node; + if (target.children.isNotEmpty) { + setState(() { + currentZoomTree[indexThread] = target; + _isFilteringActive = false; + }); + for (var child in target.children) { + print(child.data); + } + } else { + print("This child has no further children."); + } + } + + void _goToParent(int indexThread) async { + if (currentZoomTree[indexThread].parent != null) { + setState(() { + currentZoomTree[indexThread] = currentZoomTree[indexThread].parent!; + }); + } else { + print("Already at root."); + } + } + + void _goToParentFiltering(int indexThread, AugmentTree node) async { + if (node.parent != null) { + setState(() { + currentZoomTree[indexThread] = node.parent!; + }); + } else { + print("Already at root."); + } + } + + void _serializableData(String threadID) async { + emailsInThread = await ApiService().threadsInSerializable(threadID); + print("done thread serializable"); + + if (!mounted) return; + setState(() { + _isLoaded = true; + }); + } + + Widget _buildForZooms(int indexThread) { + // index of email in thread, currentZoomTree, + // + if (!_isLoaded) { + return const Center(child: CircularProgressIndicator()); // loading screen + } + + AugmentTree + currentZoomNodeForThisEmail = //each index is an email in the thread + currentZoomTree[indexThread]; + print(currentZoomNodeForThisEmail.data); + print(currentZoomNodeForThisEmail.children); + print(currentZoomNodeForThisEmail.parent); + + // if (_isFilteringActive) { + // nodesToDisplay = queryResults; + // } else { + // nodesToDisplay = currentZoomNodeForThisEmail.children; + // } + final canZoomOut = currentZoomNodeForThisEmail.parent != null; + + if (_isFilteringActive) { + return ListView.builder( + itemCount: queryResults.length, + itemBuilder: (context, index) { + AugmentTree childNode = queryResults[index]; + bool canZoomIn = childNode.children.isNotEmpty; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Material( + elevation: 1, + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.surface, + surfaceTintColor: Theme.of(context).colorScheme.surfaceBright, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 4.0, + children: [ + OutlinedButton( + onPressed: () => { + setState(() { + _goToParentFiltering(indexThread, childNode); + _isFilteringActive = false; + }) + }, + child: Icon(Icons.north_west_sharp), + ), + OutlinedButton( + onPressed: canZoomIn + ? () => _goToChildrenFiltering( + indexThread, index, childNode) + : null, + child: Icon(Icons.south_east_sharp), + ), + ], + ), + SizedBox(width: 12.0), + if (leftNumbering) + Padding( + padding: const EdgeInsets.fromLTRB(0, 10, 5, 0), + child: Text( + childNode.numbering, + style: TextStyle( + color: Color(Colors.purple[400]!.value)), + ), + ), + Expanded( + child: MarkdownBlock( + data: childNode.data, + config: MarkdownConfig + .darkConfig, // or lightConfig depending on theme + ), + ), + if (rightNumbering) + Padding( + padding: const EdgeInsets.fromLTRB(5, 10, 5, 0), + child: Text( + childNode.numbering, + style: TextStyle( + color: Color(Colors.purple[400]!.value)), + ), + ), + ], + ), + ), + ), + ); + }); + } + + return ListView.builder( + itemCount: currentZoomNodeForThisEmail.children.length, + itemBuilder: (context, index) { + final childNode = currentZoomNodeForThisEmail.children[index]; + final canZoomIn = childNode.children.isNotEmpty; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + child: Material( + elevation: 1, + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.surface, + surfaceTintColor: Theme.of(context).colorScheme.surfaceBright, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 4.0, + children: [ + OutlinedButton( + onPressed: + canZoomOut ? () => _goToParent(indexThread) : null, + child: Icon(Icons.north_west_sharp), + ), + OutlinedButton( + onPressed: canZoomIn + ? () => _goToChildren(indexThread, index) + : null, + child: Icon(Icons.south_east_sharp), + ), + ], + ), + SizedBox(width: 12.0), + if (leftNumbering) + Padding( + padding: const EdgeInsets.fromLTRB(0, 10, 5, 0), + child: Text( + childNode.numbering, + style: + TextStyle(color: Color(Colors.purple[400]!.value)), + ), + ), + Expanded( + child: MarkdownBlock( + data: childNode.data, + config: MarkdownConfig + .darkConfig, // or lightConfig depending on theme + ), + ), + if (rightNumbering) + Padding( + padding: const EdgeInsets.fromLTRB(5, 10, 5, 0), + child: Text( + childNode.numbering, + style: + TextStyle(color: Color(Colors.purple[400]!.value)), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + void _handleJump(String queryNumbering) { + print(queryNumbering); + if (queryNumbering.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Please enter a numbering to jump to.')), + ); + return; + } + + final int targetEmailIndex = _expandedEmails.first; + if (targetEmailIndex >= threadNodes.length) { + // Error handling + return; + } + + final AugmentTree rootOfCurrentEmail = threadNodes[targetEmailIndex]; + final AugmentTree? foundNode = + _findNodeByNumbering(rootOfCurrentEmail, queryNumbering); + + if (foundNode != null) { + setState(() { + currentZoomTree[targetEmailIndex] = foundNode; // Update the state + }); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Numbering "$queryNumbering" not found.')), + ); + } + } + + void _handleViewspecs(String viewspecsQuery) { + print(viewspecsQuery); + if (viewspecsQuery.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Please enter the viewspecs.')), + ); + return; + } + + final int targetEmailIndex = _expandedEmails.first; + if (targetEmailIndex >= threadNodes.length) { + // Error handling + return; + } + + if (viewspecsQuery.contains('n')) { + setState(() { + leftNumbering = false; // Update the state + rightNumbering = false; + }); + } + if (viewspecsQuery.contains('m')) { + setState(() { + rightNumbering = true; + leftNumbering = true; + }); + } + if (viewspecsQuery.contains('H')) { + setState(() { + leftNumbering = !leftNumbering; + }); + } + if (viewspecsQuery.contains('G')) { + setState(() { + rightNumbering = !rightNumbering; + }); + } + if (viewspecsQuery.contains('w')) { + setState(() { + showWhole = true; + }); + } + + // else { + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar(content: Text('Numbering "$viewspecsQuery" not found.')), + // ); + // } + } + + void _findNodesContainingStrDFS( + AugmentTree node, String query, List results) { + if (node.data.contains(query)) { + results.add(node); + } + for (var child in node.children) { + _findNodesContainingStrDFS(child, query, results); + } + } + + List _handleFilterQuery(AugmentTree root, String query) { + List results = []; + final int targetEmailIndex = _expandedEmails.first; + _findNodesContainingStrDFS(root, query, results); + print(results); + for (var res in results) { + print(res.data); + } + if (results.isNotEmpty) { + setState(() { + queryResults = results; + // currentZoomTree[targetEmailIndex] = results.first; // Update the state + _isFilteringActive = true; + currentZoomTree[targetEmailIndex] = root; + }); + print(currentZoomTree); + } + return results; + } + + AugmentTree? _findNodeByNumbering(AugmentTree root, String numbering) { + //recursively finds the node you mentioned + // to find the AugmentTree node corresponding to the `numbering`. + if (root.numbering == numbering) { + return root; + } + for (var child in root.children) { + final found = _findNodeByNumbering(child, numbering); + if (found != null) { + return found; + } + } + return null; + } + + @override + Widget build(BuildContext context) { + return _isLoaded + ? Column(children: [ + Expanded( + child: ListView.builder( + itemCount: emailsInThread.length, + itemBuilder: (context, index) { + final isExpanded = _expandedEmails + .contains(index); //check if email is expanded + return Column( + children: [ + ListTile( + title: Text(emailsInThread[index].from), + trailing: Text(emailsInThread[index].date), + onTap: () { + setState(() { + if (isExpanded) { + _expandedEmails.remove(index); + } else { + _expandedEmails.add(index); + } + }); + }, + ), + if (isExpanded) + ConstrainedBox( + constraints: BoxConstraints( + minHeight: 100, + maxHeight: MediaQuery.of(context).size.height * 0.6, + ), + child: _buildForZooms(index), //show the tree + ), + Divider(), + ], + ); + }, + ), + ), + ]) + : const Center(child: CircularProgressIndicator()); + } +} diff --git a/lib/contact.dart b/lib/contact.dart index 73d60fd..3829055 100644 --- a/lib/contact.dart +++ b/lib/contact.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:flutter_html/flutter_html.dart'; class ContactsPage extends StatefulWidget { const ContactsPage({super.key}); diff --git a/lib/email.dart b/lib/email.dart index 5dbf1a6..bae89c4 100644 --- a/lib/email.dart +++ b/lib/email.dart @@ -1,57 +1,227 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:markdown/markdown.dart' as md; import 'api_service.dart'; import 'structs.dart'; +import 'emailView.dart'; +import 'Compose.dart'; -class EmailListScreen extends StatelessWidget { +class EmailListScreen extends StatefulWidget { final List emails; final Future> Function(List, String) getEmailContent; final String folder; + final GlobalKey<_EmailListScreenState> key; + final Function(List)? onSelectionChanged; + + EmailListScreen({ + required this.key, + required this.emails, + required this.getEmailContent, + required this.folder, + this.onSelectionChanged, + }) : super(key: key); + + @override + _EmailListScreenState createState() => _EmailListScreenState(); +} + +class _EmailListScreenState extends State + with TickerProviderStateMixin { + late List selectStates; // for checkboxes if its selected or not + late List selectedEmails = + []; // holds the emails that are selected i.e. the emails that got the checkbox on + final Set _hoveredRows = {}; //the row that is being hovered over atm + bool bulkSelectMenu = false; + final GlobalKey _emailPageKey = GlobalKey(); + + @override + void initState() { + super.initState(); + selectStates = List.filled(widget.emails.length, false); + } + + @override + void didUpdateWidget(covariant EmailListScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.emails.length != widget.emails.length) { + selectStates = List.filled(widget.emails.length, false); + } + } + + bool selectAllChecks(bool selectionType) { + //perhaps it should return a list of the selected + setState(() { + selectedEmails = []; + if (selectionType) { + bulkSelectMenu = true; + for (int email = 0; email < selectStates.length; email++) { + selectStates[email] = selectionType; + selectedEmails.add(widget.emails[email]); + } + } else { + for (int email = 0; email < selectStates.length; email++) { + selectStates[email] = selectionType; + } + selectedEmails = []; + } + }); + widget.onSelectionChanged?.call(selectedEmails); + printTheSelected(); + return false; + } + + bool markAsRead(bool read) { + print("markasread $read"); + setState(() { + if (read) { + //read + for (int email = 0; email < selectedEmails.length; email++) { + selectedEmails[email].seen = read; + ApiService() + .markAsSeen(selectedEmails[email].id); //the remote or .json + print(selectedEmails[email].id); + + } + } else { + //unread + for (int email = 0; email < selectedEmails.length; email++) { + selectedEmails[email].seen = read; + ApiService() + .markAsUnseen(selectedEmails[email].id); //the remote or .json + print(selectedEmails[email].id); + } + } + }); + return false; + } + + bool moveOfSelected(String destinyFolder) { + //this should be called from a widget + print("move of folder"); + setState(() { + for (int email = 0; email < selectedEmails.length; email++) { + ApiService().moveEmail( + widget.folder, selectedEmails[email].id.toString(), destinyFolder); + } + }); + return false; + } + + // Widget moveOfFolderWidget() + + List listOfSelectedThreads() { + return selectedEmails; + } + + void printTheSelected() { + for (int i = 0; i < selectedEmails.length; i++) { + print(selectedEmails[i].subject); + } + } - EmailListScreen( - {required this.emails, - required this.getEmailContent, - required this.folder}); -//fix the email list @override Widget build(BuildContext context) { return Scaffold( body: ListView.separated( - itemCount: emails.length, + itemCount: widget.emails.length, itemBuilder: (context, index) { - final email = emails[index]; - return ListTile( - title: Text(email.from_name, - style: TextStyle(fontWeight: FontWeight.bold)), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text(email.subject)], - ), - trailing: Text(email.date.toString()), - onTap: () async { - List emailContent = // list of the html - await getEmailContent(email.messages, folder); - // print("this is what email.messages look like in email.dart ${email.messages}"); - // List emailIds = email.messages; + Color seenColour; + final email = widget.emails[index]; + if (email.seen) { + seenColour = ThemeData().highlightColor; + } else { + seenColour = Colors.transparent; + } + return MouseRegion( + onEnter: (_) => setState(() => _hoveredRows.add(index)), + onExit: (_) => setState(() => _hoveredRows.remove(index)), + child: ListTile( + leading: Checkbox( + value: selectStates[index], + onChanged: (bool? value) { + setState(() { + //works great + selectStates[index] = value ?? false; - print(email.messages); //email ids of the thread - Navigator.push( - context, - MaterialPageRoute( - // could call collapsable and inside collable each calls email view? - builder: (context) => EmailView( - emailContent: emailContent, - from: email.from_address, - name: email.from_name, - to: email.to.toString(), - subject: email.subject, - date: email.date.toString(), - id: email.id.toString(), //i think this is thread id? - messages: email.messages, - ), - ), - ); - }, + setState(() { + if (value!) { + selectedEmails.add(widget.emails[index]); + //here i must update the other side + _emailPageKey.currentState?.getListOfSelected(); + } else { + selectedEmails.remove(widget.emails[index]); + _emailPageKey.currentState?.getListOfSelected(); + } + widget.onSelectionChanged?.call(selectedEmails); + print(selectedEmails); + }); + }); + }, + ), + title: Text(email.from_name, + style: TextStyle(fontWeight: FontWeight.bold)), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text(email.subject)], + ), + trailing: _hoveredRows.contains(index) + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon(Icons.mark_email_read_outlined), + onPressed: () { + // mark email as read + setState(() { + widget.emails[index].seen = true; + ApiService().markAsSeen(email.id); + }); + }, + ), + IconButton( + icon: Icon(Icons.delete_outline), + onPressed: () { + // delete email + ApiService().deleteEmail(widget.folder, email.id); + }, + ), + ], + ) + : Text(email.date.toString()), + hoverColor: Colors.transparent, + tileColor: seenColour, + onTap: () async { + List emailContent = // list of the html + await widget.getEmailContent(email.messages, widget.folder); + // print("thread id? $email.id"); yes + print(email.messages); //email ids of the thread + if (widget.folder == "Drafts") { + print("IN DRAFTS MOVE THE CONTENT TO THE WRITING THING"); + //open the compose + OverlayService _thisInstance = OverlayService(); + _thisInstance.draftID = email; + _thisInstance.showPersistentWidget(context); + } else { + // print(email) + Navigator.push( + context, + MaterialPageRoute( + // could call collapsable and inside collable each calls email view? + builder: (context) => EmailView( + emailContent: emailContent, + from: email.from_address, + name: email.from_name, + to: email.to.toString(), + subject: email.subject, + date: email.date.toString(), + id: email.id.toString(), //i think this is thread id? + messages: email.messages, + ), + ), + ); + ApiService().markAsSeen(email.id); + } + }, + ), ); }, separatorBuilder: (context, index) => Divider(), @@ -62,10 +232,12 @@ class EmailListScreen extends StatelessWidget { // ignore: must_be_immutable class EmailPage extends StatefulWidget { - EmailPage({Key? key}) : super(key: key); String selectedFolder = "INBOX"; //starter int offset = 0; int page = 1; + final Function(List)? onSelectionChanged; + + EmailPage({Key? key, this.onSelectionChanged}) : super(key: key); @override EmailPageState createState() => EmailPageState(); @@ -78,6 +250,9 @@ class EmailPageState extends State { int page = 1; bool isBackDisabled = false; + final GlobalKey<_EmailListScreenState> emailListKey = + GlobalKey<_EmailListScreenState>(); + @override void initState() { super.initState(); @@ -86,6 +261,7 @@ class EmailPageState extends State { _fetchEmails(); } + List get getEmails => emails; String getPage() => widget.page.toString(); bool get backDisabled => isBackDisabled; @@ -116,7 +292,6 @@ class EmailPageState extends State { } }); } - // print(currentPage); print(widget.page); _fetchEmails(); } @@ -124,7 +299,7 @@ class EmailPageState extends State { void _fetchEmails() async { try { List fetchedEmails = await apiService - .fetchEmailsFromFolder(widget.selectedFolder, widget.offset); + .fetchEmailsFromFolderReversed(widget.selectedFolder, widget.offset); if (!mounted) return; setState(() { @@ -135,14 +310,36 @@ class EmailPageState extends State { } } + bool selectAllEmails(bool selectionType) { + emailListKey.currentState?.selectAllChecks(selectionType); + return selectionType; + } + + bool markSelectedAsRead(bool selectionType) { + emailListKey.currentState?.markAsRead(selectionType); + return selectionType; + } + + bool moveSelectedOfFolder(String folder) { + emailListKey.currentState?.moveOfSelected(folder); + return false; + } + + List getListOfSelected() { + return emailListKey.currentState!.listOfSelectedThreads(); + } + // return [GetThreadResponse(id: 1, messages: [], subject: "subject", date: DateTime(2025), from_name: "from_name", from_address: "from_address", to: [], seen: false)]; + @override Widget build(BuildContext context) { return Scaffold( - body: EmailListScreen( - emails: emails, - getEmailContent: apiService.fetchEmailContent, - folder: widget.selectedFolder, //try to grab from it directly - ), - ); + body: EmailListScreen( + key: emailListKey, + emails: emails, + // getEmailContent: apiService.fetchEmailContent, + getEmailContent: apiService.fetchMarkdownContent, + folder: widget.selectedFolder, //try to grab from it directly + onSelectionChanged: widget.onSelectionChanged, + )); } } diff --git a/lib/emailView.dart b/lib/emailView.dart new file mode 100644 index 0000000..61c9bf8 --- /dev/null +++ b/lib/emailView.dart @@ -0,0 +1,3 @@ +export 'emailViewStub.dart' + if (dart.library.io) 'emailViewAndroid.dart' + if (dart.library.js_interop) 'emailViewWeb.dart'; \ No newline at end of file diff --git a/lib/emailViewAndroid.dart b/lib/emailViewAndroid.dart new file mode 100644 index 0000000..5ef2efd --- /dev/null +++ b/lib/emailViewAndroid.dart @@ -0,0 +1,120 @@ +import 'package:crab_ui/augment.dart'; +import 'package:crab_ui/collapsableEmailsAndroid.dart'; +import 'package:flutter/material.dart'; +// import 'dart:ui_web' as ui; +// import 'augment.dart'; +// // import 'dart:js_interop' as js; //eventually for manipulating css +// import 'package:pointer_interceptor/pointer_interceptor.dart'; +// import 'collapsableEmails.dart'; +// import 'api_service.dart'; + +class EmailView extends StatefulWidget { + final List emailContent; + final String from; + final String name; + final String to; + final String subject; + final String date; + final String id; + final List 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 { + + @override + void initState() { + super.initState(); + } + + void _scrollToNumber(String spanId) { + // AugmentClasses.handleJump(spanId); + } + void _viewSpecs(String command){ + + } + + void _filteringQuery(String query){ + + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.name), + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + EmailToolbar( + onButtonPressed: () => {}, + onJumpToNumbering: _scrollToNumber, + onViewspecs: _viewSpecs, + onFiltering: _filteringQuery, + emails: widget.messages, subject: '', rootAugment: null, + ), + Row( + children: [ + Expanded( + child: Text( + widget.subject, + style: TextStyle(fontSize: 15), + overflow: TextOverflow.visible, + softWrap: true, + ), + ), + ], + ), + Row( + children: [ + Text( + 'from ${widget.name}', + style: TextStyle(fontSize: 8), + ), + Text( + '<${widget.from}>', + style: TextStyle(fontSize: 8), + ), + Spacer(), + Text( + widget.date, + textAlign: TextAlign.right, + ) + ], + ), + Row( + children: [ + Text( + 'to ${widget.to.toString()}', + style: TextStyle(fontSize: 8), + ) + ], + ), + Expanded( + child: CollapsableEmails( + thread: widget.messages, + threadMarkdown: widget.emailContent, + threadIDs: widget.id, + ), + ), + ], + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/emailViewStub.dart b/lib/emailViewStub.dart new file mode 100644 index 0000000..5c5f657 --- /dev/null +++ b/lib/emailViewStub.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + + +class EmailView extends StatefulWidget { + final List emailContent; + final String from; + final String name; + final String to; + final String subject; + final String date; + final String id; + final List 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 { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Text(" emailview stub, not supported") + ) + ); + } + +} \ No newline at end of file diff --git a/lib/emailViewWeb.dart b/lib/emailViewWeb.dart new file mode 100644 index 0000000..385af48 --- /dev/null +++ b/lib/emailViewWeb.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'dart:ui_web' as ui; +import 'augment.dart'; +import 'collapsableEmails.dart'; +import 'api_service.dart'; + +class EmailView extends StatefulWidget { + final List emailContent; + final String from; + final String name; + final String to; + final String subject; + final String date; + final String id; + final List 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 { + //html css rendering thing + late Key iframeKey; + late String currentContent; + late String viewTypeId; //make this a list too??? + // TextEditingController _jumpController = TextEditingController(); + late EmailToolbar toolbarInstance = EmailToolbar( + onJumpToNumbering: _handleJumpRequest, + onViewspecs: _handleViewspecsRequest, + onButtonPressed: () => {print("email tool bar pressed")}, + onFiltering: _handleFiltering, + emails: widget.messages, + subject: widget.subject, + rootAugment: localCollapsable.getAugmentRoot(), + ); + + late CollapsableEmails localCollapsable = CollapsableEmails( + //change here + thread: widget.messages, //this wont work in serializable + // threadHTML: widget.emailContent, // old html + threadMarkdown: widget.emailContent, + threadIDs: widget.id, + targetJumpNumbering: _targetJumpNumbering, + targetViewspecs: _targetViewspecs, + targetFiltering: _queryFiltering, + nameOfDocument: widget.subject, + ); + + final hardcodedMarkers = [ + {'id': 'marker1', 'x': 50, 'y': 100}, + {'id': 'marker2', 'x': 150, 'y': 200}, + {'id': 'marker3', 'x': 250, 'y': 300}, + ]; + String? _targetJumpNumbering; + String? _targetViewspecs; + String? _queryFiltering; + + @override + void initState() { + super.initState(); + print("thread id? ${widget.id}"); + List 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); + print("email content in Collapsable ${widget.emailContent}"); + + } + + void _scrollToNumber(String spanId) { + AugmentClasses.handleJump(spanId); + } + + void _handleJumpRequest(String numbering) { + setState(() { + _targetJumpNumbering = numbering; + }); + } + + void _handleViewspecsRequest(String viewspecsCommand) { + setState(() { + _targetViewspecs = viewspecsCommand; + }); + } + + void _handleFiltering(String query) { + setState(() { + _queryFiltering = query; + }); + } + + @override + Widget build(BuildContext context) { + ApiService.currThreadID = widget.id; + // AugmentClasses localAugment = AugmentClasses(localCollapsable); + return Scaffold( + appBar: AppBar( + title: Text(widget.name), + ), + body: Stack( + children: [ + Column( + children: [ + toolbarInstance, + Row( + // title of email + children: [ + Text( + widget.subject, + style: TextStyle(fontSize: 30), + ), + ], + ), + Row( + children: [ + Text( + 'from ${widget.name}', + style: TextStyle(fontSize: 18), + ), + Text( + '<${widget.from}>', + style: TextStyle(fontSize: 18), + ), + Spacer(), + Text( + '${widget.date}', + textAlign: TextAlign.right, + ) + ], + ), + // TODO: make a case where if one of these is the user's email it just says me :))))) + Row( + children: [ + Text( + 'to ${widget.to.toString()}', + style: TextStyle(fontSize: 15), + ) + ], + ), + Expanded( + child: localCollapsable, + ), + ], + ), + ], + )); + } +} diff --git a/lib/home_page.dart b/lib/home_page.dart index b4878b0..d4fe108 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -1,13 +1,13 @@ import 'package:crab_ui/sonicEmailView.dart'; +import 'package:go_router/go_router.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'folder_drawer.dart'; import 'structs.dart'; -import 'package:flutter/widgets.dart'; import 'api_service.dart'; import 'package:flutter/material.dart'; import 'email.dart'; -// import 'package:shared_preferences/shared_preferences.dart'; -// import 'serialize.dart'; +import 'Compose.dart'; class HomeScreen extends StatefulWidget { @override @@ -21,10 +21,23 @@ class _HomeScreenState extends State with TickerProviderStateMixin { bool _isSidebarOpen = true; bool querySearches = false; String? _selectedOption = "INBOX"; + List _checkBulk = [ + "All", + "None", + "Read", + "Unread", + "Starred", + "Unstarred" + ]; + bool _checkboxState = false; + bool bulkOptionsState = false; + List selectedThreads = + []; //this should store the emails that are being stored downstream too List _tabs = ['Emails']; Map _tabWidgets = {}; TabController? _tabController; + OverlayEntry? _overlayEntry; @override void initState() { @@ -32,6 +45,11 @@ class _HomeScreenState extends State with TickerProviderStateMixin { _tabController = TabController(length: _tabs.length, vsync: this); _tabWidgets['Emails'] = EmailPage( key: _emailPageKey, + onSelectionChanged: (updatedList) { + setState(() { + selectedThreads = updatedList; + }); + }, ); } @@ -47,49 +65,50 @@ class _HomeScreenState extends State with TickerProviderStateMixin { }); } - void _showOptionsSearchDialog () async { + void _showOptionsSearchDialog() async { List folders = await apiService.fetchFolders(); if (mounted) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Choose an Option'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: folders.map((option) { - return ListTile( - title: Text(option), - leading: Radio( - value: option, - groupValue: _selectedOption, // Bind with _selectedOption - onChanged: (String? value) { - setState(() { - _selectedOption = value; - }); - Navigator.of(context).pop(); // Close the dialog on selection - }, - ), - ); - }).toList(), - ), - actions: [ - ElevatedButton( - child: Text('Submit'), - onPressed: () { - Navigator.of(context).pop(); // Close the dialog - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('You selected: $_selectedOption'), - )); - }, + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Choose an Option'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: folders.map((option) { + return ListTile( + title: Text(option), + leading: Radio( + value: option, + groupValue: _selectedOption, // Bind with _selectedOption + onChanged: (String? value) { + setState(() { + _selectedOption = value; + }); + Navigator.of(context) + .pop(); // Close the dialog on selection + }, + ), + ); + }).toList(), ), - ], - ); - }, - );} + actions: [ + ElevatedButton( + child: Text('Submit'), + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('You selected: $_selectedOption'), + )); + }, + ), + ], + ); + }, + ); + } } - // Remove a tab void _removeTab(int index) { @@ -122,7 +141,7 @@ class _HomeScreenState extends State with TickerProviderStateMixin { body: ListView.separated( itemCount: result.length, itemBuilder: (context, index) { - final SerializableMessage email = result[index]; + final SerializableMessage email = result[index]; return ListTile( title: Text(email.from, style: TextStyle(fontWeight: FontWeight.bold)), @@ -135,58 +154,25 @@ class _HomeScreenState extends State with TickerProviderStateMixin { // print('tapped'); // List emailContent = // await apiService.fetchEmailContent([email.id], email.list); - //call the foldable - + //call the foldable + List emailContent = // list of the html - await apiService.fetchEmailContent([email.id], email.list); + await apiService + .fetchEmailContent([email.id], email.list); // List emailIds = email.messages; - - + Navigator.push( context, MaterialPageRoute( - builder: (context) =>SonicEmailView( - email: email, - 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], - // ), - ), + builder: (context) => SonicEmailView( + email: email, emailHTML: emailContent[0])), ); }, ); }, 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 +187,611 @@ class _HomeScreenState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { return Scaffold( - key: _scaffoldKey, - drawer: FolderDrawer( - apiService: apiService, - onFolderTap: (folder) { - _emailPageKey.currentState?.updateSelectedFolder(folder); - }, - ), - body: Stack( - children: [ - Row( - children: [ - // Sidebar - if (_isSidebarOpen) - Container( - width: 70, - color: Color.fromARGB(17, 96, 122, 135), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + backgroundColor: Theme.of(context).colorScheme.onPrimary, + body: Padding( + padding: const EdgeInsets.fromLTRB(0, 20, 0, 20), + child: Scaffold( + key: _scaffoldKey, + drawer: FolderDrawer( + apiService: apiService, + onFolderTap: (folder) { + _emailPageKey.currentState?.updateSelectedFolder(folder); + }, + ), + body: Scaffold( + backgroundColor: Theme.of(context).colorScheme.onPrimary, + body: Padding( + padding: const EdgeInsets.fromLTRB(0, 20, 0, 0), + child: Stack( + children: [ + Row( children: [ - ListTile( - leading: Icon(Icons.home), - onTap: () { - // Navigate to Home - }, - ), - ListTile( - leading: Icon(Icons.settings), - onTap: () { - // Navigate to Settings - }, - ), - ListTile( - leading: Icon(Icons.email), - onTap: () { - _scaffoldKey.currentState?.openDrawer(); - }, - ), - Spacer(), - 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; - }); - }, + // Sidebar + if (_isSidebarOpen) + Container( + width: 70, + color: Color.fromARGB(17, 96, 122, 135), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + leading: Icon(Icons.edit_note_sharp), + onTap: () { + OverlayService() + .showPersistentWidget(context); + }), + ListTile( + leading: Icon(Icons.home), + onTap: () { + // Navigate to Home + context.go("/home"); + }, + ), + // ListTile( + // leading: Icon(Icons.settings), + // onTap: () { + // // Navigate to Settings + // }, + // ), + // ListTile( + // leading: Icon(Icons.contact_mail), + // onTap: () { + // // Navigate to Contacts + // }, + // ), + + ListTile( + leading: Icon(Icons.email), + onTap: () { + _scaffoldKey.currentState?.openDrawer(); + }, + ), + Spacer(), + 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> 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( + padding: EdgeInsets.fromLTRB(0, 4, 0, 4), + color: Theme.of(context).colorScheme.onPrimary, + child: Row( + children: [ + Padding( + padding: + const EdgeInsets.fromLTRB(4, 0, 0, 0), + child: Checkbox( + value: _checkboxState, + onChanged: (value) { + setState(() { + _checkboxState = !_checkboxState; + }); + if (_checkboxState) { + // var a = _tabWidgets["Emails"].; + print(_emailPageKey.currentState! + .selectAllEmails( + true)); //now i got them all but how do i go down to select them all? + print("all"); + bulkOptionsState = true; + } else { + _emailPageKey.currentState! + .selectAllEmails(false); + bulkOptionsState = false; + print("none"); + } + }), + ), + const SizedBox( + width: 0, + ), + PopupMenuButton( + icon: const Icon( + Icons.arrow_drop_down_outlined), + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + //select all + child: Text("All"), + onTap: () { + _emailPageKey.currentState! + .selectAllEmails(true); + }, + ), + PopupMenuItem( + child: Text("None"), + onTap: () { + _emailPageKey.currentState! + .selectAllEmails(false); + }, + ), + PopupMenuItem( + child: Text("Read"), + onTap: () { + //select the read + }, + ), + PopupMenuItem( + //select the unread + child: Text("Unread"), + onTap: () { + //select the unread + }, + ), + PopupMenuItem( + child: Text("Starred")), + PopupMenuItem( + child: Text("Unstarred")), + ], + onSelected: (String result) { + print("result $result"); + }, + ), + if (selectedThreads.isNotEmpty) ...[ + //this needs to know if anything got selected, + IconButton( + onPressed: null, + icon: Icon(Icons.archive_outlined)), + IconButton( + onPressed: null, + icon: Icon(Icons.delete_outlined)), + IconButton( + onPressed: () { + _emailPageKey.currentState! + .markSelectedAsRead( + true); //mark as read + }, + icon: Icon( + Icons.mark_email_read_outlined)), + IconButton( + onPressed: () { + final overlay = Overlay.of(context); + String? + selectedFolder; // Variable to store the selected folder + + _overlayEntry = OverlayEntry( + builder: (context) => Stack( + children: [ + // Dimmed background + Container( + color: Colors.black54, + width: + MediaQuery.of(context) + .size + .width, + height: + MediaQuery.of(context) + .size + .height, + ), + // Focused content window + PointerInterceptor( + child: Center( + child: Material( + elevation: 8, + borderRadius: + BorderRadius + .circular(12), + child: ConstrainedBox( + constraints: + const BoxConstraints( + maxWidth: 400, + maxHeight: 500, + ), + child: Column( + mainAxisSize: + MainAxisSize + .min, + children: [ + Text( + 'Move email from folder ${ApiService.currFolder} to:', + style: TextStyle( + fontSize: + 16), + ), + Divider( + height: 1), + Expanded( + child: FutureBuilder< + List< + String>>( + future: ApiService() + .fetchFolders(), + builder: (context, + snapshot) { + if (snapshot + .connectionState == + ConnectionState + .waiting) { + return const Center( + child: + CircularProgressIndicator()); + } else if (snapshot + .hasError) { + return Center( + child: + Text('Error: ${snapshot.error}')); + } else if (snapshot + .hasData) { + return StatefulBuilder( + builder: + (context, + setState) { + return ListView( + shrinkWrap: + true, + children: + snapshot.data!.map((folder) { + return RadioListTile( + title: Text(folder), + value: folder, + groupValue: selectedFolder, + onChanged: (String? value) { + setState(() { + selectedFolder = value; // Update the selected folder + }); + }, + ); + }).toList(), + ); + }, + ); + } else { + return const Center( + child: + Text('No folders found.')); + } + }, + ), + ), + Divider( + height: 1), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceEvenly, + children: [ + ElevatedButton( + onPressed: + () { + // Handle Accept button + if (selectedFolder != + null) { + print( + "Selected folder: $selectedFolder"); + // Store the selected folder or perform any action + // ApiService.currFolder = selectedFolder!; + _emailPageKey + .currentState! //the one selected + .moveSelectedOfFolder(selectedFolder!); + _overlayEntry + ?.remove(); + } else { + print( + "No folder selected"); + } + }, + child: Text( + 'Accept'), + ), + ElevatedButton( + onPressed: + () { + // Handle Cancel button + _overlayEntry + ?.remove(); + }, + child: Text( + 'Cancel'), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + + if (_overlayEntry != null) { + overlay.insert(_overlayEntry!); + } + // _emailPageKey.currentState! //the one selected + // .moveSelectedOfFolder(); + }, + icon: Icon( + Icons.drive_file_move_outlined)), + ], + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (BuildContext context) { + if (selectedThreads.isEmpty) { + //why tf? + return >[ + PopupMenuItem( + child: Row( + children: [ + Icon(Icons + .mark_email_read_outlined), + const SizedBox( + width: 4.0, + ), + Text("Mark all as read") + ], + ), + onTap: () { + _emailPageKey.currentState! + .selectAllEmails(true); + _emailPageKey.currentState! + .markSelectedAsRead(true); + _emailPageKey.currentState! + .selectAllEmails(false); + }, + ), + const PopupMenuDivider(), + PopupMenuItem( + child: Text( + "Select messages to see more actions", + style: TextStyle( + color: Colors + .blueGrey.shade300), + ), + ) + ]; + } else { + return >[ + PopupMenuItem( + child: Row( + children: [ + Icon(Icons + .mark_email_unread_outlined), + const SizedBox( + width: 4.0, + ), + Text("Mark as unread") + ], + ), + onTap: () { + _emailPageKey.currentState! + .markSelectedAsRead( + false); + }, + ), + const PopupMenuItem( + child: Row( + children: [ + Icon(Icons.snooze_outlined), + const SizedBox( + width: 4.0, + ), + Text("Snooze") + ], + ), + ), + const PopupMenuItem( + child: Row( + children: [ + Icon(Icons + .star_border_outlined), + const SizedBox( + width: 4.0, + ), + Text("Add star") + ], + ), + ), + ]; + } + }), + ], + )), + 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( + 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(), + // ), + // ), + ], + ), ), ], ), - ), - // 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: [ - 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> 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), - ), - ) - ], + if (!_isSidebarOpen) + Positioned( + bottom: 16, + left: 16, + child: FloatingActionButton( + child: Icon(Icons.menu), + onPressed: () { + setState(() { + _isSidebarOpen = true; + }); + }, ), ), - 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( - 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( -// context: context, -// position: RelativeRect.fromLTRB( -// position.dx, -// position.dy, -// overlay.size.width - position.dx, -// overlay.size.height - position.dy, -// ), -// items: >[ -// PopupMenuItem( -// value: 'Open', -// child: Text('Open'), -// ), -// PopupMenuItem( -// value: 'Reply', -// child: Text('Reply'), -// ), -// PopupMenuItem( -// value: 'Delete', -// child: Text('Delete'), -// ), -// ], -// ); -// } -// } diff --git a/lib/login.dart b/lib/login.dart index ca2c3f9..1745355 100644 --- a/lib/login.dart +++ b/lib/login.dart @@ -1,25 +1,32 @@ import 'dart:convert'; // import 'package:crab_ui/api_service.dart'; +import 'package:go_router/go_router.dart'; + import 'api_service.dart'; -import 'home_page.dart'; +// import 'home_page.dart'; import 'package:flutter/material.dart'; // import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:http/http.dart' as http; // import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart' show rootBundle; -class AuthService { +class AuthService extends ChangeNotifier { Future isUserLoggedIn() async { + // ApiService.ip = '127.0.0.1'; + // ApiService.port = '3001'; + // print("setted up"); + + // return true; try { final response = - await http.get(Uri.http('localhost:6823', 'read-config')); - // await http.get(Uri.parse('http://localhost:6823/read-config')); + await http.get(Uri.http('127.0.0.1:6767', 'login_check')); print(response.statusCode); print(response.body); if (response.statusCode == 200) { final data = jsonDecode(response.body); - // return data['config']; + // print(data['state']); + // return true; try { var url = Uri.http('${data['ip']}:${data['port']}', 'is_logged_in'); var response = await http.get(url); @@ -83,6 +90,7 @@ class LoginPage extends StatefulWidget { } class SplashScreen extends StatefulWidget { + //entry point @override _SplashScreenState createState() => _SplashScreenState(); } @@ -92,19 +100,31 @@ class _SplashScreenState extends State { @override void initState() { super.initState(); - _checkLoginStatus(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkLoginStatus(); + }); } Future _checkLoginStatus() async { - // SharedPreferences prefs = await SharedPreferences.getInstance(); - // print(prefs); - // bool isLoggedIn = prefs.getBool('isLoggedIn') ?? false; - bool isLoggedIn = await _authService.isUserLoggedIn(); - print("is loogeed in $isLoggedIn"); - if (isLoggedIn) { - Navigator.pushReplacementNamed(context, '/home'); - } else { - Navigator.pushReplacementNamed(context, '/login'); + try { + final response = + await http.get(Uri.parse('http://127.0.0.1:6767/login_check')); + print(response.statusCode); + print(response.body); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + print(data['state']); + if (data['state']) { + context.go("/home"); + } else { + context.go("/login"); + } + } else { + context.go("/login"); + } + } catch (e) { + print("caught in checkloginstatus in login $e"); + context.go("/login"); } } @@ -112,7 +132,7 @@ class _SplashScreenState extends State { Widget build(BuildContext context) { return Center( child: Scaffold( - body: Center(child: CircularProgressIndicator()), + body: Center(child: CircularProgressIndicator()), //nothing happens ), ); } @@ -126,13 +146,10 @@ class _LoginPageState extends State { final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); - // final ConfigManager _configManager = - // ConfigManager("${Directory.current.parent}../crabmail2.conf"); - // Key to identify the form final _formKey = GlobalKey(); Future setIp(String ip) async { - // _configManager.setField("api_addr", ip); + //this is not done :sob: :skull: return false; } @@ -141,31 +158,16 @@ class _LoginPageState extends State { } Future login() async { - bool result = await _handleLogin(); - if (result) { - Navigator.pushReplacementNamed(context, '/home'); + var result = await _handleLogin(); + if (result[0]) { + ApiService.ip = result[1]; + ApiService.port = result[2]; + context.go('/home'); } } - // Future _checkConfiguration() async { - // return false; - // } - - // void checkLogin() async { - // try { - // var url = Uri.http('127.0.0.1:3001', 'is_logged_in'); - // var response = await http.get(url); - // print(response.body); - // if (response.statusCode == 200) { - // print('all good on the west'); - // } - // } catch (e) { - // print(e); - // } - // // bool isLoggedIn = await _authService.isUserLoggedIn(); - // } // Function to handle login action - Future _handleLogin() async { + Future _handleLogin() async { if (_formKey.currentState!.validate()) { // Perform login action (e.g., authenticate with backend) String ip = _ipController.text; @@ -179,96 +181,64 @@ class _LoginPageState extends State { print(ip); print(port); - String baseUrl = "http://$ip:$port"; - print("baseurl " + baseUrl); - print(baseUrl); try { - final response = - await http.get(Uri.parse('http://localhost:6823/read-config')); + Map requestBody = { + "user": email, + "password": password, + "ip": ip, + "port": port, + }; + var url = Uri.http("$ip:6767", "login"); + + final response = await http.post(url, + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode( + requestBody)); //keep the port but change the ip to the server that will process it? print(response.statusCode); print(response.body); if (response.statusCode == 200) { final data = jsonDecode(response.body); - // return data['config']; + if (data["state"] == true) { + try { + final response = await http + .get(Uri.parse('http://127.0.0.1:6767/login_check')); + print(response.statusCode); + print(response.body); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + print(data['state']); + if (data['state']) { + context.go("/home"); + } else { + context.go("/login"); + } + } else { + context.go("/login"); + } + } catch (e) { + print("caught in checkloginstatus in login $e"); + context.go("/login"); + } + } + return [true, ip, port]; } + return [false]; } catch (e) { print("caught in catch"); print(e); - return false; + return [false]; } - Map updates = { - // "username": email, - // "password": password, - "ip": ip, - "port": port, - }; - print("past"); - - try { - final sending = await http.post( - Uri.parse('http://localhost:6823/update-config'), - headers: {'Content-Type': "application/json"}, - body: jsonEncode(updates)); - print("sending"); - } catch (e) { - print(e); - return false; - } - try { - // String status = await http.post(Uri.parse('')) - var url_log = Uri.http('$ip:$port', 'log_in'); - Map filteredData = { - "email": email, - "password": password, - // 'email': updates['username'], - // 'password': updates['password'] - }; - print(filteredData); - var status = await http.post( - url_log, - headers: { - 'Content-Type': 'application/json', - }, - body: jsonEncode(filteredData), - ); - if (status.statusCode == 200) { - print('response status ${status.body}'); - if (status.body == "Successful log in") { - ApiService.ip = ip; - ApiService.port = port; - return true; - } - } - } catch (e) { - print(e); - return false; - } - print("after"); - return false; - // final content = await _authService.readConfFile(); - // final config = await _authService.parseConfFile(content); - // print("BASE URL ${config["base_url"]}"); - // print("api address ${config["api_addr"]}"); - // print(config); - // const url = '' - // Clear the input fields - // _ipController.clear(); - // _portController.clear(); - // _emailController.clear(); - // _passwordController.clear(); } - return false; + return [false]; } @override Widget build(BuildContext context) { - // try { - // _configManager.loadConfig(); - // print(_configManager.getField('base_url')); - // } catch (e) { - // print("broke at build $e"); - // } - // _configManager. + _ipController.text = "127.0.0.1"; + _portController.text = "3001"; + return Center( child: Scaffold( appBar: AppBar( @@ -303,14 +273,6 @@ class _LoginPageState extends State { return 'Please enter your ip'; } }, - // onSaved: (value) async { - // final content = await _authService.readConfFile(); - // final config = - // await _authService.parseConfFile(content); - // print("BASE URL ${config["base_url"]}"); - // print("api address ${config["api_addr"]}"); - // //TODO: call a function to set the field ip in conf - // }, ), ), Container( @@ -389,17 +351,6 @@ class _LoginPageState extends State { child: const Text('Login'), ), ), - // SizedBox( - // width: 200, - // child: ElevatedButton( - // // onPressed: checkLogin, - // onPressed: () async { - // await _authService.isUserLoggedIn(); - // // print(result); - // }, - // child: const Text('checker'), - // ), - // ) ], ), ), diff --git a/lib/main.dart b/lib/main.dart index da90a50..935ce87 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,18 @@ import 'package:crab_ui/contact.dart'; +import 'package:crab_ui/email.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'home_page.dart'; import 'login.dart'; - +import 'package:go_router/go_router.dart'; +import 'routingHandler.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - runApp(HyM()); + runApp(ChangeNotifierProvider( + create: (context) => AuthService(), + child: HyM(), + )); } class HyM extends StatelessWidget { @@ -15,19 +21,44 @@ class HyM extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + final GoRouter _router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute( + path: "/", + builder: (context, state) => SplashScreen(), + ), + GoRoute( + path: "/login", + builder: (context, state) => const LoginPage(), + ), + GoRoute( + path: "/home", + builder: (context, state) => HomeScreen(), + ), + GoRoute( + path: "/contacts", + builder: (context, state) => ContactsPage(), + ), + GoRoute( + path: "/email/:subject/:target/:viewspecs/:emailID", + builder: (context, state) { + final subject = state.pathParameters['subject']!; + final target = state.pathParameters['target']!; + final viewspecs = state.pathParameters['viewspecs']!; + final emailId = state.pathParameters['emailID']!; + return Routinghandler.fromParameters( + "main anchor", subject, target, viewspecs, emailId); + }), + ]); + return MaterialApp.router( debugShowCheckedModeBanner: false, - theme: ThemeData.light(), + theme: ThemeData( + colorScheme: ColorScheme.light(), + useMaterial3: true, + ), title: 'HyM', - // home: HomeScreen(), - initialRoute: "/", - - routes: { - "/": (context) => SplashScreen(), - "/login": (context) => const LoginPage(), - "/home": (context) => HomeScreen(), - "/contacts": (context) => ContactsPage(), - }, + routerConfig: _router, ); } } diff --git a/lib/routingHandler.dart b/lib/routingHandler.dart new file mode 100644 index 0000000..1d51ba6 --- /dev/null +++ b/lib/routingHandler.dart @@ -0,0 +1,229 @@ +import 'package:crab_ui/collapsableEmails.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:markdown_widget/markdown_widget.dart'; +import 'api_service.dart'; +import 'structs.dart'; + +class Routinghandler extends StatefulWidget { + Routinghandler(String link) { + bool anchorDone = false; + + bool docNameDone = false; + + bool viewspecsDone = false; + + bool targetDone = false; + + bool emailIdDone = false; + + for (int letter = 0; letter < link.length; letter++) { + if (!anchorDone) { + if (link[letter] != '<') { + //when the anchor hasnt been dissected + anchor += link[letter]; + } else { + anchorDone = true; + } + } else if (!docNameDone) { + if (link[letter] != ',') { + //when the docName hasnt been dissected + docName += link[letter]; + } else { + docNameDone = true; + } + } else if (!targetDone) { + if (link[letter] != ':') { + target += link[letter]; + } else { + targetDone = true; + } + } else if (!viewspecsDone) { + if (link[letter] != '>') { + //when the docName hasnt been dissected + viewspecs += link[letter]; + } else { + viewspecsDone = true; + } + } else if (!emailIdDone) { + emailID += link[letter]; + } + } + anchor = anchor.trim(); + docName = docName.trim(); + target = target.trim(); + viewspecs = viewspecs.trim(); + print("inside constructor uwu $emailID"); + emailID = emailID.trim(); + } + Routinghandler.fromParameters(String anchor, String docName, String target, + String viewspecs, String emailID) { + this.anchor = anchor; + this.docName = docName; + this.viewspecs = viewspecs; + this.target = target; + this.emailID = emailID; + } + Routinghandler.copyConstructor(Routinghandler other) { + anchor = other.anchor; + docName = other.docName; + viewspecs = other.viewspecs; + target = other.target; + emailID = other.emailID; + } + + String anchor = ''; + String docName = ''; + String viewspecs = ''; + String target = ''; + String emailID = ''; + + void goToLink() { + // bool anchorDone = false; + + // bool docNameDone = false; + + // bool viewspecsDone = false; + + // bool targetDone = false; + + // for (int letter = 0; letter < link.length; letter++) { + // if (!anchorDone) { + // if (link[letter] != '<') { + // //when the anchor hasnt been dissected + // anchor += link[letter]; + // } else { + // anchorDone = true; + // } + // } else if (!docNameDone) { + // if (link[letter] != ',') { + // //when the docName hasnt been dissected + // docName += link[letter]; + // } else { + // docNameDone = true; + // } + // } else if (!targetDone) { + // if (link[letter] != ':') { + // target += link[letter]; + // } else { + // targetDone = true; + // } + // } else if (!viewspecsDone) { + // if (link[letter] != '>') { + // //when the docName hasnt been dissected + // viewspecs += link[letter]; + // } else { + // viewspecsDone = true; + // } + // } + // } + print("anchor $anchor"); + print("docName $docName"); + print("target $target"); + print("viewspecs $viewspecs"); + print("emailID $emailID"); + //now it should open a widget in that part + //maybe i need a rewrite + } + + String getEmailID() { + return emailID; + } + + @override + State createState() => _RoutingHandlerState(); +} + +class _RoutingHandlerState extends State { + List markdownContent = []; + bool _isLoaded = false; + AugmentTree? aug; + + @override + void initState() { + // TODO: implement initState + super.initState(); + _loadMarkdown(); + } + + Future _loadMarkdown() async { + String folder = ApiService.currFolder; + print(widget.getEmailID()); + String emailID = widget.emailID; + print("inside _loadMarkdown in routinghandler $emailID"); + markdownContent = + await ApiService().fetchMarkdownContent([emailID], folder); + // print(markdownContent); + aug = AugmentTree.fromMD(markdownContent[0]); + aug!.addNumbering(); + + setState(() { + _isLoaded = true; + }); + } + + @override + Widget build(BuildContext context) { + if (!_isLoaded) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return Scaffold( + appBar: AppBar( + title: Text("Routing Handler"), + leading: IconButton( + onPressed: () { + GoRouter.of(context).go('/home'); + }, + icon: const Icon(Icons.arrow_back_ios)), + ), + body: ConstrainedBox( + constraints: BoxConstraints( + minHeight: 100, + maxHeight: MediaQuery.of(context).size.height * 0.7, + ), + child: SingleChildScrollView( + //inside here put the bunch rows + //make rows of markdownBlocks, but firstly i need to conveert the content into a tree + // child:MarkdownBlock(data: markdownContent[0]) + + child: Column(children: [ + for (int i = 0; i < this.aug!.children![0]!.children.length; i++) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // if (leftNumbering) + Padding( + padding: const EdgeInsets.fromLTRB(0, 10, 5, 0), + child: Text( + aug!.children![0]!.children![i]!.numbering, + style: TextStyle( + color: Color(Colors.purple[400]!.value)), + ), + ), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: Wrap( + children: [ + MarkdownBlock( + data: aug!.children![0]!.children![i]! + .data ?? + ''), + ], + ))), + Padding( + padding: const EdgeInsets.fromLTRB(0, 10, 5, 0), + child: Text( + aug!.children![0]!.children![i]!.numbering, + style: TextStyle( + color: Color(Colors.purple[400]!.value)), + ), + ), + ]), + ])))); + } +} diff --git a/lib/sonicEmailView.dart b/lib/sonicEmailView.dart index bfe0bbe..aeb7301 100644 --- a/lib/sonicEmailView.dart +++ b/lib/sonicEmailView.dart @@ -1,145 +1,3 @@ -import 'package:crab_ui/augment.dart'; -import 'package:web/web.dart' as web; -import 'dart:ui_web' as ui; -import 'dart:js_interop'; -import 'structs.dart'; -import 'package:flutter/material.dart'; - -class SonicEmailView extends StatefulWidget { - SerializableMessage email; - String emailHTML; - - SonicEmailView({required this.email, required this.emailHTML}); - - @override - _SonicEmailViewState createState() => _SonicEmailViewState(); -} - -class _SonicEmailViewState extends State { - String viewTypeIDs = ""; - int heightOFViewtype = 0; - bool _isLoaded = false; - - void _scrollToNumber(String spanId) { - AugmentClasses.handleJump(spanId); - } - - @override - void initState() { - super.initState(); - _init(); - } - - Future _init() async { - await _registerViewFactory(widget.emailHTML); - if (!mounted) return; - setState(() { - _isLoaded = true; - }); - } - - Future _registerViewFactory(String currentContent) async { - // setState(() { //update to do item per item - // each item to have itsviewtype ID - // is this necessarey here?? - - //could just move to collapsable - - // for (var emailHTML in widget.threadHTML) { - String viewTypeId = 'email-${DateTime.now().millisecondsSinceEpoch}'; - - final ghost = web.document.createElement('div') as web.HTMLDivElement - ..style.visibility = 'hidden' - ..style.position = 'absolute' - ..style.width = '100%' - ..style.overflow = 'auto' - ..innerHTML = currentContent.toJS; - web.document.body?.append(ghost); - await Future.delayed(Duration(milliseconds: 10)); - - final heightOfEmail = ghost.scrollHeight; - ghost.remove(); - - final HTMLsnippet = web.document.createElement('div') as web.HTMLDivElement - ..id = viewTypeId - ..innerHTML = widget - .emailHTML.toJS; // temporarily index because it has to do all of them - HTMLsnippet.style - ..width = '100%' - ..height = '${heightOfEmail}px' - ..overflow = 'auto' - ..scrollBehavior = 'smooth'; - - ui.platformViewRegistry.registerViewFactory( - viewTypeId, - (int viewId) => HTMLsnippet, - ); - this.viewTypeIDs = viewTypeId; - this.heightOFViewtype = heightOfEmail; - print(viewTypeIDs); - } - - @override - Widget build(BuildContext context) { - return _isLoaded - ? Scaffold( - appBar: AppBar(title: Text(widget.email.subject)), - body: Stack( - children: [ - Column( - children: [ - EmailToolbar( - onButtonPressed: () => {}, - onJumpToSpan: _scrollToNumber), - Row( - // title of email - children: [ - Text( - widget.email.subject, - style: TextStyle(fontSize: 30), - ), - ], - ), - Row( - children: [ - Text( - 'from ${widget.email.name}', - style: TextStyle(fontSize: 18), - ), - Text( - '<${widget.email.from}>', - style: TextStyle(fontSize: 18), - ), - Spacer(), - Text( - '${widget.email.date}', - textAlign: TextAlign.right, - ) - ], - ), - // TODO: make a case where if one of these is the user's email it just says me :))))) - Row( - children: [ - Text( - 'to ${widget.email.to.toString()}', - style: TextStyle(fontSize: 15), - ) - ], - ), - Expanded( - // child: SizedBox( - // height: heightOFViewtype.toDouble(), - child: HtmlElementView( - key: UniqueKey(), viewType: this.viewTypeIDs, - // ), - )) - ], - ), - ], - ), - ) - : const Center( - child: CircularProgressIndicator(), - ); - } -} +export 'SonicEmailViewStub.dart' + if (dart.library.js_interop) 'SonicEmailViewWeb.dart' + if (dart.library.io) 'SonicEmailViewAndroid.dart'; \ No newline at end of file diff --git a/lib/structs.dart b/lib/structs.dart index c7e9da4..2499c57 100644 --- a/lib/structs.dart +++ b/lib/structs.dart @@ -1,6 +1,7 @@ //data structures import 'dart:typed_data'; +import 'package:markdown/markdown.dart' as md; class GetThreadResponse { final int id; @@ -10,6 +11,7 @@ class GetThreadResponse { final String from_name; final String from_address; final List to; + late bool seen; GetThreadResponse({ required this.id, @@ -19,19 +21,20 @@ class GetThreadResponse { required this.from_name, required this.from_address, required this.to, + required this.seen, }); factory GetThreadResponse.fromJson(Map json) { var toList = json['to'] as List; return GetThreadResponse( - id: json['id'], - messages: List.from(json['messages']), - subject: json['subject'], - date: DateTime.parse(json['date']), - from_name: json['from_name'], - from_address: json['from_address'], - to: toList.map((i) => MailAddress.fromJson(i)).toList(), - ); + id: json['id'], + messages: List.from(json['messages']), + subject: json['subject'], + date: DateTime.parse(json['date']), + from_name: json['from_name'], + from_address: json['from_address'], + to: toList.map((i) => MailAddress.fromJson(i)).toList(), + seen: json['seen']); } } @@ -78,7 +81,7 @@ class SerializableMessage { required this.subject, required this.date, required this.uid, - required this.list, //email list??? + required this.list, //folder required this.id, required this.in_reply_to, }); @@ -127,7 +130,8 @@ class AttachmentInfoList extends Iterable { AttachmentInfoList(this._attachments); factory AttachmentInfoList.fromJsonList(List> jsonList) { - return AttachmentInfoList(jsonList.map((json) => AttachmentInfo.fromJson(json)).toList()); + return AttachmentInfoList( + jsonList.map((json) => AttachmentInfo.fromJson(json)).toList()); } @override @@ -143,6 +147,160 @@ class AttachmentResponse { AttachmentResponse({required this.name, required this.data}); factory AttachmentResponse.fromJson(Map json) { - return AttachmentResponse(name: json["name"], data: Uint8List.fromList(List.from(json["data"]))); + return AttachmentResponse( + name: json["name"], + data: Uint8List.fromList(List.from(json["data"]))); } } + +class AugmentTree { + List children = []; + + String data = ''; + AugmentTree? parent; + String ogTag = ''; + String numbering = ''; + Map hirarchyDict = { + "h1": 1, + "h2": 2, + "h3": 3, + "h4": 4, + "h5": 5, + "h6": 6, + "p": 8, + "ul": 8, + "li": 8, + }; + + AugmentTree(); + + AugmentTree.fromMD(String rawMD) { + //makes raw MD into an augmentTree + print("started markdown2tree"); + final List nakedList = md.Document().parseLines(rawMD.split( + '\n')); //emails md is the index of the email in the thread, since this only handles one thus it shall be removed + // AugmentTree zoomTreeRoot = AugmentTree(); + for (var node in nakedList) { + //maybe do an add function, but isn't this it? + if (node is md.Element) { + AugmentTree temp = AugmentTree(); + temp.data = node.textContent; + temp.ogTag = node.tag; + if (hirarchyDict.containsKey(node.tag)) { + // make this O(1) + _add2Tree(this, node); + // print(node); + } + } + } + this.addNumbering(); + } + + void _add2Tree(AugmentTree tree, md.Element node2add) { + // adds node to its corresponding place + AugmentTree newNode = AugmentTree(); + newNode.setData(node2add.textContent); + newNode.ogTag = node2add.tag; + // cases, + //1. a node that comes is lower than the root.children last, if so it goes beneath it + if (tree.children.isEmpty) { + // new level to be created when totally empty + tree.children.add(newNode); + newNode.parent = tree; + } else if (tree.children.isNotEmpty && + tree.children.last.ogTag.isNotEmpty) { + if ((hirarchyDict[node2add.tag] ?? + -1) < // e.g. new node is h1 and old is h2, heapify + (hirarchyDict[tree.children.last.ogTag] ?? -1)) { + //have to figure out the borthers + //assuming it all goes right + if ((hirarchyDict[node2add.tag] ?? -1) == -1 || + (hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) { + print( + 'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} < ${hirarchyDict[tree.children.last.ogTag] ?? -1}'); + return; + } else if (tree.children.last.parent == null) { + // becomes the new top level + for (AugmentTree brother in tree.children) { + brother.parent = newNode; + } + tree.children = [newNode]; + } else { + newNode.parent = tree; + tree.children.add(newNode); + } + } else if ((hirarchyDict[node2add.tag] ?? + -1) > // go down e.g. new node is h3 and old is h2 or something + (hirarchyDict[tree.children.last.ogTag] ?? -1)) { + if ((hirarchyDict[node2add.tag] ?? -1) == -1 || + (hirarchyDict[tree.children.last.ogTag] ?? -1) == -1) { + print( + 'failed and got -1 at _add2Tree \n ${hirarchyDict[node2add.tag] ?? -1} > ${hirarchyDict[tree.children.last.ogTag] ?? -1}'); + print("-1 ${tree.children.last.ogTag}"); + return; + } + + _add2Tree(tree.children.last, node2add); + } else if ((hirarchyDict[node2add.tag] ?? -1) == + (hirarchyDict[tree.children.last.ogTag] ?? -1)) { + tree.children.add(newNode); + newNode.parent = tree; + } + } + } + + void setData(String data) { + this.data = data; + } + + static String _intToLetter(int index) { + return String.fromCharCode('a'.runes.first + index); + } + + void addNumbering({String prefix = ''}) { + //if called in root, numbers them all + for (int i = 0; i < children.length; i++) { + final child = children[i]; + String childNumbering; + bool parentIsLettered = prefix.contains(RegExp(r'[a-z]')); + if (prefix.isEmpty) { + parentIsLettered = false; + } else { + parentIsLettered = prefix.runes.last >= 'a'.runes.first && + prefix.runes.last <= 'z'.runes.first; + } + + if (prefix.isEmpty) { + // Top-level children (direct children of the original root being numbered) get 1, 2, 3... + childNumbering = (i + 1).toString(); + } else if (parentIsLettered) { + // Deeper children get '1a', '1b', '2a', '2b', etc. + childNumbering = '$prefix${i + 1}'; + } else { + childNumbering = '$prefix${_intToLetter(i)}'; + } + + child.numbering = childNumbering; + + // Recursively call for children + child.addNumbering(prefix: childNumbering); + } + } +} + +//perhaps make a struct that builds augment tree, since its so complex and needs to be like recursive + +class MarkdownParsed { + //struct for holding the MD given in endpoint //not used + final String text; + MarkdownParsed({required this.text}); + factory MarkdownParsed.fromJson(Map json) { + return MarkdownParsed( + text: json['md'] ?? '', + ); + } +} + +//should make an md to tree class/struct + +// make a for loop of rows with markdown diff --git a/pubspec.lock b/pubspec.lock index f4b7708..13a53ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,106 +5,98 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + sha256: "0511d6be23b007e95105ae023db599aea731df604608978dada7f9faf2637623" url: "https://pub.dev" source: hosted - version: "1.5.8" + version: "1.6.4" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" audio_session: dependency: transitive description: name: audio_session - sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261" + sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" url: "https://pub.dev" source: hosted - version: "0.1.21" + version: "0.1.25" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" cached_network_image: dependency: transitive description: name: cached_network_image - sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7 + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.1.1" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" chewie: dependency: transitive description: name: chewie - sha256: "745e81e84c6d7f3835f89f85bb49771c0a66099e4caf8f8e9e9a372bc66fb2c1" + sha256: "4d9554a8f87cc2dc6575dfd5ad20a4375015a29edd567fd6733febe6365e2566" url: "https://pub.dev" source: hosted - version: "1.5.0" - chewie_audio: - dependency: transitive - description: - name: chewie_audio - sha256: "73948a8b9841d050433af3498a1f8b11320bd5a2cd70b449bdbe16d4405e97c5" - url: "https://pub.dev" - source: hosted - version: "1.5.0" + version: "1.11.3" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" convert: dependency: transitive description: @@ -125,10 +117,10 @@ packages: dependency: transitive description: name: csslib - sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "0.17.3" + version: "1.0.2" cupertino_icons: dependency: transitive description: @@ -137,22 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - dio: + dbus: dependency: transitive description: - name: dio - sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "5.8.0+1" - dio_web_adapter: - dependency: transitive - description: - name: dio_web_adapter - sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" - url: "https://pub.dev" - source: hosted - version: "2.1.1" + version: "0.7.11" encrypt: dependency: "direct main" description: @@ -173,42 +157,34 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" - file_saver: - dependency: "direct main" - description: - name: file_saver - sha256: "017a127de686af2d2fbbd64afea97052d95f2a0f87d19d25b87e097407bf9c1e" - url: "https://pub.dev" - source: hosted - version: "0.2.14" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -218,74 +194,10 @@ packages: dependency: transitive description: name: flutter_cache_manager - sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" url: "https://pub.dev" source: hosted - version: "3.4.0" - flutter_html: - dependency: transitive - description: - name: flutter_html - sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" - flutter_html_all: - dependency: "direct main" - description: - name: flutter_html_all - sha256: "5b4c449df76ecd186bea55414206c21bde83090d4b54d14caed78823718b7f1b" - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" - flutter_html_audio: - dependency: transitive - description: - name: flutter_html_audio - sha256: "94ae28ab56a8d556b7c5409e3eb59ca5215812bda87c67ddfa768812b76f8511" - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" - flutter_html_iframe: - dependency: transitive - description: - name: flutter_html_iframe - sha256: "979405fafcbd29c930bf96d9f3f0ade9d87dfd567a03180b13424a0e89a5de46" - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" - flutter_html_math: - dependency: transitive - description: - name: flutter_html_math - sha256: "7371f2621b77c66399e50b9fd5ff0eb2475c8c581af68e3eb21409c49a811211" - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" - flutter_html_svg: - dependency: transitive - description: - name: flutter_html_svg - sha256: "793be10cfa6fd0925a7adde9f58d73d2ce6a84c02ae12d01a985244f55ee631a" - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" - flutter_html_table: - dependency: transitive - description: - name: flutter_html_table - sha256: e20c72d67ea2512e7b4949f6f7dd13d004e773b0f82c586a21f895e6bd90383c - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" - flutter_html_video: - dependency: transitive - description: - name: flutter_html_video - sha256: ecc8bcc614dd8a8286d32ace462481817660f5a7e854c663c1fcb3424fc3be89 - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" + version: "3.4.1" flutter_layout_grid: dependency: "direct overridden" description: @@ -314,10 +226,10 @@ packages: dependency: transitive description: name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 url: "https://pub.dev" source: hosted - version: "2.0.10+1" + version: "2.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -332,74 +244,74 @@ packages: dependency: "direct main" description: name: flutter_widget_from_html - sha256: "8d2a9a7979a9c1a5d866d1f4134d2ec2cca78716c112c76803d6a552281405cc" + sha256: "0dfebf7417df2149de93926520c703db9be0c9017e60dc5cf43cebed37f4d11e" url: "https://pub.dev" source: hosted - version: "0.10.6" + version: "0.16.0" flutter_widget_from_html_core: dependency: transitive description: name: flutter_widget_from_html_core - sha256: "22140caa191cb4bba0fe4d5e4ad875c7e8a9ba47d61517f56d733019cf76396d" + sha256: f77ea1aa1ba29a38fcce04483f44f12382f541b9e8c2150df37166c23bbbd30f url: "https://pub.dev" source: hosted - version: "0.10.6" + version: "0.16.0" fwfh_cached_network_image: dependency: transitive description: name: fwfh_cached_network_image - sha256: "3de22aa3a6943c968e0d9fbcba4463b3dbbf7103171d62c84b6c672fb83eebdf" + sha256: "8f4896109ff3e42424ccacf9058ba3afe5d575b58946c8ac646ac85ae882ce23" url: "https://pub.dev" source: hosted - version: "0.7.0+7" + version: "0.16.0" fwfh_chewie: dependency: transitive description: name: fwfh_chewie - sha256: "0b51a1c976bb74da5e8e45d545c74cb54a7168ad3938dd77103a7aee485f55fa" + sha256: "1ce7c56894db19881a997813b933835dec142878431370c0eb40f1f878396a25" url: "https://pub.dev" source: hosted - version: "0.7.1+4" + version: "0.16.0" fwfh_just_audio: dependency: transitive description: name: fwfh_just_audio - sha256: "237b93a4cb9f0495a4b51940f361adda2a5abd57231dd44f07459db00144a6cd" + sha256: "17816168de1fd180fd3d1fd4500e23136630a248a6889b553e2d2067e133c1a6" url: "https://pub.dev" source: hosted - version: "0.9.0+3" + version: "0.16.0" fwfh_svg: dependency: transitive description: name: fwfh_svg - sha256: c6bb6b513f7ce2766aba76d7276caf9a96b6fee729ac3a492c366a42f82ef02e + sha256: "82f3eb378186fe39b3e2e01ed48a1830d34b0b9a237d951077e74ff0d99e2ac3" url: "https://pub.dev" source: hosted - version: "0.8.2" + version: "0.16.0" fwfh_url_launcher: dependency: transitive description: name: fwfh_url_launcher - sha256: b9f5d55a5ae2c2c07243ba33f7ba49ac9544bdb2f4c16d8139df9ccbebe3449c + sha256: "5cf1b1baa16740abaef8eb41a8e16ba430295d5ec20b880e4cb94e2924774f0a" url: "https://pub.dev" source: hosted - version: "0.9.1" + version: "0.16.0" fwfh_webview: dependency: transitive description: name: fwfh_webview - sha256: "90a8dda0695403cf57abd7e8b83f6fb1f1a12933930a0bf9cac7cafb06e06a18" + sha256: "894aa7d98ebdc2d86d79ac2309173043dec7f102575de87bf9626ddb26104e49" url: "https://pub.dev" source: hosted - version: "0.9.0+2" + version: "0.15.4" html: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.6" http: dependency: "direct main" description: @@ -412,10 +324,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -428,50 +340,50 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.2" just_audio: dependency: transitive description: name: just_audio - sha256: b7cb6bbf3750caa924d03f432ba401ec300fd90936b3f73a9b33d58b1e96286b + sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e url: "https://pub.dev" source: hosted - version: "0.9.37" + version: "0.9.46" just_audio_platform_interface: dependency: transitive description: name: just_audio_platform_interface - sha256: "0243828cce503c8366cc2090cefb2b3c871aa8ed2f520670d76fd47aa1ab2790" + sha256: "4cd94536af0219fa306205a58e78d67e02b0555283c1c094ee41e402a14a5c4a" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.5.0" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: "134356b0fe3d898293102b33b5fd618831ffdc72bb7a1b726140abdf22772b70" + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.4.16" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -488,38 +400,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - list_counter: + logging: dependency: transitive description: - name: list_counter - sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.16.0" mime: dependency: "direct main" description: @@ -544,22 +456,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" + url: "https://pub.dev" + source: hosted + version: "8.3.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" + url: "https://pub.dev" + source: hosted + version: "3.2.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: transitive description: @@ -572,18 +500,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.9" + version: "2.2.17" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -612,18 +540,18 @@ packages: dependency: "direct main" description: name: pdfrx - sha256: "29c7b03d27d647c80da8cc08bd1256c74df90e5640fdd676646e4bd04f90553a" + sha256: "90747e916a64366b8beb69e9ac175e9134df520110543ab284b839102bf4b7e1" url: "https://pub.dev" source: hosted - version: "1.0.94" + version: "1.1.29" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" photo_view: dependency: "direct main" description: @@ -636,10 +564,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -692,42 +620,42 @@ packages: dependency: "direct main" description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" quiver: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" rxdart: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -756,10 +684,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -772,15 +700,15 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -793,66 +721,90 @@ packages: dependency: transitive description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.5" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.4" tuple: dependency: transitive description: @@ -865,10 +817,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" url_launcher: dependency: transitive description: @@ -881,34 +833,34 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.8" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -921,50 +873,50 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049 + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" uuid: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.1" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.18" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.16" vector_math: dependency: transitive description: @@ -977,90 +929,66 @@ packages: dependency: transitive description: name: video_player - sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d + sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14" url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.9.5" video_player_android: dependency: transitive description: name: video_player_android - sha256: b6f0a6d241e4a3435806cb7cb78cb666db8889c1866e432b6acd204707b3ac01 + sha256: "28dcc4122079f40f93a0965b3679aff1a5f4251cf79611bd8011f937eb6b69de" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.8.4" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c + sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.7.1" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" video_player_web: dependency: transitive description: name: video_player_web - sha256: "8e9cb7fe94e49490e67bbc15149691792b58a0ade31b32e3f3688d104a0e057b" + sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.5" vm_service: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.1" - wakelock: + version: "15.0.0" + wakelock_plus: dependency: transitive description: - name: wakelock - sha256: "769ecf42eb2d07128407b50cb93d7c10bd2ee48f0276ef0119db1d25cc2f87db" + name: wakelock_plus + sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678 url: "https://pub.dev" source: hosted - version: "0.6.2" - wakelock_macos: + version: "1.3.2" + wakelock_plus_platform_interface: dependency: transitive description: - name: wakelock_macos - sha256: "047c6be2f88cb6b76d02553bca5a3a3b95323b15d30867eca53a19a0a319d4cd" + name: wakelock_plus_platform_interface + sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207 url: "https://pub.dev" source: hosted - version: "0.4.0" - wakelock_platform_interface: - dependency: transitive - description: - name: wakelock_platform_interface - sha256: "1f4aeb81fb592b863da83d2d0f7b8196067451e4df91046c26b54a403f9de621" - url: "https://pub.dev" - source: hosted - version: "0.3.0" - wakelock_web: - dependency: transitive - description: - name: wakelock_web - sha256: "1b256b811ee3f0834888efddfe03da8d18d0819317f20f6193e2922b41a501b5" - url: "https://pub.dev" - source: hosted - version: "0.4.0" - wakelock_windows: - dependency: transitive - description: - name: wakelock_windows - sha256: "857f77b3fe6ae82dd045455baa626bc4b93cb9bb6c86bf3f27c182167c3a5567" - url: "https://pub.dev" - source: hosted - version: "0.2.1" + version: "1.2.3" web: dependency: "direct main" description: @@ -1073,50 +1001,50 @@ packages: dependency: transitive description: name: webview_flutter - sha256: "6869c8786d179f929144b4a1f86e09ac0eddfe475984951ea6c634774c16b522" + sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.13.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: c66651fba15f9d7ddd31daec42da8d6bce46c85610a7127e3ebcb39a4395c3c9 + sha256: f6e6afef6e234801da77170f7a1847ded8450778caf2fe13979d140484be3678 url: "https://pub.dev" source: hosted - version: "3.16.6" + version: "4.7.0" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + sha256: "7cb32b21825bd65569665c32bb00a34ded5779786d6201f5350979d2d529940d" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.13.0" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "9c62cc46fa4f2d41e10ab81014c1de470a6c6f26051a2de32111b2ee55287feb" + sha256: a3d461fe3467014e05f3ac4962e5fdde2a4bf44c561cb53e9ae5c586600fdbc3 url: "https://pub.dev" source: hosted - version: "3.14.0" + version: "3.22.0" win32: dependency: transitive description: name: win32 - sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "5.13.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1126,5 +1054,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8de6880..284635b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,14 +12,12 @@ dependencies: flutter: sdk: flutter http: 1.2.2 - flutter_html_all: 3.0.0-beta.2 - flutter_widget_from_html: ^0.10.0 shared_preferences: ^2.0.6 encrypt: ^5.0.0 pointycastle: ^3.4.0 mime: ^1.0.3 pointer_interceptor: ^0.10.1+2 - file_saver: ^0.2.14 + english_words: ^4.0.0 @@ -28,6 +26,14 @@ dependencies: pdfrx: ^1.0.94 photo_view: ^0.15.0 web: ^1.1.1 + flutter_widget_from_html: ^0.16.0 + html2md: ^1.3.2 + markdown_widget: ^2.3.2+8 + markdown: ^7.3.0 + go_router: ^16.0.0 + super_editor: ^0.3.0-dev.27 + super_editor_markdown: 0.1.8 + dev_dependencies: flutter_test: @@ -39,6 +45,7 @@ dependency_overrides: flutter_layout_grid: 2.0.7 flutter_math_fork: 0.7.2 + flutter: uses-material-design: true assets: