Create management portal in Flutter #39

This commit is contained in:
Scott E. Graves 2025-03-14 18:41:53 -05:00
parent 9d083a1d93
commit 45ea5bab8f
4 changed files with 150 additions and 99 deletions

View File

@ -1,8 +1,9 @@
const String addMountTitle = 'New Mount Settings';
const String appTitle = 'Repertory Management Portal';
const addMountTitle = 'New Mount Settings';
const appTitle = 'Repertory Management Portal';
const databaseTypeList = ['rocksdb', 'sqlite'];
const downloadTypeList = ['default', 'direct', 'ring_buffer'];
const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace'];
const padding = 15.0;
const protocolTypeList = ['http', 'https'];
const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia'];
const ringBufferSizeList = ['128', '256', '512', '1024', '2048'];

View File

@ -1,15 +1,17 @@
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:repertory/constants.dart' as constants;
typedef Validator = bool Function(String);
bool containsRestrictedChar(String value) {
const invalidChars = [
// ignore: prefer_function_declarations_over_variables
final Validator noRestrictedChars = (value) {
return [
'!',
'"',
'\$',
'&',
'\'',
"'",
'(',
')',
'*',
@ -23,10 +25,26 @@ bool containsRestrictedChar(String value) {
'{',
'}',
'|',
];
return invalidChars.firstWhereOrNull((char) => value.contains(char)) != null;
].firstWhereOrNull((char) => value.contains(char)) ==
null;
};
// ignore: prefer_function_declarations_over_variables
final Validator notEmptyValidator = (value) => value.isNotEmpty;
// ignore: prefer_function_declarations_over_variables
final Validator trimNotEmptyValidator = (value) => value.trim().isNotEmpty;
createUriValidator<Validator>({host, port}) {
return (value) =>
Uri.tryParse('http://${host ?? value}:${port ?? value}/') != null;
}
createHostNameOrIpValidators() => <Validator>[
trimNotEmptyValidator,
createUriValidator(port: 9000),
];
Map<String, dynamic> createDefaultSettings(String mountType) {
switch (mountType) {
case 'Encrypt':
@ -80,16 +98,19 @@ String getBaseUri() {
List<Validator> getSettingValidators(String settingPath) {
switch (settingPath) {
case 'ApiAuth':
return [(value) => value.isNotEmpty];
return [notEmptyValidator];
case 'DatabaseType':
return [(value) => constants.databaseTypeList.contains(value)];
case 'PreferredDownloadType':
return [(value) => constants.downloadTypeList.contains(value)];
case 'EventLevel':
return [(value) => constants.eventLevelList.contains(value)];
case 'EncryptConfig.EncryptionToken':
return [(value) => value.isNotEmpty];
return [notEmptyValidator];
case 'EncryptConfig.Path':
return [
(value) => value.trim().isNotEmpty,
(value) => !containsRestrictedChar(value),
];
return [trimNotEmptyValidator, noRestrictedChars];
case 'HostConfig.ApiPassword':
return [(value) => value.isNotEmpty];
return [notEmptyValidator];
case 'HostConfig.ApiPort':
return [
(value) {
@ -100,29 +121,32 @@ List<Validator> getSettingValidators(String settingPath) {
return (intValue > 0 && intValue < 65536);
},
(value) => Uri.tryParse('http://localhost:$value/') != null,
createUriValidator(host: 'localhost'),
];
case 'HostConfig.HostNameOrIp':
return [
(value) => value.trim().isNotEmpty,
(value) => Uri.tryParse('http://$value:9000/') != null,
];
return createHostNameOrIpValidators();
case 'HostConfig.Protocol':
return [(value) => value == "http" || value == "https"];
return [(value) => constants.protocolTypeList.contains(value)];
case 'RemoteConfig.EncryptionToken':
return [(value) => value.isNotEmpty];
return [notEmptyValidator];
case 'RemoteConfig.HostNameOrIp':
return createHostNameOrIpValidators();
case 'RemoteMount.EncryptionToken':
return [(value) => value.isNotEmpty];
return [notEmptyValidator];
case 'RemoteMount.HostNameOrIp':
return createHostNameOrIpValidators();
case 'RingBufferFileSize':
return [(value) => constants.ringBufferSizeList.contains(value)];
case 'S3Config.AccessKey':
return [(value) => value.trim().isNotEmpty];
return [trimNotEmptyValidator];
case 'S3Config.Bucket':
return [(value) => value.trim().isNotEmpty];
return [trimNotEmptyValidator];
case 'S3Config.SecretKey':
return [(value) => value.trim().isNotEmpty];
return [trimNotEmptyValidator];
case 'S3Config.URL':
return [(value) => Uri.tryParse(value) != null];
return [trimNotEmptyValidator, (value) => Uri.tryParse(value) != null];
case 'SiaConfig.Bucket':
return [(value) => value.trim().isNotEmpty];
return [trimNotEmptyValidator];
}
return [];
@ -146,18 +170,19 @@ bool validateSettings(
String? rootKey,
}) {
settings.forEach((key, value) {
final checkKey = rootKey == null ? key : '$rootKey.$key';
final settingKey = rootKey == null ? key : '$rootKey.$key';
if (value is Map) {
validateSettings(
value as Map<String, dynamic>,
failed,
rootKey: checkKey,
rootKey: settingKey,
);
} else {
for (var validator in getSettingValidators(checkKey)) {
if (!validator(value.toString())) {
failed.add(checkKey);
for (var validator in getSettingValidators(settingKey)) {
if (validator(value.toString())) {
continue;
}
failed.add(settingKey);
}
}
});

View File

@ -1,7 +1,8 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/mount.dart';
import 'package:repertory/models/mount_list.dart';
@ -17,8 +18,6 @@ class AddMountScreen extends StatefulWidget {
}
class _AddMountScreenState extends State<AddMountScreen> {
static const _padding = 15.0;
Mount? _mount;
final _mountNameController = TextEditingController();
String _mountType = "";
@ -50,7 +49,7 @@ class _AddMountScreenState extends State<AddMountScreen> {
],
),
body: Padding(
padding: const EdgeInsets.all(_padding),
padding: const EdgeInsets.all(constants.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
@ -58,43 +57,44 @@ class _AddMountScreenState extends State<AddMountScreen> {
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(_padding),
padding: const EdgeInsets.all(constants.padding),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Provider Type'),
const SizedBox(width: _padding),
const SizedBox(width: constants.padding),
DropdownButton<String>(
value: _mountType,
onChanged: (mountType) => _handleChange(mountType ?? ''),
items:
providerTypeList.map<DropdownMenuItem<String>>((
item,
) {
constants.providerTypeList
.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
}).toList(),
})
.toList(),
),
],
),
),
),
if (_mountType.isNotEmpty) const SizedBox(height: _padding),
if (_mountType.isNotEmpty)
const SizedBox(height: constants.padding),
if (_mountType.isNotEmpty)
Card(
child: Padding(
padding: const EdgeInsets.all(_padding),
padding: const EdgeInsets.all(constants.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Configuration Name'),
const SizedBox(width: _padding),
const SizedBox(width: constants.padding),
TextField(
autofocus: true,
controller: _mountNameController,
@ -112,7 +112,7 @@ class _AddMountScreenState extends State<AddMountScreen> {
Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(_padding),
padding: const EdgeInsets.all(constants.padding),
child: MountSettingsWidget(
isAdd: true,
mount: _mount!,
@ -123,8 +123,15 @@ class _AddMountScreenState extends State<AddMountScreen> {
),
),
if (_mount != null)
ElevatedButton.icon(
Builder(
builder: (context) {
return ElevatedButton.icon(
onPressed: () {
final mountList = Provider.of<MountList>(
context,
listen: false,
);
List<String> failed = [];
if (!validateSettings(_settings[_mountType]!, failed)) {
for (var key in failed) {
@ -140,7 +147,25 @@ class _AddMountScreenState extends State<AddMountScreen> {
return;
}
Provider.of<MountList>(context, listen: false).add(
final existingMount = mountList.items.firstWhereOrNull(
(item) =>
item.name.toLowerCase() ==
_mountNameController.text.toLowerCase(),
);
if (existingMount != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"'${_mountNameController.text}' already exists",
textAlign: TextAlign.center,
),
),
);
return;
}
mountList.add(
_mountType,
_mountNameController.text,
_settings[_mountType]!,
@ -150,6 +175,8 @@ class _AddMountScreenState extends State<AddMountScreen> {
},
label: const Text('Add'),
icon: Icon(Icons.add),
);
},
),
],
),

View File

@ -1,7 +1,7 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:repertory/constants.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart' show Validator, getSettingValidators;
import 'package:repertory/models/mount.dart';
import 'package:settings_ui/settings_ui.dart';
@ -26,8 +26,6 @@ class MountSettingsWidget extends StatefulWidget {
}
class _MountSettingsWidgetState extends State<MountSettingsWidget> {
static const _padding = 15.0;
void _addBooleanSetting(list, root, key, value, isAdvanced) {
if (!isAdvanced || widget.showAdvanced) {
list.add(
@ -264,7 +262,7 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
obscuringCharacter: '*',
onChanged: (value) => updatedValue1 = value,
),
const SizedBox(height: _padding),
const SizedBox(height: constants.padding),
TextField(
autofocus: false,
controller: TextEditingController(text: updatedValue2),
@ -409,7 +407,7 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
widget.settings,
key,
value,
databaseTypeList,
constants.databaseTypeList,
Icons.dataset,
true,
);
@ -456,7 +454,7 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
widget.settings,
key,
value,
eventLevelList,
constants.eventLevelList,
Icons.event,
false,
);
@ -528,7 +526,7 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
widget.settings,
key,
value,
downloadTypeList,
constants.downloadTypeList,
Icons.download,
false,
);
@ -553,7 +551,7 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
widget.settings,
key,
value,
ringBufferSizeList,
constants.ringBufferSizeList,
512,
Icons.animation,
false,
@ -733,7 +731,7 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
widget.settings[key],
subKey,
subValue,
protocolTypeList,
constants.protocolTypeList,
Icons.http,
true,
);