You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
7.9 KiB
Dart
266 lines
7.9 KiB
Dart
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<SliderDrawerState>();
|
|
|
|
var pageTitle = "".obs;
|
|
var pageId = 0.obs;
|
|
|
|
var pageInfo = Rx<PageInfo?>(null);
|
|
var parseInfo = Rx<MWParseInfo?>(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<void> loadPageContent() async {
|
|
if (pageTitle.isNotEmpty || pageId.value != 0) {
|
|
try {
|
|
// 加载页面信息
|
|
loading.value = true;
|
|
MWResponse<List<PageInfo>> 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<StatefulWidget> createState() => _WikiViewPageState();
|
|
}
|
|
|
|
class _WikiViewPageState extends ReactiveState<WikiViewPage> {
|
|
var c = WikiViewPageController();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
Get.put(c);
|
|
|
|
c.loadPageContent();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
|
|
Get.delete<WikiViewPageController>();
|
|
}
|
|
|
|
@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<FavoriteListController>();
|
|
|
|
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;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|