[ui] UI theme should match repertory blue #61

This commit is contained in:
2025-09-04 09:09:10 -05:00
parent 4b44ce4482
commit 58451858fd
5 changed files with 367 additions and 193 deletions

View File

@@ -22,6 +22,7 @@ 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 primaryAlpha = 0.15; const primaryAlpha = 0.15;
const secondaryAlpha = 0.50;
const surfaceContainerLowDark = Color(0xFF292A2D); const surfaceContainerLowDark = Color(0xFF292A2D);
const surfaceDark = Color(0xFF202124); const surfaceDark = Color(0xFF202124);

View File

@@ -16,7 +16,9 @@ Future doShowDialog(BuildContext context, Widget child) => showDialog(
return Theme( return Theme(
data: theme.copyWith( data: theme.copyWith(
dialogTheme: DialogThemeData( dialogTheme: DialogThemeData(
backgroundColor: scheme.primary.withValues(alpha: constants.primaryAlpha), backgroundColor: scheme.primary.withValues(
alpha: constants.primaryAlpha,
),
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(constants.borderRadius), borderRadius: BorderRadius.circular(constants.borderRadius),
@@ -364,11 +366,15 @@ Map<String, dynamic> getChanged(
} }
Future<String?> editMountLocation( Future<String?> editMountLocation(
context, BuildContext context,
List<String> available, { List<String> available, {
bool allowEmpty = false, bool allowEmpty = false,
String? location, String? location,
}) async { }) async {
if (!context.mounted) {
return location;
}
String? currentLocation = location; String? currentLocation = location;
final controller = TextEditingController(text: currentLocation); final controller = TextEditingController(text: currentLocation);
return await doShowDialog( return await doShowDialog(

View File

@@ -179,17 +179,16 @@ class _HomeScreeState extends State<HomeScreen> {
child: Hero( child: Hero(
tag: 'add_mount_fab', tag: 'add_mount_fab',
child: Material( child: Material(
color: Colors.transparent, color: scheme.primary.withValues(alpha: constants.secondaryAlpha),
elevation: 12, elevation: 12,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(constants.borderRadius), borderRadius: BorderRadius.circular(constants.borderRadius),
), ),
child: Ink( child: Ink(
decoration: BoxDecoration( decoration: BoxDecoration(
color: scheme.primary.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(constants.borderRadius), borderRadius: BorderRadius.circular(constants.borderRadius),
border: Border.all( border: Border.all(
color: scheme.outlineVariant.withValues(alpha: 0.15), color: scheme.outlineVariant.withValues(alpha: 0.06),
width: 1, width: 1,
), ),
gradient: const LinearGradient( gradient: const LinearGradient(

View File

@@ -24,7 +24,21 @@ class _MountWidgetState extends State<MountWidget> {
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
return Card( 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.bodyMedium?.copyWith(
color: scheme.onSurface.withValues(alpha: 0.68),
);
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 120),
child: Card(
margin: const EdgeInsets.all(0.0), margin: const EdgeInsets.all(0.0),
elevation: 12, elevation: 12,
color: scheme.primary.withValues(alpha: constants.primaryAlpha), color: scheme.primary.withValues(alpha: constants.primaryAlpha),
@@ -35,9 +49,14 @@ class _MountWidgetState extends State<MountWidget> {
width: 1, width: 1,
), ),
), ),
child: Stack(
children: [
Positioned.fill(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall), borderRadius: BorderRadius.circular(
constants.borderRadiusSmall,
),
gradient: const LinearGradient( gradient: const LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
@@ -51,91 +70,84 @@ class _MountWidgetState extends State<MountWidget> {
), ),
], ],
), ),
),
),
Padding(
padding: const EdgeInsets.all(constants.padding),
child: Consumer<Mount>( child: Consumer<Mount>(
builder: (context, Mount mount, _) { builder: (context, Mount mount, _) {
final titleStyle = textTheme.titleMedium?.copyWith( return Column(
fontWeight: FontWeight.w700, mainAxisSize: MainAxisSize.min,
letterSpacing: 0.15, crossAxisAlignment: CrossAxisAlignment.start,
color: scheme.onSurface.withValues(alpha: 0.96), children: [
); Row(
final subStyle = textTheme.bodyMedium?.copyWith( crossAxisAlignment: CrossAxisAlignment.center,
color: scheme.onSurface.withValues(alpha: 0.78), children: [
); _GearBadge(
final pathStyle = textTheme.bodySmall?.copyWith( onTap: () {
color: scheme.onSurface.withValues(alpha: 0.68), Navigator.pushNamed(
); context,
'/edit',
final nameText = SelectableText( arguments: mount,
formatMountName(mount.type, mount.name), );
style: subStyle, },
); ),
const SizedBox(width: constants.padding),
return ListTile( Expanded(
isThreeLine: true, child: Column(
contentPadding: const EdgeInsets.all(constants.paddingSmall),
leading: Container(
width: 46,
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(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
nameText,
const SizedBox(height: 3),
SelectableText( SelectableText(
mount.path.isEmpty && mount.mounted == null mount.provider,
? 'loading...' style: titleStyle,
: mount.path.isEmpty ),
? '<mount location not set>' SelectableText(
: mount.path, '${_formatType(mount.type)}${formatMountName(mount.type, mount.name)}',
style: pathStyle, style: subStyle,
), ),
], ],
), ),
trailing: Row( ),
mainAxisAlignment: MainAxisAlignment.end, _ToggleFramed(
mainAxisSize: MainAxisSize.min, mounted: mount.mounted,
onPressed: _createMountHandler(context, mount),
),
],
),
const SizedBox(height: constants.padding),
Row(
children: [ children: [
if (mount.mounted != null && !mount.mounted!) Expanded(
IconButton( child: SelectableText(
icon: const Icon(Icons.edit), _prettyPath(mount),
color: scheme.onSurface.withValues(alpha: 0.70), style: pathStyle,
tooltip: 'Edit mount location', ),
),
const SizedBox(width: constants.padding),
_EditPathButton(
enabled: mount.mounted == false,
onPressed: () async { onPressed: () async {
if (!_editEnabled) {
return;
}
setState(() { setState(() {
_editEnabled = false; _editEnabled = false;
}); });
final available = await mount.getAvailableLocations();
if (context.mounted) { final available = await mount
.getAvailableLocations();
if (!mounted) {
setState(() {
_editEnabled = true;
});
return;
}
if (!context.mounted) {
return;
}
final location = await editMountLocation( final location = await editMountLocation(
context, context,
available, available,
@@ -144,50 +156,72 @@ class _MountWidgetState extends State<MountWidget> {
if (location != null) { if (location != null) {
await mount.setMountLocation(location); await mount.setMountLocation(location);
} }
}
if (mounted) {
setState(() { setState(() {
_editEnabled = true; _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),
),
], ],
), ),
],
); );
}, },
), ),
), ),
Positioned(
left: constants.padding,
right: constants.padding,
bottom: constants.padding - 1,
child: Container(
height: 1,
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.outlineVariant.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(1),
),
),
),
],
),
),
); );
} }
String _formatType(String type) {
if (type.toUpperCase() == 'S3') {
return 'S3';
}
if (type.isEmpty) {
return type;
}
return type[0].toUpperCase() + type.substring(1).toLowerCase();
}
String _prettyPath(Mount mount) {
if (mount.path.isEmpty && mount.mounted == null) {
return 'loading...';
}
if (mount.path.isEmpty) {
return '<mount location not set>';
}
return mount.path;
}
VoidCallback? _createMountHandler(BuildContext context, Mount mount) { VoidCallback? _createMountHandler(BuildContext context, Mount mount) {
return _enabled && mount.mounted != null if (!(_enabled && mount.mounted != null)) {
? () async { return null;
}
return () async {
if (mount.mounted == null) { if (mount.mounted == null) {
return; return;
} }
final mounted = mount.mounted!; final mounted = mount.mounted!;
setState(() { setState(() {
_enabled = false; _enabled = false;
}); });
@@ -200,8 +234,11 @@ class _MountWidgetState extends State<MountWidget> {
}); });
} }
if (!context.mounted && location == null) { if (!mounted && location == null) {
displayErrorMessage(context, "Mount location is not set"); if (!context.mounted) {
return;
}
displayErrorMessage(context, 'Mount location is not set');
cleanup(); cleanup();
return; return;
} }
@@ -217,38 +254,25 @@ class _MountWidgetState extends State<MountWidget> {
displayErrorMessage( displayErrorMessage(
context, context,
"Mount location is not available: $location", 'Mount location is not available: $location',
); );
cleanup(); cleanup();
} };
: null;
}
@override
void dispose() {
_timer?.cancel();
_timer = null;
super.dispose();
} }
Future<String?> _getMountLocation(BuildContext context, Mount mount) async { Future<String?> _getMountLocation(BuildContext context, Mount mount) async {
if (mount.mounted ?? false) { if (mount.mounted ?? false) {
return null; return null;
} }
if (mount.path.isNotEmpty) { if (mount.path.isNotEmpty) {
return mount.path; return mount.path;
} }
String? location = await mount.getMountLocation(); String? location = await mount.getMountLocation();
if (location != null) { if (location != null) {
return location; return location;
} }
if (!context.mounted) { // ignore: use_build_context_synchronously
return location;
}
return editMountLocation(context, await mount.getAvailableLocations()); return editMountLocation(context, await mount.getAvailableLocations());
} }
@@ -260,6 +284,13 @@ class _MountWidgetState extends State<MountWidget> {
}); });
} }
@override
void dispose() {
_timer?.cancel();
_timer = null;
super.dispose();
}
@override @override
void setState(VoidCallback fn) { void setState(VoidCallback fn) {
if (!mounted) { if (!mounted) {
@@ -268,3 +299,135 @@ class _MountWidgetState extends State<MountWidget> {
super.setState(fn); super.setState(fn);
} }
} }
class _FramedBox extends StatelessWidget {
final IconData icon;
final VoidCallback? onTap;
final Color? iconColor;
final double iconSize;
const _FramedBox({
required this.icon,
this.onTap,
this.iconColor,
this.iconSize = 22,
});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final radius = BorderRadius.circular(constants.borderRadiusSmall);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: radius,
child: Ink(
width: 46,
height: 46,
decoration: BoxDecoration(
color: scheme.primary.withValues(alpha: 0.12),
borderRadius: radius,
border: Border.all(
color: scheme.outlineVariant.withValues(alpha: 0.08),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.24),
blurRadius: constants.borderRadiusSmall / 2.0,
offset: const Offset(0, 5),
),
],
),
child: Center(
child: Transform.scale(
scale: 0.90,
child: Icon(
icon,
color: iconColor ?? scheme.onSurface.withValues(alpha: 0.92),
size: iconSize,
),
),
),
),
),
);
}
}
class _GearBadge extends StatelessWidget {
final VoidCallback onTap;
const _GearBadge({required this.onTap});
@override
Widget build(BuildContext context) {
return _FramedBox(icon: Icons.settings, onTap: onTap, iconSize: 22);
}
}
class _ToggleFramed extends StatelessWidget {
final bool? mounted;
final VoidCallback? onPressed;
const _ToggleFramed({required this.mounted, required this.onPressed});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final bool isOn = mounted ?? false;
IconData icon = Icons.hourglass_top;
Color iconColor = scheme.onSurface.withValues(alpha: 0.60);
if (mounted != null) {
icon = isOn ? Icons.toggle_on : Icons.toggle_off;
iconColor = isOn
? scheme.primary
: scheme.onSurface.withValues(alpha: 0.55);
}
return _FramedBox(
icon: icon,
iconColor: iconColor,
onTap: mounted == null ? null : onPressed,
iconSize: 22,
);
}
}
class _EditPathButton extends StatelessWidget {
final bool enabled;
final VoidCallback onPressed;
const _EditPathButton({required this.enabled, required this.onPressed});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Opacity(
opacity: enabled ? 1.0 : 0.45,
child: OutlinedButton.icon(
onPressed: enabled ? onPressed : null,
icon: const Icon(Icons.edit, size: 18),
label: const Text('Edit path'),
style: OutlinedButton.styleFrom(
foregroundColor: scheme.primary,
side: BorderSide(
color: scheme.primary.withValues(alpha: 0.55),
width: 1.2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
constants.borderRadiusSmall * 1.6,
),
),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
backgroundColor: scheme.primary.withValues(alpha: 0.10),
),
),
);
}
}

View File

@@ -31,6 +31,11 @@
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script type="text/javascript" src="sodium.js" async="true"></script></head> <script type="text/javascript" src="sodium.js" async="true"></script></head>
<body> <body>
<script>
window.flutterConfiguration = {
canvasKitBaseUrl: "/canvaskit/"
};
</script>
<script src="flutter_bootstrap.js" async=""></script> <script src="flutter_bootstrap.js" async=""></script>