This commit is contained in:
2026-03-07 17:24:59 +08:00
parent 4418ebecac
commit b0ec8ab4bd
417 changed files with 42546 additions and 2 deletions

View File

@@ -0,0 +1,100 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/platform_utils.dart';
import 'package:flutter_dmzj/services/app_settings_service.dart';
import 'package:flutter_dmzj/app/dialog_utils.dart';
import 'package:flutter_dmzj/app/event_bus.dart';
import 'package:flutter_dmzj/app/utils.dart';
import 'package:flutter_dmzj/modules/comic/home/comic_home_page.dart';
import 'package:flutter_dmzj/modules/news/home/news_home_controller.dart';
import 'package:flutter_dmzj/modules/news/home/news_home_page.dart';
import 'package:flutter_dmzj/modules/novel/home/novel_home_controller.dart';
import 'package:flutter_dmzj/modules/novel/home/novel_home_page.dart';
import 'package:flutter_dmzj/modules/user/user_home_page.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:multi_split_view/multi_split_view.dart';
class IndexController extends GetxController {
final index = 0.obs;
final showContent = false.obs;
final GlobalKey indexKey = GlobalKey();
final GlobalKey subRouterKey = GlobalKey();
final MultiSplitViewController multiSplitViewController =
MultiSplitViewController(areas: [
Area(minimalSize: 400, size: 500),
]);
/// 双击退出Flag
bool doubleClickExit = false;
/// 双击退出Timer
Timer? doubleClickTimer;
final pages = [
const ComicHomePage(),
const SizedBox(),
const SizedBox(),
const UserHomePage(),
];
@override
void onInit() {
if (PlatformUtils.isWindows) {
// Windows: 预先初始化所有分区控制器确保NavigationView所有PaneItem.body可用
if (!Get.isRegistered<NewsHomeController>()) {
Get.put(NewsHomeController());
}
if (!Get.isRegistered<NovelHomeController>()) {
Get.put(NovelHomeController());
}
pages[1] = const NewsHomePage();
pages[2] = const NovelHomePage();
}
Future.delayed(Duration.zero, showFirstRun);
super.onInit();
}
@override
void onClose() {}
void setIndex(i) {
if (i == 1 && pages[i] is SizedBox) {
Get.put(NewsHomeController());
pages[i] = const NewsHomePage();
} else if (i == 2 && pages[i] is SizedBox) {
Get.put(NovelHomeController());
pages[i] = const NovelHomePage();
}
if (index.value == i) {
EventBus.instance.emit<int>(EventBus.kBottomNavigationBarClicked, i);
}
index.value = i;
}
void showFirstRun() async {
if (AppSettingsService.instance.firstRun) {
AppSettingsService.instance.setNoFirstRun();
DialogUtils.showStatement();
Utils.checkUpdate();
} else {
Utils.checkUpdate();
}
}
void setDoubleExitFlag() {
if (doubleClickExit) {
doubleClickTimer?.cancel();
Get.back();
return;
}
doubleClickExit = true;
SmartDialog.showToast("再按一次退出应用");
doubleClickTimer = Timer(const Duration(seconds: 2), () {
doubleClickExit = false;
doubleClickTimer!.cancel();
});
}
}

View File

