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,243 @@
import 'dart:async';
import 'package:flutter_dmzj/app/app_constant.dart';
import 'package:flutter_dmzj/app/controller/base_controller.dart';
import 'package:flutter_dmzj/app/event_bus.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_dmzj/app/utils.dart';
import 'package:flutter_dmzj/models/db/novel_history.dart';
import 'package:flutter_dmzj/models/novel/novel_detail_model.dart';
import 'package:flutter_dmzj/requests/novel_request.dart';
import 'package:flutter_dmzj/requests/user_request.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:flutter_dmzj/services/db_service.dart';
import 'package:flutter_dmzj/services/user_service.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class NovelDetailControler extends BaseController {
final int novelId;
NovelDetailControler(this.novelId);
final NovelRequest request = NovelRequest();
final UserRequest userRequest = UserRequest();
Rx<NovelDetailInfo> detail = Rx<NovelDetailInfo>(NovelDetailInfo.empty());
var expandDescription = false.obs;
/// 是否已订阅
var subscribeStatus = false.obs;
/// 阅读记录
Rx<NovelHistory?> history = Rx<NovelHistory?>(null);
/// 更新小说记录
StreamSubscription<dynamic>? updateNovelSubscription;
@override
void onInit() {
updateNovelSubscription = EventBus.instance.listen(
EventBus.kUpdatedNovelHistory,
(id) {
if (id == novelId) {
getHistory();
}
},
);
// 从本地读取订阅状态
subscribeStatus.value =
UserService.instance.subscribedNovelIds.contains(novelId);
getHistory();
loadDetail();
loadSubscribeStatus();
//updateSubscribeRead();
super.onInit();
}
void refreshDetail() {
getHistory();
loadDetail();
loadSubscribeStatus();
}
/// 更新订阅的阅读状态
void updateSubscribeRead() {
try {
userRequest.subscribeRead(id: novelId, type: AppConstant.kTypeNovel);
} catch (e) {
Log.logPrint(e);
}
}
@override
void onClose() {
updateNovelSubscription?.cancel();
super.onClose();
}
void getHistory() {
var novelHistory = DBService.instance.getNovelHistory(novelId);
if (novelHistory != null) {
history.value = novelHistory;
history.update((val) {});
}
}
/// 加载信息
void loadDetail() async {
try {
pageLoadding.value = true;
pageError.value = false;
var result = await request.novelDetail(novelId: novelId);
detail.value = NovelDetailInfo.fromJson(result.data);
await loadChapter();
} catch (e) {
pageError.value = true;
errorMsg.value = e.toString();
} finally {
pageLoadding.value = false;
}
}
Future loadChapter() async {
try {
var result = await request.novelChapter(novelId: novelId);
detail.value.volume.value =
result.map((e) => NovelDetailVolume.fromJson(e)).toList();
} catch (e) {
SmartDialog.showToast("无法读取小说章节:$e");
}
}
/// 检查订阅状态
void loadSubscribeStatus() async {
try {
var result = await userRequest.checkSubscribeStatus(
objId: novelId,
type: AppConstant.kTypeNovel,
);
subscribeStatus.value = result;
if (subscribeStatus.value) {
UserService.instance.subscribedNovelIds.add(novelId);
} else {
UserService.instance.subscribedNovelIds.remove(novelId);
}
} catch (e) {
Log.logPrint(e);
}
}
/// 查看评论
void comment() {
AppNavigator.toComment(objId: novelId, type: AppConstant.kTypeNovel);
}
/// 分享
void share() {
Utils.share(
"http://q.idmzj.com/$novelId/index.shtml",
content: detail.value.name,
);
}
/// 订阅
void subscribe() async {
var result = await (subscribeStatus.value
? UserService.instance
.cancelSubscribe([novelId], AppConstant.kTypeNovel)
: UserService.instance.addSubscribe([novelId], AppConstant.kTypeNovel));
if (result) {
subscribeStatus.value = !subscribeStatus.value;
}
}
/// 下载
void download() {
AppNavigator.toNovelDownloadSelect(novelId);
}
/// 开始/继续阅读
void read() {
if (detail.value.volume.isEmpty) {
SmartDialog.showToast("没有可阅读的章节");
return;
}
if (detail.value.volume.first.chapters.isEmpty) {
SmartDialog.showToast("没有可阅读的章节");
return;
}
//查找记录
if (history.value != null && history.value!.chapterId != 0) {
NovelDetailChapter? chapter;
for (var volumeItem in detail.value.volume) {
var chapterItem = volumeItem.chapters.firstWhereOrNull(
(x) => x.chapterId == history.value!.chapterId,
);
if (chapterItem != null) {
chapter = chapterItem;
break;
}
}
if (chapter != null) {
List<NovelDetailChapter> chapters = [];
for (var volume in detail.value.volume) {
chapters.addAll(volume.chapters);
}
AppNavigator.toNovelReader(
novelId: novelId,
novelCover: detail.value.cover,
novelTitle: detail.value.name,
chapter: chapter,
chapters: chapters,
);
} else {
SmartDialog.showToast("未找到历史记录对应章节,将从头开始阅读");
readStart();
}
} else {
readStart();
}
}
void readStart() {
//从头开始
List<NovelDetailChapter> chapters = [];
for (var volume in detail.value.volume) {
chapters.addAll(volume.chapters);
}
var chapter = chapters.first;
AppNavigator.toNovelReader(
novelId: novelId,
novelCover: detail.value.cover,
novelTitle: detail.value.name,
chapter: chapter,
chapters: chapters,
);
}
void readChapter(NovelDetailVolume volume, NovelDetailChapter item) {
List<NovelDetailChapter> chapters = [];
for (var volume in detail.value.volume) {
chapters.addAll(volume.chapters);
}
AppNavigator.toNovelReader(
novelId: novelId,
novelCover: detail.value.cover,
novelTitle: detail.value.name,
chapters: chapters,
chapter: item,
);
}
void toAuthorDetail(String e) {
AppNavigator.toNovelSearch(keyword: e);
}
void toCategoryDetail(String e) {
AppNavigator.toNovelSearch(keyword: e);
}
}

