diff --git a/assets/translations/strings.i18n.json b/assets/translations/strings.i18n.json index 055be2d2..ea6c53c1 100644 --- a/assets/translations/strings.i18n.json +++ b/assets/translations/strings.i18n.json @@ -24,9 +24,10 @@ "subscription": { "traffic": "traffic", "updatedTimeAgo": "updated ${timeago}", - "remaining": "remaining", + "remainingDuration": "${duration} days remaining", "expired": "expired", - "noTraffic": "no traffic" + "noTraffic": "no traffic", + "gigaByte": "GB" }, "add": { "buttonText": "add new profile", @@ -36,11 +37,15 @@ "invalidUrlMsg": "unexpected url" }, "update": { + "buttonTxt": "update", "failureMsg": "failed to update profile: ${reason}", "successMsg": "successfully updated profile" }, + "edit": { + "buttonTxt": "edit" + }, "delete": { - "buttonText": "delete", + "buttonTxt": "delete", "confirmationMsg": "delete profile for ever? this can not be undone", "successMsg": "successfully deleted profile" }, diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 6a60021a..1e51da29 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -24,9 +24,10 @@ "subscription": { "traffic": "ترافیک", "updatedTimeAgo": "بروزرسانی شده در ${timeago}", - "remaining": "باقی مانده", + "remainingDuration": "${duration} روز باقی مانده", "expired": "منقضی شده", - "noTraffic": "پایان ترافیک" + "noTraffic": "پایان ترافیک", + "gigaByte": "گیگ" }, "add": { "buttonText": "افزودن پروفایل جدید", @@ -36,11 +37,15 @@ "invalidUrlMsg": "لینک نامعتبر" }, "update": { + "buttonTxt": "بروزرسانی", "failureMsg": "در بروزرسانی پروفایل خطایی رخ داد: ${reason}", "successMsg": "پروفایل با موفقیت بروزرسانی شد" }, + "edit": { + "buttonTxt": "ویرایش" + }, "delete": { - "buttonText": "حذف", + "buttonTxt": "حذف", "confirmationMsg": "حذف پروفایل برای همیشه؟ این عمل قابل لغو نیست.", "successMsg": "پروفایل با موفقیت حذف شد" }, diff --git a/lib/features/common/common.dart b/lib/features/common/common.dart index 23ab52fc..39fb5ab3 100644 --- a/lib/features/common/common.dart +++ b/lib/features/common/common.dart @@ -1,4 +1,5 @@ export 'add_profile_modal.dart'; +export 'confirmation_dialogs.dart'; export 'custom_app_bar.dart'; +export 'profile_tile.dart'; export 'qr_code_scanner_screen.dart'; -export 'remaining_traffic_indicator.dart'; diff --git a/lib/features/common/profile_tile.dart b/lib/features/common/profile_tile.dart new file mode 100644 index 00000000..66240317 --- /dev/null +++ b/lib/features/common/profile_tile.dart @@ -0,0 +1,345 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/locale/locale.dart'; +import 'package:hiddify/core/router/routes/routes.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/features/common/confirmation_dialogs.dart'; +import 'package:hiddify/features/profiles/notifier/notifier.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:percent_indicator/percent_indicator.dart'; +import 'package:recase/recase.dart'; + +class ProfileTile extends HookConsumerWidget { + const ProfileTile({ + super.key, + required this.profile, + this.isMain = false, + }); + + final Profile profile; + + /// 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) { + CustomToast.error(t.presentError(err)).show(context); + }, + ); + + final subInfo = profile.subInfo; + + 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, + child: InkWell( + onTap: isMain + ? null + : () { + if (selectActiveMutation.state.isInProgress) return; + if (profile.active) return; + selectActiveMutation.setFuture( + ref + .read(profilesNotifierProvider.notifier) + .selectActiveProfile(profile.id), + ); + }, + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + width: 48, + child: ProfileActionButton(profile, !isMain), + ), + VerticalDivider( + width: 1, + color: effectiveOutlineColor, + ), + Flexible( + 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: InkWell( + onTap: () => const ProfilesRoute().go(context), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + profile.name, + style: theme.textTheme.titleMedium, + ), + ), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ), + ) + else + Text( + profile.name, + style: theme.textTheme.titleMedium, + ), + if (subInfo?.isValid ?? false) ...[ + const Gap(4), + RemainingTrafficIndicator(subInfo!.ratio), + const Gap(4), + ProfileSubscriptionInfo(subInfo), + const Gap(4), + ], + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class ProfileActionButton extends HookConsumerWidget { + const ProfileActionButton(this.profile, this.showAllActions, {super.key}); + + final Profile profile; + final bool showAllActions; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final updateProfileMutation = useMutation( + initialOnFailure: (err) { + CustomToast.error(t.presentError(err)).show(context); + }, + initialOnSuccess: () => + CustomToast.success(t.profile.update.successMsg).show(context), + ); + + if (!showAllActions) { + return InkWell( + onTap: () { + if (updateProfileMutation.state.isInProgress) { + return; + } + updateProfileMutation.setFuture( + ref.read(profilesNotifierProvider.notifier).updateProfile(profile), + ); + }, + child: const Icon(Icons.refresh), + ); + } + return ProfileActionsMenu( + profile, + (context, controller, child) { + return InkWell( + onTap: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Icon(Icons.more_vert), + ); + }, + ); + } +} + +class ProfileActionsMenu extends HookConsumerWidget { + const ProfileActionsMenu(this.profile, this.builder, {super.key, this.child}); + + final Profile profile; + final MenuAnchorChildBuilder builder; + final Widget? child; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final updateProfileMutation = useMutation( + initialOnFailure: (err) { + CustomToast.error(t.presentError(err)).show(context); + }, + initialOnSuccess: () => + CustomToast.success(t.profile.update.successMsg).show(context), + ); + final deleteProfileMutation = useMutation( + initialOnFailure: (err) { + CustomToast.error(t.presentError(err)).show(context); + }, + ); + + return MenuAnchor( + builder: builder, + menuChildren: [ + MenuItemButton( + leadingIcon: const Icon(Icons.refresh), + child: Text(t.profile.update.buttonTxt.titleCase), + onPressed: () { + if (updateProfileMutation.state.isInProgress) { + return; + } + updateProfileMutation.setFuture( + ref + .read(profilesNotifierProvider.notifier) + .updateProfile(profile), + ); + }, + ), + MenuItemButton( + leadingIcon: const Icon(Icons.edit), + child: Text(t.profile.edit.buttonTxt.titleCase), + onPressed: () async { + await ProfileDetailsRoute(profile.id).push(context); + }, + ), + MenuItemButton( + leadingIcon: const Icon(Icons.delete), + child: Text(t.profile.delete.buttonTxt.titleCase), + onPressed: () async { + if (deleteProfileMutation.state.isInProgress) { + return; + } + final deleteConfirmed = await showConfirmationDialog( + context, + title: t.profile.delete.buttonTxt.titleCase, + message: t.profile.delete.confirmationMsg.sentenceCase, + ); + if (deleteConfirmed) { + deleteProfileMutation.setFuture( + ref + .read(profilesNotifierProvider.notifier) + .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: [ + if (subInfo.total != null) + Text.rich( + TextSpan( + children: [ + TextSpan(text: formatByte(subInfo.consumption, unit: 3).size), + const TextSpan(text: " / "), + TextSpan(text: formatByte(subInfo.total!, unit: 3).size), + const TextSpan(text: " "), + TextSpan(text: t.profile.subscription.gigaByte), + ], + ), + style: theme.textTheme.bodySmall, + ), + Text( + remaining.$1, + style: theme.textTheme.bodySmall?.copyWith(color: remaining.$2), + ), + ], + ); + } +} + +// 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], + ), + ); + } +} diff --git a/lib/features/common/remaining_traffic_indicator.dart b/lib/features/common/remaining_traffic_indicator.dart deleted file mode 100644 index 10de1ce7..00000000 --- a/lib/features/common/remaining_traffic_indicator.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:percent_indicator/percent_indicator.dart'; - -// 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], - ), - ); - } -} diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index e2399235..2b3c2246 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -51,7 +51,7 @@ class HomePage extends HookConsumerWidget { switch (activeProfile) { AsyncData(value: final profile?) => MultiSliver( children: [ - ActiveProfileCard(profile), + ProfileTile(profile: profile, isMain: true), const SliverFillRemaining( hasScrollBody: false, child: Padding( diff --git a/lib/features/home/widgets/active_profile_card.dart b/lib/features/home/widgets/active_profile_card.dart deleted file mode 100644 index 0aea5235..00000000 --- a/lib/features/home/widgets/active_profile_card.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; -import 'package:hiddify/features/common/common.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:recase/recase.dart'; - -// TODO: rewrite -class ActiveProfileCard extends HookConsumerWidget { - const ActiveProfileCard(this.profile, {super.key}); - - final Profile profile; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final t = ref.watch(translationsProvider); - - return Card( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Material( - borderRadius: BorderRadius.circular(16), - color: Colors.transparent, - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: () async { - await const ProfilesRoute().push(context); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - child: Row( - children: [ - Expanded( - child: Text( - profile.name, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - const Gap(4), - const Icon(Icons.arrow_drop_down), - ], - ), - ), - ), - ), - ), - TextButton.icon( - onPressed: () async { - const AddProfileRoute().push(context); - }, - label: Text(t.profile.add.buttonText.titleCase), - icon: const Icon(Icons.add), - ), - ], - ), - if (profile.hasSubscriptionInfo) ...[ - const Divider(thickness: 0.5), - SubscriptionInfoTile(profile.subInfo!), - ], - ], - ), - ), - ); - } -} - -class SubscriptionInfoTile extends HookConsumerWidget { - const SubscriptionInfoTile(this.subInfo, {super.key}); - - final SubscriptionInfo subInfo; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (!subInfo.isValid) return const SizedBox.shrink(); - final t = ref.watch(translationsProvider); - - final themeData = Theme.of(context); - - final updateProfileMutation = useMutation( - initialOnFailure: (err) { - CustomToast.error(t.presentError(err)).show(context); - }, - initialOnSuccess: () => - CustomToast.success(t.profile.update.successMsg).show(context), - ); - - return Row( - children: [ - Flexible( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - formatTrafficByteSize( - subInfo.consumption, - subInfo.total!, - ), - style: themeData.textTheme.titleSmall, - ), - ), - Text( - t.profile.subscription.traffic, - style: themeData.textTheme.bodySmall, - ), - ], - ), - const SizedBox(height: 2), - RemainingTrafficIndicator(subInfo.ratio), - ], - ), - ), - const Gap(8), - IconButton( - onPressed: () async { - if (updateProfileMutation.state.isInProgress) return; - updateProfileMutation.setFuture( - ref.read(activeProfileProvider.notifier).updateProfile(), - ); - }, - icon: const Icon(Icons.refresh, size: 44), - ), - const Gap(8), - if (subInfo.isExpired) - Text( - t.profile.subscription.expired, - style: themeData.textTheme.titleSmall - ?.copyWith(color: themeData.colorScheme.error), - ) - else if (subInfo.ratio >= 1) - Text( - t.profile.subscription.noTraffic, - style: themeData.textTheme.titleSmall - ?.copyWith(color: themeData.colorScheme.error), - ) - else - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - formatExpireDuration(subInfo.remaining), - style: themeData.textTheme.titleSmall, - ), - Text( - t.profile.subscription.remaining, - style: themeData.textTheme.bodySmall, - ), - ], - ), - ], - ); - } -} diff --git a/lib/features/home/widgets/widgets.dart b/lib/features/home/widgets/widgets.dart index ef518560..b043254e 100644 --- a/lib/features/home/widgets/widgets.dart +++ b/lib/features/home/widgets/widgets.dart @@ -1,3 +1,2 @@ -export 'active_profile_card.dart'; export 'connection_button.dart'; export 'empty_profiles_home_body.dart'; diff --git a/lib/features/profile_detail/view/profile_detail_page.dart b/lib/features/profile_detail/view/profile_detail_page.dart index 658a8a4c..7e402b21 100644 --- a/lib/features/profile_detail/view/profile_detail_page.dart +++ b/lib/features/profile_detail/view/profile_detail_page.dart @@ -142,7 +142,7 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger { await showConfirmationDialog( context, title: - t.profile.delete.buttonText.titleCase, + t.profile.delete.buttonTxt.titleCase, message: t.profile.delete.confirmationMsg .sentenceCase, ); @@ -156,7 +156,7 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger { ), ), child: Text( - t.profile.delete.buttonText.titleCase, + t.profile.delete.buttonTxt.titleCase, style: TextStyle( color: themeData .colorScheme.onErrorContainer, diff --git a/lib/features/profiles/notifier/profiles_notifier.dart b/lib/features/profiles/notifier/profiles_notifier.dart index a2ca2a60..73d76b5b 100644 --- a/lib/features/profiles/notifier/profiles_notifier.dart +++ b/lib/features/profiles/notifier/profiles_notifier.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:fpdart/fpdart.dart'; import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/utils/utils.dart'; @@ -26,6 +27,15 @@ class ProfilesNotifier extends _$ProfilesNotifier with AppLogger { }).run(); } + Future updateProfile(Profile profile) async { + loggy.debug("updating profile"); + return ref + .read(profilesRepositoryProvider) + .update(profile) + .getOrElse((l) => throw l) + .run(); + } + Future deleteProfile(Profile profile) async { loggy.debug('deleting profile: ${profile.name}'); await _profilesRepo.delete(profile.id).mapLeft( diff --git a/lib/features/profiles/view/profiles_modal.dart b/lib/features/profiles/view/profiles_modal.dart index 739a7609..3658137a 100644 --- a/lib/features/profiles/view/profiles_modal.dart +++ b/lib/features/profiles/view/profiles_modal.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:hiddify/features/common/common.dart'; import 'package:hiddify/features/profiles/notifier/notifier.dart'; -import 'package:hiddify/features/profiles/widgets/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class ProfilesModal extends HookConsumerWidget { @@ -24,7 +24,7 @@ class ProfilesModal extends HookConsumerWidget { AsyncData(value: final profiles) => SliverList.builder( itemBuilder: (context, index) { final profile = profiles[index]; - return ProfileTile(profile); + return ProfileTile(profile: profile); }, itemCount: profiles.length, ), diff --git a/lib/features/profiles/widgets/profile_tile.dart b/lib/features/profiles/widgets/profile_tile.dart deleted file mode 100644 index f02da509..00000000 --- a/lib/features/profiles/widgets/profile_tile.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/features/common/common.dart'; -import 'package:hiddify/features/common/confirmation_dialogs.dart'; -import 'package:hiddify/features/profiles/notifier/notifier.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:recase/recase.dart'; -import 'package:timeago/timeago.dart' as timeago; - -class ProfileTile extends HookConsumerWidget { - const ProfileTile(this.profile, {super.key}); - - final Profile profile; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final t = ref.watch(translationsProvider); - final subInfo = profile.subInfo; - - final themeData = Theme.of(context); - - final selectActiveMutation = useMutation( - initialOnFailure: (err) { - CustomToast.error(t.presentError(err)).show(context); - }, - ); - final deleteProfileMutation = useMutation( - initialOnFailure: (err) { - CustomToast.error(t.presentError(err)).show(context); - }, - ); - - return Card( - elevation: 6, - clipBehavior: Clip.antiAlias, - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - shadowColor: Colors.transparent, - color: profile.active ? themeData.colorScheme.tertiaryContainer : null, - child: InkWell( - onTap: () { - if (profile.active || selectActiveMutation.state.isInProgress) return; - selectActiveMutation.setFuture( - ref - .read(profilesNotifierProvider.notifier) - .selectActiveProfile(profile.id), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text.rich( - overflow: TextOverflow.ellipsis, - TextSpan( - children: [ - TextSpan( - text: profile.name, - style: themeData.textTheme.titleMedium, - ), - const TextSpan(text: " • "), - TextSpan( - text: t.profile.subscription.updatedTimeAgo( - timeago: timeago.format(profile.lastUpdate), - ), - ), - ], - ), - ), - ), - Row( - children: [ - const Gap(12), - SizedBox( - width: 18, - height: 18, - child: IconButton( - icon: const Icon(Icons.edit), - padding: EdgeInsets.zero, - iconSize: 18, - onPressed: () async { - // await context.push(Routes.profile(profile.id).path); - // TODO: temp - await ProfileDetailsRoute(profile.id).push(context); - }, - ), - ), - const Gap(12), - SizedBox( - width: 18, - height: 18, - child: IconButton( - icon: const Icon(Icons.delete_forever), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - iconSize: 18, - onPressed: () async { - if (deleteProfileMutation.state.isInProgress) { - return; - } - final deleteConfirmed = - await showConfirmationDialog( - context, - title: t.profile.delete.buttonText.titleCase, - message: - t.profile.delete.confirmationMsg.sentenceCase, - ); - if (deleteConfirmed) { - deleteProfileMutation.setFuture( - ref - .read(profilesNotifierProvider.notifier) - .deleteProfile(profile), - ); - } - }, - ), - ), - ], - ), - ], - ), - if (subInfo?.isValid ?? false) ...[ - const Gap(2), - Row( - children: [ - if (subInfo!.isExpired) - Text( - t.profile.subscription.expired, - style: themeData.textTheme.titleSmall - ?.copyWith(color: themeData.colorScheme.error), - ) - else if (subInfo.ratio >= 1) - Text( - t.profile.subscription.noTraffic, - style: themeData.textTheme.titleSmall?.copyWith( - color: themeData.colorScheme.error, - ), - ) - else - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - formatExpireDuration(subInfo.remaining), - style: themeData.textTheme.titleSmall, - ), - Text( - t.profile.subscription.remaining, - style: themeData.textTheme.bodySmall, - ), - ], - ), - const Gap(16), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - formatTrafficByteSize( - subInfo.consumption, - subInfo.total!, - ), - style: themeData.textTheme.titleMedium, - ), - RemainingTrafficIndicator(subInfo.ratio), - ], - ), - ), - ], - ), - ], - ], - ), - ), - ), - ); - } -} diff --git a/lib/features/profiles/widgets/widgets.dart b/lib/features/profiles/widgets/widgets.dart deleted file mode 100644 index 9d452d84..00000000 --- a/lib/features/profiles/widgets/widgets.dart +++ /dev/null @@ -1 +0,0 @@ -export 'profile_tile.dart'; diff --git a/lib/utils/number_formatters.dart b/lib/utils/number_formatters.dart index 3156a3cc..15f54915 100644 --- a/lib/utils/number_formatters.dart +++ b/lib/utils/number_formatters.dart @@ -4,6 +4,17 @@ import 'package:intl/intl.dart'; const _units = ["B", "kB", "MB", "GB", "TB"]; +({String size, String unit}) formatByte(int input, {int? unit}) { + const base = 1024; + if (input <= 0) return (size: "0", unit: _units[unit ?? 0]); + final int digitGroups = unit ?? (log(input) / log(base)).round(); + return ( + size: NumberFormat("#,##0.#").format(input / pow(base, digitGroups)), + unit: _units[digitGroups], + ); +} + +// TODO remove ({String size, String unit}) formatByteSpeed(int speed) { const base = 1024; if (speed <= 0) return (size: "0", unit: "B/s"); @@ -13,10 +24,3 @@ const _units = ["B", "kB", "MB", "GB", "TB"]; unit: "${_units[digitGroups]}/s", ); } - -String formatTrafficByteSize(int consumption, int total) { - const base = 1024; - if (total <= 0) return "0 B / 0 B"; - final formatter = NumberFormat("#,##0.#"); - return "${formatter.format(consumption / pow(base, 3))} GB / ${formatter.format(total / pow(base, 3))} GB"; -} diff --git a/lib/utils/string_formatters.dart b/lib/utils/string_formatters.dart deleted file mode 100644 index 6736d83c..00000000 --- a/lib/utils/string_formatters.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:duration/duration.dart'; - -// TODO: use a better solution -String formatExpireDuration(Duration dur) { - return prettyDuration( - dur, - upperTersity: DurationTersity.day, - tersity: DurationTersity.day, - ); -} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index a81a0c92..bb32efd6 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -9,6 +9,5 @@ export 'mutation_state.dart'; export 'number_formatters.dart'; export 'placeholders.dart'; export 'platform_utils.dart'; -export 'string_formatters.dart'; export 'text_utils.dart'; export 'validators.dart';