[ui] UI theme should match repertory blue #61
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good

This commit is contained in:
2025-08-17 09:25:34 -05:00
parent 5326b3b81f
commit b8dd642dc5
18 changed files with 1737 additions and 670 deletions

View File

@@ -1,3 +1,5 @@
auro
aurosweep
autofocus autofocus
autovalidatemode autovalidatemode
canvaskit canvaskit

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited. # This file should be version controlled and should not be manually edited.
version: version:
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1" revision: "d7b523b356d15fb81e7d340bbe52b47f93937323"
channel: "stable" channel: "stable"
project_type: app project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
- platform: web - platform: web
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
# User provided section # User provided section

View File

@@ -1,4 +1,6 @@
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'; import 'package:sodium_libs/sodium_libs.dart';
const addMountTitle = 'Add New Mount'; const addMountTitle = 'Add New Mount';
@@ -9,10 +11,14 @@ const logonWidth = 300.0;
const databaseTypeList = ['rocksdb', 'sqlite']; const databaseTypeList = ['rocksdb', 'sqlite'];
const downloadTypeList = ['default', 'direct', 'ring_buffer']; const downloadTypeList = ['default', 'direct', 'ring_buffer'];
const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace']; const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace'];
const padding = 15.0; const padding = 16.0;
const paddingSmall = 8.0;
const borderRadius = 16.0;
const borderRadiusSmall = 8.0;
const protocolTypeList = ['http', 'https']; const protocolTypeList = ['http', 'https'];
const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia']; const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia'];
const ringBufferSizeList = ['128', '256', '512', '1024', '2048']; const ringBufferSizeList = ['128', '256', '512', '1024', '2048'];
const gradientColors = [Color(0xFF0A0F1F), Color(0xFF1B1C1F)];
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

View File

@@ -1,3 +1,5 @@
// helpers.dart
import 'package:convert/convert.dart'; import 'package:convert/convert.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@@ -348,55 +350,95 @@ Future<String?> editMountLocation(
return await showDialog( return await showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return StatefulBuilder( var theme = Theme.of(context);
builder: (context, setState) { var scheme = theme.colorScheme;
return AlertDialog( return Theme(
actions: [ data: theme.copyWith(
TextButton( dialogTheme: DialogThemeData(
child: const Text('Cancel'), backgroundColor: scheme.surface.withValues(alpha: 0.40),
onPressed: () => Navigator.of(context).pop(null), surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(constants.borderRadius),
side: BorderSide(
color: scheme.outlineVariant.withValues(alpha: 0.08),
width: 1,
), ),
TextButton( ),
child: const Text('OK'), ),
onPressed: () { ),
final result = getSettingValidators('Path').firstWhereOrNull( child: StatefulBuilder(
(validator) => !validator(currentLocation ?? ''), builder: (context, setState) {
); return AlertDialog(
if (result != null) { actions: [
return displayErrorMessage( TextButton(
context, child: const Text('Cancel'),
"Mount location is not valid", onPressed: () => Navigator.of(context).pop(null),
); ),
} TextButton(
Navigator.of(context).pop(currentLocation); child: const Text('OK'),
}, onPressed: () {
), final result = getSettingValidators('Path')
], .firstWhereOrNull(
content: (validator) => !validator(currentLocation ?? ''),
available.isEmpty );
? TextField( if (result != null) {
return displayErrorMessage(
context,
"Mount location is not valid",
);
}
Navigator.of(context).pop(currentLocation);
},
),
],
content: available.isEmpty
? TextField(
autofocus: true, autofocus: true,
controller: controller, controller: controller,
onChanged: onChanged: (value) =>
(value) => setState(() => currentLocation = value), setState(() => currentLocation = value),
) )
: DropdownButton<String>( : DropdownButton<String>(
hint: const Text("Select drive"), hint: const Text("Select drive"),
value: currentLocation, value: currentLocation,
onChanged: onChanged: (value) =>
(value) => setState(() => currentLocation = value), setState(() => currentLocation = value),
items: items: available.map<DropdownMenuItem<String>>((item) {
available.map<DropdownMenuItem<String>>((item) { return DropdownMenuItem<String>(
return DropdownMenuItem<String>( value: item,
value: item, child: Text(item),
child: Text(item), );
); }).toList(),
}).toList(),
), ),
title: const Text('Mount Location', textAlign: TextAlign.center), title: const Text('Mount Location', textAlign: TextAlign.center),
); );
}, },
),
); );
}, },
); );
} }
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: scheme.surface.withValues(alpha: 0.40),
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(constants.borderRadius),
side: BorderSide(
color: scheme.outlineVariant.withValues(alpha: 0.08),
width: 1,
),
),
),
),
child: child,
);
},
);

View File

