commit cb24b68069e2f429b673a80cd7c806ea1e6499b9 Author: Sarthak Kimtani Date: Sat Feb 17 20:32:01 2024 +0530 v1 release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..2e239db --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: android + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: ios + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: linux + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: macos + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: web + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + - platform: windows + create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..43ec0c7 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Sarthak Kimtani + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..13c2a07 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# FlashMail + +FlashMail is a Mobile application designed to provide users with a temporary email solution powered by the mail.tm platform. With FlashMail, users can receive and view emails conveniently while maintaining their privacy. The app utilizes Riverpod as its state management solution for efficient and organized data handling. + +This repository contains the source code for FlashMail developed using the Flutter framework. The app is designed to provide various features and functionalities to enhance user experience. Below, you will find information about how to download the app, its features, dependencies, license, and more. + +## Download + +You can download the latest version of the app from the [GitHub Releases](https://github.com/sarthakkimtani/flash-mail/releases) page. Look for the most recent release and download the corresponding APK or installer file for your device. + +## Requirements + +- Any Operating System (ie. MacOS X, Linux, Windows) +- Any IDE with Flutter SDK installed (ie. IntelliJ, Android Studio, VSCode, etc) +- A little knowledge of Dart and Flutter + +## Features + +- Instantly create temporary email addresses to receive messages without revealing your personal email. +- Enjoy a seamless user experience with an intuitive and user-friendly interface, making it easy to navigate through your emails effortlessly. +- Access and download attachments with ease, allowing you to view and save important files directly from your emails. +- Access and read your emails effortlessly with a seamless viewing experience. +- Benefit from a compact APK size of just 22 MB, ensuring that FlashMail takes up minimal storage space on your device. + +## Screenshots + + + + +## Installation + +To run the app locally and make modifications, follow these steps: + +1. Ensure you have Flutter SDK installed on your machine. You can download it from the official Flutter website: https://flutter.dev. +2. Clone this repository to your local machine using the following command:
+ ```bash + git clone https://github.com/sarthakkimtani/flash-mail.git + ``` +3. Navigate to the project directory:

