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';
|
2023-11-01 20:36:16 +03:30
|
|
|
import 'package:hiddify/core/router/router.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';
|
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: () {
|
|
|
|
|
if (context.mounted) context.pop();
|
|
|
|
|
},
|
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
|
|
|
|
|
|
|
|
final effectiveMargin = isMain
|
|
|
|
|
? const EdgeInsets.symmetric(horizontal: 16, vertical: 8)
|
|
|
|
|
: const EdgeInsets.only(left: 12, right: 12, bottom: 12);
|
|
|
|
|
final double effectiveElevation = profile.active ? 12 : 4;
|
|
|
|
|
final effectiveOutlineColor =
|
|
|
|
|
profile.active ? theme.colorScheme.outlineVariant : Colors.transparent;
|
|
|
|
|
|
|
|
|
|
return Card(
|
|
|
|
|
margin: effectiveMargin,
|
|
|
|
|
elevation: effectiveElevation,
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
side: BorderSide(color: effectiveOutlineColor),
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
),
|
|
|
|
|
shadowColor: Colors.transparent,
|
2023-09-12 00:05:44 +03:30
|
|
|
child: IntrinsicHeight(
|
|
|
|
|
child: Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
2023-11-26 21:20:58 +03:30
|
|
|
if (profile is RemoteProfileEntity || !isMain) ...[
|
2023-10-02 18:51:14 +03:30
|
|
|
SizedBox(
|
|
|
|
|
width: 48,
|
|
|
|
|
child: Semantics(
|
|
|
|
|
sortKey: const OrdinalSortKey(1),
|
|
|
|
|
child: ProfileActionButton(profile, !isMain),
|
|
|
|
|
),
|
2023-09-14 15:20:48 +03:30
|
|
|
),
|
2023-10-02 18:51:14 +03:30
|
|
|
VerticalDivider(
|
|
|
|
|
width: 1,
|
|
|
|
|
color: effectiveOutlineColor,
|
|
|
|
|
),
|
|
|
|
|
],
|
2023-09-30 11:15:32 +03:30
|
|
|
Expanded(
|
2023-09-12 00:05:44 +03:30
|
|
|
child: Semantics(
|
|
|
|
|
button: true,
|
2023-09-14 15:20:48 +03:30
|
|
|
sortKey: isMain ? const OrdinalSortKey(0) : null,
|
|
|
|
|
focused: isMain,
|
|
|
|
|
liveRegion: isMain,
|
|
|
|
|
namesRoute: isMain,
|
2023-09-30 11:15:32 +03:30
|
|
|
label: isMain ? t.profile.activeProfileBtnSemanticLabel : null,
|
2023-09-12 00:05:44 +03:30
|
|
|
child: InkWell(
|
|
|
|
|
onTap: () {
|
|
|
|
|
if (isMain) {
|
2023-11-26 21:59:57 +03:30
|
|
|
const ProfilesOverviewRoute().go(context);
|
2023-09-12 00:05:44 +03:30
|
|
|
} else {
|
|
|
|
|
if (selectActiveMutation.state.isInProgress) return;
|
|
|
|
|
if (profile.active) return;
|
|
|
|
|
selectActiveMutation.setFuture(
|
|
|
|
|
ref
|
2023-11-26 21:20:58 +03:30
|
|
|
.read(profilesOverviewNotifierProvider.notifier)
|
2023-09-12 00:05:44 +03:30
|
|
|
.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,
|
2023-10-12 00:27:23 +03:30
|
|
|
maxLines: 2,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
style: theme.textTheme.titleMedium,
|
2023-09-14 15:20:48 +03:30
|
|
|
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(Icons.arrow_drop_down),
|
|
|
|
|
],
|
2023-07-24 19:45:58 +03:30
|
|
|
),
|
|
|
|
|
),
|
2023-09-12 00:05:44 +03:30
|
|
|
)
|
|
|
|
|
else
|
|
|
|
|
Text(
|
|
|
|
|
profile.name,
|
2023-10-12 00:27:23 +03:30
|
|
|
maxLines: 2,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
style: theme.textTheme.titleMedium,
|
2023-09-30 11:15:32 +03:30
|
|
|
semanticsLabel: profile.active
|
|
|
|
|
? t.profile.activeProfileNameSemanticLabel(
|
|
|
|
|
name: profile.name,
|
|
|
|
|
)
|
|
|
|
|
: t.profile.nonActiveProfileBtnSemanticLabel(
|
|
|
|
|
name: profile.name,
|
|
|
|
|
),
|
2023-07-24 19:45:58 +03:30
|
|
|
),
|
2023-09-12 00:05:44 +03:30
|
|
|
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-09-12 00:05:44 +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;
|
|
|
|
|
}
|
2023-11-26 21:20:58 +03:30
|
|
|
ref
|
|
|
|
|
.read(updateProfileProvider(profile.id).notifier)
|
|
|
|
|
.updateProfile(profile as RemoteProfileEntity);
|
2023-09-02 21:09:22 +03:30
|
|
|
},
|
|
|
|
|
child: const Icon(Icons.update),
|
|
|
|
|
),
|
2023-08-24 22:19:36 +03:30
|
|
|
),
|
2023-07-24 19:45:58 +03:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return ProfileActionsMenu(
|
|
|
|
|
profile,
|
|
|
|
|
(context, controller, child) {
|
2023-09-02 21:09:22 +03:30
|
|
|
return Semantics(
|
|
|
|
|
button: true,
|
|
|
|
|
child: Tooltip(
|
|
|
|
|
message: MaterialLocalizations.of(context).showMenuTooltip,
|
|
|
|
|
child: InkWell(
|
|
|
|
|
onTap: () {
|
|
|
|
|
if (controller.isOpen) {
|
|
|
|
|
controller.close();
|
|
|
|
|
} else {
|
|
|
|
|
controller.open();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
child: const Icon(Icons.more_vert),
|
|
|
|
|
),
|
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;
|
2023-07-24 19:45:58 +03:30
|
|
|
final MenuAnchorChildBuilder builder;
|
|
|
|
|
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);
|
|
|
|
|
},
|
|
|
|
|
initialOnSuccess: () =>
|
|
|
|
|
CustomToast.success(t.profile.share.exportConfigToClipboardSuccess)
|
|
|
|
|
.show(context),
|
|
|
|
|
);
|
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
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return MenuAnchor(
|
|
|
|
|
builder: builder,
|
|
|
|
|
menuChildren: [
|
2023-11-26 21:20:58 +03:30
|
|
|
if (profile case RemoteProfileEntity())
|
2023-10-02 18:51:14 +03:30
|
|
|
MenuItemButton(
|
|
|
|
|
leadingIcon: const Icon(Icons.update),
|
|
|
|
|
child: Text(t.profile.update.buttonTxt),
|
|
|
|
|
onPressed: () {
|
2023-11-26 21:20:58 +03:30
|
|
|
if (ref.read(updateProfileProvider(profile.id)).isLoading) {
|
2023-10-02 18:51:14 +03:30
|
|
|
return;
|
|
|
|
|
}
|
2023-11-26 21:20:58 +03:30
|
|
|
ref
|
|
|
|
|
.read(updateProfileProvider(profile.id).notifier)
|
|
|
|
|
.updateProfile(profile as RemoteProfileEntity);
|
2023-10-02 18:51:14 +03:30
|
|
|
},
|
|
|
|
|
),
|
2023-11-12 12:52:54 +03:30
|
|
|
SubmenuButton(
|
|
|
|
|
menuChildren: [
|
2023-11-26 21:20:58 +03:30
|
|
|
if (profile case RemoteProfileEntity(:final url, :final name)) ...[
|
2023-11-12 22:22:20 +03:30
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
2023-11-13 17:55:47 +03:30
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
2023-11-12 12:52:54 +03:30
|
|
|
MenuItemButton(
|
|
|
|
|
child: Text(t.profile.share.exportConfigToClipboard),
|
|
|
|
|
onPressed: () async {
|
|
|
|
|
if (exportConfigMutation.state.isInProgress) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
exportConfigMutation.setFuture(
|
|
|
|
|
ref
|
2023-11-26 21:20:58 +03:30
|
|
|
.read(profilesOverviewNotifierProvider.notifier)
|
2023-11-12 12:52:54 +03:30
|
|
|
.exportConfigToClipboard(profile),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
leadingIcon: const Icon(Icons.share),
|
|
|
|
|
child: Text(t.profile.share.buttonText),
|
|
|
|
|
),
|
2023-07-24 19:45:58 +03:30
|
|
|
MenuItemButton(
|
|
|
|
|
leadingIcon: const Icon(Icons.edit),
|
2023-09-07 01:56:59 +03:30
|
|
|
child: Text(t.profile.edit.buttonTxt),
|
2023-07-24 19:45:58 +03:30
|
|
|
onPressed: () async {
|
|
|
|
|
await ProfileDetailsRoute(profile.id).push(context);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
MenuItemButton(
|
|
|
|
|
leadingIcon: const Icon(Icons.delete),
|
2023-09-07 01:56:59 +03:30
|
|
|
child: Text(t.profile.delete.buttonTxt),
|
2023-07-24 19:45:58 +03:30
|
|
|
onPressed: () async {
|
|
|
|
|
if (deleteProfileMutation.state.isInProgress) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final deleteConfirmed = await showConfirmationDialog(
|
|
|
|
|
context,
|
2023-09-07 01:56:59 +03:30
|
|
|
title: t.profile.delete.buttonTxt,
|
|
|
|
|
message: t.profile.delete.confirmationMsg,
|
2023-07-24 19:45:58 +03:30
|
|
|
);
|
|
|
|
|
if (deleteConfirmed) {
|
|
|
|
|
deleteProfileMutation.setFuture(
|
|
|
|
|
ref
|
2023-11-26 21:20:58 +03:30
|
|
|
.read(profilesOverviewNotifierProvider.notifier)
|
2023-07-24 19:45:58 +03:30
|
|
|
.deleteProfile(profile),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
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 (
|
|
|
|
|
t.profile.subscription
|
|
|
|
|
.remainingDuration(duration: subInfo.remaining.inDays),
|
|
|
|
|
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),
|
|
|
|
|
semanticsLabel:
|
|
|
|
|
t.profile.subscription.remainingTrafficSemanticLabel(
|
|
|
|
|
consumed: subInfo.consumption.sizeGB(),
|
|
|
|
|
total: subInfo.total.sizeGB(),
|
|
|
|
|
),
|
|
|
|
|
style: theme.textTheme.bodySmall,
|
|
|
|
|
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],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|