@@ -1,3 +1,5 @@
// main.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
@@ -123,7 +125,7 @@ class AuthCheck extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<Auth>( return Consumer<Auth>(
builder: (context, auth, __) { builder: (context, auth, _) {
if (!auth.authenticated) { if (!auth.authenticated) {
Future.delayed(Duration(milliseconds: 1), () { Future.delayed(Duration(milliseconds: 1), () {
if (constants.navigatorKey.currentContext == null) { if (constants.navigatorKey.currentContext == null) {

View File

@@ -1,3 +1,6 @@
// add_mount_screen.dart
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -7,6 +10,7 @@ import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount.dart';
import 'package:repertory/models/mount_list.dart'; import 'package:repertory/models/mount_list.dart';
import 'package:repertory/types/mount_config.dart'; import 'package:repertory/types/mount_config.dart';
import 'package:repertory/widgets/aurora_sweep.dart';
import 'package:repertory/widgets/mount_settings.dart'; import 'package:repertory/widgets/mount_settings.dart';
class AddMountScreen extends StatefulWidget { class AddMountScreen extends StatefulWidget {
@@ -30,48 +34,206 @@ class _AddMountScreenState extends State<AddMountScreen> {
"Sia": createDefaultSettings("Sia"), "Sia": createDefaultSettings("Sia"),
}; };
@override
void dispose() {
_mountNameController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( final scheme = Theme.of(context).colorScheme;
appBar: AppBar( final textTheme = Theme.of(context).textTheme;
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title), Widget glassTile({required Widget child, EdgeInsets? padding}) {
actions: [ return Container(
Consumer<Auth>( decoration: BoxDecoration(
builder: (context, auth, _) { color: scheme.surface.withValues(alpha: 0.40),
return IconButton( borderRadius: BorderRadius.circular(constants.borderRadius),
icon: const Icon(Icons.logout), border: Border.all(
onPressed: () => auth.logoff(), color: scheme.outlineVariant.withValues(alpha: 0.08),
); width: 1,
},
), ),
], gradient: const LinearGradient(
), begin: Alignment.topCenter,
body: Padding( end: Alignment.bottomCenter,
padding: const EdgeInsets.all(constants.padding), colors: [Color(0x07FFFFFF), Color(0x00000000)],
child: Consumer<Auth>( ),
builder: (context, auth, _) { boxShadow: [
return Column( BoxShadow(
crossAxisAlignment: CrossAxisAlignment.start, color: Colors.black.withValues(alpha: 0.22),
mainAxisAlignment: MainAxisAlignment.start, blurRadius: constants.borderRadius,
mainAxisSize: MainAxisSize.max, offset: Offset(0, constants.borderRadius),
children: [ ),
Card( ],
margin: EdgeInsets.all(0.0), ),
child: Padding( child: ClipRRect(
padding: const EdgeInsets.all(constants.padding), borderRadius: BorderRadius.circular(constants.borderRadius),
child: Padding(
padding: padding ?? const EdgeInsets.all(constants.padding),
child: child,
),
),
);
}
return Scaffold(
body: SafeArea(
child: Stack(
children: [
// Background
Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: constants.gradientColors,
),
),
),
const AuroraSweep(
enabled: true,
duration: Duration(seconds: 28),
primaryAlphaA: 0.04,
primaryAlphaB: 0.03,
),
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Container(color: Colors.black.withValues(alpha: 0.06)),
),
),
// Content
Padding(
padding: const EdgeInsets.all(constants.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header: Back • Title • Logout
Row(
children: [
// Back tile
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: 0.40),
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.08,
),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.22),
blurRadius: constants.borderRadius,
offset: Offset(0, constants.borderRadius),
),
],
),
child: const Icon(Icons.arrow_back),
),
),
),
const SizedBox(width: constants.padding),
Expanded(
child: Text(
widget.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: scheme.onSurface.withValues(alpha: 0.96),
),
),
),
const SizedBox(width: constants.padding),
// Logout capsule (glassy)
ClipRRect(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: constants.borderRadius,
sigmaY: constants.borderRadius,
),
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: scheme.surface.withValues(alpha: 0.40),
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.08,
),
width: 1,
),
),
child: Consumer<Auth>(
builder: (context, auth, _) {
return IconButton(
tooltip: 'Log out',
icon: const Icon(Icons.logout),
onPressed: () {
auth.logoff();
},
);
},
),
),
),
),
],
),
const SizedBox(height: constants.padding),
// Provider Type (glassy tile)
glassTile(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Text('Provider Type'), Text(
'Provider Type',
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: constants.padding), const SizedBox(width: constants.padding),
DropdownButton<String>( DropdownButton<String>(
autofocus: true,
value: _mountType, value: _mountType,
onChanged: (mountType) => autofocus: true,
_handleChange(auth, mountType ?? ''), underline: const SizedBox.shrink(),
onChanged: (mountType) {
_handleChange(
Provider.of<Auth>(context, listen: false),
mountType ?? '',
);
},
items: constants.providerTypeList items: constants.providerTypeList
.map<DropdownMenuItem<String>>((item) { .map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
@@ -84,21 +246,21 @@ class _AddMountScreenState extends State<AddMountScreen> {
], ],
), ),
), ),
),
if (_mountType.isNotEmpty && _mountType != 'Remote') if (_mountType.isNotEmpty && _mountType != 'Remote') ...[
const SizedBox(height: constants.padding), const SizedBox(height: constants.padding),
if (_mountType.isNotEmpty && _mountType != 'Remote') // Config Name (glassy tile)
Card( glassTile(
margin: EdgeInsets.all(0.0),
child: Padding(
padding: const EdgeInsets.all(constants.padding),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Text('Configuration Name'), Text(
const SizedBox(width: constants.padding), 'Configuration Name',
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
TextField( TextField(
autofocus: true, autofocus: true,
controller: _mountNameController, controller: _mountNameController,
@@ -106,99 +268,157 @@ class _AddMountScreenState extends State<AddMountScreen> {
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')), FilteringTextInputFormatter.deny(RegExp(r'\s')),
], ],
onChanged: (_) => _handleChange(auth, _mountType), onChanged: (_) => _handleChange(
Provider.of<Auth>(context, listen: false),
_mountType,
),
decoration: InputDecoration(
hintText: 'Enter a unique name',
filled: true,
fillColor: scheme.surface.withValues(alpha: 0.30),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
),
), ),
], ],
), ),
), ),
), ],
if (_mount != null) ...[
const SizedBox(height: constants.padding), if (_mount != null) ...[
Expanded( const SizedBox(height: constants.padding),
child: Card( // Settings (large glass container)
margin: EdgeInsets.all(0.0), Expanded(
child: Padding( child: glassTile(
padding: const EdgeInsets.all(constants.padding), padding: EdgeInsets.zero,
child: MountSettingsWidget( child: Padding(
isAdd: true, padding: const EdgeInsets.all(constants.padding),
mount: _mount!, child: MountSettingsWidget(
settings: _settings[_mountType]!, isAdd: true,
showAdvanced: false, mount: _mount!,
settings: _settings[_mountType]!,
showAdvanced: false,
),
), ),
), ),
), ),
), const SizedBox(height: constants.padding),
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);
List<String> failed = []; // Action buttons row
if (!validateSettings( Row(
_settings[_mountType]!, children: [
failed, // Test
)) { ElevatedButton.icon(
for (var key in failed) { label: const Text('Test'),
displayErrorMessage( icon: const Icon(Icons.check),
context, style: ElevatedButton.styleFrom(
"Setting '$key' is not valid", backgroundColor: scheme.primary.withValues(
); alpha: 0.18,
} ),
return; foregroundColor: scheme.primary,
} elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
side: BorderSide(
color: scheme.outlineVariant.withValues(
alpha: 0.15,
),
width: 1,
),
),
),
onPressed: _handleProviderTest,
),
const SizedBox(width: constants.padding),
if (mountList.hasConfigName( // Add
_mountNameController.text, ElevatedButton.icon(
)) { label: const Text('Add'),
return displayErrorMessage( icon: const Icon(Icons.add),
style: ElevatedButton.styleFrom(
backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary,
elevation: 8,
shadowColor: scheme.primary.withValues(alpha: 0.45),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
),
),
onPressed: () async {
final mountList = Provider.of<MountList>(
context, context,
"Configuration name '${_mountNameController.text}' already exists", listen: false,
); );
}
if (_mountType == "Sia" || _mountType == "S3") { List<String> failed = [];
final bucket = if (!validateSettings(
_settings[_mountType]!["${_mountType}Config"]["Bucket"] _settings[_mountType]!,
as String; failed,
if (mountList.hasBucketName(_mountType, bucket)) { )) {
for (var key in failed) {
displayErrorMessage(
context,
"Setting '$key' is not valid",
);
}
return;
}
if (mountList.hasConfigName(
_mountNameController.text,
)) {
return displayErrorMessage( return displayErrorMessage(
context, context,
"Bucket '$bucket' already exists", "Configuration name '${_mountNameController.text}' already exists",
); );
} }
}
final success = await mountList.add( if (_mountType == "Sia" || _mountType == "S3") {
_mountType, final bucket =
_mountType == 'Remote' _settings[_mountType]!["${_mountType}Config"]["Bucket"]
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}' as String;
: _mountNameController.text, if (mountList.hasBucketName(_mountType, bucket)) {
_settings[_mountType]!, return displayErrorMessage(
); context,
"Bucket '$bucket' already exists",
);
}
}
if (!success || !context.mounted) { final success = await mountList.add(
return; _mountType,
} _mountType == 'Remote'
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}'
: _mountNameController.text,
_settings[_mountType]!,
);
Navigator.pop(context); if (!success || !context.mounted) {
}, return;
), }
],
), Navigator.pop(context);
},
),
],
),
],
], ],
], ),
); ),
}, ],
), ),
), ),
); );
@@ -251,7 +471,6 @@ class _AddMountScreenState extends State<AddMountScreen> {
if (!mounted) { if (!mounted) {
return; return;
} }
super.setState(fn); super.setState(fn);
} }
} }

