Refactor profiles

This commit is contained in:
problematicconsumer
2023-11-26 21:20:58 +03:30
parent e2f5f51176
commit 829d58a1a2
49 changed files with 1206 additions and 1024 deletions

View File

@@ -1,18 +0,0 @@
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/profiles/profiles.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'active_profile_notifier.g.dart';
@Riverpod(keepAlive: true)
class ActiveProfile extends _$ActiveProfile with AppLogger {
@override
Stream<Profile?> build() {
loggy.debug("watching active profile");
return ref
.watch(profilesRepositoryProvider)
.watchActiveProfile()
.map((event) => event.getOrElse((l) => throw l));
}
}

View File

@@ -1,14 +0,0 @@
import 'package:hiddify/data/data_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'has_any_profile_notifier.g.dart';
@Riverpod(keepAlive: true)
Stream<bool> hasAnyProfile(
HasAnyProfileRef ref,
) {
return ref
.watch(profilesRepositoryProvider)
.watchHasAnyProfile()
.map((event) => event.getOrElse((l) => throw l));
}

View File

@@ -100,26 +100,4 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger {
await _ignoreReleasePref.update(versionInfo.version);
state = AppUpdateStateIgnored(versionInfo);
}
// Future<void> _schedule() async {
// loggy.debug("scheduling app update checker");
// return ref.read(cronServiceProvider).schedule(
// key: 'app_update',
// duration: const Duration(hours: 8),
// callback: () async {
// await Future.delayed(const Duration(seconds: 5));
// final updateState = await check();
// final context = rootNavigatorKey.currentContext;
// if (context != null && context.mounted) {
// if (updateState
// case AppUpdateStateAvailable(:final versionInfo)) {
// await NewVersionDialog(
// ref.read(appInfoProvider).presentVersion,
// versionInfo,
// ).show(context);
// }
// }
// },
// );
// }
}

View File

@@ -1,9 +1,8 @@
import 'package:hiddify/core/prefs/general_prefs.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/features/common/window/window_controller.dart';
import 'package:hiddify/features/profiles/notifier/notifier.dart';
import 'package:hiddify/features/profile/notifier/profiles_update_notifier.dart';
import 'package:hiddify/features/system_tray/system_tray_controller.dart';
import 'package:hiddify/services/service_providers.dart';
import 'package:hiddify/utils/platform_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -17,7 +16,7 @@ void commonControllers(CommonControllersRef ref) {
introCompletedProvider,
(_, completed) async {
if (completed) {
await ref.read(cronServiceProvider).startScheduler();
await ref.read(foregroundProfilesUpdateNotifierProvider.future);
}
},
fireImmediately: true,
@@ -27,11 +26,6 @@ void commonControllers(CommonControllersRef ref) {
(previous, next) {},
fireImmediately: true,
);
ref.listen(
profilesUpdateNotifierProvider,
(previous, next) {},
fireImmediately: true,
);
if (PlatformUtils.isDesktop) {
ref.listen(
windowControllerProvider,

View File

@@ -3,7 +3,7 @@ import 'package:hiddify/core/prefs/service_prefs.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/domain/core_facade.dart';
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
@@ -25,7 +25,7 @@ class ConnectivityController extends _$ConnectivityController with AppLogger {
},
);
return _core.watchConnectionStatus().doOnData((event) {
if (event case Disconnected(:final connectionFailure?)
if (event case Disconnected(connectionFailure: final _?)
when PlatformUtils.isDesktop) {
ref.read(startedByUserProvider.notifier).update(false);
}

View File

@@ -1,461 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.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/confirmation_dialogs.dart';
import 'package:hiddify/features/common/qr_code_dialog.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';
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.presentShortError(err)).show(context);
},
initialOnSuccess: () {
if (context.mounted) context.pop();
},
);
final subInfo = switch (profile) {
RemoteProfile(:final subInfo) => subInfo,
_ => null,
};
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: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (profile is RemoteProfile || !isMain) ...[
SizedBox(
width: 48,
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 ProfilesRoute().go(context);
} else {
if (selectActiveMutation.state.isInProgress) return;
if (profile.active) return;
selectActiveMutation.setFuture(
ref
.read(profilesNotifierProvider.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,
semanticsLabel: t.profile
.activeProfileNameSemanticLabel(
name: profile.name,
),
),
),
const Icon(Icons.arrow_drop_down),
],
),
),
)
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),
],
],
),
),
),
),
),
],
),
),
);
}
}
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) {
CustomAlertDialog.fromErr(
t.presentError(err, action: t.profile.update.failureMsg),
).show(context);
},
initialOnSuccess: () =>
CustomToast.success(t.profile.update.successMsg).show(context),
);
if (profile case RemoteProfile() when !showAllActions) {
return Semantics(
button: true,
enabled: !updateProfileMutation.state.isInProgress,
child: Tooltip(
message: t.profile.update.tooltip,
child: InkWell(
onTap: () {
if (updateProfileMutation.state.isInProgress) {
return;
}
updateProfileMutation.setFuture(
ref
.read(profilesNotifierProvider.notifier)
.updateProfile(profile as RemoteProfile),
);
},
child: const Icon(Icons.update),
),
),
);
}
return ProfileActionsMenu(
profile,
(context, controller, child) {
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),
),
),
);
},
);
}
}
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) {
CustomAlertDialog.fromErr(
t.presentError(err, action: t.profile.update.failureMsg),
).show(context);
},
initialOnSuccess: () =>
CustomToast.success(t.profile.update.successMsg).show(context),
);
final exportConfigMutation = useMutation(
initialOnFailure: (err) {
CustomToast.error(t.presentShortError(err)).show(context);
},
initialOnSuccess: () =>
CustomToast.success(t.profile.share.exportConfigToClipboardSuccess)
.show(context),
);
final deleteProfileMutation = useMutation(
initialOnFailure: (err) {
CustomAlertDialog.fromErr(t.presentError(err)).show(context);
},
);
return MenuAnchor(
builder: builder,
menuChildren: [
if (profile case RemoteProfile())
MenuItemButton(
leadingIcon: const Icon(Icons.update),
child: Text(t.profile.update.buttonTxt),
onPressed: () {
if (updateProfileMutation.state.isInProgress) {
return;
}
updateProfileMutation.setFuture(
ref
.read(profilesNotifierProvider.notifier)
.updateProfile(profile as RemoteProfile),
);
},
),
SubmenuButton(
menuChildren: [
if (profile case RemoteProfile(: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);
}
}
},
),
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(profilesNotifierProvider.notifier)
.exportConfigToClipboard(profile),
);
},
),
],
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(
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: [
Directionality(
textDirection: TextDirection.ltr,
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,
),
),
),
Flexible(
child: Text(
remaining.$1,
style: theme.textTheme.bodySmall?.copyWith(color: remaining.$2),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
}
// 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],
),
);
}
}