Add local profile
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
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';
|
||||
@@ -16,16 +15,4 @@ class ActiveProfile extends _$ActiveProfile with AppLogger {
|
||||
.watchActiveProfile()
|
||||
.map((event) => event.getOrElse((l) => throw l));
|
||||
}
|
||||
|
||||
Future<Unit?> updateProfile() async {
|
||||
if (state case AsyncData(value: final profile?)) {
|
||||
loggy.debug("updating active profile");
|
||||
return ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.update(profile)
|
||||
.getOrElse((l) => throw l)
|
||||
.run();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,10 @@ class ProfileTile extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
|
||||
final subInfo = profile.subInfo;
|
||||
final subInfo = switch (profile) {
|
||||
RemoteProfile(:final subInfo) => subInfo,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
final effectiveMargin = isMain
|
||||
? const EdgeInsets.symmetric(horizontal: 16, vertical: 8)
|
||||
@@ -60,17 +63,19 @@ class ProfileTile extends HookConsumerWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Semantics(
|
||||
sortKey: const OrdinalSortKey(1),
|
||||
child: ProfileActionButton(profile, !isMain),
|
||||
if (profile is RemoteProfile || !isMain) ...[
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Semantics(
|
||||
sortKey: const OrdinalSortKey(1),
|
||||
child: ProfileActionButton(profile, !isMain),
|
||||
),
|
||||
),
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
color: effectiveOutlineColor,
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
color: effectiveOutlineColor,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
@@ -177,7 +182,7 @@ class ProfileActionButton extends HookConsumerWidget {
|
||||
CustomToast.success(t.profile.update.successMsg).show(context),
|
||||
);
|
||||
|
||||
if (!showAllActions) {
|
||||
if (profile case RemoteProfile() when !showAllActions) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
enabled: !updateProfileMutation.state.isInProgress,
|
||||
@@ -191,7 +196,7 @@ class ProfileActionButton extends HookConsumerWidget {
|
||||
updateProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.updateProfile(profile),
|
||||
.updateProfile(profile as RemoteProfile),
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.update),
|
||||
@@ -250,20 +255,21 @@ class ProfileActionsMenu extends HookConsumerWidget {
|
||||
return MenuAnchor(
|
||||
builder: builder,
|
||||
menuChildren: [
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.edit),
|
||||
child: Text(t.profile.edit.buttonTxt),
|
||||
|
||||
@@ -19,7 +19,7 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
||||
}) async {
|
||||
if (id == 'new') {
|
||||
return ProfileDetailState(
|
||||
profile: Profile(
|
||||
profile: RemoteProfile(
|
||||
id: const Uuid().v4(),
|
||||
active: true,
|
||||
name: profileName ?? "",
|
||||
@@ -52,15 +52,20 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
||||
if (state case AsyncData(:final value)) {
|
||||
state = AsyncData(
|
||||
value.copyWith(
|
||||
profile: value.profile.copyWith(
|
||||
name: name ?? value.profile.name,
|
||||
url: url ?? value.profile.url,
|
||||
options: updateInterval == null
|
||||
? value.profile.options
|
||||
: updateInterval.fold(
|
||||
() => null,
|
||||
(t) => ProfileOptions(updateInterval: Duration(hours: t)),
|
||||
),
|
||||
profile: value.profile.map(
|
||||
remote: (rp) => rp.copyWith(
|
||||
name: name ?? rp.name,
|
||||
url: url ?? rp.url,
|
||||
options: updateInterval == null
|
||||
? rp.options
|
||||
: updateInterval.fold(
|
||||
() => null,
|
||||
(t) => ProfileOptions(
|
||||
updateInterval: Duration(hours: t),
|
||||
),
|
||||
),
|
||||
),
|
||||
local: (lp) => lp.copyWith(name: name ?? lp.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -71,24 +76,33 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
||||
if (state case AsyncData(:final value)) {
|
||||
if (value.save.isInProgress) return;
|
||||
final profile = value.profile;
|
||||
loggy.debug(
|
||||
'saving profile, url: [${profile.url}], name: [${profile.name}]',
|
||||
);
|
||||
state = AsyncData(value.copyWith(save: const MutationInProgress()));
|
||||
Either<ProfileFailure, Unit>? failureOrSuccess;
|
||||
if (profile.name.isBlank || profile.url.isBlank) {
|
||||
loggy.debug('profile save: invalid arguments');
|
||||
} else if (value.isEditing) {
|
||||
if (_originalProfile?.url == profile.url) {
|
||||
state = AsyncData(value.copyWith(save: const MutationInProgress()));
|
||||
switch (profile) {
|
||||
case RemoteProfile():
|
||||
loggy.debug(
|
||||
'saving profile, url: [${profile.url}], name: [${profile.name}]',
|
||||
);
|
||||
if (profile.name.isBlank || profile.url.isBlank) {
|
||||
loggy.debug('profile save: invalid arguments');
|
||||
} else if (value.isEditing) {
|
||||
if (_originalProfile case RemoteProfile(:final url)
|
||||
when url == profile.url) {
|
||||
loggy.debug('editing profile');
|
||||
failureOrSuccess = await _profilesRepo.edit(profile).run();
|
||||
} else {
|
||||
loggy.debug('updating profile');
|
||||
failureOrSuccess = await _profilesRepo.update(profile).run();
|
||||
}
|
||||
} else {
|
||||
loggy.debug('adding profile, url: [${profile.url}]');
|
||||
failureOrSuccess = await _profilesRepo.add(profile).run();
|
||||
}
|
||||
case LocalProfile() when value.isEditing:
|
||||
loggy.debug('editing profile');
|
||||
failureOrSuccess = await _profilesRepo.edit(profile).run();
|
||||
} else {
|
||||
loggy.debug('updating profile');
|
||||
failureOrSuccess = await _profilesRepo.update(profile).run();
|
||||
}
|
||||
} else {
|
||||
loggy.debug('adding profile, url: [${profile.url}]');
|
||||
failureOrSuccess = await _profilesRepo.add(profile).run();
|
||||
default:
|
||||
loggy.warning("local profile can't be added manually");
|
||||
}
|
||||
state = AsyncData(
|
||||
value.copyWith(
|
||||
@@ -105,12 +119,17 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
||||
|
||||
Future<void> updateProfile() async {
|
||||
if (state case AsyncData(:final value)) {
|
||||
loggy.debug('updating profile');
|
||||
if (value.profile case LocalProfile()) {
|
||||
loggy.warning("local profile can't be updated");
|
||||
return;
|
||||
}
|
||||
if (value.update.isInProgress || !value.isEditing) return;
|
||||
final profile = value.profile;
|
||||
loggy.debug('updating profile');
|
||||
state = AsyncData(value.copyWith(update: const MutationInProgress()));
|
||||
final failureOrUpdatedProfile = await _profilesRepo
|
||||
.update(profile)
|
||||
.update(profile as RemoteProfile)
|
||||
.flatMap((_) => _profilesRepo.get(id))
|
||||
.run();
|
||||
state = AsyncData(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:fpdart/fpdart.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/core_providers.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/profile_detail/notifier/notifier.dart';
|
||||
import 'package:hiddify/features/settings/widgets/widgets.dart';
|
||||
@@ -93,12 +94,13 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Text(t.profile.update.buttonTxt),
|
||||
onTap: () async {
|
||||
await notifier.updateProfile();
|
||||
},
|
||||
),
|
||||
if (state.profile case RemoteProfile())
|
||||
PopupMenuItem(
|
||||
child: Text(t.profile.update.buttonTxt),
|
||||
onTap: () async {
|
||||
await notifier.updateProfile();
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(t.profile.delete.buttonTxt),
|
||||
onTap: () async {
|
||||
@@ -140,52 +142,55 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
||||
hint: t.profile.detailsForm.nameHint,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
if (state.profile
|
||||
case RemoteProfile(:final url, :final options)) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: CustomTextFormField(
|
||||
initialValue: url,
|
||||
onChanged: (value) =>
|
||||
notifier.setField(url: value),
|
||||
validator: (value) =>
|
||||
(value != null && !isUrl(value))
|
||||
? t.profile.detailsForm.invalidUrlMsg
|
||||
: null,
|
||||
label: t.profile.detailsForm.urlLabel,
|
||||
hint: t.profile.detailsForm.urlHint,
|
||||
),
|
||||
),
|
||||
child: CustomTextFormField(
|
||||
initialValue: state.profile.url,
|
||||
onChanged: (value) => notifier.setField(url: value),
|
||||
validator: (value) =>
|
||||
(value != null && !isUrl(value))
|
||||
? t.profile.detailsForm.invalidUrlMsg
|
||||
: null,
|
||||
label: t.profile.detailsForm.urlLabel,
|
||||
hint: t.profile.detailsForm.urlHint,
|
||||
ListTile(
|
||||
title: Text(t.profile.detailsForm.updateInterval),
|
||||
subtitle: Text(
|
||||
options?.updateInterval.toApproximateTime(
|
||||
isRelativeToNow: false,
|
||||
) ??
|
||||
t.general.toggle.disabled,
|
||||
),
|
||||
leading: const Icon(Icons.update),
|
||||
onTap: () async {
|
||||
final intervalInHours = await SettingsInputDialog(
|
||||
title: t.profile.detailsForm
|
||||
.updateIntervalDialogTitle,
|
||||
initialValue: options?.updateInterval.inHours,
|
||||
optionalAction: (
|
||||
t.general.state.disable,
|
||||
() =>
|
||||
notifier.setField(updateInterval: none()),
|
||||
),
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (intervalInHours == null) return;
|
||||
notifier.setField(
|
||||
updateInterval: optionOf(intervalInHours),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.profile.detailsForm.updateInterval),
|
||||
subtitle: Text(
|
||||
state.profile.options?.updateInterval
|
||||
.toApproximateTime(
|
||||
isRelativeToNow: false,
|
||||
) ??
|
||||
t.general.toggle.disabled,
|
||||
),
|
||||
leading: const Icon(Icons.update),
|
||||
onTap: () async {
|
||||
final intervalInHours = await SettingsInputDialog(
|
||||
title: t.profile.detailsForm
|
||||
.updateIntervalDialogTitle,
|
||||
initialValue:
|
||||
state.profile.options?.updateInterval.inHours,
|
||||
optionalAction: (
|
||||
t.general.state.disable,
|
||||
() => notifier.setField(updateInterval: none()),
|
||||
),
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (intervalInHours == null) return;
|
||||
notifier.setField(
|
||||
updateInterval: optionOf(intervalInHours),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
if (state.isEditing)
|
||||
ListTile(
|
||||
title: Text(t.profile.detailsForm.lastUpdate),
|
||||
|
||||
@@ -49,21 +49,39 @@ class ProfilesNotifier extends _$ProfilesNotifier with AppLogger {
|
||||
}).run();
|
||||
}
|
||||
|
||||
Future<Unit> addProfile(String url) async {
|
||||
Future<Unit> addProfile(String rawInput) async {
|
||||
final activeProfile = await ref.read(activeProfileProvider.future);
|
||||
final markAsActive =
|
||||
activeProfile == null || ref.read(markNewProfileActiveProvider);
|
||||
loggy.debug("adding profile, url: [$url]");
|
||||
return ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.addByUrl(url, markAsActive: markAsActive)
|
||||
.getOrElse((l) {
|
||||
loggy.warning("failed to add profile: $l");
|
||||
throw l;
|
||||
}).run();
|
||||
if (LinkParser.parse(rawInput) case (final link)?) {
|
||||
loggy.debug("adding profile, url: [${link.url}]");
|
||||
return ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.addByUrl(link.url, markAsActive: markAsActive)
|
||||
.getOrElse((l) {
|
||||
loggy.warning("failed to add profile: $l");
|
||||
throw l;
|
||||
}).run();
|
||||
} else if (LinkParser.protocol(rawInput) case (final parsed)?) {
|
||||
loggy.debug("adding profile, content");
|
||||
return ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.addByContent(
|
||||
parsed.content,
|
||||
name: parsed.name,
|
||||
markAsActive: markAsActive,
|
||||
)
|
||||
.getOrElse((l) {
|
||||
loggy.warning("failed to add profile: $l");
|
||||
throw l;
|
||||
}).run();
|
||||
} else {
|
||||
loggy.debug("invalid content");
|
||||
throw const ProfileInvalidUrlFailure();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Unit?> updateProfile(Profile profile) async {
|
||||
Future<Unit?> updateProfile(RemoteProfile profile) async {
|
||||
loggy.debug("updating profile");
|
||||
return ref
|
||||
.read(profilesRepositoryProvider)
|
||||
|
||||
@@ -50,20 +50,22 @@ class ProfilesUpdateNotifier extends _$ProfilesUpdateNotifier with AppLogger {
|
||||
await ref.read(profilesRepositoryProvider).watchAll().first;
|
||||
if (failureOrProfiles case Right(value: final profiles)) {
|
||||
for (final profile in profiles) {
|
||||
loggy.debug("checking profile: [${profile.name}]");
|
||||
final updateInterval = profile.options?.updateInterval;
|
||||
if (updateInterval != null &&
|
||||
updateInterval <=
|
||||
DateTime.now().difference(profile.lastUpdate)) {
|
||||
final failureOrSuccess = await ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.update(profile)
|
||||
.run();
|
||||
state = AsyncData(
|
||||
(name: profile.name, failureOrSuccess: failureOrSuccess),
|
||||
);
|
||||
} else {
|
||||
loggy.debug("skipping profile: [${profile.name}]");
|
||||
if (profile case RemoteProfile()) {
|
||||
loggy.debug("checking profile: [${profile.name}]");
|
||||
final updateInterval = profile.options?.updateInterval;
|
||||
if (updateInterval != null &&
|
||||
updateInterval <=
|
||||
DateTime.now().difference(profile.lastUpdate)) {
|
||||
final failureOrSuccess = await ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.update(profile)
|
||||
.run();
|
||||
state = AsyncData(
|
||||
(name: profile.name, failureOrSuccess: failureOrSuccess),
|
||||
);
|
||||
} else {
|
||||
loggy.debug("skipping profile: [${profile.name}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.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/qr_code_scanner_screen.dart';
|
||||
import 'package:hiddify/features/profiles/notifier/notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
@@ -29,8 +30,13 @@ class AddProfileModal extends HookConsumerWidget {
|
||||
final addProfileMutation = useMutation(
|
||||
initialOnFailure: (err) {
|
||||
mutationTriggered.value = false;
|
||||
// CustomToast.error(t.presentError(err)).show(context);
|
||||
CustomAlertDialog.fromErr(t.presentError(err)).show(context);
|
||||
if (err case ProfileInvalidUrlFailure()) {
|
||||
CustomToast.error(
|
||||
t.profile.add.invalidUrlMsg,
|
||||
).show(context);
|
||||
} else {
|
||||
CustomAlertDialog.fromErr(t.presentError(err)).show(context);
|
||||
}
|
||||
},
|
||||
initialOnSuccess: () {
|
||||
CustomToast.success(t.profile.save.successMsg).show(context);
|
||||
@@ -102,24 +108,15 @@ class AddProfileModal extends HookConsumerWidget {
|
||||
size: buttonWidth,
|
||||
onTap: () async {
|
||||
final captureResult =
|
||||
await Clipboard.getData(Clipboard.kTextPlain);
|
||||
final link =
|
||||
LinkParser.parse(captureResult?.text ?? '');
|
||||
if (link != null && context.mounted) {
|
||||
if (addProfileMutation.state.isInProgress) return;
|
||||
mutationTriggered.value = true;
|
||||
addProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.addProfile(link.url),
|
||||
);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
CustomToast.error(
|
||||
t.profile.add.invalidUrlMsg,
|
||||
).show(context);
|
||||
}
|
||||
}
|
||||
await Clipboard.getData(Clipboard.kTextPlain)
|
||||
.then((value) => value?.text ?? '');
|
||||
if (addProfileMutation.state.isInProgress) return;
|
||||
mutationTriggered.value = true;
|
||||
addProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.addProfile(captureResult),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(buttonsGap),
|
||||
@@ -134,24 +131,15 @@ class AddProfileModal extends HookConsumerWidget {
|
||||
await const QRCodeScannerScreen()
|
||||
.open(context);
|
||||
if (captureResult == null) return;
|
||||
final link = LinkParser.simple(captureResult);
|
||||
if (link != null && context.mounted) {
|
||||
if (addProfileMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
mutationTriggered.value = true;
|
||||
addProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.addProfile(link.url),
|
||||
);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
CustomToast.error(
|
||||
t.profile.add.invalidUrlMsg,
|
||||
).show(context);
|
||||
}
|
||||
if (addProfileMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
mutationTriggered.value = true;
|
||||
addProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.addProfile(captureResult),
|
||||
);
|
||||
},
|
||||
)
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user