import 'dart:io'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; import 'package:PiliPlus/common/widgets/flutter/tabs.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/nav_bar_config.dart'; import 'package:PiliPlus/pages/home/view.dart'; import 'package:PiliPlus/pages/main/controller.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:window_manager/window_manager.dart'; class MainApp extends StatefulWidget { const MainApp({super.key}); @override State createState() => _MainAppState(); } class _MainAppState extends PopScopeState with RouteAware, WidgetsBindingObserver, WindowListener, TrayListener { final _mainController = Get.put(MainController()); late final _setting = GStorage.setting; late EdgeInsets _padding; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); if (PlatformUtils.isDesktop) { windowManager ..addListener(this) ..setPreventClose(true); if (_mainController.showTrayIcon) { trayManager.addListener(this); _handleTray(); } } } @override void didChangeDependencies() { super.didChangeDependencies(); _padding = MediaQuery.viewPaddingOf(context); final brightness = Theme.brightnessOf(context); NetworkImgLayer.reduce = NetworkImgLayer.reduceLuxColor != null && brightness.isDark; if (PlatformUtils.isDesktop) { windowManager.setBrightness(brightness); } PageUtils.routeObserver.subscribe( this, ModalRoute.of(context) as PageRoute, ); if (!_mainController.useSideBar) { _mainController.useBottomNav = MediaQuery.sizeOf(context).isPortrait; } } @override void didPopNext() { WidgetsBinding.instance.addObserver(this); _mainController ..checkUnreadDynamic() ..checkDefaultSearch(true) ..checkUnread(_mainController.useBottomNav); super.didPopNext(); } @override void didPushNext() { WidgetsBinding.instance.removeObserver(this); super.didPushNext(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _mainController ..checkUnreadDynamic() ..checkDefaultSearch(true) ..checkUnread(_mainController.useBottomNav); } } @override void dispose() { if (PlatformUtils.isDesktop) { trayManager.removeListener(this); windowManager.removeListener(this); } PageUtils.routeObserver.unsubscribe(this); WidgetsBinding.instance.removeObserver(this); PiliScheme.listener?.cancel(); GStorage.close(); super.dispose(); } @override void onWindowMaximize() { _setting.put(SettingBoxKey.isWindowMaximized, true); } @override void onWindowUnmaximize() { _setting.put(SettingBoxKey.isWindowMaximized, false); } @override Future onWindowMoved() async { if (PlPlayerController.instance?.isDesktopPip ?? false) { return; } final Offset offset = await windowManager.getPosition(); _setting.put(SettingBoxKey.windowPosition, [offset.dx, offset.dy]); } @override Future onWindowResized() async { if (PlPlayerController.instance?.isDesktopPip ?? false) { return; } final Rect bounds = await windowManager.getBounds(); _setting.putAll({ SettingBoxKey.windowSize: [bounds.width, bounds.height], SettingBoxKey.windowPosition: [bounds.left, bounds.top], }); } @override void onWindowClose() { if (_mainController.showTrayIcon && _mainController.minimizeOnExit) { windowManager.hide(); _onHideWindow(); } else { _onClose(); } } Future _onClose() async { await GStorage.compact(); await GStorage.close(); await trayManager.destroy(); if (Platform.isWindows) { const MethodChannel('window_control').invokeMethod('closeWindow'); } else { exit(0); } } @override void onWindowMinimize() { _onHideWindow(); } @override void onWindowRestore() { _onShowWindow(); } void _onHideWindow() { if (_mainController.pauseOnMinimize) { if (PlPlayerController.instance case final player?) { if (_mainController.isPlaying = player.playerStatus.isPlaying) { player.pause(); } } else { _mainController.isPlaying = false; } } } void _onShowWindow() { if (_mainController.pauseOnMinimize && _mainController.isPlaying) { PlPlayerController.instance?.play(); } } @override Future onTrayIconMouseDown() async { if (await windowManager.isVisible()) { _onHideWindow(); windowManager.hide(); } else { _onShowWindow(); windowManager.show(); } } @override Future onTrayIconRightMouseDown() async { // ignore: deprecated_member_use trayManager.popUpContextMenu(bringAppToFront: true); } @override void onTrayMenuItemClick(MenuItem menuItem) { switch (menuItem.key) { case 'show': windowManager.show(); case 'exit': _onClose(); } } Future _handleTray() async { if (Platform.isWindows) { await trayManager.setIcon('assets/images/logo/ico/app_icon.ico'); } else { await trayManager.setIcon('assets/images/logo/desktop/logo_large.png'); } if (!Platform.isLinux) { await trayManager.setToolTip(Constants.appName); } Menu trayMenu = Menu( items: [ MenuItem(key: 'show', label: '显示窗口'), MenuItem.separator(), MenuItem(key: 'exit', label: '退出 ${Constants.appName}'), ], ); await trayManager.setContextMenu(trayMenu); } static void _onBack() { if (Platform.isAndroid) { Utils.channel.invokeMethod('back'); } } @override void onPopInvokedWithResult(bool didPop, Object? result) { if (_mainController.directExitOnBack) { _onBack(); } else { if (_mainController.selectedIndex.value != 0) { _mainController ..setIndex(0) ..barOffset?.value = 0.0 ..showBottomBar?.value = true ..setSearchBar(); } else { _onBack(); } } } Widget? get _bottomNav { Widget? bottomNav = _mainController.navigationBars.length > 1 ? _mainController.enableMYBar ? Obx( () => NavigationBar( maintainBottomViewPadding: true, onDestinationSelected: _mainController.setIndex, selectedIndex: _mainController.selectedIndex.value, destinations: _mainController.navigationBars .map( (e) => NavigationDestination( label: e.label, icon: _buildIcon(type: e), selectedIcon: _buildIcon(type: e, selected: true), ), ) .toList(), ), ) : Obx( () => BottomNavigationBar( currentIndex: _mainController.selectedIndex.value, onTap: _mainController.setIndex, iconSize: 16, selectedFontSize: 12, unselectedFontSize: 12, type: .fixed, items: _mainController.navigationBars .map( (e) => BottomNavigationBarItem( label: e.label, icon: _buildIcon(type: e), activeIcon: _buildIcon(type: e, selected: true), ), ) .toList(), ), ) : null; if (bottomNav != null && _mainController.hideBottomBar) { if (_mainController.barOffset case final barOffset?) { return Obx( () => FractionalTranslation( translation: Offset( 0.0, barOffset.value / StyleString.topBarHeight, ), child: bottomNav, ), ); } if (_mainController.showBottomBar case final showBottomBar?) { return Obx( () => AnimatedSlide( curve: Curves.easeInOutCubicEmphasized, duration: const Duration(milliseconds: 500), offset: Offset(0, showBottomBar.value ? 0 : 1), child: bottomNav, ), ); } } return bottomNav; } Widget _sideBar(ThemeData theme) { return _mainController.navigationBars.length > 1 ? context.isTablet && _mainController.optTabletNav ? Column( children: [ const SizedBox(height: 25), userAndSearchVertical(theme), const Spacer(flex: 2), Expanded( flex: 5, child: SizedBox( width: 130, child: Obx( () => NavigationDrawer( backgroundColor: Colors.transparent, tilePadding: const .symmetric( vertical: 5, horizontal: 12, ), indicatorShape: const RoundedRectangleBorder( borderRadius: .all(.circular(16)), ), onDestinationSelected: _mainController.setIndex, selectedIndex: _mainController.selectedIndex.value, children: _mainController.navigationBars .map( (e) => NavigationDrawerDestination( label: Text(e.label), icon: _buildIcon(type: e), selectedIcon: _buildIcon( type: e, selected: true, ), ), ) .toList(), ), ), ), ), ], ) : Obx( () => NavigationRail( groupAlignment: 0.5, selectedIndex: _mainController.selectedIndex.value, onDestinationSelected: _mainController.setIndex, labelType: .selected, leading: userAndSearchVertical(theme), destinations: _mainController.navigationBars .map( (e) => NavigationRailDestination( label: Text(e.label), icon: _buildIcon(type: e), selectedIcon: _buildIcon(type: e, selected: true), ), ) .toList(), ), ) : Container( width: 80, padding: const .only(top: 10), child: userAndSearchVertical(theme), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); Widget child; if (_mainController.mainTabBarView) { child = CustomTabBarView( scrollDirection: _mainController.useBottomNav ? .horizontal : .vertical, physics: const NeverScrollableScrollPhysics(), controller: _mainController.controller, children: _mainController.navigationBars.map((i) => i.page).toList(), ); } else { child = PageView( physics: const NeverScrollableScrollPhysics(), controller: _mainController.controller, children: _mainController.navigationBars.map((i) => i.page).toList(), ); } Widget? bottomNav; if (_mainController.useBottomNav) { bottomNav = _bottomNav; child = Row(children: [Expanded(child: child)]); } else { child = Row( children: [ _sideBar(theme), VerticalDivider( width: 1, endIndent: _padding.bottom, color: theme.colorScheme.outline.withValues(alpha: 0.06), ), Expanded(child: child), ], ); } child = Scaffold( extendBody: true, resizeToAvoidBottomInset: false, appBar: AppBar(toolbarHeight: 0), body: Padding( padding: EdgeInsets.only( left: _mainController.useBottomNav ? _padding.left : 0.0, right: _padding.right, ), child: child, ), bottomNavigationBar: bottomNav, ); if (PlatformUtils.isMobile) { child = AnnotatedRegion( value: SystemUiOverlayStyle( systemNavigationBarColor: Colors.transparent, systemNavigationBarIconBrightness: theme.brightness.reverse, ), child: child, ); } return child; } Widget _buildIcon({required NavigationBarType type, bool selected = false}) { final icon = selected ? type.selectIcon : type.icon; return type == .dynamics ? Obx( () { final dynCount = _mainController.dynCount.value; return Badge( isLabelVisible: dynCount > 0, label: _mainController.dynamicBadgeMode == .number ? Text(dynCount.toString()) : null, padding: const .symmetric(horizontal: 6), child: icon, ); }, ) : icon; } Widget userAndSearchVertical(ThemeData theme) { return Column( children: [ userAvatar(theme: theme, mainController: _mainController), const SizedBox(height: 8), msgBadge(_mainController), IconButton( tooltip: '搜索', icon: const Icon( Icons.search_outlined, semanticLabel: '搜索', ), onPressed: () => Get.toNamed('/search'), ), ], ); } }