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,52 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_dmzj/app/controller/base_controller.dart';
import 'package:flutter_dmzj/models/comment/comment_item.dart';
import 'package:flutter_dmzj/requests/comment_request.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class AddCommentController extends BaseController {
final int type;
final int objId;
final CommentItem? replyItem;
AddCommentController({
required this.objId,
required this.type,
this.replyItem,
});
final CommentRequest request = CommentRequest();
final TextEditingController textEditingController = TextEditingController();
void submit() async {
if (textEditingController.text.isEmpty) {
SmartDialog.showToast("内容不能为空");
return;
}
try {
SmartDialog.showLoading();
if (replyItem == null) {
await request.sendComment(
objId: objId,
type: type,
content: textEditingController.text,
);
} else {
await request.sendComment(
objId: objId,
type: type,
content: textEditingController.text,
toCommentId: replyItem!.id.toString(),
originCommentId: replyItem!.originId.toString(),
toUid: replyItem!.userId.toString(),
);
}
SmartDialog.showToast("发表成功");
AppNavigator.closePage();
} catch (e) {
SmartDialog.showToast(e.toString());
} finally {
SmartDialog.dismiss(status: SmartStatus.loading);
}
}
}

View File

@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/models/comment/comment_item.dart';
import 'package:flutter_dmzj/modules/common/comment/add_comment_controller.dart';
import 'package:get/get.dart';
class AddCommentPage extends StatelessWidget {
final int type;
final int objId;
final CommentItem? replyItem;
final AddCommentController controller;
AddCommentPage({
Key? key,
required this.objId,
required this.type,
this.replyItem,
}) : controller = Get.put(
AddCommentController(objId: objId, type: type, replyItem: replyItem),
tag: DateTime.now().millisecondsSinceEpoch.toString(),
),
super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("添加评论"),
),
body: ListView(
padding: AppStyle.edgeInsetsA12,
children: [
Visibility(
visible: replyItem != null,
child: Container(
decoration: BoxDecoration(
color: Colors.grey.withOpacity(.2),
borderRadius: AppStyle.radius4,
),
margin: AppStyle.edgeInsetsB12,
padding: AppStyle.edgeInsetsA8,
child: Text("${replyItem?.nickname}${replyItem?.content}"),
),
),
TextField(
controller: controller.textEditingController,
decoration: const InputDecoration(
hintText: "你想说点什么...",
border: OutlineInputBorder(),
),
onSubmitted: (e) {
controller.submit();
},
minLines: 4,
maxLines: 6,
maxLength: 1000,
),
AppStyle.vGap12,
ElevatedButton(
onPressed: controller.submit,
child: const Text("发布"),
),
],
),
);
}
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class CommentController extends GetxController
with GetSingleTickerProviderStateMixin {
final int type;
final int objId;
CommentController(this.type, this.objId);
late TabController tabController = TabController(length: 2, vsync: this);
}

View File

