[ui] UI theme should match repertory blue #61
This commit is contained in:
@@ -22,6 +22,7 @@ const protocolTypeList = ['http', 'https'];
|
||||
const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia'];
|
||||
const ringBufferSizeList = ['128', '256', '512', '1024', '2048'];
|
||||
const primaryAlpha = 0.15;
|
||||
const secondaryAlpha = 0.50;
|
||||
const surfaceContainerLowDark = Color(0xFF292A2D);
|
||||
const surfaceDark = Color(0xFF202124);
|
||||
|
||||
|
@@ -16,7 +16,9 @@ Future doShowDialog(BuildContext context, Widget child) => showDialog(
|
||||
return Theme(
|
||||
data: theme.copyWith(
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: scheme.primary.withValues(alpha: constants.primaryAlpha),
|
||||
backgroundColor: scheme.primary.withValues(
|
||||
alpha: constants.primaryAlpha,
|
||||
),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadius),
|
||||
@@ -364,11 +366,15 @@ Map<String, dynamic> getChanged(
|
||||
}
|
||||
|
||||
Future<String?> editMountLocation(
|
||||
context,
|
||||
BuildContext context,
|
||||
List<String> available, {
|
||||
bool allowEmpty = false,
|
||||
String? location,
|
||||
}) async {
|
||||
if (!context.mounted) {
|
||||
return location;
|
||||
}
|
||||
|
||||
String? currentLocation = location;
|
||||
final controller = TextEditingController(text: currentLocation);
|
||||
return await doShowDialog(
|
||||
|
@@ -179,17 +179,16 @@ class _HomeScreeState extends State<HomeScreen> {
|
||||
child: Hero(
|
||||
tag: 'add_mount_fab',
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
color: scheme.primary.withValues(alpha: constants.secondaryAlpha),
|
||||
elevation: 12,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadius),
|
||||
),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.primary.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(constants.borderRadius),
|
||||
border: Border.all(
|
||||
color: scheme.outlineVariant.withValues(alpha: 0.15),
|
||||
color: scheme.outlineVariant.withValues(alpha: 0.06),
|
||||
width: 1,
|
||||
),
|
||||
gradient: const LinearGradient(
|
||||
|
@@ -24,231 +24,255 @@ class _MountWidgetState extends State<MountWidget> {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(0.0),
|
||||
elevation: 12,
|
||||
color: scheme.primary.withValues(alpha: constants.primaryAlpha),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
|
||||
side: BorderSide(
|
||||
color: scheme.outlineVariant.withValues(alpha: 0.06),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
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),
|
||||
elevation: 12,
|
||||
color: scheme.primary.withValues(alpha: constants.primaryAlpha),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: constants.gradientColors2,
|
||||
side: BorderSide(
|
||||
color: scheme.outlineVariant.withValues(alpha: 0.06),
|
||||
width: 1,
|
||||
),
|
||||
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(
|
||||
formatMountName(mount.type, mount.name),
|
||||
style: subStyle,
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
contentPadding: const EdgeInsets.all(constants.paddingSmall),
|
||||
leading: Container(
|
||||
width: 46,
|
||||
height: 46,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
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,
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: constants.gradientColors2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.24),
|
||||
color: Colors.black.withValues(alpha: 0.22),
|
||||
blurRadius: constants.borderRadiusSmall,
|
||||
offset: const Offset(0, 5),
|
||||
offset: const Offset(0, constants.borderRadiusSmall),
|
||||
),
|
||||
],
|
||||
),
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: Consumer<Mount>(
|
||||
builder: (context, Mount mount, _) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_GearBadge(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/edit',
|
||||
arguments: mount,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: constants.padding),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(
|
||||
mount.provider,
|
||||
style: titleStyle,
|
||||
),
|
||||
SelectableText(
|
||||
'${_formatType(mount.type)} • ${formatMountName(mount.type, mount.name)}',
|
||||
style: subStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ToggleFramed(
|
||||
mounted: mount.mounted,
|
||||
onPressed: _createMountHandler(context, mount),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
_prettyPath(mount),
|
||||
style: pathStyle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: constants.padding),
|
||||
_EditPathButton(
|
||||
enabled: mount.mounted == false,
|
||||
onPressed: () async {
|
||||
if (!_editEnabled) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_editEnabled = false;
|
||||
});
|
||||
|
||||
final available = await mount
|
||||
.getAvailableLocations();
|
||||
|
||||
if (!mounted) {
|
||||
setState(() {
|
||||
_editEnabled = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final location = await editMountLocation(
|
||||
context,
|
||||
available,
|
||||
location: mount.path,
|
||||
);
|
||||
if (location != null) {
|
||||
await mount.setMountLocation(location);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_editEnabled = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
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(BuildContext context, Mount mount) {
|
||||
return _enabled && mount.mounted != null
|
||||
? () async {
|
||||
if (mount.mounted == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final mounted = mount.mounted!;
|
||||
|
||||
setState(() {
|
||||
_enabled = false;
|
||||
});
|
||||
|
||||
final location = await _getMountLocation(context, mount);
|
||||
|
||||
void cleanup() {
|
||||
setState(() {
|
||||
_enabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (!context.mounted && location == null) {
|
||||
displayErrorMessage(context, "Mount location is not set");
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await mount.mount(mounted, location: location);
|
||||
if (success ||
|
||||
mounted ||
|
||||
constants.navigatorKey.currentContext == null ||
|
||||
!constants.navigatorKey.currentContext!.mounted) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
displayErrorMessage(
|
||||
context,
|
||||
"Mount location is not available: $location",
|
||||
);
|
||||
cleanup();
|
||||
}
|
||||
: null;
|
||||
String _formatType(String type) {
|
||||
if (type.toUpperCase() == 'S3') {
|
||||
return 'S3';
|
||||
}
|
||||
if (type.isEmpty) {
|
||||
return type;
|
||||
}
|
||||
return type[0].toUpperCase() + type.substring(1).toLowerCase();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
super.dispose();
|
||||
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) {
|
||||
if (!(_enabled && mount.mounted != null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return () async {
|
||||
if (mount.mounted == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final mounted = mount.mounted!;
|
||||
setState(() {
|
||||
_enabled = false;
|
||||
});
|
||||
|
||||
final location = await _getMountLocation(context, mount);
|
||||
|
||||
void cleanup() {
|
||||
setState(() {
|
||||
_enabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (!mounted && location == null) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
displayErrorMessage(context, 'Mount location is not set');
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await mount.mount(mounted, location: location);
|
||||
if (success ||
|
||||
mounted ||
|
||||
constants.navigatorKey.currentContext == null ||
|
||||
!constants.navigatorKey.currentContext!.mounted) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
displayErrorMessage(
|
||||
context,
|
||||
'Mount location is not available: $location',
|
||||
);
|
||||
cleanup();
|
||||
};
|
||||
}
|
||||
|
||||
Future<String?> _getMountLocation(BuildContext context, Mount mount) async {
|
||||
if (mount.mounted ?? false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mount.path.isNotEmpty) {
|
||||
return mount.path;
|
||||
}
|
||||
|
||||
String? location = await mount.getMountLocation();
|
||||
if (location != null) {
|
||||
return location;
|
||||
}
|
||||
|
||||
if (!context.mounted) {
|
||||
return location;
|
||||
}
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
return editMountLocation(context, await mount.getAvailableLocations());
|
||||
}
|
||||
|
||||
@@ -260,6 +284,13 @@ class _MountWidgetState extends State<MountWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
@@ -268,3 +299,135 @@ class _MountWidgetState extends State<MountWidget> {
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -31,6 +31,11 @@
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<script type="text/javascript" src="sodium.js" async="true"></script></head>
|
||||
<body>
|
||||
<script>
|
||||
window.flutterConfiguration = {
|
||||
canvasKitBaseUrl: "/canvaskit/"
|
||||
};
|
||||
</script>
|
||||
<script src="flutter_bootstrap.js" async=""></script>
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user