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

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

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

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

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

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

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