Skip to content

Commit 948a8cc

Browse files
authored
Merge pull request #206 from MCDFsteve/feature/dandanplay
feat: Cupertino 主题集成弹弹play 远程库、统一毛玻璃交互与自动匹配逻辑增强
2 parents 263fb73 + ce4b8e8 commit 948a8cc

21 files changed

+3613
-133
lines changed

lib/main.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,7 @@ class _NipaPlayAppState extends State<NipaPlayApp> {
799799
isDesktop: globals.isDesktop,
800800
isPhone: globals.isPhone,
801801
isWeb: kIsWeb,
802+
isIOS: !kIsWeb && Platform.isIOS,
802803
);
803804
final overlayBuilder = (Widget child) => Stack(
804805
children: [

lib/pages/dashboard_home_page.dart

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import 'package:nipaplay/models/jellyfin_model.dart';
2020
import 'package:nipaplay/models/emby_model.dart';
2121
import 'package:nipaplay/models/bangumi_model.dart';
2222
import 'package:nipaplay/themes/nipaplay/widgets/blur_snackbar.dart';
23+
import 'package:nipaplay/themes/nipaplay/widgets/blur_dialog.dart';
2324
import 'package:nipaplay/themes/nipaplay/widgets/anime_card.dart';
2425
import 'package:nipaplay/themes/nipaplay/widgets/cached_network_image_widget.dart';
2526
import 'package:nipaplay/themes/nipaplay/widgets/floating_action_glass_button.dart';
@@ -65,6 +66,9 @@ class _DashboardHomePageState extends State<DashboardHomePage>
6566
// Provider 通知后的轻量防抖(覆盖库选择等状态变化)
6667
Timer? _jfDebounceTimer;
6768
Timer? _emDebounceTimer;
69+
70+
bool _isHistoryAutoMatching = false;
71+
bool _historyAutoMatchDialogVisible = false;
6872

6973

7074
@override
@@ -2406,7 +2410,7 @@ style: TextStyle(color: Colors.white54, fontSize: 16),
24062410

24072411
Widget _buildContinueWatchingCard(WatchHistoryItem item, {bool compact = false}) {
24082412
return GestureDetector(
2409-
onTap: () => _onWatchHistoryItemTap(item),
2413+
onTap: _isHistoryAutoMatching ? null : () => _onWatchHistoryItemTap(item),
24102414
child: SizedBox(
24112415
key: ValueKey('continue_${item.animeId ?? 0}_${item.filePath.hashCode}'), // 添加唯一key
24122416
width: compact ? 220 : 280, // 手机更窄
@@ -3146,6 +3150,11 @@ style: TextStyle(color: Colors.white54, fontSize: 16),
31463150
}
31473151

31483152
void _onWatchHistoryItemTap(WatchHistoryItem item) async {
3153+
if (_isHistoryAutoMatching) {
3154+
BlurSnackBar.show(context, '正在自动匹配,请稍候');
3155+
return;
3156+
}
3157+
31493158
var currentItem = item;
31503159
// 检查是否为网络URL或流媒体协议URL
31513160
final isNetworkUrl = currentItem.filePath.startsWith('http://') || currentItem.filePath.startsWith('https://');
@@ -3214,12 +3223,7 @@ style: TextStyle(color: Colors.white54, fontSize: 16),
32143223

32153224
if (WatchHistoryAutoMatchHelper.shouldAutoMatch(currentItem)) {
32163225
final matchablePath = actualPlayUrl ?? currentItem.filePath;
3217-
currentItem = await WatchHistoryAutoMatchHelper.tryAutoMatch(
3218-
context,
3219-
currentItem,
3220-
matchablePath: matchablePath,
3221-
onMatched: (message) => BlurSnackBar.show(context, message),
3222-
);
3226+
currentItem = await _performHistoryAutoMatch(currentItem, matchablePath);
32233227
}
32243228

32253229
final playableItem = PlayableItem(
@@ -3235,6 +3239,81 @@ style: TextStyle(color: Colors.white54, fontSize: 16),
32353239
await PlaybackService().play(playableItem);
32363240
}
32373241

3242+
Future<WatchHistoryItem> _performHistoryAutoMatch(
3243+
WatchHistoryItem currentItem,
3244+
String matchablePath,
3245+
) async {
3246+
_updateHistoryAutoMatchingState(true);
3247+
_showHistoryAutoMatchingDialog();
3248+
String? notification;
3249+
3250+
try {
3251+
return await WatchHistoryAutoMatchHelper.tryAutoMatch(
3252+
context,
3253+
currentItem,
3254+
matchablePath: matchablePath,
3255+
onMatched: (message) => notification = message,
3256+
);
3257+
} finally {
3258+
_hideHistoryAutoMatchingDialog();
3259+
_updateHistoryAutoMatchingState(false);
3260+
if (notification != null && mounted) {
3261+
BlurSnackBar.show(context, notification!);
3262+
}
3263+
}
3264+
}
3265+
3266+
void _updateHistoryAutoMatchingState(bool value) {
3267+
if (!mounted) {
3268+
_isHistoryAutoMatching = value;
3269+
return;
3270+
}
3271+
if (_isHistoryAutoMatching == value) {
3272+
return;
3273+
}
3274+
setState(() {
3275+
_isHistoryAutoMatching = value;
3276+
});
3277+
}
3278+
3279+
void _showHistoryAutoMatchingDialog() {
3280+
if (_historyAutoMatchDialogVisible || !mounted) return;
3281+
_historyAutoMatchDialogVisible = true;
3282+
BlurDialog.show(
3283+
context: context,
3284+
title: '正在自动匹配',
3285+
barrierDismissible: false,
3286+
contentWidget: Column(
3287+
mainAxisSize: MainAxisSize.min,
3288+
children: const [
3289+
SizedBox(height: 8),
3290+
CircularProgressIndicator(
3291+
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
3292+
),
3293+
SizedBox(height: 16),
3294+
Text(
3295+
'正在为历史记录匹配弹幕,请稍候…',
3296+
style: TextStyle(color: Colors.white, fontSize: 14),
3297+
textAlign: TextAlign.center,
3298+
),
3299+
],
3300+
),
3301+
).whenComplete(() {
3302+
_historyAutoMatchDialogVisible = false;
3303+
});
3304+
}
3305+
3306+
void _hideHistoryAutoMatchingDialog() {
3307+
if (!_historyAutoMatchDialogVisible) {
3308+
return;
3309+
}
3310+
if (!mounted) {
3311+
_historyAutoMatchDialogVisible = false;
3312+
return;
3313+
}
3314+
Navigator.of(context, rootNavigator: true).pop();
3315+
}
3316+
32383317
// 导航到媒体库-库管理页面
32393318
void _navigateToMediaLibraryManagement() {
32403319
debugPrint('[DashboardHomePage] 准备导航到媒体库-库管理页面');

lib/pages/media_server_detail_page.dart

Lines changed: 147 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:nipaplay/services/emby_service.dart';
77
import 'package:nipaplay/models/watch_history_model.dart';
88
import 'package:nipaplay/themes/nipaplay/widgets/cached_network_image_widget.dart';
99
import 'package:nipaplay/themes/nipaplay/widgets/blur_snackbar.dart';
10+
import 'package:nipaplay/themes/nipaplay/widgets/blur_dialog.dart';
1011
import 'package:kmbal_ionicons/kmbal_ionicons.dart';
1112
import 'package:nipaplay/themes/nipaplay/widgets/switchable_view.dart';
1213
import 'package:provider/provider.dart';
@@ -93,6 +94,10 @@ class _MediaServerDetailPageState extends State<MediaServerDetailPage> with Sing
9394
String? _error;
9495
bool _isMovie = false; // 新增状态,判断是否为电影
9596

97+
bool _isDetailAutoMatching = false;
98+
bool _detailAutoMatchDialogVisible = false;
99+
bool _detailAutoMatchCancelled = false;
100+
96101
TabController? _tabController;
97102

98103
// 辅助方法:获取演员头像URL
@@ -357,35 +362,36 @@ class _MediaServerDetailPageState extends State<MediaServerDetailPage> with Sing
357362

358363
Future<void> _playMovie() async {
359364
if (_mediaDetail == null || !_isMovie) return;
365+
if (_isDetailAutoMatching) {
366+
BlurSnackBar.show(context, '正在自动匹配,请稍候');
367+
return;
368+
}
360369

361370
try {
362-
dynamic matcher;
363-
dynamic playableItem;
364-
365-
if (widget.serverType == MediaServerType.jellyfin) {
366-
// 将MediaItemDetail转换为JellyfinMovieInfo
367-
final movieInfo = JellyfinMovieInfo(
368-
id: _mediaDetail!.id,
369-
name: _mediaDetail!.name,
370-
overview: _mediaDetail!.overview,
371-
originalTitle: _mediaDetail!.originalTitle,
372-
imagePrimaryTag: _mediaDetail!.imagePrimaryTag,
373-
imageBackdropTag: _mediaDetail!.imageBackdropTag,
374-
productionYear: _mediaDetail!.productionYear,
375-
dateAdded: _mediaDetail!.dateAdded,
376-
premiereDate: _mediaDetail!.premiereDate,
377-
communityRating: _mediaDetail!.communityRating,
378-
genres: _mediaDetail!.genres,
379-
officialRating: _mediaDetail!.officialRating,
380-
cast: _mediaDetail!.cast,
381-
directors: _mediaDetail!.directors,
382-
runTimeTicks: _mediaDetail!.runTimeTicks,
383-
studio: _mediaDetail!.seriesStudio,
384-
);
385-
matcher = JellyfinDandanplayMatcher.instance;
386-
playableItem = await matcher.createPlayableHistoryItemFromMovie(context, movieInfo);
387-
} else {
388-
// 将MediaItemDetail转换为EmbyMovieInfo
371+
final playableItem = await _runDetailAutoMatchTask<WatchHistoryItem?>(() async {
372+
if (widget.serverType == MediaServerType.jellyfin) {
373+
final movieInfo = JellyfinMovieInfo(
374+
id: _mediaDetail!.id,
375+
name: _mediaDetail!.name,
376+
overview: _mediaDetail!.overview,
377+
originalTitle: _mediaDetail!.originalTitle,
378+
imagePrimaryTag: _mediaDetail!.imagePrimaryTag,
379+
imageBackdropTag: _mediaDetail!.imageBackdropTag,
380+
productionYear: _mediaDetail!.productionYear,
381+
dateAdded: _mediaDetail!.dateAdded,
382+
premiereDate: _mediaDetail!.premiereDate,
383+
communityRating: _mediaDetail!.communityRating,
384+
genres: _mediaDetail!.genres,
385+
officialRating: _mediaDetail!.officialRating,
386+
cast: _mediaDetail!.cast,
387+
directors: _mediaDetail!.directors,
388+
runTimeTicks: _mediaDetail!.runTimeTicks,
389+
studio: _mediaDetail!.seriesStudio,
390+
);
391+
return JellyfinDandanplayMatcher.instance
392+
.createPlayableHistoryItemFromMovie(context, movieInfo);
393+
}
394+
389395
final movieInfo = EmbyMovieInfo(
390396
id: _mediaDetail!.id,
391397
name: _mediaDetail!.name,
@@ -404,19 +410,21 @@ class _MediaServerDetailPageState extends State<MediaServerDetailPage> with Sing
404410
runTimeTicks: _mediaDetail!.runTimeTicks,
405411
studio: _mediaDetail!.seriesStudio,
406412
);
407-
matcher = EmbyDandanplayMatcher.instance;
408-
playableItem = await matcher.createPlayableHistoryItemFromMovie(context, movieInfo);
413+
return EmbyDandanplayMatcher.instance
414+
.createPlayableHistoryItemFromMovie(context, movieInfo);
415+
});
416+
417+
if (playableItem == null) {
418+
if (!_detailAutoMatchCancelled && mounted) {
419+
BlurSnackBar.show(context, '未能找到匹配的弹幕信息,但仍可播放。');
420+
final basicItem = _mediaDetail!.toWatchHistoryItem();
421+
Navigator.of(context).pop(basicItem);
422+
}
423+
return;
409424
}
410-
411-
if (playableItem == null) return; // 用户取消,彻底中断
425+
412426
if (mounted) {
413427
Navigator.of(context).pop(playableItem);
414-
} else if (mounted) {
415-
// 如果匹配失败,可以给用户一个提示
416-
BlurSnackBar.show(context, '未能找到匹配的弹幕信息,但仍可播放。');
417-
// 即使没有弹幕,也创建一个基本的播放项
418-
final basicItem = _mediaDetail!.toWatchHistoryItem();
419-
Navigator.of(context).pop(basicItem);
420428
}
421429
} catch (e) {
422430
if (mounted) {
@@ -441,6 +449,95 @@ class _MediaServerDetailPageState extends State<MediaServerDetailPage> with Sing
441449
}
442450
}
443451

452+
Future<T?> _runDetailAutoMatchTask<T>(Future<T?> Function() task) async {
453+
if (_isDetailAutoMatching) {
454+
if (mounted) {
455+
BlurSnackBar.show(context, '正在自动匹配,请稍候');
456+
}
457+
return null;
458+
}
459+
460+
_updateDetailAutoMatchingState(true);
461+
_detailAutoMatchCancelled = false;
462+
_showDetailAutoMatchingDialog();
463+
464+
try {
465+
final result = await task();
466+
if (_detailAutoMatchCancelled) {
467+
if (mounted) {
468+
BlurSnackBar.show(context, '已取消自动匹配');
469+
}
470+
return null;
471+
}
472+
return result;
473+
} finally {
474+
_hideDetailAutoMatchingDialog();
475+
_updateDetailAutoMatchingState(false);
476+
}
477+
}
478+
479+
void _updateDetailAutoMatchingState(bool value) {
480+
if (!mounted) {
481+
_isDetailAutoMatching = value;
482+
return;
483+
}
484+
if (_isDetailAutoMatching == value) {
485+
return;
486+
}
487+
setState(() {
488+
_isDetailAutoMatching = value;
489+
});
490+
}
491+
492+
void _showDetailAutoMatchingDialog() {
493+
if (_detailAutoMatchDialogVisible || !mounted) {
494+
return;
495+
}
496+
_detailAutoMatchDialogVisible = true;
497+
BlurDialog.show(
498+
context: context,
499+
title: '正在自动匹配',
500+
barrierDismissible: false,
501+
contentWidget: Column(
502+
mainAxisSize: MainAxisSize.min,
503+
children: const [
504+
SizedBox(height: 8),
505+
CircularProgressIndicator(
506+
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
507+
),
508+
SizedBox(height: 16),
509+
Text(
510+
'正在为当前条目匹配弹幕,请稍候…',
511+
style: TextStyle(color: Colors.white, fontSize: 14),
512+
textAlign: TextAlign.center,
513+
),
514+
],
515+
),
516+
actions: [
517+
TextButton(
518+
onPressed: () {
519+
_detailAutoMatchCancelled = true;
520+
Navigator.of(context, rootNavigator: true).pop();
521+
},
522+
child: const Text('中断匹配', style: TextStyle(color: Colors.redAccent)),
523+
),
524+
],
525+
).whenComplete(() {
526+
_detailAutoMatchDialogVisible = false;
527+
});
528+
}
529+
530+
void _hideDetailAutoMatchingDialog() {
531+
if (!_detailAutoMatchDialogVisible) {
532+
return;
533+
}
534+
if (!mounted) {
535+
_detailAutoMatchDialogVisible = false;
536+
return;
537+
}
538+
Navigator.of(context, rootNavigator: true).pop();
539+
}
540+
444541
@override
445542
Widget build(BuildContext context) {
446543
Widget pageContent;
@@ -1059,7 +1156,13 @@ style: TextStyle(
10591156
BlurButton(
10601157
icon: Icons.play_arrow,
10611158
text: '播放',
1062-
onTap: _playMovie,
1159+
onTap: () {
1160+
if (_isDetailAutoMatching) {
1161+
BlurSnackBar.show(context, '正在自动匹配,请稍候');
1162+
return;
1163+
}
1164+
_playMovie();
1165+
},
10631166
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
10641167
fontSize: 18,
10651168
),
@@ -1199,6 +1302,7 @@ style: TextStyle(color: Colors.white70)));
11991302

12001303
return ListTile(
12011304
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
1305+
enabled: !_isDetailAutoMatching,
12021306
leading: SizedBox(
12031307
width: 100, // 调整图片宽度
12041308
height: 60, // 调整图片高度,保持宽高比
@@ -1270,6 +1374,10 @@ style: TextStyle(fontSize: 12, color: Colors.grey[400]), // 调整颜色
12701374
),
12711375
trailing: const Icon(Ionicons.play_circle_outline, color: Colors.white70, size: 22), // 添加播放按钮指示
12721376
onTap: () async {
1377+
if (_isDetailAutoMatching) {
1378+
BlurSnackBar.show(context, '正在自动匹配,请稍候');
1379+
return;
1380+
}
12731381
try {
12741382
BlurSnackBar.show(context, '准备播放: ${episode.name}');
12751383

@@ -1289,7 +1397,7 @@ style: TextStyle(fontSize: 12, color: Colors.grey[400]), // 调整颜色
12891397

12901398
// 使用JellyfinDandanplayMatcher创建增强的WatchHistoryItem
12911399
// 这一步会显示匹配对话框,阻塞直到用户完成选择或跳过
1292-
final historyItem = await _createWatchHistoryItem(episode);
1400+
final historyItem = await _runDetailAutoMatchTask<WatchHistoryItem?>(() => _createWatchHistoryItem(episode));
12931401
if (historyItem == null) return; // 用户关闭弹窗,什么都不做
12941402

12951403
// 用户已完成匹配选择,现在可以继续播放流程

0 commit comments

Comments
 (0)