View File

@@ -1,7 +1,11 @@
// auth_screen.dart
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/models/auth.dart'; import 'package:repertory/models/auth.dart';
import 'package:repertory/widgets/aurora_sweep.dart';
class AuthScreen extends StatefulWidget { class AuthScreen extends StatefulWidget {
final String title; final String title;
@@ -27,30 +31,29 @@ class _AuthScreenState extends State<AuthScreen> {
} }
@override @override
Widget build(context) { Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
InputDecoration decoration(String label, IconData icon) => InputDecoration( InputDecoration decoration(String label, IconData icon) => InputDecoration(
labelText: label, labelText: label,
prefixIcon: Icon(icon), prefixIcon: Icon(icon),
filled: true, filled: true,
fillColor: scheme.surfaceContainerLow.withValues(alpha: 0.65), fillColor: scheme.primary.withValues(alpha: 0.15),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
borderSide: BorderSide(color: scheme.primary, width: 2), borderSide: BorderSide(color: scheme.primary, width: 2),
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), contentPadding: const EdgeInsets.all(constants.paddingSmall),
); );
Future<void> doLogin(Auth auth) async { Future<void> doLogin(Auth auth) async {
if (!_enabled) { if (!_enabled) {
return; return;
} }
if (!_formKey.currentState!.validate()) { if (!_formKey.currentState!.validate()) {
return; return;
} }
@@ -89,22 +92,29 @@ class _AuthScreenState extends State<AuthScreen> {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF0A0F1F), Color(0xFF1B1C1F)], colors: constants.gradientColors,
stops: [0.0, 1.0], stops: [0.0, 1.0],
), ),
), ),
), ),
const AuroraSweep(),
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(color: Colors.black.withValues(alpha: 0.10)),
),
),
Align( Align(
alignment: const Alignment(0, 0.1), alignment: const Alignment(0, 0.06),
child: IgnorePointer( child: IgnorePointer(
child: Container( child: Container(
width: 740, width: 720,
height: 740, height: 720,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
gradient: RadialGradient( gradient: RadialGradient(
colors: [ colors: [
scheme.primary.withValues(alpha: 0.22), scheme.primary.withValues(alpha: 0.20),
Colors.transparent, Colors.transparent,
], ],
stops: const [0.0, 1.0], stops: const [0.0, 1.0],
@@ -116,7 +126,7 @@ class _AuthScreenState extends State<AuthScreen> {
Consumer<Auth>( Consumer<Auth>(
builder: (context, auth, _) { builder: (context, auth, _) {
if (auth.authenticated) { if (auth.authenticated) {
Future.delayed(const Duration(milliseconds: 1), () { WidgetsBinding.instance.addPostFrameCallback((_) {
if (constants.navigatorKey.currentContext == null) { if (constants.navigatorKey.currentContext == null) {
return; return;
} }
@@ -124,7 +134,6 @@ class _AuthScreenState extends State<AuthScreen> {
constants.navigatorKey.currentContext!, constants.navigatorKey.currentContext!,
).pushNamedAndRemoveUntil('/', (r) => false); ).pushNamedAndRemoveUntil('/', (r) => false);
}); });
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -142,13 +151,17 @@ class _AuthScreenState extends State<AuthScreen> {
minWidth: 300, minWidth: 300,
), ),
child: Card( child: Card(
elevation: 12, elevation: constants.padding,
color: scheme.surface, color: scheme.primary.withValues(alpha: 0.10),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(
constants.borderRadius,
),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(constants.padding), padding: const EdgeInsets.all(
constants.padding * 2.0,
),
child: Form( child: Form(
key: _formKey, key: _formKey,
autovalidateMode: autovalidateMode:
@@ -160,15 +173,28 @@ class _AuthScreenState extends State<AuthScreen> {
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Container( child: Container(
width: 56, width: 64,
height: 56, height: 64,
decoration: BoxDecoration( decoration: BoxDecoration(
color: scheme.primary.withValues( color: scheme.primary.withValues(
alpha: 0.18, alpha: 0.11,
), ),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(
constants.borderRadiusSmall,
),
boxShadow: [
BoxShadow(
color: scheme.primary.withValues(
alpha: 0.08,
),
blurRadius: 16,
spreadRadius: 1,
),
],
),
padding: const EdgeInsets.all(
constants.borderRadiusSmall,
), ),
padding: const EdgeInsets.all(8),
child: Image.asset( child: Image.asset(
'assets/images/repertory.png', 'assets/images/repertory.png',
fit: BoxFit.contain, fit: BoxFit.contain,
@@ -182,7 +208,9 @@ class _AuthScreenState extends State<AuthScreen> {
), ),
), ),
), ),
const SizedBox(height: 14), const SizedBox(
height: constants.paddingSmall,
),
Text( Text(
constants.appLogonTitle, constants.appLogonTitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -191,7 +219,9 @@ class _AuthScreenState extends State<AuthScreen> {
.headlineSmall .headlineSmall
?.copyWith(fontWeight: FontWeight.w600), ?.copyWith(fontWeight: FontWeight.w600),
), ),
const SizedBox(height: 6), const SizedBox(
height: constants.paddingSmall,
),
Text( Text(
"Secure access to your mounts", "Secure access to your mounts",
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -200,12 +230,13 @@ class _AuthScreenState extends State<AuthScreen> {
.bodyMedium .bodyMedium
?.copyWith( ?.copyWith(
color: scheme.onSurface.withValues( color: scheme.onSurface.withValues(
alpha: 0.7, alpha: 0.72,
), ),
), ),
), ),
const SizedBox(height: 20), const SizedBox(
height: constants.padding * 2.0,
),
TextFormField( TextFormField(
autofocus: true, autofocus: true,
controller: _userController, controller: _userController,
@@ -261,10 +292,11 @@ class _AuthScreenState extends State<AuthScreen> {
doLogin(auth); doLogin(auth);
}, },
), ),
const SizedBox(height: constants.padding), const SizedBox(
height: constants.padding * 2.0,
),
SizedBox( SizedBox(
height: 44, height: 46,
child: ElevatedButton( child: ElevatedButton(
onPressed: _enabled onPressed: _enabled
? () { ? () {
@@ -272,9 +304,13 @@ class _AuthScreenState extends State<AuthScreen> {
} }
: null, : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: scheme.primary
.withValues(alpha: 0.45),
disabledBackgroundColor: scheme.primary
.withValues(alpha: 0.15),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
12, constants.borderRadiusSmall,
), ),
), ),
), ),

View File

@@ -1,9 +1,14 @@
// edit_mount_screen.dart
import 'dart:convert'; import 'dart:convert';
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/models/auth.dart'; import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount.dart';
import 'package:repertory/widgets/aurora_sweep.dart';
import 'package:repertory/widgets/mount_settings.dart'; import 'package:repertory/widgets/mount_settings.dart';
class EditMountScreen extends StatefulWidget { class EditMountScreen extends StatefulWidget {
@@ -20,41 +25,273 @@ class _EditMountScreenState extends State<EditMountScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Scaffold( return Scaffold(
appBar: AppBar( body: SafeArea(
backgroundColor: Theme.of(context).colorScheme.inversePrimary, child: Stack(
title: Text(widget.title), children: [
actions: [ // Background gradient
Row( Container(
children: [ width: double.infinity,
Row( height: double.infinity,
children: [ decoration: const BoxDecoration(
const Text("Advanced"), gradient: LinearGradient(
IconButton( begin: Alignment.topLeft,
icon: Icon( end: Alignment.bottomRight,
_showAdvanced ? Icons.toggle_on : Icons.toggle_off, colors: constants.gradientColors,
), ),
onPressed: ),
() => setState(() => _showAdvanced = !_showAdvanced), ),
// Subtle aurora sweep
const AuroraSweep(
enabled: true,
duration: Duration(seconds: 28),
primaryAlphaA: 0.04,
primaryAlphaB: 0.03,
),
// Light blur + tint for glassiness
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Container(color: Colors.black.withValues(alpha: 0.06)),
),
),
// Foreground content
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: constants.padding),
// Header row: Back • Title • Advanced • Logout
Padding(
padding: const EdgeInsets.symmetric(
horizontal: constants.padding,
), ),
], child: Row(
), children: [
Consumer<Auth>( // Back tile (glassy)
builder: (context, auth, _) { Material(
return IconButton( color: Colors.transparent,
icon: const Icon(Icons.logout), child: InkWell(
onPressed: () => auth.logoff(), borderRadius: BorderRadius.circular(
); constants.borderRadius,
}, ),
), onTap: () {
], Navigator.of(context).pop();
), },
], child: Ink(
), width: 40,
body: MountSettingsWidget( height: 40,
mount: widget.mount, decoration: BoxDecoration(
settings: jsonDecode(jsonEncode(widget.mount.mountConfig.settings)), color: scheme.surface.withValues(alpha: 0.40),
showAdvanced: _showAdvanced, borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.08,
),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.22),
blurRadius: constants.borderRadius,
offset: Offset(0, constants.borderRadius),
),
],
),
child: const Icon(Icons.arrow_back),
),
),
),
const SizedBox(width: constants.padding),
// Title
Expanded(
child: Text(
widget.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: scheme.onSurface.withValues(alpha: 0.96),
),
),
),
const SizedBox(width: constants.padding),
// Advanced toggle capsule (glassy)
ClipRRect(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: constants.borderRadius,
sigmaY: constants.borderRadius,
),
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: scheme.surface.withValues(alpha: 0.40),
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.08,
),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Advanced",
style: textTheme.labelLarge?.copyWith(
color: scheme.onSurface.withValues(
alpha: 0.90,
),
),
),
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.withValues(
alpha: 0.70,
),
onPressed: () {
setState(() {
_showAdvanced = !_showAdvanced;
});
},
),
],
),
),
),
),
const SizedBox(width: constants.padding),
// Logout capsule (glassy)
ClipRRect(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: constants.borderRadius,
sigmaY: constants.borderRadius,
),
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: scheme.surface.withValues(alpha: 0.40),
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.08,
),
width: 1,
),
),
child: Consumer<Auth>(
builder: (context, auth, _) {
return IconButton(
tooltip: 'Log out',
icon: const Icon(Icons.logout),
onPressed: () {
auth.logoff();
},
);
},
),
),
),
),
],
),
),
const SizedBox(height: constants.padding),
// Glass container hosting the settings list
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: constants.padding,
),
child: Material(
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: scheme.surface.withValues(alpha: 0.40),
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.06,
),
width: 1,
),
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0x07FFFFFF), Color(0x00000000)],
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.22),
blurRadius: constants.borderRadius,
offset: Offset(0, constants.borderRadius),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
child: MountSettingsWidget(
mount: widget.mount,
settings: jsonDecode(
jsonEncode(widget.mount.mountConfig.settings),
),
showAdvanced: _showAdvanced,
),
),
),
),
),
),
const SizedBox(height: constants.padding),
],
),
],
),
), ),
); );
} }
@@ -64,7 +301,6 @@ class _EditMountScreenState extends State<EditMountScreen> {
if (!mounted) { if (!mounted) {
return; return;
} }
super.setState(fn); super.setState(fn);
} }
} }

