Initial commit
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'player_panel_controller.dart';
|
||||
|
||||
|
||||
String panelCategoryLabel(PlayerPanelCategory c) {
|
||||
switch (c) {
|
||||
case PlayerPanelCategory.subtitle:
|
||||
return '字幕';
|
||||
case PlayerPanelCategory.audio:
|
||||
return '音轨';
|
||||
case PlayerPanelCategory.speed:
|
||||
return '播放速度';
|
||||
case PlayerPanelCategory.fit:
|
||||
return '画面比例';
|
||||
case PlayerPanelCategory.episode:
|
||||
return '选集';
|
||||
case PlayerPanelCategory.danmaku:
|
||||
return '弹幕';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
IconData panelCategoryIcon(PlayerPanelCategory c) {
|
||||
switch (c) {
|
||||
case PlayerPanelCategory.subtitle:
|
||||
return Icons.subtitles_outlined;
|
||||
case PlayerPanelCategory.audio:
|
||||
return Icons.audiotrack;
|
||||
case PlayerPanelCategory.speed:
|
||||
return Icons.speed;
|
||||
case PlayerPanelCategory.fit:
|
||||
return Icons.aspect_ratio;
|
||||
case PlayerPanelCategory.episode:
|
||||
return Icons.format_list_numbered;
|
||||
case PlayerPanelCategory.danmaku:
|
||||
return Icons.forum_outlined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
|
||||
enum PlayerPanelCategory { subtitle, audio, speed, fit, episode, danmaku }
|
||||
|
||||
|
||||
class PlayerPanelController extends ValueNotifier<PlayerPanelCategory?> {
|
||||
PlayerPanelController() : super(null);
|
||||
|
||||
bool get isOpen => value != null;
|
||||
PlayerPanelCategory? get category => value;
|
||||
|
||||
void open(PlayerPanelCategory category) => value = category;
|
||||
|
||||
|
||||
void toggle(PlayerPanelCategory category) =>
|
||||
value = (value == category) ? null : category;
|
||||
|
||||
void close() => value = null;
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user