v1 release
44
.gitignore
vendored
Normal file
|
|
@ -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
|
||||
45
.metadata
Normal file
|
|
@ -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'
|
||||
21
LICENSE.md
Normal file
|
|
@ -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.
|
||||
80
README.md
Normal file
|
|
@ -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
|
||||
|
||||
<img src="screenshots/1.png" height="300em" /> <img src="screenshots/2.png" height="300em" /> <img src="screenshots/3.png" height="300em" />
|
||||
<img src="screenshots/4.png" height="300em" />
|
||||
|
||||
## 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:<br>
|
||||
```bash
|
||||
git clone https://github.com/sarthakkimtani/flash-mail.git
|
||||
```
|
||||
3. Navigate to the project directory:<br><br>
|
||||
```bash
|
||||
cd flash-mail
|
||||
```
|
||||
4. Fetch the app's dependencies by running the following command:<br>
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
5. Connect your device or start an emulator.
|
||||
6. Run the app using the following command:<br>
|
||||
```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.
|
||||
10
analysis_options.yaml
Normal file
|
|
@ -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
|
||||
13
android/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
88
android/app/build.gradle
Normal file
|
|
@ -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"
|
||||
}
|
||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
56
android/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="FlashMail"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<provider
|
||||
android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
|
||||
android:authorities="${applicationId}.flutter_downloader.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths"/>
|
||||
</provider>
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.DIAL" />
|
||||
<data android:scheme="tel" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.example.flash_mail
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/drawable-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/drawable-v21/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 12 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
android/app/src/main/res/drawable/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
9
android/app/src/main/res/drawable/launch_background.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-hdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-mdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 1,007 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
20
android/app/src/main/res/values-night-v31/styles.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowSplashScreenBackground">@color/ic_launcher_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
22
android/app/src/main/res/values-night/styles.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
20
android/app/src/main/res/values-v31/styles.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowSplashScreenBackground">@color/ic_launcher_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
4
android/app/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#01111d</color>
|
||||
</resources>
|
||||
9
android/app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="flutter_downloader_notification_started">Download started</string>
|
||||
<string name="flutter_downloader_notification_in_progress">Download in progress</string>
|
||||
<string name="flutter_downloader_notification_canceled">Download canceled</string>
|
||||
<string name="flutter_downloader_notification_failed">Download failed</string>
|
||||
<string name="flutter_downloader_notification_complete">Download complete</string>
|
||||
<string name="flutter_downloader_notification_paused">Download paused</string>
|
||||
</resources>
|
||||
22
android/app/src/main/res/values/styles.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
31
android/build.gradle
Normal file
|
|
@ -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
|
||||
}
|
||||
3
android/gradle.properties
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
11
android/settings.gradle
Normal file
|
|
@ -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"
|
||||
BIN
assets/fonts/FlashMailIcons.ttf
Normal file
BIN
assets/fonts/Inter-Black.ttf
Normal file
BIN
assets/fonts/Inter-Bold.ttf
Normal file
BIN
assets/fonts/Inter-ExtraBold.ttf
Normal file
BIN
assets/fonts/Inter-Light.ttf
Normal file
BIN
assets/fonts/Inter-Medium.ttf
Normal file
BIN
assets/fonts/Inter-Regular.ttf
Normal file
BIN
assets/images/avatar.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/images/empty.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
assets/images/error.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/images/icon.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/images/logo-small.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/images/splash.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
123
lib/data/api/api_client.dart
Normal file
|
|
@ -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<void> generateToken(String address, String password) async {
|
||||
final tokenResponse = await dio.post(
|
||||
"/token",
|
||||
data: jsonEncode({"address": address, "password": password}),
|
||||
);
|
||||
final Map<String, dynamic> tokenData =
|
||||
tokenResponse.data as Map<String, dynamic>;
|
||||
|
||||
ref.read(tokenProvider.notifier).state = tokenData["token"];
|
||||
}
|
||||
|
||||
Future<TMAccount> createAccount() async {
|
||||
final response = await dio.get("/domains");
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
final List<Map<String, dynamic>> domains = List.from(data["hydra:member"]);
|
||||
|
||||
final username = randomString(7);
|
||||
final domain = domains[0]["domain"];
|
||||
Map<String, String> payload = {
|
||||
"address": "$username@$domain",
|
||||
"password": randomString(10),
|
||||
};
|
||||
|
||||
final userResponse = await dio.post(
|
||||
"/accounts",
|
||||
data: jsonEncode(payload),
|
||||
);
|
||||
final Map<String, dynamic> info = userResponse.data as Map<String, dynamic>;
|
||||
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<TMAccount> login(String address, String password) async {
|
||||
final response = await dio.post(
|
||||
"/token",
|
||||
data: jsonEncode({"address": address, "password": password}),
|
||||
);
|
||||
final Map<String, dynamic> tokenData =
|
||||
response.data as Map<String, dynamic>;
|
||||
|
||||
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<String, dynamic> info = userResponse.data as Map<String, dynamic>;
|
||||
|
||||
return TMAccount(
|
||||
id: info["id"],
|
||||
email: address,
|
||||
password: password,
|
||||
quota: info["quota"],
|
||||
used: info["used"],
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<TMMessage>> 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<String, dynamic> messageData = response.data as Map<String, dynamic>;
|
||||
final messages =
|
||||
List<Map<String, dynamic>>.from(messageData["hydra:member"]);
|
||||
|
||||
return messages.map((message) => TMMessage.fromJson(message)).toList();
|
||||
}
|
||||
|
||||
Future<TMMessageDetail> 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<String, dynamic> messageData = response.data as Map<String, dynamic>;
|
||||
|
||||
return TMMessageDetail.fromJson(messageData);
|
||||
}
|
||||
|
||||
Future<void> 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);
|
||||
}
|
||||
}
|
||||
20
lib/data/models/account/account.dart
Normal file
|
|
@ -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<String, dynamic> json) =>
|
||||
_$TMAccountFromJson(json);
|
||||
}
|
||||
252
lib/data/models/account/account.freezed.dart
Normal file
|
|
@ -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>(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<String, dynamic> 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<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$TMAccountCopyWith<TMAccount> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
|
||||
}
|
||||
75
lib/data/models/account/account.g.dart
Normal file
|
|
@ -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 = <int, dynamic>{
|
||||
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<String, dynamic> 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<String, dynamic> _$$TMAccountImplToJson(_$TMAccountImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'email': instance.email,
|
||||
'password': instance.password,
|
||||
'quota': instance.quota,
|
||||
'used': instance.used,
|
||||
};
|
||||
23
lib/data/models/message/message.dart
Normal file
|
|
@ -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<String, String> from,
|
||||
required List<Map<String, String>> 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<String, dynamic> json) =>
|
||||
_$TMMessageFromJson(json);
|
||||
}
|
||||
363
lib/data/models/message/message.freezed.dart
Normal file
|
|
@ -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>(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<String, dynamic> json) {
|
||||
return _TMMessage.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TMMessage {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
Map<String, String> get from => throw _privateConstructorUsedError;
|
||||
List<Map<String, String>> 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<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$TMMessageCopyWith<TMMessage> 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<String, String> from,
|
||||
List<Map<String, String>> 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<String, String>,
|
||||
to: null == to
|
||||
? _value.to
|
||||
: to // ignore: cast_nullable_to_non_nullable
|
||||
as List<Map<String, String>>,
|
||||
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<String, String> from,
|
||||
List<Map<String, String>> 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<String, String>,
|
||||
to: null == to
|
||||
? _value._to
|
||||
: to // ignore: cast_nullable_to_non_nullable
|
||||
as List<Map<String, String>>,
|
||||
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<String, String> from,
|
||||
required final List<Map<String, String>> 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<String, dynamic> json) =>
|
||||
_$$TMMessageImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
final Map<String, String> _from;
|
||||
@override
|
||||
Map<String, String> get from {
|
||||
if (_from is EqualUnmodifiableMapView) return _from;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_from);
|
||||
}
|
||||
|
||||
final List<Map<String, String>> _to;
|
||||
@override
|
||||
List<Map<String, String>> 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<String, dynamic> toJson() {
|
||||
return _$$TMMessageImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _TMMessage implements TMMessage {
|
||||
factory _TMMessage(
|
||||
{required final String id,
|
||||
required final Map<String, String> from,
|
||||
required final List<Map<String, String>> 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<String, dynamic> json) =
|
||||
_$TMMessageImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@override
|
||||
Map<String, String> get from;
|
||||
@override
|
||||
List<Map<String, String>> 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;
|
||||
}
|
||||
37
lib/data/models/message/message.g.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'message.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$TMMessageImpl _$$TMMessageImplFromJson(Map<String, dynamic> json) =>
|
||||
_$TMMessageImpl(
|
||||
id: json['id'] as String,
|
||||
from: Map<String, String>.from(json['from'] as Map),
|
||||
to: (json['to'] as List<dynamic>)
|
||||
.map((e) => Map<String, String>.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<String, dynamic> _$$TMMessageImplToJson(_$TMMessageImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'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(),
|
||||
};
|
||||
25
lib/data/models/message_detail/message_detail.dart
Normal file
|
|
@ -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<String, String> from,
|
||||
required List<Map<String, String>> to,
|
||||
required List<String> cc,
|
||||
required List<String> bcc,
|
||||
required String subject,
|
||||
required String? text,
|
||||
required List<String> html,
|
||||
required bool hasAttachments,
|
||||
required List<Map<String, dynamic>>? attachments,
|
||||
required String downloadUrl,
|
||||
required DateTime createdAt,
|
||||
}) = _TMMessageDetail;
|
||||
|
||||
factory TMMessageDetail.fromJson(Map<String, dynamic> json) =>
|
||||
_$TMMessageDetailFromJson(json);
|
||||
}
|
||||
438
lib/data/models/message_detail/message_detail.freezed.dart
Normal file
|
|
@ -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>(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<String, dynamic> json) {
|
||||
return _TMMessageDetail.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TMMessageDetail {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
Map<String, String> get from => throw _privateConstructorUsedError;
|
||||
List<Map<String, String>> get to => throw _privateConstructorUsedError;
|
||||
List<String> get cc => throw _privateConstructorUsedError;
|
||||
List<String> get bcc => throw _privateConstructorUsedError;
|
||||
String get subject => throw _privateConstructorUsedError;
|
||||
String? get text => throw _privateConstructorUsedError;
|
||||
List<String> get html => throw _privateConstructorUsedError;
|
||||
bool get hasAttachments => throw _privateConstructorUsedError;
|
||||
List<Map<String, dynamic>>? get attachments =>
|
||||
throw _privateConstructorUsedError;
|
||||
String get downloadUrl => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$TMMessageDetailCopyWith<TMMessageDetail> 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<String, String> from,
|
||||
List<Map<String, String>> to,
|
||||
List<String> cc,
|
||||
List<String> bcc,
|
||||
String subject,
|
||||
String? text,
|
||||
List<String> html,
|
||||
bool hasAttachments,
|
||||
List<Map<String, dynamic>>? 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<String, String>,
|
||||
to: null == to
|
||||
? _value.to
|
||||
: to // ignore: cast_nullable_to_non_nullable
|
||||
as List<Map<String, String>>,
|
||||
cc: null == cc
|
||||
? _value.cc
|
||||
: cc // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
bcc: null == bcc
|
||||
? _value.bcc
|
||||
: bcc // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
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<String>,
|
||||
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<Map<String, dynamic>>?,
|
||||
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<String, String> from,
|
||||
List<Map<String, String>> to,
|
||||
List<String> cc,
|
||||
List<String> bcc,
|
||||
String subject,
|
||||
String? text,
|
||||
List<String> html,
|
||||
bool hasAttachments,
|
||||
List<Map<String, dynamic>>? 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<String, String>,
|
||||
to: null == to
|
||||
? _value._to
|
||||
: to // ignore: cast_nullable_to_non_nullable
|
||||
as List<Map<String, String>>,
|
||||
cc: null == cc
|
||||
? _value._cc
|
||||
: cc // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
bcc: null == bcc
|
||||
? _value._bcc
|
||||
: bcc // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
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<String>,
|
||||
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<Map<String, dynamic>>?,
|
||||
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<String, String> from,
|
||||
required final List<Map<String, String>> to,
|
||||
required final List<String> cc,
|
||||
required final List<String> bcc,
|
||||
required this.subject,
|
||||
required this.text,
|
||||
required final List<String> html,
|
||||
required this.hasAttachments,
|
||||
required final List<Map<String, dynamic>>? attachments,
|
||||
required this.downloadUrl,
|
||||
required this.createdAt})
|
||||
: _from = from,
|
||||
_to = to,
|
||||
_cc = cc,
|
||||
_bcc = bcc,
|
||||
_html = html,
|
||||
_attachments = attachments;
|
||||
|
||||
factory _$TMMessageDetailImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$TMMessageDetailImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
final Map<String, String> _from;
|
||||
@override
|
||||
Map<String, String> get from {
|
||||
if (_from is EqualUnmodifiableMapView) return _from;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_from);
|
||||
}
|
||||
|
||||
final List<Map<String, String>> _to;
|
||||
@override
|
||||
List<Map<String, String>> get to {
|
||||
if (_to is EqualUnmodifiableListView) return _to;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_to);
|
||||
}
|
||||
|
||||
final List<String> _cc;
|
||||
@override
|
||||
List<String> get cc {
|
||||
if (_cc is EqualUnmodifiableListView) return _cc;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_cc);
|
||||
}
|
||||
|
||||
final List<String> _bcc;
|
||||
@override
|
||||
List<String> 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<String> _html;
|
||||
@override
|
||||
List<String> get html {
|
||||
if (_html is EqualUnmodifiableListView) return _html;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_html);
|
||||
}
|
||||
|
||||
@override
|
||||
final bool hasAttachments;
|
||||
final List<Map<String, dynamic>>? _attachments;
|
||||
@override
|
||||
List<Map<String, dynamic>>? 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<String, dynamic> toJson() {
|
||||
return _$$TMMessageDetailImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _TMMessageDetail implements TMMessageDetail {
|
||||
factory _TMMessageDetail(
|
||||
{required final String id,
|
||||
required final Map<String, String> from,
|
||||
required final List<Map<String, String>> to,
|
||||
required final List<String> cc,
|
||||
required final List<String> bcc,
|
||||
required final String subject,
|
||||
required final String? text,
|
||||
required final List<String> html,
|
||||
required final bool hasAttachments,
|
||||
required final List<Map<String, dynamic>>? attachments,
|
||||
required final String downloadUrl,
|
||||
required final DateTime createdAt}) = _$TMMessageDetailImpl;
|
||||
|
||||
factory _TMMessageDetail.fromJson(Map<String, dynamic> json) =
|
||||
_$TMMessageDetailImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@override
|
||||
Map<String, String> get from;
|
||||
@override
|
||||
List<Map<String, String>> get to;
|
||||
@override
|
||||
List<String> get cc;
|
||||
@override
|
||||
List<String> get bcc;
|
||||
@override
|
||||
String get subject;
|
||||
@override
|
||||
String? get text;
|
||||
@override
|
||||
List<String> get html;
|
||||
@override
|
||||
bool get hasAttachments;
|
||||
@override
|
||||
List<Map<String, dynamic>>? get attachments;
|
||||
@override
|
||||
String get downloadUrl;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$TMMessageDetailImplCopyWith<_$TMMessageDetailImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
45
lib/data/models/message_detail/message_detail.g.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'message_detail.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$TMMessageDetailImpl _$$TMMessageDetailImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$TMMessageDetailImpl(
|
||||
id: json['id'] as String,
|
||||
from: Map<String, String>.from(json['from'] as Map),
|
||||
to: (json['to'] as List<dynamic>)
|
||||
.map((e) => Map<String, String>.from(e as Map))
|
||||
.toList(),
|
||||
cc: (json['cc'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
bcc: (json['bcc'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
subject: json['subject'] as String,
|
||||
text: json['text'] as String?,
|
||||
html: (json['html'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
hasAttachments: json['hasAttachments'] as bool,
|
||||
attachments: (json['attachments'] as List<dynamic>?)
|
||||
?.map((e) => e as Map<String, dynamic>)
|
||||
.toList(),
|
||||
downloadUrl: json['downloadUrl'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$TMMessageDetailImplToJson(
|
||||
_$TMMessageDetailImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'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(),
|
||||
};
|
||||
86
lib/data/notifiers/account_notifier.dart
Normal file
|
|
@ -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<AccountState> {
|
||||
static const _boxName = "accounts";
|
||||
static const _key = "user";
|
||||
|
||||
final Ref ref;
|
||||
|
||||
AccountNotifier(this.ref) : super(const AccountState());
|
||||
|
||||
Future<void> fetchAccount() async {
|
||||
var box = await Hive.openBox<TMAccount>(_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<void> createAccount() async {
|
||||
state = const AccountState(isLoading: true);
|
||||
|
||||
try {
|
||||
TMAccount account = await ref.read(clientProvider).createAccount();
|
||||
var box = Hive.box<TMAccount>(_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<void> 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<TMAccount>(_boxName);
|
||||
await box.put(_key, account);
|
||||
|
||||
state = AccountState(isLoading: false, user: account);
|
||||
}
|
||||
} catch (err) {
|
||||
state = const AccountState(isLoading: false);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
AccountState previous = state;
|
||||
try {
|
||||
var box = Hive.box<TMAccount>(_boxName);
|
||||
await box.delete(_key);
|
||||
state = const AccountState();
|
||||
} catch (err) {
|
||||
state = previous;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
lib/data/providers/providers.dart
Normal file
|
|
@ -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<String?>((ref) => null);
|
||||
final passwordProvider = StateProvider<String?>((ref) => null);
|
||||
|
||||
final dioProvider = Provider<Dio>(
|
||||
(ref) => Dio(
|
||||
BaseOptions(
|
||||
baseUrl: "https://api.mail.tm",
|
||||
connectTimeout: 2000,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final clientProvider = Provider<ApiClient>(
|
||||
(ref) => ApiClient(ref.read(dioProvider), ref),
|
||||
);
|
||||
|
||||
final accountProvider = StateNotifierProvider<AccountNotifier, AccountState>(
|
||||
(ref) => AccountNotifier(ref),
|
||||
);
|
||||
|
||||
final tokenProvider = StateProvider<String?>((ref) => null);
|
||||
|
||||
final messagesProvider = FutureProvider.autoDispose<List<TMMessage>>((ref) {
|
||||
final email = ref.read(emailProvider);
|
||||
final password = ref.read(passwordProvider);
|
||||
|
||||
return ref.read(clientProvider).fetchMessages(email!, password!);
|
||||
});
|
||||
|
||||
final messageDetailProvider = FutureProvider.family<TMMessageDetail, String>(
|
||||
(ref, id) => ref.read(clientProvider).fetchMessageDetail(id),
|
||||
);
|
||||
24
lib/main.dart
Normal file
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
19
lib/presentation/app_widget.dart
Normal file
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/presentation/routes/account_guard.dart
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
lib/presentation/routes/app_router.dart
Normal file
|
|
@ -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<AutoRoute> 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",
|
||||
)
|
||||
];
|
||||
}
|
||||
139
lib/presentation/routes/app_router.gr.dart
Normal file
|
|
@ -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<String, _i5.PageFactory> pagesMap = {
|
||||
AccountRoute.name: (routeData) {
|
||||
return _i5.AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const _i1.AccountScreen(),
|
||||
);
|
||||
},
|
||||
InboxRoute.name: (routeData) {
|
||||
return _i5.AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const _i2.InboxScreen(),
|
||||
);
|
||||
},
|
||||
LoginRoute.name: (routeData) {
|
||||
return _i5.AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const _i3.LoginScreen(),
|
||||
);
|
||||
},
|
||||
MessageDetailRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<MessageDetailRouteArgs>();
|
||||
return _i5.AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: _i4.MessageDetailScreen(
|
||||
key: args.key,
|
||||
id: args.id,
|
||||
subject: args.subject,
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i1.AccountScreen]
|
||||
class AccountRoute extends _i5.PageRouteInfo<void> {
|
||||
const AccountRoute({List<_i5.PageRouteInfo>? children})
|
||||
: super(
|
||||
AccountRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AccountRoute';
|
||||
|
||||
static const _i5.PageInfo<void> page = _i5.PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i2.InboxScreen]
|
||||
class InboxRoute extends _i5.PageRouteInfo<void> {
|
||||
const InboxRoute({List<_i5.PageRouteInfo>? children})
|
||||
: super(
|
||||
InboxRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'InboxRoute';
|
||||
|
||||
static const _i5.PageInfo<void> page = _i5.PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i3.LoginScreen]
|
||||
class LoginRoute extends _i5.PageRouteInfo<void> {
|
||||
const LoginRoute({List<_i5.PageRouteInfo>? children})
|
||||
: super(
|
||||
LoginRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'LoginRoute';
|
||||
|
||||
static const _i5.PageInfo<void> page = _i5.PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i4.MessageDetailScreen]
|
||||
class MessageDetailRoute extends _i5.PageRouteInfo<MessageDetailRouteArgs> {
|
||||
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<MessageDetailRouteArgs> page =
|
||||
_i5.PageInfo<MessageDetailRouteArgs>(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}';
|
||||
}
|
||||
}
|
||||
18
lib/presentation/routes/auth_guard.dart
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
lib/presentation/screens/account_screen.dart
Normal file
|
|
@ -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: <Widget>[
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Image(
|
||||
image: AssetImage("assets/images/logo-small.png"),
|
||||
),
|
||||
Text(
|
||||
"FlashMail",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
)
|
||||
],
|
||||
),
|
||||
const AccountButtons()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
62
lib/presentation/screens/inbox_screen.dart
Normal file
|
|
@ -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<InboxScreen> createState() => _InboxScreenState();
|
||||
}
|
||||
|
||||
class _InboxScreenState extends ConsumerState<InboxScreen> {
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
31
lib/presentation/screens/login_screen.dart
Normal file
|
|
@ -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: <Widget>[
|
||||
Text(
|
||||
"Login",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
LoginForm(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
71
lib/presentation/screens/message_detail_screen.dart
Normal file
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
95
lib/presentation/theme/theme.dart
Normal file
|
|
@ -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,
|
||||
);
|
||||
80
lib/presentation/widgets/attachment/attachment_button.dart
Normal file
|
|
@ -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<String, dynamic> file;
|
||||
|
||||
const AttachmentButton({super.key, required this.file});
|
||||
|
||||
Future<void> 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/presentation/widgets/attachment/attachment_list.dart
Normal file
|
|
@ -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<Map<String, dynamic>> 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]),
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/presentation/widgets/auth/account_buttons.dart
Normal file
|
|
@ -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: <Widget>[
|
||||
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"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
58
lib/presentation/widgets/auth/input_field.dart
Normal file
|
|
@ -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: <Widget>[
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
107
lib/presentation/widgets/auth/login_form.dart
Normal file
|
|
@ -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<FormState> _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: <Widget>[
|
||||
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"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/presentation/widgets/drawer/app_drawer.dart
Normal file
|
|
@ -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: <Widget>[
|
||||
UserProfile(
|
||||
email: account.user!.email,
|
||||
password: account.user!.password,
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
const DrawerList(),
|
||||
UserQuota(
|
||||
value: account.user!.used / account.user!.quota,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
lib/presentation/widgets/drawer/drawer_list.dart
Normal file
|
|
@ -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<void> 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: <Widget>[
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/presentation/widgets/drawer/drawer_list_tile.dart
Normal file
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/presentation/widgets/drawer/toast_message.dart
Normal file
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
75
lib/presentation/widgets/drawer/user_profile.dart
Normal file
|
|
@ -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<void> 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: <Widget>[
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
71
lib/presentation/widgets/drawer/user_quota.dart
Normal file
|
|
@ -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<void> 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,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
27
lib/presentation/widgets/image_banner.dart
Normal file
|
|
@ -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)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
53
lib/presentation/widgets/message/message_body.dart
Normal file
|
|
@ -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<MessageBody> createState() => _MessageBodyState();
|
||||
}
|
||||
|
||||
class _MessageBodyState extends State<MessageBody> {
|
||||
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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
82
lib/presentation/widgets/message/message_info.dart
Normal file
|
|
@ -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,
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
32
lib/presentation/widgets/message/message_list.dart
Normal file
|
|
@ -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<TMMessage> messages;
|
||||
final Future<void> 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
84
lib/presentation/widgets/message/message_sender.dart
Normal file
|
|
@ -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<MessageSender> createState() => _MessageSenderState();
|
||||
}
|
||||
|
||||
class _MessageSenderState extends State<MessageSender> {
|
||||
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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
77
lib/presentation/widgets/message/message_tile.dart
Normal file
|
|
@ -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<MessageTile> createState() => _MessageTileState();
|
||||
}
|
||||
|
||||
class _MessageTileState extends State<MessageTile> {
|
||||
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),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/presentation/widgets/message/message_view.dart
Normal file
|
|
@ -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()
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||