View File

@@ -1,10 +1,15 @@
// edit_settings_screen.dart
import 'dart:convert' show jsonDecode, jsonEncode; import 'dart:convert' show jsonDecode, jsonEncode;
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart'; import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart'; import 'package:repertory/models/auth.dart';
import 'package:repertory/widgets/aurora_sweep.dart';
import 'package:repertory/widgets/ui_settings.dart'; import 'package:repertory/widgets/ui_settings.dart';
class EditSettingsScreen extends StatefulWidget { class EditSettingsScreen extends StatefulWidget {
@@ -18,35 +23,216 @@ class EditSettingsScreen extends StatefulWidget {
class _EditSettingsScreenState extends State<EditSettingsScreen> { class _EditSettingsScreenState extends State<EditSettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( final scheme = Theme.of(context).colorScheme;
appBar: AppBar( final textTheme = Theme.of(context).textTheme;
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: FutureBuilder(
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
return UISettingsWidget( return Scaffold(
origSettings: jsonDecode(jsonEncode(snapshot.requireData)), body: SafeArea(
settings: snapshot.requireData, child: Stack(
showAdvanced: false, children: [
); // Background
}, Container(
future: _grabSettings(), width: double.infinity,
initialData: <String, dynamic>{}, height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: constants.gradientColors,
),
),
),
const AuroraSweep(
enabled: true,
duration: Duration(seconds: 28),
primaryAlphaA: 0.04,
primaryAlphaB: 0.03,
),
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Container(color: Colors.black.withValues(alpha: 0.06)),
),
),
// Content
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: constants.padding),
// Header row: Back + Title + Logout capsule
Padding(
padding: const EdgeInsets.symmetric(
horizontal: constants.padding,
),
child: Row(
children: [
// Back button styled like our glass tiles
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: 0.40),
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.08,
),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.22),
blurRadius: constants.borderRadius,
offset: Offset(0, constants.borderRadius),
),
],
),
child: const Icon(Icons.arrow_back),
),
),
),
const SizedBox(width: constants.padding),
// Title
Expanded(
child: Text(
widget.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: scheme.onSurface.withValues(alpha: 0.96),
),
),
),
const SizedBox(width: constants.padding),
// Logout capsule (glassy)
ClipRRect(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: constants.borderRadius,
sigmaY: constants.borderRadius,
),
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: scheme.surface.withValues(alpha: 0.40),
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.08,
),
width: 1,
),
),
child: Consumer<Auth>(
builder: (context, auth, _) {
return IconButton(
tooltip: 'Log out',
icon: const Icon(Icons.logout),
onPressed: () {
auth.logoff();
},
);
},
),
),
),
),
],
),
),
const SizedBox(height: constants.padding),
// Glass container holding the settings list
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: constants.padding,
),
child: Material(
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: scheme.surface.withValues(alpha: 0.40),
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.06,
),
width: 1,
),
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0x07FFFFFF), Color(0x00000000)],
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.22),
blurRadius: constants.borderRadius,
offset: Offset(0, constants.borderRadius),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
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,
);
},
),
),
),
),
),
),
const SizedBox(height: constants.padding),
],
),
],
),
), ),
); );
} }
@@ -81,7 +267,6 @@ class _EditSettingsScreenState extends State<EditSettingsScreen> {
if (!mounted) { if (!mounted) {
return; return;
} }
super.setState(fn); super.setState(fn);
} }
} }

