TF-4171 Display tag in mailbox list
This commit is contained in:
@@ -18,6 +18,7 @@ export 'presentation/extensions/map_extensions.dart';
|
||||
export 'presentation/extensions/either_view_state_extension.dart';
|
||||
export 'presentation/extensions/media_type_extension.dart';
|
||||
export 'presentation/extensions/scroll_controller_extension.dart';
|
||||
export 'presentation/extensions/hex_color_extension.dart';
|
||||
|
||||
// Exceptions
|
||||
export 'domain/exceptions/download_file_exception.dart';
|
||||
@@ -63,6 +64,7 @@ export 'utils/web_link_generator.dart';
|
||||
export 'utils/sentry/sentry_manager.dart';
|
||||
export 'utils/config/env_loader.dart';
|
||||
export 'utils/sentry/sentry_dio_helper.dart';
|
||||
export 'utils/widget_utils.dart';
|
||||
|
||||
// Views
|
||||
export 'presentation/views/text/slogan_builder.dart';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension HexColorExtension on String {
|
||||
Color toColor({Color fallback = Colors.transparent}) {
|
||||
try {
|
||||
final hex = replaceAll('#', '');
|
||||
|
||||
if (hex.length != 6 && hex.length != 8) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
final fullHex = hex.length == 6 ? 'FF$hex' : hex;
|
||||
return Color(int.parse(fullHex, radix: 16));
|
||||
} catch (_) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WidgetUtils {
|
||||
WidgetUtils._();
|
||||
|
||||
static double measureTextWidth({
|
||||
required String text,
|
||||
double fallbackWidth = 0,
|
||||
TextStyle? style,
|
||||
Locale? locale,
|
||||
double? horizontalPadding,
|
||||
}) {
|
||||
try {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: style,
|
||||
locale: locale,
|
||||
),
|
||||
maxLines: 1,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
if (horizontalPadding != null) {
|
||||
return textPainter.width + horizontalPadding * 2;
|
||||
} else {
|
||||
return textPainter.width;
|
||||
}
|
||||
} catch (e) {
|
||||
return fallbackWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:core/presentation/extensions/hex_color_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('HexColorExtension.toColor', () {
|
||||
test('should convert 6-char hex (#RRGGBB) to Color with default alpha FF',
|
||||
() {
|
||||
final color = "#FF0000".toColor();
|
||||
expect(color, const Color(0xFFFF0000));
|
||||
});
|
||||
|
||||
test('should convert 8-char hex (#AARRGGBB) correctly', () {
|
||||
final color = "#80FF0000".toColor();
|
||||
expect(color, const Color(0x80FF0000));
|
||||
});
|
||||
|
||||
test('should handle string without # prefix', () {
|
||||
final color = "00FF00".toColor();
|
||||
expect(color, const Color(0xFF00FF00));
|
||||
});
|
||||
|
||||
test('should return fallback when hex is invalid', () {
|
||||
const fallback = Colors.blue;
|
||||
final color = "INVALID".toColor(fallback: fallback);
|
||||
expect(color, fallback);
|
||||
});
|
||||
|
||||
test('should return fallback when hex contains non-hex characters', () {
|
||||
const fallback = Colors.red;
|
||||
final color = "#GGHHHH".toColor(fallback: fallback);
|
||||
expect(color, fallback);
|
||||
});
|
||||
|
||||
test('should return fallback when hex length is unsupported', () {
|
||||
const fallback = Colors.orange;
|
||||
final color = "#FFF".toColor(fallback: fallback);
|
||||
expect(color, fallback);
|
||||
});
|
||||
|
||||
test('should return fallback for empty string', () {
|
||||
const fallback = Colors.purple;
|
||||
final color = "".toColor(fallback: fallback);
|
||||
expect(color, fallback);
|
||||
});
|
||||
|
||||
test('should convert lowercase hex', () {
|
||||
final color = "#ff0000".toColor();
|
||||
expect(color, const Color(0xFFFF0000));
|
||||
});
|
||||
|
||||
test('should convert mixed-case hex', () {
|
||||
final color = "#Ff00Ff".toColor();
|
||||
expect(color, const Color(0xFFFf00Ff));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:core/presentation/extensions/color_extension.dart';
|
||||
import 'package:core/presentation/extensions/hex_color_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:labels/model/label.dart';
|
||||
|
||||
extension LabelExtension on Label {
|
||||
static const String moreLabelId = 'more-btn';
|
||||
|
||||
String get safeDisplayName => displayName ?? '';
|
||||
|
||||
Color? get backgroundColor {
|
||||
if (id?.value == moreLabelId) {
|
||||
return AppColor.grayBackgroundColor;
|
||||
} else {
|
||||
return color?.value.toColor();
|
||||
}
|
||||
}
|
||||
|
||||
Color get textColor {
|
||||
if (id?.value == moreLabelId) {
|
||||
return Colors.black;
|
||||
} else {
|
||||
return Colors.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
+720
-3
@@ -17,6 +17,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.13.0"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -33,6 +41,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
barcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: barcode
|
||||
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.9"
|
||||
bidi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bidi
|
||||
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -113,6 +137,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -153,6 +185,21 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../core"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0+1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -161,6 +208,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.3"
|
||||
cupertino_icons:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -177,6 +240,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.1"
|
||||
debounce_throttle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: debounce_throttle
|
||||
sha256: c95cf47afda975fc507794a52040a16756fb2f31ad3027d4e691c41862ff5692
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
device_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "499c61743e13909c13374a8c209075385858c614b9c0f2487b5f9995eeaf7369"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.1"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.3"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -201,14 +288,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
version: "6.1.4"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -217,11 +312,227 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
fk_user_agent:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fk_user_agent
|
||||
sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
flex_color_picker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flex_color_picker
|
||||
sha256: f0e0db8e3e47435cfbe9aa15c71b898fa218be0fc4ae409e1e42d5d5266b2c90
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
flex_seed_scheme:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flex_seed_scheme
|
||||
sha256: "4cee2f1d07259f77e8b36f4ec5f35499d19e74e17c7dce5b819554914082bc01"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_charset_detector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_charset_detector
|
||||
sha256: "5d4796d43dac2f37e14149b0f0c676fa08e8eb1ada8e99342a838d971d2fb9b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
flutter_charset_detector_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_charset_detector_android
|
||||
sha256: bee057133d5f134fe211523a27200dd22c782b93777f43359a571bd2c77da2cc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
flutter_charset_detector_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_charset_detector_darwin
|
||||
sha256: "8cf51c3e16c2fb4ec4e309f16f6046a0ddf1ff57d1b6b696410d077a9ffbfb15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
flutter_charset_detector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_charset_detector_platform_interface
|
||||
sha256: "1c09ed7b314a5a9dde76057b98b7d35458ba881eed03d5e5b6f7f74b4869d18c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter_charset_detector_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_charset_detector_web
|
||||
sha256: b547194e97e15d2cca17e957ad7f373b48abf35080e645ac5b6976d129b0265d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
flutter_image_compress:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_image_compress
|
||||
sha256: f159d2e8c4ed04b8e36994124fd4a5017a0f01e831ae3358c74095c340e9ae5e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
flutter_image_compress_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_image_compress_common
|
||||
sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
flutter_image_compress_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_image_compress_macos
|
||||
sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
flutter_image_compress_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_image_compress_platform_interface
|
||||
sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
flutter_image_compress_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_image_compress_web
|
||||
sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
flutter_inappwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview
|
||||
sha256: e5caf1649d082e333e22031c84a24d345d2e76e136345481d7ab495c2069d8ec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
flutter_inappwebview_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_android
|
||||
sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
flutter_inappwebview_internal_annotations:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_internal_annotations
|
||||
sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
flutter_inappwebview_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_ios
|
||||
sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
flutter_inappwebview_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_macos
|
||||
sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
flutter_inappwebview_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_platform_interface
|
||||
sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0+1"
|
||||
flutter_inappwebview_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_web
|
||||
sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
flutter_inappwebview_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_windows
|
||||
sha256: "2c1c13c32835c4a0afa49aabc207cd5cf80967c00bfdfe49259d082182d7c81f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
flutter_keyboard_visibility:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility
|
||||
sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_keyboard_visibility_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_linux
|
||||
sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
flutter_keyboard_visibility_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_macos
|
||||
sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
flutter_keyboard_visibility_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_platform_interface
|
||||
sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
flutter_keyboard_visibility_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_web
|
||||
sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
flutter_keyboard_visibility_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_windows
|
||||
sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -230,11 +541,32 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_svg:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_typeahead:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_typeahead
|
||||
sha256: ef2dd5a505d2d95a5b4c571c81ad2d6e7955f583dddec49064fec57acffd7a96
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -243,6 +575,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
get:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: get
|
||||
sha256: e4e7335ede17452b391ed3b2ede016545706c01a02292a6c97619705e7d2a85e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.6"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -259,6 +599,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.3"
|
||||
html_unescape:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html_unescape
|
||||
sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_mock_adapter:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -283,6 +647,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -348,6 +728,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
linkify:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: linkify
|
||||
sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -404,6 +792,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
package_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.2"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -412,6 +816,134 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.19"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
pdf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pdf
|
||||
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.11.3"
|
||||
pdf_widget_wrapper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pdf_widget_wrapper
|
||||
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointer_interceptor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointer_interceptor
|
||||
sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.1+2"
|
||||
pointer_interceptor_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointer_interceptor_ios
|
||||
sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.1"
|
||||
pointer_interceptor_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointer_interceptor_platform_interface
|
||||
sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.0+1"
|
||||
pointer_interceptor_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointer_interceptor_web
|
||||
sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.3"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -420,6 +952,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
printing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: printing
|
||||
sha256: "1c99cab90ebcc1fff65831d264627d5b529359d563e53f33ab9b8117f2d280bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.12.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -436,6 +984,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
pull_to_refresh:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pull_to_refresh
|
||||
sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
qr:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: qr
|
||||
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
quiver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -444,6 +1008,15 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
sanitize_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: sanitize_html
|
||||
ref: support_mail
|
||||
resolved-ref: fda32cde4d4baadaa988477f498ab6622ee79987
|
||||
url: "https://github.com/linagora/dart-neats.git"
|
||||
source: git
|
||||
version: "2.1.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -460,6 +1033,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
simple_observable:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: simple_observable
|
||||
sha256: b392795c48f8b5f301b4c8f73e15f56e38fe70f42278c649d8325e859a783301
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -553,6 +1134,110 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
universal_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_html
|
||||
sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
url_launcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.20"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.4"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.19"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.19"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -601,6 +1286,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.5"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -611,4 +1328,4 @@ packages:
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.8.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
flutter: ">=3.32.0"
|
||||
|
||||
@@ -11,6 +11,9 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
core:
|
||||
path: ../core
|
||||
|
||||
### Dependencies from git ###
|
||||
jmap_dart_client:
|
||||
git:
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||
import 'package:labels/model/label.dart';
|
||||
import 'package:model/email/email_action_type.dart';
|
||||
import 'package:model/email/presentation_email.dart';
|
||||
import 'package:model/extensions/list_email_address_extension.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
|
||||
import 'package:tmail_ui_user/features/thread/data/extensions/list_keyword_identifier_extension.dart';
|
||||
import 'package:tmail_ui_user/features/thread/data/extensions/map_keywords_extension.dart';
|
||||
|
||||
extension PresentationEmailExtension on PresentationEmail {
|
||||
({
|
||||
@@ -153,4 +156,19 @@ extension PresentationEmailExtension on PresentationEmail {
|
||||
replyTo: [],
|
||||
);
|
||||
}
|
||||
|
||||
List<Label> getLabelList(List<Label> labels) {
|
||||
if (keywords?.isNotEmpty != true || labels.isEmpty) return const [];
|
||||
|
||||
final enabledKeywordListString =
|
||||
keywords!.enabledKeywords.keywordListString;
|
||||
|
||||
if (enabledKeywordListString.isEmpty) return const [];
|
||||
|
||||
return labels
|
||||
.where((label) =>
|
||||
label.keyword != null &&
|
||||
enabledKeywordListString.contains(label.keyword))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import 'package:core/utils/widget_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jmap_dart_client/jmap/core/id.dart';
|
||||
import 'package:labels/extensions/label_extension.dart';
|
||||
import 'package:labels/model/label.dart';
|
||||
|
||||
@visibleForTesting
|
||||
typedef MeasureWidthFn = double Function(Label label);
|
||||
|
||||
class LabelUtils {
|
||||
LabelUtils._();
|
||||
|
||||
static const double defaultPlusMaxWidth = 40.0;
|
||||
|
||||
static ({
|
||||
List<Label> displayed,
|
||||
int hiddenCount,
|
||||
}) computeDisplayedTags({
|
||||
required List<Label> tags,
|
||||
required double maxWidth,
|
||||
required TextStyle textStyle,
|
||||
double itemSpacing = 12,
|
||||
double horizontalPadding = 4,
|
||||
double? tagMaxWidth,
|
||||
double? plusMaxWidth,
|
||||
@visibleForTesting MeasureWidthFn? measureWidth,
|
||||
}) {
|
||||
if (tags.isEmpty) {
|
||||
return (displayed: const [], hiddenCount: 0);
|
||||
}
|
||||
|
||||
final effectiveMeasure = measureWidth ??
|
||||
(label) => WidgetUtils.measureTextWidth(
|
||||
text: label.safeDisplayName,
|
||||
style: textStyle,
|
||||
horizontalPadding: horizontalPadding,
|
||||
);
|
||||
|
||||
// Measure all tag widths with per-item max constraint
|
||||
final widths = tags.map((label) {
|
||||
final width = effectiveMeasure(label);
|
||||
return tagMaxWidth != null
|
||||
? width.clamp(0, tagMaxWidth).toDouble()
|
||||
: width;
|
||||
}).toList();
|
||||
|
||||
final total = tags.length;
|
||||
|
||||
// Compute how many fit normally
|
||||
final fit = _computeFitCount(
|
||||
widths: widths,
|
||||
maxWidth: maxWidth,
|
||||
itemSpacing: itemSpacing,
|
||||
);
|
||||
|
||||
int fitCount = fit.fitCount;
|
||||
double usedWidth = fit.usedWidth;
|
||||
int remaining = total - fitCount;
|
||||
|
||||
// All fit normally
|
||||
if (fitCount > 0 && remaining == 0) {
|
||||
return (displayed: tags, hiddenCount: 0);
|
||||
}
|
||||
|
||||
// Ensure minimum 1 displayed always
|
||||
if (fitCount == 0) {
|
||||
final first = tags.first;
|
||||
double firstWidth = widths.first;
|
||||
|
||||
// First tag larger than maxWidth → still show it alone
|
||||
if (firstWidth > maxWidth) {
|
||||
return (displayed: [first], hiddenCount: total - 1);
|
||||
}
|
||||
|
||||
final remainingCount = total - 1;
|
||||
final plus = _buildPlusLabel(remainingCount);
|
||||
|
||||
double plusWidth = effectiveMeasure(plus);
|
||||
if (plusMaxWidth != null) {
|
||||
plusWidth = plusWidth.clamp(0, plusMaxWidth);
|
||||
}
|
||||
|
||||
final next = firstWidth + itemSpacing + plusWidth;
|
||||
|
||||
if (next <= maxWidth) {
|
||||
return (
|
||||
displayed: [first, plus],
|
||||
hiddenCount: remainingCount,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
displayed: [first],
|
||||
hiddenCount: remainingCount,
|
||||
);
|
||||
}
|
||||
|
||||
// Normal shrink logic
|
||||
while (fitCount > 0) {
|
||||
final plus = _buildPlusLabel(remaining);
|
||||
|
||||
double plusWidth = effectiveMeasure(plus);
|
||||
if (plusMaxWidth != null) {
|
||||
plusWidth = plusWidth.clamp(0, plusMaxWidth);
|
||||
}
|
||||
|
||||
final spacing = (fitCount == 0) ? 0 : itemSpacing;
|
||||
final next = usedWidth + spacing + plusWidth;
|
||||
|
||||
if (next <= maxWidth) {
|
||||
return (
|
||||
displayed: [...tags.take(fitCount), plus],
|
||||
hiddenCount: remaining,
|
||||
);
|
||||
}
|
||||
|
||||
fitCount--;
|
||||
remaining++;
|
||||
|
||||
usedWidth -= widths[fitCount] + (fitCount == 0 ? 0 : itemSpacing);
|
||||
}
|
||||
|
||||
// Final fallback: always show first tag only
|
||||
return (
|
||||
displayed: [tags.first],
|
||||
hiddenCount: total - 1,
|
||||
);
|
||||
}
|
||||
|
||||
static ({
|
||||
List<Label> displayed,
|
||||
int hiddenCount,
|
||||
}) safeComputeDisplayedTags({
|
||||
required List<Label> tags,
|
||||
required double maxWidth,
|
||||
required TextStyle textStyle,
|
||||
double itemSpacing = 12,
|
||||
double horizontalPadding = 4,
|
||||
double? tagMaxWidth,
|
||||
double? plusMaxWidth,
|
||||
@visibleForTesting MeasureWidthFn? measureWidth,
|
||||
}) {
|
||||
try {
|
||||
return computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: maxWidth,
|
||||
textStyle: textStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
tagMaxWidth: tagMaxWidth,
|
||||
plusMaxWidth: plusMaxWidth,
|
||||
measureWidth: measureWidth,
|
||||
);
|
||||
} catch (_, __) {
|
||||
if (tags.isEmpty) {
|
||||
return (displayed: const [], hiddenCount: 0);
|
||||
}
|
||||
|
||||
return (
|
||||
displayed: [tags.first],
|
||||
hiddenCount: tags.length - 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static ({int fitCount, double usedWidth}) _computeFitCount({
|
||||
required List<double> widths,
|
||||
required double maxWidth,
|
||||
required double itemSpacing,
|
||||
}) {
|
||||
double used = 0;
|
||||
int fit = 0;
|
||||
|
||||
for (var i = 0; i < widths.length; i++) {
|
||||
final space = (i == 0) ? 0 : itemSpacing;
|
||||
final next = used + space + widths[i];
|
||||
|
||||
if (next <= maxWidth) {
|
||||
used = next;
|
||||
fit++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (fitCount: fit, usedWidth: used);
|
||||
}
|
||||
|
||||
static Label _buildPlusLabel(int n) => Label(
|
||||
id: Id(LabelExtension.moreLabelId),
|
||||
displayName: n > 999 ? '+999+' : '+$n',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:core/presentation/utils/theme_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:labels/labels.dart';
|
||||
import 'package:tmail_ui_user/features/labels/presentation/utils/label_utils.dart';
|
||||
import 'package:tmail_ui_user/features/labels/presentation/widgets/label_widget.dart';
|
||||
|
||||
class LabelTagListWidget extends StatelessWidget {
|
||||
final List<Label> tags;
|
||||
final double horizontalPadding;
|
||||
final double itemSpacing;
|
||||
|
||||
const LabelTagListWidget({
|
||||
super.key,
|
||||
required this.tags,
|
||||
this.horizontalPadding = 4,
|
||||
this.itemSpacing = 12,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
final maxWidth = constraints.maxWidth;
|
||||
|
||||
final tagMaxWidth =
|
||||
maxWidth - LabelUtils.defaultPlusMaxWidth - itemSpacing;
|
||||
|
||||
final displayedTagsDataResult = LabelUtils.safeComputeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: maxWidth,
|
||||
textStyle: ThemeUtils.textStyleContentCaption(),
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
tagMaxWidth: tagMaxWidth,
|
||||
plusMaxWidth: LabelUtils.defaultPlusMaxWidth,
|
||||
);
|
||||
|
||||
return Wrap(
|
||||
spacing: itemSpacing,
|
||||
runSpacing: 6,
|
||||
children: displayedTagsDataResult.displayed
|
||||
.map(
|
||||
(tagDisplayed) => LabelWidget(
|
||||
label: tagDisplayed,
|
||||
horizontalPadding: horizontalPadding,
|
||||
maxWidth: tagMaxWidth,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:labels/extensions/label_extension.dart';
|
||||
import 'package:labels/model/label.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/labels/tag_widget.dart';
|
||||
|
||||
class LabelWidget extends StatelessWidget {
|
||||
final Label label;
|
||||
final double horizontalPadding;
|
||||
final double? maxWidth;
|
||||
|
||||
const LabelWidget({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.horizontalPadding = 4,
|
||||
this.maxWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TagWidget(
|
||||
text: label.safeDisplayName,
|
||||
backgroundColor: label.backgroundColor,
|
||||
textColor: label.textColor,
|
||||
horizontalPadding: horizontalPadding,
|
||||
maxWidth: maxWidth,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
|
||||
|
||||
extension ListKeywordIdentifierExtension on List<KeyWordIdentifier> {
|
||||
List<String> get keywordListString =>
|
||||
map((identifier) => identifier.value).toList();
|
||||
}
|
||||
@@ -4,4 +4,7 @@ import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
|
||||
extension MapKeywordsExtension on Map<KeyWordIdentifier, bool> {
|
||||
|
||||
Map<String, bool> toMapString() => Map.fromIterables(keys.map((keyword) => keyword.value), values);
|
||||
|
||||
List<KeyWordIdentifier> get enabledKeywords =>
|
||||
entries.where((e) => e.value).map((e) => e.key).toList();
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/base/widget/compose_floating_button.dart'
|
||||
import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wrapper.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_action_group_widget.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/report_message_banner.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/model/context_item_email_action.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.dart';
|
||||
@@ -621,20 +622,27 @@ class ThreadView extends GetWidget<ThreadController>
|
||||
presentationEmail,
|
||||
direction,
|
||||
),
|
||||
child: EmailTileBuilder(
|
||||
key: Key('email_tile_builder_${presentationEmail.id?.asString}'),
|
||||
presentationEmail: presentationEmail,
|
||||
selectAllMode: selectModeAll,
|
||||
isShowingEmailContent: isShowingEmailContent,
|
||||
isSenderImportantFlagEnabled: isSenderImportantFlagEnabled,
|
||||
searchQuery: controller.searchQuery,
|
||||
mailboxContain: presentationEmail.mailboxContain,
|
||||
isSearchEmailRunning: isSearchEmailRunning,
|
||||
isAINeedsActionEnabled: isAINeedsActionEnabled,
|
||||
emailActionClick: _handleEmailActionClicked,
|
||||
onMoreActionClick: (email, position) =>
|
||||
_handleEmailContextMenuAction(context, email, position),
|
||||
),
|
||||
child: Obx(() {
|
||||
final allLabels =
|
||||
controller.mailboxDashBoardController.labelController.labels;
|
||||
final emailLabels = presentationEmail.getLabelList(allLabels);
|
||||
|
||||
return EmailTileBuilder(
|
||||
key: Key('email_tile_builder_${presentationEmail.id?.asString}'),
|
||||
presentationEmail: presentationEmail,
|
||||
selectAllMode: selectModeAll,
|
||||
isShowingEmailContent: isShowingEmailContent,
|
||||
isSenderImportantFlagEnabled: isSenderImportantFlagEnabled,
|
||||
searchQuery: controller.searchQuery,
|
||||
mailboxContain: presentationEmail.mailboxContain,
|
||||
isSearchEmailRunning: controller.searchController.isSearchEmailRunning,
|
||||
isAINeedsActionEnabled: isAINeedsActionEnabled,
|
||||
labels: emailLabels,
|
||||
emailActionClick: _handleEmailActionClicked,
|
||||
onMoreActionClick: (email, position) =>
|
||||
_handleEmailContextMenuAction(context, email, position),
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:core/presentation/extensions/color_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:labels/model/label.dart';
|
||||
import 'package:model/email/email_action_type.dart';
|
||||
import 'package:model/email/presentation_email.dart';
|
||||
import 'package:model/mailbox/presentation_mailbox.dart';
|
||||
@@ -21,6 +22,7 @@ class EmailTileBuilder extends StatelessWidget with BaseEmailItemTile {
|
||||
final bool isShowingEmailContent;
|
||||
final bool isSenderImportantFlagEnabled;
|
||||
final bool isAINeedsActionEnabled;
|
||||
final List<Label>? labels;
|
||||
final OnPressEmailActionClick? emailActionClick;
|
||||
final OnMoreActionClick? onMoreActionClick;
|
||||
|
||||
@@ -29,6 +31,7 @@ class EmailTileBuilder extends StatelessWidget with BaseEmailItemTile {
|
||||
required this.presentationEmail,
|
||||
required this.selectAllMode,
|
||||
required this.isShowingEmailContent,
|
||||
this.labels,
|
||||
this.searchQuery,
|
||||
this.isSearchEmailRunning = false,
|
||||
this.isSenderImportantFlagEnabled = true,
|
||||
|
||||
@@ -4,12 +4,14 @@ import 'package:core/presentation/views/button/icon_button_web.dart';
|
||||
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||
import 'package:core/presentation/views/responsive/responsive_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:labels/model/label.dart';
|
||||
import 'package:model/email/email_action_type.dart';
|
||||
import 'package:model/email/presentation_email.dart';
|
||||
import 'package:model/extensions/presentation_mailbox_extension.dart';
|
||||
import 'package:model/mailbox/presentation_mailbox.dart';
|
||||
import 'package:model/mailbox/select_mode.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/labels/ai_action_tag_widget.dart';
|
||||
import 'package:tmail_ui_user/features/labels/presentation/widgets/label_list_widget.dart';
|
||||
import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart';
|
||||
import 'package:tmail_ui_user/features/thread/presentation/mixin/base_email_item_tile.dart';
|
||||
import 'package:tmail_ui_user/features/thread/presentation/widgets/desktop_list_email_action_hover_widget.dart';
|
||||
@@ -28,6 +30,7 @@ class EmailTileBuilder extends StatefulWidget {
|
||||
final bool isShowingEmailContent;
|
||||
final bool isSenderImportantFlagEnabled;
|
||||
final bool isAINeedsActionEnabled;
|
||||
final List<Label>? labels;
|
||||
final OnPressEmailActionClick? emailActionClick;
|
||||
final OnMoreActionClick? onMoreActionClick;
|
||||
|
||||
@@ -36,6 +39,7 @@ class EmailTileBuilder extends StatefulWidget {
|
||||
required this.presentationEmail,
|
||||
required this.selectAllMode,
|
||||
required this.isShowingEmailContent,
|
||||
this.labels,
|
||||
this.searchQuery,
|
||||
this.isSearchEmailRunning = false,
|
||||
this.isSenderImportantFlagEnabled = true,
|
||||
@@ -370,34 +374,102 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
|
||||
}
|
||||
|
||||
Widget _buildSubjectAndContent() {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Row(children: [
|
||||
final emailTitle = widget.presentationEmail.getEmailTitle();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (widget.presentationEmail.hasCalendarEvent)
|
||||
buildCalendarEventIcon(context: context, presentationEmail: widget.presentationEmail),
|
||||
if (widget.presentationEmail.isMarkAsImportant && widget.isSenderImportantFlagEnabled)
|
||||
buildCalendarEventIcon(
|
||||
context: context,
|
||||
presentationEmail: widget.presentationEmail,
|
||||
),
|
||||
if (widget.presentationEmail.isMarkAsImportant &&
|
||||
widget.isSenderImportantFlagEnabled)
|
||||
buildMarkAsImportantIcon(context),
|
||||
if (_shouldShowAIAction)
|
||||
const AiActionTagWidget(margin: EdgeInsetsDirectional.only(end: 12)),
|
||||
if (widget.presentationEmail.getEmailTitle().isNotEmpty)
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: constraints.maxWidth / 2),
|
||||
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||
child: buildEmailTitle(
|
||||
context,
|
||||
widget.presentationEmail,
|
||||
widget.isSearchEmailRunning,
|
||||
widget.searchQuery
|
||||
)),
|
||||
Expanded(
|
||||
child: buildEmailPartialContent(
|
||||
context,
|
||||
widget.presentationEmail,
|
||||
widget.isSearchEmailRunning,
|
||||
widget.searchQuery
|
||||
child: LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
final maxWidth = constraints.maxWidth;
|
||||
double horizontalPadding = 12;
|
||||
final maxWidthNoPadding = maxWidth - horizontalPadding * 2;
|
||||
|
||||
final labelTagsMaxWidth = emailTitle.isNotEmpty
|
||||
? maxWidthNoPadding * 0.4
|
||||
: maxWidthNoPadding * 0.5;
|
||||
|
||||
final emailTitleMaxWidth = widget.labels?.isNotEmpty == true
|
||||
? maxWidthNoPadding * 0.4
|
||||
: maxWidthNoPadding * 0.5;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (widget.labels?.isNotEmpty == true) ...[
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: labelTagsMaxWidth,
|
||||
),
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
end: horizontalPadding,
|
||||
),
|
||||
child: LabelTagListWidget(tags: widget.labels!),
|
||||
),
|
||||
if (emailTitle.isNotEmpty)
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: emailTitleMaxWidth,
|
||||
),
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
end: horizontalPadding,
|
||||
),
|
||||
child: buildEmailTitle(
|
||||
context,
|
||||
widget.presentationEmail,
|
||||
widget.isSearchEmailRunning,
|
||||
widget.searchQuery,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: buildEmailPartialContent(
|
||||
context,
|
||||
widget.presentationEmail,
|
||||
widget.isSearchEmailRunning,
|
||||
widget.searchQuery,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
if (emailTitle.isNotEmpty)
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: emailTitleMaxWidth,
|
||||
),
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
end: horizontalPadding,
|
||||
),
|
||||
child: buildEmailTitle(
|
||||
context,
|
||||
widget.presentationEmail,
|
||||
widget.isSearchEmailRunning,
|
||||
widget.searchQuery,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: buildEmailPartialContent(
|
||||
context,
|
||||
widget.presentationEmail,
|
||||
widget.isSearchEmailRunning,
|
||||
widget.searchQuery,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
]);
|
||||
});
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatarIcon({
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:labels/extensions/label_extension.dart';
|
||||
import 'package:labels/model/label.dart';
|
||||
import 'package:tmail_ui_user/features/labels/presentation/utils/label_utils.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
const testStyle = TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
letterSpacing: 0,
|
||||
fontSize: 11,
|
||||
height: 14 / 11,
|
||||
);
|
||||
|
||||
const itemSpacing = 12.0;
|
||||
const horizontalPadding = 4.0;
|
||||
|
||||
Label label(String name) => Label(displayName: name);
|
||||
|
||||
group('LabelUtils.computeDisplayedTags', () {
|
||||
test('returns empty result when tag list is empty', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [],
|
||||
maxWidth: 200,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 40,
|
||||
);
|
||||
|
||||
expect(result.displayed, isEmpty);
|
||||
expect(result.hiddenCount, 0);
|
||||
});
|
||||
|
||||
test('displays a single tag when it fits within max width', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('Completed')],
|
||||
maxWidth: 60,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 60,
|
||||
);
|
||||
|
||||
expect(result.displayed.length, 1);
|
||||
expect(result.displayed.first.safeDisplayName, 'Completed');
|
||||
expect(result.hiddenCount, 0);
|
||||
});
|
||||
|
||||
test('shows first tag when single tag does not fit', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('Completed')],
|
||||
maxWidth: 59,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 60,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'Completed');
|
||||
expect(result.hiddenCount, 0);
|
||||
});
|
||||
|
||||
test('displays all tags when both fit without overflow', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('A'), label('B')],
|
||||
maxWidth: 100,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 40,
|
||||
);
|
||||
|
||||
expect(
|
||||
result.displayed.map((e) => e.safeDisplayName),
|
||||
['A', 'B'],
|
||||
);
|
||||
expect(result.hiddenCount, 0);
|
||||
});
|
||||
|
||||
test('shows first tag when others overflow immediately', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('Completed'), label('Action'), label('Info')],
|
||||
maxWidth: 50,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 40,
|
||||
);
|
||||
|
||||
expect(
|
||||
result.displayed.map((e) => e.safeDisplayName),
|
||||
['Completed'],
|
||||
);
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('shows only first tag when +N does not fit', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('A'), label('B'), label('C')],
|
||||
maxWidth: 20,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 40,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'A');
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('shows first tag when no tag fits at all', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('Completed'), label('Action')],
|
||||
maxWidth: 10,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 50,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'Completed');
|
||||
expect(result.hiddenCount, 1);
|
||||
});
|
||||
|
||||
test('shrinks down until first tag and +N fit if possible', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('Completed'), label('Action'), label('Information')],
|
||||
maxWidth: 92,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 40,
|
||||
);
|
||||
|
||||
expect(
|
||||
result.displayed.map((e) => e.safeDisplayName),
|
||||
['Completed', '+2'],
|
||||
);
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('shows all tags when max width allows full rendering', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('Completed'), label('Action'), label('Information')],
|
||||
maxWidth: 999,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 100,
|
||||
);
|
||||
|
||||
expect(result.displayed.length, 3);
|
||||
expect(result.hiddenCount, 0);
|
||||
});
|
||||
|
||||
test('falls back to first tag when second tag is too long', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('Completed'), label('LONG TEXT'), label('A')],
|
||||
maxWidth: 60,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (label) =>
|
||||
label.safeDisplayName == 'Completed' ? 50 : 200,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'Completed');
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('still only shows first tag when shrinking cannot allow +N', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('Completed'), label('Action'), label('Information')],
|
||||
maxWidth: 90,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 60,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'Completed');
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('shows first tag when +N exactly matches max width', () {
|
||||
final tags = List.generate(4, (i) => label('T$i'));
|
||||
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: 30,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 30,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'T0');
|
||||
expect(result.hiddenCount, 3);
|
||||
});
|
||||
|
||||
test('shows first tag and +N when both fit', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('Completed'), label('Action'), label('Info')],
|
||||
maxWidth: 100,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (label) => label.safeDisplayName == 'Completed' ? 40 : 30,
|
||||
);
|
||||
|
||||
expect(
|
||||
result.displayed.map((e) => e.safeDisplayName),
|
||||
['Completed', '+2'],
|
||||
);
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('shows only first tag when shrink still exceeds max width', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('Completed'), label('Action'), label('Info')],
|
||||
maxWidth: 39,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 40,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'Completed');
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('handles large hidden counts correctly using +N logic', () {
|
||||
final tags = List.generate(50, (i) => label('Tag$i'));
|
||||
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: 999,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 500,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'Tag0');
|
||||
expect(result.hiddenCount, 49);
|
||||
});
|
||||
|
||||
test('treats null displayName as empty and keeps first tag', () {
|
||||
final tags = [
|
||||
Label(displayName: null),
|
||||
Label(displayName: 'A'),
|
||||
];
|
||||
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: 50,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (label) => label.safeDisplayName.isEmpty ? 10 : 40,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, '');
|
||||
expect(result.hiddenCount, 1);
|
||||
});
|
||||
|
||||
test('correctly fits tags when spacing is zero', () {
|
||||
final tags = [label('A'), label('B'), label('C')];
|
||||
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: 80,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: 0,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 40,
|
||||
);
|
||||
|
||||
expect(
|
||||
result.displayed.map((e) => e.safeDisplayName),
|
||||
['A', '+2'],
|
||||
);
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('handles RTL labels the same as LTR while keeping first tag', () {
|
||||
final tags = [
|
||||
label('שלום'),
|
||||
label('مرحبا'),
|
||||
label('Hello'),
|
||||
];
|
||||
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: 80,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 40,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'שלום');
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('shows first tag when first label is extremely long', () {
|
||||
final tags = [
|
||||
label('THIS_IS_A_VERY_LONG_LABEL_THAT_WILL_NEVER_FIT'),
|
||||
label('A'),
|
||||
label('B'),
|
||||
];
|
||||
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: 60,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (label) =>
|
||||
label.safeDisplayName.startsWith('THIS') ? 500 : 40,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName,
|
||||
'THIS_IS_A_VERY_LONG_LABEL_THAT_WILL_NEVER_FIT');
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('computes remaining count correctly with mixed label widths', () {
|
||||
final tags = [
|
||||
label('A'),
|
||||
label('BBBB'),
|
||||
label('CC'),
|
||||
label('DDDDDD'),
|
||||
];
|
||||
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: 95,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (label) {
|
||||
switch (label.safeDisplayName.length) {
|
||||
case 1:
|
||||
return 30;
|
||||
case 2:
|
||||
return 40;
|
||||
case 4:
|
||||
return 60;
|
||||
default:
|
||||
return 100;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.displayed.first.safeDisplayName, 'A');
|
||||
expect(result.hiddenCount, 3);
|
||||
});
|
||||
|
||||
test('shows first tag when both tag and +1 cannot fit', () {
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: [label('A')],
|
||||
maxWidth: 10,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 20,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'A');
|
||||
expect(result.hiddenCount, 0);
|
||||
});
|
||||
|
||||
test('handles duplicate labels consistently and keeps first tag', () {
|
||||
final tags = [
|
||||
label('A'),
|
||||
label('A'),
|
||||
label('A'),
|
||||
];
|
||||
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: 45,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 40,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'A');
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('safeComputeDisplayedTags falls back gracefully on exception', () {
|
||||
final result = LabelUtils.safeComputeDisplayedTags(
|
||||
tags: [Label(displayName: 'A'), Label(displayName: 'B')],
|
||||
maxWidth: 100,
|
||||
textStyle: const TextStyle(),
|
||||
measureWidth: (_) => throw Exception('boom'),
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'A');
|
||||
expect(result.hiddenCount, 1);
|
||||
});
|
||||
|
||||
test('respects tagMaxWidth by clamping oversized label width', () {
|
||||
final tags = [
|
||||
label('SUPER_LONG_TAG_NAME'),
|
||||
label('B'),
|
||||
label('C'),
|
||||
];
|
||||
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: 80,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
tagMaxWidth: 40,
|
||||
measureWidth: (_) => 200,
|
||||
);
|
||||
|
||||
expect(result.displayed.single.safeDisplayName, 'SUPER_LONG_TAG_NAME');
|
||||
expect(result.hiddenCount, 2);
|
||||
});
|
||||
|
||||
test('respects plusMaxWidth to prevent +N from exceeding layout', () {
|
||||
final tags = [
|
||||
label('A'),
|
||||
label('B'),
|
||||
label('C'),
|
||||
label('D'),
|
||||
];
|
||||
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: 62,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
plusMaxWidth: 20,
|
||||
measureWidth: (label) =>
|
||||
label.safeDisplayName.startsWith('+') ? 50 : 30,
|
||||
);
|
||||
|
||||
expect(
|
||||
result.displayed.map((e) => e.safeDisplayName),
|
||||
['A', '+3'],
|
||||
);
|
||||
expect(result.hiddenCount, 3);
|
||||
});
|
||||
|
||||
test('performs efficiently with 1k labels', () {
|
||||
final tags = List.generate(1000, (i) => label('Tag$i'));
|
||||
|
||||
final watch = Stopwatch()..start();
|
||||
|
||||
final result = LabelUtils.computeDisplayedTags(
|
||||
tags: tags,
|
||||
maxWidth: 300,
|
||||
textStyle: testStyle,
|
||||
itemSpacing: itemSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
measureWidth: (_) => 30,
|
||||
);
|
||||
|
||||
watch.stop();
|
||||
|
||||
expect(result.displayed.isNotEmpty, true);
|
||||
expect(result.hiddenCount, greaterThan(0));
|
||||
|
||||
expect(
|
||||
watch.elapsedMilliseconds < 50,
|
||||
true,
|
||||
reason: 'computeDisplayedTags took too long',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+5
@@ -457,6 +457,7 @@ void main() {
|
||||
when(mailboxDashboardController.spamReportController.spamReportState).thenReturn(Rx(SpamReportState.disabled));
|
||||
when(mailboxDashboardController.spamReportController.presentationSpamMailbox).thenReturn(Rxn(null));
|
||||
when(mailboxDashboardController.downloadController.listDownloadTaskState).thenReturn(RxList([]));
|
||||
when(mailboxDashboardController.labelController.labels).thenReturn(RxList([]));
|
||||
when(composerManager.composers).thenReturn(RxMap({}));
|
||||
|
||||
final widget = makeTestableWidget(child: MailboxDashBoardView());
|
||||
@@ -533,6 +534,7 @@ void main() {
|
||||
when(mailboxDashboardController.spamReportController.spamReportState).thenReturn(Rx(SpamReportState.disabled));
|
||||
when(mailboxDashboardController.spamReportController.presentationSpamMailbox).thenReturn(Rxn(null));
|
||||
when(mailboxDashboardController.downloadController.listDownloadTaskState).thenReturn(RxList([]));
|
||||
when(mailboxDashboardController.labelController.labels).thenReturn(RxList([]));
|
||||
when(composerManager.composers).thenReturn(RxMap({}));
|
||||
|
||||
final widget = makeTestableWidget(child: MailboxDashBoardView());
|
||||
@@ -596,6 +598,7 @@ void main() {
|
||||
when(mailboxDashboardController.spamReportController.spamReportState).thenReturn(Rx(SpamReportState.disabled));
|
||||
when(mailboxDashboardController.spamReportController.presentationSpamMailbox).thenReturn(Rxn(null));
|
||||
when(mailboxDashboardController.downloadController.listDownloadTaskState).thenReturn(RxList([]));
|
||||
when(mailboxDashboardController.labelController.labels).thenReturn(RxList([]));
|
||||
when(composerManager.composers).thenReturn(RxMap({}));
|
||||
|
||||
final widget = makeTestableWidget(child: MailboxDashBoardView());
|
||||
@@ -656,6 +659,7 @@ void main() {
|
||||
when(mailboxDashboardController.spamReportController.spamReportState).thenReturn(Rx(SpamReportState.disabled));
|
||||
when(mailboxDashboardController.spamReportController.presentationSpamMailbox).thenReturn(Rxn(null));
|
||||
when(mailboxDashboardController.downloadController.listDownloadTaskState).thenReturn(RxList([]));
|
||||
when(mailboxDashboardController.labelController.labels).thenReturn(RxList([]));
|
||||
when(composerManager.composers).thenReturn(RxMap({}));
|
||||
|
||||
final widget = makeTestableWidget(child: MailboxDashBoardView());
|
||||
@@ -712,6 +716,7 @@ void main() {
|
||||
when(mailboxDashboardController.spamReportController.spamReportState).thenReturn(Rx(SpamReportState.disabled));
|
||||
when(mailboxDashboardController.spamReportController.presentationSpamMailbox).thenReturn(Rxn(null));
|
||||
when(mailboxDashboardController.downloadController.listDownloadTaskState).thenReturn(RxList([]));
|
||||
when(mailboxDashboardController.labelController.labels).thenReturn(RxList([]));
|
||||
when(composerManager.composers).thenReturn(RxMap({}));
|
||||
|
||||
final widget = makeTestableWidget(child: MailboxDashBoardView());
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
|
||||
import 'package:labels/model/label.dart';
|
||||
import 'package:model/email/presentation_email.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart';
|
||||
|
||||
void main() {
|
||||
group('getLabelList', () {
|
||||
test('returns empty when keywords is null', () {
|
||||
final email = PresentationEmail(keywords: null);
|
||||
final labels = [Label(keyword: KeyWordIdentifier.emailFlagged.value)];
|
||||
|
||||
final result = email.getLabelList(labels);
|
||||
|
||||
expect(result, isEmpty);
|
||||
});
|
||||
|
||||
test('returns empty when keywords is empty', () {
|
||||
final email = PresentationEmail(keywords: {});
|
||||
final labels = [Label(keyword: KeyWordIdentifier.emailFlagged.value)];
|
||||
|
||||
final result = email.getLabelList(labels);
|
||||
|
||||
expect(result, isEmpty);
|
||||
});
|
||||
|
||||
test('returns empty when labels is empty', () {
|
||||
final email = PresentationEmail(keywords: {
|
||||
KeyWordIdentifier.emailFlagged: true,
|
||||
});
|
||||
|
||||
final result = email.getLabelList([]);
|
||||
|
||||
expect(result, isEmpty);
|
||||
});
|
||||
|
||||
test('returns empty when no enabled keywords', () {
|
||||
final email = PresentationEmail(keywords: {
|
||||
KeyWordIdentifier.emailFlagged: false,
|
||||
KeyWordIdentifier.emailDraft: false,
|
||||
});
|
||||
|
||||
final labels = [
|
||||
Label(keyword: KeyWordIdentifier.emailFlagged.value),
|
||||
Label(keyword: KeyWordIdentifier.emailDraft.value),
|
||||
];
|
||||
|
||||
final result = email.getLabelList(labels);
|
||||
|
||||
expect(result, isEmpty);
|
||||
});
|
||||
|
||||
test('returns only labels with enabled keywords', () {
|
||||
final email = PresentationEmail(keywords: {
|
||||
KeyWordIdentifier.emailFlagged: true,
|
||||
KeyWordIdentifier.emailSeen: false,
|
||||
KeyWordIdentifier.emailDraft: true,
|
||||
});
|
||||
|
||||
final labels = [
|
||||
Label(keyword: KeyWordIdentifier.emailFlagged.value),
|
||||
Label(keyword: KeyWordIdentifier.emailSeen.value),
|
||||
Label(keyword: KeyWordIdentifier.emailDraft.value),
|
||||
];
|
||||
|
||||
final result = email.getLabelList(labels);
|
||||
|
||||
expect(result.length, 2);
|
||||
expect(
|
||||
result.map((l) => l.keyword),
|
||||
contains(KeyWordIdentifier.emailFlagged.value),
|
||||
);
|
||||
expect(
|
||||
result.map((l) => l.keyword),
|
||||
contains(KeyWordIdentifier.emailDraft.value),
|
||||
);
|
||||
expect(
|
||||
result.map((l) => l.keyword),
|
||||
isNot(contains(KeyWordIdentifier.emailSeen.value)),
|
||||
);
|
||||
});
|
||||
|
||||
test('ignores labels where keyword is null', () {
|
||||
final email = PresentationEmail(keywords: {
|
||||
KeyWordIdentifier.emailFlagged: true,
|
||||
});
|
||||
|
||||
final labels = [
|
||||
Label(),
|
||||
Label(keyword: KeyWordIdentifier.emailFlagged.value),
|
||||
];
|
||||
|
||||
final result = email.getLabelList(labels);
|
||||
|
||||
expect(result.length, 1);
|
||||
expect(result.first.keyword, KeyWordIdentifier.emailFlagged.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user