Files
repertory/web/repertory/lib/widgets/mount_widget.dart
Scott E. Graves f5df53f781
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
refactor ui
2025-09-05 09:27:32 -05:00

406 lines
12 KiB
Dart

// mount_widget.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/mount.dart';
import 'package:repertory/utils/safe_set_state_mixin.dart';
class MountWidget extends StatefulWidget {
const MountWidget({super.key});
@override
State<MountWidget> createState() => _MountWidgetState();
}
class _MountWidgetState extends State<MountWidget>
with SafeSetState<MountWidget> {
bool _enabled = true;
bool _editEnabled = true;
Timer? _timer;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
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),
);
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 120),
child: Card(
margin: const EdgeInsets.all(0.0),
elevation: 12,
color: scheme.primary.withValues(alpha: constants.primaryAlpha),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(constants.borderRadiusSmall),
side: BorderSide(
color: scheme.outlineVariant.withValues(alpha: 0.06),
width: 1,
),
),
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
constants.borderRadiusSmall,
),
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: constants.gradientColors2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.22),
blurRadius: constants.borderRadiusSmall,
offset: const Offset(0, constants.borderRadiusSmall),
),
],
),
),
),
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(
'Name • ${formatMountName(mount.type, mount.name)}',
style: subStyle,
),
],
),
),
_ToggleFramed(
mounted: mount.mounted,
onPressed: _createMountHandler(context, mount),
),
],
),
const SizedBox(height: constants.padding),
Row(
children: [
_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;
});
}
},
),
const SizedBox(width: constants.padding),
Expanded(
child: SelectableText(
_prettyPath(mount),
style: subStyle,
),
),
],
),
],
);
},
),
),
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 _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;
}
// ignore: use_build_context_synchronously
return editMountLocation(context, await mount.getAvailableLocations());
}
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 5), (_) {
Provider.of<Mount>(context, listen: false).refresh();
});
}
@override
void dispose() {
_timer?.cancel();
_timer = null;
super.dispose();
}
}
class _FramedBox extends StatelessWidget {
final IconData icon;
final VoidCallback? onTap;
final Color? iconColor;
const _FramedBox({required this.icon, this.onTap, this.iconColor});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final radius = BorderRadius.circular(constants.borderRadiusSmall);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: 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: 32.0,
),
),
),
),
),
);
}
}
class _GearBadge extends StatelessWidget {
final VoidCallback onTap;
const _GearBadge({required this.onTap});
@override
Widget build(BuildContext context) {
return _FramedBox(icon: Icons.settings, onTap: onTap);
}
}
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,
);
}
}
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),
),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
backgroundColor: scheme.primary.withValues(alpha: 0.10),
),
),
);
}
}