Refactor profile details page
This commit is contained in:
@@ -6,6 +6,9 @@
|
|||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"disabled": "Disabled"
|
"disabled": "Disabled"
|
||||||
},
|
},
|
||||||
|
"state": {
|
||||||
|
"disable": "Disable"
|
||||||
|
},
|
||||||
"sort": "Sort",
|
"sort": "Sort",
|
||||||
"sortBy": "Sort by"
|
"sortBy": "Sort by"
|
||||||
},
|
},
|
||||||
@@ -78,10 +81,15 @@
|
|||||||
"successMsg": "Profile saved successfully"
|
"successMsg": "Profile saved successfully"
|
||||||
},
|
},
|
||||||
"detailsForm": {
|
"detailsForm": {
|
||||||
"nameHint": "Name",
|
"nameLabel": "Name",
|
||||||
"urlHint": "URL",
|
"nameHint": "Profile name",
|
||||||
|
"urlLabel": "URL",
|
||||||
|
"urlHint": "Full config URL",
|
||||||
"emptyNameMsg": "Name is required",
|
"emptyNameMsg": "Name is required",
|
||||||
"invalidUrlMsg": "Invalid URL"
|
"invalidUrlMsg": "Invalid URL",
|
||||||
|
"lastUpdate": "Last Update",
|
||||||
|
"updateInterval": "Auto Update",
|
||||||
|
"updateIntervalDialogTitle": "Auto Update Interval (in hours)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxies": {
|
"proxies": {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
"enabled": "فعال",
|
"enabled": "فعال",
|
||||||
"disabled": "غیر فعال"
|
"disabled": "غیر فعال"
|
||||||
},
|
},
|
||||||
|
"state": {
|
||||||
|
"disable": "غیر فعال"
|
||||||
|
},
|
||||||
"sort": "مرتبسازی",
|
"sort": "مرتبسازی",
|
||||||
"sortBy": "مرتبسازی براساس"
|
"sortBy": "مرتبسازی براساس"
|
||||||
},
|
},
|
||||||
@@ -78,10 +81,15 @@
|
|||||||
"successMsg": "پروفایل با موفقیت ذخیره شد"
|
"successMsg": "پروفایل با موفقیت ذخیره شد"
|
||||||
},
|
},
|
||||||
"detailsForm": {
|
"detailsForm": {
|
||||||
"nameHint": "نام",
|
"nameLabel": "نام",
|
||||||
"urlHint": "لینک",
|
"nameHint": "نام پروفایل",
|
||||||
|
"urlLabel": "لینک",
|
||||||
|
"urlHint": "آدرس کامل کانفیگ",
|
||||||
"emptyNameMsg": "نام نمیتواند خالی باشد",
|
"emptyNameMsg": "نام نمیتواند خالی باشد",
|
||||||
"invalidUrlMsg": "لینک نامعتبر"
|
"invalidUrlMsg": "لینک نامعتبر",
|
||||||
|
"lastUpdate": "آخرین بروزرسانی",
|
||||||
|
"updateInterval": "بروزرسانی خودکار",
|
||||||
|
"updateIntervalDialogTitle": "فاصله زمانی بروزرسانی خودکار (ساعت)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxies": {
|
"proxies": {
|
||||||
|
|||||||
@@ -93,24 +93,18 @@ class ProfilesRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NewProfileRoute extends GoRouteData {
|
class NewProfileRoute extends GoRouteData {
|
||||||
const NewProfileRoute({this.url, this.profileName});
|
const NewProfileRoute();
|
||||||
static const path = 'profiles/new';
|
static const path = 'profiles/new';
|
||||||
static const name = 'New Profile';
|
static const name = 'New Profile';
|
||||||
final String? url;
|
|
||||||
final String? profileName;
|
|
||||||
|
|
||||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||||
return MaterialPage(
|
return const MaterialPage(
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
name: name,
|
name: name,
|
||||||
child: ProfileDetailPage(
|
child: ProfileDetailPage("new"),
|
||||||
"new",
|
|
||||||
url: url,
|
|
||||||
name: profileName,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,23 @@ class ProfilesRepositoryImpl
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TaskEither<ProfileFailure, Unit> edit(Profile profile) {
|
||||||
|
return exceptionHandler(
|
||||||
|
() async {
|
||||||
|
loggy.debug(
|
||||||
|
"editing profile [${profile.name} (${profile.id})]",
|
||||||
|
);
|
||||||
|
await profilesDao.edit(profile);
|
||||||
|
return right(unit);
|
||||||
|
},
|
||||||
|
(error, stackTrace) {
|
||||||
|
loggy.warning("error editing profile", error, stackTrace);
|
||||||
|
return ProfileUnexpectedFailure(error, stackTrace);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TaskEither<ProfileFailure, Unit> setAsActive(String id) {
|
TaskEither<ProfileFailure, Unit> setAsActive(String id) {
|
||||||
return TaskEither.tryCatch(
|
return TaskEither.tryCatch(
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ abstract class ProfilesRepository {
|
|||||||
|
|
||||||
TaskEither<ProfileFailure, Unit> update(Profile baseProfile);
|
TaskEither<ProfileFailure, Unit> update(Profile baseProfile);
|
||||||
|
|
||||||
|
TaskEither<ProfileFailure, Unit> edit(Profile profile);
|
||||||
|
|
||||||
TaskEither<ProfileFailure, Unit> setAsActive(String id);
|
TaskEither<ProfileFailure, Unit> setAsActive(String id);
|
||||||
|
|
||||||
TaskEither<ProfileFailure, Unit> delete(String id);
|
TaskEither<ProfileFailure, Unit> delete(String id);
|
||||||
|
|||||||
@@ -39,23 +39,31 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
|||||||
loggy.warning('profile with id: [$id] does not exist');
|
loggy.warning('profile with id: [$id] does not exist');
|
||||||
throw const ProfileNotFoundFailure();
|
throw const ProfileNotFoundFailure();
|
||||||
}
|
}
|
||||||
|
_originalProfile = profile;
|
||||||
return ProfileDetailState(profile: profile, isEditing: true);
|
return ProfileDetailState(profile: profile, isEditing: true);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfilesRepository get _profilesRepo => ref.read(profilesRepositoryProvider);
|
ProfilesRepository get _profilesRepo => ref.read(profilesRepositoryProvider);
|
||||||
|
Profile? _originalProfile;
|
||||||
|
|
||||||
void setField({String? name, String? url}) {
|
void setField({String? name, String? url, Option<int>? updateInterval}) {
|
||||||
if (state case AsyncData(:final value)) {
|
if (state case AsyncData(:final value)) {
|
||||||
state = AsyncData(
|
state = AsyncData(
|
||||||
value.copyWith(
|
value.copyWith(
|
||||||
profile: value.profile.copyWith(
|
profile: value.profile.copyWith(
|
||||||
name: name ?? value.profile.name,
|
name: name ?? value.profile.name,
|
||||||
url: url ?? value.profile.url,
|
url: url ?? value.profile.url,
|
||||||
|
options: updateInterval == null
|
||||||
|
? value.profile.options
|
||||||
|
: updateInterval.fold(
|
||||||
|
() => null,
|
||||||
|
(t) => ProfileOptions(updateInterval: Duration(hours: t)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).copyWithPrevious(state);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,14 +74,18 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
|||||||
loggy.debug(
|
loggy.debug(
|
||||||
'saving profile, url: [${profile.url}], name: [${profile.name}]',
|
'saving profile, url: [${profile.url}], name: [${profile.name}]',
|
||||||
);
|
);
|
||||||
state = AsyncData(value.copyWith(save: const MutationInProgress()))
|
state = AsyncData(value.copyWith(save: const MutationInProgress()));
|
||||||
.copyWithPrevious(state);
|
|
||||||
Either<ProfileFailure, Unit>? failureOrSuccess;
|
Either<ProfileFailure, Unit>? failureOrSuccess;
|
||||||
if (profile.name.isBlank || profile.url.isBlank) {
|
if (profile.name.isBlank || profile.url.isBlank) {
|
||||||
loggy.debug('profile save: invalid arguments');
|
loggy.debug('profile save: invalid arguments');
|
||||||
} else if (value.isEditing) {
|
} else if (value.isEditing) {
|
||||||
loggy.debug('updating profile');
|
if (_originalProfile?.url == profile.url) {
|
||||||
failureOrSuccess = await _profilesRepo.update(profile).run();
|
loggy.debug('editing profile');
|
||||||
|
failureOrSuccess = await _profilesRepo.edit(profile).run();
|
||||||
|
} else {
|
||||||
|
loggy.debug('updating profile');
|
||||||
|
failureOrSuccess = await _profilesRepo.update(profile).run();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
loggy.debug('adding profile, url: [${profile.url}]');
|
loggy.debug('adding profile, url: [${profile.url}]');
|
||||||
failureOrSuccess = await _profilesRepo.add(profile).run();
|
failureOrSuccess = await _profilesRepo.add(profile).run();
|
||||||
@@ -87,7 +99,32 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
|||||||
value.save,
|
value.save,
|
||||||
showErrorMessages: true,
|
showErrorMessages: true,
|
||||||
),
|
),
|
||||||
).copyWithPrevious(state);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateProfile() async {
|
||||||
|
if (state case AsyncData(:final value)) {
|
||||||
|
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)
|
||||||
|
.flatMap((_) => _profilesRepo.get(id))
|
||||||
|
.run();
|
||||||
|
state = AsyncData(
|
||||||
|
value.copyWith(
|
||||||
|
update: failureOrUpdatedProfile.match(
|
||||||
|
(l) => MutationFailure(l),
|
||||||
|
(_) => const MutationSuccess(),
|
||||||
|
),
|
||||||
|
profile: failureOrUpdatedProfile.match(
|
||||||
|
(_) => profile,
|
||||||
|
(updatedProfile) => updatedProfile ?? profile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,9 +133,7 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
|||||||
if (value.delete.isInProgress) return;
|
if (value.delete.isInProgress) return;
|
||||||
final profile = value.profile;
|
final profile = value.profile;
|
||||||
loggy.debug('deleting profile');
|
loggy.debug('deleting profile');
|
||||||
state = AsyncData(
|
state = AsyncData(value.copyWith(delete: const MutationInProgress()));
|
||||||
value.copyWith(delete: const MutationState.inProgress()),
|
|
||||||
).copyWithPrevious(state);
|
|
||||||
final result = await _profilesRepo.delete(profile.id).run();
|
final result = await _profilesRepo.delete(profile.id).run();
|
||||||
state = AsyncData(
|
state = AsyncData(
|
||||||
value.copyWith(
|
value.copyWith(
|
||||||
@@ -107,7 +142,7 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
|||||||
(_) => const MutationSuccess(),
|
(_) => const MutationSuccess(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).copyWithPrevious(state);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ class ProfileDetailState with _$ProfileDetailState {
|
|||||||
@Default(false) bool isEditing,
|
@Default(false) bool isEditing,
|
||||||
@Default(false) bool showErrorMessages,
|
@Default(false) bool showErrorMessages,
|
||||||
@Default(MutationState.initial()) MutationState<ProfileFailure> save,
|
@Default(MutationState.initial()) MutationState<ProfileFailure> save,
|
||||||
|
@Default(MutationState.initial()) MutationState<ProfileFailure> update,
|
||||||
@Default(MutationState.initial()) MutationState<ProfileFailure> delete,
|
@Default(MutationState.initial()) MutationState<ProfileFailure> delete,
|
||||||
}) = _ProfileDetailState;
|
}) = _ProfileDetailState;
|
||||||
|
|
||||||
bool get isBusy =>
|
bool get isBusy =>
|
||||||
(save.isInProgress || save is MutationSuccess) ||
|
save.isInProgress || delete.isInProgress || update.isInProgress;
|
||||||
(delete.isInProgress || delete is MutationSuccess);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,26 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:fpdart/fpdart.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hiddify/core/core_providers.dart';
|
import 'package:hiddify/core/core_providers.dart';
|
||||||
import 'package:hiddify/domain/failures.dart';
|
import 'package:hiddify/domain/failures.dart';
|
||||||
import 'package:hiddify/features/common/confirmation_dialogs.dart';
|
import 'package:hiddify/features/common/confirmation_dialogs.dart';
|
||||||
import 'package:hiddify/features/profile_detail/notifier/notifier.dart';
|
import 'package:hiddify/features/profile_detail/notifier/notifier.dart';
|
||||||
|
import 'package:hiddify/features/settings/widgets/widgets.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:humanizer/humanizer.dart';
|
||||||
|
|
||||||
// TODO: test and improve
|
|
||||||
// TODO: prevent popping screen when busy
|
|
||||||
class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
||||||
const ProfileDetailPage(
|
const ProfileDetailPage(this.id, {super.key});
|
||||||
this.id, {
|
|
||||||
super.key,
|
|
||||||
this.url,
|
|
||||||
this.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final String? url;
|
|
||||||
final String? name;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final provider =
|
|
||||||
profileDetailNotifierProvider(id, url: url, profileName: name);
|
|
||||||
final t = ref.watch(translationsProvider);
|
final t = ref.watch(translationsProvider);
|
||||||
final asyncState = ref.watch(provider);
|
|
||||||
final notifier = ref.watch(provider.notifier);
|
|
||||||
|
|
||||||
final themeData = Theme.of(context);
|
final provider = profileDetailNotifierProvider(id);
|
||||||
|
final notifier = ref.watch(provider.notifier);
|
||||||
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
provider.select((data) => data.whenData((value) => value.save)),
|
provider.select((data) => data.whenData((value) => value.save)),
|
||||||
@@ -38,7 +28,7 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
|||||||
if (asyncSave case AsyncData(value: final save)) {
|
if (asyncSave case AsyncData(value: final save)) {
|
||||||
switch (save) {
|
switch (save) {
|
||||||
case MutationFailure(:final failure):
|
case MutationFailure(:final failure):
|
||||||
CustomToast.error(t.printError(failure)).show(context);
|
CustomAlertDialog.fromErr(t.presentError(failure)).show(context);
|
||||||
case MutationSuccess():
|
case MutationSuccess():
|
||||||
CustomToast.success(t.profile.save.successMsg).show(context);
|
CustomToast.success(t.profile.save.successMsg).show(context);
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
@@ -51,10 +41,24 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
provider.select((data) => data.whenData((value) => value.update)),
|
||||||
|
(_, asyncUpdate) {
|
||||||
|
if (asyncUpdate case AsyncData(value: final update)) {
|
||||||
|
switch (update) {
|
||||||
|
case MutationFailure(:final failure):
|
||||||
|
CustomAlertDialog.fromErr(t.presentError(failure)).show(context);
|
||||||
|
case MutationSuccess():
|
||||||
|
CustomToast.success(t.profile.update.successMsg).show(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
provider.select((data) => data.whenData((value) => value.delete)),
|
provider.select((data) => data.whenData((value) => value.delete)),
|
||||||
(_, asyncSave) {
|
(_, asyncDelete) {
|
||||||
if (asyncSave case AsyncData(value: final delete)) {
|
if (asyncDelete case AsyncData(value: final delete)) {
|
||||||
switch (delete) {
|
switch (delete) {
|
||||||
case MutationFailure(:final failure):
|
case MutationFailure(:final failure):
|
||||||
CustomToast.error(t.printError(failure)).show(context);
|
CustomToast.error(t.printError(failure)).show(context);
|
||||||
@@ -70,52 +74,125 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (asyncState) {
|
switch (ref.watch(provider)) {
|
||||||
case AsyncData(value: final state):
|
case AsyncData(value: final state):
|
||||||
|
final showLoadingOverlay = state.isBusy ||
|
||||||
|
state.save is MutationSuccess ||
|
||||||
|
state.delete is MutationSuccess;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Scaffold(
|
Scaffold(
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
pinned: true,
|
|
||||||
title: Text(t.profile.detailsPageTitle),
|
title: Text(t.profile.detailsPageTitle),
|
||||||
),
|
pinned: true,
|
||||||
const SliverGap(8),
|
actions: [
|
||||||
SliverPadding(
|
if (state.isEditing)
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
PopupMenuButton(
|
||||||
sliver: Form(
|
itemBuilder: (context) {
|
||||||
autovalidateMode: state.showErrorMessages
|
return [
|
||||||
? AutovalidateMode.always
|
PopupMenuItem(
|
||||||
: AutovalidateMode.disabled,
|
child: Text(t.profile.update.buttonTxt),
|
||||||
child: SliverList(
|
onTap: () async {
|
||||||
delegate: SliverChildListDelegate(
|
await notifier.updateProfile();
|
||||||
[
|
},
|
||||||
const Gap(8),
|
),
|
||||||
CustomTextFormField(
|
PopupMenuItem(
|
||||||
initialValue: state.profile.name,
|
child: Text(t.profile.delete.buttonTxt),
|
||||||
onChanged: (value) =>
|
onTap: () async {
|
||||||
notifier.setField(name: value),
|
final deleteConfirmed =
|
||||||
validator: (value) => (value?.isEmpty ?? true)
|
await showConfirmationDialog(
|
||||||
? t.profile.detailsForm.emptyNameMsg
|
context,
|
||||||
: null,
|
title: t.profile.delete.buttonTxt,
|
||||||
label: t.profile.detailsForm.nameHint,
|
message: t.profile.delete.confirmationMsg,
|
||||||
),
|
);
|
||||||
const Gap(16),
|
if (deleteConfirmed) {
|
||||||
CustomTextFormField(
|
await notifier.delete();
|
||||||
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.urlHint.toUpperCase(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
Form(
|
||||||
|
autovalidateMode: state.showErrorMessages
|
||||||
|
? AutovalidateMode.always
|
||||||
|
: AutovalidateMode.disabled,
|
||||||
|
child: SliverList.list(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
child: CustomTextFormField(
|
||||||
|
initialValue: state.profile.name,
|
||||||
|
onChanged: (value) =>
|
||||||
|
notifier.setField(name: value),
|
||||||
|
validator: (value) => (value?.isEmpty ?? true)
|
||||||
|
? t.profile.detailsForm.emptyNameMsg
|
||||||
|
: null,
|
||||||
|
label: t.profile.detailsForm.nameLabel,
|
||||||
|
hint: t.profile.detailsForm.nameHint,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
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),
|
||||||
|
subtitle: Text(state.profile.lastUpdate.format()),
|
||||||
|
dense: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
@@ -133,33 +210,14 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
|||||||
spacing: 12,
|
spacing: 12,
|
||||||
overflowAlignment: OverflowBarAlignment.end,
|
overflowAlignment: OverflowBarAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
if (state.isEditing)
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final deleteConfirmed =
|
|
||||||
await showConfirmationDialog(
|
|
||||||
context,
|
|
||||||
title: t.profile.delete.buttonTxt,
|
|
||||||
message: t.profile.delete.confirmationMsg,
|
|
||||||
);
|
|
||||||
if (deleteConfirmed) {
|
|
||||||
await notifier.delete();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: MaterialStatePropertyAll(
|
|
||||||
themeData.colorScheme.errorContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
t.profile.delete.buttonTxt,
|
|
||||||
style: TextStyle(
|
|
||||||
color: themeData
|
|
||||||
.colorScheme.onErrorContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
|
onPressed: context.pop,
|
||||||
|
child: Text(
|
||||||
|
MaterialLocalizations.of(context)
|
||||||
|
.cancelButtonLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
onPressed: notifier.save,
|
onPressed: notifier.save,
|
||||||
child: Text(t.profile.save.buttonText),
|
child: Text(t.profile.save.buttonText),
|
||||||
),
|
),
|
||||||
@@ -172,7 +230,7 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (state.isBusy)
|
if (showLoadingOverlay)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
@@ -190,7 +248,19 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: handle loading and error states
|
case AsyncError(:final error):
|
||||||
|
return Scaffold(
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
title: Text(t.profile.detailsPageTitle),
|
||||||
|
pinned: true,
|
||||||
|
),
|
||||||
|
SliverErrorBodyPlaceholder(t.printError(error)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return const Scaffold();
|
return const Scaffold();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class SettingsInputDialog<T> extends HookConsumerWidget with PresLogger {
|
|||||||
this.mapTo,
|
this.mapTo,
|
||||||
this.validator,
|
this.validator,
|
||||||
this.resetValue,
|
this.resetValue,
|
||||||
|
this.optionalAction,
|
||||||
this.icon,
|
this.icon,
|
||||||
this.digitsOnly = false,
|
this.digitsOnly = false,
|
||||||
});
|
});
|
||||||
@@ -23,6 +24,7 @@ class SettingsInputDialog<T> extends HookConsumerWidget with PresLogger {
|
|||||||
final T? Function(String value)? mapTo;
|
final T? Function(String value)? mapTo;
|
||||||
final bool Function(String value)? validator;
|
final bool Function(String value)? validator;
|
||||||
final T? resetValue;
|
final T? resetValue;
|
||||||
|
final (String text, VoidCallback)? optionalAction;
|
||||||
final IconData? icon;
|
final IconData? icon;
|
||||||
final bool digitsOnly;
|
final bool digitsOnly;
|
||||||
|
|
||||||
@@ -55,6 +57,15 @@ class SettingsInputDialog<T> extends HookConsumerWidget with PresLogger {
|
|||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
if (optionalAction != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
optionalAction!.$2();
|
||||||
|
await Navigator.of(context)
|
||||||
|
.maybePop(T == String ? textController.value.text : null);
|
||||||
|
},
|
||||||
|
child: Text(optionalAction!.$1.toUpperCase()),
|
||||||
|
),
|
||||||
if (resetValue != null)
|
if (resetValue != null)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class CustomTextFormField extends HookConsumerWidget {
|
|||||||
this.suffixIcon,
|
this.suffixIcon,
|
||||||
this.label,
|
this.label,
|
||||||
this.hint,
|
this.hint,
|
||||||
this.maxLines = 1,
|
this.maxLines,
|
||||||
this.isDense = false,
|
this.isDense = false,
|
||||||
this.autoValidate = false,
|
this.autoValidate = false,
|
||||||
this.autoCorrect = false,
|
this.autoCorrect = false,
|
||||||
@@ -26,7 +26,7 @@ class CustomTextFormField extends HookConsumerWidget {
|
|||||||
final Widget? suffixIcon;
|
final Widget? suffixIcon;
|
||||||
final String? label;
|
final String? label;
|
||||||
final String? hint;
|
final String? hint;
|
||||||
final int maxLines;
|
final int? maxLines;
|
||||||
final bool isDense;
|
final bool isDense;
|
||||||
final bool autoValidate;
|
final bool autoValidate;
|
||||||
final bool autoCorrect;
|
final bool autoCorrect;
|
||||||
|
|||||||
7
lib/utils/date_time_formatter.dart
Normal file
7
lib/utils/date_time_formatter.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
extension DateTimeFormatter on DateTime {
|
||||||
|
String format() {
|
||||||
|
return DateFormat.yMMMd().add_Hm().format(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ export 'callback_debouncer.dart';
|
|||||||
export 'custom_log_printer.dart';
|
export 'custom_log_printer.dart';
|
||||||
export 'custom_loggers.dart';
|
export 'custom_loggers.dart';
|
||||||
export 'custom_text_form_field.dart';
|
export 'custom_text_form_field.dart';
|
||||||
|
export 'date_time_formatter.dart';
|
||||||
export 'link_parsers.dart';
|
export 'link_parsers.dart';
|
||||||
export 'mutation_state.dart';
|
export 'mutation_state.dart';
|
||||||
export 'number_formatters.dart';
|
export 'number_formatters.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user