@@ -0,0 +1,33 @@
import 'package:flutter_dmzj/app/controller/base_controller.dart';
import 'package:flutter_dmzj/models/comment/comment_item.dart';
import 'package:flutter_dmzj/requests/comment_request.dart';
class CommentListController extends BasePageController<CommentItem> {
final int type;
final int objId;
final bool isHot;
final CommentRequest request = CommentRequest();
CommentListController({
required this.type,
required this.objId,
required this.isHot,
});
@override
Future<List<CommentItem>> getData(int page, int pageSize) async {
if (isHot) {
return await request.getComment(
type: type,
objId: objId,
page: page,
sort: 2,
);
} else {
return await request.getComment(
type: type,
objId: objId,
page: page,
);
}
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/modules/common/comment/comment_list_controller.dart';
import 'package:flutter_dmzj/widgets/comment_item_widget.dart';
import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart';
import 'package:flutter_dmzj/widgets/page_list_view.dart';
import 'package:get/get.dart';
class CommentListView extends StatelessWidget {
final int type;
final int objId;
final bool isHot;
final CommentListController controller;
CommentListView({
Key? key,
required this.objId,
required this.type,
required this.isHot,
}) : controller = Get.put(
CommentListController(objId: objId, type: type, isHot: isHot),
tag: "${objId}_${type}_${isHot ? 1 : 0}",
),
super(key: key);
@override
Widget build(BuildContext context) {
return KeepAliveWrapper(
child: PageListView(
pageController: controller,
firstRefresh: true,
separatorBuilder: (context, i) => Divider(
endIndent: 12,
indent: 12,
color: Colors.grey.withOpacity(.2),
height: 4,
),
itemBuilder: (context, i) {
var item = controller.list[i];
return CommentItemWidget(item);
},
),
);
}
}

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/modules/common/comment/comment_list_view.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:get/get.dart';
class CommentPage extends StatelessWidget {
final int objId;
final int type;
const CommentPage({required this.objId, required this.type, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Container(
alignment: Alignment.center,
child: TabBar(
isScrollable: true,
labelPadding: AppStyle.edgeInsetsH24,
tabAlignment: TabAlignment.start,
indicatorSize: TabBarIndicatorSize.label,
indicatorColor: Theme.of(context).colorScheme.primary,
labelColor: Theme.of(context).colorScheme.primary,
unselectedLabelColor:
Get.isDarkMode ? Colors.white70 : Colors.black87,
tabs: const [
Tab(text: "最新评论"),
Tab(text: "热门评论"),
],
),
),
),
body: TabBarView(
children: [
CommentListView(objId: objId, type: type, isHot: false),
CommentListView(objId: objId, type: type, isHot: true),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
AppNavigator.toAddComment(objId: objId, type: type);
},
child: const Icon(Icons.add),
),
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/modules/common/download/comic/comic_downloaded_view.dart';
import 'package:flutter_dmzj/modules/common/download/comic/comic_downloading_view.dart';
import 'package:flutter_dmzj/services/comic_download_service.dart';
import 'package:get/get.dart';
class ComicDownloadPage extends StatelessWidget {
final int type;
const ComicDownloadPage(this.type, {super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
initialIndex: type,
child: Scaffold(
appBar: AppBar(
title: Container(
alignment: Alignment.center,
padding: const EdgeInsets.only(right: 56),
child: TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
indicatorSize: TabBarIndicatorSize.label,
indicatorColor: Theme.of(context).colorScheme.primary,
labelColor: Theme.of(context).colorScheme.primary,
unselectedLabelColor:
Get.isDarkMode ? Colors.white70 : Colors.black87,
tabs: [
const Tab(text: "已完成"),
Obx(
() => Tab(
text: ComicDownloadService.instance.taskQueues.isEmpty
? "下载中"
: "下载中(${ComicDownloadService.instance.taskQueues.length})"),
)
],
),
),
),
body: const TabBarView(
children: [
ComicDownloadedView(),
ComicDownloadingView(),
],
),
),
);
}
}

View File

@@ -0,0 +1,182 @@
import 'dart:async';
import 'package:flutter_dmzj/app/event_bus.dart';
import 'package:flutter_dmzj/models/comic/detail_info.dart';
import 'package:flutter_dmzj/models/db/comic_history.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:flutter_dmzj/services/comic_download_service.dart';
import 'package:flutter_dmzj/services/db_service.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class ComicDownloadedDetailController extends GetxController {
final ComicDownloadedItem info;
ComicDownloadedDetailController(this.info);
/// 阅读记录
Rx<ComicHistory?> history = Rx<ComicHistory?>(null);
/// 更新漫画记录
StreamSubscription<dynamic>? updateComicSubscription;
/// 编辑模式
var editMode = false.obs;
RxSet<ComicDetailChapterItem> selectItems = RxSet<ComicDetailChapterItem>();
@override
void onInit() {
updateComicSubscription = EventBus.instance.listen(
EventBus.kUpdatedComicHistory,
(id) {
if (id == info.comicId) {
getHistory();
}
},
);
getHistory();
super.onInit();
}
@override
void onClose() {
updateComicSubscription?.cancel();
super.onClose();
}
void getHistory() {
var comicHistory = DBService.instance.getComicHistory(info.comicId);
if (comicHistory != null) {
history.value = comicHistory;
history.update((val) {});
}
}
/// 开始/继续阅读
void read() {
if (info.volumes.isEmpty) {
SmartDialog.showToast("没有可阅读的章节");
return;
}
if (info.volumes.first.chapters.isEmpty) {
SmartDialog.showToast("没有可阅读的章节");
return;
}
//查找记录
if (history.value != null && history.value!.chapterId != 0) {
ComicDetailVolume? volume;
ComicDetailChapterItem? chapter;
for (var volumeItem in info.volumes) {
var chapterItem = volumeItem.chapters.firstWhereOrNull(
(x) => x.chapterId == history.value!.chapterId,
);
if (chapterItem != null) {
volume = volumeItem;
chapter = chapterItem;
break;
}
}
if (volume != null && chapter != null) {
var chapters = List<ComicDetailChapterItem>.from(volume.chapters);
//正序
chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder));
AppNavigator.toComicReader(
comicId: info.comicId,
comicTitle: info.comicName,
comicCover: info.comicCover,
chapters: chapters,
chapter: chapter,
isLongComic: info.isLongComic,
);
} else {
SmartDialog.showToast("未找到历史记录对应章节,将从头开始阅读");
readStart();
}
} else {
readStart();
}
}
void readStart() {
//从头开始
var volume = info.volumes.first;
var chapters = List<ComicDetailChapterItem>.from(volume.chapters);
//正序
chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder));
var chapter = chapters.first;
AppNavigator.toComicReader(
comicId: info.comicId,
comicCover: info.comicCover,
comicTitle: info.comicName,
chapters: chapters,
chapter: chapter,
isLongComic: info.isLongComic,
);
}
void readChapter(ComicDetailVolume volume, ComicDetailChapterItem item) {
var chapters = List<ComicDetailChapterItem>.from(volume.chapters);
//正序
chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder));
AppNavigator.toComicReader(
comicId: info.comicId,
comicCover: info.comicCover,
comicTitle: info.comicName,
chapters: chapters,
chapter: item,
isLongComic: info.isLongComic,
);
}
void toDetail() {
AppNavigator.toComicDetail(info.comicId);
}
void toAddDownload() {
AppNavigator.toComicDownloadSelect(info.comicId);
}
void setEditMode() {
selectItems.clear();
editMode.value = true;
}
void exitEditMode() {
selectItems.clear();
editMode.value = false;
}
var isSelectAll = false;
void selectAll() {
if (isSelectAll) {
selectItems.clear();
isSelectAll = false;
return;
}
for (var volume in info.volumes) {
for (var chapter in volume.chapters) {
selectItems.add(chapter);
}
}
isSelectAll = true;
}
void delete() {
for (var item in selectItems) {
ComicDownloadService.instance.deleteChapter(info.comicId, item.chapterId);
}
exitEditMode();
SmartDialog.showToast("删除成功");
AppNavigator.closePage();
}
void selectItem(ComicDetailChapterItem item) {
if (selectItems.contains(item)) {
selectItems.remove(item.chapterId);
} else {
selectItems.add(item);
}
}
}

