[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,7 +350,23 @@ Future<String?> editMountLocation(
return await showDialog( return await showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return StatefulBuilder( var theme = Theme.of(context);
var 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: StatefulBuilder(
builder: (context, setState) { builder: (context, setState) {
return AlertDialog( return AlertDialog(
actions: [ actions: [
@@ -359,7 +377,8 @@ Future<String?> editMountLocation(
TextButton( TextButton(
child: const Text('OK'), child: const Text('OK'),
onPressed: () { onPressed: () {
final result = getSettingValidators('Path').firstWhereOrNull( final result = getSettingValidators('Path')
.firstWhereOrNull(
(validator) => !validator(currentLocation ?? ''), (validator) => !validator(currentLocation ?? ''),
); );
if (result != null) { if (result != null) {
@@ -372,21 +391,19 @@ Future<String?> editMountLocation(
}, },
), ),
], ],
content: content: available.isEmpty
available.isEmpty
? TextField( ? 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),
@@ -396,7 +413,32 @@ Future<String?> editMountLocation(
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,
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),
), ),
], ],
), ),
body: Padding( child: ClipRRect(
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), 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>( child: Consumer<Auth>(
builder: (context, auth, _) { builder: (context, auth, _) {
return Column( return IconButton(
crossAxisAlignment: CrossAxisAlignment.start, tooltip: 'Log out',
mainAxisAlignment: MainAxisAlignment.start, icon: const Icon(Icons.logout),
mainAxisSize: MainAxisSize.max, onPressed: () {
children: [ auth.logoff();
Card( },
margin: EdgeInsets.all(0.0), );
child: Padding( },
padding: const EdgeInsets.all(constants.padding), ),
),
),
),
],
),
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,17 +268,37 @@ 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) ...[ if (_mount != null) ...[
const SizedBox(height: constants.padding), const SizedBox(height: constants.padding),
// Settings (large glass container)
Expanded( Expanded(
child: Card( child: glassTile(
margin: EdgeInsets.all(0.0), padding: EdgeInsets.zero,
child: Padding( child: Padding(
padding: const EdgeInsets.all(constants.padding), padding: const EdgeInsets.all(constants.padding),
child: MountSettingsWidget( child: MountSettingsWidget(
@@ -129,19 +311,56 @@ class _AddMountScreenState extends State<AddMountScreen> {
), ),
), ),
const SizedBox(height: constants.padding), const SizedBox(height: constants.padding),
// Action buttons row
Row( Row(
children: [ children: [
// Test
ElevatedButton.icon( ElevatedButton.icon(
label: const Text('Test'), label: const Text('Test'),
icon: const Icon(Icons.check), icon: const Icon(Icons.check),
style: ElevatedButton.styleFrom(
backgroundColor: scheme.primary.withValues(
alpha: 0.18,
),
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, onPressed: _handleProviderTest,
), ),
const SizedBox(width: constants.padding), const SizedBox(width: constants.padding),
// Add
ElevatedButton.icon( ElevatedButton.icon(
label: const Text('Add'), label: const Text('Add'),
icon: const Icon(Icons.add), 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 { onPressed: () async {
final mountList = Provider.of<MountList>(context); final mountList = Provider.of<MountList>(
context,
listen: false,
);
List<String> failed = []; List<String> failed = [];
if (!validateSettings( if (!validateSettings(
@@ -197,8 +416,9 @@ class _AddMountScreenState extends State<AddMountScreen> {
), ),
], ],
], ],
); ),
}, ),
],
), ),
), ),
); );
@@ -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,42 +25,274 @@ 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),
actions: [
Row(
children: [ children: [
Row( // Background gradient
children: [ Container(
const Text("Advanced"), width: double.infinity,
IconButton( height: double.infinity,
icon: Icon( decoration: const BoxDecoration(
_showAdvanced ? Icons.toggle_on : Icons.toggle_off, gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
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: [
// Back tile (glassy)
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),
), ),
], ],
), ),
Consumer<Auth>( child: const Icon(Icons.arrow_back),
builder: (context, auth, _) { ),
return IconButton( ),
icon: const Icon(Icons.logout), ),
onPressed: () => auth.logoff(),
); 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();
},
);
},
),
),
),
),
], ],
), ),
body: MountSettingsWidget( ),
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, mount: widget.mount,
settings: jsonDecode(jsonEncode(widget.mount.mountConfig.settings)), settings: jsonDecode(
jsonEncode(widget.mount.mountConfig.settings),
),
showAdvanced: _showAdvanced, 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) {
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
Consumer<Auth>( Container(
builder: (context, auth, _) { width: double.infinity,
return IconButton( height: double.infinity,
icon: const Icon(Icons.logout), decoration: const BoxDecoration(
onPressed: () => auth.logoff(), 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),
), ),
], ],
), ),
body: FutureBuilder( 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) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator()); return const Center(
child: CircularProgressIndicator(),
);
} }
return UISettingsWidget( return UISettingsWidget(
origSettings: jsonDecode(jsonEncode(snapshot.requireData)), origSettings: jsonDecode(
jsonEncode(snapshot.requireData),
),
settings: snapshot.requireData, settings: snapshot.requireData,
showAdvanced: false, showAdvanced: false,
); );
}, },
future: _grabSettings(), ),
initialData: <String, dynamic>{}, ),
),
),
),
),
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,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: constants.gradientColors,
), ),
title: Text(widget.title),
actions: [
const Text("Auto-start"),
Consumer<Settings>(
builder: (context, settings, _) {
return IconButton(
icon: Icon(
settings.autoStart ? Icons.toggle_on : Icons.toggle_off,
), ),
onPressed: () => settings.setAutoStart(!settings.autoStart), ),
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)),
),
),
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), 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>( Consumer<Auth>(
builder: (context, auth, _) { builder: (context, auth, _) {
return IconButton( return IconButton(
tooltip: 'Log out',
icon: const Icon(Icons.logout), icon: const Icon(Icons.logout),
onPressed: () => auth.logoff(), onPressed: () {
auth.logoff();
},
); );
}, },
), ),
], ],
), ),
body: Padding(
padding: const EdgeInsets.all(constants.padding),
child: MountListWidget(),
), ),
floatingActionButton: FloatingActionButton( ),
onPressed: () => Navigator.pushNamed(context, '/add'), ),
tooltip: 'Add Mount', ],
child: const Icon(Icons.add), ),
),
const SizedBox(height: constants.padding),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: constants.padding,
),
child: const MountListWidget(),
),
),
],
),
],
),
),
floatingActionButton: Padding(
padding: const EdgeInsets.all(constants.padding),
child: Hero(
tag: 'add_mount_fab',
child: Material(
color: Colors.transparent,
elevation: 12, // match card depth
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,14 +58,12 @@ 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,10 +94,9 @@ 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'),
@@ -129,8 +127,7 @@ void createIntSetting(
onChanged: (nextValue) => updatedValue = nextValue, onChanged: (nextValue) => updatedValue = nextValue,
), ),
title: createSettingTitle(context, key, description), title: createSettingTitle(context, key, description),
); ),
},
); );
}, },
), ),
@@ -163,10 +160,9 @@ 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: [
@@ -218,8 +214,7 @@ void createPasswordSetting(
), ),
), ),
IconButton( IconButton(
onPressed: onPressed: () => setDialogState(
() => setDialogState(
() => hidePassword1 = !hidePassword1, () => hidePassword1 = !hidePassword1,
), ),
icon: Icon( icon: Icon(
@@ -245,8 +240,7 @@ void createPasswordSetting(
), ),
), ),
IconButton( IconButton(
onPressed: onPressed: () => setDialogState(
() => setDialogState(
() => hidePassword2 = !hidePassword2, () => hidePassword2 = !hidePassword2,
), ),
icon: Icon( icon: Icon(
@@ -262,8 +256,7 @@ void createPasswordSetting(
title: createSettingTitle(context, key, description), title: createSettingTitle(context, key, description),
); );
}, },
); ),
},
); );
}, },
), ),
@@ -313,8 +306,7 @@ 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,10 +337,9 @@ 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'),
@@ -380,8 +371,7 @@ void createStringSetting(
onChanged: (value) => updatedValue = value, onChanged: (value) => updatedValue = value,
), ),
title: createSettingTitle(context, key, description), 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,8 +428,7 @@ 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]],
), ),
@@ -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,57 +21,119 @@ 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),
elevation: 12,
color: scheme.primary.withValues(alpha: 0.15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
side: BorderSide(
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>( child: Consumer<Mount>(
builder: (context, Mount mount, _) { builder: (context, Mount mount, _) {
final textColor = Theme.of(context).colorScheme.onSurface; final titleStyle = textTheme.titleMedium?.copyWith(
final subTextColor = Theme.of(context).brightness == Brightness.dark fontWeight: FontWeight.w700,
? Colors.white38 letterSpacing: 0.15,
: Colors.black87; 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(
color: scheme.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(
constants.borderRadiusSmall,
), ),
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(
Icons.settings,
color: scheme.onSurface.withValues(alpha: 0.92),
size: 22,
),
onPressed: () {
Navigator.pushNamed(context, '/edit', arguments: mount);
},
),
),
title: SelectableText(mount.provider, style: titleStyle),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
nameText, nameText,
const SizedBox(height: 3),
SelectableText( SelectableText(
mount.path.isEmpty && mount.mounted == null mount.path.isEmpty && mount.mounted == null
? 'loading...' ? 'loading...'
: mount.path.isEmpty : mount.path.isEmpty
? '<mount location not set>' ? '<mount location not set>'
: mount.path, : mount.path,
style: TextStyle(color: subTextColor), style: pathStyle,
), ),
], ],
), ),
title: SelectableText(
mount.provider,
style: TextStyle(color: textColor, fontWeight: FontWeight.bold),
),
trailing: Row( trailing: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (mount.mounted != null && !mount.mounted!) if (mount.mounted != null && !mount.mounted!)
IconButton( IconButton(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
color: subTextColor, color: scheme.onSurface.withValues(alpha: 0.70),
tooltip: 'Edit mount location', tooltip: 'Edit mount location',
onPressed: () async { onPressed: () async {
setState(() => _editEnabled = false); setState(() {
_editEnabled = false;
});
final available = await mount.getAvailableLocations(); final available = await mount.getAvailableLocations();
if (context.mounted) { if (context.mounted) {
final location = await editMountLocation( final location = await editMountLocation(
@@ -84,20 +145,24 @@ class _MountWidgetState extends State<MountWidget> {
await mount.setMountLocation(location); await mount.setMountLocation(location);
} }
} }
setState(() => _editEnabled = true); setState(() {
_editEnabled = true;
});
}, },
), ),
IconButton( IconButton(
iconSize: 32,
splashRadius: 26,
icon: Icon( icon: Icon(
mount.mounted == null mount.mounted == null
? Icons.hourglass_top ? Icons.hourglass_top
: mount.mounted! : mount.mounted!
? Icons.toggle_on ? Icons.toggle_on
: Icons.toggle_off, : Icons.toggle_off,
color: mount.mounted ?? false
? Theme.of(context).colorScheme.primary
: subTextColor,
), ),
color: mount.mounted ?? false
? scheme.primary
: scheme.outline.withValues(alpha: 0.70),
tooltip: mount.mounted == null tooltip: mount.mounted == null
? '' ? ''
: mount.mounted! : mount.mounted!
@@ -110,10 +175,11 @@ class _MountWidgetState extends State<MountWidget> {
); );
}, },
), ),
),
); );
} }
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,8 +129,7 @@ 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;
@@ -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,6 +1,4 @@
<!DOCTYPE html> <!DOCTYPE html><html><head>
<html>
<head>
<!-- <!--
If you are serving your web app in a path other than the root, change the 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. href value below to reflect the base path you are serving from.
@@ -31,14 +29,9 @@
<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>
window.flutterConfiguration = {
canvasKitBaseUrl: "/canvaskit/"
};
</script>
<script src="flutter_bootstrap.js" async=""></script> <script src="flutter_bootstrap.js" async=""></script>
</body>
</html>
</body></html>