feat: replace mail protocol with smolmail and rebuild the UI

This commit is contained in:
randogoth 2026-09-26 22:45:38 +03:00
parent cb24b68069
commit e94e4158cc
93 changed files with 6387 additions and 3479 deletions

View file

@ -1,17 +1,17 @@
// ignore_for_file: constant_identifier_names
///
/// Flutter icons FlashMailIcons
/// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
///
/// To use this font, place it in your fonts/ directory and include the
/// following in your pubspec.yaml
///
/// flutter:
/// fonts:
/// - family: FlashMailIcons
/// fonts:
/// - asset: fonts/FlashMailIcons.ttf
//
// Flutter icons FlashMailIcons
// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com
// This font was generated by FlutterIcon.com, which is derived from Fontello.
//
// To use this font, place it in your fonts/ directory and include the
// following in your pubspec.yaml
//
// flutter:
// fonts:
// - family: FlashMailIcons
// fonts:
// - asset: fonts/FlashMailIcons.ttf
import "package:flutter/widgets.dart";

View file

@ -0,0 +1,15 @@
import "package:flutter/material.dart";
/// A deterministic color for [key] (e.g. a contact address), picked from
/// [palette] — same contact always gets the same avatar color, and every
/// choice comes from the app's own palette instead of arbitrary RGB space.
Color colorForKey(String key, List<Color> palette) {
if (palette.isEmpty) return Colors.grey;
final hash = key.codeUnits.fold<int>(0, (acc, c) => (acc * 31 + c) & 0x7fffffff);
return palette[hash % palette.length];
}
/// Black or white, whichever reads clearly on [background].
Color contrastingTextColor(Color background) {
return background.computeLuminance() > 0.5 ? Colors.black : Colors.white;
}

View file

@ -0,0 +1,37 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:intl/intl.dart";
/// One path for all replies so the Re: rule lives once.
String replySubject(String subject) =>
subject.isEmpty || subject.startsWith("Re:") ? subject : "Re: $subject";
String formatTime(int? seconds) {
if (seconds == null) return "";
final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000, isUtc: true);
return DateFormat.yMMMMd().add_jm().format(date.toLocal());
}
/// The full date is too wide for a list row's trailing slot — this is what
/// message_tile.dart shows instead: just a time for today, else a short date.
String formatShortTime(int? seconds) {
if (seconds == null) return "";
final date =
DateTime.fromMillisecondsSinceEpoch(seconds * 1000, isUtc: true).toLocal();
final now = DateTime.now();
if (date.year == now.year && date.month == now.month && date.day == now.day) {
return DateFormat.jm().format(date);
}
if (date.year == now.year) {
return DateFormat.MMMd().format(date);
}
return DateFormat.yMd().format(date);
}
Future<void> copyText(BuildContext context, String text) async {
await Clipboard.setData(ClipboardData(text: text));
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text("Copied to clipboard")));
}
}

View file

@ -1,12 +0,0 @@
import "dart:math";
import "dart:ui";
Color getRandomColor() {
Random random = Random();
return Color.fromRGBO(
random.nextInt(256),
random.nextInt(256),
random.nextInt(256),
1,
);
}

View file

@ -1,12 +0,0 @@
import "dart:math";
String randomString(int length) {
const charset =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
final random = Random.secure();
final codeUnits = List<int>.generate(
length,
(index) => charset.codeUnitAt(random.nextInt(charset.length)),
);
return String.fromCharCodes(codeUnits);
}

View file

@ -1,3 +0,0 @@
final RegExp emailRegex = RegExp(
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$',
);

View file

@ -0,0 +1,15 @@
import "package:flutter/material.dart";
/// Errors get the error color explicitly — the theme's default SnackBar is
/// neutral, so a plain SnackBar(content: ...) is *not* red anymore.
void showErrorSnackBar(BuildContext context, String message,
{Duration? duration}) {
final scheme = Theme.of(context).colorScheme;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: scheme.error,
duration: duration ?? const Duration(seconds: 4),
content: Text(message, style: TextStyle(color: scheme.onError)),
),
);
}