@@ -1,8 +1,13 @@
|
||||
auro
|
||||
aurosweep
|
||||
autofocus
|
||||
autovalidatemode
|
||||
canvaskit
|
||||
cupertino
|
||||
cupertinoicons
|
||||
entrypoint
|
||||
fromargb
|
||||
onetwothree
|
||||
renterd
|
||||
rocksdb
|
||||
rocksdb
|
||||
vsync
|
||||
@@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
|
||||
revision: "d7b523b356d15fb81e7d340bbe52b47f93937323"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@@ -13,11 +13,11 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
||||
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
||||
create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
|
||||
base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
|
||||
- platform: web
|
||||
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
||||
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
||||
create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
|
||||
base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
BIN
web/repertory/assets/images/repertory.png
Normal file
|
After Width: | Height: | Size: 368 B |
@@ -1,18 +1,43 @@
|
||||
import 'package:flutter/material.dart' show GlobalKey, NavigatorState;
|
||||
// constants.dart
|
||||
|
||||
import 'package:flutter/material.dart' show GlobalKey, NavigatorState, Color;
|
||||
import 'package:sodium_libs/sodium_libs.dart';
|
||||
|
||||
const accentBlue = Color(0xFF1050A0);
|
||||
const addMountTitle = 'Add New Mount';
|
||||
const appLogonTitle = 'Repertory Portal Login';
|
||||
const appSettingsTitle = 'Portal Settings';
|
||||
const appTitle = 'Repertory Management Portal';
|
||||
const logonWidth = 300.0;
|
||||
const borderRadius = 16.0;
|
||||
const borderRadiusSmall = borderRadius / 2.0;
|
||||
const borderRadiusTiny = borderRadiusSmall / 2.0;
|
||||
const boxShadowAlpha = 0.20;
|
||||
const databaseTypeList = ['rocksdb', 'sqlite'];
|
||||
const dialogAlpha = 0.95;
|
||||
const downloadTypeList = ['default', 'direct', 'ring_buffer'];
|
||||
const dropDownAlpha = 99.0;
|
||||
const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace'];
|
||||
const padding = 15.0;
|
||||
const gradientColors = [Color(0xFF0A0F1F), Color(0xFF1B1C1F)];
|
||||
const gradientColors2 = [Color(0x07FFFFFF), Color(0x00000000)];
|
||||
const highlightAlpha = 0.10;
|
||||
const largeIconSize = 32.0;
|
||||
const loginIconSize = 54.0;
|
||||
const logonWidth = 420.0;
|
||||
const outlineAlpha = 0.15;
|
||||
const padding = 16.0;
|
||||
const paddingLarge = padding * 2.0;
|
||||
const paddingMedium = 12.0;
|
||||
const paddingSmall = padding / 2.0;
|
||||
const primaryAlpha = 0.12;
|
||||
const primarySurfaceAlpha = 92.0;
|
||||
const protocolTypeList = ['http', 'https'];
|
||||
const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia'];
|
||||
const ringBufferSizeList = ['128', '256', '512', '1024', '2048'];
|
||||
const secondaryAlpha = 0.45;
|
||||
const secondarySurfaceAlpha = 70.0;
|
||||
const smallIconSize = 18.0;
|
||||
const surfaceContainerLowDark = Color(0xFF292A2D);
|
||||
const surfaceDark = Color(0xFF202124);
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
|
||||
@@ -1,11 +1,44 @@
|
||||
// helpers.dart
|
||||
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/widgets/app_dropdown.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
import 'package:sodium_libs/sodium_libs.dart' show SecureKey, StringX;
|
||||
|
||||
Future doShowDialog(BuildContext context, Widget child) => showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
return Theme(
|
||||
data: theme.copyWith(
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: darken(
|
||||
scheme.primary,
|
||||
0.95,
|
||||
).withValues(alpha: constants.dropDownAlpha),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadius),
|
||||
side: BorderSide(
|
||||
color: scheme.outlineVariant.withValues(
|
||||
alpha: constants.outlineAlpha,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
typedef Validator = bool Function(String);
|
||||
|
||||
class NullPasswordException implements Exception {
|
||||
@@ -62,7 +95,7 @@ createUriValidator<Validator>({host, port}) {
|
||||
Uri.tryParse('http://${host ?? value}:${port ?? value}/') != null;
|
||||
}
|
||||
|
||||
createHostNameOrIpValidators() => <Validator>[
|
||||
List<Validator> createHostNameOrIpValidators() => <Validator>[
|
||||
trimNotEmptyValidator,
|
||||
createUriValidator(port: 9000),
|
||||
];
|
||||
@@ -86,6 +119,7 @@ Map<String, dynamic> createDefaultSettings(String mountType) {
|
||||
'S3Config': {
|
||||
'AccessKey': '',
|
||||
'Bucket': '',
|
||||
'ForceLegacyEncryption': false,
|
||||
'Region': 'any',
|
||||
'SecretKey': '',
|
||||
'URL': '',
|
||||
@@ -100,7 +134,7 @@ Map<String, dynamic> createDefaultSettings(String mountType) {
|
||||
'ApiPort': 9980,
|
||||
'HostNameOrIp': 'localhost',
|
||||
},
|
||||
'SiaConfig': {'Bucket': 'default'},
|
||||
'SiaConfig': {'Bucket': ''},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,7 +153,11 @@ void displayAuthError(Auth auth) {
|
||||
);
|
||||
}
|
||||
|
||||
void displayErrorMessage(context, String text, {bool clear = false}) {
|
||||
void displayErrorMessage(
|
||||
BuildContext context,
|
||||
String text, {
|
||||
bool clear = false,
|
||||
}) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
@@ -158,6 +196,8 @@ String? getSettingDescription(String settingPath) {
|
||||
return "HTTP authentication user";
|
||||
case 'HostConfig.ApiPassword':
|
||||
return "RENTERD_API_PASSWORD";
|
||||
case 'S3Config.ForceLegacyEncryption':
|
||||
return "Effectively disables Argon2id KDF";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -338,65 +378,129 @@ Map<String, dynamic> getChanged(
|
||||
}
|
||||
|
||||
Future<String?> editMountLocation(
|
||||
context,
|
||||
BuildContext context,
|
||||
List<String> available, {
|
||||
bool allowEmpty = false,
|
||||
String? location,
|
||||
}) async {
|
||||
if (!context.mounted) {
|
||||
return location;
|
||||
}
|
||||
|
||||
String? currentLocation = location;
|
||||
final controller = TextEditingController(text: currentLocation);
|
||||
return await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(null),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: () {
|
||||
final result = getSettingValidators('Path').firstWhereOrNull(
|
||||
(validator) => !validator(currentLocation ?? ''),
|
||||
return await doShowDialog(
|
||||
context,
|
||||
StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(null),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: () {
|
||||
final result = getSettingValidators('Path').firstWhereOrNull(
|
||||
(validator) => !validator(currentLocation ?? ''),
|
||||
);
|
||||
if (result != null) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Mount location is not valid",
|
||||
);
|
||||
if (result != null) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Mount location is not valid",
|
||||
);
|
||||
}
|
||||
Navigator.of(context).pop(currentLocation);
|
||||
},
|
||||
),
|
||||
],
|
||||
content:
|
||||
available.isEmpty
|
||||
? TextField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
onChanged:
|
||||
(value) => setState(() => currentLocation = value),
|
||||
)
|
||||
: DropdownButton<String>(
|
||||
hint: const Text("Select drive"),
|
||||
value: currentLocation,
|
||||
onChanged:
|
||||
(value) => setState(() => currentLocation = value),
|
||||
items:
|
||||
available.map<DropdownMenuItem<String>>((item) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: item,
|
||||
child: Text(item),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
title: const Text('Mount Location', textAlign: TextAlign.center),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
}
|
||||
Navigator.of(context).pop(currentLocation);
|
||||
},
|
||||
),
|
||||
],
|
||||
content: available.isEmpty
|
||||
? TextField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
onChanged: (value) => setState(() => currentLocation = value),
|
||||
)
|
||||
: AppDropdown<String>(
|
||||
labelOf: (s) => s,
|
||||
labelText: "Select drive",
|
||||
onChanged: (value) => setState(() => currentLocation = value),
|
||||
prefixIcon: Icons.computer,
|
||||
value: currentLocation,
|
||||
values: available.toList(),
|
||||
),
|
||||
title: const Text('Mount Location', textAlign: TextAlign.center),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration createCommonDecoration(
|
||||
ColorScheme colorScheme,
|
||||
String label, {
|
||||
bool filled = true,
|
||||
String? hintText,
|
||||
IconData? icon,
|
||||
}) => InputDecoration(
|
||||
labelText: label,
|
||||
prefixIcon: icon == null ? null : Icon(icon),
|
||||
filled: filled,
|
||||
fillColor: colorScheme.primary.withValues(alpha: constants.primaryAlpha),
|
||||
hintText: hintText,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
|
||||
borderSide: BorderSide(color: colorScheme.primary, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(constants.paddingSmall),
|
||||
);
|
||||
|
||||
IconData getProviderTypeIcon(String mountType) {
|
||||
switch (mountType.toLowerCase()) {
|
||||
case "encrypt":
|
||||
return Icons.key_outlined;
|
||||
case "remote":
|
||||
return Icons.network_ping_outlined;
|
||||
default:
|
||||
return Icons.cloud_outlined;
|
||||
}
|
||||
}
|
||||
|
||||
SettingsThemeData createSettingsTheme(ColorScheme scheme) {
|
||||
return SettingsThemeData(
|
||||
settingsListBackground: Colors.transparent,
|
||||
settingsSectionBackground: scheme.primary.withValues(
|
||||
alpha: constants.primaryAlpha,
|
||||
),
|
||||
titleTextColor: scheme.onSurface.withValues(
|
||||
alpha: constants.primarySurfaceAlpha,
|
||||
),
|
||||
trailingTextColor: scheme.onSurface.withValues(
|
||||
alpha: constants.primarySurfaceAlpha,
|
||||
),
|
||||
tileDescriptionTextColor: scheme.onSurface.withValues(
|
||||
alpha: constants.secondarySurfaceAlpha,
|
||||
),
|
||||
leadingIconsColor: scheme.onSurface.withValues(
|
||||
alpha: constants.primarySurfaceAlpha,
|
||||
),
|
||||
dividerColor: scheme.outlineVariant.withValues(
|
||||
alpha: constants.outlineAlpha,
|
||||
),
|
||||
tileHighlightColor: scheme.primary.withValues(
|
||||
alpha: constants.highlightAlpha,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color darken(Color color, [double percentage = 0.1]) {
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
final hslDark = hsl.withLightness(
|
||||
(hsl.lightness - (hsl.lightness * percentage)).clamp(0.0, 1.0),
|
||||
);
|
||||
return hslDark.toColor();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// main.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
@@ -5,11 +7,13 @@ import 'package:repertory/helpers.dart';
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/models/mount.dart';
|
||||
import 'package:repertory/models/mount_list.dart';
|
||||
import 'package:repertory/models/settings.dart';
|
||||
import 'package:repertory/screens/add_mount_screen.dart';
|
||||
import 'package:repertory/screens/auth_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';
|
||||
import 'package:repertory/widgets/auth_check.dart';
|
||||
import 'package:sodium_libs/sodium_libs.dart' show SodiumInit;
|
||||
|
||||
void main() async {
|
||||
@@ -24,6 +28,7 @@ void main() async {
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => auth),
|
||||
ChangeNotifierProvider(create: (_) => Settings(auth)),
|
||||
ChangeNotifierProvider(create: (_) => MountList(auth)),
|
||||
],
|
||||
child: const MyApp(),
|
||||
@@ -54,28 +59,39 @@ class _MyAppState extends State<MyApp> {
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
brightness: Brightness.dark,
|
||||
seedColor: constants.accentBlue,
|
||||
onSurface: Colors.white70,
|
||||
seedColor: Colors.deepOrange,
|
||||
surface: Color.fromARGB(255, 32, 33, 36),
|
||||
surfaceContainerLow: Color.fromARGB(255, 41, 42, 45),
|
||||
surface: constants.surfaceDark,
|
||||
surfaceContainerLow: constants.surfaceContainerLowDark,
|
||||
),
|
||||
scaffoldBackgroundColor: constants.surfaceDark,
|
||||
snackBarTheme: snackBarTheme,
|
||||
appBarTheme: const AppBarTheme(scrolledUnderElevation: 0),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(width: 2)),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(40),
|
||||
),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(40)),
|
||||
),
|
||||
dividerTheme: const DividerThemeData(thickness: 0.6, space: 0),
|
||||
),
|
||||
title: constants.appTitle,
|
||||
initialRoute: '/auth',
|
||||
routes: {
|
||||
'/':
|
||||
(context) =>
|
||||
const AuthCheck(child: HomeScreen(title: constants.appTitle)),
|
||||
'/add':
|
||||
(context) => const AuthCheck(
|
||||
child: AddMountScreen(title: constants.addMountTitle),
|
||||
),
|
||||
'/': (context) =>
|
||||
const AuthCheck(child: HomeScreen(title: constants.appTitle)),
|
||||
'/add': (context) => const AuthCheck(
|
||||
child: AddMountScreen(title: constants.addMountTitle),
|
||||
),
|
||||
'/auth': (context) => const AuthScreen(title: constants.appTitle),
|
||||
'/settings':
|
||||
(context) => const AuthCheck(
|
||||
child: EditSettingsScreen(title: constants.appSettingsTitle),
|
||||
),
|
||||
'/settings': (context) => const AuthCheck(
|
||||
child: EditSettingsScreen(title: constants.appSettingsTitle),
|
||||
),
|
||||
},
|
||||
onGenerateRoute: (settings) {
|
||||
if (settings.name != '/edit') {
|
||||
@@ -89,7 +105,7 @@ class _MyAppState extends State<MyApp> {
|
||||
child: EditMountScreen(
|
||||
mount: mount,
|
||||
title:
|
||||
'${mount.provider} [${formatMountName(mount.type, mount.name)}] Settings',
|
||||
'${mount.provider} Settings • ${formatMountName(mount.type, mount.name)}',
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -98,29 +114,3 @@ class _MyAppState extends State<MyApp> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthCheck extends StatelessWidget {
|
||||
final Widget child;
|
||||
const AuthCheck({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<Auth>(
|
||||
builder: (context, auth, __) {
|
||||
if (!auth.authenticated) {
|
||||
Future.delayed(Duration(milliseconds: 1), () {
|
||||
if (constants.navigatorKey.currentContext == null) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(
|
||||
constants.navigatorKey.currentContext!,
|
||||
).pushNamedAndRemoveUntil('/auth', (Route<dynamic> route) => false);
|
||||
});
|
||||
return child;
|
||||
}
|
||||
|
||||
return child;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,19 +17,37 @@ class Auth with ChangeNotifier {
|
||||
bool get authenticated => _authenticated;
|
||||
SecureKey get key => _key;
|
||||
|
||||
Future<void> authenticate(String user, String password) async {
|
||||
final sodium = constants.sodium;
|
||||
Future<bool> authenticate(String user, String password) async {
|
||||
try {
|
||||
final sodium = constants.sodium;
|
||||
|
||||
final keyHash = sodium.crypto.genericHash(
|
||||
outLen: sodium.crypto.aeadXChaCha20Poly1305IETF.keyBytes,
|
||||
message: Uint8List.fromList(password.toCharArray()),
|
||||
);
|
||||
final keyHash = sodium.crypto.genericHash(
|
||||
outLen: sodium.crypto.aeadXChaCha20Poly1305IETF.keyBytes,
|
||||
message: Uint8List.fromList(password.toCharArray()),
|
||||
);
|
||||
|
||||
_authenticated = true;
|
||||
_key = SecureKey.fromList(sodium, keyHash);
|
||||
_user = user;
|
||||
_key = SecureKey.fromList(sodium, keyHash);
|
||||
_user = user;
|
||||
|
||||
notifyListeners();
|
||||
final auth = await createAuth();
|
||||
final response = await http.get(
|
||||
Uri.parse(
|
||||
Uri.encodeFull('${getBaseUri()}/api/v1/locations?auth=$auth'),
|
||||
),
|
||||
);
|
||||
|
||||
_authenticated = (response.statusCode == 200);
|
||||
if (_authenticated) {
|
||||
notifyListeners();
|
||||
|
||||
return _authenticated;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
|
||||
logoff();
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<String> createAuth() async {
|
||||
@@ -56,9 +74,8 @@ class Auth with ChangeNotifier {
|
||||
_authenticated = false;
|
||||
_key = SecureKey.random(constants.sodium, 32);
|
||||
_user = "";
|
||||
mountList?.clear(notify: false);
|
||||
|
||||
notifyListeners();
|
||||
|
||||
mountList?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class Mount with ChangeNotifier {
|
||||
refresh();
|
||||
}
|
||||
|
||||
bool get autoStart => mountConfig.autoStart;
|
||||
String? get bucket => mountConfig.bucket;
|
||||
String get id => '${type}_$name';
|
||||
bool? get mounted => mountConfig.mounted;
|
||||
@@ -101,6 +102,35 @@ class Mount with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setMountAutoStart(bool autoStart) async {
|
||||
try {
|
||||
mountConfig.autoStart = autoStart;
|
||||
|
||||
final auth = await _auth.createAuth();
|
||||
final response = await http.put(
|
||||
Uri.parse(
|
||||
Uri.encodeFull(
|
||||
'${getBaseUri()}/api/v1/mount_auto_start?auth=$auth&name=$name&type=$type&auto_start=$autoStart',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
_auth.logoff();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
_mountList?.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
return refresh();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setMountLocation(String location) async {
|
||||
try {
|
||||
mountConfig.path = location;
|
||||
@@ -252,6 +282,29 @@ class Mount with ChangeNotifier {
|
||||
_isRefreshing = false;
|
||||
}
|
||||
|
||||
Future<void> remove() async {
|
||||
try {
|
||||
final auth = await _auth.createAuth();
|
||||
final response = await http.delete(
|
||||
Uri.parse(
|
||||
Uri.encodeFull(
|
||||
'${getBaseUri()}/api/v1/remove_mount?auth=$auth&name=$name&type=$type',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
_auth.logoff();
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
_mountList?.remove(name, type);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setValue(String key, String value) async {
|
||||
try {
|
||||
final auth = await _auth.createAuth();
|
||||
|
||||
@@ -16,11 +16,7 @@ class MountList with ChangeNotifier {
|
||||
|
||||
MountList(this._auth) {
|
||||
_auth.mountList = this;
|
||||
_auth.addListener(() {
|
||||
if (_auth.authenticated) {
|
||||
_fetch();
|
||||
}
|
||||
});
|
||||
_auth.addListener(_listener);
|
||||
}
|
||||
|
||||
List<Mount> _mountList = [];
|
||||
@@ -38,9 +34,9 @@ class MountList with ChangeNotifier {
|
||||
return (excludeName == null
|
||||
? list
|
||||
: list.whereNot(
|
||||
(item) =>
|
||||
item.name.toLowerCase() == excludeName.toLowerCase(),
|
||||
))
|
||||
(item) =>
|
||||
item.name.toLowerCase() == excludeName.toLowerCase(),
|
||||
))
|
||||
.firstWhereOrNull((Mount item) {
|
||||
return item.bucket != null &&
|
||||
item.bucket!.toLowerCase() == bucket.toLowerCase();
|
||||
@@ -98,7 +94,7 @@ class MountList with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _sort(list) {
|
||||
void _sort(List list) {
|
||||
list.sort((a, b) {
|
||||
final res = a.type.compareTo(b.type);
|
||||
if (res != 0) {
|
||||
@@ -177,25 +173,50 @@ class MountList with ChangeNotifier {
|
||||
return ret;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
void clear({bool notify = true}) {
|
||||
_mountList = [];
|
||||
if (!notify) {
|
||||
return;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void remove(String name, String type) {
|
||||
_mountList.removeWhere((mount) => mount.name == name && mount.type == type);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> reset() async {
|
||||
if (constants.navigatorKey.currentContext == null ||
|
||||
ModalRoute.of(constants.navigatorKey.currentContext!)?.settings.name !=
|
||||
'/') {
|
||||
await constants.navigatorKey.currentState?.pushReplacementNamed('/');
|
||||
if (_mountList.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
clear();
|
||||
|
||||
Future.delayed(Duration(seconds: 1), () => _fetch());
|
||||
|
||||
displayErrorMessage(
|
||||
constants.navigatorKey.currentContext!,
|
||||
'Mount removed externally. Reloading...',
|
||||
);
|
||||
|
||||
clear();
|
||||
if (constants.navigatorKey.currentContext == null ||
|
||||
ModalRoute.of(constants.navigatorKey.currentContext!)?.settings.name !=
|
||||
'/') {
|
||||
await constants.navigatorKey.currentState?.pushReplacementNamed('/');
|
||||
}
|
||||
}
|
||||
|
||||
return _fetch();
|
||||
void _listener() {
|
||||
if (_auth.authenticated) {
|
||||
_fetch();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_auth.removeListener(_listener);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
116
web/repertory/lib/models/settings.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:repertory/helpers.dart';
|
||||
import 'package:repertory/models/auth.dart';
|
||||
|
||||
class Settings with ChangeNotifier {
|
||||
final Auth _auth;
|
||||
bool _autoStart = false;
|
||||
bool _enableAnimations = true;
|
||||
|
||||
Settings(this._auth) {
|
||||
_auth.addListener(() {
|
||||
if (_auth.authenticated) {
|
||||
_fetch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool get autoStart => _autoStart;
|
||||
bool get enableAnimations => _enableAnimations;
|
||||
|
||||
void _reset() {
|
||||
_autoStart = false;
|
||||
}
|
||||
|
||||
Future<void> setEnableAnimations(bool value) async {
|
||||
try {
|
||||
final auth = await _auth.createAuth();
|
||||
final response = await http.put(
|
||||
Uri.parse(
|
||||
Uri.encodeFull(
|
||||
'${getBaseUri()}/api/v1/setting?auth=$auth&name=Animations&value=$value',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
_auth.logoff();
|
||||
_reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
_reset();
|
||||
return;
|
||||
}
|
||||
|
||||
_enableAnimations = value;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
_reset();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setAutoStart(bool value) async {
|
||||
try {
|
||||
final auth = await _auth.createAuth();
|
||||
final response = await http.put(
|
||||
Uri.parse(
|
||||
Uri.encodeFull(
|
||||
'${getBaseUri()}/api/v1/setting?auth=$auth&name=AutoStart&value=$value',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
_auth.logoff();
|
||||
_reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
_reset();
|
||||
return;
|
||||
}
|
||||
|
||||
_autoStart = value;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
_reset();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetch() async {
|
||||
try {
|
||||
final auth = await _auth.createAuth();
|
||||
final response = await http.get(
|
||||
Uri.parse(Uri.encodeFull('${getBaseUri()}/api/v1/settings?auth=$auth')),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
_auth.logoff();
|
||||
_reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
_reset();
|
||||
return;
|
||||
}
|
||||
|
||||
final jsonData = jsonDecode(response.body);
|
||||
_enableAnimations = jsonData["Animations"] as bool;
|
||||
_autoStart = jsonData["AutoStart"] as bool;
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
_reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// add_mount_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -7,6 +9,10 @@ import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/models/mount.dart';
|
||||
import 'package:repertory/models/mount_list.dart';
|
||||
import 'package:repertory/types/mount_config.dart';
|
||||
import 'package:repertory/utils/safe_set_state_mixin.dart';
|
||||
import 'package:repertory/widgets/app_dropdown.dart';
|
||||
import 'package:repertory/widgets/app_outlined_icon_button.dart';
|
||||
import 'package:repertory/widgets/app_scaffold.dart';
|
||||
import 'package:repertory/widgets/mount_settings.dart';
|
||||
|
||||
class AddMountScreen extends StatefulWidget {
|
||||
@@ -17,10 +23,12 @@ class AddMountScreen extends StatefulWidget {
|
||||
State<AddMountScreen> createState() => _AddMountScreenState();
|
||||
}
|
||||
|
||||
class _AddMountScreenState extends State<AddMountScreen> {
|
||||
class _AddMountScreenState extends State<AddMountScreen>
|
||||
with SafeSetState<AddMountScreen> {
|
||||
Mount? _mount;
|
||||
final _mountNameController = TextEditingController();
|
||||
String _mountType = "";
|
||||
bool _enabled = true;
|
||||
|
||||
final Map<String, Map<String, dynamic>> _settings = {
|
||||
"": {},
|
||||
@@ -30,177 +38,156 @@ class _AddMountScreenState extends State<AddMountScreen> {
|
||||
"Sia": createDefaultSettings("Sia"),
|
||||
};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mountNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
actions: [
|
||||
Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => auth.logoff(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.all(0.0),
|
||||
child: 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: constants.padding),
|
||||
DropdownButton<String>(
|
||||
autofocus: true,
|
||||
value: _mountType,
|
||||
onChanged: (mountType) =>
|
||||
_handleChange(auth, mountType ?? ''),
|
||||
items: constants.providerTypeList
|
||||
.map<DropdownMenuItem<String>>((item) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: item,
|
||||
child: Text(item),
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_mountType.isNotEmpty && _mountType != 'Remote')
|
||||
const SizedBox(height: constants.padding),
|
||||
if (_mountType.isNotEmpty && _mountType != 'Remote')
|
||||
Card(
|
||||
margin: EdgeInsets.all(0.0),
|
||||
child: 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: constants.padding),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
controller: _mountNameController,
|
||||
keyboardType: TextInputType.text,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||
],
|
||||
onChanged: (_) => _handleChange(auth, _mountType),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_mount != null) ...[
|
||||
const SizedBox(height: constants.padding),
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.all(0.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: MountSettingsWidget(
|
||||
isAdd: true,
|
||||
mount: _mount!,
|
||||
settings: _settings[_mountType]!,
|
||||
showAdvanced: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
label: const Text('Test'),
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: _handleProviderTest,
|
||||
),
|
||||
const SizedBox(width: constants.padding),
|
||||
ElevatedButton.icon(
|
||||
label: const Text('Add'),
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
final mountList = Provider.of<MountList>(context);
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
List<String> failed = [];
|
||||
if (!validateSettings(
|
||||
_settings[_mountType]!,
|
||||
failed,
|
||||
)) {
|
||||
for (var key in failed) {
|
||||
displayErrorMessage(
|
||||
context,
|
||||
"Setting '$key' is not valid",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mountList.hasConfigName(
|
||||
_mountNameController.text,
|
||||
)) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Configuration name '${_mountNameController.text}' already exists",
|
||||
);
|
||||
}
|
||||
|
||||
if (_mountType == "Sia" || _mountType == "S3") {
|
||||
final bucket =
|
||||
_settings[_mountType]!["${_mountType}Config"]["Bucket"]
|
||||
as String;
|
||||
if (mountList.hasBucketName(_mountType, bucket)) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Bucket '$bucket' already exists",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final success = await mountList.add(
|
||||
_mountType,
|
||||
_mountType == 'Remote'
|
||||
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}'
|
||||
: _mountNameController.text,
|
||||
_settings[_mountType]!,
|
||||
);
|
||||
|
||||
if (!success || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
return AppScaffold(
|
||||
title: widget.title,
|
||||
showBack: true,
|
||||
children: [
|
||||
AppDropdown<String>(
|
||||
constrainToIntrinsic: true,
|
||||
isExpanded: false,
|
||||
labelOf: (s) => s,
|
||||
labelText: 'Provider Type',
|
||||
onChanged: (mountType) {
|
||||
_handleChange(
|
||||
Provider.of<Auth>(context, listen: false),
|
||||
mountType ?? '',
|
||||
);
|
||||
},
|
||||
prefixIcon: Icons.miscellaneous_services,
|
||||
value: _mountType.isEmpty ? null : _mountType,
|
||||
values: constants.providerTypeList,
|
||||
widthMultiplier: 2.0,
|
||||
),
|
||||
),
|
||||
if (_mountType.isNotEmpty && _mountType != 'Remote') ...[
|
||||
const SizedBox(height: constants.padding),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
controller: _mountNameController,
|
||||
keyboardType: TextInputType.text,
|
||||
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
|
||||
onChanged: (_) => _handleChange(
|
||||
Provider.of<Auth>(context, listen: false),
|
||||
_mountType,
|
||||
),
|
||||
decoration: createCommonDecoration(
|
||||
scheme,
|
||||
'Configuration Name',
|
||||
hintText: 'Enter a unique name',
|
||||
icon: Icons.drive_file_rename_outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_mount != null) ...[
|
||||
const SizedBox(height: constants.padding),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: constants.padding,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadius),
|
||||
child: MountSettingsWidget(
|
||||
isAdd: true,
|
||||
mount: _mount!,
|
||||
settings: _settings[_mountType]!,
|
||||
showAdvanced: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
Row(
|
||||
children: [
|
||||
IntrinsicWidth(
|
||||
child: AppOutlinedIconButton(
|
||||
enabled: _enabled,
|
||||
icon: Icons.check,
|
||||
text: 'Test',
|
||||
onPressed: _handleProviderTest,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: constants.padding),
|
||||
IntrinsicWidth(
|
||||
child: AppOutlinedIconButton(
|
||||
enabled: _enabled,
|
||||
icon: Icons.add,
|
||||
text: 'Add',
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
_enabled = false;
|
||||
});
|
||||
|
||||
try {
|
||||
final mountList = Provider.of<MountList>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
|
||||
List<String> failed = [];
|
||||
if (!validateSettings(_settings[_mountType]!, failed)) {
|
||||
for (var key in failed) {
|
||||
displayErrorMessage(
|
||||
context,
|
||||
"Setting '$key' is not valid",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mountList.hasConfigName(_mountNameController.text)) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Configuration name '${_mountNameController.text}' already exists",
|
||||
);
|
||||
}
|
||||
|
||||
if (_mountType == "Sia" || _mountType == "S3") {
|
||||
final bucket =
|
||||
_settings[_mountType]!["${_mountType}Config"]["Bucket"]
|
||||
as String;
|
||||
if (mountList.hasBucketName(_mountType, bucket)) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Bucket '$bucket' already exists",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final success = await mountList.add(
|
||||
_mountType,
|
||||
_mountType == 'Remote'
|
||||
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}'
|
||||
: _mountNameController.text,
|
||||
_settings[_mountType]!,
|
||||
);
|
||||
|
||||
if (!success || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
} finally {
|
||||
setState(() {
|
||||
_enabled = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,7 +199,7 @@ class _AddMountScreenState extends State<AddMountScreen> {
|
||||
if (_mountType == 'Remote') {
|
||||
_mountNameController.text = 'remote';
|
||||
} else if (changed) {
|
||||
_mountNameController.text = mountType == 'Sia' ? 'default' : '';
|
||||
_mountNameController.text = '';
|
||||
}
|
||||
|
||||
_mount = (_mountNameController.text.isEmpty)
|
||||
@@ -231,27 +218,28 @@ class _AddMountScreenState extends State<AddMountScreen> {
|
||||
}
|
||||
|
||||
Future<void> _handleProviderTest() async {
|
||||
if (_mount == null) {
|
||||
return;
|
||||
setState(() {
|
||||
_enabled = false;
|
||||
});
|
||||
|
||||
try {
|
||||
if (_mount == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await _mount!.test();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
displayErrorMessage(
|
||||
context,
|
||||
success ? "Success" : "Provider settings are invalid!",
|
||||
);
|
||||
} finally {
|
||||
setState(() {
|
||||
_enabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
final success = await _mount!.test();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
displayErrorMessage(
|
||||
context,
|
||||
success ? "Success" : "Provider settings are invalid!",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
// auth_screen.dart
|
||||
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart';
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/models/settings.dart';
|
||||
import 'package:repertory/widgets/app_text_field.dart';
|
||||
import 'package:repertory/widgets/aurora_sweep.dart';
|
||||
|
||||
class AuthScreen extends StatefulWidget {
|
||||
final String title;
|
||||
@@ -12,105 +19,315 @@ class AuthScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AuthScreenState extends State<AuthScreen> {
|
||||
bool _enabled = true;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _passwordController = TextEditingController();
|
||||
final _userController = TextEditingController();
|
||||
|
||||
bool _enabled = true;
|
||||
bool _obscure = true;
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
void dispose() {
|
||||
_passwordController.dispose();
|
||||
_userController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
Future<void> doLogin(Auth auth) async {
|
||||
if (!_enabled) {
|
||||
return;
|
||||
}
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _enabled = false);
|
||||
final authenticated = await auth.authenticate(
|
||||
_userController.text.trim(),
|
||||
_passwordController.text,
|
||||
);
|
||||
setState(() => _enabled = true);
|
||||
|
||||
if (authenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
displayErrorMessage(context, 'Invalid username or password', clear: true);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
if (auth.authenticated) {
|
||||
Future.delayed(Duration(milliseconds: 1), () {
|
||||
if (constants.navigatorKey.currentContext == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(
|
||||
constants.navigatorKey.currentContext!,
|
||||
).pushNamedAndRemoveUntil('/', (Route<dynamic> route) => false);
|
||||
});
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
createLoginHandler() {
|
||||
return _enabled
|
||||
? () async {
|
||||
setState(() => _enabled = false);
|
||||
await auth.authenticate(
|
||||
_userController.text,
|
||||
_passwordController.text,
|
||||
);
|
||||
setState(() => _enabled = true);
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: SizedBox(
|
||||
width: constants.logonWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
constants.appLogonTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(labelText: 'Username'),
|
||||
controller: _userController,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
TextField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(labelText: 'Password'),
|
||||
controller: _passwordController,
|
||||
textInputAction: TextInputAction.go,
|
||||
onSubmitted: (_) {
|
||||
final handler = createLoginHandler();
|
||||
if (handler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
ElevatedButton(
|
||||
onPressed: createLoginHandler(),
|
||||
child: const Text('Login'),
|
||||
),
|
||||
],
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: constants.gradientColors,
|
||||
stops: [0.0, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
Consumer<Settings>(
|
||||
builder: (_, settings, _) =>
|
||||
AuroraSweep(enabled: settings.enableAnimations),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: constants.outlineAlpha),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: const Alignment(0, 0.06),
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
width: 720,
|
||||
height: 720,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
scheme.primary.withValues(
|
||||
alpha: constants.primaryAlpha,
|
||||
),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0.0, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
if (auth.authenticated) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (constants.navigatorKey.currentContext == null) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(
|
||||
constants.navigatorKey.currentContext!,
|
||||
).pushNamedAndRemoveUntil('/', (r) => false);
|
||||
});
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: AnimatedScale(
|
||||
scale: 1.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: AnimatedOpacity(
|
||||
opacity: 1.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: constants.logonWidth,
|
||||
minWidth: constants.logonWidth,
|
||||
),
|
||||
child: Card(
|
||||
elevation: constants.padding,
|
||||
color: scheme.primary.withValues(
|
||||
alpha: constants.outlineAlpha,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
constants.borderRadius,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(
|
||||
constants.paddingLarge,
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
autovalidateMode:
|
||||
AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
width:
|
||||
constants.loginIconSize +
|
||||
constants.paddingMedium * 2.0,
|
||||
height:
|
||||
constants.loginIconSize +
|
||||
constants.paddingMedium * 2.0,
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.primary.withValues(
|
||||
alpha: constants.outlineAlpha,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
constants.borderRadiusSmall,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(
|
||||
alpha: constants.boxShadowAlpha,
|
||||
),
|
||||
blurRadius: constants.borderRadius,
|
||||
offset: Offset(
|
||||
0,
|
||||
constants.borderRadius,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(
|
||||
constants.paddingMedium,
|
||||
),
|
||||
child: Image.asset(
|
||||
'assets/images/repertory.png',
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, _, _) {
|
||||
return Icon(
|
||||
Icons.folder,
|
||||
color: scheme.primary,
|
||||
size: constants.loginIconSize,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: constants.paddingSmall,
|
||||
),
|
||||
Text(
|
||||
constants.appLogonTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(
|
||||
height: constants.paddingSmall,
|
||||
),
|
||||
Text(
|
||||
"Secure access to your mounts",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: scheme.onSurface),
|
||||
),
|
||||
const SizedBox(
|
||||
height: constants.paddingLarge,
|
||||
),
|
||||
AppTextField(
|
||||
autofocus: true,
|
||||
controller: _userController,
|
||||
icon: Icons.person,
|
||||
labelText: 'Username',
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) {
|
||||
return 'Enter your username';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
FocusScope.of(context).nextFocus();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
AppTextField(
|
||||
controller: _passwordController,
|
||||
icon: Icons.lock,
|
||||
labelText: 'Password',
|
||||
obscureText: _obscure,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: _obscure
|
||||
? 'Show password'
|
||||
: 'Hide password',
|
||||
icon: Icon(
|
||||
_obscure
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscure = !_obscure;
|
||||
});
|
||||
},
|
||||
),
|
||||
textInputAction: TextInputAction.go,
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) {
|
||||
return 'Enter your password';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
doLogin(auth);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
SizedBox(
|
||||
height: 46,
|
||||
child: ElevatedButton(
|
||||
onPressed: _enabled
|
||||
? () {
|
||||
doLogin(auth);
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: scheme.primary
|
||||
.withValues(
|
||||
alpha: constants.secondaryAlpha,
|
||||
),
|
||||
disabledBackgroundColor: scheme.primary
|
||||
.withValues(
|
||||
alpha: constants.outlineAlpha,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
constants.borderRadiusSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _enabled
|
||||
? const Text('Login')
|
||||
: const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'dart:convert';
|
||||
// edit_mount_screen.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/models/mount.dart';
|
||||
import 'package:repertory/utils/safe_set_state_mixin.dart';
|
||||
import 'package:repertory/widgets/app_scaffold.dart';
|
||||
import 'package:repertory/widgets/mount_settings.dart';
|
||||
|
||||
class EditMountScreen extends StatefulWidget {
|
||||
@@ -15,56 +17,43 @@ class EditMountScreen extends StatefulWidget {
|
||||
State<EditMountScreen> createState() => _EditMountScreenState();
|
||||
}
|
||||
|
||||
class _EditMountScreenState extends State<EditMountScreen> {
|
||||
class _EditMountScreenState extends State<EditMountScreen>
|
||||
with SafeSetState<EditMountScreen> {
|
||||
bool _showAdvanced = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
actions: [
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text("Advanced"),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_showAdvanced ? Icons.toggle_on : Icons.toggle_off,
|
||||
),
|
||||
onPressed:
|
||||
() => setState(() => _showAdvanced = !_showAdvanced),
|
||||
),
|
||||
],
|
||||
),
|
||||
Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => auth.logoff(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return AppScaffold(
|
||||
title: widget.title,
|
||||
showBack: true,
|
||||
advancedWidget: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Advanced",
|
||||
style: textTheme.labelLarge?.copyWith(color: scheme.onSurface),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
IconButton(
|
||||
tooltip: _showAdvanced ? 'Hide advanced' : 'Show advanced',
|
||||
icon: Icon(_showAdvanced ? Icons.toggle_on : Icons.toggle_off),
|
||||
color: _showAdvanced ? scheme.primary : scheme.onSurface,
|
||||
onPressed: () => setState(() => _showAdvanced = !_showAdvanced),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: MountSettingsWidget(
|
||||
mount: widget.mount,
|
||||
settings: jsonDecode(jsonEncode(widget.mount.mountConfig.settings)),
|
||||
showAdvanced: _showAdvanced,
|
||||
),
|
||||
children: [
|
||||
Expanded(
|
||||
child: MountSettingsWidget(
|
||||
mount: widget.mount,
|
||||
settings: jsonDecode(jsonEncode(widget.mount.mountConfig.settings)),
|
||||
showAdvanced: _showAdvanced,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
// edit_settings_screen.dart
|
||||
|
||||
import 'dart:convert' show jsonDecode, jsonEncode;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart';
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/utils/safe_set_state_mixin.dart';
|
||||
import 'package:repertory/widgets/app_scaffold.dart';
|
||||
import 'package:repertory/widgets/ui_settings.dart';
|
||||
|
||||
class EditSettingsScreen extends StatefulWidget {
|
||||
@@ -15,39 +20,34 @@ class EditSettingsScreen extends StatefulWidget {
|
||||
State<EditSettingsScreen> createState() => _EditSettingsScreenState();
|
||||
}
|
||||
|
||||
class _EditSettingsScreenState extends State<EditSettingsScreen> {
|
||||
class _EditSettingsScreenState extends State<EditSettingsScreen>
|
||||
with SafeSetState<EditSettingsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
actions: [
|
||||
Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => auth.logoff(),
|
||||
return AppScaffold(
|
||||
title: widget.title,
|
||||
showBack: true,
|
||||
showUISettings: true,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FutureBuilder<Map<String, dynamic>>(
|
||||
future: _grabSettings(),
|
||||
initialData: const <String, dynamic>{},
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return UISettingsWidget(
|
||||
origSettings: jsonDecode(jsonEncode(snapshot.requireData)),
|
||||
settings: snapshot.requireData,
|
||||
showAdvanced: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder(
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return UISettingsWidget(
|
||||
origSettings: jsonDecode(jsonEncode(snapshot.requireData)),
|
||||
settings: snapshot.requireData,
|
||||
showAdvanced: false,
|
||||
);
|
||||
},
|
||||
future: _grabSettings(),
|
||||
initialData: <String, dynamic>{},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,13 +75,4 @@ class _EditSettingsScreenState extends State<EditSettingsScreen> {
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// home_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/widgets/app_scaffold.dart';
|
||||
import 'package:repertory/widgets/mount_list_widget.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
@@ -15,43 +16,62 @@ class HomeScreen extends StatefulWidget {
|
||||
class _HomeScreeState extends State<HomeScreen> {
|
||||
@override
|
||||
Widget build(context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
leading: IconButton(
|
||||
onPressed: () => Navigator.pushNamed(context, '/settings'),
|
||||
icon: const Icon(Icons.storage),
|
||||
),
|
||||
title: Text(widget.title),
|
||||
actions: [
|
||||
Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => auth.logoff(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return AppScaffold(
|
||||
title: widget.title,
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: MountListWidget(),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => Navigator.pushNamed(context, '/add'),
|
||||
tooltip: 'Add Mount',
|
||||
child: const Icon(Icons.add),
|
||||
child: Hero(
|
||||
tag: 'add_mount_fab',
|
||||
child: Material(
|
||||
color: scheme.primary.withValues(alpha: constants.secondaryAlpha),
|
||||
elevation: 12,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadius),
|
||||
),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadius),
|
||||
border: Border.all(
|
||||
color: scheme.outlineVariant.withValues(
|
||||
alpha: constants.outlineAlpha,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: constants.gradientColors2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(
|
||||
alpha: constants.boxShadowAlpha,
|
||||
),
|
||||
blurRadius: constants.borderRadius,
|
||||
offset: Offset(0, constants.borderRadius),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadius),
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/add');
|
||||
},
|
||||
child: const SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: Center(
|
||||
child: Icon(Icons.add, size: constants.largeIconSize),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
children: [Expanded(child: const MountListWidget())],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
// settings.dart
|
||||
|
||||
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';
|
||||
import 'package:repertory/helpers.dart'
|
||||
show Validator, displayErrorMessage, doShowDialog;
|
||||
import 'package:repertory/widgets/app_dropdown.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
|
||||
void createBooleanSetting(
|
||||
context,
|
||||
BuildContext context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
@@ -34,7 +38,7 @@ void createBooleanSetting(
|
||||
}
|
||||
|
||||
void createIntListSetting(
|
||||
context,
|
||||
BuildContext context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
@@ -53,20 +57,18 @@ void createIntListSetting(
|
||||
SettingsTile.navigation(
|
||||
title: createSettingTitle(context, key, description),
|
||||
leading: Icon(icon),
|
||||
value: DropdownButton<String>(
|
||||
value: value.toString(),
|
||||
value: AppDropdown<String>(
|
||||
labelOf: (s) => s,
|
||||
constrainToIntrinsic: true,
|
||||
onChanged: (newValue) {
|
||||
setState(
|
||||
() =>
|
||||
settings[key] = int.parse(
|
||||
newValue ?? defaultValue.toString(),
|
||||
),
|
||||
() => settings[key] = int.parse(
|
||||
newValue ?? defaultValue.toString(),
|
||||
),
|
||||
);
|
||||
},
|
||||
items:
|
||||
valueList.map<DropdownMenuItem<String>>((item) {
|
||||
return DropdownMenuItem<String>(value: item, child: Text(item));
|
||||
}).toList(),
|
||||
value: value.toString(),
|
||||
values: valueList,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -74,7 +76,7 @@ void createIntListSetting(
|
||||
}
|
||||
|
||||
void createIntSetting(
|
||||
context,
|
||||
BuildContext context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
@@ -95,42 +97,40 @@ void createIntSetting(
|
||||
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));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
content: TextField(
|
||||
autofocus: true,
|
||||
controller: TextEditingController(text: updatedValue),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (nextValue) => updatedValue = nextValue,
|
||||
doShowDialog(
|
||||
context,
|
||||
AlertDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: createSettingTitle(context, key, description),
|
||||
);
|
||||
},
|
||||
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));
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -139,7 +139,7 @@ void createIntSetting(
|
||||
}
|
||||
|
||||
void createPasswordSetting(
|
||||
context,
|
||||
BuildContext context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
@@ -163,107 +163,103 @@ void createPasswordSetting(
|
||||
String updatedValue2 = value;
|
||||
bool hidePassword1 = true;
|
||||
bool hidePassword2 = true;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
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),
|
||||
doShowDialog(
|
||||
context,
|
||||
StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
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",
|
||||
);
|
||||
if (result != null) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Setting '$key' is not valid",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() => settings[key] = updatedValue1);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
final result = validators.firstWhereOrNull(
|
||||
(validator) => !validator(updatedValue1),
|
||||
);
|
||||
if (result != null) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Setting '$key' is not valid",
|
||||
);
|
||||
}
|
||||
|
||||
setState(() => settings[key] = updatedValue1);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: TextEditingController(
|
||||
text: updatedValue1,
|
||||
),
|
||||
obscureText: hidePassword1,
|
||||
obscuringCharacter: '*',
|
||||
onChanged: (value) => updatedValue1 = value,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => setDialogState(
|
||||
() => hidePassword1 = !hidePassword1,
|
||||
),
|
||||
icon: Icon(
|
||||
hidePassword1
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: false,
|
||||
controller: TextEditingController(
|
||||
text: updatedValue2,
|
||||
),
|
||||
obscureText: hidePassword2,
|
||||
obscuringCharacter: '*',
|
||||
onChanged: (value) => updatedValue2 = value,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => setDialogState(
|
||||
() => hidePassword2 = !hidePassword2,
|
||||
),
|
||||
icon: Icon(
|
||||
hidePassword2
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: TextEditingController(
|
||||
text: updatedValue1,
|
||||
),
|
||||
obscureText: hidePassword1,
|
||||
obscuringCharacter: '*',
|
||||
onChanged: (value) => updatedValue1 = value,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed:
|
||||
() => setDialogState(
|
||||
() => hidePassword1 = !hidePassword1,
|
||||
),
|
||||
icon: Icon(
|
||||
hidePassword1
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: false,
|
||||
controller: TextEditingController(
|
||||
text: updatedValue2,
|
||||
),
|
||||
obscureText: hidePassword2,
|
||||
obscuringCharacter: '*',
|
||||
onChanged: (value) => updatedValue2 = value,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed:
|
||||
() => setDialogState(
|
||||
() => hidePassword2 = !hidePassword2,
|
||||
),
|
||||
icon: Icon(
|
||||
hidePassword2
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
title: createSettingTitle(context, key, description),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
title: createSettingTitle(context, key, description),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -271,7 +267,11 @@ void createPasswordSetting(
|
||||
}
|
||||
}
|
||||
|
||||
Widget createSettingTitle(context, String key, String? description) {
|
||||
Widget createSettingTitle(
|
||||
BuildContext context,
|
||||
String key,
|
||||
String? description,
|
||||
) {
|
||||
if (description == null) {
|
||||
return Text(key);
|
||||
}
|
||||
@@ -292,7 +292,7 @@ Widget createSettingTitle(context, String key, String? description) {
|
||||
}
|
||||
|
||||
void createStringListSetting(
|
||||
context,
|
||||
BuildContext context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
@@ -310,13 +310,12 @@ void createStringListSetting(
|
||||
SettingsTile.navigation(
|
||||
title: createSettingTitle(context, key, description),
|
||||
leading: Icon(icon),
|
||||
value: DropdownButton<String>(
|
||||
value: value,
|
||||
value: AppDropdown<String>(
|
||||
constrainToIntrinsic: true,
|
||||
labelOf: (s) => s,
|
||||
onChanged: (newValue) => setState(() => settings[key] = newValue),
|
||||
items:
|
||||
valueList.map<DropdownMenuItem<String>>((item) {
|
||||
return DropdownMenuItem<String>(value: item, child: Text(item));
|
||||
}).toList(),
|
||||
value: value,
|
||||
values: valueList,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -324,7 +323,7 @@ void createStringListSetting(
|
||||
}
|
||||
|
||||
void createStringSetting(
|
||||
context,
|
||||
BuildContext context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
@@ -345,43 +344,41 @@ void createStringSetting(
|
||||
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);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
content: TextField(
|
||||
autofocus: true,
|
||||
controller: TextEditingController(text: updatedValue),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||
],
|
||||
onChanged: (value) => updatedValue = value,
|
||||
doShowDialog(
|
||||
context,
|
||||
AlertDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: createSettingTitle(context, key, description),
|
||||
);
|
||||
},
|
||||
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);
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -3,13 +3,17 @@ import 'package:repertory/helpers.dart' show initialCaps;
|
||||
|
||||
class MountConfig {
|
||||
bool? mounted;
|
||||
bool autoStart = false;
|
||||
final String _name;
|
||||
String path = '';
|
||||
Map<String, dynamic> _settings = {};
|
||||
final String _type;
|
||||
MountConfig({required name, required type, Map<String, dynamic>? settings})
|
||||
: _name = name,
|
||||
_type = type {
|
||||
MountConfig({
|
||||
required String name,
|
||||
required String type,
|
||||
Map<String, dynamic>? settings,
|
||||
}) : _name = name,
|
||||
_type = type {
|
||||
if (settings != null) {
|
||||
_settings = settings;
|
||||
}
|
||||
@@ -27,6 +31,9 @@ class MountConfig {
|
||||
}
|
||||
|
||||
void updateStatus(Map<String, dynamic> status) {
|
||||
autoStart = status.containsKey('AutoStart')
|
||||
? status['AutoStart'] as bool
|
||||
: false;
|
||||
path = status['Location'] as String;
|
||||
mounted = status['Active'] as bool;
|
||||
}
|
||||
|
||||
14
web/repertory/lib/utils/safe_set_state_mixin.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
// safe_set_state_mixin.dart
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
mixin SafeSetState<T extends StatefulWidget> on State<T> {
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
145
web/repertory/lib/widgets/app_dropdown.dart
Normal file
@@ -0,0 +1,145 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart';
|
||||
|
||||
class AppDropdown<T> extends StatelessWidget {
|
||||
const AppDropdown({
|
||||
super.key,
|
||||
required this.labelOf,
|
||||
required this.values,
|
||||
this.constrainToIntrinsic = false,
|
||||
this.contentPadding,
|
||||
this.dropdownColor,
|
||||
this.enabled = true,
|
||||
this.fillColor,
|
||||
this.isExpanded = false,
|
||||
this.labelText,
|
||||
this.maxWidth,
|
||||
this.onChanged,
|
||||
this.prefixIcon,
|
||||
this.textStyle,
|
||||
this.validator,
|
||||
this.value,
|
||||
this.widthMultiplier = 1.0,
|
||||
});
|
||||
|
||||
final List<T> values;
|
||||
final String Function(T value) labelOf;
|
||||
final T? value;
|
||||
final ValueChanged<T?>? onChanged;
|
||||
final FormFieldValidator<T>? validator;
|
||||
final String? labelText;
|
||||
final IconData? prefixIcon;
|
||||
final bool enabled;
|
||||
final bool constrainToIntrinsic;
|
||||
final double widthMultiplier;
|
||||
final double? maxWidth;
|
||||
final bool isExpanded;
|
||||
final Color? dropdownColor;
|
||||
final TextStyle? textStyle;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
final Color? fillColor;
|
||||
|
||||
double _measureTextWidth(
|
||||
BuildContext context,
|
||||
String text,
|
||||
TextStyle? style,
|
||||
) {
|
||||
final tp = TextPainter(
|
||||
text: TextSpan(text: text, style: style),
|
||||
maxLines: 1,
|
||||
textDirection: Directionality.of(context),
|
||||
)..layout();
|
||||
return tp.width;
|
||||
}
|
||||
|
||||
double? _computedMaxWidth(BuildContext context) {
|
||||
if (!constrainToIntrinsic) {
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final effectiveStyle =
|
||||
textStyle ??
|
||||
theme.textTheme.bodyMedium?.copyWith(color: scheme.onSurface);
|
||||
|
||||
final longest = values.isEmpty
|
||||
? ''
|
||||
: values
|
||||
.map((v) => labelOf(v))
|
||||
.reduce((a, b) => a.length >= b.length ? a : b);
|
||||
|
||||
final labelW = _measureTextWidth(context, longest, effectiveStyle);
|
||||
|
||||
final prefixW = prefixIcon == null ? 0.0 : 48.0;
|
||||
const arrowW = constants.largeIconSize;
|
||||
final pad = contentPadding ?? const EdgeInsets.all(constants.paddingSmall);
|
||||
final padW = (pad is EdgeInsets)
|
||||
? (pad.left + pad.right)
|
||||
: constants.padding;
|
||||
|
||||
final base = labelW + prefixW + arrowW + padW;
|
||||
|
||||
final cap =
|
||||
maxWidth ?? (MediaQuery.of(context).size.width - constants.padding * 2);
|
||||
return (base * widthMultiplier).clamp(0.0, cap);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
|
||||
final effectiveFill =
|
||||
fillColor ??
|
||||
darken(scheme.primary, 0.95).withValues(alpha: constants.dropDownAlpha);
|
||||
|
||||
final effectiveTextStyle =
|
||||
textStyle ??
|
||||
theme.textTheme.bodyMedium?.copyWith(color: scheme.onSurface);
|
||||
|
||||
final items = values.map((v) {
|
||||
return DropdownMenuItem<T>(
|
||||
value: v,
|
||||
child: Text(
|
||||
labelOf(v),
|
||||
style: effectiveTextStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final field = DropdownButtonFormField<T>(
|
||||
decoration: createCommonDecoration(
|
||||
scheme,
|
||||
labelText ?? "",
|
||||
filled: true,
|
||||
icon: prefixIcon,
|
||||
),
|
||||
dropdownColor: dropdownColor ?? effectiveFill,
|
||||
iconEnabledColor: scheme.onSurface,
|
||||
initialValue: value,
|
||||
isExpanded: isExpanded,
|
||||
items: items,
|
||||
onChanged: enabled ? onChanged : null,
|
||||
style: effectiveTextStyle,
|
||||
validator: validator,
|
||||
);
|
||||
|
||||
final maxWidth = _computedMaxWidth(context);
|
||||
final wrapped = maxWidth == null
|
||||
? field
|
||||
: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: field,
|
||||
);
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
heightFactor: 1.0,
|
||||
child: wrapped,
|
||||
);
|
||||
}
|
||||
}
|
||||
62
web/repertory/lib/widgets/app_icon_button_framed.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
// app_icon_button_framed.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
|
||||
class AppIconButtonFramed extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback? onPressed;
|
||||
final Color? iconColor;
|
||||
|
||||
const AppIconButtonFramed({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.onPressed,
|
||||
this.iconColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final radius = BorderRadius.circular(constants.borderRadiusSmall);
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: radius,
|
||||
child: Ink(
|
||||
width: 46,
|
||||
height: 46,
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.primary.withValues(alpha: constants.outlineAlpha),
|
||||
borderRadius: radius,
|
||||
border: Border.all(
|
||||
color: scheme.outlineVariant.withValues(
|
||||
alpha: constants.outlineAlpha,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: constants.boxShadowAlpha),
|
||||
blurRadius: constants.borderRadiusTiny,
|
||||
offset: const Offset(0, constants.borderRadiusSmall),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Transform.scale(
|
||||
scale: 0.90,
|
||||
child: Icon(
|
||||
icon,
|
||||
color: iconColor ?? scheme.onSurface,
|
||||
size: constants.largeIconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
47
web/repertory/lib/widgets/app_outlined_icon_button.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
// app_outlined_icon_button.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
|
||||
class AppOutlinedIconButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final bool enabled;
|
||||
final String text;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const AppOutlinedIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.enabled,
|
||||
required this.onPressed,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Opacity(
|
||||
opacity: enabled ? 1.0 : constants.secondaryAlpha,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: enabled ? onPressed : null,
|
||||
icon: Icon(icon, size: constants.smallIconSize),
|
||||
label: Text(text),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: scheme.primary,
|
||||
side: BorderSide(
|
||||
color: scheme.primary.withValues(alpha: constants.secondaryAlpha),
|
||||
width: 1.2,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
backgroundColor: scheme.primary.withValues(
|
||||
alpha: constants.outlineAlpha,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
207
web/repertory/lib/widgets/app_scaffold.dart
Normal file
@@ -0,0 +1,207 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/models/settings.dart';
|
||||
import 'package:repertory/widgets/aurora_sweep.dart';
|
||||
|
||||
class AppScaffold extends StatelessWidget {
|
||||
const AppScaffold({
|
||||
super.key,
|
||||
required this.children,
|
||||
required this.title,
|
||||
this.advancedWidget,
|
||||
this.floatingActionButton,
|
||||
this.showBack = false,
|
||||
this.showUISettings = false,
|
||||
});
|
||||
|
||||
final List<Widget> children;
|
||||
final String title;
|
||||
final Widget? advancedWidget;
|
||||
final Widget? floatingActionButton;
|
||||
final bool showBack;
|
||||
final bool showUISettings;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: constants.gradientColors,
|
||||
stops: [0.0, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
Consumer<Settings>(
|
||||
builder: (_, settings, _) =>
|
||||
AuroraSweep(enabled: settings.enableAnimations),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: constants.outlineAlpha),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (!showBack) ...[
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Image.asset(
|
||||
'assets/images/repertory.png',
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, _, _) {
|
||||
return Icon(
|
||||
Icons.folder,
|
||||
color: scheme.primary,
|
||||
size: constants.largeIconSize,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: constants.padding),
|
||||
],
|
||||
if (showBack) ...[
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(
|
||||
constants.borderRadius,
|
||||
),
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Ink(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surface.withValues(
|
||||
alpha: constants.secondaryAlpha,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
constants.borderRadius,
|
||||
),
|
||||
border: Border.all(
|
||||
color: scheme.outlineVariant.withValues(
|
||||
alpha: constants.highlightAlpha,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(
|
||||
alpha: constants.boxShadowAlpha,
|
||||
),
|
||||
blurRadius: constants.borderRadius,
|
||||
offset: Offset(0, constants.borderRadius),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: constants.padding),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: constants.padding),
|
||||
if (!showBack || showUISettings) ...[
|
||||
const Text("Animations"),
|
||||
Consumer<Settings>(
|
||||
builder: (context, settings, _) => IconButton(
|
||||
icon: Icon(
|
||||
settings.enableAnimations
|
||||
? Icons.toggle_on
|
||||
: Icons.toggle_off,
|
||||
),
|
||||
color: settings.enableAnimations
|
||||
? scheme.primary
|
||||
: scheme.onSurface,
|
||||
onPressed: () => settings.setEnableAnimations(
|
||||
!settings.enableAnimations,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text("Auto-start"),
|
||||
Consumer<Settings>(
|
||||
builder: (context, settings, _) => IconButton(
|
||||
icon: Icon(
|
||||
settings.autoStart
|
||||
? Icons.toggle_on
|
||||
: Icons.toggle_off,
|
||||
),
|
||||
color: settings.autoStart
|
||||
? scheme.primary
|
||||
: scheme.onSurface,
|
||||
onPressed: () =>
|
||||
settings.setAutoStart(!settings.autoStart),
|
||||
),
|
||||
),
|
||||
if (showUISettings)
|
||||
const SizedBox(width: constants.padding),
|
||||
],
|
||||
if (!showBack) ...[
|
||||
IconButton(
|
||||
tooltip: 'Settings',
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/settings');
|
||||
},
|
||||
),
|
||||
const SizedBox(width: constants.padding),
|
||||
],
|
||||
if (showBack && advancedWidget != null) ...[
|
||||
advancedWidget!,
|
||||
const SizedBox(width: constants.padding),
|
||||
],
|
||||
Consumer<Auth>(
|
||||
builder: (context, auth, _) => IconButton(
|
||||
tooltip: 'Log out',
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: auth.logoff,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: floatingActionButton,
|
||||
);
|
||||
}
|
||||
}
|
||||
66
web/repertory/lib/widgets/app_text_field.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:repertory/helpers.dart' as helpers;
|
||||
|
||||
class AppTextField extends StatelessWidget {
|
||||
const AppTextField({
|
||||
super.key,
|
||||
this.autofocus = false,
|
||||
this.controller,
|
||||
this.enabled = true,
|
||||
this.hintText,
|
||||
this.icon,
|
||||
this.keyboardType,
|
||||
this.labelText,
|
||||
this.maxLines = 1,
|
||||
this.obscureText = false,
|
||||
this.onChanged,
|
||||
this.onFieldSubmitted,
|
||||
this.suffixIcon,
|
||||
this.textInputAction,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
final bool autofocus;
|
||||
final TextEditingController? controller;
|
||||
final bool enabled;
|
||||
final String? hintText;
|
||||
final IconData? icon;
|
||||
final TextInputType? keyboardType;
|
||||
final String? labelText;
|
||||
final int? maxLines;
|
||||
final bool obscureText;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final ValueChanged<String>? onFieldSubmitted;
|
||||
final Widget? suffixIcon;
|
||||
final TextInputAction? textInputAction;
|
||||
final FormFieldValidator<String>? validator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
final decoration = helpers
|
||||
.createCommonDecoration(
|
||||
scheme,
|
||||
labelText ?? '',
|
||||
filled: true,
|
||||
hintText: hintText,
|
||||
icon: icon,
|
||||
)
|
||||
.copyWith(suffixIcon: suffixIcon);
|
||||
|
||||
return TextFormField(
|
||||
autofocus: autofocus,
|
||||
controller: controller,
|
||||
decoration: decoration,
|
||||
enabled: enabled,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
obscureText: obscureText,
|
||||
onChanged: onChanged,
|
||||
onFieldSubmitted: onFieldSubmitted,
|
||||
textInputAction: textInputAction,
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
}
|
||||
35
web/repertory/lib/widgets/app_toggle_button_framed.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
// app_toggle_button_framed.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:repertory/widgets/app_icon_button_framed.dart';
|
||||
|
||||
class AppToggleButtonFramed extends StatelessWidget {
|
||||
final bool? mounted;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const AppToggleButtonFramed({
|
||||
super.key,
|
||||
required this.mounted,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final bool isOn = mounted ?? false;
|
||||
|
||||
IconData icon = Icons.hourglass_top;
|
||||
Color iconColor = scheme.onSurface;
|
||||
|
||||
if (mounted != null) {
|
||||
icon = isOn ? Icons.toggle_on : Icons.toggle_off;
|
||||
iconColor = isOn ? scheme.primary : scheme.onSurface;
|
||||
}
|
||||
|
||||
return AppIconButtonFramed(
|
||||
icon: icon,
|
||||
iconColor: iconColor,
|
||||
onPressed: mounted == null ? null : onPressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
122
web/repertory/lib/widgets/aurora_sweep.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
// aurora_sweep.dart
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AuroraSweep extends StatefulWidget {
|
||||
const AuroraSweep({
|
||||
super.key,
|
||||
this.enabled = true,
|
||||
this.duration = const Duration(seconds: 28),
|
||||
this.primaryAlphaA = 0.08, // default dimmer for crispness
|
||||
this.primaryAlphaB = 0.07,
|
||||
this.staticPhase = 0.25,
|
||||
this.radiusX = 0.85,
|
||||
this.radiusY = 0.35,
|
||||
this.beginYOffset = -0.55,
|
||||
this.endYOffset = 0.90,
|
||||
});
|
||||
|
||||
final bool enabled;
|
||||
final Duration duration;
|
||||
final double primaryAlphaA;
|
||||
final double primaryAlphaB;
|
||||
final double staticPhase;
|
||||
final double radiusX;
|
||||
final double radiusY;
|
||||
final double beginYOffset;
|
||||
final double endYOffset;
|
||||
|
||||
@override
|
||||
State<AuroraSweep> createState() => _AuroraSweepState();
|
||||
}
|
||||
|
||||
class _AuroraSweepState extends State<AuroraSweep>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _c = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.enabled) {
|
||||
_c.repeat();
|
||||
} else {
|
||||
_c.value = widget.staticPhase.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant AuroraSweep oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.duration != widget.duration) {
|
||||
_c.duration = widget.duration;
|
||||
}
|
||||
|
||||
if (widget.enabled && !_c.isAnimating) {
|
||||
_c.repeat();
|
||||
} else if (!widget.enabled && _c.isAnimating) {
|
||||
_c.stop();
|
||||
_c.value = widget.staticPhase.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_c.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
(Alignment, Alignment) _alignmentsFromPhase(double t) {
|
||||
final theta = 2 * math.pi * t;
|
||||
final begin = Alignment(
|
||||
widget.radiusX * math.cos(theta),
|
||||
widget.beginYOffset + widget.radiusY * math.sin(theta),
|
||||
);
|
||||
final end = Alignment(
|
||||
widget.radiusX * math.cos(theta + math.pi / 2),
|
||||
widget.endYOffset + widget.radiusY * math.sin(theta + math.pi / 2),
|
||||
);
|
||||
return (begin, end);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
Widget paint(double t) {
|
||||
final (begin, end) = _alignmentsFromPhase(t);
|
||||
return IgnorePointer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: begin,
|
||||
end: end,
|
||||
colors: [
|
||||
scheme.primary.withValues(alpha: widget.primaryAlphaA),
|
||||
Colors.transparent,
|
||||
scheme.primary.withValues(alpha: widget.primaryAlphaB),
|
||||
],
|
||||
stops: const [0.0, 0.5, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!widget.enabled) {
|
||||
return paint(_c.value);
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _c,
|
||||
builder: (_, _) {
|
||||
final t = _c.value;
|
||||
return paint(t);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
32
web/repertory/lib/widgets/auth_check.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
// auth_check.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/models/auth.dart';
|
||||
|
||||
class AuthCheck extends StatelessWidget {
|
||||
final Widget child;
|
||||
const AuthCheck({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
if (!auth.authenticated) {
|
||||
Future.delayed(Duration(milliseconds: 1), () {
|
||||
if (constants.navigatorKey.currentContext == null) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(
|
||||
constants.navigatorKey.currentContext!,
|
||||
).pushNamedAndRemoveUntil('/auth', (Route<dynamic> route) => false);
|
||||
});
|
||||
return child;
|
||||
}
|
||||
|
||||
return child;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
// mount_settings.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart'
|
||||
show SettingsTile, SettingsList, DevicePlatform;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart'
|
||||
@@ -6,12 +10,13 @@ import 'package:repertory/helpers.dart'
|
||||
convertAllToString,
|
||||
getChanged,
|
||||
getSettingDescription,
|
||||
getSettingValidators;
|
||||
getSettingValidators,
|
||||
createSettingsTheme;
|
||||
import 'package:repertory/models/auth.dart';
|
||||
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';
|
||||
import 'package:repertory/widgets/settings/settings_section.dart';
|
||||
|
||||
class MountSettingsWidget extends StatefulWidget {
|
||||
final bool isAdd;
|
||||
@@ -35,6 +40,10 @@ class MountSettingsWidget extends StatefulWidget {
|
||||
class _MountSettingsWidgetState extends State<MountSettingsWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final settingsTheme = createSettingsTheme(scheme);
|
||||
|
||||
List<SettingsTile> commonSettings = [];
|
||||
List<SettingsTile> encryptConfigSettings = [];
|
||||
List<SettingsTile> hostConfigSettings = [];
|
||||
@@ -363,11 +372,8 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
|
||||
description: getSettingDescription('$key.$subKey'),
|
||||
validators: [
|
||||
...getSettingValidators('$key.$subKey'),
|
||||
(value) =>
|
||||
!Provider.of<MountList>(
|
||||
context,
|
||||
listen: false,
|
||||
).hasBucketName(
|
||||
(value) => !Provider.of<MountList>(context, listen: false)
|
||||
.hasBucketName(
|
||||
widget.mount.type,
|
||||
value,
|
||||
excludeName: widget.mount.name,
|
||||
@@ -381,44 +387,54 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
|
||||
}
|
||||
});
|
||||
|
||||
final titleStyle = theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: scheme.onSurface,
|
||||
);
|
||||
|
||||
return SettingsList(
|
||||
shrinkWrap: false,
|
||||
platform: DevicePlatform.web,
|
||||
lightTheme: settingsTheme,
|
||||
darkTheme: settingsTheme,
|
||||
sections: [
|
||||
if (encryptConfigSettings.isNotEmpty)
|
||||
SettingsSection(
|
||||
title: const Text('Encrypt Config'),
|
||||
title: Text('Encrypt Config', style: titleStyle),
|
||||
tiles: encryptConfigSettings,
|
||||
),
|
||||
if (hostConfigSettings.isNotEmpty)
|
||||
SettingsSection(
|
||||
title: const Text('Host Config'),
|
||||
title: Text('Host Config', style: titleStyle),
|
||||
tiles: hostConfigSettings,
|
||||
),
|
||||
if (remoteConfigSettings.isNotEmpty)
|
||||
SettingsSection(
|
||||
title: const Text('Remote Config'),
|
||||
title: Text('Remote Config', style: titleStyle),
|
||||
tiles: remoteConfigSettings,
|
||||
),
|
||||
if (s3ConfigSettings.isNotEmpty)
|
||||
SettingsSection(
|
||||
title: const Text('S3 Config'),
|
||||
title: Text('S3 Config', style: titleStyle),
|
||||
tiles: s3ConfigSettings,
|
||||
),
|
||||
if (siaConfigSettings.isNotEmpty)
|
||||
SettingsSection(
|
||||
title: const Text('Sia Config'),
|
||||
title: Text('Sia Config', style: titleStyle),
|
||||
tiles: siaConfigSettings,
|
||||
),
|
||||
if (remoteMountSettings.isNotEmpty)
|
||||
SettingsSection(
|
||||
title: const Text('Remote Mount'),
|
||||
tiles:
|
||||
widget.settings['RemoteMount']['Enable'] as bool
|
||||
? remoteMountSettings
|
||||
: [remoteMountSettings[0]],
|
||||
title: Text('Remote Mount', style: titleStyle),
|
||||
tiles: (widget.settings['RemoteMount']['Enable'] as bool)
|
||||
? remoteMountSettings
|
||||
: [remoteMountSettings[0]],
|
||||
),
|
||||
if (commonSettings.isNotEmpty)
|
||||
SettingsSection(title: const Text('Settings'), tiles: commonSettings),
|
||||
SettingsSection(
|
||||
title: Text('Settings', style: titleStyle),
|
||||
tiles: commonSettings,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -617,38 +633,6 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!widget.isAdd) {
|
||||
final settings = getChanged(
|
||||
widget.mount.mountConfig.settings,
|
||||
widget.settings,
|
||||
);
|
||||
if (settings.isNotEmpty) {
|
||||
final mount = widget.mount;
|
||||
final key =
|
||||
Provider.of<Auth>(
|
||||
constants.navigatorKey.currentContext!,
|
||||
listen: false,
|
||||
).key;
|
||||
convertAllToString(settings, key).then((map) {
|
||||
map.forEach((key, value) {
|
||||
if (value is Map<String, dynamic>) {
|
||||
value.forEach((subKey, subValue) {
|
||||
mount.setValue('$key.$subKey', subValue);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
mount.setValue(key, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _parseS3Config(List<SettingsTile> s3ConfigSettings, String key, value) {
|
||||
value.forEach((subKey, subValue) {
|
||||
switch (subKey) {
|
||||
@@ -686,11 +670,8 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
|
||||
description: getSettingDescription('$key.$subKey'),
|
||||
validators: [
|
||||
...getSettingValidators('$key.$subKey'),
|
||||
(value) =>
|
||||
!Provider.of<MountList>(
|
||||
context,
|
||||
listen: false,
|
||||
).hasBucketName(
|
||||
(value) => !Provider.of<MountList>(context, listen: false)
|
||||
.hasBucketName(
|
||||
widget.mount.type,
|
||||
value,
|
||||
excludeName: widget.mount.name,
|
||||
@@ -716,6 +697,22 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'ForceLegacyEncryption':
|
||||
{
|
||||
createBooleanSetting(
|
||||
context,
|
||||
s3ConfigSettings,
|
||||
widget.settings[key],
|
||||
subKey,
|
||||
subValue,
|
||||
false,
|
||||
widget.showAdvanced,
|
||||
widget,
|
||||
setState,
|
||||
description: getSettingDescription('$key.$subKey'),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'Region':
|
||||
{
|
||||
createStringSetting(
|
||||
@@ -926,6 +923,23 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'ConnectTimeoutMs':
|
||||
{
|
||||
createIntSetting(
|
||||
context,
|
||||
remoteConfigSettings,
|
||||
widget.settings[key],
|
||||
subKey,
|
||||
subValue,
|
||||
true,
|
||||
widget.showAdvanced,
|
||||
widget,
|
||||
setState,
|
||||
description: getSettingDescription('$key.$subKey'),
|
||||
validators: getSettingValidators('$key.$subKey'),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'EncryptionToken':
|
||||
{
|
||||
createPasswordSetting(
|
||||
@@ -1016,6 +1030,35 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!widget.isAdd) {
|
||||
final settings = getChanged(
|
||||
widget.mount.mountConfig.settings,
|
||||
widget.settings,
|
||||
);
|
||||
if (settings.isNotEmpty) {
|
||||
final mount = widget.mount;
|
||||
final key = Provider.of<Auth>(
|
||||
constants.navigatorKey.currentContext!,
|
||||
listen: false,
|
||||
).key;
|
||||
convertAllToString(settings, key).then((map) {
|
||||
map.forEach((key, value) {
|
||||
if (value is Map<String, dynamic>) {
|
||||
value.forEach((subKey, subValue) {
|
||||
mount.setValue('$key.$subKey', subValue);
|
||||
});
|
||||
return;
|
||||
}
|
||||
mount.setValue(key, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'dart:async';
|
||||
// mount_widget.dart
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart';
|
||||
import 'package:repertory/models/mount.dart';
|
||||
import 'package:repertory/utils/safe_set_state_mixin.dart';
|
||||
import 'package:repertory/widgets/app_outlined_icon_button.dart';
|
||||
import 'package:repertory/widgets/app_icon_button_framed.dart';
|
||||
import 'package:repertory/widgets/app_toggle_button_framed.dart';
|
||||
|
||||
class MountWidget extends StatefulWidget {
|
||||
const MountWidget({super.key});
|
||||
@@ -15,175 +18,295 @@ class MountWidget extends StatefulWidget {
|
||||
State<MountWidget> createState() => _MountWidgetState();
|
||||
}
|
||||
|
||||
class _MountWidgetState extends State<MountWidget> {
|
||||
class _MountWidgetState extends State<MountWidget>
|
||||
with SafeSetState<MountWidget> {
|
||||
bool _enabled = true;
|
||||
bool _editEnabled = true;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(0.0),
|
||||
child: Consumer<Mount>(
|
||||
builder: (context, Mount mount, _) {
|
||||
final textColor = Theme.of(context).colorScheme.onSurface;
|
||||
final subTextColor =
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white38
|
||||
: Colors.black87;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
final nameText = SelectableText(
|
||||
formatMountName(mount.type, mount.name),
|
||||
style: TextStyle(color: subTextColor),
|
||||
);
|
||||
final titleStyle = textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: scheme.onSurface,
|
||||
);
|
||||
final subStyle = textTheme.bodyMedium?.copyWith(color: scheme.onSurface);
|
||||
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.settings, color: textColor),
|
||||
onPressed:
|
||||
() => Navigator.pushNamed(context, '/edit', arguments: mount),
|
||||
final visualDensity = VisualDensity(
|
||||
horizontal: -VisualDensity.maximumDensity,
|
||||
vertical: -(VisualDensity.maximumDensity * 2.0),
|
||||
);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 120),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(0.0),
|
||||
elevation: 12,
|
||||
color: scheme.primary.withValues(alpha: constants.primaryAlpha),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
|
||||
side: BorderSide(
|
||||
color: scheme.outlineVariant.withValues(
|
||||
alpha: constants.outlineAlpha,
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
nameText,
|
||||
SelectableText(
|
||||
mount.path.isEmpty && mount.mounted == null
|
||||
? 'loading...'
|
||||
: mount.path.isEmpty
|
||||
? '<mount location not set>'
|
||||
: mount.path,
|
||||
style: TextStyle(color: subTextColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: SelectableText(
|
||||
mount.provider,
|
||||
style: TextStyle(color: textColor, fontWeight: FontWeight.bold),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (mount.mounted != null && !mount.mounted!)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
color: subTextColor,
|
||||
tooltip: 'Edit mount location',
|
||||
onPressed: () async {
|
||||
setState(() => _editEnabled = false);
|
||||
final available = await mount.getAvailableLocations();
|
||||
if (context.mounted) {
|
||||
final location = await editMountLocation(
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: Consumer<Mount>(
|
||||
builder: (context, Mount mount, _) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
AppIconButtonFramed(
|
||||
icon: Icons.settings,
|
||||
onPressed: () => Navigator.pushNamed(
|
||||
context,
|
||||
available,
|
||||
location: mount.path,
|
||||
);
|
||||
if (location != null) {
|
||||
await mount.setMountLocation(location);
|
||||
}
|
||||
}
|
||||
setState(() => _editEnabled = true);
|
||||
},
|
||||
'/edit',
|
||||
arguments: mount,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: constants.padding),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(mount.provider, style: titleStyle),
|
||||
SelectableText(
|
||||
'Name • ${formatMountName(mount.type, mount.name)}',
|
||||
style: subStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (mount.mounted == false) ...[
|
||||
AppIconButtonFramed(
|
||||
icon: Icons.delete,
|
||||
onPressed: _enabled
|
||||
? () async {
|
||||
setState(() {
|
||||
_enabled = false;
|
||||
});
|
||||
return doShowDialog(
|
||||
context,
|
||||
AlertDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Yes'),
|
||||
onPressed: () async {
|
||||
await mount.remove();
|
||||
setState(() {
|
||||
_enabled = true;
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('No'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_enabled = true;
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
title: const Text(
|
||||
'Are you sure?',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
SizedBox(width: constants.paddingSmall),
|
||||
],
|
||||
AppToggleButtonFramed(
|
||||
mounted: mount.mounted,
|
||||
onPressed: _createMountHandler(context, mount),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
mount.mounted == null
|
||||
? Icons.hourglass_top
|
||||
: mount.mounted!
|
||||
? Icons.toggle_on
|
||||
: Icons.toggle_off,
|
||||
color:
|
||||
mount.mounted ?? false
|
||||
? Color.fromARGB(255, 163, 96, 76)
|
||||
: subTextColor,
|
||||
const SizedBox(height: constants.padding),
|
||||
Row(
|
||||
children: [
|
||||
AppOutlinedIconButton(
|
||||
text: 'Edit path',
|
||||
icon: Icons.edit,
|
||||
enabled: _enabled && mount.mounted == false,
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
_enabled = false;
|
||||
});
|
||||
|
||||
final available = await mount.getAvailableLocations();
|
||||
|
||||
if (!mounted) {
|
||||
setState(() {
|
||||
_enabled = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final location = await editMountLocation(
|
||||
context,
|
||||
available,
|
||||
location: mount.path,
|
||||
);
|
||||
if (location != null) {
|
||||
await mount.setMountLocation(location);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_enabled = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: constants.padding),
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
_prettyPath(mount),
|
||||
style: subStyle,
|
||||
),
|
||||
),
|
||||
IntrinsicWidth(
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
listTileTheme: const ListTileThemeData(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
horizontalTitleGap: 0,
|
||||
minLeadingWidth: 0,
|
||||
minVerticalPadding: 0,
|
||||
),
|
||||
checkboxTheme: CheckboxThemeData(
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: visualDensity,
|
||||
),
|
||||
),
|
||||
child: CheckboxListTile(
|
||||
enabled:
|
||||
!(mount.path.isEmpty && mount.mounted == null),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
onChanged: (_) async {
|
||||
return mount.setMountAutoStart(!mount.autoStart);
|
||||
},
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.auto_mode,
|
||||
size: constants.smallIconSize,
|
||||
),
|
||||
SizedBox(width: constants.paddingSmall),
|
||||
Text('Auto-mount'),
|
||||
SizedBox(width: constants.paddingSmall),
|
||||
],
|
||||
),
|
||||
value: mount.autoStart,
|
||||
visualDensity: visualDensity,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
tooltip:
|
||||
mount.mounted == null
|
||||
? ''
|
||||
: mount.mounted!
|
||||
? 'Unmount'
|
||||
: 'Mount',
|
||||
onPressed: _createMountHandler(context, mount),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
VoidCallback? _createMountHandler(context, Mount mount) {
|
||||
return _enabled && mount.mounted != null
|
||||
? () async {
|
||||
if (mount.mounted == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final mounted = mount.mounted!;
|
||||
|
||||
setState(() {
|
||||
_enabled = false;
|
||||
});
|
||||
|
||||
final location = await _getMountLocation(context, mount);
|
||||
|
||||
cleanup() {
|
||||
setState(() {
|
||||
_enabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (!mounted && location == null) {
|
||||
displayErrorMessage(context, "Mount location is not set");
|
||||
return cleanup();
|
||||
}
|
||||
|
||||
final success = await mount.mount(mounted, location: location);
|
||||
if (success ||
|
||||
mounted ||
|
||||
constants.navigatorKey.currentContext == null ||
|
||||
!constants.navigatorKey.currentContext!.mounted) {
|
||||
return cleanup();
|
||||
}
|
||||
|
||||
displayErrorMessage(
|
||||
context,
|
||||
"Mount location is not available: $location",
|
||||
);
|
||||
cleanup();
|
||||
}
|
||||
: null;
|
||||
String _prettyPath(Mount mount) {
|
||||
if (mount.path.isEmpty && mount.mounted == null) {
|
||||
return 'loading...';
|
||||
}
|
||||
if (mount.path.isEmpty) {
|
||||
return '<mount location not set>';
|
||||
}
|
||||
return mount.path;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<String?> _getMountLocation(context, Mount mount) async {
|
||||
if (mount.mounted ?? false) {
|
||||
VoidCallback? _createMountHandler(BuildContext context, Mount mount) {
|
||||
if (!(_enabled && mount.mounted != null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return () async {
|
||||
if (mount.mounted == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final mounted = mount.mounted!;
|
||||
setState(() {
|
||||
_enabled = false;
|
||||
});
|
||||
|
||||
final location = await _getMountLocation(context, mount);
|
||||
|
||||
void cleanup() {
|
||||
setState(() {
|
||||
_enabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (!mounted && location == null) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
displayErrorMessage(context, 'Mount location is not set');
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await mount.mount(mounted, location: location);
|
||||
if (success ||
|
||||
mounted ||
|
||||
constants.navigatorKey.currentContext == null ||
|
||||
!constants.navigatorKey.currentContext!.mounted) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
displayErrorMessage(
|
||||
context,
|
||||
'Mount location is not available: $location',
|
||||
);
|
||||
cleanup();
|
||||
};
|
||||
}
|
||||
|
||||
Future<String?> _getMountLocation(BuildContext context, Mount mount) async {
|
||||
if (mount.mounted ?? false) {
|
||||
return null;
|
||||
}
|
||||
if (mount.path.isNotEmpty) {
|
||||
return mount.path;
|
||||
}
|
||||
|
||||
String? location = await mount.getMountLocation();
|
||||
if (location != null) {
|
||||
return location;
|
||||
}
|
||||
|
||||
if (!context.mounted) {
|
||||
return location;
|
||||
}
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
return editMountLocation(context, await mount.getAvailableLocations());
|
||||
}
|
||||
|
||||
@@ -196,11 +319,9 @@ class _MountWidgetState extends State<MountWidget> {
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
33
web/repertory/lib/widgets/section_card.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
// section_card.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
|
||||
class SectionCard extends StatelessWidget {
|
||||
const SectionCard({super.key, required this.title, required this.child});
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Card(
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.symmetric(vertical: constants.paddingSmall),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: constants.paddingSmall),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
web/repertory/lib/widgets/settings/settings_section.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_settings_ui/src/sections/abstract_settings_section.dart';
|
||||
import 'package:flutter_settings_ui/src/sections/platforms/android_settings_section.dart';
|
||||
import 'package:flutter_settings_ui/src/sections/platforms/ios_settings_section.dart';
|
||||
import 'package:repertory/widgets/settings/web_settings_section.dart';
|
||||
import 'package:flutter_settings_ui/src/tiles/abstract_settings_tile.dart';
|
||||
import 'package:flutter_settings_ui/src/utils/platform_utils.dart';
|
||||
import 'package:flutter_settings_ui/src/utils/settings_theme.dart';
|
||||
|
||||
class SettingsSection extends AbstractSettingsSection {
|
||||
const SettingsSection({
|
||||
required this.tiles,
|
||||
this.margin,
|
||||
this.title,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<AbstractSettingsTile> tiles;
|
||||
final EdgeInsetsDirectional? margin;
|
||||
final Widget? title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = SettingsTheme.of(context);
|
||||
|
||||
switch (theme.platform) {
|
||||
case DevicePlatform.android:
|
||||
case DevicePlatform.fuchsia:
|
||||
case DevicePlatform.linux:
|
||||
return AndroidSettingsSection(
|
||||
title: title,
|
||||
tiles: tiles,
|
||||
margin: margin,
|
||||
);
|
||||
case DevicePlatform.iOS:
|
||||
case DevicePlatform.macOS:
|
||||
case DevicePlatform.windows:
|
||||
return IOSSettingsSection(title: title, tiles: tiles, margin: margin);
|
||||
case DevicePlatform.web:
|
||||
return WebSettingsSection(title: title, tiles: tiles, margin: margin);
|
||||
case DevicePlatform.device:
|
||||
throw Exception(
|
||||
"You can't use the DevicePlatform.device in this context. "
|
||||
'Incorrect platform: SettingsSection.build',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
web/repertory/lib/widgets/settings/web_settings_section.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
|
||||
// Container(
|
||||
// height: 65 * scaleFactor,
|
||||
// padding: EdgeInsetsDirectional.only(
|
||||
// bottom: 5 * scaleFactor,
|
||||
// start: 6,
|
||||
// top: 40 * scaleFactor,
|
||||
// ),
|
||||
// child: title!,
|
||||
// ),
|
||||
|
||||
class WebSettingsSection extends StatelessWidget {
|
||||
const WebSettingsSection({
|
||||
required this.tiles,
|
||||
required this.margin,
|
||||
required this.title,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<AbstractSettingsTile> tiles;
|
||||
final EdgeInsetsDirectional? margin;
|
||||
final Widget? title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return buildSectionBody(context);
|
||||
}
|
||||
|
||||
Widget buildSectionBody(BuildContext context) {
|
||||
final theme = SettingsTheme.of(context);
|
||||
final scaleFactor = MediaQuery.textScalerOf(context).scale(1);
|
||||
|
||||
return Padding(
|
||||
padding: margin ?? EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null)
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
bottom: 5 * scaleFactor,
|
||||
start: 6,
|
||||
top: 20 * scaleFactor,
|
||||
),
|
||||
child: title!,
|
||||
),
|
||||
Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
elevation: 4,
|
||||
color: theme.themeData.settingsSectionBackground,
|
||||
child: buildTileList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTileList() {
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: tiles.length,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return tiles[index];
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(height: 0, thickness: 1);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
// ui_settings.dart
|
||||
|
||||
import 'dart:convert' show jsonEncode;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart'
|
||||
show DevicePlatform, SettingsList, SettingsTile;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart'
|
||||
show
|
||||
convertAllToString,
|
||||
createSettingsTheme,
|
||||
displayAuthError,
|
||||
getBaseUri,
|
||||
getChanged,
|
||||
@@ -15,7 +20,8 @@ import 'package:repertory/helpers.dart'
|
||||
trimNotEmptyValidator;
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/settings.dart';
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
import 'package:repertory/utils/safe_set_state_mixin.dart';
|
||||
import 'package:repertory/widgets/settings/settings_section.dart';
|
||||
|
||||
class UISettingsWidget extends StatefulWidget {
|
||||
final bool showAdvanced;
|
||||
@@ -32,9 +38,14 @@ class UISettingsWidget extends StatefulWidget {
|
||||
State<UISettingsWidget> createState() => _UISettingsWidgetState();
|
||||
}
|
||||
|
||||
class _UISettingsWidgetState extends State<UISettingsWidget> {
|
||||
class _UISettingsWidgetState extends State<UISettingsWidget>
|
||||
with SafeSetState<UISettingsWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final settingsTheme = createSettingsTheme(scheme);
|
||||
|
||||
List<SettingsTile> commonSettings = [];
|
||||
|
||||
widget.settings.forEach((key, value) {
|
||||
@@ -56,6 +67,7 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ApiPort':
|
||||
{
|
||||
createIntSetting(
|
||||
@@ -73,6 +85,7 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ApiUser':
|
||||
{
|
||||
createStringSetting(
|
||||
@@ -94,10 +107,21 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
|
||||
}
|
||||
});
|
||||
|
||||
final titleStyle = theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: scheme.onSurface,
|
||||
);
|
||||
|
||||
return SettingsList(
|
||||
shrinkWrap: false,
|
||||
platform: DevicePlatform.web,
|
||||
lightTheme: settingsTheme,
|
||||
darkTheme: settingsTheme,
|
||||
sections: [
|
||||
SettingsSection(title: const Text('Settings'), tiles: commonSettings),
|
||||
SettingsSection(
|
||||
title: Text('Settings', style: titleStyle),
|
||||
tiles: commonSettings,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -106,11 +130,10 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
|
||||
void dispose() {
|
||||
final settings = getChanged(widget.origSettings, widget.settings);
|
||||
if (settings.isNotEmpty) {
|
||||
final key =
|
||||
Provider.of<Auth>(
|
||||
constants.navigatorKey.currentContext!,
|
||||
listen: false,
|
||||
).key;
|
||||
final key = Provider.of<Auth>(
|
||||
constants.navigatorKey.currentContext!,
|
||||
listen: false,
|
||||
).key;
|
||||
convertAllToString(settings, key)
|
||||
.then((map) async {
|
||||
try {
|
||||
@@ -143,13 +166,4 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ dependencies:
|
||||
collection: ^1.19.1
|
||||
http: ^1.3.0
|
||||
provider: ^6.1.2
|
||||
settings_ui: ^2.0.2
|
||||
sodium_libs: ^3.4.4+1
|
||||
flutter_settings_ui: ^3.0.0
|
||||
sodium_libs: ^3.4.6+1
|
||||
convert: ^3.1.2
|
||||
|
||||
dev_dependencies:
|
||||
@@ -50,7 +50,7 @@ dev_dependencies:
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
@@ -64,9 +64,8 @@ flutter:
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
assets:
|
||||
- assets/images/repertory.png
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 368 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -31,14 +31,17 @@
|
||||
|
||||
<title>repertory</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<script type="text/javascript" src="sodium.js" async="true"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.flutterConfiguration = {
|
||||
canvasKitBaseUrl: "/canvaskit/"
|
||||
};
|
||||
{{flutter_js}}
|
||||
{{flutter_build_config}}
|
||||
|
||||
_flutter.loader.load({
|
||||
config: {'canvasKitBaseUrl': "$FLUTTER_BASE_HREFcanvaskit/"},
|
||||
});
|
||||
</script>
|
||||
<script src="flutter_bootstrap.js" async=""></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||