Initial commit

This commit is contained in:
admin1
2026-07-14 11:11:36 +08:00
commit 656499cf94
604 changed files with 119518 additions and 0 deletions
@@ -0,0 +1,166 @@
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),
),
),
),
),
);
}
}