Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions assets/images/ic_thumbs_up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions core/lib/presentation/resources/image_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ class ImagePaths {
String get icUser => _getImagePath('ic_user.svg');
String get icTag => _getImagePath('ic_tag.svg');
String get icColorPicker => _getImagePath('ic_color_picker.svg');
String get icThumbsUp => _getImagePath('ic_thumbs_up.svg');

String _getImagePath(String imageName) {
return AssetsPaths.images + imageName;
Expand Down
14 changes: 14 additions & 0 deletions labels/lib/converter/keyword_identifier_nullable_converter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:json_annotation/json_annotation.dart';

class KeywordIdentifierNullableConverter
implements JsonConverter<KeyWordIdentifier?, String?> {
const KeywordIdentifierNullableConverter();

@override
KeyWordIdentifier? fromJson(String? json) =>
json != null ? KeyWordIdentifier(json) : null;

@override
String? toJson(KeyWordIdentifier? object) => object?.value;
}
3 changes: 2 additions & 1 deletion labels/lib/extensions/label_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/extensions/hex_color_extension.dart';
import 'package:flutter/material.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:labels/model/hex_color.dart';
import 'package:labels/model/label.dart';

Expand All @@ -28,7 +29,7 @@ extension LabelExtension on Label {

Label copyWith({
Id? id,
String? keyword,
KeyWordIdentifier? keyword,
String? displayName,
HexColor? color,
}) {
Expand Down
5 changes: 4 additions & 1 deletion labels/lib/model/label.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/http/converter/id_converter.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:labels/converter/hex_color_nullable_converter.dart';
import 'package:labels/converter/keyword_identifier_nullable_converter.dart';
import 'package:labels/model/hex_color.dart';

part 'label.g.dart';
Expand All @@ -13,11 +15,12 @@ part 'label.g.dart';
converters: [
IdConverter(),
HexColorNullableConverter(),
KeywordIdentifierNullableConverter(),
],
)
class Label with EquatableMixin {
final Id? id;
final String? keyword;
final KeyWordIdentifier? keyword;
final String? displayName;
final HexColor? color;

Expand Down
7 changes: 4 additions & 3 deletions labels/test/method/get/get_label_method_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:http_mock_adapter/http_mock_adapter.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:labels/labels.dart';

import '../method_fixtures.dart';
Expand Down Expand Up @@ -41,13 +42,13 @@ void main() {

final labelA = Label(
id: Id('A'),
keyword: 'labelA',
keyword: KeyWordIdentifier('labelA'),
displayName: 'Label A',
color: HexColor('#111111'),
);
final labelB = Label(
id: Id('B'),
keyword: 'labelB',
keyword: KeyWordIdentifier('labelB'),
displayName: 'Label B',
color: HexColor('#222222'),
);
Expand Down Expand Up @@ -205,7 +206,7 @@ void main() {
expect(parsed.notFound, isEmpty);
});

test('should throw DioException when server returns 500', () async {
test('should throw DioError when server returns 500', () async {
// Arrange
final dio = createDio();
final adapter = DioAdapter(dio: dio);
Expand Down
6 changes: 3 additions & 3 deletions labels/test/method/set/set_label_method_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ void main() {

// Assert
expect(parsed, isNotNull);
expect(parsed!.created![Id('4f29')]!.keyword, equals('important'));
expect(parsed!.created![Id('4f29')]!.keyword?.value, equals('important'));
});

test('should process multiple created labels', () async {
Expand Down Expand Up @@ -143,8 +143,8 @@ void main() {
// Assert
expect(parsed, isNotNull);
expect(parsed!.created!.length, equals(2));
expect(parsed.created![Id('A')]!.keyword, equals('tagA'));
expect(parsed.created![Id('B')]!.keyword, equals('tagB'));
expect(parsed.created![Id('A')]!.keyword?.value, equals('tagA'));
expect(parsed.created![Id('B')]!.keyword?.value, equals('tagB'));
});

test('should throw DioException when backend returns 500', () async {
Expand Down
4 changes: 2 additions & 2 deletions lib/features/base/base_mailbox_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ abstract class BaseMailboxController extends BaseController
);

final destinationMailbox = PlatformInfo.isWeb
? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments)
? await DialogRouter().pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments)
: await push(AppRoutes.destinationPicker, arguments: arguments);

if (destinationMailbox is PresentationMailbox) {
Expand Down Expand Up @@ -658,7 +658,7 @@ abstract class BaseMailboxController extends BaseController
);

final destinationMailbox = PlatformInfo.isWeb
? await DialogRouter.pushGeneralDialog(
? await DialogRouter().pushGeneralDialog(
routeName: AppRoutes.destinationPicker,
arguments: arguments,
)
Expand Down
31 changes: 27 additions & 4 deletions lib/features/base/model/popup_menu_item_action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';

typedef OnPopupMenuActionClick = void Function(PopupMenuItemAction action);
typedef OnHoverShowSubmenu = void Function(GlobalKey key);

abstract class PopupMenuItemAction<T> with EquatableMixin {
final T action;
final String? key;
final int category;
final Widget? submenu;

PopupMenuItemAction(this.action, {this.key, this.category = -1});
PopupMenuItemAction(
this.action, {
this.key,
this.category = -1,
this.submenu,
});

@override
List<Object?> get props => [action, key, category];
List<Object?> get props => [action, key, category, submenu];

String get actionName;

Expand Down Expand Up @@ -40,9 +47,22 @@ mixin OptionalPopupSelectedIcon<T> {
double get selectedIconSize => 16.0;
}

mixin OptionalPopupHoverIcon {
String get hoverIcon;

Color get hoverIconColor => AppColor.steelGrayA540;

double get hoverIconSize => 16.0;
}

abstract class PopupMenuItemActionRequiredIcon<T> extends PopupMenuItemAction<T>
with OptionalPopupIcon {
PopupMenuItemActionRequiredIcon(super.action, {super.key, super.category});
with OptionalPopupIcon, OptionalPopupHoverIcon {
PopupMenuItemActionRequiredIcon(
super.action, {
super.key,
super.category,
super.submenu,
});
}

abstract class PopupMenuItemActionRequiredSelectedIcon<T>
Expand All @@ -54,6 +74,7 @@ abstract class PopupMenuItemActionRequiredSelectedIcon<T>
this.selectedAction, {
super.key,
super.category,
super.submenu,
});
}

Expand All @@ -66,6 +87,7 @@ abstract class PopupMenuItemActionRequiredFull<T> extends PopupMenuItemAction<T>
this.selectedAction, {
super.key,
super.category,
super.submenu,
});
}

Expand All @@ -79,5 +101,6 @@ abstract class PopupMenuItemActionRequiredIconWithMultipleSelected<T>
this.selectedActions, {
super.key,
super.category,
super.submenu,
});
}
42 changes: 42 additions & 0 deletions lib/features/base/widget/popup_menu/hover_submenu_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'dart:async';

import 'package:flutter/foundation.dart';

class HoverSubmenuController {
HoverSubmenuController({
this.exitDelay = const Duration(milliseconds: 120),
});

final Duration exitDelay;

final ValueNotifier<bool> isHovering = ValueNotifier(false);

int _hoverRefCount = 0;
Timer? _exitTimer;

void enter() {
_exitTimer?.cancel();
_hoverRefCount++;
if (!isHovering.value) {
isHovering.value = true;
}
}

void exit() {
_hoverRefCount = (_hoverRefCount - 1).clamp(0, 999);

if (_hoverRefCount == 0) {
_exitTimer?.cancel();
_exitTimer = Timer(exitDelay, () {
if (_hoverRefCount == 0) {
isHovering.value = false;
}
});
}
}

void dispose() {
_exitTimer?.cancel();
isHovering.dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import 'package:tmail_ui_user/features/base/extensions/popup_menu_action_list_ex
import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart';
import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart';
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart';
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_submenu_controller.dart';

typedef OnPopupMenuActionSelected = void Function(PopupMenuItemAction action);

class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
final List<PopupMenuItemAction> actions;
final OnPopupMenuActionSelected onActionSelected;
final double dividerOpacity;
final PopupSubmenuController? submenuController;

const PopupMenuActionGroupWidget({
PopupMenuActionGroupWidget({
required this.actions,
required this.onActionSelected,
this.dividerOpacity = 0.12,
this.submenuController,
});

Future<void> show(
Expand All @@ -34,9 +37,19 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
child: PopupMenuItemActionWidget(
menuAction: menuAction,
menuActionClick: (menuAction) {
submenuController?.hide();
Navigator.pop(context);
onActionSelected(menuAction);
},
onHoverShowSubmenu: submenuController != null && menuAction.submenu != null
? (itemKey) => _showPopupSubmenu(
context: context,
itemKey: itemKey,
submenuController: submenuController!,
submenu: menuAction.submenu!,
)
: null,
onHoverOtherItem: submenuController?.hide,
),
),
),
Expand All @@ -48,6 +61,30 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
],
];

return openPopupMenuAction(context, position, popupMenuItems);
try {
await openPopupMenuAction(context, position, popupMenuItems);
} finally {
submenuController?.hide();
}
}

void _showPopupSubmenu({
required BuildContext context,
required GlobalKey itemKey,
required PopupSubmenuController submenuController,
required Widget submenu,
}) {
final renderObject = itemKey.currentContext?.findRenderObject();
if (renderObject is! RenderBox) return;
final renderBox = renderObject;

final offset = renderBox.localToGlobal(Offset.zero);
final rect = offset & renderBox.size;

submenuController.show(
context: context,
anchor: rect,
submenu: submenu,
);
}
}
Loading
Loading