Change profile options modal
This commit is contained in:
179
lib/core/widget/adaptive_menu.dart
Normal file
179
lib/core/widget/adaptive_menu.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hiddify/core/localization/translations.dart';
|
import 'package:hiddify/core/localization/translations.dart';
|
||||||
import 'package:hiddify/core/model/failures.dart';
|
import 'package:hiddify/core/model/failures.dart';
|
||||||
import 'package:hiddify/core/router/router.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/confirmation_dialogs.dart';
|
||||||
import 'package:hiddify/features/common/qr_code_dialog.dart';
|
import 'package:hiddify/features/common/qr_code_dialog.dart';
|
||||||
import 'package:hiddify/features/profile/model/profile_entity.dart';
|
import 'package:hiddify/features/profile/model/profile_entity.dart';
|
||||||
@@ -202,19 +203,13 @@ class ProfileActionButton extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
return ProfileActionsMenu(
|
return ProfileActionsMenu(
|
||||||
profile,
|
profile,
|
||||||
(context, controller, child) {
|
(context, toggleVisibility, _) {
|
||||||
return Semantics(
|
return Semantics(
|
||||||
button: true,
|
button: true,
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: MaterialLocalizations.of(context).showMenuTooltip,
|
message: MaterialLocalizations.of(context).showMenuTooltip,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: toggleVisibility,
|
||||||
if (controller.isOpen) {
|
|
||||||
controller.close();
|
|
||||||
} else {
|
|
||||||
controller.open();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.more_vert),
|
child: const Icon(Icons.more_vert),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -228,7 +223,7 @@ class ProfileActionsMenu extends HookConsumerWidget {
|
|||||||
const ProfileActionsMenu(this.profile, this.builder, {super.key, this.child});
|
const ProfileActionsMenu(this.profile, this.builder, {super.key, this.child});
|
||||||
|
|
||||||
final ProfileEntity profile;
|
final ProfileEntity profile;
|
||||||
final MenuAnchorChildBuilder builder;
|
final AdaptiveMenuBuilder builder;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -249,97 +244,99 @@ class ProfileActionsMenu extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return MenuAnchor(
|
final menuItems = [
|
||||||
builder: builder,
|
if (profile case RemoteProfileEntity())
|
||||||
menuChildren: [
|
AdaptiveMenuItem(
|
||||||
if (profile case RemoteProfileEntity())
|
title: t.profile.update.buttonTxt,
|
||||||
MenuItemButton(
|
icon: Icons.update,
|
||||||
leadingIcon: const Icon(Icons.update),
|
onTap: () {
|
||||||
child: Text(t.profile.update.buttonTxt),
|
if (ref.read(updateProfileProvider(profile.id)).isLoading) {
|
||||||
onPressed: () {
|
return;
|
||||||
if (ref.read(updateProfileProvider(profile.id)).isLoading) {
|
}
|
||||||
return;
|
ref
|
||||||
}
|
.read(updateProfileProvider(profile.id).notifier)
|
||||||
ref
|
.updateProfile(profile as RemoteProfileEntity);
|
||||||
.read(updateProfileProvider(profile.id).notifier)
|
},
|
||||||
.updateProfile(profile as RemoteProfileEntity);
|
),
|
||||||
},
|
AdaptiveMenuItem(
|
||||||
),
|
title: t.profile.share.buttonText,
|
||||||
SubmenuButton(
|
icon: Icons.share,
|
||||||
menuChildren: [
|
subItems: [
|
||||||
if (profile case RemoteProfileEntity(:final url, :final name)) ...[
|
if (profile case RemoteProfileEntity(:final url, :final name)) ...[
|
||||||
MenuItemButton(
|
AdaptiveMenuItem(
|
||||||
child: Text(t.profile.share.exportSubLinkToClipboard),
|
title: t.profile.share.exportSubLinkToClipboard,
|
||||||
onPressed: () async {
|
onTap: () async {
|
||||||
final link = LinkParser.generateSubShareLink(url, name);
|
final link = LinkParser.generateSubShareLink(url, name);
|
||||||
if (link.isNotEmpty) {
|
if (link.isNotEmpty) {
|
||||||
await Clipboard.setData(ClipboardData(text: link));
|
await Clipboard.setData(ClipboardData(text: link));
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
CustomToast(t.profile.share.exportToClipboardSuccess)
|
CustomToast(t.profile.share.exportToClipboardSuccess)
|
||||||
.show(context);
|
.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)
|
AdaptiveMenuItem(
|
||||||
.exportConfigToClipboard(profile),
|
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),
|
AdaptiveMenuItem(
|
||||||
child: Text(t.profile.share.buttonText),
|
title: t.profile.share.exportConfigToClipboard,
|
||||||
),
|
onTap: () async {
|
||||||
MenuItemButton(
|
if (exportConfigMutation.state.isInProgress) {
|
||||||
leadingIcon: const Icon(Icons.edit),
|
return;
|
||||||
child: Text(t.profile.edit.buttonTxt),
|
}
|
||||||
onPressed: () async {
|
exportConfigMutation.setFuture(
|
||||||
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(
|
|
||||||
ref
|
ref
|
||||||
.read(profilesOverviewNotifierProvider.notifier)
|
.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,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
56
pubspec.lock
56
pubspec.lock
@@ -486,6 +486,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.4"
|
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:
|
flutter_localizations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -1642,6 +1690,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.7"
|
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:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ dependencies:
|
|||||||
flutter_loggy_dio: ^3.0.1
|
flutter_loggy_dio: ^3.0.1
|
||||||
dio_smart_retry: ^6.0.0
|
dio_smart_retry: ^6.0.0
|
||||||
cupertino_http: ^1.2.0
|
cupertino_http: ^1.2.0
|
||||||
|
wolt_modal_sheet: ^0.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user