View File

@@ -0,0 +1,292 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/models/comic/detail_info.dart';
import 'package:flutter_dmzj/modules/common/download/comic/comic_downloaded_detail_controller.dart';
import 'package:flutter_dmzj/services/comic_download_service.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:get/get.dart';
import 'package:remixicon/remixicon.dart';
class ComicDownloadedDetailPage extends StatelessWidget {
final ComicDownloadedItem info;
final ComicDownloadedDetailController controller;
ComicDownloadedDetailPage(this.info, {super.key})
: controller = Get.put(
ComicDownloadedDetailController(info),
tag: DateTime.now().millisecondsSinceEpoch.toString(),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(info.comicName),
),
body: ListView.builder(
padding: AppStyle.edgeInsetsA12,
itemCount: info.volumes.length,
itemBuilder: (_, i) {
var item = info.volumes[i];
return _buildChapters(item);
},
),
bottomNavigationBar: BottomAppBar(
child: SizedBox(
height: 48,
child: Obx(
() => Column(
children: [
Visibility(
visible: !controller.editMode.value,
child: Row(
children: [
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.setEditMode,
icon: const Icon(
Remix.checkbox_line,
size: 20,
),
label: const Text("选择"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.toDetail,
icon: const Icon(
Remix.information_line,
size: 20,
),
label: const Text("详情"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.toAddDownload,
icon: const Icon(
Remix.add_line,
size: 20,
),
label: const Text("追加"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.read,
icon: const Icon(
Remix.play_line,
size: 20,
),
label: const Text("阅读"),
),
),
],
),
),
Visibility(
visible: controller.editMode.value,
child: Row(
children: [
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.selectAll,
icon: const Icon(
Remix.checkbox_line,
size: 20,
),
label: const Text("全选"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.delete,
icon: const Icon(
Remix.delete_bin_line,
size: 20,
),
label: const Text("删除"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.exitEditMode,
icon: const Icon(
Remix.close_line,
size: 20,
),
label: const Text("取消"),
),
),
],
),
),
],
),
),
),
),
);
}
Widget _buildChapters(ComicDetailVolume item) {
return Obx(
() => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: AppStyle.edgeInsetsV8,
child: Row(
children: [
Expanded(
child: Text(
"${item.title}(共${item.chapters.length}话)",
style: Get.textTheme.titleSmall,
),
),
item.sortType.value == 1
? TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
item.sortType.value = 0;
item.sort();
},
icon: const Icon(
Remix.sort_asc,
size: 20,
),
label: const Text("升序"),
)
: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
item.sortType.value = 1;
item.sort();
},
icon: const Icon(
Remix.sort_desc,
size: 20,
),
label: const Text("倒序"),
),
],
),
),
LayoutBuilder(builder: (ctx, constraints) {
var count = constraints.maxWidth ~/ 160;
if (count < 3) count = 3;
return Obx(
() => MasonryGridView.count(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemCount: (item.showMoreButton && !item.showAll.value)
? 15
: item.chapters.length,
itemBuilder: (_, i) {
if (item.showMoreButton && !item.showAll.value && i == 14) {
return Tooltip(
message: "展开全部章节",
child: OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey,
textStyle: const TextStyle(fontSize: 14),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size.fromHeight(40),
),
onPressed: () {
item.showAll.value = true;
},
child: const Icon(Icons.arrow_drop_down),
),
);
}
var chapter = item.chapters[i];
return Tooltip(
message: chapter.chapterTitle,
child: Obx(
() => controller.editMode.value
? OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor:
controller.selectItems.contains(chapter)
? Get.theme.colorScheme.primary
: Get.textTheme.bodyMedium!.color,
side: controller.selectItems.contains(chapter)
? BorderSide(color: Get.theme.colorScheme.primary)
: null,
textStyle: const TextStyle(fontSize: 14),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size.fromHeight(40),
),
onPressed: () {
controller.selectItem(chapter);
},
child: Text(
item.chapters[i].chapterTitle,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
)
: OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: item.chapters[i].chapterId ==
controller.history.value?.chapterId
? Get.theme.colorScheme.primary
: Get.textTheme.bodyMedium!.color,
textStyle: const TextStyle(fontSize: 14),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size.fromHeight(40),
),
onPressed: () {
controller.readChapter(item, chapter);
},
child: Text(
item.chapters[i].chapterTitle,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
),
);
},
crossAxisCount: count,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
);
})
],
),
);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:flutter_dmzj/services/comic_download_service.dart';
import 'package:flutter_dmzj/widgets/net_image.dart';
import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart';
import 'package:get/get.dart';
class ComicDownloadedView extends StatelessWidget {
const ComicDownloadedView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(
() => Stack(
children: [
EasyRefresh(
header: const MaterialHeader(),
onRefresh: () async {
ComicDownloadService.instance.updateDownlaoded();
},
child: ListView.separated(
itemCount: ComicDownloadService.instance.downloaded.length,
separatorBuilder: (_, i) => Divider(
endIndent: 12,
indent: 12,
color: Colors.grey.withOpacity(.2),
height: 1,
),
itemBuilder: (_, i) {
var item = ComicDownloadService.instance.downloaded[i];
return buildItem(item);
},
),
),
Offstage(
offstage: ComicDownloadService.instance.downloaded.isNotEmpty,
child: AppEmptyWidget(
onRefresh: () {
ComicDownloadService.instance.updateDownlaoded();
},
),
),
],
),
);
}
Widget buildItem(ComicDownloadedItem item) {
return InkWell(
onTap: () {
AppNavigator.toComicDownloadDetail(item);
},
child: Container(
padding: AppStyle.edgeInsetsA12,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
NetImage(
item.comicCover,
width: 60,
borderRadius: 4,
),
AppStyle.hGap12,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
item.comicName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
AppStyle.vGap4,
Text(
"已下载${item.chapterCount}",
style: const TextStyle(color: Colors.grey, fontSize: 14),
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,225 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/models/db/download_status.dart';
import 'package:flutter_dmzj/services/comic_download_service.dart';
import 'package:flutter_dmzj/services/download_task/comic_downloader.dart';
import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart';
import 'package:get/get.dart';
import 'package:remixicon/remixicon.dart';
class ComicDownloadingView extends StatelessWidget {
const ComicDownloadingView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: Obx(
() => Stack(
children: [
ListView.separated(
itemCount: ComicDownloadService.instance.taskQueues.length,
separatorBuilder: (_, i) => const Divider(
height: 1,
),
itemBuilder: (_, i) {
var task = ComicDownloadService.instance.taskQueues[i];
return buildItem(task);
},
),
Offstage(
offstage: ComicDownloadService.instance.taskQueues.isNotEmpty,
child: const AppEmptyWidget(),
),
],
),
),
),
BottomAppBar(
child: SizedBox(
height: 48,
child: Row(
children: [
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: ComicDownloadService.instance.pauseAll,
icon: const Icon(
Remix.pause_line,
size: 20,
),
label: const Text("暂停全部"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: ComicDownloadService.instance.resumeAll,
icon: const Icon(
Remix.download_line,
size: 20,
),
label: const Text("开始全部"),
),
),
],
),
),
),
],
);
}
Widget buildItem(ComicDownloader task) {
return Obx(
() => Padding(
padding: AppStyle.edgeInsetsA12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"${task.info.value.volumeName} - ${task.info.value.chapterName}",
),
Text(
task.info.value.comicName,
style: Get.textTheme.bodySmall,
),
Row(
children: [
Expanded(
child: ClipRRect(
borderRadius: AppStyle.radius4,
child: LinearProgressIndicator(
value: task.info.value.total > 0
? (task.info.value.index + 1) / task.info.value.total
: 0,
),
),
),
AppStyle.hGap8,
Text(
"${task.info.value.index + 1}/${task.info.value.total}",
style: Get.textTheme.bodySmall,
),
],
),
Row(
children: [
Expanded(
child: Text(
parseStatus(task.info.value.status),
style: Get.textTheme.bodySmall,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
buildButton(
icon: Icons.refresh_rounded,
text: "重试",
visible: task.status == DownloadStatus.error ||
task.status == DownloadStatus.errorLoad,
onPressed: () {
task.retry();
},
),
buildButton(
icon: Icons.play_arrow_rounded,
visible: task.status == DownloadStatus.wait ||
task.status == DownloadStatus.pauseCellular,
text: "开始",
onPressed: () {
task.start();
},
),
buildButton(
icon: Icons.play_arrow_rounded,
visible: task.status == DownloadStatus.pause,
text: "继续",
onPressed: () {
task.resume();
},
),
buildButton(
icon: Icons.pause_rounded,
visible: task.status == DownloadStatus.downloading,
text: "暂停",
onPressed: () {
task.pause();
},
),
buildButton(
icon: Icons.cancel_outlined,
text: "取消",
onPressed: () {
task.cancel();
},
),
],
),
],
),
],
),
),
);
}
String parseStatus(DownloadStatus status) {
switch (status) {
case DownloadStatus.cancel:
return "已取消";
case DownloadStatus.complete:
return "已完成";
case DownloadStatus.downloading:
return "下载中";
case DownloadStatus.error:
return "下载失败";
case DownloadStatus.errorLoad:
return "无法读取信息";
case DownloadStatus.loadding:
return "读取信息中";
case DownloadStatus.pause:
return "暂停中";
case DownloadStatus.pauseCellular:
return "等待Wi-Fi";
case DownloadStatus.wait:
return "等待下载";
case DownloadStatus.waitNetwork:
return "等待网络连接";
default:
return status.toString();
}
}
Widget buildButton({
required String text,
required IconData icon,
Function()? onPressed,
bool visible = true,
}) {
return Visibility(
visible: visible,
child: Padding(
padding: AppStyle.edgeInsetsL4,
child: TextButton.icon(
style: TextButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
textStyle: const TextStyle(fontSize: 14),
),
onPressed: onPressed,
icon: Icon(
icon,
size: 16,
),
label: Text(text),
),
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/modules/common/download/novel/novel_downloaded_view.dart';
import 'package:flutter_dmzj/modules/common/download/novel/novel_downloading_view.dart';
import 'package:flutter_dmzj/services/novel_download_service.dart';
import 'package:get/get.dart';
class NovelDownloadPage extends StatelessWidget {
final int type;
const NovelDownloadPage(this.type, {super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
initialIndex: type,
child: Scaffold(
appBar: AppBar(
title: Container(
alignment: Alignment.center,
padding: const EdgeInsets.only(right: 56),
child: TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
indicatorSize: TabBarIndicatorSize.label,
indicatorColor: Theme.of(context).colorScheme.primary,
labelColor: Theme.of(context).colorScheme.primary,
unselectedLabelColor:
Get.isDarkMode ? Colors.white70 : Colors.black87,
tabs: [
const Tab(text: "已完成"),
Obx(
() => Tab(
text: NovelDownloadService.instance.taskQueues.isEmpty
? "下载中"
: "下载中(${NovelDownloadService.instance.taskQueues.length})"),
)
],
),
),
),
body: const TabBarView(
children: [
NovelDownloadedView(),
NovelDownloadingView(),
],
),
),
);
}
}

View File

@@ -0,0 +1,182 @@
import 'dart:async';
import 'package:flutter_dmzj/app/event_bus.dart';
import 'package:flutter_dmzj/models/db/novel_history.dart';
import 'package:flutter_dmzj/models/novel/novel_detail_model.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:flutter_dmzj/services/novel_download_service.dart';
import 'package:flutter_dmzj/services/db_service.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class NovelDownloadedDetailController extends GetxController {
final NovelDownloadedItem info;
NovelDownloadedDetailController(this.info);
/// 阅读记录
Rx<NovelHistory?> history = Rx<NovelHistory?>(null);
/// 更新漫画记录
StreamSubscription<dynamic>? updateNovelSubscription;
/// 编辑模式
var editMode = false.obs;
RxSet<NovelDetailChapter> selectItems = RxSet<NovelDetailChapter>();
@override
void onInit() {
updateNovelSubscription = EventBus.instance.listen(
EventBus.kUpdatedNovelHistory,
(id) {
if (id == info.novelId) {
getHistory();
}
},
);
getHistory();
super.onInit();
}
@override
void onClose() {
updateNovelSubscription?.cancel();
super.onClose();
}
void getHistory() {
var novelHistory = DBService.instance.getNovelHistory(info.novelId);
if (novelHistory != null) {
history.value = novelHistory;
history.update((val) {});
}
}
/// 开始/继续阅读
void read() {
if (info.volumes.isEmpty) {
SmartDialog.showToast("没有可阅读的章节");
return;
}
if (info.volumes.first.chapters.isEmpty) {
SmartDialog.showToast("没有可阅读的章节");
return;
}
//查找记录
if (history.value != null && history.value!.chapterId != 0) {
NovelDetailChapter? chapter;
for (var volumeItem in info.volumes) {
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 info.volumes) {
chapters.addAll(volume.chapters);
}
AppNavigator.toNovelReader(
novelId: info.novelId,
novelCover: info.novelCover,
novelTitle: info.novelName,
chapter: chapter,
chapters: chapters,
);
} else {
SmartDialog.showToast("未找到历史记录对应章节,将从头开始阅读");
readStart();
}
} else {
readStart();
}
}
void readStart() {
//从头开始
List<NovelDetailChapter> chapters = [];
for (var volume in info.volumes) {
chapters.addAll(volume.chapters);
}
var chapter = chapters.first;
AppNavigator.toNovelReader(
novelId: info.novelId,
novelCover: info.novelCover,
novelTitle: info.novelName,
chapter: chapter,
chapters: chapters,
);
}
void readChapter(NovelDetailVolume volume, NovelDetailChapter item) {
List<NovelDetailChapter> chapters = [];
for (var volume in info.volumes) {
chapters.addAll(volume.chapters);
}
AppNavigator.toNovelReader(
novelId: info.novelId,
novelCover: info.novelCover,
novelTitle: info.novelName,
chapters: chapters,
chapter: item,
);
}
void toDetail() {
AppNavigator.toNovelDetail(info.novelId);
}
void toAddDownload() {
AppNavigator.toNovelDownloadSelect(info.novelId);
}
void setEditMode() {
selectItems.clear();
editMode.value = true;
}
void exitEditMode() {
selectItems.clear();
editMode.value = false;
}
var isSelectAll = false;
void selectAll() {
if (isSelectAll) {
selectItems.clear();
isSelectAll = false;
return;
}
for (var volume in info.volumes) {
selectItems.addAll(volume.chapters);
}
isSelectAll = true;
}
void delete() {
for (var item in selectItems) {
NovelDownloadService.instance
.deleteChapter(info.novelId, item.volumeId, item.chapterId);
}
exitEditMode();
SmartDialog.showToast("删除成功");
AppNavigator.closePage();
}
void selectItem(NovelDetailChapter item) {
if (selectItems.contains(item)) {
selectItems.remove(item.chapterId);
} else {
selectItems.add(item);
}
}
}