@@ -0,0 +1,223 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/app/platform_utils.dart';
import 'package:flutter_dmzj/modules/common/empty_page.dart';
import 'package:flutter_dmzj/modules/index/index_controller.dart';
import 'package:flutter_dmzj/modules/index/windows_index_page.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:flutter_dmzj/routes/app_pages.dart';
import 'package:get/get.dart';
import 'package:remixicon/remixicon.dart';
class IndexPage extends GetView<IndexController> {
const IndexPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// Windows平台使用Fluent UI的NavigationView
if (PlatformUtils.isWindows) {
return const WindowsIndexPage();
}
final content = _buildContentNavigator();
final indexStack = _buildIndexStack();
return OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.landscape
? _buildWide(context, indexStack, content)
: _buildNarrow(context, indexStack, content);
},
);
}
Widget _buildNarrow(BuildContext context, Widget indexStack, Widget content) {
return Stack(
children: [
Obx(
() => Scaffold(
body: indexStack,
bottomNavigationBar: NavigationBar(
selectedIndex: controller.index.value,
onDestinationSelected: controller.setIndex,
destinations: const [
NavigationDestination(
icon: Icon(Remix.bear_smile_line),
selectedIcon: Icon(Remix.bear_smile_fill),
label: "漫画",
),
NavigationDestination(
icon: Icon(Remix.article_line),
selectedIcon: Icon(Remix.article_fill),
label: "资讯",
),
NavigationDestination(
icon: Icon(Remix.book_open_line),
selectedIcon: Icon(Remix.book_open_fill),
label: "轻小说",
),
NavigationDestination(
icon: Icon(Remix.user_smile_line),
selectedIcon: Icon(Remix.user_smile_fill),
label: "我的",
),
],
),
),
),
Obx(
() => IgnorePointer(
ignoring: !controller.showContent.value,
child: content,
),
)
],
);
}
Widget _buildWide(BuildContext context, Widget indexStack, Widget content) {
return Scaffold(
body: Row(
children: [
Obx(
() => Padding(
padding: const EdgeInsets.only(right: 2),
child: NavigationRail(
elevation: 2,
labelType: NavigationRailLabelType.all,
onDestinationSelected: controller.setIndex,
selectedIndex: controller.index.value,
leading: SizedBox(
height: AppStyle.statusBarHeight,
),
selectedLabelTextStyle: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.secondary,
),
unselectedLabelTextStyle: TextStyle(
fontSize: 10,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
destinations: const [
NavigationRailDestination(
icon: Icon(Remix.bear_smile_line),
label: Text("漫画"),
),
NavigationRailDestination(
icon: Icon(Remix.article_line),
label: Text("资讯"),
),
NavigationRailDestination(
icon: Icon(Remix.book_open_line),
label: Text("轻小说"),
),
NavigationRailDestination(
icon: Icon(Remix.user_smile_line),
label: Text("我的"),
),
],
),
),
),
Container(
// constraints: const BoxConstraints(maxWidth: 450),
width: 450,
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Colors.grey.withOpacity(.1),
),
),
),
child: indexStack,
),
Expanded(
child: content,
),
],
),
);
}
Widget _buildIndexStack() {
return Obx(
() => IndexedStack(
key: controller.indexKey,
index: controller.index.value,
children: controller.pages,
),
);
}
/// 子路由
Widget _buildContentNavigator() {
/// 拦截子路由的返回
return PopScope(
canPop: false,
onPopInvoked: (didPop) {
if (!didPop) {
if (Navigator.canPop(Get.context!)) {
Get.back();
return;
} else if (AppNavigator.subNavigatorKey!.currentState!.canPop()) {
AppNavigator.subNavigatorKey!.currentState!.pop();
return;
}
if (controller.doubleClickExit) {
controller.doubleClickTimer?.cancel();
SystemNavigator.pop();
return;
}
controller.setDoubleExitFlag();
}
},
// onWillPop: () async {
// if (Navigator.canPop(Get.context!)) {
// return true;
// }
// if (AppNavigator.subNavigatorKey!.currentState!.canPop()) {
// AppNavigator.subNavigatorKey!.currentState!.pop();
// return false;
// }
// return true;
// },
child: ClipRect(
child: Navigator(
key: AppNavigator.subNavigatorKey,
initialRoute: '/',
onUnknownRoute: (settings) => GetPageRoute(
page: () => const EmptyPage(),
),
observers: [
SubNavigatorObserver(),
],
onGenerateRoute: AppPages.generateSubRoute,
),
),
);
}
}
/// 子路由监听
class SubNavigatorObserver extends NavigatorObserver {
@override
void didPush(Route route, Route? previousRoute) {
super.didPush(route, previousRoute);
if (previousRoute != null) {
var routeName = route.settings.name ?? "";
AppNavigator.currentContentRouteName = routeName;
Get.find<IndexController>().showContent.value = routeName != '/';
}
}
@override
void didPop(Route route, Route? previousRoute) {
super.didPop(route, previousRoute);
var routeName = previousRoute?.settings.name ?? "";
AppNavigator.currentContentRouteName = routeName;
Get.find<IndexController>().showContent.value = routeName != '/';
}
}

View File

