Files
social-app/apps/lib/shared/widgets/detail_header_action_menu.dart
T

235 lines
6.4 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import '../../core/theme/design_tokens.dart';
class DetailHeaderActionItem<T> {
const DetailHeaderActionItem({
required this.value,
required this.label,
required this.icon,
this.isDestructive = false,
this.enabled = true,
});
final T value;
final String label;
final IconData icon;
final bool isDestructive;
final bool enabled;
}
class DetailHeaderActionMenu<T> extends StatefulWidget {
const DetailHeaderActionMenu({
super.key,
required this.items,
required this.onSelected,
});
final List<DetailHeaderActionItem<T>> items;
final ValueChanged<T> onSelected;
@override
State<DetailHeaderActionMenu<T>> createState() =>
_DetailHeaderActionMenuState<T>();
}
class _DetailHeaderActionMenuState<T> extends State<DetailHeaderActionMenu<T>> {
static const double _buttonSize = AppSpacing.xl * 2;
static const double _menuWidth = AppSpacing.xxl * 8;
final LayerLink _layerLink = LayerLink();
OverlayEntry? _menuEntry;
bool get _isMenuOpen => _menuEntry != null;
@override
void dispose() {
_removeOverlayEntry();
super.dispose();
}
void _toggleMenu() {
if (_isMenuOpen) {
_hideMenu();
return;
}
_showMenu();
}
void _showMenu() {
final overlay = Overlay.of(context);
_menuEntry = OverlayEntry(
builder: (context) {
return Stack(
children: [
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _hideMenu,
child: const SizedBox.expand(),
),
),
CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
offset: Offset(
_buttonSize - _menuWidth,
_buttonSize + AppSpacing.sm,
),
child: _buildMenuCard(),
),
],
);
},
);
overlay.insert(_menuEntry!);
setState(() {});
}
void _removeOverlayEntry() {
_menuEntry?.remove();
_menuEntry = null;
}
void _hideMenu() {
_removeOverlayEntry();
if (mounted) {
setState(() {});
}
}
void _handleSelect(T value) {
_hideMenu();
widget.onSelected(value);
}
Widget _buildMenuCard() {
final colorScheme = Theme.of(context).colorScheme;
return Material(
color: colorScheme.surface.withValues(alpha: 0),
child: Container(
width: _menuWidth,
padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: colorScheme.outlineVariant),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.42),
blurRadius: AppSpacing.xl,
offset: const Offset(0, AppSpacing.sm),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (int i = 0; i < widget.items.length; i++) ...[
_buildMenuItem(widget.items[i]),
if (i < widget.items.length - 1)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
),
child: Divider(
height: 1,
thickness: 1,
color: colorScheme.outlineVariant,
),
),
],
],
),
),
);
}
Widget _buildMenuItem(DetailHeaderActionItem<T> item) {
final colorScheme = Theme.of(context).colorScheme;
final textColor = item.isDestructive
? colorScheme.error
: (item.enabled ? colorScheme.onSurface : colorScheme.onSurfaceVariant);
final pressedColor = item.isDestructive
? colorScheme.errorContainer
: colorScheme.primaryContainer;
return SizedBox(
height: AppSpacing.xxl * 2,
child: Material(
color: colorScheme.surface.withValues(alpha: 0),
child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.md),
splashColor: item.enabled
? pressedColor
: colorScheme.surface.withValues(alpha: 0),
highlightColor: item.enabled
? pressedColor
: colorScheme.surface.withValues(alpha: 0),
onTap: item.enabled ? () => _handleSelect(item.value) : null,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
item.icon,
size: AppSpacing.lg + AppSpacing.xs,
color: textColor,
),
const SizedBox(width: AppSpacing.md),
Text(
item.label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: textColor,
),
),
],
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
if (widget.items.isEmpty) {
return const SizedBox.shrink();
}
final colorScheme = Theme.of(context).colorScheme;
return CompositedTransformTarget(
link: _layerLink,
child: GestureDetector(
onTap: _toggleMenu,
child: AnimatedContainer(
duration: const Duration(milliseconds: 140),
width: _buttonSize,
height: _buttonSize,
decoration: BoxDecoration(
color: _isMenuOpen
? colorScheme.secondaryContainer
: colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: _isMenuOpen
? colorScheme.outlineVariant
: colorScheme.outline,
),
),
child: Icon(
Icons.more_horiz,
size: AppSpacing.lg + AppSpacing.xs,
color: colorScheme.onSurfaceVariant,
),
),
),
);
}
}