Skip to content

Commit 2573670

Browse files
committed
现在支持在外观设置调节是否显示番剧卡片的简介。现在媒体库界面支持了显示远程访问快捷入口。现在支持在播放器菜单里导出弹幕文件。让程序实现单进程以防止多开软件。现在库管理内对文件夹进行操作之后不会显示为空文件夹了。细节调整。2026.0129
1 parent 97f9543 commit 2573670

24 files changed

+1262
-186
lines changed

lib/main.dart

Lines changed: 75 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import 'package:nipaplay/utils/theme_notifier.dart';
1313
import 'package:nipaplay/utils/system_resource_monitor.dart';
1414
import 'package:nipaplay/themes/nipaplay/widgets/custom_scaffold.dart';
1515
import 'package:nipaplay/themes/nipaplay/widgets/menu_button.dart';
16-
import 'package:nipaplay/themes/nipaplay/widgets/nipaplay_window.dart';
1716
import 'package:nipaplay/themes/nipaplay/widgets/system_resource_display.dart';
1817
import 'package:window_manager/window_manager.dart';
1918
import 'package:provider/provider.dart';
@@ -53,6 +52,7 @@ import 'package:nipaplay/utils/storage_service.dart';
5352
import 'package:permission_handler/permission_handler.dart';
5453
import 'package:nipaplay/services/debug_log_service.dart';
5554
import 'package:nipaplay/services/file_association_service.dart';
55+
import 'package:nipaplay/services/single_instance_service.dart';
5656
import 'package:nipaplay/danmaku_abstraction/danmaku_kernel_factory.dart';
5757
import 'package:nipaplay/themes/nipaplay/widgets/splash_screen.dart';
5858
import 'package:shared_preferences/shared_preferences.dart';
@@ -75,6 +75,7 @@ import 'package:nipaplay/models/anime_detail_display_mode.dart';
7575
import 'package:nipaplay/models/background_image_render_mode.dart';
7676
import 'constants/settings_keys.dart';
7777
import 'player_abstraction/media_kit_player_adapter.dart';
78+
import 'utils/launch_file_handler.dart';
7879
import 'package:nipaplay/services/desktop_exit_handler_stub.dart'
7980
if (dart.library.io) 'package:nipaplay/services/desktop_exit_handler.dart';
8081

@@ -110,6 +111,23 @@ void main(List<String> args) async {
110111
return;
111112
}
112113

114+
String? launchFilePath;
115+
if (globals.isDesktop && args.isNotEmpty) {
116+
final filePath = args.first;
117+
if (await File(filePath).exists()) {
118+
launchFilePath = filePath;
119+
}
120+
}
121+
122+
if (globals.isDesktop) {
123+
final isPrimary = await SingleInstanceService.ensureSingleInstance(
124+
launchFilePath: launchFilePath,
125+
);
126+
if (!isPrimary) {
127+
return;
128+
}
129+
}
130+
113131
WatchHistoryDatabase.ensureInitialized();
114132

115133
// 安装 HTTP 客户端覆盖(自签名证书信任规则),尽早生效
@@ -124,17 +142,9 @@ void main(List<String> args) async {
124142
final debugLogService = DebugLogService();
125143
debugLogService.initialize();
126144

127-
// 检查是否有文件路径参数传入
128-
String? launchFilePath;
129-
130-
// 桌面平台通过命令行参数传入
131-
if (!kIsWeb && args.isNotEmpty && globals.isDesktop) {
132-
final filePath = args.first;
133-
if (await File(filePath).exists()) {
134-
launchFilePath = filePath;
135-
debugLogService.addLog('应用启动时收到命令行文件路径: $filePath',
136-
level: 'INFO', tag: 'FileAssociation');
137-
}
145+
if (launchFilePath != null) {
146+
debugLogService.addLog('应用启动时收到命令行文件路径: $launchFilePath',
147+
level: 'INFO', tag: 'FileAssociation');
138148
}
139149

140150
// Android平台通过Intent传入
@@ -394,7 +404,7 @@ void main(List<String> args) async {
394404
if (uiThemeProvider.isCupertinoTheme) {
395405
_navigateToPage(context, 3); // Cupertino 主题保留底部设置页
396406
} else {
397-
_showSettingsWindow(context); // Nipaplay 主题使用弹窗设置页
407+
SettingsPage.showWindow(context); // Nipaplay 主题使用弹窗设置页
398408
}
399409
});
400410

@@ -658,6 +668,11 @@ class _NipaPlayAppState extends State<NipaPlayApp> {
658668
super.initState();
659669
// 启动后设置WatchHistoryProvider监听ScanService
660670
WidgetsBinding.instance.addPostFrameCallback((_) {
671+
if (globals.isDesktop) {
672+
SingleInstanceService.registerMessageHandler(
673+
_handleSingleInstanceMessage,
674+
);
675+
}
661676
DesktopExitHandler.instance.initialize(navigatorKey);
662677

663678
// 调试:启动时打印数据库内容
@@ -685,6 +700,44 @@ class _NipaPlayAppState extends State<NipaPlayApp> {
685700
});
686701
}
687702

703+
Future<void> _handleSingleInstanceMessage(
704+
SingleInstanceMessage message,
705+
) async {
706+
if (!mounted) {
707+
return;
708+
}
709+
if (message.focus) {
710+
await _focusMainWindow();
711+
}
712+
final filePath = message.filePath;
713+
if (filePath != null) {
714+
await LaunchFileHandler.handle(
715+
filePath,
716+
onError: (error) {
717+
if (mounted) {
718+
BlurSnackBar.show(context, error);
719+
}
720+
},
721+
);
722+
}
723+
}
724+
725+
Future<void> _focusMainWindow() async {
726+
if (!globals.isDesktop) {
727+
return;
728+
}
729+
try {
730+
final isMinimized = await windowManager.isMinimized();
731+
if (isMinimized) {
732+
await windowManager.restore();
733+
}
734+
await windowManager.show();
735+
await windowManager.focus();
736+
} catch (e) {
737+
debugPrint('[SingleInstance] 唤起窗口失败: $e');
738+
}
739+
}
740+
688741
@override
689742
Widget build(BuildContext context) {
690743
return DropTarget(
@@ -1000,37 +1053,14 @@ class MainPageState extends State<MainPage>
10001053

10011054
// 处理启动文件
10021055
Future<void> _handleLaunchFile(String filePath) async {
1003-
try {
1004-
debugPrint('[FileAssociation] 处理启动文件: $filePath');
1005-
1006-
// 检查是否存在历史记录
1007-
WatchHistoryItem? historyItem =
1008-
await WatchHistoryManager.getHistoryItem(filePath);
1009-
1010-
historyItem ??= WatchHistoryItem(
1011-
filePath: filePath,
1012-
animeName: path.basenameWithoutExtension(filePath),
1013-
watchProgress: 0,
1014-
lastPosition: 0,
1015-
duration: 0,
1016-
lastWatchTime: DateTime.now(),
1017-
);
1018-
1019-
final playableItem = PlayableItem(
1020-
videoPath: filePath,
1021-
title: historyItem.animeName,
1022-
historyItem: historyItem,
1023-
);
1024-
1025-
await PlaybackService().play(playableItem);
1026-
1027-
debugPrint('[FileAssociation] 启动文件已提交给PlaybackService');
1028-
} catch (e) {
1029-
debugPrint('[FileAssociation] 启动文件播放失败: $e');
1030-
if (mounted) {
1031-
BlurSnackBar.show(context, '无法播放启动文件: $e');
1032-
}
1033-
}
1056+
await LaunchFileHandler.handle(
1057+
filePath,
1058+
onError: (error) {
1059+
if (mounted) {
1060+
BlurSnackBar.show(context, error);
1061+
}
1062+
},
1063+
);
10341064
}
10351065

10361066
// 检查窗口是否已最大化
@@ -1246,7 +1276,7 @@ class MainPageState extends State<MainPage>
12461276
height: kWindowCaptionHeight,
12471277
child: Center(
12481278
child: _SettingsEntryButton(
1249-
onPressed: () => _showSettingsWindow(context),
1279+
onPressed: () => SettingsPage.showWindow(context),
12501280
),
12511281
),
12521282
),
@@ -1523,48 +1553,3 @@ void _navigateToPage(BuildContext context, int pageIndex) {
15231553
'[Dart - _navigateToPage] 备选方案: 使用TabChangeNotifier请求切换到标签页$pageIndex');
15241554
}
15251555
}
1526-
1527-
void _showSettingsWindow(BuildContext context) {
1528-
final appearanceSettings =
1529-
Provider.of<AppearanceSettingsProvider>(context, listen: false);
1530-
final enableAnimation = appearanceSettings.enablePageAnimation;
1531-
final screenSize = MediaQuery.of(context).size;
1532-
final isCompactLayout = screenSize.width < 900;
1533-
final maxWidth = isCompactLayout ? screenSize.width * 0.95 : 980.0;
1534-
final maxHeightFactor = isCompactLayout ? 0.9 : 0.85;
1535-
1536-
NipaplayWindow.show(
1537-
context: context,
1538-
enableAnimation: enableAnimation,
1539-
child: NipaplayWindowScaffold(
1540-
maxWidth: maxWidth,
1541-
maxHeightFactor: maxHeightFactor,
1542-
onClose: () => Navigator.of(context).pop(),
1543-
child: Column(
1544-
crossAxisAlignment: CrossAxisAlignment.start,
1545-
children: [
1546-
Builder(
1547-
builder: (innerContext) {
1548-
final titleStyle = Theme.of(innerContext)
1549-
.textTheme
1550-
.titleLarge
1551-
?.copyWith(fontWeight: FontWeight.bold);
1552-
return GestureDetector(
1553-
behavior: HitTestBehavior.opaque,
1554-
onPanUpdate: (details) {
1555-
NipaplayWindowPositionProvider.of(innerContext)
1556-
?.onMove(details.delta);
1557-
},
1558-
child: Padding(
1559-
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
1560-
child: Text('设置', style: titleStyle),
1561-
),
1562-
);
1563-
},
1564-
),
1565-
const Expanded(child: SettingsPage()),
1566-
],
1567-
),
1568-
),
1569-
);
1570-
}

lib/pages/anime_page.dart

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import 'package:nipaplay/models/playable_item.dart';
4040
import 'package:nipaplay/providers/dandanplay_remote_provider.dart';
4141
import 'package:nipaplay/pages/tab_labels.dart';
4242
import 'package:flutter_svg/flutter_svg.dart';
43+
import 'package:nipaplay/themes/nipaplay/pages/settings_page.dart';
44+
import 'package:nipaplay/utils/globals.dart' as globals;
4345

4446
// Custom ScrollBehavior for NoScrollbarBehavior is removed as NestedScrollView handles scrolling differently.
4547

@@ -274,6 +276,7 @@ class _MediaLibraryTabsState extends State<_MediaLibraryTabs> with TickerProvide
274276

275277
// 添加变量追踪“添加媒体服务器”入口的悬停状态
276278
bool _isAddEntryHovered = false;
279+
bool _isRemoteAccessHovered = false;
277280

278281
// 动态计算标签页数量
279282
int get _tabCount {
@@ -555,6 +558,54 @@ class _MediaLibraryTabsState extends State<_MediaLibraryTabs> with TickerProvide
555558
}
556559
}
557560

561+
void _openRemoteAccessSettings() {
562+
SettingsPage.showWindow(
563+
context,
564+
initialEntryId: SettingsPage.entryRemoteAccess,
565+
);
566+
}
567+
568+
Widget _buildRemoteAccessEntry({
569+
required Color iconColor,
570+
required Color textColor,
571+
}) {
572+
const Color hoverColor = Color(0xFFFF2E55);
573+
final Color displayColor = _isRemoteAccessHovered ? hoverColor : textColor;
574+
575+
return Padding(
576+
padding: const EdgeInsets.only(bottom: 12.0),
577+
child: MouseRegion(
578+
cursor: SystemMouseCursors.click,
579+
onEnter: (_) => setState(() => _isRemoteAccessHovered = true),
580+
onExit: (_) => setState(() => _isRemoteAccessHovered = false),
581+
child: GestureDetector(
582+
onTap: _openRemoteAccessSettings,
583+
child: AnimatedScale(
584+
scale: _isRemoteAccessHovered ? 1.1 : 1.0,
585+
duration: const Duration(milliseconds: 200),
586+
curve: Curves.easeOut,
587+
child: Row(
588+
mainAxisSize: MainAxisSize.min,
589+
children: [
590+
Icon(Icons.link, size: 18, color: displayColor),
591+
const SizedBox(width: 6),
592+
Text(
593+
'远程访问',
594+
locale: const Locale("zh-Hans", "zh"),
595+
style: TextStyle(
596+
color: displayColor,
597+
fontSize: 18,
598+
fontWeight: FontWeight.bold,
599+
),
600+
),
601+
],
602+
),
603+
),
604+
),
605+
),
606+
);
607+
}
608+
558609
Widget _buildAddMediaServerEntry({
559610
required Color iconColor,
560611
required Color textColor,
@@ -920,6 +971,13 @@ class _MediaLibraryTabsState extends State<_MediaLibraryTabs> with TickerProvide
920971
),
921972
),
922973
const SizedBox(width: 8),
974+
if (!globals.isPhone) ...[
975+
_buildRemoteAccessEntry(
976+
iconColor: activeColor,
977+
textColor: unselectedLabelColor,
978+
),
979+
const SizedBox(width: 12),
980+
],
923981
_buildAddMediaServerEntry(
924982
iconColor: activeColor,
925983
textColor: unselectedLabelColor,

lib/pages/media_library_page.dart

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import 'package:nipaplay/themes/nipaplay/widgets/horizontal_anime_card.dart';
2525
import 'package:nipaplay/themes/nipaplay/widgets/local_library_control_bar.dart';
2626
import 'package:nipaplay/themes/nipaplay/widgets/smb_connection_dialog.dart';
2727
import 'package:nipaplay/themes/nipaplay/widgets/webdav_connection_dialog.dart';
28+
import 'package:nipaplay/providers/appearance_settings_provider.dart';
2829
import 'dart:ui' as ui;
2930

3031
// Define a callback type for when an episode is selected for playing
@@ -697,9 +698,17 @@ style: TextStyle(color: Colors.grey, fontSize: 16),
697698
radius: const Radius.circular(2),
698699
child: GridView.builder(
699700
controller: _gridScrollController,
700-
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
701-
maxCrossAxisExtent: 500,
702-
mainAxisExtent: 140,
701+
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
702+
maxCrossAxisExtent: context
703+
.watch<AppearanceSettingsProvider>()
704+
.showAnimeCardSummary
705+
? HorizontalAnimeCard.detailedGridMaxCrossAxisExtent
706+
: HorizontalAnimeCard.compactGridMaxCrossAxisExtent,
707+
mainAxisExtent: context
708+
.watch<AppearanceSettingsProvider>()
709+
.showAnimeCardSummary
710+
? HorizontalAnimeCard.detailedCardHeight
711+
: HorizontalAnimeCard.compactCardHeight,
703712
mainAxisSpacing: 16,
704713
crossAxisSpacing: 16,
705714
),

lib/pages/new_series_page.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,19 @@ class _NewSeriesPageState extends State<NewSeriesPage> with AutomaticKeepAliveCl
180180
}
181181

182182
SliverPadding _buildAnimeGridSliver(List<BangumiAnime> animes, int weekdayKey) {
183+
final showSummary =
184+
context.watch<AppearanceSettingsProvider>().showAnimeCardSummary;
183185
return SliverPadding(
184186
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
185187
sliver: SliverGrid(
186188
key: ValueKey<String>('sliver_grid_for_weekday_$weekdayKey'),
187-
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
188-
maxCrossAxisExtent: 500,
189-
mainAxisExtent: 140,
189+
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
190+
maxCrossAxisExtent: showSummary
191+
? HorizontalAnimeCard.detailedGridMaxCrossAxisExtent
192+
: HorizontalAnimeCard.compactGridMaxCrossAxisExtent,
193+
mainAxisExtent: showSummary
194+
? HorizontalAnimeCard.detailedCardHeight
195+
: HorizontalAnimeCard.compactCardHeight,
190196
mainAxisSpacing: 16,
191197
crossAxisSpacing: 16,
192198
),

0 commit comments

Comments
 (0)