added portal settings screen

This commit is contained in:
Scott E. Graves 2025-03-16 08:09:42 -05:00
parent 9b5d642106
commit e7accd1cc1
11 changed files with 808 additions and 398 deletions

View File

@ -63,6 +63,8 @@ private:
void handle_get_mount_status(auto &&req, auto &&res) const;
void handle_get_settings(auto &&res) const;
void handle_post_add_mount(auto &&req, auto &&res) const;
void handle_post_mount(auto &&req, auto &&res) const;

View File

@ -42,6 +42,8 @@ private:
void save() const;
public:
[[nodiscard]] auto to_json() const -> nlohmann::json;
[[nodiscard]] auto get_api_password() const -> std::string {
return api_password_;
}

View File

@ -102,6 +102,11 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
handle_get_mount_status(req, res);
});
server->Get("/api/v1/settings",
[this](const httplib::Request & /* req */, auto &&res) {
handle_get_settings(res);
});
server->Post("/api/v1/add_mount", [this](auto &&req, auto &&res) {
handle_post_add_mount(req, res);
});
@ -307,6 +312,11 @@ void handlers::handle_get_mount_status(auto &&req, auto &&res) const {
res.status = http_error_codes::ok;
}
void handlers::handle_get_settings(auto &&res) const {
res.set_content(config_->to_json().dump(), "application/json");
res.status = http_error_codes::ok;
}
void handlers::handle_post_add_mount(auto &&req, auto &&res) const {
auto name = req.get_param_value("name");
auto prov = provider_type_from_string(req.get_param_value("type"));

View File

@ -59,7 +59,7 @@ namespace {
return map_of_maps;
}
[[nodiscard]] auto to_json(const auto &map_of_maps) -> nlohmann::json {
[[nodiscard]] auto map_to_json(const auto &map_of_maps) -> nlohmann::json {
auto json = nlohmann::json::object();
for (const auto &[prov, map] : map_of_maps) {
for (const auto &[key, value] : map) {
@ -134,12 +134,7 @@ void mgmt_app_config::save() const {
return;
}
nlohmann::json data;
data[JSON_API_PASSWORD] = api_password_;
data[JSON_API_PORT] = api_port_;
data[JSON_API_USER] = api_user_;
data[JSON_MOUNT_LOCATIONS] = to_json(locations_);
if (utils::file::write_json_file(config_file, data)) {
if (utils::file::write_json_file(config_file, to_json())) {
return;
}
@ -181,4 +176,13 @@ void mgmt_app_config::set_mount_location(provider_type prov,
save();
}
auto mgmt_app_config::to_json() const -> nlohmann::json {
nlohmann::json data;
data[JSON_API_PASSWORD] = api_password_;
data[JSON_API_PORT] = api_port_;
data[JSON_API_USER] = api_user_;
data[JSON_MOUNT_LOCATIONS] = map_to_json(locations_);
return data;
}
} // namespace repertory::ui

View File

@ -125,7 +125,7 @@ String getBaseUri() {
String? getSettingDescription(String settingPath) {
switch (settingPath) {
case 'ApiPassword':
return "'repertory' REST API password";
return "HTTP basic authentication password";
case 'HostConfig.ApiPassword':
return "RENTERD_API_PASSWORD";
default:

View File

@ -6,6 +6,7 @@ import 'package:repertory/models/mount.dart';
import 'package:repertory/models/mount_list.dart';
import 'package:repertory/screens/add_mount_screen.dart';
import 'package:repertory/screens/edit_mount_screen.dart';
import 'package:repertory/screens/edit_settings_screen.dart';
import 'package:repertory/screens/home_screen.dart';
void main() {
@ -50,6 +51,8 @@ class _MyAppState extends State<MyApp> {
'/': (context) => const HomeScreen(title: constants.appTitle),
'/add':
(context) => const AddMountScreen(title: constants.addMountTitle),
'/settings':
(context) => const EditSettingsScreen(title: constants.appTitle),
},
onGenerateRoute: (settings) {
if (settings.name != '/edit') {

View File

@ -0,0 +1,68 @@
import 'dart:convert' show jsonDecode;
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:repertory/helpers.dart';
import 'package:repertory/widgets/ui_settings.dart';
class EditSettingsScreen extends StatefulWidget {
final String title;
const EditSettingsScreen({super.key, required this.title});
@override
State<EditSettingsScreen> createState() => _EditSettingsScreenState();
}
class _EditSettingsScreenState extends State<EditSettingsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: FutureBuilder(
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
return UISettingsWidget(
settings: snapshot.requireData,
showAdvanced: false,
);
},
future: _grabSettings(),
initialData: <String, dynamic>{},
),
);
}
Future<Map<String, dynamic>> _grabSettings() async {
try {
final response = await http.get(
Uri.parse('${getBaseUri()}/api/v1/settings'),
);
if (response.statusCode != 200) {
return {};
}
return jsonDecode(response.body);
} catch (e) {
debugPrint('$e');
}
return {};
}
// UISettingsWidget(settings: {}, showAdvanced: false),
@override
void setState(VoidCallback fn) {
if (!mounted) {
return;
}
super.setState(fn);
}
}

View File

@ -16,7 +16,10 @@ class _HomeScreeState extends State<HomeScreen> {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
leading: const Icon(Icons.storage),
leading: IconButton(
onPressed: () => Navigator.pushNamed(context, '/settings'),
icon: const Icon(Icons.storage),
),
title: Text(widget.title),
),
body: Padding(

View File

@ -0,0 +1,361 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart' show Validator, displayErrorMessage;
import 'package:settings_ui/settings_ui.dart';
void createBooleanSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
}) {
if (!isAdvanced || showAdvanced) {
list.add(
SettingsTile.switchTile(
leading: const Icon(Icons.quiz),
title: createSettingTitle(context, key, description),
initialValue: (value as bool),
onPressed:
(_) => setState(() {
settings[key] = !value;
widget.onChanged?.call(widget.settings);
}),
onToggle: (bool nextValue) {
setState(() {
settings[key] = nextValue;
widget.onChanged?.call(widget.settings);
});
},
),
);
}
}
void createIntListSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
List<String> valueList,
defaultValue,
IconData icon,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
}) {
if (!isAdvanced || widget.showAdvanced) {
list.add(
SettingsTile.navigation(
title: createSettingTitle(context, key, description),
leading: Icon(icon),
value: DropdownButton<String>(
value: value.toString(),
onChanged: (newValue) {
setState(() {
settings[key] = int.parse(newValue ?? defaultValue.toString());
widget.onChanged?.call(widget.settings);
});
},
items:
valueList.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(value: item, child: Text(item));
}).toList(),
),
),
);
}
}
void createIntSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
List<Validator> validators = const [],
}) {
if (!isAdvanced || widget.showAdvanced) {
list.add(
SettingsTile.navigation(
leading: const Icon(Icons.onetwothree),
title: createSettingTitle(context, key, description),
value: Text(value.toString()),
onPressed: (_) {
String updatedValue = value.toString();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('OK'),
onPressed: () {
final result = validators.firstWhereOrNull(
(validator) => !validator(updatedValue),
);
if (result != null) {
return displayErrorMessage(
context,
"Setting '$key' is not valid",
);
}
setState(() {
settings[key] = int.parse(updatedValue);
widget.onChanged?.call(widget.settings);
});
Navigator.of(context).pop();
},
),
],
content: TextField(
autofocus: true,
controller: TextEditingController(text: updatedValue),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
keyboardType: TextInputType.number,
onChanged: (nextValue) => updatedValue = nextValue,
),
title: createSettingTitle(context, key, description),
);
},
);
},
),
);
}
}
void createPasswordSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
List<Validator> validators = const [],
}) {
if (!isAdvanced || widget.showAdvanced) {
list.add(
SettingsTile.navigation(
leading: const Icon(Icons.password),
title: createSettingTitle(context, key, description),
value: Text('*' * (value as String).length),
onPressed: (_) {
String updatedValue1 = value;
String updatedValue2 = value;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('OK'),
onPressed: () {
if (updatedValue1 != updatedValue2) {
return displayErrorMessage(
context,
"Setting '$key' does not match",
);
}
final result = validators.firstWhereOrNull(
(validator) => !validator(updatedValue1),
);
if (result != null) {
return displayErrorMessage(
context,
"Setting '$key' is not valid",
);
}
setState(() {
settings[key] = updatedValue1;
widget.onChanged?.call(widget.settings);
});
Navigator.of(context).pop();
},
),
],
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
TextField(
autofocus: true,
controller: TextEditingController(text: updatedValue1),
obscureText: true,
obscuringCharacter: '*',
onChanged: (value) => updatedValue1 = value,
),
const SizedBox(height: constants.padding),
TextField(
autofocus: false,
controller: TextEditingController(text: updatedValue2),
obscureText: true,
obscuringCharacter: '*',
onChanged: (value) => updatedValue2 = value,
),
],
),
title: createSettingTitle(context, key, description),
);
},
);
},
),
);
}
}
Widget createSettingTitle(context, String key, String? description) {
if (description == null) {
return Text(key);
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(key, textAlign: TextAlign.start),
Text(
description,
style: Theme.of(context).textTheme.titleSmall,
textAlign: TextAlign.start,
),
],
);
}
void createStringListSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
List<String> valueList,
IconData icon,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
}) {
if (!isAdvanced || widget.showAdvanced) {
list.add(
SettingsTile.navigation(
title: createSettingTitle(context, key, description),
leading: Icon(icon),
value: DropdownButton<String>(
value: value,
onChanged:
(newValue) => setState(() {
settings[key] = newValue;
widget.onChanged?.call(widget.settings);
}),
items:
valueList.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(value: item, child: Text(item));
}).toList(),
),
),
);
}
}
void createStringSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
IconData icon,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
List<Validator> validators = const [],
}) {
if (!isAdvanced || widget.showAdvanced) {
list.add(
SettingsTile.navigation(
leading: Icon(icon),
title: createSettingTitle(context, key, description),
value: Text(value),
onPressed: (_) {
String updatedValue = value;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('OK'),
onPressed: () {
final result = validators.firstWhereOrNull(
(validator) => !validator(updatedValue),
);
if (result != null) {
return displayErrorMessage(
context,
"Setting '$key' is not valid",
);
}
setState(() {
settings[key] = updatedValue;
widget.onChanged?.call(widget.settings);
});
Navigator.of(context).pop();
},
),
],
content: TextField(
autofocus: true,
controller: TextEditingController(text: updatedValue),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
],
onChanged: (value) => updatedValue = value,
),
title: createSettingTitle(context, key, description),
);
},
);
},
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart'
show getSettingDescription, getSettingValidators;
import 'package:repertory/models/mount.dart';
import 'package:repertory/models/mount_list.dart';
import 'package:repertory/settings.dart';
import 'package:settings_ui/settings_ui.dart';
class UISettingsWidget extends StatefulWidget {
final bool showAdvanced;
final Function? onChanged;
final Map<String, dynamic> settings;
const UISettingsWidget({
super.key,
this.onChanged,
required this.settings,
required this.showAdvanced,
});
@override
State<UISettingsWidget> createState() => _UISettingsWidgetState();
}
class _UISettingsWidgetState extends State<UISettingsWidget> {
@override
Widget build(BuildContext context) {
List<SettingsTile> commonSettings = [];
widget.settings.forEach((key, value) {
switch (key) {
case 'ApiPassword':
{
createPasswordSetting(
context,
commonSettings,
widget.settings,
key,
value,
false,
widget.showAdvanced,
widget,
setState,
description: getSettingDescription(key),
validators: getSettingValidators(key),
);
}
break;
case 'ApiPort':
{
createIntSetting(
context,
commonSettings,
widget.settings,
key,
value,
true,
widget.showAdvanced,
widget,
setState,
description: getSettingDescription(key),
validators: getSettingValidators(key),
);
}
break;
case 'ApiUser':
{
createStringSetting(
context,
commonSettings,
widget.settings,
key,
value,
Icons.person,
true,
widget.showAdvanced,
widget,
setState,
description: getSettingDescription(key),
validators: getSettingValidators(key),
);
}
break;
}
});
return SettingsList(
shrinkWrap: false,
sections: [
SettingsSection(title: const Text('Settings'), tiles: commonSettings),
],
);
}
@override
void setState(VoidCallback fn) {
if (!mounted) {
return;
}
super.setState(fn);
}
}