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

222 lines
5.9 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() {
_hideMenu();
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: const Offset(
_buttonSize - _menuWidth,
_buttonSize + AppSpacing.sm,
),
child: _buildMenuCard(),
),
],
);
},
);
overlay.insert(_menuEntry!);
setState(() {});
}
void _hideMenu() {
_menuEntry?.remove();
_menuEntry = null;
if (mounted) {
setState(() {});
}
}
void _handleSelect(T value) {
_hideMenu();
widget.onSelected(value);
}
Widget _buildMenuCard() {
return Material(
color: Colors.transparent,
child: Container(
width: _menuWidth,
padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: AppColors.borderSecondary),
boxShadow: [
BoxShadow(
color: AppColors.slate300.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)
const Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Divider(
height: 1,
thickness: 1,
color: AppColors.slate100,
),
),
],
],
),
),
);
}
Widget _buildMenuItem(DetailHeaderActionItem<T> item) {
final textColor = item.isDestructive
? AppColors.red500
: (item.enabled ? AppColors.slate700 : AppColors.slate400);
final pressedColor = item.isDestructive
? AppColors.feedbackErrorSurface
: AppColors.surfaceInfoLight;
return SizedBox(
height: AppSpacing.xxl * 2,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.md),
splashColor: item.enabled ? pressedColor : Colors.transparent,
highlightColor: item.enabled ? pressedColor : Colors.transparent,
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();
}
return CompositedTransformTarget(
link: _layerLink,
child: GestureDetector(
onTap: _toggleMenu,
child: AnimatedContainer(
duration: const Duration(milliseconds: 140),
width: _buttonSize,
height: _buttonSize,
decoration: BoxDecoration(
color: _isMenuOpen
? AppColors.surfaceInfo
: AppColors.surfaceTertiary,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: _isMenuOpen
? AppColors.borderQuaternary
: AppColors.borderTertiary,
),
),
child: const Icon(
Icons.more_horiz,
size: AppSpacing.lg + AppSpacing.xs,
color: AppColors.slate600,
),
),
),
);
}
}