View File

@@ -1,9 +1,12 @@
// home_screen.dart
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/models/auth.dart'; import 'package:repertory/models/auth.dart';
import 'package:repertory/models/settings.dart';
import 'package:repertory/widgets/mount_list_widget.dart'; import 'package:repertory/widgets/mount_list_widget.dart';
import 'package:repertory/widgets/aurora_sweep.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
final String title; final String title;
@@ -16,55 +19,189 @@ class HomeScreen extends StatefulWidget {
class _HomeScreeState extends State<HomeScreen> { class _HomeScreeState extends State<HomeScreen> {
@override @override
Widget build(context) { Widget build(context) {
final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Scaffold( return Scaffold(
appBar: AppBar( body: SafeArea(
backgroundColor: Theme.of(context).colorScheme.inversePrimary, child: Stack(
leading: IconButton( children: [
onPressed: () => Navigator.pushNamed(context, '/settings'), Container(
icon: const Icon(Icons.storage), width: double.infinity,
), height: double.infinity,
title: Text(widget.title), decoration: const BoxDecoration(
actions: [ gradient: LinearGradient(
const Text("Auto-start"), begin: Alignment.topLeft,
Consumer<Settings>( end: Alignment.bottomRight,
builder: (context, settings, _) { colors: constants.gradientColors,
return IconButton(
icon: Icon(
settings.autoStart ? Icons.toggle_on : Icons.toggle_off,
), ),
onPressed: () => settings.setAutoStart(!settings.autoStart), ),
); ),
}, const AuroraSweep(
), enabled: true,
const SizedBox(width: constants.padding), duration: Duration(seconds: 28),
Consumer<Auth>( primaryAlphaA: 0.04,
builder: (context, auth, _) { primaryAlphaB: 0.03,
return IconButton( ),
icon: const Icon(Icons.logout), Positioned.fill(
onPressed: () => auth.logoff(), child: BackdropFilter(
); filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
}, child: Container(color: Colors.black.withValues(alpha: 0.06)),
), ),
], ),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: constants.padding),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: constants.padding,
),
child: Row(
children: [
SizedBox(
width: 48,
height: 48,
child: Image.asset(
'assets/images/repertory.png',
fit: BoxFit.contain,
errorBuilder: (_, _, _) {
return Icon(
Icons.folder,
color: scheme.primary,
size: 40,
);
},
),
),
const SizedBox(width: constants.padding),
Expanded(
child: Text(
widget.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: scheme.onSurface.withValues(alpha: 0.96),
),
),
),
const SizedBox(width: constants.padding),
ClipRRect(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: constants.borderRadius,
sigmaY: constants.borderRadius,
),
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: scheme.surface.withValues(alpha: 0.40),
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.08,
),
width: 1,
),
),
child: Row(
children: [
IconButton(
tooltip: 'Settings',
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.pushNamed(context, '/settings');
},
),
Consumer<Auth>(
builder: (context, auth, _) {
return IconButton(
tooltip: 'Log out',
icon: const Icon(Icons.logout),
onPressed: () {
auth.logoff();
},
);
},
),
],
),
),
),
),
],
),
),
const SizedBox(height: constants.padding),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: constants.padding,
),
child: const MountListWidget(),
),
),
],
),
],
),
), ),
body: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.all(constants.padding), padding: const EdgeInsets.all(constants.padding),
child: MountListWidget(), child: Hero(
), tag: 'add_mount_fab',
floatingActionButton: FloatingActionButton( child: Material(
onPressed: () => Navigator.pushNamed(context, '/add'), color: Colors.transparent,
tooltip: 'Add Mount', elevation: 12, // match card depth
child: const Icon(Icons.add), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(constants.borderRadius),
),
child: Ink(
decoration: BoxDecoration(
// glassy base like MountWidget Card (but a touch bluer)
color: scheme.primary.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(constants.borderRadius),
border: Border.all(
color: scheme.outlineVariant.withValues(alpha: 0.15),
width: 1,
),
// subtle top highlight for crisp glass
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0x07FFFFFF), Color(0x00000000)],
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.22),
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: 28)),
),
),
),
),
),
), ),
); );
} }
@override
void setState(VoidCallback fn) {
if (!mounted) {
return;
}
super.setState(fn);
}
} }

