200 lines
5.6 KiB
Dart
200 lines
5.6 KiB
Dart
|
|
import 'package:canvas_danmaku/canvas_danmaku.dart';
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:flutter/services.dart';
|
||
|
|
import 'package:forui/forui.dart';
|
||
|
|
|
||
|
|
import '../../../../core/contracts/danmaku.dart';
|
||
|
|
import '../../../../providers/danmaku_settings_provider.dart';
|
||
|
|
import '../../../../shared/widgets/app_loading_ring.dart';
|
||
|
|
import '../danmaku/danmaku_match_tile.dart';
|
||
|
|
import '../player_drawer_panel.dart';
|
||
|
|
import 'panel_setting_rows.dart';
|
||
|
|
|
||
|
|
part 'danmaku_panel_body_view.dart';
|
||
|
|
|
||
|
|
|
||
|
|
extension DanmakuPanelSettingsCanvas on DanmakuPanelSettings {
|
||
|
|
DanmakuOption toDanmakuOption({double? screenHeight}) {
|
||
|
|
final double effectiveArea;
|
||
|
|
if (maxRows > 0 && screenHeight != null && screenHeight > 0) {
|
||
|
|
final lineHeightPx = _measureDanmakuLineHeight(
|
||
|
|
fontSize: fontSize,
|
||
|
|
lineHeight: lineHeight,
|
||
|
|
fontFamily: fontFamily,
|
||
|
|
);
|
||
|
|
final computed = (maxRows + 0.5) * lineHeightPx / screenHeight;
|
||
|
|
effectiveArea = computed.clamp(0.05, 1.0);
|
||
|
|
} else {
|
||
|
|
effectiveArea = area;
|
||
|
|
}
|
||
|
|
return DanmakuOption(
|
||
|
|
|
||
|
|
|
||
|
|
opacity: 1.0,
|
||
|
|
area: effectiveArea,
|
||
|
|
fontSize: fontSize,
|
||
|
|
fontWeight: bold ? 7 : 4,
|
||
|
|
fontFamily: fontFamily,
|
||
|
|
duration: 10.0 / speed.clamp(0.1, 5.0),
|
||
|
|
lineHeight: lineHeight,
|
||
|
|
hideTop: fixedToScroll ? true : !showTop,
|
||
|
|
hideScroll: !showScroll,
|
||
|
|
hideBottom: fixedToScroll ? true : !showBottom,
|
||
|
|
strokeWidth: strokeWidth,
|
||
|
|
safeArea: true,
|
||
|
|
massiveMode: false,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
double _measureDanmakuLineHeight({
|
||
|
|
required double fontSize,
|
||
|
|
required double lineHeight,
|
||
|
|
String? fontFamily,
|
||
|
|
}) {
|
||
|
|
final painter = TextPainter(
|
||
|
|
text: TextSpan(
|
||
|
|
text: '弹幕',
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: fontSize,
|
||
|
|
height: lineHeight,
|
||
|
|
fontFamily: fontFamily,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
textDirection: TextDirection.ltr,
|
||
|
|
)..layout();
|
||
|
|
return painter.height;
|
||
|
|
}
|
||
|
|
|
||
|
|
enum _DanmakuView { main, settings, filter, search }
|
||
|
|
|
||
|
|
class DanmakuPanelBody extends StatefulWidget {
|
||
|
|
final DanmakuPanelSettings settings;
|
||
|
|
final ValueChanged<DanmakuPanelSettings> onChanged;
|
||
|
|
|
||
|
|
final DanmakuSessionState sessionState;
|
||
|
|
final bool danmakuGlobalEnabled;
|
||
|
|
final bool danmakuSourceConfigured;
|
||
|
|
|
||
|
|
final List<DanmakuMatchCandidate> matches;
|
||
|
|
final int selectedMatchIndex;
|
||
|
|
final ValueChanged<int> onMatchSelected;
|
||
|
|
final VoidCallback onOpenSearch;
|
||
|
|
final bool isLoading;
|
||
|
|
final int commentCount;
|
||
|
|
|
||
|
|
final List<String> blockedKeywords;
|
||
|
|
final ValueChanged<String> onAddKeyword;
|
||
|
|
final ValueChanged<String> onRemoveKeyword;
|
||
|
|
|
||
|
|
final VoidCallback? onRetry;
|
||
|
|
final VoidCallback? onGoToSettings;
|
||
|
|
final VoidCallback? onToggleDanmakuEnabled;
|
||
|
|
|
||
|
|
const DanmakuPanelBody({
|
||
|
|
super.key,
|
||
|
|
required this.settings,
|
||
|
|
required this.onChanged,
|
||
|
|
required this.sessionState,
|
||
|
|
this.danmakuGlobalEnabled = true,
|
||
|
|
this.danmakuSourceConfigured = true,
|
||
|
|
this.matches = const [],
|
||
|
|
this.selectedMatchIndex = -1,
|
||
|
|
required this.onMatchSelected,
|
||
|
|
required this.onOpenSearch,
|
||
|
|
this.isLoading = false,
|
||
|
|
this.commentCount = 0,
|
||
|
|
this.blockedKeywords = const [],
|
||
|
|
required this.onAddKeyword,
|
||
|
|
required this.onRemoveKeyword,
|
||
|
|
this.onRetry,
|
||
|
|
this.onGoToSettings,
|
||
|
|
this.onToggleDanmakuEnabled,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<DanmakuPanelBody> createState() => _DanmakuPanelBodyState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _DanmakuPanelBodyState extends State<DanmakuPanelBody> {
|
||
|
|
_DanmakuView _view = _DanmakuView.main;
|
||
|
|
bool _navForward = true;
|
||
|
|
|
||
|
|
DanmakuPanelSettings get s => widget.settings;
|
||
|
|
|
||
|
|
void _emit(DanmakuPanelSettings next) => widget.onChanged(next);
|
||
|
|
|
||
|
|
void _navigateTo(_DanmakuView view) {
|
||
|
|
if (view == _view) return;
|
||
|
|
setState(() {
|
||
|
|
_navForward = view != _DanmakuView.main;
|
||
|
|
_view = view;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void _back() => _navigateTo(_DanmakuView.main);
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return Material(
|
||
|
|
color: Colors.transparent,
|
||
|
|
child: AnimatedSwitcher(
|
||
|
|
duration: const Duration(milliseconds: 200),
|
||
|
|
switchInCurve: Curves.easeOutCubic,
|
||
|
|
switchOutCurve: Curves.easeInCubic,
|
||
|
|
transitionBuilder: (child, animation) {
|
||
|
|
final isEntering =
|
||
|
|
(child.key as ValueKey<_DanmakuView>).value == _view;
|
||
|
|
final enterOffset = _navForward
|
||
|
|
? const Offset(0.2, 0.0)
|
||
|
|
: const Offset(-0.2, 0.0);
|
||
|
|
final exitOffset = _navForward
|
||
|
|
? const Offset(-0.15, 0.0)
|
||
|
|
: const Offset(0.15, 0.0);
|
||
|
|
final slide = isEntering
|
||
|
|
? Tween<Offset>(
|
||
|
|
begin: enterOffset,
|
||
|
|
end: Offset.zero,
|
||
|
|
).animate(animation)
|
||
|
|
: Tween<Offset>(
|
||
|
|
begin: Offset.zero,
|
||
|
|
end: exitOffset,
|
||
|
|
).animate(animation);
|
||
|
|
return ClipRect(
|
||
|
|
child: SlideTransition(
|
||
|
|
position: slide,
|
||
|
|
child: FadeTransition(opacity: animation, child: child),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
child: KeyedSubtree(
|
||
|
|
key: ValueKey<_DanmakuView>(_view),
|
||
|
|
child: _buildView(),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildView() {
|
||
|
|
if (_view == _DanmakuView.main) {
|
||
|
|
final emptyState = _buildEmptyState();
|
||
|
|
if (emptyState != null) return emptyState;
|
||
|
|
}
|
||
|
|
return switch (_view) {
|
||
|
|
_DanmakuView.main => _buildMainMenu(),
|
||
|
|
_DanmakuView.settings => _buildSettingsDrawer(),
|
||
|
|
_DanmakuView.filter => _buildFilterDrawer(),
|
||
|
|
_DanmakuView.search => _buildSearchDrawer(),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
final TextEditingController _keywordController = TextEditingController();
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_keywordController.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
}
|