@@ -0,0 +1,150 @@
import 'package:fluent_ui/fluent_ui.dart' as fluent;
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/platform_utils.dart';
import 'package:flutter_dmzj/modules/common/empty_page.dart';
import 'package:flutter_dmzj/modules/index/index_controller.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:flutter_dmzj/routes/app_pages.dart';
import 'package:get/get.dart';
import 'package:remixicon/remixicon.dart';
/// Windows平台专用导航页面 - 使用Fluent UI的NavigationView
class WindowsIndexPage extends GetView<IndexController> {
const WindowsIndexPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return fluent.FluentTheme(
data: PlatformUtils.getFluentTheme(context),
child: Obx(
() => fluent.NavigationView(
paneBodyBuilder: (item, body) {
// Builder ensures ctx is INSIDE the FluentTheme ancestor tree
return Builder(
builder: (ctx) => KeyedSubtree(
key: const ValueKey('windows_main_content'),
child: _buildMasterDetail(ctx),
),
);
},
pane: fluent.NavigationPane(
selected: controller.index.value,
onChanged: controller.setIndex,
displayMode: fluent.PaneDisplayMode.auto,
indicator: const fluent.StickyNavigationIndicator(),
items: [
fluent.PaneItem(
icon: const Icon(Remix.bear_smile_line),
title: const Text('漫画'),
body: const SizedBox.shrink(),
),
fluent.PaneItem(
icon: const Icon(Remix.article_line),
title: const Text('资讯'),
body: const SizedBox.shrink(),
),
fluent.PaneItem(
icon: const Icon(Remix.book_open_line),
title: const Text('轻小说'),
body: const SizedBox.shrink(),
),
],
footerItems: [
fluent.PaneItem(
icon: const Icon(Remix.user_smile_line),
title: const Text('我的'),
body: const SizedBox.shrink(),
),
],
),
),
),
);
}
/// 主内容区section列表(左) + 子路由详情(右)
/// 使用Material主题颜色避免FluentTheme.of()需要特定祖先
Widget _buildMasterDetail(BuildContext context) {
final materialTheme = Theme.of(context);
final isDark = materialTheme.brightness == Brightness.dark;
// 使用Material主题颜色衍生背景色
final scaffoldBg = materialTheme.scaffoldBackgroundColor;
final panelBg = isDark ? const Color(0xff202020) : const Color(0xfff0f0f0);
final dividerColor = materialTheme.dividerColor;
return ColoredBox(
color: scaffoldBg,
child: Row(
children: [
// 左侧各模块首页IndexedStack切换
SizedBox(
width: 450,
child: ColoredBox(
color: panelBg,
child: Obx(
() => IndexedStack(
key: controller.indexKey,
index: controller.index.value,
children: controller.pages,
),
),
),
),
// 分隔线
Container(width: 1, color: dividerColor),
// 右侧:子路由(详情页、阅读器等)
Expanded(
child: _buildContentNavigator(),
),
],
),
);
}
/// 子路由导航器(处理详情页、阅读器等)
Widget _buildContentNavigator() {
return PopScope(
canPop: false,
onPopInvoked: (didPop) {
if (!didPop) {
if (Navigator.canPop(Get.context!)) {
Get.back();
return;
}
if (AppNavigator.subNavigatorKey!.currentState!.canPop()) {
AppNavigator.subNavigatorKey!.currentState!.pop();
}
}
},
child: ClipRect(
child: Navigator(
key: AppNavigator.subNavigatorKey,
initialRoute: '/',
onUnknownRoute: (settings) => GetPageRoute(
page: () => const EmptyPage(),
),
observers: [WindowsSubNavigatorObserver()],
onGenerateRoute: AppPages.generateSubRoute,
),
),
);
}
}
/// Windows子路由监听不需要更新showContent因为采用固定master-detail布局
class WindowsSubNavigatorObserver extends NavigatorObserver {
@override
void didPush(Route route, Route? previousRoute) {
super.didPush(route, previousRoute);
if (previousRoute != null) {
final routeName = route.settings.name ?? '';
AppNavigator.currentContentRouteName = routeName;
}
}
@override
void didPop(Route route, Route? previousRoute) {
super.didPop(route, previousRoute);
final routeName = previousRoute?.settings.name ?? '';
AppNavigator.currentContentRouteName = routeName;
}
}