Change profile options modal

This commit is contained in:
problematicconsumer
2024-01-16 14:32:30 +03:30
parent 46107f2b5f
commit c182c46638
4 changed files with 325 additions and 92 deletions

View File

@@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hiddify/utils/platform_utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:wolt_modal_sheet/wolt_modal_sheet.dart';
typedef AdaptiveMenuBuilder = Widget Function(
BuildContext context,
void Function() toggleVisibility,
Widget? child,
);
class AdaptiveMenuItem<T> {
AdaptiveMenuItem({
required this.title,
this.icon,
this.onTap,
this.isSelected,
this.subItems,
});
final String title;
final IconData? icon;
final T Function()? onTap;
final bool? isSelected;
final List<AdaptiveMenuItem>? subItems;
(String, IconData?, T Function()?, bool?, List<AdaptiveMenuItem>?)
_equality() => (title, icon, onTap, isSelected, subItems);
@override
bool operator ==(covariant AdaptiveMenuItem other) {
if (identical(this, other)) return true;
return other._equality() == _equality();
}
@override
int get hashCode => _equality().hashCode;
}
class AdaptiveMenu extends HookConsumerWidget {
const AdaptiveMenu({
super.key,
required this.items,
required this.builder,
required this.child,
});
final Iterable<AdaptiveMenuItem> items;
final AdaptiveMenuBuilder builder;
final Widget? child;
@override
Widget build(BuildContext context, WidgetRef ref) {
if (PlatformUtils.isDesktop) {
List<Widget> buildMenuItems(Iterable<AdaptiveMenuItem> scopeItems) {
final menuItems = <Widget>[];
for (final item in scopeItems) {
if (item.subItems != null) {
final subItems = buildMenuItems(item.subItems!);
menuItems.add(
SubmenuButton(
menuChildren: subItems,
leadingIcon: item.icon != null ? Icon(item.icon) : null,
child: Text(item.title),
),
);
} else {
menuItems.add(
MenuItemButton(
leadingIcon: item.icon != null ? Icon(item.icon) : null,
onPressed: item.onTap,
child: Text(item.title),
),
);
}
}
return menuItems;
}
return MenuAnchor(
builder: (context, controller, child) => builder(
context,
() {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child,
),
menuChildren: buildMenuItems(items),
child: child,
);
}
final pageIndexNotifier = useValueNotifier(0);
final nestedSheets = <SliverWoltModalSheetPage>[];
int pageIndex = 0;
void popSheets() {
if (context.mounted) {
Navigator.pop(context);
}
Future.delayed(const Duration(milliseconds: 200))
.then((_) => pageIndexNotifier.value = 0);
}
List<Widget> buildSheetItems(
Iterable<AdaptiveMenuItem> menuItems,
int index,
) {
final sheetItems = <Widget>[];
for (final item in menuItems) {
if (item.subItems != null) {
final subItems = buildSheetItems(item.subItems!, index + 1);
final subSheetIndex = ++pageIndex;
sheetItems.add(
ListTile(
title: Text(item.title),
leading: item.icon != null ? Icon(item.icon) : null,
trailing: const Icon(Icons.chevron_right),
onTap: () {
pageIndexNotifier.value = subSheetIndex;
},
),
);
nestedSheets.add(
SliverWoltModalSheetPage(
hasTopBarLayer: false,
isTopBarLayerAlwaysVisible: true,
topBarTitle: Text(item.title),
mainContentSlivers: [
SliverList.list(children: subItems),
],
),
);
} else {
sheetItems.add(
ListTile(
title: Text(item.title),
leading: item.icon != null ? Icon(item.icon) : null,
onTap: () async {
popSheets();
await item.onTap!();
},
),
);
}
}
return sheetItems;
}
return builder(
context,
() async {
await WoltModalSheet.show(
context: context,
pageIndexNotifier: pageIndexNotifier,
onModalDismissedWithDrag: popSheets,
onModalDismissedWithBarrierTap: popSheets,
useSafeArea: true,
showDragHandle: false,
pageListBuilder: (context) => [
SliverWoltModalSheetPage(
hasTopBarLayer: false,
mainContentSlivers: [
SliverList.list(children: buildSheetItems(items, 0)),
],
),
...nestedSheets,
],
);
},
child,
);
}
}

View File

@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/failures.dart';
import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/core/widget/adaptive_menu.dart';
import 'package:hiddify/features/common/confirmation_dialogs.dart';
import 'package:hiddify/features/common/qr_code_dialog.dart';
import 'package:hiddify/features/profile/model/profile_entity.dart';
@@ -202,19 +203,13 @@ class ProfileActionButton extends HookConsumerWidget {
}
return ProfileActionsMenu(
profile,
(context, controller, child) {
(context, toggleVisibility, _) {
return Semantics(
button: true,
child: Tooltip(
message: MaterialLocalizations.of(context).showMenuTooltip,
child: InkWell(
onTap: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
onTap: toggleVisibility,
child: const Icon(Icons.more_vert),
),
),
@@ -228,7 +223,7 @@ class ProfileActionsMenu extends HookConsumerWidget {
const ProfileActionsMenu(this.profile, this.builder, {super.key, this.child});
final ProfileEntity profile;
final MenuAnchorChildBuilder builder;
final AdaptiveMenuBuilder builder;
final Widget? child;
@override
@@ -249,97 +244,99 @@ class ProfileActionsMenu extends HookConsumerWidget {
},
);
return MenuAnchor(
builder: builder,
menuChildren: [
if (profile case RemoteProfileEntity())
MenuItemButton(
leadingIcon: const Icon(Icons.update),
child: Text(t.profile.update.buttonTxt),
onPressed: () {
if (ref.read(updateProfileProvider(profile.id)).isLoading) {
return;
}
ref
.read(updateProfileProvider(profile.id).notifier)
.updateProfile(profile as RemoteProfileEntity);
},
),
SubmenuButton(
menuChildren: [
if (profile case RemoteProfileEntity(:final url, :final name)) ...[
MenuItemButton(
child: Text(t.profile.share.exportSubLinkToClipboard),
onPressed: () async {
final link = LinkParser.generateSubShareLink(url, name);
if (link.isNotEmpty) {
await Clipboard.setData(ClipboardData(text: link));
if (context.mounted) {
CustomToast(t.profile.share.exportToClipboardSuccess)
.show(context);
}
final menuItems = [
if (profile case RemoteProfileEntity())
AdaptiveMenuItem(
title: t.profile.update.buttonTxt,
icon: Icons.update,
onTap: () {
if (ref.read(updateProfileProvider(profile.id)).isLoading) {
return;
}
ref
.read(updateProfileProvider(profile.id).notifier)
.updateProfile(profile as RemoteProfileEntity);
},
),
AdaptiveMenuItem(
title: t.profile.share.buttonText,
icon: Icons.share,
subItems: [
if (profile case RemoteProfileEntity(:final url, :final name)) ...[
AdaptiveMenuItem(
title: t.profile.share.exportSubLinkToClipboard,
onTap: () async {
final link = LinkParser.generateSubShareLink(url, name);
if (link.isNotEmpty) {
await Clipboard.setData(ClipboardData(text: link));
if (context.mounted) {
CustomToast(t.profile.share.exportToClipboardSuccess)
.show(context);
}
},
),
MenuItemButton(
child: Text(t.profile.share.subLinkQrCode),
onPressed: () async {
final link = LinkParser.generateSubShareLink(url, name);
if (link.isNotEmpty) {
await QrCodeDialog(
link,
message: name,
).show(context);
}
},
),
],
MenuItemButton(
child: Text(t.profile.share.exportConfigToClipboard),
onPressed: () async {
if (exportConfigMutation.state.isInProgress) {
return;
}
exportConfigMutation.setFuture(
ref
.read(profilesOverviewNotifierProvider.notifier)
.exportConfigToClipboard(profile),
);
},
),
AdaptiveMenuItem(
title: t.profile.share.subLinkQrCode,
onTap: () async {
final link = LinkParser.generateSubShareLink(url, name);
if (link.isNotEmpty) {
await QrCodeDialog(
link,
message: name,
).show(context);
}
},
),
],
leadingIcon: const Icon(Icons.share),
child: Text(t.profile.share.buttonText),
),
MenuItemButton(
leadingIcon: const Icon(Icons.edit),
child: Text(t.profile.edit.buttonTxt),
onPressed: () async {
await ProfileDetailsRoute(profile.id).push(context);
},
),
MenuItemButton(
leadingIcon: const Icon(Icons.delete),
child: Text(t.profile.delete.buttonTxt),
onPressed: () async {
if (deleteProfileMutation.state.isInProgress) {
return;
}
final deleteConfirmed = await showConfirmationDialog(
context,
title: t.profile.delete.buttonTxt,
message: t.profile.delete.confirmationMsg,
);
if (deleteConfirmed) {
deleteProfileMutation.setFuture(
AdaptiveMenuItem(
title: t.profile.share.exportConfigToClipboard,
onTap: () async {
if (exportConfigMutation.state.isInProgress) {
return;
}
exportConfigMutation.setFuture(
ref
.read(profilesOverviewNotifierProvider.notifier)
.deleteProfile(profile),
.exportConfigToClipboard(profile),
);
}
},
),
],
},
),
],
),
AdaptiveMenuItem(
icon: Icons.edit,
title: t.profile.edit.buttonTxt,
onTap: () async {
await ProfileDetailsRoute(profile.id).push(context);
},
),
AdaptiveMenuItem(
icon: Icons.delete,
title: t.profile.delete.buttonTxt,
onTap: () async {
if (deleteProfileMutation.state.isInProgress) {
return;
}
final deleteConfirmed = await showConfirmationDialog(
context,
title: t.profile.delete.buttonTxt,
message: t.profile.delete.confirmationMsg,
);
if (deleteConfirmed) {
deleteProfileMutation.setFuture(
ref
.read(profilesOverviewNotifierProvider.notifier)
.deleteProfile(profile),
);
}
},
),
];
return AdaptiveMenu(
builder: builder,
items: menuItems,
child: child,
);
}

View File

@@ -486,6 +486,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.20.4"
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_localizations:
dependency: "direct main"
description: flutter
@@ -1642,6 +1690,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.7"
wolt_modal_sheet:
dependency: "direct main"
description:
name: wolt_modal_sheet
sha256: "5dcf57ac13bf2614a38ea3e51ed9ca1883bc2368d0e19b8c0cd53bb1467ffeea"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
xdg_directories:
dependency: transitive
description:

View File

@@ -70,6 +70,7 @@ dependencies:
flutter_loggy_dio: ^3.0.1
dio_smart_retry: ^6.0.0
cupertino_http: ^1.2.0
wolt_modal_sheet: ^0.3.0
dev_dependencies:
flutter_test: