@@ -13,7 +13,6 @@ import 'package:nipaplay/utils/theme_notifier.dart';
1313import 'package:nipaplay/utils/system_resource_monitor.dart' ;
1414import 'package:nipaplay/themes/nipaplay/widgets/custom_scaffold.dart' ;
1515import 'package:nipaplay/themes/nipaplay/widgets/menu_button.dart' ;
16- import 'package:nipaplay/themes/nipaplay/widgets/nipaplay_window.dart' ;
1716import 'package:nipaplay/themes/nipaplay/widgets/system_resource_display.dart' ;
1817import 'package:window_manager/window_manager.dart' ;
1918import 'package:provider/provider.dart' ;
@@ -53,6 +52,7 @@ import 'package:nipaplay/utils/storage_service.dart';
5352import 'package:permission_handler/permission_handler.dart' ;
5453import 'package:nipaplay/services/debug_log_service.dart' ;
5554import 'package:nipaplay/services/file_association_service.dart' ;
55+ import 'package:nipaplay/services/single_instance_service.dart' ;
5656import 'package:nipaplay/danmaku_abstraction/danmaku_kernel_factory.dart' ;
5757import 'package:nipaplay/themes/nipaplay/widgets/splash_screen.dart' ;
5858import 'package:shared_preferences/shared_preferences.dart' ;
@@ -75,6 +75,7 @@ import 'package:nipaplay/models/anime_detail_display_mode.dart';
7575import 'package:nipaplay/models/background_image_render_mode.dart' ;
7676import 'constants/settings_keys.dart' ;
7777import 'player_abstraction/media_kit_player_adapter.dart' ;
78+ import 'utils/launch_file_handler.dart' ;
7879import '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- }
0 commit comments