import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_slider_drawer/flutter_slider_drawer.dart'; import 'package:get/get.dart'; import 'package:isekai_wiki/api/mw/list.dart'; import 'package:isekai_wiki/api/mw/mw_api.dart'; import 'package:isekai_wiki/api/mw/parse.dart'; import 'package:isekai_wiki/api/response/page_info.dart'; import 'package:isekai_wiki/api/response/parse.dart'; import 'package:isekai_wiki/components/bottom_nav_bar.dart'; import 'package:isekai_wiki/components/nav_bar_button.dart'; import 'package:isekai_wiki/components/safearea_builder.dart'; import 'package:isekai_wiki/components/toc.dart'; import 'package:isekai_wiki/components/wikipage_parser.dart'; import 'package:isekai_wiki/components/isekai_nav_bar.dart'; import 'package:isekai_wiki/components/isekai_page_scaffold.dart'; import 'package:isekai_wiki/global.dart'; import 'package:isekai_wiki/models/favorite_list.dart'; import 'package:isekai_wiki/reactive/reactive.dart'; import 'package:isekai_wiki/utils/dialog.dart'; import 'package:isekai_wiki/utils/error.dart'; import 'package:isekai_wiki/utils/utils.dart'; import 'package:share_plus/share_plus.dart'; class MinimumArticleData { final String title; final String? description; final String? mainCategory; final DateTime? updateTime; MinimumArticleData({required this.title, this.description, this.mainCategory, this.updateTime}); } class ArticleCategoryData { final String name; final String identity; ArticleCategoryData({required this.name, required this.identity}); } class WikiViewPageController extends GetxController { WikiPageParserController? parserController; var loading = true.obs; var menuSlider = GlobalKey(); var pageTitle = "".obs; var pageId = 0.obs; var pageInfo = Rx(null); var parseInfo = Rx(null); var displayTitle = "".obs; var activedSection = "_firstSection".obs; bool get hasFirstSection { if (parseInfo.value == null) return false; if (parseInfo.value!.sections.isEmpty) return false; return parseInfo.value!.sections.first.byteoffset == 0; } @override void onInit() { super.onInit(); } Future loadPageContent() async { if (pageTitle.isNotEmpty || pageId.value != 0) { try { // 加载页面信息 loading.value = true; MWResponse> pageInfoRes; if (pageId.value != 0) { pageInfoRes = await MWApiList.getPageInfoList(pageids: [pageId.value]); } else { pageInfoRes = await MWApiList.getPageInfoList(titles: [pageTitle.value]); } if (pageInfoRes.data.isEmpty) { throw MWApiErrorException(code: 'no-page', info: "页面信息丢失"); } pageInfo.value = pageInfoRes.data[0]; displayTitle.value = pageInfo.value!.mainTitle; pageId.value = pageInfo.value!.pageid; pageTitle.value = pageInfo.value!.title; // 获取页面HTML var parseRes = await MWApiParse.parse(pageId: pageId.value); parseInfo.value = parseRes.data; } catch (err, stack) { alert(Get.overlayContext!, ErrorUtils.getErrorMessage(err), title: "错误"); if (kDebugMode) { print("Exception in page: $err"); stack.printError(); } loading.value = false; } } } void handleFlowButtonClick() { alert(Get.overlayContext!, "评论功能暂未完成"); } void handleShareButtonClick() { if (pageInfo.value != null) { var pageUrl = pageInfo.value!.fullurl ?? getPageUrl(pageInfo.value!.title); var shareTitle = "${pageInfo.value!.title} - ${Global.siteTitle}"; var shareText = "$shareTitle\n$pageUrl"; Share.share(shareText, subject: shareTitle); } else { alert(Get.overlayContext!, "页面还未加载完成"); } } void handleTOCButtonClick() { menuSlider.currentState?.openSlider(); } void handleSwitchTitle(String anchor) { menuSlider.currentState?.closeSlider(); Future.delayed(const Duration(milliseconds: 360)).then((_) { activedSection.value = anchor; }); parserController?.scrollToAnchor(anchor); } void handleSectionChange(String anchor) { activedSection.value = anchor; } } class WikiViewPage extends StatefulWidget { final MinimumArticleData? initialArticleData; final String? targetPage; final int? targetPageId; const WikiViewPage({super.key, this.targetPage, this.targetPageId, this.initialArticleData}); @override State createState() => _WikiViewPageState(); } class _WikiViewPageState extends ReactiveState { var c = WikiViewPageController(); @override void initState() { super.initState(); Get.put(c); c.loadPageContent(); } @override void dispose() { super.dispose(); Get.delete(); } @override void receiveProps() { c.pageTitle.value = widget.targetPage ?? ""; c.pageId.value = widget.targetPageId ?? 0; c.displayTitle.value = widget.initialArticleData?.title ?? "加载中"; } Widget renderTOC(BuildContext context) { return Obx( () => TOCList( sections: c.parseInfo.value?.sections ?? const [], activedItem: c.activedSection.value, hasFirstSection: c.hasFirstSection, onSwitchTitle: c.handleSwitchTitle, ), ); } @override Widget render(BuildContext context) { final flc = Get.find(); return SliderDrawer( key: c.menuSlider, isCupertino: true, appBar: null, showCover: true, slideDirection: SlideDirection.RIGHT_TO_LEFT, slider: renderTOC(context), animationDuration: 350, child: IsekaiPageScaffold( navigationBar: IsekaiNavigationBar( middle: Obx(() => Text(c.displayTitle.value)), ), bottomNavigationBar: BottomNavigationBar( child: Padding( padding: const EdgeInsets.all(0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ NavBarButton( icon: CupertinoIcons.chat_bubble_2, onPressed: c.handleFlowButtonClick, semanticLabel: "讨论", ), Obx(() { if (c.pageInfo.value != null && flc.isFavorite(c.pageInfo.value!)) { return NavBarButton( icon: CupertinoIcons.heart_fill, onPressed: () {}, semanticLabel: "取消收藏", ); } else { return NavBarButton( icon: CupertinoIcons.heart, onPressed: () {}, semanticLabel: "收藏", ); } }), NavBarButton( icon: CupertinoIcons.share, onPressed: c.handleShareButtonClick, semanticLabel: "分享", ), NavBarButton( icon: CupertinoIcons.textformat_alt, onPressed: c.handleFlowButtonClick, semanticLabel: "阅读设置", ), NavBarButton( icon: CupertinoIcons.list_bullet, onPressed: c.handleTOCButtonClick, semanticLabel: "目录", ), ], ), ), ), child: SafeAreaBuilder( builder: (context, padding) => Obx( () => WikiPageParser( padding: padding, pageInfo: c.pageInfo.value, parseInfo: c.parseInfo.value, onSectionChange: c.handleSectionChange, ref: (controller) { c.parserController = controller; }, ), ), ), ), ); } }