View File

@@ -0,0 +1,361 @@
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_color.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/app/utils.dart';
import 'package:flutter_dmzj/modules/novel/detail/novel_detail_controller.dart';
import 'package:flutter_dmzj/widgets/net_image.dart';
import 'package:flutter_dmzj/widgets/status/app_error_widget.dart';
import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart';
import 'package:get/get.dart';
import 'package:remixicon/remixicon.dart';
class NovelDetailPage extends StatelessWidget {
final int id;
final NovelDetailControler controller;
NovelDetailPage(this.id, {super.key})
: controller = Get.put(
NovelDetailControler(id),
tag: DateTime.now().millisecondsSinceEpoch.toString(),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Obx(
() => Text(
controller.detail.value.name.isEmpty
? "小说详情"
: controller.detail.value.name,
),
),
actions: [
IconButton(
onPressed: controller.share,
icon: const Icon(Icons.share),
),
],
),
body: Stack(
children: [
Obx(
() => Offstage(
offstage: controller.detail.value.novelId == 0,
child: EasyRefresh(
header: const MaterialHeader(),
onRefresh: controller.refreshDetail,
child: ListView(
padding: AppStyle.edgeInsetsA12,
children: [
_buildHeader(),
Obx(
() => Offstage(
offstage: controller.history.value == null,
child: Column(
children: [
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
"上次看到:${controller.history.value?.volumeName ?? ""} ${controller.history.value?.chapterName ?? ""}",
style: Get.textTheme.titleSmall,
),
trailing: const Icon(Icons.chevron_right),
onTap: () {
controller.read();
},
),
Divider(
color: Colors.grey.withOpacity(.2),
height: 1.0,
),
],
),
),
),
_buildChapter(),
],
),
),
),
),
Obx(
() => Offstage(
offstage: !controller.pageLoadding.value,
child: const AppLoaddingWidget(),
),
),
Obx(
() => Offstage(
offstage: !controller.pageError.value,
child: AppErrorWidget(
errorMsg: controller.errorMsg.value,
onRefresh: () => controller.loadDetail(),
),
),
),
],
),
floatingActionButton: FloatingActionButton(
elevation: 2,
onPressed: controller.read,
child: const Icon(Icons.play_circle_outline_rounded),
),
bottomNavigationBar: BottomAppBar(
child: SizedBox(
height: 48,
child: Row(
children: [
Expanded(
child: Obx(
() => TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.subscribe,
icon: Icon(
controller.subscribeStatus.value
? Remix.heart_fill
: Remix.heart_line,
size: 20,
),
label: Text(controller.subscribeStatus.value ? "取消" : "订阅"),
),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.comment,
icon: const Icon(
Remix.chat_2_line,
size: 20,
),
label: const Text("评论"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.download,
icon: const Icon(
Remix.download_line,
size: 20,
),
label: const Text("下载"),
),
),
],
),
),
),
);
}
Widget _buildHeader() {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//信息
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
NetImage(
controller.detail.value.cover,
width: 120,
height: 160,
borderRadius: 4,
),
AppStyle.hGap12,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
controller.detail.value.name,
style: Get.textTheme.titleMedium,
),
AppStyle.vGap8,
_buildInfoItems(
iconData: Remix.user_smile_line,
children: controller.detail.value.authors
.split("/")
.map(
(e) => GestureDetector(
onTap: () => controller.toAuthorDetail(e),
child: Text(
e,
style: TextStyle(
fontSize: 14,
height: 1.2,
decoration: TextDecoration.underline,
color: Get.isDarkMode
? Colors.white
: AppColor.black333,
),
),
),
)
.toList(),
),
_buildInfo(
title:
controller.detail.value.types.map((e) => e).join("/"),
iconData: Remix.hashtag,
),
_buildInfo(
title: "人气 ${controller.detail.value.hotHits}",
iconData: Remix.fire_line,
),
_buildInfo(
title: "订阅 ${controller.detail.value.subscribeNum}",
iconData: Remix.heart_line,
),
_buildInfo(
title:
"${Utils.formatTimestampToDate(controller.detail.value.lastUpdateTime)} ${controller.detail.value.status}",
iconData: Icons.schedule,
),
],
),
),
],
),
AppStyle.vGap12,
GestureDetector(
onTap: () {
controller.expandDescription.value =
!controller.expandDescription.value;
},
child: Text(
controller.detail.value.introduction,
style: const TextStyle(
color: Colors.grey,
fontSize: 14,
),
maxLines: controller.expandDescription.value ? 999 : 2,
overflow: TextOverflow.ellipsis,
),
),
AppStyle.vGap12,
Divider(
color: Colors.grey.withOpacity(.2),
height: 1.0,
),
],
);
}
Widget _buildChapter() {
return Obx(
() => Column(
children: controller.detail.value.volume
.map(
(item) => ExpansionTile(
title: Text(
"${item.volumeName}(共${item.chapters.length}章)",
style: Get.textTheme.titleSmall,
),
tilePadding: AppStyle.edgeInsetsH4,
children: [
ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemCount: item.chapters.length,
separatorBuilder: (_, i) => const Divider(
height: 1,
),
itemBuilder: (context, i) {
var chapter = item.chapters[i];
return ListTile(
title: Text(
chapter.chapterName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Get.textTheme.bodyMedium!.copyWith(
color: controller.history.value?.chapterId ==
chapter.chapterId
? Get.theme.colorScheme.primary
: null,
),
),
contentPadding: AppStyle.edgeInsetsA4,
visualDensity: const VisualDensity(
vertical: VisualDensity.minimumDensity),
onTap: () {
controller.readChapter(item, chapter);
},
);
},
),
],
),
)
.toList(),
),
);
}
Widget _buildInfo({
required String title,
IconData iconData = Icons.tag,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
iconData,
color: Colors.grey,
size: 16,
),
AppStyle.hGap8,
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 14,
color: Get.isDarkMode ? Colors.white : AppColor.black333,
),
),
),
],
),
);
}
Widget _buildInfoItems({
required List<Widget> children,
IconData iconData = Icons.tag,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
iconData,
color: Colors.grey,
size: 16,
),
AppStyle.hGap8,
Expanded(
child: Wrap(
spacing: 8,
children: children,
),
),
],
),
);
}
}