+ ```bash + cd flash-mail + ``` +4. Fetch the app's dependencies by running the following command:
+ ```bash + flutter pub get + ``` +5. Connect your device or start an emulator. +6. Run the app using the following command:
+ ```bash + flutter run + ``` + +## Dependencies + +| Name | Usage | +| ------------------------------------------------------------------------- | --------------------------- | +| [**Auto Route**](https://pub.dev/packages/auto_route) | Navigation & Routing | +| [**Dio**](https://pub.dev/packages/dio) | HTTP Requests | +| [**External Path**](https://pub.dev/packages/external_path) | External Storage Path | +| [**Flash**](https://pub.dev/packages/flash) | Alerts & Dialogs | +| [**Flutter Downloader**](https://pub.dev/packages/flutter_downloader) | Download Files | +| [**Flutter InAppWebView**](https://pub.dev/packages/flutter_inappwebview) | In-App WebView | +| [**Flutter Riverpod**](https://pub.dev/packages/flutter_riverpod) | Global State Management | +| [**Freezed**](https://pub.dev/packages/freezed) | Code Generation | +| [**Hive**](https://pub.dev/packages/hive) | Local Database | +| [**Intl**](https://pub.dev/packages/intl) | Internationalization | +| [**Shimmer**](https://pub.dev/packages/shimmer) | Shimmer for Loading Screens | +| [**URL Launcher**](https://pub.dev/packages/url_launcher) | URL Launcher | + +## Contributing + +Contributions to this app are welcome! If you find any issues or have ideas for improvements, please open an issue or submit a pull request. Make sure to follow the repository's guidelines for contributing. + +## License + +This app is distributed under the [MIT License](https://github.com/sarthakkimtani/flash-mail/blob/main/LICENSE). Feel free to modify and use it as per your requirements. + +## Disclaimer + +FlashMail is an independent project and is not affiliated with or endorsed by the mail.tm platform. Please review and comply with the terms of service of mail.tm when using this application. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..b07c1de --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,10 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - lib/data/models/*/*.g.dart + - lib/data/models/*/*.freezed.dart + +linter: + rules: + prefer_double_quotes: true diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.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/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..a1b61ba --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,88 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +android { + namespace "com.example.flash_mail" + compileSdkVersion 34 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + applicationId "com.app.flash_mail" + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + } + } + + buildTypes { + release { + signingConfig signingConfigs.release + } + } + } + + buildTypes { + release { + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /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..a32e4d5 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/flash_mail/MainActivity.kt b/android/app/src/main/kotlin/com/example/flash_mail/MainActivity.kt new file mode 100644 index 0000000..9ffc340 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/flash_mail/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.flash_mail + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2a2e0b7 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 0000000..783f21f Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..502c348 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splash.png b/android/app/src/main/res/drawable-mdpi/splash.png new file mode 100644 index 0000000..e1c91a4 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png new file mode 100644 index 0000000..bcfb067 Binary files /dev/null and b/android/app/src/main/res/drawable-v21/background.png differ 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..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f93eeeb Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splash.png b/android/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 0000000..69ae71b Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..1a9b6e6 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splash.png b/android/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 0000000..2c74b8b Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..00c6753 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png new file mode 100644 index 0000000..a14ee37 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png new file mode 100644 index 0000000..bcfb067 Binary files /dev/null and b/android/app/src/main/res/drawable/background.png differ 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..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml b/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml new file mode 100644 index 0000000..5f349f7 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml @@ -0,0 +1,5 @@ + + + + + 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-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..534cec8 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.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-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..5e5bc83 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.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-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..3354320 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.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-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..70eee0b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.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/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..8a4d17c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 0000000..7cc5bc8 --- /dev/null +++ b/android/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,20 @@ + + + + + + + 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..dbc9ea9 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 0000000..abd3d6f --- /dev/null +++ b/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..3288102 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #01111d + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..7af18d9 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + Download started + Download in progress + Download canceled + Download failed + Download complete + Download paused + \ No newline at end of file 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..0d1fa8f --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..f7eb7f6 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.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/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +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..3c472b9 --- /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-7.5-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/assets/fonts/FlashMailIcons.ttf b/assets/fonts/FlashMailIcons.ttf new file mode 100644 index 0000000..5c448b1 Binary files /dev/null and b/assets/fonts/FlashMailIcons.ttf differ diff --git a/assets/fonts/Inter-Black.ttf b/assets/fonts/Inter-Black.ttf new file mode 100644 index 0000000..b27822b Binary files /dev/null and b/assets/fonts/Inter-Black.ttf differ diff --git a/assets/fonts/Inter-Bold.ttf b/assets/fonts/Inter-Bold.ttf new file mode 100644 index 0000000..fe23eeb Binary files /dev/null and b/assets/fonts/Inter-Bold.ttf differ diff --git a/assets/fonts/Inter-ExtraBold.ttf b/assets/fonts/Inter-ExtraBold.ttf new file mode 100644 index 0000000..874b1b0 Binary files /dev/null and b/assets/fonts/Inter-ExtraBold.ttf differ diff --git a/assets/fonts/Inter-Light.ttf b/assets/fonts/Inter-Light.ttf new file mode 100644 index 0000000..71188f5 Binary files /dev/null and b/assets/fonts/Inter-Light.ttf differ diff --git a/assets/fonts/Inter-Medium.ttf b/assets/fonts/Inter-Medium.ttf new file mode 100644 index 0000000..a01f377 Binary files /dev/null and b/assets/fonts/Inter-Medium.ttf differ diff --git a/assets/fonts/Inter-Regular.ttf b/assets/fonts/Inter-Regular.ttf new file mode 100644 index 0000000..5e4851f Binary files /dev/null and b/assets/fonts/Inter-Regular.ttf differ diff --git a/assets/images/avatar.png b/assets/images/avatar.png new file mode 100644 index 0000000..0eaf3c6 Binary files /dev/null and b/assets/images/avatar.png differ diff --git a/assets/images/empty.png b/assets/images/empty.png new file mode 100644 index 0000000..f7029ac Binary files /dev/null and b/assets/images/empty.png differ diff --git a/assets/images/error.png b/assets/images/error.png new file mode 100644 index 0000000..86204e3 Binary files /dev/null and b/assets/images/error.png differ diff --git a/assets/images/icon.png b/assets/images/icon.png new file mode 100644 index 0000000..ac7995c Binary files /dev/null and b/assets/images/icon.png differ diff --git a/assets/images/logo-small.png b/assets/images/logo-small.png new file mode 100644 index 0000000..882ee68 Binary files /dev/null and b/assets/images/logo-small.png differ diff --git a/assets/images/splash.png b/assets/images/splash.png new file mode 100644 index 0000000..de0bb59 Binary files /dev/null and b/assets/images/splash.png differ diff --git a/lib/data/api/api_client.dart b/lib/data/api/api_client.dart new file mode 100644 index 0000000..4bad7f6 --- /dev/null +++ b/lib/data/api/api_client.dart @@ -0,0 +1,123 @@ +import "dart:convert"; + +import "package:dio/dio.dart"; +import "package:flash_mail/data/providers/providers.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "package:flash_mail/data/models/account/account.dart"; +import "package:flash_mail/data/models/message/message.dart"; +import "package:flash_mail/data/models/message_detail/message_detail.dart"; +import "package:flash_mail/shared/utils/random_string.dart"; + +class ApiClient { + final Dio dio; + final Ref ref; + + ApiClient(this.dio, this.ref); + + Future generateToken(String address, String password) async { + final tokenResponse = await dio.post( + "/token", + data: jsonEncode({"address": address, "password": password}), + ); + final Map tokenData = + tokenResponse.data as Map; + + ref.read(tokenProvider.notifier).state = tokenData["token"]; + } + + Future createAccount() async { + final response = await dio.get("/domains"); + final Map data = response.data as Map; + final List> domains = List.from(data["hydra:member"]); + + final username = randomString(7); + final domain = domains[0]["domain"]; + Map payload = { + "address": "$username@$domain", + "password": randomString(10), + }; + + final userResponse = await dio.post( + "/accounts", + data: jsonEncode(payload), + ); + final Map info = userResponse.data as Map; + payload["address"] = info["address"]; + + await generateToken( + payload["address"] as String, + payload["password"] as String, + ); + + return TMAccount( + id: info["id"], + email: info["address"], + password: payload["password"]!, + quota: info["quota"], + used: info["used"], + ); + } + + Future login(String address, String password) async { + final response = await dio.post( + "/token", + data: jsonEncode({"address": address, "password": password}), + ); + final Map tokenData = + response.data as Map; + + ref.read(tokenProvider.notifier).state = tokenData["token"]; + final userId = tokenData["id"]; + + final options = Options(headers: { + "Authorization": "Bearer ${tokenData["token"]}", + }); + final userResponse = await dio.get("/accounts/$userId", options: options); + final Map info = userResponse.data as Map; + + return TMAccount( + id: info["id"], + email: address, + password: password, + quota: info["quota"], + used: info["used"], + ); + } + + Future> fetchMessages(String email, String password) async { + if (ref.read(tokenProvider) == null) { + await generateToken(email, password); + } + + final token = ref.read(tokenProvider); + final options = Options(headers: {"Authorization": "Bearer $token"}); + final response = await dio.get("/messages", options: options); + + Map messageData = response.data as Map; + final messages = + List>.from(messageData["hydra:member"]); + + return messages.map((message) => TMMessage.fromJson(message)).toList(); + } + + Future fetchMessageDetail(String id) async { + if (ref.read(tokenProvider) == null) throw Exception("Token not found"); + + final token = ref.read(tokenProvider); + final options = Options(headers: {"Authorization": "Bearer $token"}); + final response = await dio.get("/messages/$id", options: options); + + Map messageData = response.data as Map; + + return TMMessageDetail.fromJson(messageData); + } + + Future deleteMessage(String id) async { + if (ref.read(tokenProvider) == null) throw Exception("Token not found"); + + final token = ref.read(tokenProvider); + final options = Options(headers: {"Authorization": "Bearer $token"}); + await dio.delete("/messages/$id", options: options); + } +} diff --git a/lib/data/models/account/account.dart b/lib/data/models/account/account.dart new file mode 100644 index 0000000..22fe7f0 --- /dev/null +++ b/lib/data/models/account/account.dart @@ -0,0 +1,20 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:hive/hive.dart"; + +part "account.freezed.dart"; +part "account.g.dart"; + +@freezed +class TMAccount with _$TMAccount { + @HiveType(typeId: 0, adapterName: "AccountAdapter") + factory TMAccount({ + @HiveField(0) required String id, + @HiveField(1) required String email, + @HiveField(2) required String password, + @HiveField(3) required int quota, + @HiveField(4) required int used, + }) = _TMAccount; + + factory TMAccount.fromJson(Map json) => + _$TMAccountFromJson(json); +} diff --git a/lib/data/models/account/account.freezed.dart b/lib/data/models/account/account.freezed.dart new file mode 100644 index 0000000..3abb17b --- /dev/null +++ b/lib/data/models/account/account.freezed.dart @@ -0,0 +1,252 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'account.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +TMAccount _$TMAccountFromJson(Map json) { + return _TMAccount.fromJson(json); +} + +/// @nodoc +mixin _$TMAccount { + @HiveField(0) + String get id => throw _privateConstructorUsedError; + @HiveField(1) + String get email => throw _privateConstructorUsedError; + @HiveField(2) + String get password => throw _privateConstructorUsedError; + @HiveField(3) + int get quota => throw _privateConstructorUsedError; + @HiveField(4) + int get used => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $TMAccountCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TMAccountCopyWith<$Res> { + factory $TMAccountCopyWith(TMAccount value, $Res Function(TMAccount) then) = + _$TMAccountCopyWithImpl<$Res, TMAccount>; + @useResult + $Res call( + {@HiveField(0) String id, + @HiveField(1) String email, + @HiveField(2) String password, + @HiveField(3) int quota, + @HiveField(4) int used}); +} + +/// @nodoc +class _$TMAccountCopyWithImpl<$Res, $Val extends TMAccount> + implements $TMAccountCopyWith<$Res> { + _$TMAccountCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? email = null, + Object? password = null, + Object? quota = null, + Object? used = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + quota: null == quota + ? _value.quota + : quota // ignore: cast_nullable_to_non_nullable + as int, + used: null == used + ? _value.used + : used // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TMAccountImplCopyWith<$Res> + implements $TMAccountCopyWith<$Res> { + factory _$$TMAccountImplCopyWith( + _$TMAccountImpl value, $Res Function(_$TMAccountImpl) then) = + __$$TMAccountImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@HiveField(0) String id, + @HiveField(1) String email, + @HiveField(2) String password, + @HiveField(3) int quota, + @HiveField(4) int used}); +} + +/// @nodoc +class __$$TMAccountImplCopyWithImpl<$Res> + extends _$TMAccountCopyWithImpl<$Res, _$TMAccountImpl> + implements _$$TMAccountImplCopyWith<$Res> { + __$$TMAccountImplCopyWithImpl( + _$TMAccountImpl _value, $Res Function(_$TMAccountImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? email = null, + Object? password = null, + Object? quota = null, + Object? used = null, + }) { + return _then(_$TMAccountImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + quota: null == quota + ? _value.quota + : quota // ignore: cast_nullable_to_non_nullable + as int, + used: null == used + ? _value.used + : used // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +@HiveType(typeId: 0, adapterName: "AccountAdapter") +class _$TMAccountImpl implements _TMAccount { + _$TMAccountImpl( + {@HiveField(0) required this.id, + @HiveField(1) required this.email, + @HiveField(2) required this.password, + @HiveField(3) required this.quota, + @HiveField(4) required this.used}); + + factory _$TMAccountImpl.fromJson(Map json) => + _$$TMAccountImplFromJson(json); + + @override + @HiveField(0) + final String id; + @override + @HiveField(1) + final String email; + @override + @HiveField(2) + final String password; + @override + @HiveField(3) + final int quota; + @override + @HiveField(4) + final int used; + + @override + String toString() { + return 'TMAccount(id: $id, email: $email, password: $password, quota: $quota, used: $used)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TMAccountImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.email, email) || other.email == email) && + (identical(other.password, password) || + other.password == password) && + (identical(other.quota, quota) || other.quota == quota) && + (identical(other.used, used) || other.used == used)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, id, email, password, quota, used); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$TMAccountImplCopyWith<_$TMAccountImpl> get copyWith => + __$$TMAccountImplCopyWithImpl<_$TMAccountImpl>(this, _$identity); + + @override + Map toJson() { + return _$$TMAccountImplToJson( + this, + ); + } +} + +abstract class _TMAccount implements TMAccount { + factory _TMAccount( + {@HiveField(0) required final String id, + @HiveField(1) required final String email, + @HiveField(2) required final String password, + @HiveField(3) required final int quota, + @HiveField(4) required final int used}) = _$TMAccountImpl; + + factory _TMAccount.fromJson(Map json) = + _$TMAccountImpl.fromJson; + + @override + @HiveField(0) + String get id; + @override + @HiveField(1) + String get email; + @override + @HiveField(2) + String get password; + @override + @HiveField(3) + int get quota; + @override + @HiveField(4) + int get used; + @override + @JsonKey(ignore: true) + _$$TMAccountImplCopyWith<_$TMAccountImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/account/account.g.dart b/lib/data/models/account/account.g.dart new file mode 100644 index 0000000..e467819 --- /dev/null +++ b/lib/data/models/account/account.g.dart @@ -0,0 +1,75 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'account.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class AccountAdapter extends TypeAdapter<_$TMAccountImpl> { + @override + final int typeId = 0; + + @override + _$TMAccountImpl read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return _$TMAccountImpl( + id: fields[0] as String, + email: fields[1] as String, + password: fields[2] as String, + quota: fields[3] as int, + used: fields[4] as int, + ); + } + + @override + void write(BinaryWriter writer, _$TMAccountImpl obj) { + writer + ..writeByte(5) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.email) + ..writeByte(2) + ..write(obj.password) + ..writeByte(3) + ..write(obj.quota) + ..writeByte(4) + ..write(obj.used); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AccountAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$TMAccountImpl _$$TMAccountImplFromJson(Map json) => + _$TMAccountImpl( + id: json['id'] as String, + email: json['email'] as String, + password: json['password'] as String, + quota: json['quota'] as int, + used: json['used'] as int, + ); + +Map _$$TMAccountImplToJson(_$TMAccountImpl instance) => + { + 'id': instance.id, + 'email': instance.email, + 'password': instance.password, + 'quota': instance.quota, + 'used': instance.used, + }; diff --git a/lib/data/models/message/message.dart b/lib/data/models/message/message.dart new file mode 100644 index 0000000..f1536c6 --- /dev/null +++ b/lib/data/models/message/message.dart @@ -0,0 +1,23 @@ +import "package:freezed_annotation/freezed_annotation.dart"; + +part "message.freezed.dart"; +part "message.g.dart"; + +@freezed +class TMMessage with _$TMMessage { + factory TMMessage({ + required String id, + required Map from, + required List> to, + required String? intro, + required String subject, + required bool seen, + required bool isDeleted, + required bool hasAttachments, + required String downloadUrl, + required DateTime createdAt, + }) = _TMMessage; + + factory TMMessage.fromJson(Map json) => + _$TMMessageFromJson(json); +} diff --git a/lib/data/models/message/message.freezed.dart b/lib/data/models/message/message.freezed.dart new file mode 100644 index 0000000..f0815ca --- /dev/null +++ b/lib/data/models/message/message.freezed.dart @@ -0,0 +1,363 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'message.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +TMMessage _$TMMessageFromJson(Map json) { + return _TMMessage.fromJson(json); +} + +/// @nodoc +mixin _$TMMessage { + String get id => throw _privateConstructorUsedError; + Map get from => throw _privateConstructorUsedError; + List> get to => throw _privateConstructorUsedError; + String? get intro => throw _privateConstructorUsedError; + String get subject => throw _privateConstructorUsedError; + bool get seen => throw _privateConstructorUsedError; + bool get isDeleted => throw _privateConstructorUsedError; + bool get hasAttachments => throw _privateConstructorUsedError; + String get downloadUrl => throw _privateConstructorUsedError; + DateTime get createdAt => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $TMMessageCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TMMessageCopyWith<$Res> { + factory $TMMessageCopyWith(TMMessage value, $Res Function(TMMessage) then) = + _$TMMessageCopyWithImpl<$Res, TMMessage>; + @useResult + $Res call( + {String id, + Map from, + List> to, + String? intro, + String subject, + bool seen, + bool isDeleted, + bool hasAttachments, + String downloadUrl, + DateTime createdAt}); +} + +/// @nodoc +class _$TMMessageCopyWithImpl<$Res, $Val extends TMMessage> + implements $TMMessageCopyWith<$Res> { + _$TMMessageCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? from = null, + Object? to = null, + Object? intro = freezed, + Object? subject = null, + Object? seen = null, + Object? isDeleted = null, + Object? hasAttachments = null, + Object? downloadUrl = null, + Object? createdAt = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + from: null == from + ? _value.from + : from // ignore: cast_nullable_to_non_nullable + as Map, + to: null == to + ? _value.to + : to // ignore: cast_nullable_to_non_nullable + as List>, + intro: freezed == intro + ? _value.intro + : intro // ignore: cast_nullable_to_non_nullable + as String?, + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + seen: null == seen + ? _value.seen + : seen // ignore: cast_nullable_to_non_nullable + as bool, + isDeleted: null == isDeleted + ? _value.isDeleted + : isDeleted // ignore: cast_nullable_to_non_nullable + as bool, + hasAttachments: null == hasAttachments + ? _value.hasAttachments + : hasAttachments // ignore: cast_nullable_to_non_nullable + as bool, + downloadUrl: null == downloadUrl + ? _value.downloadUrl + : downloadUrl // ignore: cast_nullable_to_non_nullable + as String, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TMMessageImplCopyWith<$Res> + implements $TMMessageCopyWith<$Res> { + factory _$$TMMessageImplCopyWith( + _$TMMessageImpl value, $Res Function(_$TMMessageImpl) then) = + __$$TMMessageImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + Map from, + List> to, + String? intro, + String subject, + bool seen, + bool isDeleted, + bool hasAttachments, + String downloadUrl, + DateTime createdAt}); +} + +/// @nodoc +class __$$TMMessageImplCopyWithImpl<$Res> + extends _$TMMessageCopyWithImpl<$Res, _$TMMessageImpl> + implements _$$TMMessageImplCopyWith<$Res> { + __$$TMMessageImplCopyWithImpl( + _$TMMessageImpl _value, $Res Function(_$TMMessageImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? from = null, + Object? to = null, + Object? intro = freezed, + Object? subject = null, + Object? seen = null, + Object? isDeleted = null, + Object? hasAttachments = null, + Object? downloadUrl = null, + Object? createdAt = null, + }) { + return _then(_$TMMessageImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + from: null == from + ? _value._from + : from // ignore: cast_nullable_to_non_nullable + as Map, + to: null == to + ? _value._to + : to // ignore: cast_nullable_to_non_nullable + as List>, + intro: freezed == intro + ? _value.intro + : intro // ignore: cast_nullable_to_non_nullable + as String?, + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + seen: null == seen + ? _value.seen + : seen // ignore: cast_nullable_to_non_nullable + as bool, + isDeleted: null == isDeleted + ? _value.isDeleted + : isDeleted // ignore: cast_nullable_to_non_nullable + as bool, + hasAttachments: null == hasAttachments + ? _value.hasAttachments + : hasAttachments // ignore: cast_nullable_to_non_nullable + as bool, + downloadUrl: null == downloadUrl + ? _value.downloadUrl + : downloadUrl // ignore: cast_nullable_to_non_nullable + as String, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$TMMessageImpl implements _TMMessage { + _$TMMessageImpl( + {required this.id, + required final Map from, + required final List> to, + required this.intro, + required this.subject, + required this.seen, + required this.isDeleted, + required this.hasAttachments, + required this.downloadUrl, + required this.createdAt}) + : _from = from, + _to = to; + + factory _$TMMessageImpl.fromJson(Map json) => + _$$TMMessageImplFromJson(json); + + @override + final String id; + final Map _from; + @override + Map get from { + if (_from is EqualUnmodifiableMapView) return _from; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_from); + } + + final List> _to; + @override + List> get to { + if (_to is EqualUnmodifiableListView) return _to; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_to); + } + + @override + final String? intro; + @override + final String subject; + @override + final bool seen; + @override + final bool isDeleted; + @override + final bool hasAttachments; + @override + final String downloadUrl; + @override + final DateTime createdAt; + + @override + String toString() { + return 'TMMessage(id: $id, from: $from, to: $to, intro: $intro, subject: $subject, seen: $seen, isDeleted: $isDeleted, hasAttachments: $hasAttachments, downloadUrl: $downloadUrl, createdAt: $createdAt)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TMMessageImpl && + (identical(other.id, id) || other.id == id) && + const DeepCollectionEquality().equals(other._from, _from) && + const DeepCollectionEquality().equals(other._to, _to) && + (identical(other.intro, intro) || other.intro == intro) && + (identical(other.subject, subject) || other.subject == subject) && + (identical(other.seen, seen) || other.seen == seen) && + (identical(other.isDeleted, isDeleted) || + other.isDeleted == isDeleted) && + (identical(other.hasAttachments, hasAttachments) || + other.hasAttachments == hasAttachments) && + (identical(other.downloadUrl, downloadUrl) || + other.downloadUrl == downloadUrl) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + id, + const DeepCollectionEquality().hash(_from), + const DeepCollectionEquality().hash(_to), + intro, + subject, + seen, + isDeleted, + hasAttachments, + downloadUrl, + createdAt); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$TMMessageImplCopyWith<_$TMMessageImpl> get copyWith => + __$$TMMessageImplCopyWithImpl<_$TMMessageImpl>(this, _$identity); + + @override + Map toJson() { + return _$$TMMessageImplToJson( + this, + ); + } +} + +abstract class _TMMessage implements TMMessage { + factory _TMMessage( + {required final String id, + required final Map from, + required final List> to, + required final String? intro, + required final String subject, + required final bool seen, + required final bool isDeleted, + required final bool hasAttachments, + required final String downloadUrl, + required final DateTime createdAt}) = _$TMMessageImpl; + + factory _TMMessage.fromJson(Map json) = + _$TMMessageImpl.fromJson; + + @override + String get id; + @override + Map get from; + @override + List> get to; + @override + String? get intro; + @override + String get subject; + @override + bool get seen; + @override + bool get isDeleted; + @override + bool get hasAttachments; + @override + String get downloadUrl; + @override + DateTime get createdAt; + @override + @JsonKey(ignore: true) + _$$TMMessageImplCopyWith<_$TMMessageImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/message/message.g.dart b/lib/data/models/message/message.g.dart new file mode 100644 index 0000000..1eea392 --- /dev/null +++ b/lib/data/models/message/message.g.dart @@ -0,0 +1,37 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'message.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$TMMessageImpl _$$TMMessageImplFromJson(Map json) => + _$TMMessageImpl( + id: json['id'] as String, + from: Map.from(json['from'] as Map), + to: (json['to'] as List) + .map((e) => Map.from(e as Map)) + .toList(), + intro: json['intro'] as String?, + subject: json['subject'] as String, + seen: json['seen'] as bool, + isDeleted: json['isDeleted'] as bool, + hasAttachments: json['hasAttachments'] as bool, + downloadUrl: json['downloadUrl'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + ); + +Map _$$TMMessageImplToJson(_$TMMessageImpl instance) => + { + 'id': instance.id, + 'from': instance.from, + 'to': instance.to, + 'intro': instance.intro, + 'subject': instance.subject, + 'seen': instance.seen, + 'isDeleted': instance.isDeleted, + 'hasAttachments': instance.hasAttachments, + 'downloadUrl': instance.downloadUrl, + 'createdAt': instance.createdAt.toIso8601String(), + }; diff --git a/lib/data/models/message_detail/message_detail.dart b/lib/data/models/message_detail/message_detail.dart new file mode 100644 index 0000000..4dcb23e --- /dev/null +++ b/lib/data/models/message_detail/message_detail.dart @@ -0,0 +1,25 @@ +import "package:freezed_annotation/freezed_annotation.dart"; + +part "message_detail.freezed.dart"; +part "message_detail.g.dart"; + +@freezed +class TMMessageDetail with _$TMMessageDetail { + factory TMMessageDetail({ + required String id, + required Map from, + required List> to, + required List cc, + required List bcc, + required String subject, + required String? text, + required List html, + required bool hasAttachments, + required List>? attachments, + required String downloadUrl, + required DateTime createdAt, + }) = _TMMessageDetail; + + factory TMMessageDetail.fromJson(Map json) => + _$TMMessageDetailFromJson(json); +} diff --git a/lib/data/models/message_detail/message_detail.freezed.dart b/lib/data/models/message_detail/message_detail.freezed.dart new file mode 100644 index 0000000..f21460a --- /dev/null +++ b/lib/data/models/message_detail/message_detail.freezed.dart @@ -0,0 +1,438 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'message_detail.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +TMMessageDetail _$TMMessageDetailFromJson(Map json) { + return _TMMessageDetail.fromJson(json); +} + +/// @nodoc +mixin _$TMMessageDetail { + String get id => throw _privateConstructorUsedError; + Map get from => throw _privateConstructorUsedError; + List> get to => throw _privateConstructorUsedError; + List get cc => throw _privateConstructorUsedError; + List get bcc => throw _privateConstructorUsedError; + String get subject => throw _privateConstructorUsedError; + String? get text => throw _privateConstructorUsedError; + List get html => throw _privateConstructorUsedError; + bool get hasAttachments => throw _privateConstructorUsedError; + List>? get attachments => + throw _privateConstructorUsedError; + String get downloadUrl => throw _privateConstructorUsedError; + DateTime get createdAt => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $TMMessageDetailCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TMMessageDetailCopyWith<$Res> { + factory $TMMessageDetailCopyWith( + TMMessageDetail value, $Res Function(TMMessageDetail) then) = + _$TMMessageDetailCopyWithImpl<$Res, TMMessageDetail>; + @useResult + $Res call( + {String id, + Map from, + List> to, + List cc, + List bcc, + String subject, + String? text, + List html, + bool hasAttachments, + List>? attachments, + String downloadUrl, + DateTime createdAt}); +} + +/// @nodoc +class _$TMMessageDetailCopyWithImpl<$Res, $Val extends TMMessageDetail> + implements $TMMessageDetailCopyWith<$Res> { + _$TMMessageDetailCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? from = null, + Object? to = null, + Object? cc = null, + Object? bcc = null, + Object? subject = null, + Object? text = freezed, + Object? html = null, + Object? hasAttachments = null, + Object? attachments = freezed, + Object? downloadUrl = null, + Object? createdAt = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + from: null == from + ? _value.from + : from // ignore: cast_nullable_to_non_nullable + as Map, + to: null == to + ? _value.to + : to // ignore: cast_nullable_to_non_nullable + as List>, + cc: null == cc + ? _value.cc + : cc // ignore: cast_nullable_to_non_nullable + as List, + bcc: null == bcc + ? _value.bcc + : bcc // ignore: cast_nullable_to_non_nullable + as List, + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + text: freezed == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String?, + html: null == html + ? _value.html + : html // ignore: cast_nullable_to_non_nullable + as List, + hasAttachments: null == hasAttachments + ? _value.hasAttachments + : hasAttachments // ignore: cast_nullable_to_non_nullable + as bool, + attachments: freezed == attachments + ? _value.attachments + : attachments // ignore: cast_nullable_to_non_nullable + as List>?, + downloadUrl: null == downloadUrl + ? _value.downloadUrl + : downloadUrl // ignore: cast_nullable_to_non_nullable + as String, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TMMessageDetailImplCopyWith<$Res> + implements $TMMessageDetailCopyWith<$Res> { + factory _$$TMMessageDetailImplCopyWith(_$TMMessageDetailImpl value, + $Res Function(_$TMMessageDetailImpl) then) = + __$$TMMessageDetailImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + Map from, + List> to, + List cc, + List bcc, + String subject, + String? text, + List html, + bool hasAttachments, + List>? attachments, + String downloadUrl, + DateTime createdAt}); +} + +/// @nodoc +class __$$TMMessageDetailImplCopyWithImpl<$Res> + extends _$TMMessageDetailCopyWithImpl<$Res, _$TMMessageDetailImpl> + implements _$$TMMessageDetailImplCopyWith<$Res> { + __$$TMMessageDetailImplCopyWithImpl( + _$TMMessageDetailImpl _value, $Res Function(_$TMMessageDetailImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? from = null, + Object? to = null, + Object? cc = null, + Object? bcc = null, + Object? subject = null, + Object? text = freezed, + Object? html = null, + Object? hasAttachments = null, + Object? attachments = freezed, + Object? downloadUrl = null, + Object? createdAt = null, + }) { + return _then(_$TMMessageDetailImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + from: null == from + ? _value._from + : from // ignore: cast_nullable_to_non_nullable + as Map, + to: null == to + ? _value._to + : to // ignore: cast_nullable_to_non_nullable + as List>, + cc: null == cc + ? _value._cc + : cc // ignore: cast_nullable_to_non_nullable + as List, + bcc: null == bcc + ? _value._bcc + : bcc // ignore: cast_nullable_to_non_nullable + as List, + subject: null == subject + ? _value.subject + : subject // ignore: cast_nullable_to_non_nullable + as String, + text: freezed == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String?, + html: null == html + ? _value._html + : html // ignore: cast_nullable_to_non_nullable + as List, + hasAttachments: null == hasAttachments + ? _value.hasAttachments + : hasAttachments // ignore: cast_nullable_to_non_nullable + as bool, + attachments: freezed == attachments + ? _value._attachments + : attachments // ignore: cast_nullable_to_non_nullable + as List>?, + downloadUrl: null == downloadUrl + ? _value.downloadUrl + : downloadUrl // ignore: cast_nullable_to_non_nullable + as String, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$TMMessageDetailImpl implements _TMMessageDetail { + _$TMMessageDetailImpl( + {required this.id, + required final Map from, + required final List> to, + required final List cc, + required final List bcc, + required this.subject, + required this.text, + required final List html, + required this.hasAttachments, + required final List>? attachments, + required this.downloadUrl, + required this.createdAt}) + : _from = from, + _to = to, + _cc = cc, + _bcc = bcc, + _html = html, + _attachments = attachments; + + factory _$TMMessageDetailImpl.fromJson(Map json) => + _$$TMMessageDetailImplFromJson(json); + + @override + final String id; + final Map _from; + @override + Map get from { + if (_from is EqualUnmodifiableMapView) return _from; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_from); + } + + final List> _to; + @override + List> get to { + if (_to is EqualUnmodifiableListView) return _to; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_to); + } + + final List _cc; + @override + List get cc { + if (_cc is EqualUnmodifiableListView) return _cc; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_cc); + } + + final List _bcc; + @override + List get bcc { + if (_bcc is EqualUnmodifiableListView) return _bcc; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_bcc); + } + + @override + final String subject; + @override + final String? text; + final List _html; + @override + List get html { + if (_html is EqualUnmodifiableListView) return _html; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_html); + } + + @override + final bool hasAttachments; + final List>? _attachments; + @override + List>? get attachments { + final value = _attachments; + if (value == null) return null; + if (_attachments is EqualUnmodifiableListView) return _attachments; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + final String downloadUrl; + @override + final DateTime createdAt; + + @override + String toString() { + return 'TMMessageDetail(id: $id, from: $from, to: $to, cc: $cc, bcc: $bcc, subject: $subject, text: $text, html: $html, hasAttachments: $hasAttachments, attachments: $attachments, downloadUrl: $downloadUrl, createdAt: $createdAt)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TMMessageDetailImpl && + (identical(other.id, id) || other.id == id) && + const DeepCollectionEquality().equals(other._from, _from) && + const DeepCollectionEquality().equals(other._to, _to) && + const DeepCollectionEquality().equals(other._cc, _cc) && + const DeepCollectionEquality().equals(other._bcc, _bcc) && + (identical(other.subject, subject) || other.subject == subject) && + (identical(other.text, text) || other.text == text) && + const DeepCollectionEquality().equals(other._html, _html) && + (identical(other.hasAttachments, hasAttachments) || + other.hasAttachments == hasAttachments) && + const DeepCollectionEquality() + .equals(other._attachments, _attachments) && + (identical(other.downloadUrl, downloadUrl) || + other.downloadUrl == downloadUrl) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + id, + const DeepCollectionEquality().hash(_from), + const DeepCollectionEquality().hash(_to), + const DeepCollectionEquality().hash(_cc), + const DeepCollectionEquality().hash(_bcc), + subject, + text, + const DeepCollectionEquality().hash(_html), + hasAttachments, + const DeepCollectionEquality().hash(_attachments), + downloadUrl, + createdAt); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$TMMessageDetailImplCopyWith<_$TMMessageDetailImpl> get copyWith => + __$$TMMessageDetailImplCopyWithImpl<_$TMMessageDetailImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$TMMessageDetailImplToJson( + this, + ); + } +} + +abstract class _TMMessageDetail implements TMMessageDetail { + factory _TMMessageDetail( + {required final String id, + required final Map from, + required final List> to, + required final List cc, + required final List bcc, + required final String subject, + required final String? text, + required final List html, + required final bool hasAttachments, + required final List>? attachments, + required final String downloadUrl, + required final DateTime createdAt}) = _$TMMessageDetailImpl; + + factory _TMMessageDetail.fromJson(Map json) = + _$TMMessageDetailImpl.fromJson; + + @override + String get id; + @override + Map get from; + @override + List> get to; + @override + List get cc; + @override + List get bcc; + @override + String get subject; + @override + String? get text; + @override + List get html; + @override + bool get hasAttachments; + @override + List>? get attachments; + @override + String get downloadUrl; + @override + DateTime get createdAt; + @override + @JsonKey(ignore: true) + _$$TMMessageDetailImplCopyWith<_$TMMessageDetailImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/message_detail/message_detail.g.dart b/lib/data/models/message_detail/message_detail.g.dart new file mode 100644 index 0000000..0bdbe51 --- /dev/null +++ b/lib/data/models/message_detail/message_detail.g.dart @@ -0,0 +1,45 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'message_detail.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$TMMessageDetailImpl _$$TMMessageDetailImplFromJson( + Map json) => + _$TMMessageDetailImpl( + id: json['id'] as String, + from: Map.from(json['from'] as Map), + to: (json['to'] as List) + .map((e) => Map.from(e as Map)) + .toList(), + cc: (json['cc'] as List).map((e) => e as String).toList(), + bcc: (json['bcc'] as List).map((e) => e as String).toList(), + subject: json['subject'] as String, + text: json['text'] as String?, + html: (json['html'] as List).map((e) => e as String).toList(), + hasAttachments: json['hasAttachments'] as bool, + attachments: (json['attachments'] as List?) + ?.map((e) => e as Map) + .toList(), + downloadUrl: json['downloadUrl'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + ); + +Map _$$TMMessageDetailImplToJson( + _$TMMessageDetailImpl instance) => + { + 'id': instance.id, + 'from': instance.from, + 'to': instance.to, + 'cc': instance.cc, + 'bcc': instance.bcc, + 'subject': instance.subject, + 'text': instance.text, + 'html': instance.html, + 'hasAttachments': instance.hasAttachments, + 'attachments': instance.attachments, + 'downloadUrl': instance.downloadUrl, + 'createdAt': instance.createdAt.toIso8601String(), + }; diff --git a/lib/data/notifiers/account_notifier.dart b/lib/data/notifiers/account_notifier.dart new file mode 100644 index 0000000..2158d25 --- /dev/null +++ b/lib/data/notifiers/account_notifier.dart @@ -0,0 +1,86 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:hive_flutter/adapters.dart"; + +import "package:flash_mail/data/models/account/account.dart"; +import "package:flash_mail/data/providers/providers.dart"; + +class AccountState { + final bool isLoading; + final TMAccount? user; + + const AccountState({ + this.isLoading = false, + this.user, + }); + + bool isAuthenticated() { + return user != null; + } +} + +class AccountNotifier extends StateNotifier { + static const _boxName = "accounts"; + static const _key = "user"; + + final Ref ref; + + AccountNotifier(this.ref) : super(const AccountState()); + + Future fetchAccount() async { + var box = await Hive.openBox(_boxName); + final user = box.get(_key, defaultValue: null); + + if (user != null) { + ref.read(emailProvider.notifier).state = user.email; + ref.read(passwordProvider.notifier).state = user.password; + state = AccountState(isLoading: false, user: user); + } + } + + Future createAccount() async { + state = const AccountState(isLoading: true); + + try { + TMAccount account = await ref.read(clientProvider).createAccount(); + var box = Hive.box(_boxName); + await box.put(_key, account); + + ref.read(emailProvider.notifier).state = account.email; + ref.read(passwordProvider.notifier).state = account.password; + state = AccountState(isLoading: false, user: account); + } catch (err) { + state = const AccountState(isLoading: false); + rethrow; + } + } + + Future login(String? address, String? password) async { + state = const AccountState(isLoading: true); + + try { + if (address != null && password != null) { + TMAccount account = + await ref.read(clientProvider).login(address, password); + var box = Hive.box(_boxName); + await box.put(_key, account); + + state = AccountState(isLoading: false, user: account); + } + } catch (err) { + state = const AccountState(isLoading: false); + rethrow; + } + } + + Future logout() async { + AccountState previous = state; + try { + var box = Hive.box(_boxName); + await box.delete(_key); + state = const AccountState(); + } catch (err) { + state = previous; + rethrow; + } + } +} diff --git a/lib/data/providers/providers.dart b/lib/data/providers/providers.dart new file mode 100644 index 0000000..341b562 --- /dev/null +++ b/lib/data/providers/providers.dart @@ -0,0 +1,40 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:dio/dio.dart"; + +import "package:flash_mail/data/api/api_client.dart"; +import "package:flash_mail/data/models/message/message.dart"; +import "package:flash_mail/data/models/message_detail/message_detail.dart"; +import "package:flash_mail/data/notifiers/account_notifier.dart"; + +final emailProvider = StateProvider((ref) => null); +final passwordProvider = StateProvider((ref) => null); + +final dioProvider = Provider( + (ref) => Dio( + BaseOptions( + baseUrl: "https://api.mail.tm", + connectTimeout: 2000, + ), + ), +); + +final clientProvider = Provider( + (ref) => ApiClient(ref.read(dioProvider), ref), +); + +final accountProvider = StateNotifierProvider( + (ref) => AccountNotifier(ref), +); + +final tokenProvider = StateProvider((ref) => null); + +final messagesProvider = FutureProvider.autoDispose>((ref) { + final email = ref.read(emailProvider); + final password = ref.read(passwordProvider); + + return ref.read(clientProvider).fetchMessages(email!, password!); +}); + +final messageDetailProvider = FutureProvider.family( + (ref, id) => ref.read(clientProvider).fetchMessageDetail(id), +); diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..f3bc786 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,24 @@ +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:flutter_downloader/flutter_downloader.dart"; +import "package:hive_flutter/adapters.dart"; + +import "package:flash_mail/data/models/account/account.dart"; +import "package:flash_mail/data/providers/providers.dart"; +import "package:flash_mail/presentation/app_widget.dart"; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Hive.initFlutter(); + Hive.registerAdapter(AccountAdapter()); + ProviderContainer container = ProviderContainer(); + await container.read(accountProvider.notifier).fetchAccount(); + await FlutterDownloader.initialize(); + + runApp( + UncontrolledProviderScope( + container: container, + child: const AppWidget(), + ), + ); +} diff --git a/lib/presentation/app_widget.dart b/lib/presentation/app_widget.dart new file mode 100644 index 0000000..3cd5b5e --- /dev/null +++ b/lib/presentation/app_widget.dart @@ -0,0 +1,19 @@ +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "package:flash_mail/presentation/theme/theme.dart"; +import "package:flash_mail/presentation/routes/app_router.dart"; + +class AppWidget extends ConsumerWidget { + const AppWidget({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return MaterialApp.router( + title: "FlashMail", + theme: appTheme, + debugShowCheckedModeBanner: false, + routerConfig: AppRouter(ref).config(), + ); + } +} diff --git a/lib/presentation/routes/account_guard.dart b/lib/presentation/routes/account_guard.dart new file mode 100644 index 0000000..3aa608a --- /dev/null +++ b/lib/presentation/routes/account_guard.dart @@ -0,0 +1,18 @@ +import "package:auto_route/auto_route.dart"; +import "package:flash_mail/data/providers/providers.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +class AccountGuard extends AutoRouteGuard { + final WidgetRef ref; + + AccountGuard(this.ref); + + @override + void onNavigation(NavigationResolver resolver, StackRouter router) { + if (!ref.read(accountProvider).isAuthenticated()) { + router.replaceNamed("/"); + } else { + resolver.next(true); + } + } +} diff --git a/lib/presentation/routes/app_router.dart b/lib/presentation/routes/app_router.dart new file mode 100644 index 0000000..334131a --- /dev/null +++ b/lib/presentation/routes/app_router.dart @@ -0,0 +1,37 @@ +import "package:auto_route/auto_route.dart"; +import "package:flash_mail/presentation/routes/account_guard.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "package:flash_mail/presentation/routes/auth_guard.dart"; + +import "app_router.gr.dart"; + +@AutoRouterConfig(replaceInRouteName: "Screen,Route") +class AppRouter extends $AppRouter { + final WidgetRef ref; + + AppRouter(this.ref); + + @override + List get routes => [ + AutoRoute( + page: AccountRoute.page, + path: "/", + initial: true, + guards: [AuthGuard(ref)], + ), + AutoRoute( + page: LoginRoute.page, + path: "/login", + ), + AutoRoute( + page: InboxRoute.page, + path: "/inbox", + guards: [AccountGuard(ref)], + ), + AutoRoute( + page: MessageDetailRoute.page, + path: "/message", + ) + ]; +} diff --git a/lib/presentation/routes/app_router.gr.dart b/lib/presentation/routes/app_router.gr.dart new file mode 100644 index 0000000..8fe6c27 --- /dev/null +++ b/lib/presentation/routes/app_router.gr.dart @@ -0,0 +1,139 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AutoRouterGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +// coverage:ignore-file + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:auto_route/auto_route.dart' as _i5; +import 'package:flash_mail/presentation/screens/account_screen.dart' as _i1; +import 'package:flash_mail/presentation/screens/inbox_screen.dart' as _i2; +import 'package:flash_mail/presentation/screens/login_screen.dart' as _i3; +import 'package:flash_mail/presentation/screens/message_detail_screen.dart' + as _i4; +import 'package:flutter/material.dart' as _i6; + +abstract class $AppRouter extends _i5.RootStackRouter { + $AppRouter({super.navigatorKey}); + + @override + final Map pagesMap = { + AccountRoute.name: (routeData) { + return _i5.AutoRoutePage( + routeData: routeData, + child: const _i1.AccountScreen(), + ); + }, + InboxRoute.name: (routeData) { + return _i5.AutoRoutePage( + routeData: routeData, + child: const _i2.InboxScreen(), + ); + }, + LoginRoute.name: (routeData) { + return _i5.AutoRoutePage( + routeData: routeData, + child: const _i3.LoginScreen(), + ); + }, + MessageDetailRoute.name: (routeData) { + final args = routeData.argsAs(); + return _i5.AutoRoutePage( + routeData: routeData, + child: _i4.MessageDetailScreen( + key: args.key, + id: args.id, + subject: args.subject, + ), + ); + }, + }; +} + +/// generated route for +/// [_i1.AccountScreen] +class AccountRoute extends _i5.PageRouteInfo { + const AccountRoute({List<_i5.PageRouteInfo>? children}) + : super( + AccountRoute.name, + initialChildren: children, + ); + + static const String name = 'AccountRoute'; + + static const _i5.PageInfo page = _i5.PageInfo(name); +} + +/// generated route for +/// [_i2.InboxScreen] +class InboxRoute extends _i5.PageRouteInfo { + const InboxRoute({List<_i5.PageRouteInfo>? children}) + : super( + InboxRoute.name, + initialChildren: children, + ); + + static const String name = 'InboxRoute'; + + static const _i5.PageInfo page = _i5.PageInfo(name); +} + +/// generated route for +/// [_i3.LoginScreen] +class LoginRoute extends _i5.PageRouteInfo { + const LoginRoute({List<_i5.PageRouteInfo>? children}) + : super( + LoginRoute.name, + initialChildren: children, + ); + + static const String name = 'LoginRoute'; + + static const _i5.PageInfo page = _i5.PageInfo(name); +} + +/// generated route for +/// [_i4.MessageDetailScreen] +class MessageDetailRoute extends _i5.PageRouteInfo { + MessageDetailRoute({ + _i6.Key? key, + required String id, + required String subject, + List<_i5.PageRouteInfo>? children, + }) : super( + MessageDetailRoute.name, + args: MessageDetailRouteArgs( + key: key, + id: id, + subject: subject, + ), + initialChildren: children, + ); + + static const String name = 'MessageDetailRoute'; + + static const _i5.PageInfo page = + _i5.PageInfo(name); +} + +class MessageDetailRouteArgs { + const MessageDetailRouteArgs({ + this.key, + required this.id, + required this.subject, + }); + + final _i6.Key? key; + + final String id; + + final String subject; + + @override + String toString() { + return 'MessageDetailRouteArgs{key: $key, id: $id, subject: $subject}'; + } +} diff --git a/lib/presentation/routes/auth_guard.dart b/lib/presentation/routes/auth_guard.dart new file mode 100644 index 0000000..4e53bb9 --- /dev/null +++ b/lib/presentation/routes/auth_guard.dart @@ -0,0 +1,18 @@ +import "package:auto_route/auto_route.dart"; +import "package:flash_mail/data/providers/providers.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +class AuthGuard extends AutoRouteGuard { + final WidgetRef ref; + + AuthGuard(this.ref); + + @override + void onNavigation(NavigationResolver resolver, StackRouter router) { + if (ref.read(accountProvider).isAuthenticated()) { + router.replaceNamed("/inbox"); + } else { + resolver.next(true); + } + } +} diff --git a/lib/presentation/screens/account_screen.dart b/lib/presentation/screens/account_screen.dart new file mode 100644 index 0000000..5125bdc --- /dev/null +++ b/lib/presentation/screens/account_screen.dart @@ -0,0 +1,34 @@ +import "package:flutter/material.dart"; +import "package:auto_route/auto_route.dart"; + +import "package:flash_mail/presentation/widgets/auth/account_buttons.dart"; + +@RoutePage() +class AccountScreen extends StatelessWidget { + const AccountScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Image( + image: AssetImage("assets/images/logo-small.png"), + ), + Text( + "FlashMail", + style: Theme.of(context).textTheme.titleLarge, + ) + ], + ), + const AccountButtons() + ], + ), + ); + } +} diff --git a/lib/presentation/screens/inbox_screen.dart b/lib/presentation/screens/inbox_screen.dart new file mode 100644 index 0000000..dc8f5a1 --- /dev/null +++ b/lib/presentation/screens/inbox_screen.dart @@ -0,0 +1,62 @@ +import "dart:async"; + +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "package:flash_mail/data/providers/providers.dart"; +import "package:flash_mail/presentation/widgets/drawer/app_drawer.dart"; +import "package:flash_mail/presentation/widgets/image_banner.dart"; +import "package:flash_mail/presentation/widgets/message/message_list.dart"; +import "package:flash_mail/presentation/widgets/shimmer/message_shimmer.dart"; + +@RoutePage() +class InboxScreen extends ConsumerStatefulWidget { + const InboxScreen({super.key}); + + @override + ConsumerState createState() => _InboxScreenState(); +} + +class _InboxScreenState extends ConsumerState { + Timer? _timer; + + @override + void initState() { + super.initState(); + _timer = Timer.periodic( + const Duration(seconds: 30), + (timer) => ref.refresh(messagesProvider), + ); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final messages = ref.watch(messagesProvider); + + return Scaffold( + appBar: AppBar( + title: const Text("Inbox"), + ), + drawer: const AppDrawer(), + body: messages.when( + data: (data) => MessageList( + messages: data, + onRefresh: () => ref.refresh(messagesProvider.future), + ), + error: (err, _) => const ImageBanner( + imgSrc: "assets/images/error.png", + text: "Something went wrong!", + ), + loading: () => const MessageShimmer(), + skipLoadingOnRefresh: true, + ), + ); + } +} diff --git a/lib/presentation/screens/login_screen.dart b/lib/presentation/screens/login_screen.dart new file mode 100644 index 0000000..80f1a85 --- /dev/null +++ b/lib/presentation/screens/login_screen.dart @@ -0,0 +1,31 @@ +import "package:flutter/material.dart"; +import "package:auto_route/auto_route.dart"; + +import "package:flash_mail/presentation/widgets/auth/login_form.dart"; + +@RoutePage() +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Padding( + padding: const EdgeInsets.all(25), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Login", + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 25), + LoginForm(), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/screens/message_detail_screen.dart b/lib/presentation/screens/message_detail_screen.dart new file mode 100644 index 0000000..45ba171 --- /dev/null +++ b/lib/presentation/screens/message_detail_screen.dart @@ -0,0 +1,71 @@ +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "package:flash_mail/data/providers/providers.dart"; +import "package:flash_mail/presentation/widgets/image_banner.dart"; +import "package:flash_mail/presentation/widgets/message/message_view.dart"; + +@RoutePage() +class MessageDetailScreen extends ConsumerWidget { + final String id; + final String subject; + + const MessageDetailScreen({ + super.key, + required this.id, + required this.subject, + }); + + void deleteMessage(BuildContext context, WidgetRef ref) async { + await ref.read(clientProvider).deleteMessage(id); + ref.invalidate(messagesProvider); + if (context.mounted) { + AutoRouter.of(context).pop(); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final message = ref.watch(messageDetailProvider(id)); + + return Scaffold( + appBar: AppBar( + actions: [ + IconButton( + onPressed: () => deleteMessage(context, ref), + icon: const Icon(Icons.delete, color: Colors.white), + ) + ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Column( + children: [ + Container( + alignment: Alignment.centerLeft, + child: Text( + subject, + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(fontSize: 20), + ), + ), + const SizedBox(height: 30), + message.when( + data: (data) => MessageView(message: data), + error: (err, _) => const ImageBanner( + imgSrc: "assets/images/error.png", + text: "Something went wrong!", + ), + loading: () => const SizedBox.shrink(), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/theme/theme.dart b/lib/presentation/theme/theme.dart new file mode 100644 index 0000000..d903bee --- /dev/null +++ b/lib/presentation/theme/theme.dart @@ -0,0 +1,95 @@ +import "package:flutter/material.dart"; + +final ThemeData appTheme = ThemeData( + colorScheme: ColorScheme.fromSwatch().copyWith( + primary: const Color.fromRGBO(63, 207, 142, 1), + background: const Color.fromRGBO(1, 17, 29, 1), + ), + appBarTheme: const AppBarTheme( + backgroundColor: Color.fromRGBO(1, 17, 29, 1), + iconTheme: IconThemeData( + color: Colors.white, + size: 30, + ), + titleTextStyle: TextStyle( + color: Colors.white, + fontFamily: "Inter", + fontWeight: FontWeight.w900, + fontSize: 30, + ), + elevation: 0.0, + ), + textTheme: const TextTheme( + bodySmall: TextStyle( + fontFamily: "Inter", + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.normal, + ), + bodyMedium: TextStyle( + fontFamily: "Inter", + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + bodyLarge: TextStyle( + fontFamily: "Inter", + fontSize: 24, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + titleMedium: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w300, + ), + titleLarge: TextStyle( + fontFamily: "Inter", + fontSize: 35, + color: Colors.white, + fontWeight: FontWeight.w900, + ), + labelSmall: TextStyle( + fontFamily: "Inter", + fontSize: 16, + color: Color.fromRGBO(127, 133, 140, 1), + fontWeight: FontWeight.w700, + ), + ), + snackBarTheme: const SnackBarThemeData( + backgroundColor: Colors.red, + contentTextStyle: TextStyle( + fontFamily: "Inter", + fontSize: 16, + color: Colors.white, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromRGBO(63, 207, 142, 1), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + textStyle: const TextStyle( + fontFamily: "Inter", + fontSize: 17, + fontWeight: FontWeight.w500, + ), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: const Color.fromRGBO(63, 207, 412, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + textStyle: const TextStyle( + fontFamily: "Inter", + fontWeight: FontWeight.w500, + fontSize: 17, + ), + ), + ), + useMaterial3: true, +); diff --git a/lib/presentation/widgets/attachment/attachment_button.dart b/lib/presentation/widgets/attachment/attachment_button.dart new file mode 100644 index 0000000..b5f9812 --- /dev/null +++ b/lib/presentation/widgets/attachment/attachment_button.dart @@ -0,0 +1,80 @@ +import "package:external_path/external_path.dart"; +import "package:flash/flash.dart"; +import "package:flutter/material.dart"; +import "package:flutter_downloader/flutter_downloader.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "package:flash_mail/data/providers/providers.dart"; +import "package:flash_mail/presentation/widgets/drawer/toast_message.dart"; + +class AttachmentButton extends ConsumerWidget { + final Map file; + + const AttachmentButton({super.key, required this.file}); + + Future downloadFile(BuildContext ctx, WidgetRef ref, String url) async { + final token = ref.read(tokenProvider); + showFlash( + context: ctx, + duration: const Duration(seconds: 2), + builder: (context, controller) => ToastMessage( + controller: controller, + icon: Icons.arrow_downward, + text: "Starting download!", + ), + ); + + await FlutterDownloader.enqueue( + headers: {"Authorization": "Bearer $token"}, + url: "https://api.mail.tm$url", + savedDir: await ExternalPath.getExternalStoragePublicDirectory( + ExternalPath.DIRECTORY_DOWNLOADS, + ), + saveInPublicStorage: true, + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), + decoration: BoxDecoration( + border: Border.all( + color: const Color.fromRGBO(137, 147, 164, 1), + ), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + const Icon( + Icons.file_open_rounded, + color: Colors.red, + size: 24, + ), + const SizedBox(width: 10), + Text( + file["filename"], + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w500, + ), + ), + Expanded( + child: Container( + alignment: Alignment.centerRight, + child: IconButton( + splashColor: const Color.fromRGBO(40, 70, 100, 1), + onPressed: () => + downloadFile(context, ref, file["downloadUrl"]), + icon: const Icon( + Icons.arrow_downward, + size: 24, + color: Colors.white, + ), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/attachment/attachment_list.dart b/lib/presentation/widgets/attachment/attachment_list.dart new file mode 100644 index 0000000..879de26 --- /dev/null +++ b/lib/presentation/widgets/attachment/attachment_list.dart @@ -0,0 +1,18 @@ +import "package:flutter/material.dart"; + +import "package:flash_mail/presentation/widgets/attachment/attachment_button.dart"; + +class AttachmentList extends StatelessWidget { + final List> attachments; + + const AttachmentList({super.key, required this.attachments}); + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + itemCount: attachments.length, + itemBuilder: (ctx, i) => AttachmentButton(file: attachments[i]), + ); + } +} diff --git a/lib/presentation/widgets/auth/account_buttons.dart b/lib/presentation/widgets/auth/account_buttons.dart new file mode 100644 index 0000000..5cc99ab --- /dev/null +++ b/lib/presentation/widgets/auth/account_buttons.dart @@ -0,0 +1,67 @@ +import "package:auto_route/auto_route.dart"; +import "package:dio/dio.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "package:flash_mail/data/providers/providers.dart"; +import "package:flash_mail/presentation/widgets/primary_button.dart"; +import "package:flash_mail/presentation/widgets/secondary_button.dart"; +import "package:flash_mail/presentation/widgets/small_loading_spinner.dart"; + +class AccountButtons extends StatelessWidget { + const AccountButtons({super.key}); + + void showErrorSnackBar(BuildContext ctx, String message) { + ScaffoldMessenger.of(ctx).showSnackBar( + SnackBar( + content: Text(message), + behavior: SnackBarBehavior.floating, + ), + ); + } + + void _handleSubmit(BuildContext context, WidgetRef ref) async { + try { + await ref.read(accountProvider.notifier).createAccount(); + if (context.mounted) { + AutoRouter.of(context).replaceNamed("/inbox"); + } + } on DioError catch (err) { + if (context.mounted) { + showErrorSnackBar(context, err.message); + } + } catch (err) { + if (context.mounted) { + showErrorSnackBar(context, "Something went wrong!"); + } + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Your Inbox, Your Privacy, Zero Ads.", + style: Theme.of(context).textTheme.labelSmall, + ), + const SizedBox(height: 15), + Consumer( + builder: (context, ref, child) { + return PrimaryButton( + child: ref.watch(accountProvider).isLoading + ? const SmallLoadingSpinner() + : const Text("Create New Account"), + onPressed: () => _handleSubmit(context, ref), + ); + }, + ), + const SizedBox(height: 5), + SecondaryButton( + text: "Use Existing", + onPressed: () => AutoRouter.of(context).pushNamed("/login"), + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/auth/input_field.dart b/lib/presentation/widgets/auth/input_field.dart new file mode 100644 index 0000000..e04863d --- /dev/null +++ b/lib/presentation/widgets/auth/input_field.dart @@ -0,0 +1,58 @@ +import "package:flutter/material.dart"; + +class InputField extends StatelessWidget { + final String label; + final String placeholder; + final TextInputAction inputAction; + final String? Function(String?) validator; + final void Function(String?) onSaved; + final TextInputType? inputType; + final bool? obscureText; + + const InputField({ + super.key, + required this.label, + required this.placeholder, + required this.inputAction, + required this.validator, + required this.onSaved, + this.inputType, + this.obscureText, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Theme.of(context).primaryColor, + ), + ), + const SizedBox(height: 10), + TextFormField( + keyboardType: inputType, + textInputAction: inputAction, + obscureText: obscureText ?? false, + validator: validator, + onSaved: onSaved, + style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 18), + decoration: InputDecoration( + filled: true, + fillColor: const Color.fromRGBO(22, 39, 56, 1), + hintText: placeholder, + hintStyle: const TextStyle( + color: Color.fromRGBO(127, 133, 141, 1), + ), + errorStyle: const TextStyle( + fontFamily: "Inter", + fontSize: 16, + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/auth/login_form.dart b/lib/presentation/widgets/auth/login_form.dart new file mode 100644 index 0000000..04b8b38 --- /dev/null +++ b/lib/presentation/widgets/auth/login_form.dart @@ -0,0 +1,107 @@ +import "package:auto_route/auto_route.dart"; +import "package:dio/dio.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "package:flash_mail/data/providers/providers.dart"; +import "package:flash_mail/presentation/widgets/auth/input_field.dart"; +import "package:flash_mail/presentation/widgets/primary_button.dart"; +import "package:flash_mail/presentation/widgets/small_loading_spinner.dart"; +import "package:flash_mail/shared/utils/regex_patterns.dart"; + +class LoginForm extends ConsumerWidget { + final GlobalKey _formKey = GlobalKey(); + + LoginForm({super.key}); + + String? _validateEmail(String? value) { + if (value!.isEmpty || !emailRegex.hasMatch(value)) { + return "Invalid email address"; + } + return null; + } + + String? _validatePassword(String? value) { + if (value!.isEmpty || value.length < 8) { + return "Password too short"; + } + return null; + } + + void showErrorSnackBar(BuildContext ctx, String message) { + ScaffoldMessenger.of(ctx).showSnackBar( + SnackBar( + content: Text(message), + behavior: SnackBarBehavior.floating, + ), + ); + } + + void _handleSubmit(BuildContext context, WidgetRef ref) async { + if (!_formKey.currentState!.validate()) { + return; + } + FocusScope.of(context).unfocus(); + _formKey.currentState!.save(); + + try { + String? email = ref.read(emailProvider.notifier).state; + String? password = ref.read(passwordProvider.notifier).state; + + await ref.read(accountProvider.notifier).login(email, password); + + if (context.mounted) { + AutoRouter.of(context).replaceNamed("/inbox"); + } + } on DioError catch (err) { + if (context.mounted) { + showErrorSnackBar(context, err.message); + } + } catch (_) { + if (context.mounted) { + showErrorSnackBar(context, "Something went wrong!"); + } + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final account = ref.watch(accountProvider); + + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InputField( + label: "Email Address", + placeholder: "Enter email", + inputType: TextInputType.emailAddress, + inputAction: TextInputAction.next, + validator: _validateEmail, + onSaved: (value) => ref.read(emailProvider.notifier).state = value, + ), + const SizedBox(height: 25), + InputField( + label: "Password", + placeholder: "Enter password", + obscureText: true, + inputAction: TextInputAction.done, + validator: _validatePassword, + onSaved: (value) => + ref.read(passwordProvider.notifier).state = value, + ), + const SizedBox(height: 30), + Center( + child: PrimaryButton( + onPressed: () => _handleSubmit(context, ref), + child: account.isLoading + ? const SmallLoadingSpinner() + : const Text("Login"), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/drawer/app_drawer.dart b/lib/presentation/widgets/drawer/app_drawer.dart new file mode 100644 index 0000000..2a37eb0 --- /dev/null +++ b/lib/presentation/widgets/drawer/app_drawer.dart @@ -0,0 +1,37 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:flutter/material.dart"; + +import "package:flash_mail/data/providers/providers.dart"; +import "package:flash_mail/presentation/widgets/drawer/user_profile.dart"; +import "package:flash_mail/presentation/widgets/drawer/drawer_list.dart"; +import "package:flash_mail/presentation/widgets/drawer/user_quota.dart"; + +class AppDrawer extends ConsumerWidget { + const AppDrawer({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final account = ref.read(accountProvider); + + return Drawer( + backgroundColor: const Color.fromRGBO(22, 39, 56, 1), + child: Padding( + padding: const EdgeInsets.only(top: 90), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + UserProfile( + email: account.user!.email, + password: account.user!.password, + ), + const SizedBox(height: 50), + const DrawerList(), + UserQuota( + value: account.user!.used / account.user!.quota, + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/drawer/drawer_list.dart b/lib/presentation/widgets/drawer/drawer_list.dart new file mode 100644 index 0000000..6bcc948 --- /dev/null +++ b/lib/presentation/widgets/drawer/drawer_list.dart @@ -0,0 +1,44 @@ +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:auto_route/auto_route.dart"; + +import "package:flash_mail/data/providers/providers.dart"; +import "package:flash_mail/shared/configs/flash_mail_icons.dart"; +import "package:flash_mail/presentation/widgets/drawer/drawer_list_tile.dart"; + +class DrawerList extends ConsumerWidget { + const DrawerList({super.key}); + + Future signOut(BuildContext context, WidgetRef ref) async { + Navigator.of(context).pop(); + await ref.read(accountProvider.notifier).logout(); + if (context.mounted) { + AutoRouter.of(context).replaceNamed("/"); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.only(left: 25), + child: Column( + children: [ + DrawerListTile( + color: Theme.of(context).primaryColor, + icon: FlashMailIcons.inbox, + title: "Inbox", + onTap: () => Navigator.of(context).pop(), + ), + Consumer( + builder: (context, ref, _) => DrawerListTile( + color: Theme.of(context).colorScheme.error, + icon: FlashMailIcons.sign_out, + title: "Sign out", + onTap: () => signOut(context, ref), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/drawer/drawer_list_tile.dart b/lib/presentation/widgets/drawer/drawer_list_tile.dart new file mode 100644 index 0000000..b3757f8 --- /dev/null +++ b/lib/presentation/widgets/drawer/drawer_list_tile.dart @@ -0,0 +1,39 @@ +import "package:flutter/material.dart"; + +class DrawerListTile extends StatelessWidget { + final Color color; + final IconData icon; + final String title; + final void Function() onTap; + + const DrawerListTile({ + super.key, + required this.color, + required this.icon, + required this.title, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Theme( + data: Theme.of(context).copyWith( + splashColor: const Color.fromRGBO(40, 70, 100, 1), + highlightColor: const Color.fromRGBO(22, 39, 56, 1), + ), + child: ListTile( + onTap: onTap, + leading: Icon(icon, color: color, size: 18), + title: Text( + title, + style: TextStyle( + fontFamily: "Inter", + color: color, + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/drawer/toast_message.dart b/lib/presentation/widgets/drawer/toast_message.dart new file mode 100644 index 0000000..33f8710 --- /dev/null +++ b/lib/presentation/widgets/drawer/toast_message.dart @@ -0,0 +1,46 @@ +import "package:flutter/material.dart"; +import "package:flash/flash.dart"; + +class ToastMessage extends StatelessWidget { + final FlashController controller; + final IconData icon; + final String text; + + const ToastMessage({ + super.key, + required this.controller, + required this.icon, + required this.text, + }); + + @override + Widget build(BuildContext context) { + return Flash( + position: FlashPosition.bottom, + controller: controller, + child: AlertDialog( + alignment: Alignment.bottomCenter, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + backgroundColor: const Color.fromRGBO(217, 217, 217, 1), + content: Row( + children: [ + Icon( + icon, + color: Colors.black, + ), + const SizedBox(width: 20), + Text( + text, + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/drawer/user_profile.dart b/lib/presentation/widgets/drawer/user_profile.dart new file mode 100644 index 0000000..e211d51 --- /dev/null +++ b/lib/presentation/widgets/drawer/user_profile.dart @@ -0,0 +1,75 @@ +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flash/flash.dart"; + +import "package:flash_mail/presentation/widgets/drawer/toast_message.dart"; + +class UserProfile extends StatelessWidget { + final String email; + final String password; + + const UserProfile({ + super.key, + required this.email, + required this.password, + }); + + Future copyClipboard(BuildContext context, String text) async { + await Clipboard.setData(ClipboardData(text: text)); + if (context.mounted) { + showFlash( + context: context, + duration: const Duration(seconds: 2), + builder: (context, controller) => ToastMessage( + controller: controller, + icon: Icons.check, + text: "Copied to Clipboard!", + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + CircleAvatar( + backgroundColor: Theme.of(context).primaryColor, + radius: 35, + child: Text( + email[0].toUpperCase(), + style: + Theme.of(context).textTheme.bodyLarge!.copyWith(fontSize: 30), + ), + ), + const SizedBox(height: 20), + InkWell( + onTap: () => copyClipboard(context, email), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + email, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + ), + ), + const SizedBox(height: 5), + InkWell( + onTap: () => copyClipboard(context, password), + child: Text( + "password: $password", + style: const TextStyle( + color: Color.fromRGBO(127, 133, 141, 1), + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/drawer/user_quota.dart b/lib/presentation/widgets/drawer/user_quota.dart new file mode 100644 index 0000000..4522930 --- /dev/null +++ b/lib/presentation/widgets/drawer/user_quota.dart @@ -0,0 +1,71 @@ +import "package:flutter/material.dart"; +import "package:url_launcher/url_launcher.dart"; + +import "package:flash_mail/shared/configs/flash_mail_icons.dart"; + +class UserQuota extends StatelessWidget { + final String _url = "https://mail.tm/"; + + final double value; + + const UserQuota({super.key, required this.value}); + + Future openUrl(Uri url) async { + if (await canLaunchUrl(url)) { + launchUrl(url); + } + } + + @override + Widget build(BuildContext context) { + return Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 30, left: 40, right: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Quota", + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 18, + ), + ), + const SizedBox(height: 15), + LinearProgressIndicator( + backgroundColor: const Color.fromRGBO(85, 97, 112, 1), + color: Theme.of(context).primaryColor, + value: value, + minHeight: 10, + borderRadius: BorderRadius.circular(5), + ), + const SizedBox(height: 25), + InkWell( + onTap: () => openUrl(Uri.parse(_url)), + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: Row( + children: [ + Text( + "Powered By Mail.tm", + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 18, + ), + ), + const SizedBox(width: 10), + Icon( + FlashMailIcons.redirect, + color: Theme.of(context).primaryColor, + size: 16, + ) + ], + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/image_banner.dart b/lib/presentation/widgets/image_banner.dart new file mode 100644 index 0000000..110b274 --- /dev/null +++ b/lib/presentation/widgets/image_banner.dart @@ -0,0 +1,27 @@ +import "package:flutter/material.dart"; + +class ImageBanner extends StatelessWidget { + final String imgSrc; + final String text; + + const ImageBanner({ + Key? key, + required this.imgSrc, + required this.text, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(imgSrc, width: 250, height: 250), + const SizedBox(height: 10), + Text(text, style: Theme.of(context).textTheme.bodyLarge) + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/message/message_body.dart b/lib/presentation/widgets/message/message_body.dart new file mode 100644 index 0000000..d12459e --- /dev/null +++ b/lib/presentation/widgets/message/message_body.dart @@ -0,0 +1,53 @@ +import "package:flutter/widgets.dart"; +import "package:flutter_inappwebview/flutter_inappwebview.dart"; +import "package:url_launcher/url_launcher.dart"; + +class MessageBody extends StatefulWidget { + final String html; + + const MessageBody({super.key, required this.html}); + + @override + State createState() => _MessageBodyState(); +} + +class _MessageBodyState extends State { + double webviewHeight = 1; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: webviewHeight, + child: InAppWebView( + initialData: InAppWebViewInitialData(data: widget.html), + initialSettings: InAppWebViewSettings( + transparentBackground: true, + forceDark: ForceDark.ON, + algorithmicDarkeningAllowed: true, + supportZoom: false, + javaScriptEnabled: true, + disableHorizontalScroll: false, + disableVerticalScroll: true, + useWideViewPort: false, + useHybridComposition: false, + preferredContentMode: UserPreferredContentMode.MOBILE, + ), + onLoadStop: (controller, url) async { + final contentHeight = await controller.getContentHeight(); + if (contentHeight != null) { + setState(() { + webviewHeight = contentHeight.toDouble(); + }); + } + }, + shouldOverrideUrlLoading: (controller, navigationAction) async { + final url = navigationAction.request.url; + if (url != null && await canLaunchUrl(url)) { + await launchUrl(url); + } + return NavigationActionPolicy.CANCEL; + }, + ), + ); + } +} diff --git a/lib/presentation/widgets/message/message_info.dart b/lib/presentation/widgets/message/message_info.dart new file mode 100644 index 0000000..318714c --- /dev/null +++ b/lib/presentation/widgets/message/message_info.dart @@ -0,0 +1,82 @@ +import "package:flutter/material.dart"; +import "package:intl/intl.dart"; + +class MessageInfo extends StatelessWidget { + final String senderAddress; + final String receiverAddress; + final DateTime date; + + const MessageInfo({ + super.key, + required this.senderAddress, + required this.receiverAddress, + required this.date, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 15), + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + border: Border.all( + color: const Color.fromRGBO(127, 133, 140, 1), + ), + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + "From", + style: Theme.of(context).textTheme.labelSmall, + ), + const SizedBox(width: 10), + SizedBox( + width: MediaQuery.of(context).size.width * 0.65, + child: Text( + senderAddress, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + ) + ], + ), + Row( + children: [ + Text( + "To", + style: Theme.of(context).textTheme.labelSmall, + ), + const SizedBox(width: 10), + SizedBox( + width: MediaQuery.of(context).size.width * 0.65, + child: Text( + receiverAddress, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + ) + ], + ), + Row( + children: [ + Text( + "Date", + style: Theme.of(context).textTheme.labelSmall, + ), + const SizedBox(width: 10), + Text( + DateFormat.yMMMMd().add_jm().format(date.toLocal()), + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ) + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/message/message_list.dart b/lib/presentation/widgets/message/message_list.dart new file mode 100644 index 0000000..38dcef3 --- /dev/null +++ b/lib/presentation/widgets/message/message_list.dart @@ -0,0 +1,32 @@ +import "package:flutter/material.dart"; + +import "package:flash_mail/data/models/message/message.dart"; +import "package:flash_mail/presentation/widgets/image_banner.dart"; +import "package:flash_mail/presentation/widgets/message/message_tile.dart"; + +class MessageList extends StatelessWidget { + final List messages; + final Future Function() onRefresh; + + const MessageList({ + super.key, + required this.messages, + required this.onRefresh, + }); + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: onRefresh, + child: messages.isEmpty + ? const ImageBanner( + imgSrc: "assets/images/empty.png", + text: "Your inbox is empty!", + ) + : ListView.builder( + itemBuilder: (ctx, i) => MessageTile(message: messages[i]), + itemCount: messages.length, + ), + ); + } +} diff --git a/lib/presentation/widgets/message/message_sender.dart b/lib/presentation/widgets/message/message_sender.dart new file mode 100644 index 0000000..a6a57a7 --- /dev/null +++ b/lib/presentation/widgets/message/message_sender.dart @@ -0,0 +1,84 @@ +import "package:flutter/material.dart"; + +import "package:flash_mail/presentation/widgets/message/message_info.dart"; + +class MessageSender extends StatefulWidget { + final String senderName; + final String senderAddress; + final String receiverAddress; + final DateTime date; + + const MessageSender({ + super.key, + required this.senderName, + required this.senderAddress, + required this.receiverAddress, + required this.date, + }); + + @override + State createState() => _MessageSenderState(); +} + +class _MessageSenderState extends State { + bool showInfo = false; + + void toggleInfo() { + setState(() { + showInfo = !showInfo; + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + Image.asset("assets/images/avatar.png", width: 50, height: 50), + const SizedBox(width: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.senderName, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + GestureDetector( + onTap: () => toggleInfo(), + child: Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.6, + child: Text( + widget.senderAddress, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelSmall, + ), + ), + Icon( + showInfo + ? Icons.keyboard_arrow_up_outlined + : Icons.keyboard_arrow_down_outlined, + color: const Color.fromRGBO(127, 133, 140, 1), + ), + ], + ), + ), + ], + ) + ], + ), + showInfo + ? MessageInfo( + senderAddress: widget.senderAddress, + receiverAddress: widget.receiverAddress, + date: widget.date, + ) + : const SizedBox.shrink(), + ], + ); + } +} diff --git a/lib/presentation/widgets/message/message_tile.dart b/lib/presentation/widgets/message/message_tile.dart new file mode 100644 index 0000000..f3e3e77 --- /dev/null +++ b/lib/presentation/widgets/message/message_tile.dart @@ -0,0 +1,77 @@ +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; + +import "package:flash_mail/data/models/message/message.dart"; +import "package:flash_mail/presentation/routes/app_router.gr.dart"; +import "package:flash_mail/shared/utils/random_color.dart"; + +class MessageTile extends StatefulWidget { + final TMMessage message; + + const MessageTile({super.key, required this.message}); + + @override + State createState() => _MessageTileState(); +} + +class _MessageTileState extends State { + late Color randomColor; + + @override + void initState() { + super.initState(); + randomColor = getRandomColor(); + } + + @override + Widget build(BuildContext context) { + return ListTile( + splashColor: const Color.fromRGBO(40, 70, 100, 1), + focusColor: const Color.fromRGBO(22, 39, 56, 1), + onTap: () => AutoRouter.of(context).push( + MessageDetailRoute( + id: widget.message.id, + subject: widget.message.subject, + ), + ), + leading: CircleAvatar( + radius: 25, + backgroundColor: randomColor, + child: Text( + widget.message.from["name"]!.toUpperCase()[0], + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ), + title: Text( + widget.message.from["name"] as String, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontWeight: + widget.message.seen ? FontWeight.w500 : FontWeight.bold, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.message.subject, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: + widget.message.seen ? FontWeight.w300 : FontWeight.w500, + ), + ), + Text( + widget.message.intro ?? widget.message.subject, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: const Color.fromRGBO(193, 203, 213, 1), + ), + ) + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/message/message_view.dart b/lib/presentation/widgets/message/message_view.dart new file mode 100644 index 0000000..8051135 --- /dev/null +++ b/lib/presentation/widgets/message/message_view.dart @@ -0,0 +1,39 @@ +import "package:flutter/widgets.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "package:flash_mail/data/models/message_detail/message_detail.dart"; +import "package:flash_mail/data/providers/providers.dart"; +import "package:flash_mail/presentation/widgets/attachment/attachment_list.dart"; +import "package:flash_mail/presentation/widgets/message/message_body.dart"; +import "package:flash_mail/presentation/widgets/message/message_sender.dart"; + +class MessageView extends StatelessWidget { + final TMMessageDetail message; + + const MessageView({super.key, required this.message}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Consumer( + builder: (context, ref, child) => MessageSender( + senderName: message.from["name"] as String, + senderAddress: message.from["address"] as String, + receiverAddress: ref.read(emailProvider) as String, + date: message.createdAt, + ), + ), + const SizedBox(height: 25), + MessageBody( + html: message.html[0], + ), + message.hasAttachments + ? AttachmentList( + attachments: message.attachments!, + ) + : const SizedBox.shrink() + ], + ); + } +} diff --git a/lib/presentation/widgets/primary_button.dart b/lib/presentation/widgets/primary_button.dart new file mode 100644 index 0000000..02b985e --- /dev/null +++ b/lib/presentation/widgets/primary_button.dart @@ -0,0 +1,24 @@ +import "package:flutter/material.dart"; + +class PrimaryButton extends StatelessWidget { + final Widget child; + final void Function() onPressed; + + const PrimaryButton({ + super.key, + required this.child, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + height: 40, + child: ElevatedButton( + onPressed: onPressed, + child: child, + ), + ); + } +} diff --git a/lib/presentation/widgets/secondary_button.dart b/lib/presentation/widgets/secondary_button.dart new file mode 100644 index 0000000..a736805 --- /dev/null +++ b/lib/presentation/widgets/secondary_button.dart @@ -0,0 +1,23 @@ +import "package:flutter/material.dart"; + +class SecondaryButton extends StatelessWidget { + final String text; + final void Function() onPressed; + + const SecondaryButton({ + super.key, + required this.text, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + child: TextButton( + onPressed: onPressed, + child: Text(text), + ), + ); + } +} diff --git a/lib/presentation/widgets/shimmer/list_shimmer.dart b/lib/presentation/widgets/shimmer/list_shimmer.dart new file mode 100644 index 0000000..1e9b516 --- /dev/null +++ b/lib/presentation/widgets/shimmer/list_shimmer.dart @@ -0,0 +1,45 @@ +import "package:flutter/material.dart"; + +class ListShimmer extends StatelessWidget { + const ListShimmer({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + color: const Color.fromRGBO(160, 176, 186, 1), + ), + ), + const SizedBox(width: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + color: const Color.fromRGBO(160, 176, 186, 1), + width: MediaQuery.of(context).size.width * 0.65, + height: 18, + ), + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + color: const Color.fromRGBO(160, 176, 186, 1), + width: MediaQuery.of(context).size.width * 0.65, + height: 18, + ), + ), + ], + ) + ], + ); + } +} diff --git a/lib/presentation/widgets/shimmer/message_shimmer.dart b/lib/presentation/widgets/shimmer/message_shimmer.dart new file mode 100644 index 0000000..640d40a --- /dev/null +++ b/lib/presentation/widgets/shimmer/message_shimmer.dart @@ -0,0 +1,28 @@ +import "package:flutter/material.dart"; +import "package:shimmer/shimmer.dart"; + +import "package:flash_mail/presentation/widgets/shimmer/list_shimmer.dart"; + +class MessageShimmer extends StatelessWidget { + const MessageShimmer({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(15), + child: Shimmer.fromColors( + baseColor: Colors.grey[500]!, + highlightColor: Colors.grey[300]!, + child: ListView.builder( + itemBuilder: (ctx, i) => const Column( + children: [ + ListShimmer(), + SizedBox(height: 20), + ], + ), + itemCount: 8, + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/small_loading_spinner.dart b/lib/presentation/widgets/small_loading_spinner.dart new file mode 100644 index 0000000..980d25b --- /dev/null +++ b/lib/presentation/widgets/small_loading_spinner.dart @@ -0,0 +1,19 @@ +import "package:flutter/material.dart"; + +class SmallLoadingSpinner extends StatelessWidget { + const SmallLoadingSpinner({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const SizedBox( + width: 20, + height: 20, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ), + ); + } +} diff --git a/lib/shared/configs/flash_mail_icons.dart b/lib/shared/configs/flash_mail_icons.dart new file mode 100644 index 0000000..6f8c51b --- /dev/null +++ b/lib/shared/configs/flash_mail_icons.dart @@ -0,0 +1,34 @@ +// ignore_for_file: constant_identifier_names +/// +/// Flutter icons FlashMailIcons +/// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com +/// This font was generated by FlutterIcon.com, which is derived from Fontello. +/// +/// To use this font, place it in your fonts/ directory and include the +/// following in your pubspec.yaml +/// +/// flutter: +/// fonts: +/// - family: FlashMailIcons +/// fonts: +/// - asset: fonts/FlashMailIcons.ttf + +import "package:flutter/widgets.dart"; + +class FlashMailIcons { + FlashMailIcons._(); + + static const _kFontFam = "FlashMailIcons"; + static const String? _kFontPkg = null; + + static const IconData sign_out = + IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData inbox = + IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData redirect = + IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData bin = + IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData arrow_left = + IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); +} diff --git a/lib/shared/utils/random_color.dart b/lib/shared/utils/random_color.dart new file mode 100644 index 0000000..ead4c5c --- /dev/null +++ b/lib/shared/utils/random_color.dart @@ -0,0 +1,12 @@ +import "dart:math"; +import "dart:ui"; + +Color getRandomColor() { + Random random = Random(); + return Color.fromRGBO( + random.nextInt(256), + random.nextInt(256), + random.nextInt(256), + 1, + ); +} diff --git a/lib/shared/utils/random_string.dart b/lib/shared/utils/random_string.dart new file mode 100644 index 0000000..3f12989 --- /dev/null +++ b/lib/shared/utils/random_string.dart @@ -0,0 +1,12 @@ +import "dart:math"; + +String randomString(int length) { + const charset = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + final random = Random.secure(); + final codeUnits = List.generate( + length, + (index) => charset.codeUnitAt(random.nextInt(charset.length)), + ); + return String.fromCharCodes(codeUnits); +} diff --git a/lib/shared/utils/regex_patterns.dart b/lib/shared/utils/regex_patterns.dart new file mode 100644 index 0000000..c3de514 --- /dev/null +++ b/lib/shared/utils/regex_patterns.dart @@ -0,0 +1,3 @@ +final RegExp emailRegex = RegExp( + r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', +); diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..4797e65 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,954 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + archive: + dependency: transitive + description: + name: archive + sha256: "7e0d52067d05f2e0324268097ba723b71cb41ac8a6a2b24d1edf9c536b987b03" + url: "https://pub.dev" + source: hosted + version: "3.4.6" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + auto_route: + dependency: "direct main" + description: + name: auto_route + sha256: "82f8df1d177416bc6b7a449127d0270ff1f0f633a91f2ceb7a85d4f07c3affa1" + url: "https://pub.dev" + source: hosted + version: "7.8.4" + auto_route_generator: + dependency: "direct dev" + description: + name: auto_route_generator + sha256: "11067a3bcd643812518fe26c0c9ec073990286cabfd9d74b6da9ef9b913c4d22" + url: "https://pub.dev" + source: hosted + version: "7.3.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + url: "https://pub.dev" + source: hosted + version: "2.4.6" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + url: "https://pub.dev" + source: hosted + version: "7.2.11" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74 + url: "https://pub.dev" + source: hosted + version: "8.6.3" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + collection: + dependency: transitive + description: + name: collection + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + url: "https://pub.dev" + source: hosted + version: "1.17.2" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f" + url: "https://pub.dev" + source: hosted + version: "0.17.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334 + url: "https://pub.dev" + source: hosted + version: "2.3.3" + dio: + dependency: "direct main" + description: + name: dio + sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + url: "https://pub.dev" + source: hosted + version: "4.0.6" + external_path: + dependency: "direct main" + description: + name: external_path + sha256: "2095c626fbbefe70d5a4afc9b1137172a68ee2c276e51c3c1283394485bea8f4" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flash: + dependency: "direct main" + description: + name: flash + sha256: "1f1632d83f42fe0b9038f72694969a6adf52fa2a3df46d05cfc392dde3c1505f" + url: "https://pub.dev" + source: hosted + version: "3.0.5+2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_downloader: + dependency: "direct main" + description: + name: flutter_downloader + sha256: e130001cf85d8d7450b8318a4670c19e495dd8c102ad4b15a512e9405e7c451d + url: "https://pub.dev" + source: hosted + version: "1.11.6" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: fd4db51e46f49b140d83a3206851432c54ea920b381137c0ba82d0cf59be1dee + url: "https://pub.dev" + source: hosted + version: "1.0.12" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + sha256: "5bf4c3e5e5a0426c1e2fc8ca3555a9e617e76369c3442e1dae8385c7767ba97a" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: e667e406a74d67715f1fa0bd941d9ded49aff72f3a9f4440a36aece4e8d457a7 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "21bf2825311de65501d22e563e3d7605dff57fb5e6da982db785ae5372ff018a" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + url: "https://pub.dev" + source: hosted + version: "4.1.3" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + url: "https://pub.dev" + source: hosted + version: "6.7.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + url: "https://pub.dev" + source: hosted + version: "2.1.6" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "494bf2cfb4df30000273d3052bdb1cc1de738574c6b678f0beb146ea56f5e208" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba + url: "https://pub.dev" + source: hosted + version: "6.2.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.dev" + source: hosted + version: "5.0.9" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + xml: + dependency: transitive + description: + name: xml + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..4271a88 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,82 @@ +name: flash_mail +description: Temporary E-mail App, powered by mail.tm + +publish_to: "none" + +version: 1.0.0+1 + +environment: + sdk: ">=3.0.1 <4.0.0" + +dependencies: + flutter: + sdk: flutter + hive: ^2.2.3 + hive_flutter: ^1.1.0 + auto_route: ^7.4.0 + flutter_riverpod: ^2.4.0 + url_launcher: ^6.2.1 + flash: ^3.0.5+2 + freezed_annotation: ^2.4.1 + json_annotation: ^4.8.1 + dio: ^4.0.6 + shimmer: ^3.0.0 + flutter_inappwebview: ^6.0.0 + flutter_downloader: ^1.11.6 + external_path: ^1.0.3 + intl: ^0.19.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.6 + flutter_lints: ^2.0.0 + auto_route_generator: ^7.3.0 + flutter_native_splash: ^2.3.4 + flutter_launcher_icons: ^0.13.1 + freezed: ^2.4.5 + json_serializable: ^6.7.1 + hive_generator: ^2.0.1 + +flutter_launcher_icons: + android: "launcher_icon" + ios: false + min_sdk_android: 20 + image_path: assets/images/icon.png + adaptive_icon_foreground: assets/images/icon.png + adaptive_icon_background: "#01111d" + +flutter_native_splash: + web: false + ios: false + color: "#01111d" + image: assets/images/splash.png + android12: + color: "#01111d" + branding: assets/images/splash.png + +flutter: + uses-material-design: true + assets: + - assets/images/logo-small.png + - assets/images/empty.png + - assets/images/error.png + - assets/images/avatar.png + fonts: + - family: FlashMailIcons + fonts: + - asset: assets/fonts/FlashMailIcons.ttf + - family: Inter + fonts: + - asset: assets/fonts/Inter-Light.ttf + weight: 300 + - asset: assets/fonts/Inter-Regular.ttf + weight: 400 + - asset: assets/fonts/Inter-Medium.ttf + weight: 500 + - asset: assets/fonts/Inter-Bold.ttf + weight: 700 + - asset: assets/fonts/Inter-ExtraBold.ttf + weight: 800 + - asset: assets/fonts/Inter-Black.ttf + weight: 900 diff --git a/screenshots/1.png b/screenshots/1.png new file mode 100644 index 0000000..828554b Binary files /dev/null and b/screenshots/1.png differ diff --git a/screenshots/2.png b/screenshots/2.png new file mode 100644 index 0000000..fb4957c Binary files /dev/null and b/screenshots/2.png differ diff --git a/screenshots/3.png b/screenshots/3.png new file mode 100644 index 0000000..c48e680 Binary files /dev/null and b/screenshots/3.png differ diff --git a/screenshots/4.png b/screenshots/4.png new file mode 100644 index 0000000..21412d0 Binary files /dev/null and b/screenshots/4.png differ diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..a350d30 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,19 @@ +import "package:flutter/material.dart"; +import "package:flutter_test/flutter_test.dart"; + +import "package:flash_mail/presentation/app_widget.dart"; + +void main() { + testWidgets("Counter increments smoke test", (WidgetTester tester) async { + await tester.pumpWidget(const AppWidget()); + + expect(find.text("0"), findsOneWidget); + expect(find.text("1"), findsNothing); + + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + expect(find.text("0"), findsNothing); + expect(find.text("1"), findsOneWidget); + }); +}