167 lines
4.4 KiB
Dart
167 lines
4.4 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:flutter/services.dart';
|
||
|
|
|
||
|
|
import '../player_panel_shell.dart';
|
||
|
|
import 'panel_category_meta.dart';
|
||
|
|
import 'player_panel_controller.dart';
|
||
|
|
|
||
|
|
|
||
|
|
class PlayerPanelPopover extends StatefulWidget {
|
||
|
|
|
||
|
|
final PlayerPanelController controller;
|
||
|
|
|
||
|
|
|
||
|
|
final Map<PlayerPanelCategory, LayerLink> anchorLinks;
|
||
|
|
|
||
|
|
|
||
|
|
final Set<PlayerPanelCategory> downwardCategories;
|
||
|
|
|
||
|
|
|
||
|
|
final Widget Function(PlayerPanelCategory) bodyBuilder;
|
||
|
|
|
||
|
|
|
||
|
|
final double width;
|
||
|
|
|
||
|
|
const PlayerPanelPopover({
|
||
|
|
super.key,
|
||
|
|
required this.controller,
|
||
|
|
required this.anchorLinks,
|
||
|
|
this.downwardCategories = const {},
|
||
|
|
required this.bodyBuilder,
|
||
|
|
this.width = 340,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<PlayerPanelPopover> createState() => _PlayerPanelPopoverState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _PlayerPanelPopoverState extends State<PlayerPanelPopover>
|
||
|
|
with SingleTickerProviderStateMixin {
|
||
|
|
late final AnimationController _anim;
|
||
|
|
late final Animation<double> _t;
|
||
|
|
late final Animation<double> _scale;
|
||
|
|
|
||
|
|
|
||
|
|
PlayerPanelCategory? _visibleCategory;
|
||
|
|
|
||
|
|
|
||
|
|
bool _showing = false;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_anim = AnimationController(
|
||
|
|
vsync: this,
|
||
|
|
duration: const Duration(milliseconds: 180),
|
||
|
|
reverseDuration: const Duration(milliseconds: 130),
|
||
|
|
value: widget.controller.value != null ? 1.0 : 0.0,
|
||
|
|
);
|
||
|
|
_t = CurvedAnimation(
|
||
|
|
parent: _anim,
|
||
|
|
curve: Curves.easeOutCubic,
|
||
|
|
reverseCurve: Curves.easeInCubic,
|
||
|
|
);
|
||
|
|
_scale = Tween<double>(begin: 0.96, end: 1.0).animate(_t);
|
||
|
|
_visibleCategory = widget.controller.value;
|
||
|
|
_showing = widget.controller.value != null;
|
||
|
|
_anim.addStatusListener(_onStatus);
|
||
|
|
widget.controller.addListener(_onController);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _onController() {
|
||
|
|
final c = widget.controller.value;
|
||
|
|
if (c != null) {
|
||
|
|
setState(() {
|
||
|
|
_visibleCategory = c;
|
||
|
|
_showing = true;
|
||
|
|
});
|
||
|
|
_anim.forward();
|
||
|
|
} else {
|
||
|
|
_anim.reverse();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _onStatus(AnimationStatus status) {
|
||
|
|
if (status == AnimationStatus.dismissed && mounted && _showing) {
|
||
|
|
setState(() => _showing = false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
widget.controller.removeListener(_onController);
|
||
|
|
_anim.removeStatusListener(_onStatus);
|
||
|
|
_anim.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final category = _visibleCategory;
|
||
|
|
if (!_showing || category == null) return const SizedBox.shrink();
|
||
|
|
final link = widget.anchorLinks[category];
|
||
|
|
if (link == null) return const SizedBox.shrink();
|
||
|
|
final downward = widget.downwardCategories.contains(category);
|
||
|
|
final maxH = (MediaQuery.of(context).size.height * 0.55).clamp(0.0, 360.0);
|
||
|
|
return Stack(
|
||
|
|
children: [
|
||
|
|
Positioned.fill(
|
||
|
|
child: GestureDetector(
|
||
|
|
behavior: HitTestBehavior.opaque,
|
||
|
|
onTap: widget.controller.close,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Positioned(
|
||
|
|
left: 0,
|
||
|
|
top: 0,
|
||
|
|
child: CompositedTransformFollower(
|
||
|
|
link: link,
|
||
|
|
targetAnchor: downward ? Alignment.bottomRight : Alignment.topRight,
|
||
|
|
followerAnchor: downward
|
||
|
|
? Alignment.topRight
|
||
|
|
: Alignment.bottomRight,
|
||
|
|
offset: Offset(0, downward ? 8 : -8),
|
||
|
|
showWhenUnlinked: false,
|
||
|
|
child: FadeTransition(
|
||
|
|
opacity: _t,
|
||
|
|
child: ScaleTransition(
|
||
|
|
scale: _scale,
|
||
|
|
alignment: downward
|
||
|
|
? Alignment.topRight
|
||
|
|
: Alignment.bottomRight,
|
||
|
|
child: _bubble(category, maxH),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _bubble(PlayerPanelCategory category, double maxHeight) {
|
||
|
|
return CallbackShortcuts(
|
||
|
|
bindings: <ShortcutActivator, VoidCallback>{
|
||
|
|
const SingleActivator(LogicalKeyboardKey.escape):
|
||
|
|
widget.controller.close,
|
||
|
|
},
|
||
|
|
child: Focus(
|
||
|
|
autofocus: true,
|
||
|
|
child: GestureDetector(
|
||
|
|
behavior: HitTestBehavior.opaque,
|
||
|
|
onTap: () {},
|
||
|
|
child: ConstrainedBox(
|
||
|
|
constraints: BoxConstraints(maxHeight: maxHeight),
|
||
|
|
child: PlayerPanelShell(
|
||
|
|
icon: panelCategoryIcon(category),
|
||
|
|
title: panelCategoryLabel(category),
|
||
|
|
width: widget.width,
|
||
|
|
onClose: widget.controller.close,
|
||
|
|
child: widget.bodyBuilder(category),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|