Files
MWClash/lib/views/profiles/edit.dart
chen08209 6e404ab19c Fix windows some issues
Optimize overwrite handle

Optimize access control page

Optimize some details
2025-12-12 14:33:03 +08:00

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,
),
),
),
),
);
}
}