v1 release
This commit is contained in:
commit
cb24b68069
116 changed files with 4913 additions and 0 deletions
80
lib/presentation/widgets/attachment/attachment_button.dart
Normal file
80
lib/presentation/widgets/attachment/attachment_button.dart
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import "package:external_path/external_path.dart";
|
||||
import "package:flash/flash.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_downloader/flutter_downloader.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
|
||||
import "package:flash_mail/data/providers/providers.dart";
|
||||
import "package:flash_mail/presentation/widgets/drawer/toast_message.dart";
|
||||
|
||||
class AttachmentButton extends ConsumerWidget {
|
||||
final Map<String, dynamic> file;
|
||||
|
||||
const AttachmentButton({super.key, required this.file});
|
||||
|
||||
Future<void> downloadFile(BuildContext ctx, WidgetRef ref, String url) async {
|
||||
final token = ref.read(tokenProvider);
|
||||
showFlash(
|
||||
context: ctx,
|
||||
duration: const Duration(seconds: 2),
|
||||
builder: (context, controller) => ToastMessage(
|
||||
controller: controller,
|
||||
icon: Icons.arrow_downward,
|
||||
text: "Starting download!",
|
||||
),
|
||||
);
|
||||
|
||||
await FlutterDownloader.enqueue(
|
||||
headers: {"Authorization": "Bearer $token"},
|
||||
url: "https://api.mail.tm$url",
|
||||
savedDir: await ExternalPath.getExternalStoragePublicDirectory(
|
||||
ExternalPath.DIRECTORY_DOWNLOADS,
|
||||
),
|
||||
saveInPublicStorage: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color.fromRGBO(137, 147, 164, 1),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.file_open_rounded,
|
||||
color: Colors.red,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
file["filename"],
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
splashColor: const Color.fromRGBO(40, 70, 100, 1),
|
||||
onPressed: () =>
|
||||
downloadFile(context, ref, file["downloadUrl"]),
|
||||
icon: const Icon(
|
||||
Icons.arrow_downward,
|
||||
size: 24,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/presentation/widgets/attachment/attachment_list.dart
Normal file
18
lib/presentation/widgets/attachment/attachment_list.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:flash_mail/presentation/widgets/attachment/attachment_button.dart";
|
||||
|
||||
class AttachmentList extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> attachments;
|
||||
|
||||
const AttachmentList({super.key, required this.attachments});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: attachments.length,
|
||||
itemBuilder: (ctx, i) => AttachmentButton(file: attachments[i]),
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/presentation/widgets/auth/account_buttons.dart
Normal file
67
lib/presentation/widgets/auth/account_buttons.dart
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import "package:auto_route/auto_route.dart";
|
||||
import "package:dio/dio.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
|
||||
import "package:flash_mail/data/providers/providers.dart";
|
||||
import "package:flash_mail/presentation/widgets/primary_button.dart";
|
||||
import "package:flash_mail/presentation/widgets/secondary_button.dart";
|
||||
import "package:flash_mail/presentation/widgets/small_loading_spinner.dart";
|
||||
|
||||
class AccountButtons extends StatelessWidget {
|
||||
const AccountButtons({super.key});
|
||||
|
||||
void showErrorSnackBar(BuildContext ctx, String message) {
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSubmit(BuildContext context, WidgetRef ref) async {
|
||||
try {
|
||||
await ref.read(accountProvider.notifier).createAccount();
|
||||
if (context.mounted) {
|
||||
AutoRouter.of(context).replaceNamed("/inbox");
|
||||
}
|
||||
} on DioError catch (err) {
|
||||
if (context.mounted) {
|
||||
showErrorSnackBar(context, err.message);
|
||||
}
|
||||
} catch (err) {
|
||||
if (context.mounted) {
|
||||
showErrorSnackBar(context, "Something went wrong!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Your Inbox, Your Privacy, Zero Ads.",
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
return PrimaryButton(
|
||||
child: ref.watch(accountProvider).isLoading
|
||||
? const SmallLoadingSpinner()
|
||||
: const Text("Create New Account"),
|
||||
onPressed: () => _handleSubmit(context, ref),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
SecondaryButton(
|
||||
text: "Use Existing",
|
||||
onPressed: () => AutoRouter.of(context).pushNamed("/login"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
58
lib/presentation/widgets/auth/input_field.dart
Normal file
58
lib/presentation/widgets/auth/input_field.dart
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
class InputField extends StatelessWidget {
|
||||
final String label;
|
||||
final String placeholder;
|
||||
final TextInputAction inputAction;
|
||||
final String? Function(String?) validator;
|
||||
final void Function(String?) onSaved;
|
||||
final TextInputType? inputType;
|
||||
final bool? obscureText;
|
||||
|
||||
const InputField({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.placeholder,
|
||||
required this.inputAction,
|
||||
required this.validator,
|
||||
required this.onSaved,
|
||||
this.inputType,
|
||||
this.obscureText,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
keyboardType: inputType,
|
||||
textInputAction: inputAction,
|
||||
obscureText: obscureText ?? false,
|
||||
validator: validator,
|
||||
onSaved: onSaved,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 18),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: const Color.fromRGBO(22, 39, 56, 1),
|
||||
hintText: placeholder,
|
||||
hintStyle: const TextStyle(
|
||||
color: Color.fromRGBO(127, 133, 141, 1),
|
||||
),
|
||||
errorStyle: const TextStyle(
|
||||
fontFamily: "Inter",
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
107
lib/presentation/widgets/auth/login_form.dart
Normal file
107
lib/presentation/widgets/auth/login_form.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import "package:auto_route/auto_route.dart";
|
||||
import "package:dio/dio.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
|
||||
import "package:flash_mail/data/providers/providers.dart";
|
||||
import "package:flash_mail/presentation/widgets/auth/input_field.dart";
|
||||
import "package:flash_mail/presentation/widgets/primary_button.dart";
|
||||
import "package:flash_mail/presentation/widgets/small_loading_spinner.dart";
|
||||
import "package:flash_mail/shared/utils/regex_patterns.dart";
|
||||
|
||||
class LoginForm extends ConsumerWidget {
|
||||
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||
|
||||
LoginForm({super.key});
|
||||
|
||||
String? _validateEmail(String? value) {
|
||||
if (value!.isEmpty || !emailRegex.hasMatch(value)) {
|
||||
return "Invalid email address";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _validatePassword(String? value) {
|
||||
if (value!.isEmpty || value.length < 8) {
|
||||
return "Password too short";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void showErrorSnackBar(BuildContext ctx, String message) {
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSubmit(BuildContext context, WidgetRef ref) async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
_formKey.currentState!.save();
|
||||
|
||||
try {
|
||||
String? email = ref.read(emailProvider.notifier).state;
|
||||
String? password = ref.read(passwordProvider.notifier).state;
|
||||
|
||||
await ref.read(accountProvider.notifier).login(email, password);
|
||||
|
||||
if (context.mounted) {
|
||||
AutoRouter.of(context).replaceNamed("/inbox");
|
||||
}
|
||||
} on DioError catch (err) {
|
||||
if (context.mounted) {
|
||||
showErrorSnackBar(context, err.message);
|
||||
}
|
||||
} catch (_) {
|
||||
if (context.mounted) {
|
||||
showErrorSnackBar(context, "Something went wrong!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final account = ref.watch(accountProvider);
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
InputField(
|
||||
label: "Email Address",
|
||||
placeholder: "Enter email",
|
||||
inputType: TextInputType.emailAddress,
|
||||
inputAction: TextInputAction.next,
|
||||
validator: _validateEmail,
|
||||
onSaved: (value) => ref.read(emailProvider.notifier).state = value,
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
InputField(
|
||||
label: "Password",
|
||||
placeholder: "Enter password",
|
||||
obscureText: true,
|
||||
inputAction: TextInputAction.done,
|
||||
validator: _validatePassword,
|
||||
onSaved: (value) =>
|
||||
ref.read(passwordProvider.notifier).state = value,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Center(
|
||||
child: PrimaryButton(
|
||||
onPressed: () => _handleSubmit(context, ref),
|
||||
child: account.isLoading
|
||||
? const SmallLoadingSpinner()
|
||||
: const Text("Login"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/presentation/widgets/drawer/app_drawer.dart
Normal file
37
lib/presentation/widgets/drawer/app_drawer.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:flash_mail/data/providers/providers.dart";
|
||||
import "package:flash_mail/presentation/widgets/drawer/user_profile.dart";
|
||||
import "package:flash_mail/presentation/widgets/drawer/drawer_list.dart";
|
||||
import "package:flash_mail/presentation/widgets/drawer/user_quota.dart";
|
||||
|
||||
class AppDrawer extends ConsumerWidget {
|
||||
const AppDrawer({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final account = ref.read(accountProvider);
|
||||
|
||||
return Drawer(
|
||||
backgroundColor: const Color.fromRGBO(22, 39, 56, 1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 90),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
UserProfile(
|
||||
email: account.user!.email,
|
||||
password: account.user!.password,
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
const DrawerList(),
|
||||
UserQuota(
|
||||
value: account.user!.used / account.user!.quota,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
lib/presentation/widgets/drawer/drawer_list.dart
Normal file
44
lib/presentation/widgets/drawer/drawer_list.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:auto_route/auto_route.dart";
|
||||
|
||||
import "package:flash_mail/data/providers/providers.dart";
|
||||
import "package:flash_mail/shared/configs/flash_mail_icons.dart";
|
||||
import "package:flash_mail/presentation/widgets/drawer/drawer_list_tile.dart";
|
||||
|
||||
class DrawerList extends ConsumerWidget {
|
||||
const DrawerList({super.key});
|
||||
|
||||
Future<void> signOut(BuildContext context, WidgetRef ref) async {
|
||||
Navigator.of(context).pop();
|
||||
await ref.read(accountProvider.notifier).logout();
|
||||
if (context.mounted) {
|
||||
AutoRouter.of(context).replaceNamed("/");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 25),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
DrawerListTile(
|
||||
color: Theme.of(context).primaryColor,
|
||||
icon: FlashMailIcons.inbox,
|
||||
title: "Inbox",
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Consumer(
|
||||
builder: (context, ref, _) => DrawerListTile(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
icon: FlashMailIcons.sign_out,
|
||||
title: "Sign out",
|
||||
onTap: () => signOut(context, ref),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/presentation/widgets/drawer/drawer_list_tile.dart
Normal file
39
lib/presentation/widgets/drawer/drawer_list_tile.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
class DrawerListTile extends StatelessWidget {
|
||||
final Color color;
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final void Function() onTap;
|
||||
|
||||
const DrawerListTile({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
splashColor: const Color.fromRGBO(40, 70, 100, 1),
|
||||
highlightColor: const Color.fromRGBO(22, 39, 56, 1),
|
||||
),
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
leading: Icon(icon, color: color, size: 18),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontFamily: "Inter",
|
||||
color: color,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/presentation/widgets/drawer/toast_message.dart
Normal file
46
lib/presentation/widgets/drawer/toast_message.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flash/flash.dart";
|
||||
|
||||
class ToastMessage extends StatelessWidget {
|
||||
final FlashController controller;
|
||||
final IconData icon;
|
||||
final String text;
|
||||
|
||||
const ToastMessage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.icon,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Flash(
|
||||
position: FlashPosition.bottom,
|
||||
controller: controller,
|
||||
child: AlertDialog(
|
||||
alignment: Alignment.bottomCenter,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
backgroundColor: const Color.fromRGBO(217, 217, 217, 1),
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: Colors.black,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Text(
|
||||
text,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: Colors.black),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
75
lib/presentation/widgets/drawer/user_profile.dart
Normal file
75
lib/presentation/widgets/drawer/user_profile.dart
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flash/flash.dart";
|
||||
|
||||
import "package:flash_mail/presentation/widgets/drawer/toast_message.dart";
|
||||
|
||||
class UserProfile extends StatelessWidget {
|
||||
final String email;
|
||||
final String password;
|
||||
|
||||
const UserProfile({
|
||||
super.key,
|
||||
required this.email,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
Future<void> copyClipboard(BuildContext context, String text) async {
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (context.mounted) {
|
||||
showFlash(
|
||||
context: context,
|
||||
duration: const Duration(seconds: 2),
|
||||
builder: (context, controller) => ToastMessage(
|
||||
controller: controller,
|
||||
icon: Icons.check,
|
||||
text: "Copied to Clipboard!",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
CircleAvatar(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
radius: 35,
|
||||
child: Text(
|
||||
email[0].toUpperCase(),
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyLarge!.copyWith(fontSize: 30),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
InkWell(
|
||||
onTap: () => copyClipboard(context, email),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
email,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
InkWell(
|
||||
onTap: () => copyClipboard(context, password),
|
||||
child: Text(
|
||||
"password: $password",
|
||||
style: const TextStyle(
|
||||
color: Color.fromRGBO(127, 133, 141, 1),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
71
lib/presentation/widgets/drawer/user_quota.dart
Normal file
71
lib/presentation/widgets/drawer/user_quota.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:url_launcher/url_launcher.dart";
|
||||
|
||||
import "package:flash_mail/shared/configs/flash_mail_icons.dart";
|
||||
|
||||
class UserQuota extends StatelessWidget {
|
||||
final String _url = "https://mail.tm/";
|
||||
|
||||
final double value;
|
||||
|
||||
const UserQuota({super.key, required this.value});
|
||||
|
||||
Future<void> openUrl(Uri url) async {
|
||||
if (await canLaunchUrl(url)) {
|
||||
launchUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30, left: 40, right: 40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Quota",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
LinearProgressIndicator(
|
||||
backgroundColor: const Color.fromRGBO(85, 97, 112, 1),
|
||||
color: Theme.of(context).primaryColor,
|
||||
value: value,
|
||||
minHeight: 10,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
InkWell(
|
||||
onTap: () => openUrl(Uri.parse(_url)),
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Powered By Mail.tm",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Icon(
|
||||
FlashMailIcons.redirect,
|
||||
color: Theme.of(context).primaryColor,
|
||||
size: 16,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
27
lib/presentation/widgets/image_banner.dart
Normal file
27
lib/presentation/widgets/image_banner.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
class ImageBanner extends StatelessWidget {
|
||||
final String imgSrc;
|
||||
final String text;
|
||||
|
||||
const ImageBanner({
|
||||
Key? key,
|
||||
required this.imgSrc,
|
||||
required this.text,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(imgSrc, width: 250, height: 250),
|
||||
const SizedBox(height: 10),
|
||||
Text(text, style: Theme.of(context).textTheme.bodyLarge)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
53
lib/presentation/widgets/message/message_body.dart
Normal file
53
lib/presentation/widgets/message/message_body.dart
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_inappwebview/flutter_inappwebview.dart";
|
||||
import "package:url_launcher/url_launcher.dart";
|
||||
|
||||
class MessageBody extends StatefulWidget {
|
||||
final String html;
|
||||
|
||||
const MessageBody({super.key, required this.html});
|
||||
|
||||
@override
|
||||
State<MessageBody> createState() => _MessageBodyState();
|
||||
}
|
||||
|
||||
class _MessageBodyState extends State<MessageBody> {
|
||||
double webviewHeight = 1;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: webviewHeight,
|
||||
child: InAppWebView(
|
||||
initialData: InAppWebViewInitialData(data: widget.html),
|
||||
initialSettings: InAppWebViewSettings(
|
||||
transparentBackground: true,
|
||||
forceDark: ForceDark.ON,
|
||||
algorithmicDarkeningAllowed: true,
|
||||
supportZoom: false,
|
||||
javaScriptEnabled: true,
|
||||
disableHorizontalScroll: false,
|
||||
disableVerticalScroll: true,
|
||||
useWideViewPort: false,
|
||||
useHybridComposition: false,
|
||||
preferredContentMode: UserPreferredContentMode.MOBILE,
|
||||
),
|
||||
onLoadStop: (controller, url) async {
|
||||
final contentHeight = await controller.getContentHeight();
|
||||
if (contentHeight != null) {
|
||||
setState(() {
|
||||
webviewHeight = contentHeight.toDouble();
|
||||
});
|
||||
}
|
||||
},
|
||||
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
||||
final url = navigationAction.request.url;
|
||||
if (url != null && await canLaunchUrl(url)) {
|
||||
await launchUrl(url);
|
||||
}
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
82
lib/presentation/widgets/message/message_info.dart
Normal file
82
lib/presentation/widgets/message/message_info.dart
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:intl/intl.dart";
|
||||
|
||||
class MessageInfo extends StatelessWidget {
|
||||
final String senderAddress;
|
||||
final String receiverAddress;
|
||||
final DateTime date;
|
||||
|
||||
const MessageInfo({
|
||||
super.key,
|
||||
required this.senderAddress,
|
||||
required this.receiverAddress,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 15),
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color.fromRGBO(127, 133, 140, 1),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"From",
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.65,
|
||||
child: Text(
|
||||
senderAddress,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"To",
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.65,
|
||||
child: Text(
|
||||
receiverAddress,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"Date",
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
DateFormat.yMMMMd().add_jm().format(date.toLocal()),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
32
lib/presentation/widgets/message/message_list.dart
Normal file
32
lib/presentation/widgets/message/message_list.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:flash_mail/data/models/message/message.dart";
|
||||
import "package:flash_mail/presentation/widgets/image_banner.dart";
|
||||
import "package:flash_mail/presentation/widgets/message/message_tile.dart";
|
||||
|
||||
class MessageList extends StatelessWidget {
|
||||
final List<TMMessage> messages;
|
||||
final Future<void> Function() onRefresh;
|
||||
|
||||
const MessageList({
|
||||
super.key,
|
||||
required this.messages,
|
||||
required this.onRefresh,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: onRefresh,
|
||||
child: messages.isEmpty
|
||||
? const ImageBanner(
|
||||
imgSrc: "assets/images/empty.png",
|
||||
text: "Your inbox is empty!",
|
||||
)
|
||||
: ListView.builder(
|
||||
itemBuilder: (ctx, i) => MessageTile(message: messages[i]),
|
||||
itemCount: messages.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
84
lib/presentation/widgets/message/message_sender.dart
Normal file
84
lib/presentation/widgets/message/message_sender.dart
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:flash_mail/presentation/widgets/message/message_info.dart";
|
||||
|
||||
class MessageSender extends StatefulWidget {
|
||||
final String senderName;
|
||||
final String senderAddress;
|
||||
final String receiverAddress;
|
||||
final DateTime date;
|
||||
|
||||
const MessageSender({
|
||||
super.key,
|
||||
required this.senderName,
|
||||
required this.senderAddress,
|
||||
required this.receiverAddress,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MessageSender> createState() => _MessageSenderState();
|
||||
}
|
||||
|
||||
class _MessageSenderState extends State<MessageSender> {
|
||||
bool showInfo = false;
|
||||
|
||||
void toggleInfo() {
|
||||
setState(() {
|
||||
showInfo = !showInfo;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Image.asset("assets/images/avatar.png", width: 50, height: 50),
|
||||
const SizedBox(width: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.senderName,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => toggleInfo(),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.6,
|
||||
child: Text(
|
||||
widget.senderAddress,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
showInfo
|
||||
? Icons.keyboard_arrow_up_outlined
|
||||
: Icons.keyboard_arrow_down_outlined,
|
||||
color: const Color.fromRGBO(127, 133, 140, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
showInfo
|
||||
? MessageInfo(
|
||||
senderAddress: widget.senderAddress,
|
||||
receiverAddress: widget.receiverAddress,
|
||||
date: widget.date,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
77
lib/presentation/widgets/message/message_tile.dart
Normal file
77
lib/presentation/widgets/message/message_tile.dart
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import "package:auto_route/auto_route.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:flash_mail/data/models/message/message.dart";
|
||||
import "package:flash_mail/presentation/routes/app_router.gr.dart";
|
||||
import "package:flash_mail/shared/utils/random_color.dart";
|
||||
|
||||
class MessageTile extends StatefulWidget {
|
||||
final TMMessage message;
|
||||
|
||||
const MessageTile({super.key, required this.message});
|
||||
|
||||
@override
|
||||
State<MessageTile> createState() => _MessageTileState();
|
||||
}
|
||||
|
||||
class _MessageTileState extends State<MessageTile> {
|
||||
late Color randomColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
randomColor = getRandomColor();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
splashColor: const Color.fromRGBO(40, 70, 100, 1),
|
||||
focusColor: const Color.fromRGBO(22, 39, 56, 1),
|
||||
onTap: () => AutoRouter.of(context).push(
|
||||
MessageDetailRoute(
|
||||
id: widget.message.id,
|
||||
subject: widget.message.subject,
|
||||
),
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: randomColor,
|
||||
child: Text(
|
||||
widget.message.from["name"]!.toUpperCase()[0],
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
widget.message.from["name"] as String,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight:
|
||||
widget.message.seen ? FontWeight.w500 : FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.message.subject,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight:
|
||||
widget.message.seen ? FontWeight.w300 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.message.intro ?? widget.message.subject,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: const Color.fromRGBO(193, 203, 213, 1),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/presentation/widgets/message/message_view.dart
Normal file
39
lib/presentation/widgets/message/message_view.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
|
||||
import "package:flash_mail/data/models/message_detail/message_detail.dart";
|
||||
import "package:flash_mail/data/providers/providers.dart";
|
||||
import "package:flash_mail/presentation/widgets/attachment/attachment_list.dart";
|
||||
import "package:flash_mail/presentation/widgets/message/message_body.dart";
|
||||
import "package:flash_mail/presentation/widgets/message/message_sender.dart";
|
||||
|
||||
class MessageView extends StatelessWidget {
|
||||
final TMMessageDetail message;
|
||||
|
||||
const MessageView({super.key, required this.message});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Consumer(
|
||||
builder: (context, ref, child) => MessageSender(
|
||||
senderName: message.from["name"] as String,
|
||||
senderAddress: message.from["address"] as String,
|
||||
receiverAddress: ref.read(emailProvider) as String,
|
||||
date: message.createdAt,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
MessageBody(
|
||||
html: message.html[0],
|
||||
),
|
||||
message.hasAttachments
|
||||
? AttachmentList(
|
||||
attachments: message.attachments!,
|
||||
)
|
||||
: const SizedBox.shrink()
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
24
lib/presentation/widgets/primary_button.dart
Normal file
24
lib/presentation/widgets/primary_button.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
class PrimaryButton extends StatelessWidget {
|
||||
final Widget child;
|
||||
final void Function() onPressed;
|
||||
|
||||
const PrimaryButton({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
height: 40,
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
23
lib/presentation/widgets/secondary_button.dart
Normal file
23
lib/presentation/widgets/secondary_button.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
class SecondaryButton extends StatelessWidget {
|
||||
final String text;
|
||||
final void Function() onPressed;
|
||||
|
||||
const SecondaryButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
child: TextButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(text),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
45
lib/presentation/widgets/shimmer/list_shimmer.dart
Normal file
45
lib/presentation/widgets/shimmer/list_shimmer.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
class ListShimmer extends StatelessWidget {
|
||||
const ListShimmer({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
color: const Color.fromRGBO(160, 176, 186, 1),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: const Color.fromRGBO(160, 176, 186, 1),
|
||||
width: MediaQuery.of(context).size.width * 0.65,
|
||||
height: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: const Color.fromRGBO(160, 176, 186, 1),
|
||||
width: MediaQuery.of(context).size.width * 0.65,
|
||||
height: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
28
lib/presentation/widgets/shimmer/message_shimmer.dart
Normal file
28
lib/presentation/widgets/shimmer/message_shimmer.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:shimmer/shimmer.dart";
|
||||
|
||||
import "package:flash_mail/presentation/widgets/shimmer/list_shimmer.dart";
|
||||
|
||||
class MessageShimmer extends StatelessWidget {
|
||||
const MessageShimmer({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Shimmer.fromColors(
|
||||
baseColor: Colors.grey[500]!,
|
||||
highlightColor: Colors.grey[300]!,
|
||||
child: ListView.builder(
|
||||
itemBuilder: (ctx, i) => const Column(
|
||||
children: [
|
||||
ListShimmer(),
|
||||
SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
itemCount: 8,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
19
lib/presentation/widgets/small_loading_spinner.dart
Normal file
19
lib/presentation/widgets/small_loading_spinner.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
class SmallLoadingSpinner extends StatelessWidget {
|
||||
const SmallLoadingSpinner({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue