Files
umbrix/lib/features/profile/widget/profile_tile.dart

423 lines
14 KiB
Dart
Raw Normal View History

2024-02-15 15:23:02 +03:30
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
2023-07-24 19:45:58 +03:30
import 'package:flutter/material.dart';
2023-09-14 15:20:48 +03:30
import 'package:flutter/rendering.dart';
2023-11-12 22:22:20 +03:30
import 'package:flutter/services.dart';
2023-07-24 19:45:58 +03:30
import 'package:gap/gap.dart';
2023-09-10 14:16:44 +03:30
import 'package:go_router/go_router.dart';
2023-12-01 12:56:24 +03:30
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/failures.dart';
import 'package:hiddify/core/router/router.dart';
2024-02-15 15:23:02 +03:30
import 'package:hiddify/core/widget/adaptive_icon.dart';
2024-01-16 14:32:30 +03:30
import 'package:hiddify/core/widget/adaptive_menu.dart';
2023-07-24 19:45:58 +03:30
import 'package:hiddify/features/common/confirmation_dialogs.dart';
2023-11-13 17:55:47 +03:30
import 'package:hiddify/features/common/qr_code_dialog.dart';
2023-11-26 21:20:58 +03:30
import 'package:hiddify/features/profile/model/profile_entity.dart';
import 'package:hiddify/features/profile/notifier/profile_notifier.dart';
import 'package:hiddify/features/profile/overview/profiles_overview_notifier.dart';
2024-03-08 14:16:56 +01:00
import 'package:hiddify/gen/fonts.gen.dart';
2023-07-24 19:45:58 +03:30
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:percent_indicator/percent_indicator.dart';
class ProfileTile extends HookConsumerWidget {
const ProfileTile({
super.key,
required this.profile,
this.isMain = false,
});
2023-11-26 21:20:58 +03:30
final ProfileEntity profile;
2023-07-24 19:45:58 +03:30
/// home screen active profile card
final bool isMain;
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final theme = Theme.of(context);
final selectActiveMutation = useMutation(
initialOnFailure: (err) {
2023-10-04 18:06:48 +03:30
CustomToast.error(t.presentShortError(err)).show(context);
2023-07-24 19:45:58 +03:30
},
2023-09-10 14:16:44 +03:30
initialOnSuccess: () {
2024-08-05 02:07:07 +02:00
if (context.mounted && context.canPop()) context.pop();
2023-09-10 14:16:44 +03:30
},
2023-07-24 19:45:58 +03:30
);
2023-10-02 18:51:14 +03:30
final subInfo = switch (profile) {
2023-11-26 21:20:58 +03:30
RemoteProfileEntity(:final subInfo) => subInfo,
2023-10-02 18:51:14 +03:30
_ => null,
};
2023-07-24 19:45:58 +03:30
2024-08-05 02:07:07 +02:00
final effectiveMargin = isMain ? const EdgeInsets.symmetric(horizontal: 16, vertical: 8) : const EdgeInsets.only(left: 12, right: 12, bottom: 12);
2023-07-24 19:45:58 +03:30
final double effectiveElevation = profile.active ? 12 : 4;
2024-08-05 02:07:07 +02:00
final effectiveOutlineColor = profile.active ? theme.colorScheme.outlineVariant : Colors.transparent;
2023-07-24 19:45:58 +03:30
return Card(
margin: effectiveMargin,
elevation: effectiveElevation,
shape: RoundedRectangleBorder(
side: BorderSide(color: effectiveOutlineColor),
borderRadius: BorderRadius.circular(16),
),
shadowColor: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (profile is RemoteProfileEntity || !isMain) ...[
SizedBox(
width: 48,
2023-09-12 00:05:44 +03:30
child: Semantics(
sortKey: const OrdinalSortKey(1),
child: ProfileActionButton(profile, !isMain),
),
),
VerticalDivider(
width: 1,
color: effectiveOutlineColor,
),
],
Expanded(
child: Semantics(
button: true,
sortKey: isMain ? const OrdinalSortKey(0) : null,
focused: isMain,
liveRegion: isMain,
namesRoute: isMain,
label: isMain ? t.profile.activeProfileBtnSemanticLabel : null,
child: InkWell(
onTap: () {
if (isMain) {
const ProfilesOverviewRoute().go(context);
} else {
if (selectActiveMutation.state.isInProgress) return;
if (profile.active) return;
selectActiveMutation.setFuture(
ref.read(profilesOverviewNotifierProvider.notifier).selectActiveProfile(profile.id),
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isMain)
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Material(
borderRadius: BorderRadius.circular(8),
color: Colors.transparent,
clipBehavior: Clip.antiAlias,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
profile.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontFamily: FontFamily.emoji,
),
semanticsLabel: t.profile.activeProfileNameSemanticLabel(
name: profile.name,
2023-07-24 19:45:58 +03:30
),
2023-09-12 00:05:44 +03:30
),
),
const Icon(
FluentIcons.caret_down_16_filled,
size: 16,
),
],
2023-07-24 19:45:58 +03:30
),
),
)
else
Text(
profile.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium,
semanticsLabel: profile.active
? t.profile.activeProfileNameSemanticLabel(
name: profile.name,
)
: t.profile.nonActiveProfileBtnSemanticLabel(
name: profile.name,
),
),
if (subInfo != null) ...[
const Gap(4),
RemainingTrafficIndicator(subInfo.ratio),
const Gap(4),
ProfileSubscriptionInfo(subInfo),
const Gap(4),
2023-07-24 19:45:58 +03:30
],
],
2023-07-24 19:45:58 +03:30
),
),
),
2023-09-12 00:05:44 +03:30
),
),
],
2023-07-24 19:45:58 +03:30
),
);
}
}
class ProfileActionButton extends HookConsumerWidget {
const ProfileActionButton(this.profile, this.showAllActions, {super.key});
2023-11-26 21:20:58 +03:30
final ProfileEntity profile;
2023-07-24 19:45:58 +03:30
final bool showAllActions;
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
2023-11-26 21:20:58 +03:30
if (profile case RemoteProfileEntity() when !showAllActions) {
2023-09-02 21:09:22 +03:30
return Semantics(
button: true,
2023-11-26 21:20:58 +03:30
enabled: !ref.watch(updateProfileProvider(profile.id)).isLoading,
2023-09-02 21:09:22 +03:30
child: Tooltip(
message: t.profile.update.tooltip,
child: InkWell(
onTap: () {
2023-11-26 21:20:58 +03:30
if (ref.read(updateProfileProvider(profile.id)).isLoading) {
2023-09-02 21:09:22 +03:30
return;
}
2024-08-05 02:07:07 +02:00
ref.read(updateProfileProvider(profile.id).notifier).updateProfile(profile as RemoteProfileEntity);
2023-09-02 21:09:22 +03:30
},
2024-02-15 15:23:02 +03:30
child: const Icon(FluentIcons.arrow_sync_24_filled),
2023-09-02 21:09:22 +03:30
),
2023-08-24 22:19:36 +03:30
),
2023-07-24 19:45:58 +03:30
);
}
return ProfileActionsMenu(
profile,
2024-01-16 14:32:30 +03:30
(context, toggleVisibility, _) {
2023-09-02 21:09:22 +03:30
return Semantics(
button: true,
child: Tooltip(
message: MaterialLocalizations.of(context).showMenuTooltip,
child: InkWell(
2024-01-16 14:32:30 +03:30
onTap: toggleVisibility,
2024-02-15 15:23:02 +03:30
child: Icon(AdaptiveIcon(context).more),
2023-09-02 21:09:22 +03:30
),
2023-08-24 22:19:36 +03:30
),
2023-07-24 19:45:58 +03:30
);
},
);
}
}
class ProfileActionsMenu extends HookConsumerWidget {
const ProfileActionsMenu(this.profile, this.builder, {super.key, this.child});
2023-11-26 21:20:58 +03:30
final ProfileEntity profile;
2024-01-16 14:32:30 +03:30
final AdaptiveMenuBuilder builder;
2023-07-24 19:45:58 +03:30
final Widget? child;
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
2023-11-12 12:52:54 +03:30
final exportConfigMutation = useMutation(
initialOnFailure: (err) {
CustomToast.error(t.presentShortError(err)).show(context);
},
2024-08-05 02:07:07 +02:00
initialOnSuccess: () => CustomToast.success(t.profile.share.exportConfigToClipboardSuccess).show(context),
2023-11-12 12:52:54 +03:30
);
2023-07-24 19:45:58 +03:30
final deleteProfileMutation = useMutation(
initialOnFailure: (err) {
2023-08-26 17:01:51 +03:30
CustomAlertDialog.fromErr(t.presentError(err)).show(context);
2023-07-24 19:45:58 +03:30
},
);
2024-01-16 14:32:30 +03:30
final menuItems = [
if (profile case RemoteProfileEntity())
AdaptiveMenuItem(
title: t.profile.update.buttonTxt,
2024-02-15 15:23:02 +03:30
icon: FluentIcons.arrow_sync_24_regular,
2024-01-16 14:32:30 +03:30
onTap: () {
if (ref.read(updateProfileProvider(profile.id)).isLoading) {
return;
}
2024-08-05 02:07:07 +02:00
ref.read(updateProfileProvider(profile.id).notifier).updateProfile(profile as RemoteProfileEntity);
2024-01-16 14:32:30 +03:30
},
),
AdaptiveMenuItem(
title: t.profile.share.buttonText,
2024-02-15 15:23:02 +03:30
icon: AdaptiveIcon(context).share,
2024-01-16 14:32:30 +03:30
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) {
2024-08-05 02:07:07 +02:00
CustomToast(t.profile.share.exportToClipboardSuccess).show(context);
2023-11-13 17:55:47 +03:30
}
2023-11-12 12:52:54 +03:30
}
2024-01-16 14:32:30 +03:30
},
),
AdaptiveMenuItem(
title: t.profile.share.subLinkQrCode,
onTap: () async {
final link = LinkParser.generateSubShareLink(url, name);
if (link.isNotEmpty) {
await QrCodeDialog(
link,
message: name,
).show(context);
}
2023-11-12 12:52:54 +03:30
},
),
],
2024-01-16 14:32:30 +03:30
AdaptiveMenuItem(
title: t.profile.share.exportConfigToClipboard,
onTap: () async {
if (exportConfigMutation.state.isInProgress) {
return;
}
exportConfigMutation.setFuture(
2024-08-05 02:07:07 +02:00
ref.read(profilesOverviewNotifierProvider.notifier).exportConfigToClipboard(profile),
2023-07-24 19:45:58 +03:30
);
2024-01-16 14:32:30 +03:30
},
),
],
),
AdaptiveMenuItem(
2024-02-15 15:23:02 +03:30
icon: FluentIcons.edit_24_regular,
2024-01-16 14:32:30 +03:30
title: t.profile.edit.buttonTxt,
onTap: () async {
await ProfileDetailsRoute(profile.id).push(context);
},
),
AdaptiveMenuItem(
2024-02-15 15:23:02 +03:30
icon: FluentIcons.delete_24_regular,
2024-01-16 14:32:30 +03:30
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,
2024-03-04 15:58:56 +03:30
icon: FluentIcons.delete_24_regular,
2024-01-16 14:32:30 +03:30
);
if (deleteConfirmed) {
deleteProfileMutation.setFuture(
2024-08-05 02:07:07 +02:00
ref.read(profilesOverviewNotifierProvider.notifier).deleteProfile(profile),
2024-01-16 14:32:30 +03:30
);
}
},
),
];
return AdaptiveMenu(
builder: builder,
items: menuItems,
2023-07-24 19:45:58 +03:30
child: child,
);
}
}
// TODO add support url
class ProfileSubscriptionInfo extends HookConsumerWidget {
const ProfileSubscriptionInfo(this.subInfo, {super.key});
final SubscriptionInfo subInfo;
(String, Color?) remainingText(TranslationsEn t, ThemeData theme) {
if (subInfo.isExpired) {
return (t.profile.subscription.expired, theme.colorScheme.error);
} else if (subInfo.ratio >= 1) {
return (t.profile.subscription.noTraffic, theme.colorScheme.error);
} else if (subInfo.remaining.inDays > 365) {
return (t.profile.subscription.remainingDuration(duration: ""), null);
} else {
return (
2024-08-05 02:07:07 +02:00
t.profile.subscription.remainingDuration(duration: subInfo.remaining.inDays),
2023-07-24 19:45:58 +03:30
null,
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final theme = Theme.of(context);
final remaining = remainingText(t, theme);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
2023-09-10 12:46:22 +02:00
Directionality(
textDirection: TextDirection.ltr,
2023-11-10 15:35:44 +03:30
child: Flexible(
child: Text(
subInfo.total > 10 * 1099511627776 //10TB
? "∞ GiB"
: subInfo.consumption.sizeOf(subInfo.total),
2024-08-05 02:07:07 +02:00
semanticsLabel: t.profile.subscription.remainingTrafficSemanticLabel(
2023-11-10 15:35:44 +03:30
consumed: subInfo.consumption.sizeGB(),
total: subInfo.total.sizeGB(),
),
2024-03-08 14:16:56 +01:00
style: theme.textTheme.bodySmall,
2023-11-10 15:35:44 +03:30
overflow: TextOverflow.ellipsis,
2023-09-14 15:20:48 +03:30
),
2023-09-10 12:46:22 +02:00
),
2023-07-25 21:41:12 +03:30
),
2023-11-10 15:35:44 +03:30
Flexible(
child: Text(
remaining.$1,
style: theme.textTheme.bodySmall?.copyWith(color: remaining.$2),
overflow: TextOverflow.ellipsis,
),
2023-07-24 19:45:58 +03:30
),
],
);
}
}
// TODO change colors
class RemainingTrafficIndicator extends StatelessWidget {
const RemainingTrafficIndicator(this.ratio, {super.key});
final double ratio;
@override
Widget build(BuildContext context) {
final startColor = ratio < 0.25
? const Color.fromRGBO(93, 205, 251, 1.0)
: ratio < 0.65
? const Color.fromRGBO(205, 199, 64, 1.0)
: const Color.fromRGBO(241, 82, 81, 1.0);
final endColor = ratio < 0.25
? const Color.fromRGBO(49, 146, 248, 1.0)
: ratio < 0.65
? const Color.fromRGBO(98, 115, 32, 1.0)
: const Color.fromRGBO(139, 30, 36, 1.0);
return LinearPercentIndicator(
percent: ratio,
animation: true,
padding: EdgeInsets.zero,
lineHeight: 6,
barRadius: const Radius.circular(16),
linearGradient: LinearGradient(
colors: [startColor, endColor],
),
);
}
}