v2.1.0-rc (#63)

Reviewed-on: #63
This commit is contained in:
2025-10-16 17:23:36 -05:00
parent 5ab7301cbe
commit f198cd49ee
471 changed files with 24173 additions and 9459 deletions

View File

@@ -1,8 +1,13 @@
auro
aurosweep
autofocus
autovalidatemode
canvaskit
cupertino
cupertinoicons
entrypoint
fromargb
onetwothree
renterd
rocksdb
rocksdb
vsync

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

View File

@@ -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>();

View File

@@ -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();
}

View File

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

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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();
}
}

View 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();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}

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

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

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

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

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

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

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

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

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

View File

@@ -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) {

View File

@@ -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();
}
}

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

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

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

View File

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

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -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>