View File

@@ -2,7 +2,8 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart' show Validator, displayErrorMessage; import 'package:repertory/helpers.dart'
show Validator, displayErrorMessage, doShowDialog;
import 'package:settings_ui/settings_ui.dart'; import 'package:settings_ui/settings_ui.dart';
void createBooleanSetting( void createBooleanSetting(
@@ -57,16 +58,14 @@ void createIntListSetting(
value: value.toString(), value: value.toString(),
onChanged: (newValue) { onChanged: (newValue) {
setState( setState(
() => () => settings[key] = int.parse(
settings[key] = int.parse( newValue ?? defaultValue.toString(),
newValue ?? defaultValue.toString(), ),
),
); );
}, },
items: items: valueList.map<DropdownMenuItem<String>>((item) {
valueList.map<DropdownMenuItem<String>>((item) { return DropdownMenuItem<String>(value: item, child: Text(item));
return DropdownMenuItem<String>(value: item, child: Text(item)); }).toList(),
}).toList(),
), ),
), ),
); );
@@ -95,42 +94,40 @@ void createIntSetting(
value: Text(value.toString()), value: Text(value.toString()),
onPressed: (_) { onPressed: (_) {
String updatedValue = value.toString(); String updatedValue = value.toString();
showDialog( doShowDialog(
context: context, context,
builder: (context) { AlertDialog(
return AlertDialog( actions: [
actions: [ TextButton(
TextButton( child: const Text('Cancel'),
child: const Text('Cancel'), onPressed: () => Navigator.of(context).pop(),
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,
), ),
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),
),
); );
}, },
), ),
@@ -163,107 +160,103 @@ void createPasswordSetting(
String updatedValue2 = value; String updatedValue2 = value;
bool hidePassword1 = true; bool hidePassword1 = true;
bool hidePassword2 = true; bool hidePassword2 = true;
showDialog( doShowDialog(
context: context, context,
builder: (context) { StatefulBuilder(
return StatefulBuilder( builder: (context, setDialogState) {
builder: (context, setDialogState) { return AlertDialog(
return AlertDialog( actions: [
actions: [ TextButton(
TextButton( child: const Text('Cancel'),
child: const Text('Cancel'), onPressed: () => Navigator.of(context).pop(),
onPressed: () => Navigator.of(context).pop(), ),
), TextButton(
TextButton( child: const Text('OK'),
child: const Text('OK'), onPressed: () {
onPressed: () { if (updatedValue1 != updatedValue2) {
if (updatedValue1 != updatedValue2) { return displayErrorMessage(
return displayErrorMessage( context,
context, "Setting '$key' does not match",
"Setting '$key' does not match",
);
}
final result = validators.firstWhereOrNull(
(validator) => !validator(updatedValue1),
); );
if (result != null) { }
return displayErrorMessage(
context,
"Setting '$key' is not valid",
);
}
setState(() => settings[key] = updatedValue1); final result = validators.firstWhereOrNull(
Navigator.of(context).pop(); (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, title: createSettingTitle(context, key, description),
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),
);
},
);
},
); );
}, },
), ),
@@ -313,10 +306,9 @@ void createStringListSetting(
value: DropdownButton<String>( value: DropdownButton<String>(
value: value, value: value,
onChanged: (newValue) => setState(() => settings[key] = newValue), onChanged: (newValue) => setState(() => settings[key] = newValue),
items: items: valueList.map<DropdownMenuItem<String>>((item) {
valueList.map<DropdownMenuItem<String>>((item) { return DropdownMenuItem<String>(value: item, child: Text(item));
return DropdownMenuItem<String>(value: item, child: Text(item)); }).toList(),
}).toList(),
), ),
), ),
); );
@@ -345,43 +337,41 @@ void createStringSetting(
value: Text(value), value: Text(value),
onPressed: (_) { onPressed: (_) {
String updatedValue = value; String updatedValue = value;
showDialog( doShowDialog(
context: context, context,
builder: (context) { AlertDialog(
return AlertDialog( actions: [
actions: [ TextButton(
TextButton( child: const Text('Cancel'),
child: const Text('Cancel'), onPressed: () => Navigator.of(context).pop(),
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,
), ),
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

@@ -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.04, // default dimmer for crispness
this.primaryAlphaB = 0.03,
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

@@ -1,3 +1,5 @@
// mount_settings.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
@@ -35,6 +37,20 @@ class MountSettingsWidget extends StatefulWidget {
class _MountSettingsWidgetState extends State<MountSettingsWidget> { class _MountSettingsWidgetState extends State<MountSettingsWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
// Theme SettingsList to your glass aesthetic (consistent with UISettingsWidget)
final theme = SettingsThemeData(
settingsListBackground: Colors.transparent,
settingsSectionBackground: scheme.surface.withValues(alpha: 0.30),
titleTextColor: scheme.onSurface.withValues(alpha: 0.96),
trailingTextColor: scheme.onSurface.withValues(alpha: 0.80),
tileDescriptionTextColor: scheme.onSurface.withValues(alpha: 0.68),
leadingIconsColor: scheme.onSurface.withValues(alpha: 0.90),
dividerColor: scheme.outlineVariant.withValues(alpha: 0.10),
tileHighlightColor: scheme.primary.withValues(alpha: 0.08),
);
List<SettingsTile> commonSettings = []; List<SettingsTile> commonSettings = [];
List<SettingsTile> encryptConfigSettings = []; List<SettingsTile> encryptConfigSettings = [];
List<SettingsTile> hostConfigSettings = []; List<SettingsTile> hostConfigSettings = [];
@@ -363,11 +379,8 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
description: getSettingDescription('$key.$subKey'), description: getSettingDescription('$key.$subKey'),
validators: [ validators: [
...getSettingValidators('$key.$subKey'), ...getSettingValidators('$key.$subKey'),
(value) => (value) => !Provider.of<MountList>(context, listen: false)
!Provider.of<MountList>( .hasBucketName(
context,
listen: false,
).hasBucketName(
widget.mount.type, widget.mount.type,
value, value,
excludeName: widget.mount.name, excludeName: widget.mount.name,
@@ -383,6 +396,9 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
return SettingsList( return SettingsList(
shrinkWrap: false, shrinkWrap: false,
platform: DevicePlatform.device,
lightTheme: theme,
darkTheme: theme,
sections: [ sections: [
if (encryptConfigSettings.isNotEmpty) if (encryptConfigSettings.isNotEmpty)
SettingsSection( SettingsSection(
@@ -412,10 +428,9 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
if (remoteMountSettings.isNotEmpty) if (remoteMountSettings.isNotEmpty)
SettingsSection( SettingsSection(
title: const Text('Remote Mount'), title: const Text('Remote Mount'),
tiles: tiles: (widget.settings['RemoteMount']['Enable'] as bool)
widget.settings['RemoteMount']['Enable'] as bool ? remoteMountSettings
? remoteMountSettings : [remoteMountSettings[0]],
: [remoteMountSettings[0]],
), ),
if (commonSettings.isNotEmpty) if (commonSettings.isNotEmpty)
SettingsSection(title: const Text('Settings'), tiles: commonSettings), SettingsSection(title: const Text('Settings'), tiles: commonSettings),
@@ -617,38 +632,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) { void _parseS3Config(List<SettingsTile> s3ConfigSettings, String key, value) {
value.forEach((subKey, subValue) { value.forEach((subKey, subValue) {
switch (subKey) { switch (subKey) {
@@ -686,11 +669,8 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
description: getSettingDescription('$key.$subKey'), description: getSettingDescription('$key.$subKey'),
validators: [ validators: [
...getSettingValidators('$key.$subKey'), ...getSettingValidators('$key.$subKey'),
(value) => (value) => !Provider.of<MountList>(context, listen: false)
!Provider.of<MountList>( .hasBucketName(
context,
listen: false,
).hasBucketName(
widget.mount.type, widget.mount.type,
value, value,
excludeName: widget.mount.name, excludeName: widget.mount.name,
@@ -1016,16 +996,45 @@ 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 @override
void setState(VoidCallback fn) { void setState(VoidCallback fn) {
if (!mounted) { if (!mounted) {
return; return;
} }
if (widget.onChanged != null) { if (widget.onChanged != null) {
widget.onChanged!(); widget.onChanged!();
} }
super.setState(fn); super.setState(fn);
} }
} }

View File

@@ -1,8 +1,7 @@
import 'dart:async'; // mount_widget.dart
import 'package:collection/collection.dart'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart'; import 'package:repertory/helpers.dart';
@@ -22,98 +21,165 @@ class _MountWidgetState extends State<MountWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Card( return Card(
margin: const EdgeInsets.all(0.0), margin: const EdgeInsets.all(0.0),
child: Consumer<Mount>( elevation: 12,
builder: (context, Mount mount, _) { color: scheme.primary.withValues(alpha: 0.15),
final textColor = Theme.of(context).colorScheme.onSurface; shape: RoundedRectangleBorder(
final subTextColor = Theme.of(context).brightness == Brightness.dark borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
? Colors.white38 side: BorderSide(
: Colors.black87; color: scheme.outlineVariant.withValues(alpha: 0.06),
width: 1,
),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0x07FFFFFF), Color(0x00000000)],
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.22),
blurRadius: constants.borderRadiusSmall,
offset: const Offset(0, constants.borderRadiusSmall),
),
],
),
child: Consumer<Mount>(
builder: (context, Mount mount, _) {
final titleStyle = textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: 0.15,
color: scheme.onSurface.withValues(alpha: 0.96),
);
final subStyle = textTheme.bodyMedium?.copyWith(
color: scheme.onSurface.withValues(alpha: 0.78),
);
final pathStyle = textTheme.bodySmall?.copyWith(
color: scheme.onSurface.withValues(alpha: 0.68),
);
final nameText = SelectableText( final nameText = SelectableText(
formatMountName(mount.type, mount.name), formatMountName(mount.type, mount.name),
style: TextStyle(color: subTextColor), style: subStyle,
); );
return ListTile( return ListTile(
isThreeLine: true, isThreeLine: true,
leading: IconButton( contentPadding: const EdgeInsets.all(constants.paddingSmall),
icon: Icon(Icons.settings, color: textColor), leading: Container(
onPressed: () => width: 46,
Navigator.pushNamed(context, '/edit', arguments: mount), height: 46,
), decoration: BoxDecoration(
subtitle: Column( color: scheme.primary.withValues(alpha: 0.12),
crossAxisAlignment: CrossAxisAlignment.start, borderRadius: BorderRadius.circular(
children: [ constants.borderRadiusSmall,
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(
context,
available,
location: mount.path,
);
if (location != null) {
await mount.setMountLocation(location);
}
}
setState(() => _editEnabled = true);
},
), ),
IconButton( border: Border.all(
color: scheme.outlineVariant.withValues(alpha: 0.08),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.24),
blurRadius: constants.borderRadiusSmall,
offset: const Offset(0, 5),
),
],
),
child: IconButton(
tooltip: 'Edit settings',
icon: Icon( icon: Icon(
mount.mounted == null Icons.settings,
? Icons.hourglass_top color: scheme.onSurface.withValues(alpha: 0.92),
: mount.mounted! size: 22,
? Icons.toggle_on
: Icons.toggle_off,
color: mount.mounted ?? false
? Theme.of(context).colorScheme.primary
: subTextColor,
), ),
tooltip: mount.mounted == null onPressed: () {
? '' Navigator.pushNamed(context, '/edit', arguments: mount);
: mount.mounted! },
? 'Unmount'
: 'Mount',
onPressed: _createMountHandler(context, mount),
), ),
], ),
), title: SelectableText(mount.provider, style: titleStyle),
); subtitle: Column(
}, crossAxisAlignment: CrossAxisAlignment.start,
children: [
nameText,
const SizedBox(height: 3),
SelectableText(
mount.path.isEmpty && mount.mounted == null
? 'loading...'
: mount.path.isEmpty
? '<mount location not set>'
: mount.path,
style: pathStyle,
),
],
),
trailing: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
if (mount.mounted != null && !mount.mounted!)
IconButton(
icon: const Icon(Icons.edit),
color: scheme.onSurface.withValues(alpha: 0.70),
tooltip: 'Edit mount location',
onPressed: () async {
setState(() {
_editEnabled = false;
});
final available = await mount.getAvailableLocations();
if (context.mounted) {
final location = await editMountLocation(
context,
available,
location: mount.path,
);
if (location != null) {
await mount.setMountLocation(location);
}
}
setState(() {
_editEnabled = true;
});
},
),
IconButton(
iconSize: 32,
splashRadius: 26,
icon: Icon(
mount.mounted == null
? Icons.hourglass_top
: mount.mounted!
? Icons.toggle_on
: Icons.toggle_off,
),
color: mount.mounted ?? false
? scheme.primary
: scheme.outline.withValues(alpha: 0.70),
tooltip: mount.mounted == null
? ''
: mount.mounted!
? 'Unmount'
: 'Mount',
onPressed: _createMountHandler(context, mount),
),
],
),
);
},
),
), ),
); );
} }
VoidCallback? _createMountHandler(context, Mount mount) { VoidCallback? _createMountHandler(BuildContext context, Mount mount) {
return _enabled && mount.mounted != null return _enabled && mount.mounted != null
? () async { ? () async {
if (mount.mounted == null) { if (mount.mounted == null) {
@@ -128,15 +194,16 @@ class _MountWidgetState extends State<MountWidget> {
final location = await _getMountLocation(context, mount); final location = await _getMountLocation(context, mount);
cleanup() { void cleanup() {
setState(() { setState(() {
_enabled = true; _enabled = true;
}); });
} }
if (!mounted && location == null) { if (!context.mounted && location == null) {
displayErrorMessage(context, "Mount location is not set"); displayErrorMessage(context, "Mount location is not set");
return cleanup(); cleanup();
return;
} }
final success = await mount.mount(mounted, location: location); final success = await mount.mount(mounted, location: location);
@@ -144,7 +211,8 @@ class _MountWidgetState extends State<MountWidget> {
mounted || mounted ||
constants.navigatorKey.currentContext == null || constants.navigatorKey.currentContext == null ||
!constants.navigatorKey.currentContext!.mounted) { !constants.navigatorKey.currentContext!.mounted) {
return cleanup(); cleanup();
return;
} }
displayErrorMessage( displayErrorMessage(
@@ -163,7 +231,7 @@ class _MountWidgetState extends State<MountWidget> {
super.dispose(); super.dispose();
} }
Future<String?> _getMountLocation(context, Mount mount) async { Future<String?> _getMountLocation(BuildContext context, Mount mount) async {
if (mount.mounted ?? false) { if (mount.mounted ?? false) {
return null; return null;
} }
@@ -197,7 +265,6 @@ class _MountWidgetState extends State<MountWidget> {
if (!mounted) { if (!mounted) {
return; return;
} }
super.setState(fn); super.setState(fn);
} }
} }

View File

@@ -1,3 +1,5 @@
// ui_settings.dart
import 'dart:convert' show jsonEncode; import 'dart:convert' show jsonEncode;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -35,6 +37,22 @@ class UISettingsWidget extends StatefulWidget {
class _UISettingsWidgetState extends State<UISettingsWidget> { class _UISettingsWidgetState extends State<UISettingsWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
// Theme the SettingsList to align with your glass look
final theme = SettingsThemeData(
settingsListBackground: Colors.transparent,
tileDescriptionTextColor: scheme.onSurface.withValues(alpha: 0.68),
settingsSectionBackground: scheme.surface.withValues(
alpha: 0.30,
), // slight glass base
tileHighlightColor: scheme.primary.withValues(alpha: 0.08),
titleTextColor: scheme.onSurface.withValues(alpha: 0.96),
trailingTextColor: scheme.onSurface.withValues(alpha: 0.80),
leadingIconsColor: scheme.onSurface.withValues(alpha: 0.90),
dividerColor: scheme.outlineVariant.withValues(alpha: 0.10),
);
List<SettingsTile> commonSettings = []; List<SettingsTile> commonSettings = [];
widget.settings.forEach((key, value) { widget.settings.forEach((key, value) {
@@ -56,6 +74,7 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
); );
} }
break; break;
case 'ApiPort': case 'ApiPort':
{ {
createIntSetting( createIntSetting(
@@ -73,6 +92,7 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
); );
} }
break; break;
case 'ApiUser': case 'ApiUser':
{ {
createStringSetting( createStringSetting(
@@ -96,6 +116,9 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
return SettingsList( return SettingsList(
shrinkWrap: false, shrinkWrap: false,
platform: DevicePlatform.device,
lightTheme: theme,
darkTheme: theme,
sections: [ sections: [
SettingsSection(title: const Text('Settings'), tiles: commonSettings), SettingsSection(title: const Text('Settings'), tiles: commonSettings),
], ],
@@ -106,11 +129,10 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
void dispose() { void dispose() {
final settings = getChanged(widget.origSettings, widget.settings); final settings = getChanged(widget.origSettings, widget.settings);
if (settings.isNotEmpty) { if (settings.isNotEmpty) {
final key = final key = Provider.of<Auth>(
Provider.of<Auth>( constants.navigatorKey.currentContext!,
constants.navigatorKey.currentContext!, listen: false,
listen: false, ).key;
).key;
convertAllToString(settings, key) convertAllToString(settings, key)
.then((map) async { .then((map) async {
try { try {
@@ -149,7 +171,6 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
if (!mounted) { if (!mounted) {
return; return;
} }
super.setState(fn); super.setState(fn);
} }
} }

View File

@@ -38,7 +38,7 @@ dependencies:
http: ^1.3.0 http: ^1.3.0
provider: ^6.1.2 provider: ^6.1.2
settings_ui: ^2.0.2 settings_ui: ^2.0.2
sodium_libs: ^3.4.4+1 sodium_libs: ^3.4.6+1
convert: ^3.1.2 convert: ^3.1.2
dev_dependencies: dev_dependencies:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

After

Width:  |  Height:  |  Size: 368 B

View File

@@ -1,44 +1,37 @@
<!DOCTYPE html> <!DOCTYPE html><html><head>
<html> <!--
<head> If you are serving your web app in a path other than the root, change the
<!-- href value below to reflect the base path you are serving from.
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for The path provided below has to start and end with a slash "/" in order for
it to work correctly. it to work correctly.
For more details: For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`. the `--base-href` argument provided to `flutter build`.
--> -->
<base href="$FLUTTER_BASE_HREF"> <base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project."> <meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons --> <!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="repertory"> <meta name="apple-mobile-web-app-title" content="repertory">
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"> <link rel="icon" type="image/png" href="favicon.png">
<title>repertory</title> <title>repertory</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script type="text/javascript" src="sodium.js" async="true"></script> <script type="text/javascript" src="sodium.js" async="true"></script></head>
</head> <body>
<body> <script src="flutter_bootstrap.js" async=""></script>
<script>
window.flutterConfiguration = {
canvasKitBaseUrl: "/canvaskit/" </body></html>
};
</script>
<script src="flutter_bootstrap.js" async=""></script>
</body>
</html>