View File

@@ -0,0 +1,289 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/models/novel/novel_detail_model.dart';
import 'package:flutter_dmzj/modules/common/download/novel/novel_downloaded_detail_controller.dart';
import 'package:flutter_dmzj/services/novel_download_service.dart';
import 'package:get/get.dart';
import 'package:remixicon/remixicon.dart';
class NovelDownloadedDetailPage extends StatelessWidget {
final NovelDownloadedItem info;
final NovelDownloadedDetailController controller;
NovelDownloadedDetailPage(this.info, {super.key})
: controller = Get.put(
NovelDownloadedDetailController(info),
tag: DateTime.now().millisecondsSinceEpoch.toString(),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(info.novelName),
),
body: ListView.builder(
padding: AppStyle.edgeInsetsA12,
itemCount: info.volumes.length,
itemBuilder: (_, i) {
var item = info.volumes[i];
return _buildChapters(item);
},
),
bottomNavigationBar: BottomAppBar(
child: SizedBox(
height: 48,
child: Obx(
() => Column(
children: [
Visibility(
visible: !controller.editMode.value,
child: Row(
children: [
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.setEditMode,
icon: const Icon(
Remix.checkbox_line,
size: 20,
),
label: const Text("选择"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.toDetail,
icon: const Icon(
Remix.information_line,
size: 20,
),
label: const Text("详情"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.toAddDownload,
icon: const Icon(
Remix.add_line,
size: 20,
),
label: const Text("追加"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.read,
icon: const Icon(
Remix.play_line,
size: 20,
),
label: const Text("阅读"),
),
),
],
),
),
Visibility(
visible: controller.editMode.value,
child: Row(
children: [
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.selectAll,
icon: const Icon(
Remix.checkbox_line,
size: 20,
),
label: const Text("全选"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.delete,
icon: const Icon(
Remix.delete_bin_line,
size: 20,
),
label: const Text("删除"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: controller.exitEditMode,
icon: const Icon(
Remix.close_line,
size: 20,
),
label: const Text("取消"),
),
),
],
),
),
],
),
),
),
),
);
}
Widget _buildChapters(NovelDetailVolume item) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: AppStyle.edgeInsetsV8,
child: Row(children: [
Expanded(
child: Text(
"${item.volumeName}(共${item.chapters.length}话)",
style: Get.textTheme.titleSmall,
),
),
]),
),
ListView.separated(
itemCount: item.chapters.length,
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (_, i) => const Divider(
height: 1,
),
itemBuilder: (_, i) {
var chapter = item.chapters[i];
return Obx(
() => controller.editMode.value
? CheckboxListTile(
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),
value: controller.selectItems.contains(chapter),
onChanged: (e) {
controller.selectItem(chapter);
},
)
: 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);
},
),
);
})
// LayoutBuilder(builder: (ctx, constraints) {
// var count = constraints.maxWidth ~/ 160;
// if (count < 3) count = 3;
// return MasonryGridView.count(
// shrinkWrap: true,
// padding: EdgeInsets.zero,
// physics: const NeverScrollableScrollPhysics(),
// itemCount: item.chapters.length,
// itemBuilder: (_, i) {
// var chapter = item.chapters[i];
// return Tooltip(
// message: chapter.chapterName,
// child: Obx(
// () => controller.editMode.value
// ? OutlinedButton(
// style: OutlinedButton.styleFrom(
// foregroundColor:
// controller.selectItems.contains(chapter)
// ? Colors.blue
// : Get.textTheme.bodyMedium!.color,
// side: controller.selectItems.contains(chapter)
// ? const BorderSide(color: Colors.blue)
// : null,
// textStyle: const TextStyle(fontSize: 14),
// tapTargetSize: MaterialTapTargetSize.shrinkWrap,
// minimumSize: const Size.fromHeight(40),
// ),
// onPressed: () {
// controller.selectItem(chapter);
// },
// child: Text(
// item.chapters[i].chapterName,
// textAlign: TextAlign.center,
// overflow: TextOverflow.ellipsis,
// ),
// )
// : OutlinedButton(
// style: OutlinedButton.styleFrom(
// foregroundColor: item.chapters[i].chapterId ==
// controller.history.value?.chapterId
// ? Colors.blue
// : Get.textTheme.bodyMedium!.color,
// textStyle: const TextStyle(fontSize: 14),
// tapTargetSize: MaterialTapTargetSize.shrinkWrap,
// minimumSize: const Size.fromHeight(40),
// ),
// onPressed: () {
// controller.readChapter(item, chapter);
// },
// child: Text(
// item.chapters[i].chapterName,
// textAlign: TextAlign.center,
// overflow: TextOverflow.ellipsis,
// ),
// ),
// ),
// );
// },
// crossAxisCount: count,
// crossAxisSpacing: 8,
// mainAxisSpacing: 8,
// );
// })
],
);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
import 'package:flutter_dmzj/services/novel_download_service.dart';
import 'package:flutter_dmzj/widgets/net_image.dart';
import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart';
import 'package:get/get.dart';
class NovelDownloadedView extends StatelessWidget {
const NovelDownloadedView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(
() => Stack(
children: [
EasyRefresh(
header: const MaterialHeader(),
onRefresh: () async {
NovelDownloadService.instance.updateDownlaoded();
},
child: ListView.separated(
itemCount: NovelDownloadService.instance.downloaded.length,
separatorBuilder: (_, i) => Divider(
endIndent: 12,
indent: 12,
color: Colors.grey.withOpacity(.2),
height: 1,
),
itemBuilder: (_, i) {
var item = NovelDownloadService.instance.downloaded[i];
return buildItem(item);
},
),
),
Offstage(
offstage: NovelDownloadService.instance.downloaded.isNotEmpty,
child: AppEmptyWidget(
onRefresh: () {
NovelDownloadService.instance.updateDownlaoded();
},
),
),
],
),
);
}
Widget buildItem(NovelDownloadedItem item) {
return InkWell(
onTap: () {
AppNavigator.toNovelDownloadDetail(item);
},
child: Container(
padding: AppStyle.edgeInsetsA12,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
NetImage(
item.novelCover,
width: 60,
borderRadius: 4,
),
AppStyle.hGap12,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
item.novelName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
AppStyle.vGap4,
Text(
"已下载${item.chapterCount}",
style: const TextStyle(color: Colors.grey, fontSize: 14),
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/models/db/download_status.dart';
import 'package:flutter_dmzj/services/novel_download_service.dart';
import 'package:flutter_dmzj/services/download_task/novel_downloader.dart';
import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart';
import 'package:get/get.dart';
import 'package:remixicon/remixicon.dart';
class NovelDownloadingView extends StatelessWidget {
const NovelDownloadingView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: Obx(
() => Stack(
children: [
ListView.separated(
itemCount: NovelDownloadService.instance.taskQueues.length,
separatorBuilder: (_, i) => const Divider(
height: 1,
),
itemBuilder: (_, i) {
var task = NovelDownloadService.instance.taskQueues[i];
return buildItem(task);
},
),
Offstage(
offstage: NovelDownloadService.instance.taskQueues.isNotEmpty,
child: const AppEmptyWidget(),
),
],
),
),
),
BottomAppBar(
child: SizedBox(
height: 48,
child: Row(
children: [
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: NovelDownloadService.instance.pauseAll,
icon: const Icon(
Remix.pause_line,
size: 20,
),
label: const Text("暂停全部"),
),
),
Expanded(
child: TextButton.icon(
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
),
onPressed: NovelDownloadService.instance.resumeAll,
icon: const Icon(
Remix.download_line,
size: 20,
),
label: const Text("开始全部"),
),
),
],
),
),
),
],
);
}
Widget buildItem(NovelDownloader task) {
return Obx(
() => Padding(
padding: AppStyle.edgeInsetsA12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"${task.info.value.volumeName} - ${task.info.value.chapterName}",
),
Text(
task.info.value.novelName,
style: Get.textTheme.bodySmall,
),
Row(
children: [
Expanded(
child: Text(
parseStatus(task.info.value.status),
style: Get.textTheme.bodySmall,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
buildButton(
icon: Icons.refresh_rounded,
text: "重试",
visible: task.status == DownloadStatus.error ||
task.status == DownloadStatus.errorLoad,
onPressed: () {
task.retry();
},
),
buildButton(
icon: Icons.play_arrow_rounded,
visible: task.status == DownloadStatus.wait ||
task.status == DownloadStatus.pauseCellular,
text: "开始",
onPressed: () {
task.start();
},
),
buildButton(
icon: Icons.play_arrow_rounded,
visible: task.status == DownloadStatus.pause,
text: "继续",
onPressed: () {
task.resume();
},
),
buildButton(
icon: Icons.pause_rounded,
visible: task.status == DownloadStatus.downloading,
text: "暂停",
onPressed: () {
task.pause();
},
),
buildButton(
icon: Icons.cancel_outlined,
text: "取消",
onPressed: () {
task.cancel();
},
),
],
),
],
),
],
),
),
);
}
String parseStatus(DownloadStatus status) {
switch (status) {
case DownloadStatus.cancel:
return "已取消";
case DownloadStatus.complete:
return "已完成";
case DownloadStatus.downloading:
return "下载中";
case DownloadStatus.error:
return "下载失败";
case DownloadStatus.errorLoad:
return "无法读取信息";
case DownloadStatus.loadding:
return "读取信息中";
case DownloadStatus.pause:
return "暂停中";
case DownloadStatus.pauseCellular:
return "等待Wi-Fi";
case DownloadStatus.wait:
return "等待下载";
case DownloadStatus.waitNetwork:
return "等待网络连接";
default:
return status.toString();
}
}
Widget buildButton({
required String text,
required IconData icon,
Function()? onPressed,
bool visible = true,
}) {
return Visibility(
visible: visible,
child: Padding(
padding: AppStyle.edgeInsetsL4,
child: TextButton.icon(
style: TextButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
textStyle: const TextStyle(fontSize: 14),
),
onPressed: onPressed,
icon: Icon(
icon,
size: 16,
),
label: Text(text),
),
),
);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_constant.dart';
class EmptyPage extends StatelessWidget {
const EmptyPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MediaQuery.of(context).size.width <= AppConstant.kTabletWidth
? Container()
: Scaffold(
resizeToAvoidBottomInset: false,
body: Center(
child: Image.asset(
"assets/images/logo_dmzj.png",
height: 80,
),
),
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/routes/app_navigator.dart';
class TestSubRoutePage extends StatelessWidget {
const TestSubRoutePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("测试路由"),
leading: IconButton(
onPressed: () {
AppNavigator.closePage();
},
icon: const Icon(Icons.arrow_back),
),
),
body: Center(
child: ElevatedButton(
child: const Text("Back"),
onPressed: () {
AppNavigator.closePage();
},
),
),
);
}
}

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_color.dart';
import 'package:flutter_dmzj/app/controller/base_controller.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_dmzj/services/user_service.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebViewPageController extends BaseController {
final String url;
WebViewPageController(this.url);
final WebViewController webViewController = WebViewController();
var title = "加载中".obs;
@override
void onInit() {
initWebView();
super.onInit();
}
void initWebView() async {
webViewController.setJavaScriptMode(JavaScriptMode.unrestricted);
webViewController.setBackgroundColor(
Get.isDarkMode ? Colors.black : AppColor.backgroundColor);
webViewController.setNavigationDelegate(
NavigationDelegate(
onPageStarted: (String url) {
pageLoadding.value = true;
},
onPageFinished: (String url) async {
pageLoadding.value = false;
title.value = (await webViewController.getTitle()) ?? "";
},
onNavigationRequest: (NavigationRequest request) {
var uri = Uri.parse(request.url);
Log.d(request.url);
if (uri.scheme == "https" || uri.scheme == "http") {
return NavigationDecision.navigate;
}
return NavigationDecision.prevent;
},
),
);
webViewController.loadRequest(Uri.parse(url), headers: {
"Cookie": UserService.instance.userProfile.value?.cookieVal ?? "",
});
/// TODO 无法加载Mixed Content
/// 19年的问题了Flutter还没解决...
/// https://github.com/flutter/flutter/issues/43595
}
void refreshWeb() {
webViewController.reload();
}
}

