v1 release

This commit is contained in:
Sarthak Kimtani 2024-02-17 20:32:01 +05:30
commit cb24b68069
116 changed files with 4913 additions and 0 deletions

44
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View file

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

88
android/app/build.gradle Normal file
View 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"
}

View file

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

View file

@ -0,0 +1,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>

View file

@ -0,0 +1,6 @@
package com.example.flash_mail
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,007 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

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

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

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

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#01111d</color>
</resources>

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

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

View file

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

31
android/build.gradle Normal file
View 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
}

View file

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View file

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

11
android/settings.gradle Normal file
View 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"

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/Inter-Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/images/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
assets/images/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
assets/images/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
assets/images/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

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

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

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

View 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,
};

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

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

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

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

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

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

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

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

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

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

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

View 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}';
}
}

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

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

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

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

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

View 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,
);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Some files were not shown because too many files have changed in this diff Show more