Support core status check and force restart Optimize proxies page and access page Update flutter and pub dependencies Update go version Optimize more details
345 lines
11 KiB
Dart
345 lines
11 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/core/controller.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/models/models.dart';
|
|
import 'package:fl_clash/pages/editor.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
class EditProfileView extends StatefulWidget {
|
|
final Profile profile;
|
|
final BuildContext context;
|
|
|
|
const EditProfileView({
|
|
super.key,
|
|
required this.context,
|
|
required this.profile,
|
|
});
|
|
|
|
@override
|
|
State<EditProfileView> createState() => _EditProfileViewState();
|
|
}
|
|
|
|
class _EditProfileViewState extends State<EditProfileView> {
|
|
late TextEditingController labelController;
|
|
late TextEditingController urlController;
|
|
late TextEditingController autoUpdateDurationController;
|
|
late bool autoUpdate;
|
|
String? rawText;
|
|
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
|
final fileInfoNotifier = ValueNotifier<FileInfo?>(null);
|
|
Uint8List? fileData;
|
|
|
|
Profile get profile => widget.profile;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
labelController = TextEditingController(text: widget.profile.label);
|
|
urlController = TextEditingController(text: widget.profile.url);
|
|
autoUpdate = widget.profile.autoUpdate;
|
|
autoUpdateDurationController = TextEditingController(
|
|
text: widget.profile.autoUpdateDuration.inMinutes.toString(),
|
|
);
|
|
appPath.getProfilePath(widget.profile.id).then((path) async {
|
|
fileInfoNotifier.value = await _getFileInfo(path);
|
|
});
|
|
}
|
|
|
|
Future<void> _handleConfirm() async {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
final appController = globalState.appController;
|
|
Profile profile = this.profile.copyWith(
|
|
url: urlController.text,
|
|
label: labelController.text,
|
|
autoUpdate: autoUpdate,
|
|
autoUpdateDuration: Duration(
|
|
minutes: int.parse(autoUpdateDurationController.text),
|
|
),
|
|
);
|
|
final hasUpdate = widget.profile.url != profile.url;
|
|
if (fileData != null) {
|
|
if (profile.type == ProfileType.url && autoUpdate) {
|
|
final res = await globalState.showMessage(
|
|
title: appLocalizations.tip,
|
|
message: TextSpan(text: appLocalizations.profileHasUpdate),
|
|
);
|
|
if (res == true) {
|
|
profile = profile.copyWith(autoUpdate: false);
|
|
}
|
|
}
|
|
appController.setProfileAndAutoApply(await profile.saveFile(fileData!));
|
|
} else if (!hasUpdate) {
|
|
appController.setProfileAndAutoApply(profile);
|
|
} else {
|
|
globalState.appController.safeRun(() async {
|
|
await Future.delayed(commonDuration);
|
|
if (hasUpdate) {
|
|
await appController.updateProfile(profile);
|
|
}
|
|
});
|
|
}
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
}
|
|
|
|
void _setAutoUpdate(bool value) {
|
|
if (autoUpdate == value) return;
|
|
setState(() {
|
|
autoUpdate = value;
|
|
});
|
|
}
|
|
|
|
Future<FileInfo?> _getFileInfo(String path) async {
|
|
final file = File(path);
|
|
if (!await file.exists()) {
|
|
return null;
|
|
}
|
|
final lastModified = await file.lastModified();
|
|
final size = await file.length();
|
|
return FileInfo(size: size, lastModified: lastModified);
|
|
}
|
|
|
|
Future<void> _handleSaveEdit(BuildContext context, String data) async {
|
|
final message = await globalState.appController.safeRun<String>(() async {
|
|
final message = await coreController.validateConfig(data);
|
|
return message;
|
|
}, silence: false);
|
|
if (message?.isNotEmpty == true) {
|
|
globalState.showMessage(
|
|
title: appLocalizations.tip,
|
|
message: TextSpan(text: message),
|
|
);
|
|
return;
|
|
}
|
|
if (context.mounted) {
|
|
Navigator.of(context).pop(data);
|
|
}
|
|
}
|
|
|
|
Future<void> _editProfileFile() async {
|
|
if (rawText == null) {
|
|
final profilePath = await appPath.getProfilePath(widget.profile.id);
|
|
final file = File(profilePath);
|
|
if (await file.exists()) {
|
|
rawText = await file.readAsString();
|
|
}
|
|
}
|
|
if (!mounted) return;
|
|
final title = widget.profile.label ?? widget.profile.id;
|
|
final editorPage = EditorPage(
|
|
title: title,
|
|
content: rawText!,
|
|
onSave: (context, _, content) {
|
|
_handleSaveEdit(context, content);
|
|
},
|
|
onPop: (context, _, content) async {
|
|
if (content == rawText) {
|
|
return true;
|
|
}
|
|
final res = await globalState.showMessage(
|
|
title: title,
|
|
message: TextSpan(text: appLocalizations.hasCacheChange),
|
|
);
|
|
if (res == true && context.mounted) {
|
|
_handleSaveEdit(context, content);
|
|
} else {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
);
|
|
final data = await BaseNavigator.push<String>(context, editorPage);
|
|
if (data == null) {
|
|
return;
|
|
}
|
|
rawText = data;
|
|
fileData = Uint8List.fromList(utf8.encode(data));
|
|
fileInfoNotifier.value = fileInfoNotifier.value?.copyWith(
|
|
size: fileData?.length ?? 0,
|
|
lastModified: DateTime.now(),
|
|
);
|
|
}
|
|
|
|
Future<void> _uploadProfileFile() async {
|
|
final platformFile = await globalState.appController.safeRun(
|
|
picker.pickerFile,
|
|
);
|
|
if (platformFile?.bytes == null) return;
|
|
fileData = platformFile?.bytes;
|
|
fileInfoNotifier.value = fileInfoNotifier.value?.copyWith(
|
|
size: fileData?.length ?? 0,
|
|
lastModified: DateTime.now(),
|
|
);
|
|
}
|
|
|
|
Future<void> _handleBack() async {
|
|
final res = await globalState.showMessage(
|
|
title: appLocalizations.tip,
|
|
message: TextSpan(text: appLocalizations.fileIsUpdate),
|
|
);
|
|
if (res == true) {
|
|
_handleConfirm();
|
|
} else {
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final items = [
|
|
ListItem(
|
|
title: TextFormField(
|
|
textInputAction: TextInputAction.next,
|
|
controller: labelController,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: appLocalizations.name,
|
|
),
|
|
validator: (String? value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.profileNameNullValidationDesc;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
if (widget.profile.type == ProfileType.url) ...[
|
|
ListItem(
|
|
title: TextFormField(
|
|
textInputAction: TextInputAction.next,
|
|
keyboardType: TextInputType.url,
|
|
controller: urlController,
|
|
maxLines: 5,
|
|
minLines: 1,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: appLocalizations.url,
|
|
),
|
|
validator: (String? value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.profileUrlNullValidationDesc;
|
|
}
|
|
if (!value.isUrl) {
|
|
return appLocalizations.profileUrlInvalidValidationDesc;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
ListItem.switchItem(
|
|
title: Text(appLocalizations.autoUpdate),
|
|
delegate: SwitchDelegate<bool>(
|
|
value: autoUpdate,
|
|
onChanged: _setAutoUpdate,
|
|
),
|
|
),
|
|
if (autoUpdate)
|
|
ListItem(
|
|
title: TextFormField(
|
|
textInputAction: TextInputAction.next,
|
|
controller: autoUpdateDurationController,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
labelText: appLocalizations.autoUpdateInterval,
|
|
),
|
|
validator: (String? value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations
|
|
.profileAutoUpdateIntervalNullValidationDesc;
|
|
}
|
|
try {
|
|
int.parse(value);
|
|
} catch (_) {
|
|
return appLocalizations
|
|
.profileAutoUpdateIntervalInvalidValidationDesc;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
],
|
|
ValueListenableBuilder<FileInfo?>(
|
|
valueListenable: fileInfoNotifier,
|
|
builder: (_, fileInfo, _) {
|
|
return FadeThroughBox(
|
|
alignment: Alignment.centerLeft,
|
|
child: fileInfo == null
|
|
? Container()
|
|
: ListItem(
|
|
title: Text(appLocalizations.profile),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 4),
|
|
Text(fileInfo.desc),
|
|
const SizedBox(height: 8),
|
|
Wrap(
|
|
runSpacing: 6,
|
|
spacing: 12,
|
|
children: [
|
|
CommonChip(
|
|
avatar: const Icon(Icons.edit),
|
|
label: appLocalizations.edit,
|
|
onPressed: _editProfileFile,
|
|
),
|
|
CommonChip(
|
|
avatar: const Icon(Icons.upload),
|
|
label: appLocalizations.upload,
|
|
onPressed: _uploadProfileFile,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
];
|
|
return CommonPopScope(
|
|
onPop: (context) {
|
|
if (fileData == null) {
|
|
return true;
|
|
}
|
|
_handleBack();
|
|
return false;
|
|
},
|
|
child: FloatLayout(
|
|
floatingWidget: FloatWrapper(
|
|
child: FloatingActionButton.extended(
|
|
heroTag: null,
|
|
onPressed: _handleConfirm,
|
|
label: Text(appLocalizations.save),
|
|
icon: const Icon(Icons.save),
|
|
),
|
|
),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
child: ListView.separated(
|
|
padding: kMaterialListPadding.copyWith(bottom: 72),
|
|
itemBuilder: (_, index) {
|
|
return items[index];
|
|
},
|
|
separatorBuilder: (_, _) {
|
|
return const SizedBox(height: 24);
|
|
},
|
|
itemCount: items.length,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|