View File

@@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/utils.dart';
import 'package:flutter_dmzj/modules/common/webview/webview_controller.dart';
import 'package:flutter_dmzj/widgets/status/app_error_widget.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebViewPage extends StatelessWidget {
final String url;
final WebViewPageController controller;
WebViewPage({required this.url, Key? key})
: controller = Get.put(
WebViewPageController(url),
tag: DateTime.now().millisecondsSinceEpoch.toString(),
),
super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Obx(() => Text(controller.title.value)),
),
body: Stack(
children: [
Obx(
() => Offstage(
offstage: controller.pageLoadding.value,
child: WebViewWidget(
controller: controller.webViewController,
),
),
),
Obx(
() => Offstage(
offstage: !controller.pageError.value,
child: AppErrorWidget(
errorMsg: controller.errorMsg.value,
onRefresh: () => controller.refreshWeb(),
),
),
),
],
),
bottomNavigationBar: Stack(
children: [
BottomAppBar(
child: SizedBox(
height: 56,
child: Row(
children: [
Expanded(
child: IconButton(
onPressed: () {
controller.webViewController.goBack();
},
icon: const Icon(
Icons.chevron_left,
),
),
),
Expanded(
child: IconButton(
onPressed: () {
controller.webViewController.reload();
},
icon: const Icon(
Icons.refresh,
),
),
),
Expanded(
child: IconButton(
onPressed: () {
controller.webViewController.goForward();
},
icon: const Icon(
Icons.chevron_right,
),
),
),
Expanded(
child: IconButton(
onPressed: () async {
Utils.share(
(await controller.webViewController.currentUrl())
.toString(),
);
},
icon: const Icon(
Icons.share,
size: 20,
),
),
),
Expanded(
child: IconButton(
onPressed: () async {
var url =
await controller.webViewController.currentUrl();
if (url != null) {
launchUrlString(url,
mode: LaunchMode.externalApplication);
}
},
icon: const Icon(
Icons.open_in_browser,
),
),
),
],
),
),
),
Positioned.fill(
top: 0,
left: 0,
child: Obx(
() => Offstage(
offstage: !controller.pageLoadding.value,
child: Container(
alignment: Alignment.topLeft,
child: const LinearProgressIndicator(),
),
),
),
)
],
),
);
}
}