Files
PiliPlus/lib/pages/main/view.dart
dom e892411092 opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-07-30 15:01:52 +08:00

551 lines
16 KiB
Dart

import 'dart:io';
import 'package:PiliPlus/common/assets.dart';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/style.dart';
import 'package:PiliPlus/common/widgets/floating_navigation_bar.dart';
import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/main_layout.dart';
import 'package:PiliPlus/common/widgets/route_aware_mixin.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/android/android_helper.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/mobile_observer.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_key.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:win32/win32.dart' as kernel32;
import 'package:window_manager/window_manager.dart';
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends PopScopeState<MainApp>
with
RouteAware,
RouteAwareMixin,
WidgetsBindingObserver,
WindowListener,
TrayListener {
final _mainController = Get.put(MainController());
late final _setting = GStorage.setting;
late EdgeInsets _padding;
late ColorScheme _colorScheme;
Brightness? _brightness;
@override
bool get initCanPop => false;
@override
void initState() {
super.initState();
addObserverMobile(this);
if (PlatformUtils.isDesktop) {
windowManager
..addListener(this)
..setPreventClose(true);
if (_mainController.showTrayIcon) {
trayManager.addListener(this);
_handleTray();
}
} else {
// FlutterSmartDialog throws
PiliScheme.init();
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_padding = MediaQuery.viewPaddingOf(context);
_colorScheme = ColorScheme.of(context);
final brightness = _colorScheme.brightness;
NetworkImgLayer.reduce =
NetworkImgLayer.reduceLuxColor != null && brightness.isDark;
if (PlatformUtils.isDesktop) {
if (_brightness != brightness) {
_brightness = brightness;
windowManager.setBrightness(brightness);
}
}
if (!_mainController.useSideBar) {
_mainController.useBottomNav = MediaQuery.sizeOf(context).isPortrait;
}
}
@override
void didPopNext() {
addObserverMobile(this);
_mainController
..checkUnreadDynamic()
..checkDefaultSearch(true)
..checkUnread(_mainController.useBottomNav);
super.didPopNext();
}
@override
void didPushNext() {
removeObserverMobile(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);
}
removeObserverMobile(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<void> onWindowMoved() async {
if (PlPlayerController.instance?.isDesktopPip ?? false) {
return;
}
final Offset offset = await windowManager.getPosition();
_setting.put(SettingBoxKey.windowPosition, [offset.dx, offset.dy]);
}
@override
Future<void> 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<void> _onClose() async {
await GStorage.compact();
await GStorage.close();
await trayManager.destroy();
if (Platform.isWindows) {
// flutter_inappwebview
// 6.2.0-beta.2+ https://github.com/pichillilorenzo/flutter_inappwebview/issues/2482
// 6.1.5 https://github.com/pichillilorenzo/flutter_inappwebview/issues/2512#issuecomment-3031039587
final hProcess = kernel32.GetCurrentProcess();
kernel32.TerminateProcess(hProcess, 0);
} 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<void> onTrayIconMouseDown() async {
if (await windowManager.isVisible()) {
_onHideWindow();
windowManager.hide();
} else {
_onShowWindow();
windowManager.show();
}
}
@override
Future<void> 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<void> _handleTray() async {
if (Platform.isWindows) {
await trayManager.setIcon(Assets.logoIco);
} else {
await trayManager.setIcon(Assets.logoLarge);
}
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);
}
@pragma('vm:prefer-inline')
static void _onBack() {
if (Platform.isAndroid) {
PiliAndroidHelper.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;
if (_mainController.navigationBars.length > 1) {
if (_mainController.floatingNavBar) {
bottomNav = Obx(
() => FloatingNavigationBar(
onDestinationSelected: _mainController.setIndex,
selectedIndex: _mainController.selectedIndex.value,
destinations: _mainController.navigationBars
.map(
(e) => FloatingNavigationDestination(
label: e.label,
icon: _buildIcon(type: e),
selectedIcon: _buildIcon(type: e, selected: true),
),
)
.toList(),
),
);
} else if (_mainController.enableMYBar) {
bottomNav = 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(),
),
);
} else {
bottomNav = 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(),
),
);
}
if (_mainController.hideBottomBar) {
if (_mainController.barOffset case final barOffset?) {
return Obx(
() => FractionalTranslation(
translation: Offset(
0.0,
barOffset.value / Style.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() {
if (_mainController.navigationBars.length > 1) {
if (context.isTablet && _mainController.optTabletNav) {
return Padding(
padding: const .only(top: 25),
child: MediaQuery.removePadding(
context: context,
removeRight: true,
child: DrawerTheme(
data: DrawerThemeData(width: 130 + _padding.left),
child: Obx(
() => NavigationDrawer(
/// apply `lib/scripts/navigation_drawer.patch`
flex: 5,
backgroundColor: Colors.transparent,
onDestinationSelected: _mainController.setIndex,
selectedIndex: _mainController.selectedIndex.value,
header: Expanded(flex: 4, child: userAndSearchVertical()),
tilePadding: const .symmetric(vertical: 5, horizontal: 12),
indicatorShape: const RoundedRectangleBorder(
borderRadius: .all(.circular(16)),
),
children: _mainController.navigationBars
.map(
(e) => NavigationDrawerDestination(
label: Text(e.label),
icon: _buildIcon(type: e),
selectedIcon: _buildIcon(
type: e,
selected: true,
),
),
)
.toList(),
),
),
),
),
);
}
return Obx(
() => NavigationRail(
groupAlignment: 0.5,
labelType: .selected,
leading: userAndSearchVertical(),
backgroundColor: Colors.transparent,
onDestinationSelected: _mainController.setIndex,
selectedIndex: _mainController.selectedIndex.value,
destinations: _mainController.navigationBars
.map(
(e) => NavigationRailDestination(
label: Text(e.label),
icon: _buildIcon(type: e),
selectedIcon: _buildIcon(type: e, selected: true),
),
)
.toList(),
),
);
}
return Container(
width: 80,
margin: .only(top: 12 + _padding.top, left: _padding.left),
child: userAndSearchVertical(),
);
}
@override
Widget build(BuildContext context) {
Widget child;
if (_mainController.mainTabBarView) {
child = TabBarView(
controller: _mainController.controller,
physics: const NeverScrollableScrollPhysics(),
/// apply `lib/scripts/tabs.patch`
scrollDirection: _mainController.useBottomNav ? .horizontal : .vertical,
children: _mainController.navigationBars.map((i) => i.page).toList(),
);
} else {
child = PageView(
controller: _mainController.controller,
physics: const NeverScrollableScrollPhysics(),
children: _mainController.navigationBars.map((i) => i.page).toList(),
);
}
Widget? sideBar;
Widget? bottomNav;
final EdgeInsets padding;
if (_mainController.useBottomNav) {
bottomNav = _bottomNav;
if (bottomNav != null) {
bottomNav = MediaQuery.removePadding(
context: context,
removeTop: true,
child: bottomNav,
);
}
padding = .only(
top: _padding.top,
left: _padding.left,
right: _padding.right,
);
} else {
sideBar = DecoratedBox(
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: _colorScheme.outline.withValues(alpha: 0.06),
),
),
),
child: _sideBar(),
);
padding = .only(top: _padding.top, right: _padding.right);
}
child = Material(
child: MainLayout(
sideBar: sideBar,
bottomNav: bottomNav,
body: Padding(padding: padding, child: child),
),
);
if (PlatformUtils.isMobile) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarBrightness: _colorScheme.brightness,
statusBarIconBrightness: _colorScheme.brightness.reverse,
systemStatusBarContrastEnforced: false,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness: _colorScheme.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() {
return Column(
children: [
userAvatar(colorScheme: _colorScheme, mainController: _mainController),
const SizedBox(height: 8),
msgBadge(_mainController),
IconButton(
tooltip: '搜索',
icon: const Icon(
Icons.search_outlined,
semanticLabel: '搜索',
),
onPressed: () => Get.toNamed('/search'),
),
],
);
}
}