v1.0.1
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter_dmzj/app/app_constant.dart';
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/models/comic/author_model.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.dart';
|
||||
import 'package:flutter_dmzj/services/user_service.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicAuthorDetailController extends BaseController {
|
||||
final int id;
|
||||
ComicAuthorDetailController(this.id);
|
||||
|
||||
final ComicRequest request = ComicRequest();
|
||||
|
||||
Rx<ComicAuthorModel?> detail = Rx<ComicAuthorModel?>(null);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
loadData();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void loadData() async {
|
||||
try {
|
||||
pageLoadding.value = true;
|
||||
pageError.value = false;
|
||||
var result = await request.authorDetail(id: id);
|
||||
detail.value = result;
|
||||
} catch (e) {
|
||||
pageError.value = true;
|
||||
errorMsg.value = e.toString();
|
||||
} finally {
|
||||
pageLoadding.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void subscribeAll() {
|
||||
if (detail.value == null) {
|
||||
return;
|
||||
}
|
||||
UserService.instance.addSubscribe(
|
||||
detail.value!.data.map((e) => e.id).toList(),
|
||||
AppConstant.kTypeComic,
|
||||
);
|
||||
}
|
||||
}
|
||||
152
lib/modules/comic/author_detail/author_detail_page.dart
Normal file
152
lib/modules/comic/author_detail/author_detail_page.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_constant.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/models/comic/author_model.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/author_detail/author_detail_controller.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/services/user_service.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 ComicAuthorDetailPage extends StatelessWidget {
|
||||
final int id;
|
||||
final ComicAuthorDetailController controller;
|
||||
ComicAuthorDetailPage(this.id, {super.key})
|
||||
: controller = Get.put(
|
||||
ComicAuthorDetailController(id),
|
||||
tag: "$id",
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
title: Obx(
|
||||
() => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
NetImage(
|
||||
controller.detail.value?.cover ?? "",
|
||||
borderRadius: 24,
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
AppStyle.hGap8,
|
||||
Text(controller.detail.value?.nickname ?? "作者"),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
onPressed: controller.subscribeAll,
|
||||
icon: const Icon(Remix.heart_line),
|
||||
label: const Text("全部订阅"),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Obx(
|
||||
() => Stack(
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: controller.detail.value == null,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: controller.detail.value?.data.length ?? 0,
|
||||
separatorBuilder: (context, i) => Divider(
|
||||
endIndent: 12,
|
||||
indent: 12,
|
||||
color: Colors.grey.withOpacity(.2),
|
||||
height: 1,
|
||||
),
|
||||
itemBuilder: (_, i) {
|
||||
var item = controller.detail.value!.data[i];
|
||||
return buildItem(item);
|
||||
},
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: !controller.pageLoadding.value,
|
||||
child: const AppLoaddingWidget(),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: !controller.pageError.value,
|
||||
child: AppErrorWidget(
|
||||
errorMsg: controller.errorMsg.value,
|
||||
onRefresh: () => controller.loadData(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildItem(ComicAuthorComicModel item) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
AppNavigator.toComicDetail(item.id);
|
||||
},
|
||||
child: Container(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
NetImage(
|
||||
item.cover,
|
||||
width: 80,
|
||||
height: 110,
|
||||
borderRadius: 4,
|
||||
),
|
||||
AppStyle.hGap12,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
AppStyle.vGap4,
|
||||
Text(item.status,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Obx(
|
||||
() => UserService.instance.subscribedComicIds.contains(item.id)
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.favorite),
|
||||
onPressed: () {
|
||||
UserService.instance.cancelSubscribe(
|
||||
[item.id],
|
||||
AppConstant.kTypeComic,
|
||||
);
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.favorite_border),
|
||||
onPressed: () {
|
||||
UserService.instance.addSubscribe(
|
||||
[item.id],
|
||||
AppConstant.kTypeComic,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/models/comic/category_comic_model.dart';
|
||||
import 'package:flutter_dmzj/models/comic/category_filter_model.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CategoryDetailController
|
||||
extends BasePageController<ComicCategoryComicModel> {
|
||||
final int id;
|
||||
CategoryDetailController(this.id);
|
||||
final ComicRequest request = ComicRequest();
|
||||
RxList<ComicCategoryFilterModel> filters = RxList<ComicCategoryFilterModel>();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
loadFilter();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
var items = filters.where((x) => x.selectId.value != 0 && x.title != "排序");
|
||||
|
||||
if (items.isEmpty) {
|
||||
return "全部漫画";
|
||||
} else {
|
||||
return items
|
||||
.map((e) =>
|
||||
e.items.firstWhere((x) => x.tagId == e.selectId.value).tagName)
|
||||
.join("-");
|
||||
}
|
||||
}
|
||||
|
||||
void loadFilter() async {
|
||||
try {
|
||||
filters.value = await request.categoryFilter();
|
||||
for (var item in filters) {
|
||||
var tag = item.items.firstWhereOrNull((x) => x.tagId == id);
|
||||
if (tag != null) {
|
||||
item.selectId.value = tag.tagId;
|
||||
}
|
||||
}
|
||||
filters.insert(
|
||||
0,
|
||||
ComicCategoryFilterModel(
|
||||
title: "排序",
|
||||
items: [
|
||||
ComicCategoryFilterItemModel(tagId: 1, tagName: "更新排序"),
|
||||
ComicCategoryFilterItemModel(tagId: 2, tagName: "热度排序"),
|
||||
],
|
||||
)..selectId.value = 1,
|
||||
);
|
||||
filters.insert(
|
||||
1,
|
||||
ComicCategoryFilterModel(
|
||||
title: "状态",
|
||||
items: [
|
||||
ComicCategoryFilterItemModel(tagId: 0, tagName: "全部"),
|
||||
ComicCategoryFilterItemModel(tagId: 1, tagName: "连载中"),
|
||||
ComicCategoryFilterItemModel(tagId: 2, tagName: "已完结"),
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ComicCategoryComicModel>> getData(int page, int pageSize) async {
|
||||
if (filters.isEmpty) {
|
||||
return await request.categoryComic(id: id, page: page);
|
||||
} else {
|
||||
var sort = filters.first.selectId.value;
|
||||
var status = filters[1].selectId.value;
|
||||
|
||||
return await request.categoryComic(
|
||||
id: filters.last.selectId.value,
|
||||
sort: sort,
|
||||
page: page,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
193
lib/modules/comic/category_detail/category_detail_page.dart
Normal file
193
lib/modules/comic/category_detail/category_detail_page.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/category_detail/category_detail_controller.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/widgets/net_image.dart';
|
||||
import 'package:flutter_dmzj/widgets/page_grid_view.dart';
|
||||
import 'package:flutter_dmzj/widgets/shadow_card.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class CategoryDetailPage extends StatelessWidget {
|
||||
final int id;
|
||||
final CategoryDetailController controller;
|
||||
CategoryDetailPage(this.id, {super.key})
|
||||
: controller = Get.put(
|
||||
CategoryDetailController(id),
|
||||
tag: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Obx(
|
||||
() => Text(
|
||||
controller.getTitle(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Builder(
|
||||
builder: (BuildContext context) => IconButton(
|
||||
icon: const Icon(Remix.filter_line),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).openEndDrawer();
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
endDrawer: Drawer(
|
||||
child: Obx(
|
||||
() => SafeArea(
|
||||
child: ListView.builder(
|
||||
padding: AppStyle.edgeInsetsA12.copyWith(top: 12),
|
||||
itemCount: controller.filters.length,
|
||||
itemBuilder: (context, i) {
|
||||
var item = controller.filters[i];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: AppStyle.edgeInsetsV12,
|
||||
child: Text(
|
||||
item.title,
|
||||
style: Get.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: item.items
|
||||
.map(
|
||||
(x) => OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
foregroundColor: x.tagId == item.selectId.value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.grey,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: x.tagId == item.selectId.value
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
x.tagName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
item.selectId.value = x.tagId;
|
||||
|
||||
Navigator.pop(context);
|
||||
controller.refreshData();
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: LayoutBuilder(builder: (context, constraints) {
|
||||
var count = constraints.maxWidth ~/ 160;
|
||||
if (count < 3) count = 3;
|
||||
return PageGridView(
|
||||
pageController: controller,
|
||||
firstRefresh: true,
|
||||
crossAxisCount: count,
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
itemBuilder: (context, i) {
|
||||
var item = controller.list[i];
|
||||
return ShadowCard(
|
||||
onTap: () {
|
||||
AppNavigator.toComicDetail(item.id);
|
||||
},
|
||||
radius: 4,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 27 / 36,
|
||||
child: NetImage(
|
||||
item.cover ?? "",
|
||||
borderRadius: 4,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: item.status == "连载中"
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.orange,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(4),
|
||||
bottomLeft: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
padding:
|
||||
AppStyle.edgeInsetsH8.copyWith(top: 2, bottom: 2),
|
||||
child: Text(
|
||||
item.status ?? "",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
AppStyle.vGap4,
|
||||
Padding(
|
||||
padding: AppStyle.edgeInsetsH4,
|
||||
child: Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
AppStyle.vGap4,
|
||||
Padding(
|
||||
padding: AppStyle.edgeInsetsH4,
|
||||
child: Text(
|
||||
item.authors ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12.0,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
AppStyle.vGap4,
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
318
lib/modules/comic/detail/comic_detail_controller.dart
Normal file
318
lib/modules/comic/detail/comic_detail_controller.dart
Normal file
@@ -0,0 +1,318 @@
|
||||
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/comic/detail_info.dart';
|
||||
import 'package:flutter_dmzj/models/db/comic_history.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/detail/comic_detail_related_page.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.dart';
|
||||
import 'package:flutter_dmzj/requests/user_request.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/services/app_settings_service.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 ComicDetailControler extends BaseController {
|
||||
final int comicId;
|
||||
ComicDetailControler(this.comicId);
|
||||
|
||||
final ComicRequest request = ComicRequest();
|
||||
final UserRequest userRequest = UserRequest();
|
||||
|
||||
Rx<ComicDetailInfo> detail = Rx<ComicDetailInfo>(ComicDetailInfo.empty());
|
||||
|
||||
var expandDescription = false.obs;
|
||||
|
||||
/// 是否已订阅
|
||||
var subscribeStatus = false.obs;
|
||||
|
||||
/// 是否已收藏
|
||||
/// 收藏是收藏到本地的,订阅是同步到动漫之家服务器的
|
||||
var favorited = false.obs;
|
||||
|
||||
/// 阅读记录
|
||||
Rx<ComicHistory?> history = Rx<ComicHistory?>(null);
|
||||
|
||||
/// 更新漫画记录
|
||||
StreamSubscription<dynamic>? updateComicSubscription;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
updateComicSubscription = EventBus.instance.listen(
|
||||
EventBus.kUpdatedComicHistory,
|
||||
(id) {
|
||||
if (id == comicId) {
|
||||
getHistory();
|
||||
}
|
||||
},
|
||||
);
|
||||
favorited.value = DBService.instance.hasComicFavorited(comicId: comicId);
|
||||
// 从本地读取订阅状态
|
||||
subscribeStatus.value =
|
||||
UserService.instance.subscribedComicIds.contains(comicId);
|
||||
getHistory();
|
||||
loadDetail();
|
||||
loadSubscribeStatus();
|
||||
//updateSubscribeRead();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void refreshDetail() {
|
||||
getHistory();
|
||||
loadDetail();
|
||||
loadSubscribeStatus();
|
||||
}
|
||||
|
||||
/// 更新订阅的阅读状态
|
||||
void updateSubscribeRead() {
|
||||
try {
|
||||
userRequest.subscribeRead(id: comicId, type: AppConstant.kTypeComic);
|
||||
} catch (e) {
|
||||
Log.logPrint(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
updateComicSubscription?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void getHistory() {
|
||||
var comicHistory = DBService.instance.getComicHistory(comicId);
|
||||
if (comicHistory != null) {
|
||||
history.value = comicHistory;
|
||||
history.update((val) {});
|
||||
}
|
||||
}
|
||||
|
||||
void refreshV1() async {
|
||||
try {
|
||||
var result =
|
||||
await request.comicDetail(comicId: comicId, priorityV1: true);
|
||||
if (result.volumes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (result.isHide && AppSettingsService.instance.collectHideComic.value) {
|
||||
favorite();
|
||||
}
|
||||
detail.update((val) {
|
||||
val!.volumes = result.volumes;
|
||||
});
|
||||
} catch (e) {
|
||||
SmartDialog.showToast("无法获取章节");
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载信息
|
||||
void loadDetail() async {
|
||||
try {
|
||||
pageLoadding.value = true;
|
||||
pageError.value = false;
|
||||
var result = await request.comicDetail(comicId: comicId);
|
||||
detail.value = result;
|
||||
if (result.volumes.isEmpty && !result.isHide) {
|
||||
refreshV1();
|
||||
}
|
||||
if (result.isHide && AppSettingsService.instance.collectHideComic.value) {
|
||||
favorite();
|
||||
}
|
||||
} catch (e) {
|
||||
pageError.value = true;
|
||||
errorMsg.value = e.toString();
|
||||
} finally {
|
||||
pageLoadding.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查订阅状态
|
||||
void loadSubscribeStatus() async {
|
||||
try {
|
||||
var result = await userRequest.checkSubscribeStatus(
|
||||
objId: comicId,
|
||||
type: AppConstant.kTypeComic,
|
||||
);
|
||||
subscribeStatus.value = result;
|
||||
if (subscribeStatus.value) {
|
||||
UserService.instance.subscribedComicIds.add(comicId);
|
||||
} else {
|
||||
UserService.instance.subscribedComicIds.remove(comicId);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.logPrint(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 查看评论
|
||||
void comment() {
|
||||
AppNavigator.toComment(objId: comicId, type: AppConstant.kTypeComic);
|
||||
}
|
||||
|
||||
/// 分享
|
||||
void share() {
|
||||
if (detail.value.id == 0) {
|
||||
return;
|
||||
}
|
||||
Utils.share(
|
||||
"http://m.idmzj.com/info/${detail.value.comicPy}.html",
|
||||
content: detail.value.title,
|
||||
);
|
||||
}
|
||||
|
||||
/// 订阅
|
||||
void subscribe() async {
|
||||
var result = await (subscribeStatus.value
|
||||
? UserService.instance
|
||||
.cancelSubscribe([comicId], AppConstant.kTypeComic)
|
||||
: UserService.instance.addSubscribe([comicId], AppConstant.kTypeComic));
|
||||
if (result) {
|
||||
subscribeStatus.value = !subscribeStatus.value;
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载
|
||||
void download() {
|
||||
AppNavigator.toComicDownloadSelect(comicId);
|
||||
}
|
||||
|
||||
/// 开始/继续阅读
|
||||
void read() {
|
||||
if (detail.value.volumes.isEmpty) {
|
||||
SmartDialog.showToast("没有可阅读的章节");
|
||||
return;
|
||||
}
|
||||
if (detail.value.volumes.first.chapters.isEmpty) {
|
||||
SmartDialog.showToast("没有可阅读的章节");
|
||||
return;
|
||||
}
|
||||
//查找记录
|
||||
if (history.value != null && history.value!.chapterId != 0) {
|
||||
ComicDetailVolume? volume;
|
||||
ComicDetailChapterItem? chapter;
|
||||
for (var volumeItem in detail.value.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: comicId,
|
||||
comicTitle: detail.value.title,
|
||||
comicCover: detail.value.cover,
|
||||
chapters: chapters,
|
||||
chapter: chapter,
|
||||
isLongComic: detail.value.isLong,
|
||||
);
|
||||
} else {
|
||||
SmartDialog.showToast("未找到历史记录对应章节,将从头开始阅读");
|
||||
readStart();
|
||||
}
|
||||
} else {
|
||||
readStart();
|
||||
}
|
||||
}
|
||||
|
||||
void readStart() {
|
||||
//从头开始
|
||||
var volume = detail.value.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: comicId,
|
||||
comicCover: detail.value.cover,
|
||||
comicTitle: detail.value.title,
|
||||
chapters: chapters,
|
||||
chapter: chapter,
|
||||
isLongComic: detail.value.isLong,
|
||||
);
|
||||
}
|
||||
|
||||
void readChapter(ComicDetailVolume volume, ComicDetailChapterItem item) {
|
||||
//禁止观看VIP章节
|
||||
if (item.isVip) {
|
||||
SmartDialog.showToast("请使用动漫之家官方APP观看VIP章节");
|
||||
return;
|
||||
}
|
||||
var chapters = List<ComicDetailChapterItem>.from(volume.chapters);
|
||||
//正序
|
||||
chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder));
|
||||
AppNavigator.toComicReader(
|
||||
comicId: comicId,
|
||||
comicCover: detail.value.cover,
|
||||
comicTitle: detail.value.title,
|
||||
chapters: chapters,
|
||||
chapter: item,
|
||||
isLongComic: detail.value.isLong,
|
||||
);
|
||||
}
|
||||
|
||||
void related() async {
|
||||
try {
|
||||
SmartDialog.showLoading();
|
||||
var data = await request.related(id: comicId);
|
||||
SmartDialog.dismiss(status: SmartStatus.loading);
|
||||
AppNavigator.showBottomSheet(
|
||||
ComicDetailRelatedPage(data),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
} finally {
|
||||
SmartDialog.dismiss(status: SmartStatus.loading);
|
||||
}
|
||||
}
|
||||
|
||||
void toAuthorDetail(ComicDetailTag e) {
|
||||
if (e.tagId == 0) {
|
||||
//神隐漫画没有ID,直接跳转搜索
|
||||
AppNavigator.toComicSearch(keyword: e.tagName);
|
||||
} else {
|
||||
AppNavigator.toComicAuthorDetail(e.tagId);
|
||||
}
|
||||
}
|
||||
|
||||
void toCategoryDetail(ComicDetailTag e) {
|
||||
if (e.tagId == 0) {
|
||||
//神隐漫画没有ID,直接跳转搜索
|
||||
AppNavigator.toComicSearch(keyword: e.tagName);
|
||||
} else {
|
||||
AppNavigator.toComicCategoryDetail(e.tagId);
|
||||
}
|
||||
}
|
||||
|
||||
void favorite() {
|
||||
if (detail.value.id == 0) {
|
||||
return;
|
||||
}
|
||||
if (!DBService.instance.hasComicFavorited(comicId: comicId)) {
|
||||
DBService.instance.putComicFavorite(
|
||||
comicId: comicId,
|
||||
title: detail.value.title,
|
||||
cover: detail.value.cover,
|
||||
);
|
||||
favorited.value = true;
|
||||
SmartDialog.showToast("已将漫画添加至本地收藏");
|
||||
}
|
||||
}
|
||||
|
||||
void cancelFavorite() {
|
||||
DBService.instance.removeComicFavorite(comicId: comicId);
|
||||
favorited.value = false;
|
||||
SmartDialog.showToast("已从本地收藏删除漫画");
|
||||
}
|
||||
}
|
||||
533
lib/modules/comic/detail/comic_detail_page.dart
Normal file
533
lib/modules/comic/detail/comic_detail_page.dart
Normal file
@@ -0,0 +1,533 @@
|
||||
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/comic/detail/comic_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:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class ComicDetailPage extends StatelessWidget {
|
||||
final int id;
|
||||
final ComicDetailControler controller;
|
||||
ComicDetailPage(this.id, {super.key})
|
||||
: controller = Get.put(
|
||||
ComicDetailControler(id),
|
||||
tag: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Obx(
|
||||
() => Text(
|
||||
controller.detail.value.title.isEmpty
|
||||
? "漫画详情"
|
||||
: controller.detail.value.title,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Obx(
|
||||
() => IconButton(
|
||||
onPressed: controller.favorited.value
|
||||
? controller.cancelFavorite
|
||||
: controller.favorite,
|
||||
icon: Icon(controller.favorited.value
|
||||
? Remix.star_fill
|
||||
: Remix.star_line),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: controller.share,
|
||||
icon: const Icon(Icons.share),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: controller.detail.value.id == 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?.chapterName ?? ""} 第${controller.history.value?.page}页",
|
||||
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("下载"),
|
||||
),
|
||||
),
|
||||
// Expanded(
|
||||
// child: TextButton.icon(
|
||||
// style: TextButton.styleFrom(
|
||||
// textStyle: const TextStyle(fontSize: 14),
|
||||
// ),
|
||||
// onPressed: controller.related,
|
||||
// icon: const Icon(
|
||||
// Remix.links_line,
|
||||
// size: 20,
|
||||
// ),
|
||||
// label: const Text("相关"),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
//信息
|
||||
Stack(
|
||||
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.title,
|
||||
style: Get.textTheme.titleMedium,
|
||||
),
|
||||
AppStyle.vGap8,
|
||||
_buildInfoItems(
|
||||
iconData: Remix.user_smile_line,
|
||||
children: controller.detail.value.authors
|
||||
.map(
|
||||
(e) => GestureDetector(
|
||||
onTap: () => controller.toAuthorDetail(e),
|
||||
child: Text(
|
||||
e.tagName,
|
||||
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.tagName)
|
||||
// .join("/"),
|
||||
// iconData: Remix.hashtag,
|
||||
// ),
|
||||
_buildInfoItems(
|
||||
iconData: Remix.hashtag,
|
||||
children: controller.detail.value.types
|
||||
.map(
|
||||
(e) => GestureDetector(
|
||||
onTap: () => controller.toCategoryDetail(e),
|
||||
child: Text(
|
||||
e.tagName,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1.2,
|
||||
decoration: TextDecoration.underline,
|
||||
color: Get.isDarkMode
|
||||
? Colors.white
|
||||
: AppColor.black333,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
// _buildInfo(
|
||||
// title: "人气 ${controller.detail.value.hitNum}",
|
||||
// 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.map((e) => e.tagName).join("/")}",
|
||||
iconData: Icons.schedule,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Obx(
|
||||
() => Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Offstage(
|
||||
offstage: !controller.detail.value.isVip,
|
||||
child: Image.asset(
|
||||
"assets/images/vip_comic.png",
|
||||
width: 36,
|
||||
height: 36,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
AppStyle.vGap12,
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
controller.expandDescription.value =
|
||||
!controller.expandDescription.value;
|
||||
},
|
||||
child: Text(
|
||||
controller.detail.value.description,
|
||||
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 Column(
|
||||
children: controller.detail.value.volumes.isEmpty
|
||||
? [
|
||||
const Padding(
|
||||
padding: AppStyle.edgeInsetsA24,
|
||||
child: Text(
|
||||
"(~ ̄▽ ̄)~\n没有可阅读的章节\n漫画可能已下架或您没有阅读的权限",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||
),
|
||||
)
|
||||
]
|
||||
: controller.detail.value.volumes
|
||||
.map(
|
||||
(item) => 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Tooltip(
|
||||
message: item.chapters[i].chapterTitle,
|
||||
child: Obx(
|
||||
() => Stack(
|
||||
children: [
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: item
|
||||
.chapters[i].chapterId ==
|
||||
controller
|
||||
.history.value?.chapterId
|
||||
? Theme.of(ctx).colorScheme.primary
|
||||
: Get.textTheme.bodyMedium!.color,
|
||||
textStyle:
|
||||
const TextStyle(fontSize: 14),
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize:
|
||||
const Size.fromHeight(40),
|
||||
),
|
||||
onPressed: () {
|
||||
controller.readChapter(
|
||||
item, item.chapters[i]);
|
||||
},
|
||||
child: Text(
|
||||
item.chapters[i].chapterTitle,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -2,
|
||||
top: 0,
|
||||
child: Offstage(
|
||||
offstage: !item.chapters[i].isVip,
|
||||
child: Image.asset(
|
||||
"assets/images/vip_chapter.png",
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
crossAxisCount: count,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
164
lib/modules/comic/detail/comic_detail_related_page.dart
Normal file
164
lib/modules/comic/detail/comic_detail_related_page.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/models/comic/comic_related_model.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/widgets/net_image.dart';
|
||||
import 'package:flutter_dmzj/widgets/shadow_card.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicDetailRelatedPage extends StatelessWidget {
|
||||
final ComicRelatedModel related;
|
||||
const ComicDetailRelatedPage(this.related, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text("作品相关"),
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
AppNavigator.closePage();
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
contentPadding: AppStyle.edgeInsetsL12,
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: AppStyle.edgeInsetsA12.copyWith(top: 0),
|
||||
children: [
|
||||
...related.authorComics
|
||||
.map(
|
||||
(e) =>
|
||||
buildCard("${e.authorName}的其他作品", e.data, onTap: () {
|
||||
AppNavigator.toComicAuthorDetail(e.authorId);
|
||||
}),
|
||||
)
|
||||
.toList(),
|
||||
buildCard("同类题材作品", related.themeComics),
|
||||
buildCard("轻小说", related.novels, isComic: false),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildCard(String title, List<ComicRelatedItemModel> list,
|
||||
{Function()? onTap, bool isComic = true}) {
|
||||
return Visibility(
|
||||
visible: list.isNotEmpty,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: AppStyle.edgeInsetsV8,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: Get.textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: onTap != null,
|
||||
child: IconButton(
|
||||
onPressed: onTap,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
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: list.length,
|
||||
crossAxisCount: count,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
itemBuilder: (_, i) {
|
||||
var item = list[i];
|
||||
return ShadowCard(
|
||||
onTap: () {
|
||||
if (isComic) {
|
||||
AppNavigator.toComicDetail(item.id);
|
||||
} else {
|
||||
AppNavigator.toNovelDetail(item.id);
|
||||
}
|
||||
},
|
||||
radius: 4,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 27 / 36,
|
||||
child: NetImage(
|
||||
item.cover,
|
||||
borderRadius: 4,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: item.status == "连载中"
|
||||
? Get.theme.colorScheme.primary
|
||||
: Colors.orange,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(4),
|
||||
bottomLeft: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
padding: AppStyle.edgeInsetsH8
|
||||
.copyWith(top: 2, bottom: 2),
|
||||
child: Text(
|
||||
item.status,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
AppStyle.vGap4,
|
||||
Padding(
|
||||
padding: AppStyle.edgeInsetsA4,
|
||||
child: Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/models/comic/category_item_model.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
|
||||
class ComicCategoryController
|
||||
extends BasePageController<ComicCategoryItemModel> {
|
||||
final ComicRequest request = ComicRequest();
|
||||
|
||||
@override
|
||||
Future<List<ComicCategoryItemModel>> getData(int page, int pageSize) async {
|
||||
if (page > 1) {
|
||||
return [];
|
||||
}
|
||||
var ls = await request.categores();
|
||||
|
||||
return ls;
|
||||
}
|
||||
|
||||
void toDetail(ComicCategoryItemModel item) {
|
||||
AppNavigator.toComicCategoryDetail(item.tagId);
|
||||
}
|
||||
}
|
||||
61
lib/modules/comic/home/category/comic_category_view.dart
Normal file
61
lib/modules/comic/home/category/comic_category_view.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/category/comic_category_controller.dart';
|
||||
import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart';
|
||||
import 'package:flutter_dmzj/widgets/net_image.dart';
|
||||
import 'package:flutter_dmzj/widgets/page_grid_view.dart';
|
||||
import 'package:flutter_dmzj/widgets/shadow_card.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicCategoryView extends StatelessWidget {
|
||||
final ComicCategoryController controller;
|
||||
ComicCategoryView({Key? key})
|
||||
: controller = Get.put(ComicCategoryController()),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
var count = constraints.maxWidth ~/ 160;
|
||||
if (count < 3) count = 3;
|
||||
return KeepAliveWrapper(
|
||||
child: PageGridView(
|
||||
pageController: controller,
|
||||
firstRefresh: true,
|
||||
loadMore: false,
|
||||
crossAxisCount: count,
|
||||
padding: AppStyle.edgeInsetsH12.copyWith(bottom: 12),
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
itemBuilder: (context, i) {
|
||||
var item = controller.list[i];
|
||||
return ShadowCard(
|
||||
onTap: () {
|
||||
controller.toDetail(item);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: NetImage(
|
||||
item.cover,
|
||||
borderRadius: 8,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: AppStyle.edgeInsetsA8,
|
||||
child: Text(
|
||||
item.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(height: 1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
57
lib/modules/comic/home/comic_home_controller.dart
Normal file
57
lib/modules/comic/home/comic_home_controller.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/app/event_bus.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/category/comic_category_controller.dart';
|
||||
//import 'package:flutter_dmzj/modules/comic/home/category/comic_category_controller.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/latest/comic_latest_controller.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/rank/comic_rank_controller.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/recommend/comic_recommend_controller.dart';
|
||||
//import 'package:flutter_dmzj/modules/comic/home/special/comic_special_controller.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicHomeController extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
late TabController tabController;
|
||||
|
||||
StreamSubscription<dynamic>? streamSubscription;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
streamSubscription = EventBus.instance.listen(
|
||||
EventBus.kBottomNavigationBarClicked,
|
||||
(index) {
|
||||
if (index == 0) {
|
||||
refreshOrScrollTop();
|
||||
}
|
||||
},
|
||||
);
|
||||
tabController = TabController(length: 4, vsync: this);
|
||||
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void refreshOrScrollTop() {
|
||||
var tabIndex = tabController.index;
|
||||
BasePageController? controller;
|
||||
if (tabIndex == 0) {
|
||||
controller = Get.find<ComicRecommendController>();
|
||||
} else if (tabIndex == 1) {
|
||||
controller = Get.find<ComicLatestController>();
|
||||
} else if (tabIndex == 2) {
|
||||
controller = Get.find<ComicCategoryController>();
|
||||
} else if (tabIndex == 3) {
|
||||
controller = Get.find<ComicRankController>();
|
||||
}
|
||||
// else if (tabIndex == 4) {
|
||||
// controller = Get.find<ComicSpecialController>();
|
||||
// }
|
||||
controller?.scrollToTopOrRefresh();
|
||||
}
|
||||
|
||||
void search() {
|
||||
AppNavigator.toComicSearch();
|
||||
}
|
||||
}
|
||||
62
lib/modules/comic/home/comic_home_page.dart
Normal file
62
lib/modules/comic/home/comic_home_page.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/platform_utils.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/category/comic_category_view.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/comic_home_controller.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/latest/comic_latest_view.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/rank/comic_rank_view.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/recommend/comic_recommend_view.dart';
|
||||
//import 'package:flutter_dmzj/modules/comic/home/special/comic_special_view.dart';
|
||||
import 'package:flutter_dmzj/widgets/tab_appbar.dart';
|
||||
import 'package:flutter_dmzj/widgets/windows_tab_page.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicHomePage extends GetView<ComicHomeController> {
|
||||
const ComicHomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (PlatformUtils.isWindows) {
|
||||
return WindowsTabPage(
|
||||
tabs: [
|
||||
WindowsTabItem(label: '推荐', body: ComicRecommendView()),
|
||||
WindowsTabItem(label: '更新', body: ComicLatestView()),
|
||||
WindowsTabItem(label: '分类', body: ComicCategoryView()),
|
||||
WindowsTabItem(label: '排行', body: ComicRankView()),
|
||||
],
|
||||
headerAction: IconButton(
|
||||
onPressed: controller.search,
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: TabAppBar(
|
||||
tabs: const [
|
||||
Tab(text: "推荐"),
|
||||
Tab(text: "更新"),
|
||||
Tab(text: "分类"),
|
||||
Tab(text: "排行"),
|
||||
// Tab(text: "专题"),
|
||||
],
|
||||
controller: controller.tabController,
|
||||
action: IconButton(
|
||||
onPressed: controller.search,
|
||||
icon: const Icon(
|
||||
Icons.search,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: controller.tabController,
|
||||
children: [
|
||||
ComicRecommendView(),
|
||||
ComicLatestView(),
|
||||
ComicCategoryView(),
|
||||
ComicRankView(),
|
||||
//ComicSpecialView(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
21
lib/modules/comic/home/latest/comic_latest_controller.dart
Normal file
21
lib/modules/comic/home/latest/comic_latest_controller.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/models/comic/update_item_model.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicLatestController extends BasePageController<ComicUpdateItemModel> {
|
||||
final ComicRequest request = ComicRequest();
|
||||
Map types = {
|
||||
"全部漫画": 100,
|
||||
"原创漫画": 1,
|
||||
"译制漫画": 0,
|
||||
};
|
||||
var type = 100.obs;
|
||||
|
||||
@override
|
||||
Future<List<ComicUpdateItemModel>> getData(int page, int pageSize) async {
|
||||
var ls = await request.latest(type: type.value, page: page);
|
||||
|
||||
return ls;
|
||||
}
|
||||
}
|
||||
176
lib/modules/comic/home/latest/comic_latest_view.dart
Normal file
176
lib/modules/comic/home/latest/comic_latest_view.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_constant.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/app/utils.dart';
|
||||
import 'package:flutter_dmzj/models/comic/update_item_model.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/latest/comic_latest_controller.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/services/user_service.dart';
|
||||
import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart';
|
||||
import 'package:flutter_dmzj/widgets/net_image.dart';
|
||||
import 'package:flutter_dmzj/widgets/page_list_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicLatestView extends StatelessWidget {
|
||||
final ComicLatestController controller;
|
||||
ComicLatestView({Key? key})
|
||||
: controller = Get.put(ComicLatestController()),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return KeepAliveWrapper(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AppStyle.hGap12,
|
||||
...controller.types.keys.map(
|
||||
(e) => buildFilterButton(
|
||||
title: e,
|
||||
value: controller.types[e],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
AppStyle.vGap12,
|
||||
Expanded(
|
||||
child: PageListView(
|
||||
pageController: controller,
|
||||
firstRefresh: true,
|
||||
showPageLoadding: false,
|
||||
separatorBuilder: (context, i) => Divider(
|
||||
endIndent: 12,
|
||||
indent: 12,
|
||||
color: Colors.grey.withOpacity(.2),
|
||||
height: 1,
|
||||
),
|
||||
itemBuilder: (context, i) {
|
||||
var item = controller.list[i];
|
||||
return buildItem(item);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildItem(ComicUpdateItemModel item) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
AppNavigator.toComicDetail(item.comicId.toInt());
|
||||
},
|
||||
child: Container(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
NetImage(
|
||||
item.cover ?? '',
|
||||
width: 80,
|
||||
height: 110,
|
||||
borderRadius: 4,
|
||||
),
|
||||
AppStyle.hGap12,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
item.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text.rich(
|
||||
TextSpan(children: [
|
||||
const WidgetSpan(
|
||||
child: Icon(
|
||||
Icons.account_circle,
|
||||
color: Colors.grey,
|
||||
size: 18,
|
||||
)),
|
||||
const TextSpan(
|
||||
text: " ",
|
||||
),
|
||||
TextSpan(
|
||||
text: item.authors,
|
||||
style:
|
||||
const TextStyle(color: Colors.grey, fontSize: 14))
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(item.types ?? '',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
const SizedBox(height: 2),
|
||||
Text(item.lastUpdateChapterName ?? '',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
const SizedBox(height: 2),
|
||||
Text("更新于${Utils.formatTimestamp(item.lastUpdatetime ?? 0)}",
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Obx(
|
||||
() => UserService.instance.subscribedComicIds
|
||||
.contains(item.comicId.toInt())
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.favorite),
|
||||
onPressed: () {
|
||||
UserService.instance.cancelSubscribe(
|
||||
[item.comicId.toInt()],
|
||||
AppConstant.kTypeComic,
|
||||
);
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.favorite_border),
|
||||
onPressed: () {
|
||||
UserService.instance.addSubscribe(
|
||||
[item.comicId.toInt()],
|
||||
AppConstant.kTypeComic,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildFilterButton({required String title, required int value}) {
|
||||
return Container(
|
||||
height: 32,
|
||||
margin: AppStyle.edgeInsetsR8,
|
||||
child: Obx(
|
||||
() => TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
controller.type.value == value ? Get.theme.colorScheme.primary : Colors.grey,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: AppStyle.radius24,
|
||||
side: BorderSide(
|
||||
color:
|
||||
controller.type.value == value ? Get.theme.colorScheme.primary : Colors.grey,
|
||||
),
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 14),
|
||||
padding: AppStyle.edgeInsetsH16,
|
||||
),
|
||||
onPressed: () {
|
||||
if (controller.type.value == value) {
|
||||
return;
|
||||
}
|
||||
controller.type.value = value;
|
||||
controller.refreshData();
|
||||
},
|
||||
child: Text(title),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
54
lib/modules/comic/home/rank/comic_rank_controller.dart
Normal file
54
lib/modules/comic/home/rank/comic_rank_controller.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/models/comic/rank_item_model.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicRankController extends BasePageController<ComicRankListItemModel> {
|
||||
final ComicRequest request = ComicRequest();
|
||||
RxMap<int, String> tags = {
|
||||
0: "全部分类",
|
||||
}.obs;
|
||||
var tag = 0.obs;
|
||||
|
||||
Map<int, String> byTimes = {
|
||||
0: "日排行",
|
||||
1: "周排行",
|
||||
2: "月排行",
|
||||
3: "总排行",
|
||||
};
|
||||
var byTime = 3.obs;
|
||||
|
||||
Map<int, String> rankTypes = {
|
||||
0: "人气排行",
|
||||
1: "吐槽排行",
|
||||
2: "订阅排行",
|
||||
};
|
||||
var rankType = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
loadFilter();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void loadFilter() async {
|
||||
try {
|
||||
tags.value = await request.rankFilter();
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ComicRankListItemModel>> getData(int page, int pageSize) async {
|
||||
var ls = await request.rank(
|
||||
tagId: tag.value,
|
||||
byTime: byTime.value,
|
||||
rankType: rankType.value,
|
||||
page: page,
|
||||
);
|
||||
|
||||
return ls;
|
||||
}
|
||||
}
|
||||
200
lib/modules/comic/home/rank/comic_rank_view.dart
Normal file
200
lib/modules/comic/home/rank/comic_rank_view.dart
Normal file
@@ -0,0 +1,200 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_constant.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/app/utils.dart';
|
||||
import 'package:flutter_dmzj/models/comic/rank_item_model.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/rank/comic_rank_controller.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/services/user_service.dart';
|
||||
import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart';
|
||||
import 'package:flutter_dmzj/widgets/net_image.dart';
|
||||
import 'package:flutter_dmzj/widgets/page_list_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicRankView extends StatelessWidget {
|
||||
final ComicRankController controller;
|
||||
ComicRankView({Key? key})
|
||||
: controller = Get.put(ComicRankController()),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return KeepAliveWrapper(
|
||||
child: Column(
|
||||
children: [
|
||||
Obx(
|
||||
() => Row(
|
||||
children: [
|
||||
buildFilter(
|
||||
// ignore: invalid_use_of_protected_member
|
||||
types: controller.tags.value,
|
||||
value: controller.tag.value,
|
||||
onSelected: (e) {
|
||||
controller.tag.value = e;
|
||||
controller.refreshData();
|
||||
},
|
||||
),
|
||||
buildFilter(
|
||||
types: controller.byTimes,
|
||||
value: controller.byTime.value,
|
||||
onSelected: (e) {
|
||||
controller.byTime.value = e;
|
||||
controller.refreshData();
|
||||
},
|
||||
),
|
||||
buildFilter(
|
||||
types: controller.rankTypes,
|
||||
value: controller.rankType.value,
|
||||
onSelected: (e) {
|
||||
controller.rankType.value = e;
|
||||
controller.refreshData();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AppStyle.vGap12,
|
||||
Expanded(
|
||||
child: PageListView(
|
||||
pageController: controller,
|
||||
firstRefresh: true,
|
||||
showPageLoadding: false,
|
||||
separatorBuilder: (context, i) => Divider(
|
||||
endIndent: 12,
|
||||
indent: 12,
|
||||
color: Colors.grey.withOpacity(.2),
|
||||
height: 1,
|
||||
),
|
||||
itemBuilder: (context, i) {
|
||||
var item = controller.list[i];
|
||||
return buildItem(item);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildFilter({
|
||||
required Map<int, String> types,
|
||||
required int value,
|
||||
required Function(int) onSelected,
|
||||
}) {
|
||||
return Expanded(
|
||||
child: PopupMenuButton<int>(
|
||||
onSelected: onSelected,
|
||||
itemBuilder: (c) => types.keys
|
||||
.map(
|
||||
(k) => CheckedPopupMenuItem<int>(
|
||||
value: k,
|
||||
checked: k == value,
|
||||
child: Text(types[k] ?? ""),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
types[value] ?? "",
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: Colors.grey,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildItem(ComicRankListItemModel item) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
AppNavigator.toComicDetail(item.comicId.toInt());
|
||||
},
|
||||
child: Container(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
NetImage(
|
||||
item.cover ?? '',
|
||||
width: 80,
|
||||
height: 110,
|
||||
borderRadius: 4,
|
||||
),
|
||||
AppStyle.hGap12,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
item.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text.rich(
|
||||
TextSpan(children: [
|
||||
const WidgetSpan(
|
||||
child: Icon(
|
||||
Icons.account_circle,
|
||||
color: Colors.grey,
|
||||
size: 18,
|
||||
)),
|
||||
const TextSpan(
|
||||
text: " ",
|
||||
),
|
||||
TextSpan(
|
||||
text: item.authors,
|
||||
style:
|
||||
const TextStyle(color: Colors.grey, fontSize: 14))
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(item.types ?? '-',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
const SizedBox(height: 2),
|
||||
Text(item.lastUpdateChapterName ?? '-',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
const SizedBox(height: 2),
|
||||
Text("更新于${Utils.formatTimestamp(item.lastUpdatetime ?? 0)}",
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Obx(
|
||||
() => UserService.instance.subscribedComicIds
|
||||
.contains(item.comicId.toInt())
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.favorite),
|
||||
onPressed: () {
|
||||
UserService.instance.cancelSubscribe(
|
||||
[item.comicId.toInt()],
|
||||
AppConstant.kTypeComic,
|
||||
);
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.favorite_border),
|
||||
onPressed: () {
|
||||
UserService.instance.addSubscribe(
|
||||
[item.comicId.toInt()],
|
||||
AppConstant.kTypeComic,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
188
lib/modules/comic/home/recommend/comic_recommend_controller.dart
Normal file
188
lib/modules/comic/home/recommend/comic_recommend_controller.dart
Normal file
@@ -0,0 +1,188 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/app/log.dart';
|
||||
import 'package:flutter_dmzj/models/comic/recommend_model.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/comic_home_controller.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/services/user_service.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class ComicRecommendController extends BasePageController<ComicRecommendModel> {
|
||||
final ComicRequest request = ComicRequest();
|
||||
StreamSubscription<dynamic>? subLogin;
|
||||
StreamSubscription<dynamic>? subLogout;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
subLogin = UserService.loginedStream.listen((event) {
|
||||
loadSubscribe();
|
||||
});
|
||||
subLogout = UserService.logoutStream.listen((event) {
|
||||
list.removeWhere((x) => x.categoryId == 49);
|
||||
});
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ComicRecommendModel>> getData(int page, int pageSize) async {
|
||||
var ls = await request.recommend();
|
||||
|
||||
// ls.insert(
|
||||
// ls.length > 3 ? 2 : 1,
|
||||
// ComicRecommendModel(
|
||||
// categoryId: 50,
|
||||
// title: "随便看看",
|
||||
// sort: 6,
|
||||
// data: [],
|
||||
// ),
|
||||
// );
|
||||
//loadRandom();
|
||||
if (UserService.instance.logined.value) {
|
||||
loadSubscribe();
|
||||
}
|
||||
return ls;
|
||||
}
|
||||
|
||||
// /// 加载随机漫画
|
||||
// Future<void> loadRandom() async {
|
||||
// try {
|
||||
// var result = await request.refreshRecommend(50);
|
||||
// var index = list.indexWhere((x) => x.categoryId == 50);
|
||||
// if (index != -1) {
|
||||
// list[index].data = result;
|
||||
// } else {
|
||||
// list.insert(
|
||||
// list.length > 3 ? 2 : 1,
|
||||
// result,
|
||||
// );
|
||||
// }
|
||||
// } catch (e) {
|
||||
// Log.logPrint(e);
|
||||
// }
|
||||
// }
|
||||
|
||||
/// 刷新国漫
|
||||
Future<void> refreshGuoman() async {
|
||||
try {
|
||||
var index = list.indexWhere((x) => x.categoryId == 111);
|
||||
var result =
|
||||
await request.refreshRecommend(111, size: 6, page: list[index].page);
|
||||
|
||||
if (index != -1) {
|
||||
list[index].data = result;
|
||||
list[index].page++;
|
||||
list.refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.logPrint(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新近期必看
|
||||
Future<void> refreshRecommend() async {
|
||||
try {
|
||||
var index = list.indexWhere((x) => x.categoryId == 110);
|
||||
|
||||
var result = await request.refreshRecommend(110, page: list[index].page);
|
||||
|
||||
if (index != -1) {
|
||||
list[index].data = result;
|
||||
list[index].page++;
|
||||
list.refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.logPrint(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载订阅
|
||||
void loadSubscribe() async {
|
||||
try {
|
||||
var result = await request.recommendSubscribe();
|
||||
var index = list.indexWhere((x) => x.categoryId == 49);
|
||||
if (index != -1) {
|
||||
list[index] = result;
|
||||
} else {
|
||||
list.insert(1, result);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.logPrint(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新热门连载
|
||||
Future<void> refreshHot() async {
|
||||
try {
|
||||
var index = list.indexWhere((x) => x.categoryId == 112);
|
||||
var result =
|
||||
await request.refreshRecommend(112, page: list[index].page, size: 6);
|
||||
|
||||
if (index != -1) {
|
||||
list[index].data = result;
|
||||
list[index].page++;
|
||||
list.refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.logPrint(e);
|
||||
}
|
||||
}
|
||||
|
||||
void openDetail(ComicRecommendItemModel item) {
|
||||
//漫画=1
|
||||
if (item.type == null || item.type == 1) {
|
||||
AppNavigator.toComicDetail(
|
||||
item.objId ?? item.id ?? 0,
|
||||
);
|
||||
} else if (item.type == 5) {
|
||||
//专题=5
|
||||
AppNavigator.toSpecialDetail(
|
||||
item.objId ?? 0,
|
||||
);
|
||||
} else if (item.type == 6) {
|
||||
//网页=6
|
||||
AppNavigator.toWebView(item.url ?? "");
|
||||
} else if (item.type == 7) {
|
||||
//新闻=7
|
||||
AppNavigator.toNewsDetail(
|
||||
url: item.url ?? "",
|
||||
newsId: item.objId ?? 0,
|
||||
title: item.title,
|
||||
);
|
||||
} else if (item.type == 8) {
|
||||
//作者=8
|
||||
AppNavigator.toComicAuthorDetail(item.objId ?? 0);
|
||||
} else if (item.type == 13) {
|
||||
//社区=13
|
||||
//直接跳转至网页
|
||||
launchUrlString(
|
||||
"http://m.forum.idmzj.com/thread/detail?tid=${item.objId}",
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
// AppNavigator.toWebView(
|
||||
// "http://m.forum.dmzj.com/thread/detail?tid=${item.objId}",
|
||||
// );
|
||||
} else {
|
||||
SmartDialog.showToast("未知类型,无法跳转");
|
||||
}
|
||||
}
|
||||
|
||||
void toSpecial() {
|
||||
var homeController = Get.find<ComicHomeController>();
|
||||
homeController.tabController.animateTo(4);
|
||||
}
|
||||
|
||||
void toMySubscribe() {
|
||||
AppNavigator.toUserSubscribe();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
subLogin?.cancel();
|
||||
subLogout?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
372
lib/modules/comic/home/recommend/comic_recommend_view.dart
Normal file
372
lib/modules/comic/home/recommend/comic_recommend_view.dart
Normal file
@@ -0,0 +1,372 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/models/comic/recommend_model.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/recommend/comic_recommend_controller.dart';
|
||||
import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart';
|
||||
import 'package:flutter_dmzj/widgets/net_image.dart';
|
||||
import 'package:flutter_dmzj/widgets/page_list_view.dart';
|
||||
import 'package:flutter_dmzj/widgets/refresh_until_widget.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:flutter_swiper_view/flutter_swiper_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicRecommendView extends StatelessWidget {
|
||||
final ComicRecommendController controller;
|
||||
ComicRecommendView({Key? key})
|
||||
: controller = Get.put(ComicRecommendController()),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return KeepAliveWrapper(
|
||||
child: PageListView(
|
||||
pageController: controller,
|
||||
padding: AppStyle.edgeInsetsH12,
|
||||
firstRefresh: true,
|
||||
loadMore: false,
|
||||
showPageLoadding: true,
|
||||
itemBuilder: (context, i) {
|
||||
var item = controller.list[i];
|
||||
//大图推荐
|
||||
if (item.categoryId == 109) {
|
||||
return buildBanner(item);
|
||||
}
|
||||
//随便看看
|
||||
// if (item.categoryId == 50) {
|
||||
// return buildCard(
|
||||
// context,
|
||||
// child: buildTreeColumnGridView(item.data),
|
||||
// title: item.title.toString(),
|
||||
// action: buildRefresh(onRefresh: controller.loadRandom),
|
||||
// );
|
||||
// }
|
||||
//我的订阅
|
||||
if (item.categoryId == 49) {
|
||||
return buildCard(
|
||||
context,
|
||||
child: buildTreeColumnGridView(item.data),
|
||||
title: item.title.toString(),
|
||||
action: buildShowMore(onTap: controller.toMySubscribe),
|
||||
);
|
||||
}
|
||||
//近期必看\国漫\热门连载\最新上架
|
||||
if (item.categoryId == 110 ||
|
||||
item.categoryId == 111 ||
|
||||
item.categoryId == 112 ||
|
||||
item.categoryId == 56) {
|
||||
Widget? action;
|
||||
//刷新国漫
|
||||
if (item.categoryId == 110) {
|
||||
action = buildRefresh(onRefresh: controller.refreshRecommend);
|
||||
}
|
||||
if (item.categoryId == 111) {
|
||||
action = buildRefresh(onRefresh: controller.refreshGuoman);
|
||||
}
|
||||
if (item.categoryId == 112) {
|
||||
action = buildRefresh(onRefresh: controller.refreshHot);
|
||||
}
|
||||
return buildCard(
|
||||
context,
|
||||
child: buildTreeColumnGridView(item.data),
|
||||
title: item.title.toString(),
|
||||
action: action,
|
||||
);
|
||||
}
|
||||
//火热专题\美漫大事件\条漫
|
||||
if (item.categoryId == 48 ||
|
||||
item.categoryId == 53 ||
|
||||
item.categoryId == 55) {
|
||||
return buildCard(
|
||||
context,
|
||||
child: buildTwoColumnGridView(item.data),
|
||||
title: item.title.toString(),
|
||||
action: item.categoryId == 48
|
||||
? buildShowMore(onTap: controller.toSpecial)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
//大师
|
||||
if (item.categoryId == 51) {
|
||||
return buildCard(
|
||||
context,
|
||||
child: buildAuthorGridView(item.data),
|
||||
title: item.title.toString(),
|
||||
);
|
||||
}
|
||||
return buildCard(
|
||||
context,
|
||||
child: buildTreeColumnGridView(item.data),
|
||||
title: item.title.toString(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildCard(
|
||||
BuildContext context, {
|
||||
required Widget child,
|
||||
required String title,
|
||||
Widget? action,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: AppStyle.edgeInsetsB8,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: AppStyle.radius8,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, height: 1.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: action,
|
||||
),
|
||||
],
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildShowMore({required Function() onTap}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: const Row(
|
||||
children: [
|
||||
Text(
|
||||
"查看更多",
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
Icon(Icons.chevron_right, size: 18, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildRefresh({required Future Function() onRefresh}) {
|
||||
return RefreshUntilWidget(onRefresh: onRefresh, text: "换一批");
|
||||
}
|
||||
|
||||
Widget buildBanner(ComicRecommendModel item) {
|
||||
return Padding(
|
||||
padding: AppStyle.edgeInsetsB12,
|
||||
child: ClipRRect(
|
||||
borderRadius: AppStyle.radius4,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 75 / 40,
|
||||
child: Swiper(
|
||||
itemWidth: 750,
|
||||
itemHeight: 400,
|
||||
autoplay: true,
|
||||
itemCount: item.data.length,
|
||||
itemBuilder: (_, i) => NetImage(
|
||||
item.data[i].cover,
|
||||
width: 750,
|
||||
height: 400,
|
||||
),
|
||||
onTap: (i) {
|
||||
controller.openDetail(item.data[i]);
|
||||
},
|
||||
pagination: SwiperCustomPagination(
|
||||
builder: (BuildContext context, SwiperPluginConfig config) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 12,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
),
|
||||
//color: Colors.black12,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black38,
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.data[config.activeIndex].title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: Colors.white),
|
||||
),
|
||||
),
|
||||
AppStyle.hGap8,
|
||||
PageIndicator(
|
||||
controller: config.pageController!,
|
||||
count: config.itemCount,
|
||||
size: 10,
|
||||
layout: PageIndicatorLayout.SCALE,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTreeColumnGridView(List<ComicRecommendItemModel> items) {
|
||||
return MasonryGridView.count(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) {
|
||||
var item = items[i];
|
||||
return InkWell(
|
||||
onTap: () => controller.openDetail(item),
|
||||
borderRadius: AppStyle.radius4,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: AppStyle.radius4,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 27 / 36,
|
||||
child: NetImage(
|
||||
item.cover,
|
||||
width: 270,
|
||||
height: 360,
|
||||
),
|
||||
),
|
||||
),
|
||||
AppStyle.vGap8,
|
||||
Text(
|
||||
item.title,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(height: 1.2),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
item.subTitle ?? item.status ?? '',
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
height: 1.2,
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
AppStyle.vGap8,
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAuthorGridView(List<ComicRecommendItemModel> items) {
|
||||
return MasonryGridView.count(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) {
|
||||
var item = items[i];
|
||||
return InkWell(
|
||||
onTap: () => controller.openDetail(item),
|
||||
borderRadius: AppStyle.radius8,
|
||||
child: Padding(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
NetImage(
|
||||
item.cover,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 32,
|
||||
),
|
||||
Padding(
|
||||
padding: AppStyle.edgeInsetsV8,
|
||||
child: Text(
|
||||
item.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(height: 1.2, fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTwoColumnGridView(List<ComicRecommendItemModel> items) {
|
||||
return MasonryGridView.count(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, i) {
|
||||
var item = items[i];
|
||||
return InkWell(
|
||||
onTap: () => controller.openDetail(item),
|
||||
borderRadius: AppStyle.radius4,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: AppStyle.radius4,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 32 / 17,
|
||||
child: NetImage(
|
||||
item.cover,
|
||||
width: 320,
|
||||
height: 170,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: AppStyle.edgeInsetsV8,
|
||||
child: Text(
|
||||
item.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(height: 1.2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
23
lib/modules/comic/home/special/comic_special_controller.dart
Normal file
23
lib/modules/comic/home/special/comic_special_controller.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/models/comic/special_model.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
|
||||
class ComicSpecialController extends BasePageController<ComicSpecialModel> {
|
||||
final ComicRequest request = ComicRequest();
|
||||
|
||||
@override
|
||||
Future<List<ComicSpecialModel>> getData(int page, int pageSize) async {
|
||||
var ls = await request.special(page: page - 1);
|
||||
|
||||
return ls;
|
||||
}
|
||||
|
||||
void toDetail(ComicSpecialModel item) {
|
||||
if (item.pageType == 3) {
|
||||
AppNavigator.toSpecialDetail(item.id);
|
||||
} else {
|
||||
AppNavigator.toWebView(item.pageUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
lib/modules/comic/home/special/comic_special_view.dart
Normal file
65
lib/modules/comic/home/special/comic_special_view.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/app/utils.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/home/special/comic_special_controller.dart';
|
||||
import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart';
|
||||
import 'package:flutter_dmzj/widgets/net_image.dart';
|
||||
import 'package:flutter_dmzj/widgets/page_list_view.dart';
|
||||
import 'package:flutter_dmzj/widgets/shadow_card.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicSpecialView extends StatelessWidget {
|
||||
final ComicSpecialController controller;
|
||||
ComicSpecialView({Key? key})
|
||||
: controller = Get.put(ComicSpecialController()),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return KeepAliveWrapper(
|
||||
child: PageListView(
|
||||
pageController: controller,
|
||||
firstRefresh: true,
|
||||
showPageLoadding: false,
|
||||
padding: AppStyle.edgeInsetsA12.copyWith(top: 0),
|
||||
separatorBuilder: (context, i) => AppStyle.vGap12,
|
||||
itemBuilder: (context, i) {
|
||||
var item = controller.list[i];
|
||||
return ShadowCard(
|
||||
onTap: () {
|
||||
controller.toDetail(item);
|
||||
},
|
||||
radius: 8,
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 710 / 284,
|
||||
child: NetImage(
|
||||
item.smallCover,
|
||||
width: 710,
|
||||
height: 354,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: AppStyle.edgeInsetsA8,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(item.title)),
|
||||
Text(
|
||||
Utils.formatTimestampToDate(item.createTime),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
843
lib/modules/comic/reader/comic_reader_controller.dart
Normal file
843
lib/modules/comic/reader/comic_reader_controller.dart
Normal file
@@ -0,0 +1,843 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_dmzj/app/app_constant.dart';
|
||||
import 'package:flutter_dmzj/app/app_error.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/app/utils.dart';
|
||||
import 'package:flutter_dmzj/services/app_settings_service.dart';
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/app/log.dart';
|
||||
import 'package:flutter_dmzj/models/comic/chapter_info.dart';
|
||||
import 'package:flutter_dmzj/models/comic/detail_info.dart';
|
||||
import 'package:flutter_dmzj/models/comic/view_point_model.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.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';
|
||||
import 'package:preload_page_view/preload_page_view.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
class ComicReaderController extends BaseController {
|
||||
/// 是否为条漫
|
||||
final bool isLongComic;
|
||||
final int comicId;
|
||||
final String comicTitle;
|
||||
final String comicCover;
|
||||
final ComicDetailChapterItem chapter;
|
||||
final List<ComicDetailChapterItem> chapters;
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final ComicRequest request = ComicRequest();
|
||||
ComicReaderController({
|
||||
required this.comicId,
|
||||
required this.comicTitle,
|
||||
required this.chapters,
|
||||
required this.chapter,
|
||||
required this.comicCover,
|
||||
required this.isLongComic,
|
||||
}) {
|
||||
chapterIndex.value = chapters.indexOf(chapter);
|
||||
}
|
||||
|
||||
/// APP设置控制器
|
||||
final settings = AppSettingsService.instance;
|
||||
|
||||
/// 预加载控制器
|
||||
final PreloadPageController preloadPageController = PreloadPageController();
|
||||
|
||||
/// 上下模式控制器
|
||||
final ItemScrollController itemScrollController = ItemScrollController();
|
||||
|
||||
/// 监听上下滚动
|
||||
final ItemPositionsListener itemPositionsListener =
|
||||
ItemPositionsListener.create();
|
||||
|
||||
/// 章节详情
|
||||
Rx<ComicChapterDetail> detail =
|
||||
Rx<ComicChapterDetail>(ComicChapterDetail.empty());
|
||||
|
||||
/// 连接信息监听
|
||||
StreamSubscription<ConnectivityResult>? connectivitySubscription;
|
||||
|
||||
/// 电量信息监听
|
||||
StreamSubscription<BatteryState>? batterySubscription;
|
||||
|
||||
/// 当处于放大图片时,锁定滑动手势
|
||||
var lockSwipe = false.obs;
|
||||
|
||||
/// 当前章节索引
|
||||
var chapterIndex = 0.obs;
|
||||
|
||||
/// 当前页面
|
||||
var currentIndex = 0.obs;
|
||||
|
||||
/// 初始化
|
||||
var initialIndex = 0;
|
||||
|
||||
/// 是否显示控制器
|
||||
var showControls = false.obs;
|
||||
|
||||
/// 阅读方向
|
||||
var direction = 0.obs;
|
||||
|
||||
/// 左手模式
|
||||
bool get leftHandMode => settings.comicReaderLeftHandMode.value;
|
||||
|
||||
/// 翻页动画
|
||||
bool get pageAnimation => settings.comicReaderPageAnimation.value;
|
||||
|
||||
/// 观点、吐槽
|
||||
RxList<ComicViewPointModel> viewPoints = RxList<ComicViewPointModel>();
|
||||
|
||||
/// 连接类型
|
||||
Rx<ConnectivityResult> connectivityType =
|
||||
Rx<ConnectivityResult>(ConnectivityResult.other);
|
||||
|
||||
/// 电量信息
|
||||
Rx<int> batteryLevel = 0.obs;
|
||||
|
||||
/// 显示电量
|
||||
RxBool showBattery = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
initConnectivity();
|
||||
initBattery();
|
||||
if (isLongComic) {
|
||||
direction.value = ReaderDirection.kUpToDown;
|
||||
} else {
|
||||
direction.value = settings.comicReaderDirection.value;
|
||||
}
|
||||
|
||||
if (settings.comicReaderFullScreen.value) {
|
||||
setFull();
|
||||
}
|
||||
|
||||
itemPositionsListener.itemPositions.addListener(updateItemPosition);
|
||||
loadDetail();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
/// 初始化电池信息
|
||||
void initBattery() async {
|
||||
try {
|
||||
//没有电池的Mac似乎会闪退,暂时屏蔽Mac
|
||||
//https://github.com/xiaoyaocz/flutter_dmzj/discussions/146
|
||||
if (Platform.isMacOS) {
|
||||
showBattery.value = false;
|
||||
return;
|
||||
}
|
||||
var battery = Battery();
|
||||
batterySubscription =
|
||||
battery.onBatteryStateChanged.listen((BatteryState state) async {
|
||||
try {
|
||||
var level = await battery.batteryLevel;
|
||||
batteryLevel.value = level;
|
||||
showBattery.value = true;
|
||||
} catch (e) {
|
||||
showBattery.value = false;
|
||||
}
|
||||
});
|
||||
batteryLevel.value = await battery.batteryLevel;
|
||||
showBattery.value = true;
|
||||
} catch (e) {
|
||||
showBattery.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化连接状态
|
||||
void initConnectivity() async {
|
||||
var connectivity = Connectivity();
|
||||
connectivitySubscription =
|
||||
connectivity.onConnectivityChanged.listen((ConnectivityResult result) {
|
||||
//提醒
|
||||
if (connectivityType.value != result &&
|
||||
result == ConnectivityResult.mobile) {
|
||||
SmartDialog.showToast("您已切换至数据网络,请注意流量消耗");
|
||||
}
|
||||
connectivityType.value = result;
|
||||
});
|
||||
connectivityType.value = await connectivity.checkConnectivity();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
focusNode.dispose();
|
||||
connectivitySubscription?.cancel();
|
||||
batterySubscription?.cancel();
|
||||
exitFull();
|
||||
itemPositionsListener.itemPositions.removeListener(updateItemPosition);
|
||||
uploadHistory();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void updateItemPosition() {
|
||||
var items = itemPositionsListener.itemPositions.value;
|
||||
if (items.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
var index = items
|
||||
.where((ItemPosition position) => position.itemTrailingEdge > 0)
|
||||
.reduce((ItemPosition min, ItemPosition position) =>
|
||||
position.itemTrailingEdge < min.itemTrailingEdge ? position : min)
|
||||
.index;
|
||||
|
||||
currentIndex.value = index;
|
||||
}
|
||||
|
||||
/// 加载信息
|
||||
void loadDetail() async {
|
||||
try {
|
||||
pageLoadding.value = true;
|
||||
pageError.value = false;
|
||||
|
||||
detail.value = ComicChapterDetail.empty();
|
||||
var chapterId = chapters[chapterIndex.value].chapterId;
|
||||
if (chapters[chapterIndex.value].isVip) {
|
||||
//禁止观看VIP章节
|
||||
throw AppError("请使用动漫之家官方APP观看VIP章节");
|
||||
}
|
||||
//loadViewPoints();
|
||||
|
||||
var result = await request.chapterDetail(
|
||||
comicId: comicId,
|
||||
chapterId: chapterId,
|
||||
useHD: AppSettingsService.instance.comicReaderHD.value,
|
||||
);
|
||||
var his = DBService.instance.getComicHistory(comicId);
|
||||
if (his != null && his.chapterId == chapterId && his.page != 0) {
|
||||
var hisIndex = (his.page - 1) < 0 ? 0 : his.page - 1;
|
||||
if (hisIndex >= result.pageUrls.length - 1) {
|
||||
hisIndex = 0;
|
||||
}
|
||||
initialIndex = hisIndex;
|
||||
} else {
|
||||
initialIndex = 0;
|
||||
}
|
||||
currentIndex.value = initialIndex;
|
||||
// if (settings.comicReaderShowViewPoint.value) {
|
||||
// result.pageUrls.add("TC");
|
||||
// }
|
||||
|
||||
detail.value = result;
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
jumpToPage(initialIndex);
|
||||
});
|
||||
//上传记录
|
||||
uploadHistory();
|
||||
} catch (e) {
|
||||
pageError.value = true;
|
||||
errorMsg.value = e.toString();
|
||||
setShowControls();
|
||||
} finally {
|
||||
pageLoadding.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载吐槽、观点
|
||||
void loadViewPoints() async {
|
||||
try {
|
||||
viewPoints.clear();
|
||||
var result = await request.viewPoints(
|
||||
comicId: comicId,
|
||||
chapterId: chapters[chapterIndex.value].chapterId,
|
||||
);
|
||||
result.sort((a, b) => b.num.value.compareTo(a.num.value));
|
||||
viewPoints.value = result;
|
||||
} catch (e) {
|
||||
//SmartDialog.showToast("读取吐槽失败");
|
||||
Log.logPrint(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置显示/隐藏控制按钮
|
||||
void setShowControls() {
|
||||
if (settings.comicReaderFullScreen.value) {
|
||||
if (showControls.value) {
|
||||
setFull();
|
||||
} else {
|
||||
setFullEdge();
|
||||
}
|
||||
}
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
showControls.value = !showControls.value;
|
||||
});
|
||||
}
|
||||
|
||||
/// 显示目录
|
||||
void showMenu() async {
|
||||
setShowControls();
|
||||
showModalBottomSheet(
|
||||
context: Get.context!,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 500,
|
||||
),
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text("目录(${chapters.length})"),
|
||||
trailing: IconButton(
|
||||
onPressed: Get.back,
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
contentPadding: AppStyle.edgeInsetsL12,
|
||||
),
|
||||
Divider(
|
||||
height: 1.0,
|
||||
color: Theme.of(context).dividerColor.withOpacity(.2),
|
||||
),
|
||||
Expanded(
|
||||
child: ScrollablePositionedList.separated(
|
||||
initialScrollIndex: chapterIndex.value,
|
||||
itemCount: chapters.length,
|
||||
separatorBuilder: (_, i) => Divider(
|
||||
indent: 12,
|
||||
endIndent: 12,
|
||||
height: 1.0,
|
||||
color: Theme.of(context).dividerColor.withOpacity(.2),
|
||||
),
|
||||
itemBuilder: (_, i) {
|
||||
var item = chapters[i];
|
||||
return ListTile(
|
||||
selected: i == chapterIndex.value,
|
||||
selectedTileColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
selectedColor:
|
||||
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
title: Text(item.chapterTitle),
|
||||
subtitle: item.updateTime != 0
|
||||
? Text(
|
||||
"更新于${Utils.formatTimestampToDate(item.updateTime)}")
|
||||
: null,
|
||||
onTap: () {
|
||||
chapterIndex.value = i;
|
||||
loadDetail();
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
routeSettings: const RouteSettings(name: "/modalBottomSheet"),
|
||||
);
|
||||
}
|
||||
|
||||
/// 下一章
|
||||
void nextChapter() {
|
||||
if (chapterIndex.value == chapters.length - 1) {
|
||||
SmartDialog.showToast("后面没有了");
|
||||
return;
|
||||
}
|
||||
|
||||
chapterIndex.value += 1;
|
||||
loadDetail();
|
||||
}
|
||||
|
||||
/// 上一章
|
||||
void forwardChapter() {
|
||||
if (chapterIndex.value == 0) {
|
||||
SmartDialog.showToast("前面没有了");
|
||||
return;
|
||||
}
|
||||
|
||||
chapterIndex.value -= 1;
|
||||
loadDetail();
|
||||
}
|
||||
|
||||
/// 下一页
|
||||
void nextPage() {
|
||||
var value = currentIndex.value;
|
||||
Log.w("下一页$value");
|
||||
var max = detail.value.pageUrls.length;
|
||||
if (value >= max - 1) {
|
||||
nextChapter();
|
||||
} else {
|
||||
jumpToPage(value + 1, anime: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 上一页
|
||||
void forwardPage() {
|
||||
var value = currentIndex.value;
|
||||
Log.w("上一页$value");
|
||||
if (value == 0) {
|
||||
forwardChapter();
|
||||
} else {
|
||||
jumpToPage(value - 1, anime: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 跳转页数
|
||||
void jumpToPage(int page, {bool anime = false}) {
|
||||
//竖向
|
||||
if (direction.value == ReaderDirection.kUpToDown) {
|
||||
itemScrollController.jumpTo(index: page);
|
||||
} else {
|
||||
anime && pageAnimation
|
||||
? preloadPageController.animateToPage(page,
|
||||
duration: const Duration(milliseconds: 200), curve: Curves.linear)
|
||||
: preloadPageController.jumpToPage(page);
|
||||
}
|
||||
}
|
||||
|
||||
/// 查看吐槽
|
||||
void showComment() {
|
||||
setShowControls();
|
||||
TextEditingController tucaoController = TextEditingController();
|
||||
showModalBottomSheet(
|
||||
context: Get.context!,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 500,
|
||||
),
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text("吐槽(${viewPoints.length})"),
|
||||
trailing: IconButton(
|
||||
onPressed: Get.back,
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
contentPadding: AppStyle.edgeInsetsL12,
|
||||
),
|
||||
Divider(
|
||||
height: 1.0,
|
||||
color: Theme.of(context).dividerColor.withOpacity(.2),
|
||||
),
|
||||
Expanded(
|
||||
child: EasyRefresh(
|
||||
header: const MaterialHeader(),
|
||||
onRefresh: () async {
|
||||
loadViewPoints();
|
||||
},
|
||||
child: Obx(
|
||||
() => settings.comicReaderOldViewPoint.value
|
||||
? SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: viewPoints.map<Widget>((item) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
likeViewPoint(item);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
borderRadius: AppStyle.radius8,
|
||||
),
|
||||
child: Text(
|
||||
item.content,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: viewPoints.length,
|
||||
separatorBuilder: (_, i) => Divider(
|
||||
indent: 12,
|
||||
endIndent: 12,
|
||||
height: 1.0,
|
||||
color: Theme.of(context).dividerColor.withOpacity(.2),
|
||||
),
|
||||
itemBuilder: (_, i) {
|
||||
var item = viewPoints[i];
|
||||
return Padding(
|
||||
padding: AppStyle.edgeInsetsA12
|
||||
.copyWith(top: 8, bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.content,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
AppStyle.hGap12,
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
onPressed: () {
|
||||
likeViewPoint(item);
|
||||
},
|
||||
icon: const Icon(
|
||||
Remix.thumb_up_line,
|
||||
size: 16,
|
||||
),
|
||||
label: Obx(() => Text("${item.num.value}")),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: AppStyle.edgeInsetsA8.copyWith(
|
||||
bottom: 8 + AppStyle.bottomBarHeight,
|
||||
),
|
||||
child: TextField(
|
||||
controller: tucaoController,
|
||||
onSubmitted: (e) {
|
||||
sendViewPoint(e);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: "发表吐槽",
|
||||
contentPadding: AppStyle.edgeInsetsH12,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: TextButton(
|
||||
onPressed: () {
|
||||
sendViewPoint(tucaoController.text);
|
||||
},
|
||||
child: const Text("发布"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
routeSettings: const RouteSettings(name: "/modalBottomSheet"),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示设置
|
||||
void showSettings() {
|
||||
setShowControls();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: Get.context!,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 500,
|
||||
),
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text("设置"),
|
||||
trailing: IconButton(
|
||||
onPressed: Get.back,
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
contentPadding: AppStyle.edgeInsetsL12,
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => ListView(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
children: [
|
||||
buildBGItem(
|
||||
context,
|
||||
child: SwitchListTile(
|
||||
value: settings.comicReaderHD.value,
|
||||
onChanged: (e) {
|
||||
settings.setComicReaderHD(e);
|
||||
loadDetail();
|
||||
},
|
||||
title: const Text("优先加载高清图"),
|
||||
subtitle: const Text("部分单行本可能未分页"),
|
||||
),
|
||||
),
|
||||
//AppStyle.vGap12,
|
||||
Visibility(
|
||||
//条漫不允许修改阅读方向
|
||||
visible: !isLongComic,
|
||||
child: Padding(
|
||||
padding: AppStyle.edgeInsetsT12,
|
||||
child: buildBGItem(
|
||||
context,
|
||||
child: ListTile(
|
||||
title: const Text("阅读方向"),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildSelectedButton(
|
||||
onTap: () {
|
||||
setDirection(ReaderDirection.kLeftToRight);
|
||||
},
|
||||
selected: settings.comicReaderDirection.value ==
|
||||
ReaderDirection.kLeftToRight,
|
||||
child: const Icon(Remix.arrow_right_line),
|
||||
),
|
||||
AppStyle.hGap8,
|
||||
buildSelectedButton(
|
||||
onTap: () {
|
||||
setDirection(ReaderDirection.kRightToLeft);
|
||||
},
|
||||
selected: settings.comicReaderDirection.value ==
|
||||
ReaderDirection.kRightToLeft,
|
||||
child: const Icon(Remix.arrow_left_line),
|
||||
),
|
||||
AppStyle.hGap8,
|
||||
buildSelectedButton(
|
||||
onTap: () {
|
||||
setDirection(ReaderDirection.kUpToDown);
|
||||
},
|
||||
selected: settings.comicReaderDirection.value ==
|
||||
ReaderDirection.kUpToDown,
|
||||
child: const Icon(Remix.arrow_down_line),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AppStyle.vGap12,
|
||||
buildBGItem(
|
||||
context,
|
||||
child: SwitchListTile(
|
||||
value: settings.comicReaderLeftHandMode.value,
|
||||
onChanged: (e) {
|
||||
settings.setComicReaderLeftHandMode(e);
|
||||
},
|
||||
title: const Text("操作反转"),
|
||||
subtitle: const Text("点击左侧下一页,右侧上一页"),
|
||||
),
|
||||
),
|
||||
AppStyle.vGap12,
|
||||
buildBGItem(
|
||||
context,
|
||||
child: SwitchListTile(
|
||||
value: settings.comicReaderFullScreen.value,
|
||||
onChanged: (e) {
|
||||
settings.setComicReaderFullScreen(e);
|
||||
if (e) {
|
||||
setFull();
|
||||
} else {
|
||||
exitFull();
|
||||
}
|
||||
},
|
||||
title: const Text("全屏阅读"),
|
||||
),
|
||||
),
|
||||
AppStyle.vGap12,
|
||||
buildBGItem(
|
||||
context,
|
||||
child: SwitchListTile(
|
||||
value: settings.comicReaderShowStatus.value,
|
||||
onChanged: (e) {
|
||||
settings.setComicReaderShowStatus(e);
|
||||
},
|
||||
title: const Text("显示状态信息"),
|
||||
),
|
||||
),
|
||||
AppStyle.vGap12,
|
||||
buildBGItem(
|
||||
context,
|
||||
child: SwitchListTile(
|
||||
value: settings.comicReaderPageAnimation.value,
|
||||
onChanged: (e) {
|
||||
settings.setComicReaderPageAnimation(e);
|
||||
},
|
||||
title: const Text("翻页动画"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBGItem(BuildContext context, {required Widget child}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: AppStyle.radius8,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSelectedButton(
|
||||
{required Widget child, bool selected = false, Function()? onTap}) {
|
||||
return Builder(builder: (context) {
|
||||
return OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor:
|
||||
selected ? Theme.of(context).colorScheme.primary : Colors.grey,
|
||||
side: BorderSide(
|
||||
color:
|
||||
selected ? Theme.of(context).colorScheme.primary : Colors.grey,
|
||||
),
|
||||
),
|
||||
onPressed: onTap,
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void setDirection(int value) {
|
||||
initialIndex = currentIndex.value;
|
||||
settings.setComicReaderDirection(value);
|
||||
direction.value = value;
|
||||
if (initialIndex != 0) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
jumpToPage(initialIndex);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void setShowViewPoint(bool value) {
|
||||
if (value) {
|
||||
if (!detail.value.pageUrls.contains("TC")) {
|
||||
detail.update((val) {
|
||||
val!.pageUrls.add("TC");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (detail.value.pageUrls.contains("TC")) {
|
||||
detail.update((val) {
|
||||
val!.pageUrls.remove("TC");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void uploadHistory() {
|
||||
var chapter = chapters[chapterIndex.value];
|
||||
UserService.instance.updateComicHistory(
|
||||
comicId: comicId,
|
||||
chapterId: chapter.chapterId,
|
||||
page: currentIndex.value + 1,
|
||||
comicName: comicTitle,
|
||||
comicCover: comicCover,
|
||||
chapterName: chapter.chapterTitle,
|
||||
);
|
||||
}
|
||||
|
||||
/// 进入全屏
|
||||
void setFull() {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// 进入全屏edgeToEdge模式
|
||||
void setFullEdge() {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarIconBrightness: Brightness.light,
|
||||
));
|
||||
}
|
||||
|
||||
/// 退出全屏
|
||||
void exitFull() {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
}
|
||||
|
||||
void likeViewPoint(ComicViewPointModel item) async {
|
||||
try {
|
||||
await request.likeViewPoint(comicId: comicId, id: item.id);
|
||||
|
||||
item.num.value += 1;
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void sendViewPoint(String content) async {
|
||||
if (!await UserService.instance.login()) {
|
||||
SmartDialog.showToast("请先登录");
|
||||
return;
|
||||
}
|
||||
if (content.isEmpty) {
|
||||
SmartDialog.showToast("内容不能为空");
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
try {
|
||||
SmartDialog.showLoading();
|
||||
await request.sendViewPoint(
|
||||
comicId: comicId,
|
||||
chapterId: chapters[chapterIndex.value].chapterId,
|
||||
content: content,
|
||||
page: currentIndex.value + 1,
|
||||
);
|
||||
loadViewPoints();
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
} finally {
|
||||
SmartDialog.dismiss(status: SmartStatus.loading);
|
||||
}
|
||||
}
|
||||
|
||||
void keyDown(LogicalKeyboardKey key) {
|
||||
if (key == LogicalKeyboardKey.arrowLeft ||
|
||||
key == LogicalKeyboardKey.pageUp) {
|
||||
if (leftHandMode) {
|
||||
nextPage();
|
||||
} else {
|
||||
forwardPage();
|
||||
}
|
||||
} else if (key == LogicalKeyboardKey.arrowRight ||
|
||||
key == LogicalKeyboardKey.pageDown) {
|
||||
if (leftHandMode) {
|
||||
forwardPage();
|
||||
} else {
|
||||
nextPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
612
lib/modules/comic/reader/comic_reader_page.dart
Normal file
612
lib/modules/comic/reader/comic_reader_page.dart
Normal file
@@ -0,0 +1,612 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_dmzj/app/app_constant.dart';
|
||||
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/app/log.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/reader/comic_reader_controller.dart';
|
||||
import 'package:flutter_dmzj/widgets/custom_header.dart';
|
||||
import 'package:flutter_dmzj/widgets/local_image.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:photo_view/photo_view.dart';
|
||||
import 'package:preload_page_view/preload_page_view.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
class ComicReaderPage extends GetView<ComicReaderController> {
|
||||
const ComicReaderPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return KeyboardListener(
|
||||
onKeyEvent: (e) {
|
||||
if (e.runtimeType == KeyUpEvent) {
|
||||
controller.keyDown(e.logicalKey);
|
||||
Log.d(e.toString());
|
||||
}
|
||||
},
|
||||
focusNode: controller.focusNode,
|
||||
autofocus: true,
|
||||
child: Theme(
|
||||
data: Theme.of(context),
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Stack(
|
||||
children: [
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: controller.detail.value.chapterId == 0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
controller.setShowControls();
|
||||
},
|
||||
child:
|
||||
controller.direction.value == ReaderDirection.kUpToDown
|
||||
? buildVertical(context)
|
||||
: buildHorizontal(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
controller.leftHandMode
|
||||
? controller.nextPage()
|
||||
: controller.forwardPage();
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: Container(),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
controller.leftHandMode
|
||||
? controller.forwardPage()
|
||||
: controller.nextPage();
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: !controller.pageLoadding.value,
|
||||
child: const AppLoaddingWidget(),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: !controller.pageError.value,
|
||||
child: AppErrorWidget(
|
||||
errorMsg: controller.errorMsg.value,
|
||||
onRefresh: () => controller.loadDetail(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Obx(
|
||||
() => Offstage(
|
||||
offstage: !controller.settings.comicReaderShowStatus.value,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
padding:
|
||||
AppStyle.edgeInsetsA12.copyWith(top: 4, bottom: 4),
|
||||
child: Obx(
|
||||
() => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
buildConnectivity(),
|
||||
buildBattery(),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 100),
|
||||
child: Text(
|
||||
controller.detail.value.chapterTitle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.0,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
AppStyle.hGap8,
|
||||
Text(
|
||||
"${controller.currentIndex.value + 1} / ${controller.detail.value.pageUrls.length}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.0,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
//顶部
|
||||
Obx(
|
||||
() => AnimatedPositioned(
|
||||
top: controller.showControls.value
|
||||
? 0
|
||||
: -(64 + AppStyle.statusBarHeight),
|
||||
left: 0,
|
||||
right: 0,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
height: 64 + AppStyle.statusBarHeight,
|
||||
padding: EdgeInsets.only(top: AppStyle.statusBarHeight),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: Get.back,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
AppStyle.hGap12,
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
controller.chapters[controller.chapterIndex.value]
|
||||
.chapterTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
//底部
|
||||
Obx(
|
||||
() => AnimatedPositioned(
|
||||
bottom: controller.showControls.value
|
||||
? 0
|
||||
: -(136 + AppStyle.bottomBarHeight),
|
||||
left: 0,
|
||||
right: 0,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
height: 136 + AppStyle.bottomBarHeight,
|
||||
padding: EdgeInsets.only(bottom: AppStyle.bottomBarHeight),
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 600,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
buildSilderBar(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
onPressed: controller.forwardChapter,
|
||||
icon: const Icon(Remix.skip_back_line),
|
||||
tooltip: "上一话",
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
onPressed: controller.showMenu,
|
||||
icon: const Icon(Remix.file_list_line),
|
||||
tooltip: "目录",
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
onPressed: controller.showSettings,
|
||||
icon: const Icon(Remix.settings_line),
|
||||
tooltip: "设置",
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
onPressed: controller.nextChapter,
|
||||
icon: const Icon(Remix.skip_forward_line),
|
||||
tooltip: "下一话",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildHorizontal(BuildContext context) {
|
||||
return EasyRefresh(
|
||||
header: MaterialHeader2(
|
||||
triggerOffset: 80,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: AppStyle.radius24,
|
||||
),
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Icon(
|
||||
Icons.arrow_circle_left,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
footer: MaterialFooter2(
|
||||
triggerOffset: 80,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: AppStyle.radius24,
|
||||
),
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Icon(
|
||||
Icons.arrow_circle_right,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
refreshOnStart: false,
|
||||
onRefresh: () async {
|
||||
controller.forwardChapter();
|
||||
},
|
||||
onLoad: () async {
|
||||
controller.nextChapter();
|
||||
},
|
||||
child: PreloadPageView.builder(
|
||||
controller: controller.preloadPageController,
|
||||
onPageChanged: (e) {
|
||||
controller.currentIndex.value = e;
|
||||
},
|
||||
reverse: controller.direction.value == ReaderDirection.kRightToLeft,
|
||||
physics: controller.lockSwipe.value
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
itemCount: controller.detail.value.pageUrls.length,
|
||||
preloadPagesCount: 4,
|
||||
itemBuilder: (_, i) {
|
||||
var url = controller.detail.value.pageUrls[i];
|
||||
// if (i == controller.detail.value.pageUrls.length - 1 && url == "TC") {
|
||||
// return buildViewPoints();
|
||||
// }
|
||||
return PhotoView.customChild(
|
||||
wantKeepAlive: true,
|
||||
initialScale: 1.0,
|
||||
onScaleEnd: (context, detail, e) {
|
||||
controller.lockSwipe.value = (e.scale ?? 1) > 1.0;
|
||||
},
|
||||
child: controller.detail.value.isLocal
|
||||
? LocalImage(url, fit: BoxFit.contain)
|
||||
: NetImage(
|
||||
url,
|
||||
fit: BoxFit.contain,
|
||||
progress: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildVertical(BuildContext context) {
|
||||
return EasyRefresh(
|
||||
header: MaterialHeader2(
|
||||
triggerOffset: 80,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: AppStyle.radius24,
|
||||
),
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Icon(
|
||||
Icons.arrow_circle_up,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
footer: MaterialFooter2(
|
||||
triggerOffset: 80,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: AppStyle.radius24,
|
||||
),
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Icon(
|
||||
Icons.arrow_circle_down,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
refreshOnStart: false,
|
||||
onRefresh: () async {
|
||||
controller.forwardChapter();
|
||||
},
|
||||
onLoad: () async {
|
||||
controller.nextChapter();
|
||||
},
|
||||
child: ScrollablePositionedList.builder(
|
||||
itemScrollController: controller.itemScrollController,
|
||||
itemCount: controller.detail.value.pageUrls.length,
|
||||
itemPositionsListener: controller.itemPositionsListener,
|
||||
itemBuilder: (_, i) {
|
||||
// if (i == controller.detail.value.pageUrls.length - 1 &&
|
||||
// controller.detail.value.pageUrls[i] == "TC") {
|
||||
// return buildViewPoints(shrinkWrap: true);
|
||||
// }
|
||||
var url = controller.detail.value.pageUrls[i];
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 200,
|
||||
),
|
||||
child: controller.detail.value.isLocal
|
||||
? LocalImage(url, fit: BoxFit.contain)
|
||||
: NetImage(
|
||||
url,
|
||||
fit: BoxFit.fitWidth,
|
||||
progress: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSilderBar() {
|
||||
return Obx(
|
||||
() {
|
||||
var value = controller.currentIndex.value + 1.0;
|
||||
var max = controller.detail.value.pageUrls.length.toDouble();
|
||||
if (value > max) {
|
||||
return const SizedBox(
|
||||
height: 48,
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: value,
|
||||
max: max,
|
||||
onChanged: (e) {
|
||||
controller.jumpToPage((e - 1).toInt());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildViewPoints({bool shrinkWrap = false}) {
|
||||
return Obx(
|
||||
() => ListView(
|
||||
shrinkWrap: shrinkWrap,
|
||||
physics: shrinkWrap ? const NeverScrollableScrollPhysics() : null,
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text("吐槽(${controller.viewPoints.length})"),
|
||||
),
|
||||
Padding(
|
||||
padding: AppStyle.edgeInsetsH12,
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: controller.viewPoints
|
||||
.take(10)
|
||||
.map(
|
||||
(e) => OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
onPressed: () {
|
||||
controller.likeViewPoint(e);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
e.content,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: Colors.white),
|
||||
),
|
||||
AppStyle.hGap12,
|
||||
const Icon(
|
||||
Remix.thumb_up_line,
|
||||
size: 16,
|
||||
),
|
||||
AppStyle.hGap4,
|
||||
Obx(
|
||||
() => Text(
|
||||
"${e.num.value}",
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
width: 100,
|
||||
margin: AppStyle.edgeInsetsA12,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
controller.showComment();
|
||||
},
|
||||
child: const Text("查看更多"),
|
||||
),
|
||||
),
|
||||
AppStyle.vGap12,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildConnectivity() {
|
||||
var connectivityType = controller.connectivityType.value;
|
||||
IconData icon = Remix.wifi_line;
|
||||
var name = "WiFi";
|
||||
switch (connectivityType) {
|
||||
case ConnectivityResult.bluetooth:
|
||||
icon = Remix.wifi_line;
|
||||
name = "蓝牙";
|
||||
break;
|
||||
case ConnectivityResult.ethernet:
|
||||
icon = Remix.computer_line;
|
||||
name = "有线";
|
||||
break;
|
||||
case ConnectivityResult.mobile:
|
||||
icon = Remix.base_station_line;
|
||||
name = "流量";
|
||||
break;
|
||||
case ConnectivityResult.wifi:
|
||||
icon = Remix.wifi_line;
|
||||
name = "WiFi";
|
||||
break;
|
||||
case ConnectivityResult.vpn:
|
||||
icon = Remix.shield_keyhole_line;
|
||||
name = "VPN";
|
||||
break;
|
||||
case ConnectivityResult.none:
|
||||
icon = Remix.wifi_off_line;
|
||||
name = "无网络";
|
||||
break;
|
||||
case ConnectivityResult.other:
|
||||
icon = Remix.question_line;
|
||||
name = "未知";
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
AppStyle.hGap4,
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.0,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
AppStyle.hGap8,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBattery() {
|
||||
var battery = controller.batteryLevel.value;
|
||||
// IconData icon = Icons.battery_0_bar;
|
||||
// if (battery >= 90) {
|
||||
// icon = Icons.battery_full;
|
||||
// } else if (battery < 90 && battery >= 80) {
|
||||
// icon = Icons.battery_6_bar;
|
||||
// } else if (battery < 80 && battery >= 70) {
|
||||
// icon = Icons.battery_5_bar;
|
||||
// } else if (battery < 70 && battery >= 50) {
|
||||
// icon = Icons.battery_4_bar;
|
||||
// } else if (battery < 50 && battery >= 30) {
|
||||
// icon = Icons.battery_3_bar;
|
||||
// } else if (battery < 30 && battery >= 20) {
|
||||
// icon = Icons.battery_2_bar;
|
||||
// } else if (battery < 20 && battery >= 10) {
|
||||
// icon = Icons.battery_1_bar;
|
||||
// } else {
|
||||
// icon = Icons.battery_0_bar;
|
||||
// }
|
||||
return Visibility(
|
||||
visible: controller.showBattery.value,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Icon(
|
||||
// icon,
|
||||
// size: 16,
|
||||
// ),
|
||||
// AppStyle.hGap4,
|
||||
Text(
|
||||
"电量 $battery%",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.0,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
AppStyle.hGap8,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
98
lib/modules/comic/search/comic_search_controller.dart
Normal file
98
lib/modules/comic/search/comic_search_controller.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/dialog_utils.dart';
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/app/log.dart';
|
||||
import 'package:flutter_dmzj/models/comic/search_item.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicSearchController extends BasePageController<SearchComicItem> {
|
||||
final String keyword;
|
||||
ComicSearchController(this.keyword) {
|
||||
searchController = TextEditingController(text: keyword);
|
||||
showHotWord.value = keyword.isEmpty;
|
||||
}
|
||||
late TextEditingController searchController;
|
||||
final ComicRequest request = ComicRequest();
|
||||
|
||||
String _keyword = "";
|
||||
|
||||
RxMap<int, String> hotWords = <int, String>{}.obs;
|
||||
|
||||
var showHotWord = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
// loadHotWord();
|
||||
if (keyword.isNotEmpty) {
|
||||
submit();
|
||||
}
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void submit() async {
|
||||
if (searchController.text.isEmpty) {
|
||||
list.clear();
|
||||
showHotWord.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (int.tryParse(searchController.text) != null &&
|
||||
await numberJumpComic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchController.text.startsWith("id:\\") && await handelJumpComic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
showHotWord.value = false;
|
||||
_keyword = searchController.text;
|
||||
refreshData();
|
||||
}
|
||||
|
||||
Future<bool> handelJumpComic() async {
|
||||
var id = int.tryParse(searchController.text.replaceAll("id:\\", "")) ?? 0;
|
||||
if (id != 0) {
|
||||
AppNavigator.toComicDetail(id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future numberJumpComic() async {
|
||||
if (!await DialogUtils.showAlertDialog(
|
||||
"你输入了纯数字,是否跳转至对应的漫画?",
|
||||
title: "漫画ID跳转",
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
return await handelJumpComic();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<SearchComicItem>> getData(int page, int pageSize) async {
|
||||
if (searchController.text.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
// if (AppSettingsService.instance.comicSearchUseWebApi.value) {
|
||||
// //WEB接口不能分页
|
||||
// if (page > 1) {
|
||||
// return [];
|
||||
// }
|
||||
// return await request.searchWeb(keyword: _keyword);
|
||||
// } else {
|
||||
return await request.search(keyword: _keyword, page: page);
|
||||
//}
|
||||
}
|
||||
|
||||
void loadHotWord() async {
|
||||
try {
|
||||
hotWords.value = await request.searchHotWord();
|
||||
} catch (e) {
|
||||
Log.logPrint(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
171
lib/modules/comic/search/comic_search_page.dart
Normal file
171
lib/modules/comic/search/comic_search_page.dart
Normal file
@@ -0,0 +1,171 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/models/comic/search_item.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/search/comic_search_controller.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/widgets/net_image.dart';
|
||||
import 'package:flutter_dmzj/widgets/page_list_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicSearchPage extends StatelessWidget {
|
||||
final String keyword;
|
||||
final ComicSearchController controller;
|
||||
ComicSearchPage({this.keyword = "", super.key})
|
||||
: controller = Get.put(ComicSearchController(keyword));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 8,
|
||||
title: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controller.searchController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: "搜索漫画",
|
||||
contentPadding: AppStyle.edgeInsetsH12,
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: SizedBox(
|
||||
width: 48,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
AppNavigator.closePage();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
suffixIcon: SizedBox(
|
||||
width: 48,
|
||||
child: IconButton(
|
||||
onPressed: controller.submit,
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
),
|
||||
),
|
||||
onSubmitted: (e) {
|
||||
controller.submit();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
PageListView(
|
||||
pageController: controller,
|
||||
firstRefresh: false,
|
||||
showPageLoadding: true,
|
||||
separatorBuilder: (context, i) => Divider(
|
||||
endIndent: 12,
|
||||
indent: 12,
|
||||
color: Colors.grey.withOpacity(.2),
|
||||
height: 1,
|
||||
),
|
||||
itemBuilder: (context, i) {
|
||||
var item = controller.list[i];
|
||||
return buildItem(item);
|
||||
},
|
||||
),
|
||||
// Positioned.fill(
|
||||
// child: Obx(
|
||||
// () => Offstage(
|
||||
// offstage: !controller.showHotWord.value,
|
||||
// child: SingleChildScrollView(
|
||||
// child: Column(
|
||||
// children: [
|
||||
// const ListTile(
|
||||
// title: Text("热门搜索"),
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: AppStyle.edgeInsetsH12.copyWith(bottom: 12),
|
||||
// child: Wrap(
|
||||
// spacing: 8,
|
||||
// runSpacing: 8,
|
||||
// children: controller.hotWords.keys
|
||||
// .map(
|
||||
// (e) => OutlinedButton(
|
||||
// style: OutlinedButton.styleFrom(
|
||||
// tapTargetSize:
|
||||
// MaterialTapTargetSize.shrinkWrap,
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// AppNavigator.toComicDetail(e);
|
||||
// },
|
||||
// child: Text(controller.hotWords[e] ?? ""),
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// ),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildItem(SearchComicItem item) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
AppNavigator.toComicDetail(item.comicId);
|
||||
},
|
||||
child: Container(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
NetImage(
|
||||
item.cover,
|
||||
width: 80,
|
||||
height: 110,
|
||||
borderRadius: 4,
|
||||
),
|
||||
AppStyle.hGap12,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
item.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text.rich(
|
||||
TextSpan(children: [
|
||||
const WidgetSpan(
|
||||
child: Icon(
|
||||
Icons.account_circle,
|
||||
color: Colors.grey,
|
||||
size: 18,
|
||||
)),
|
||||
const TextSpan(
|
||||
text: " ",
|
||||
),
|
||||
TextSpan(
|
||||
text: item.author,
|
||||
style:
|
||||
const TextStyle(color: Colors.grey, fontSize: 14))
|
||||
]),
|
||||
),
|
||||
AppStyle.vGap4,
|
||||
Text(item.tags,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
AppStyle.vGap4,
|
||||
Text(item.lastChapterName,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/models/comic/detail_info.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/services/comic_download_service.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ComicSelectChapterController extends BaseController {
|
||||
final int comicId;
|
||||
ComicSelectChapterController(this.comicId);
|
||||
final ComicRequest request = ComicRequest();
|
||||
|
||||
RxList<ComicDetailVolume> volumes = RxList<ComicDetailVolume>();
|
||||
|
||||
RxSet<int> chapterIds = RxSet<int>();
|
||||
|
||||
String comicTitle = "";
|
||||
String comicCover = "";
|
||||
bool islong = false;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
loadDetail();
|
||||
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void refreshV1() async {
|
||||
try {
|
||||
var result =
|
||||
await request.comicDetail(comicId: comicId, priorityV1: true);
|
||||
if (result.volumes.isEmpty) {
|
||||
SmartDialog.showToast("没有找到任何章节");
|
||||
return;
|
||||
}
|
||||
comicTitle = result.title;
|
||||
comicCover = result.cover;
|
||||
islong = result.isLong;
|
||||
for (var volume in result.volumes) {
|
||||
volume.sortType.value = 1;
|
||||
volume.sort();
|
||||
}
|
||||
volumes.value = result.volumes;
|
||||
} catch (e) {
|
||||
SmartDialog.showToast("无法获取章节");
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载信息
|
||||
void loadDetail() async {
|
||||
try {
|
||||
pageLoadding.value = true;
|
||||
pageError.value = false;
|
||||
var result = await request.comicDetail(comicId: comicId);
|
||||
comicTitle = result.title;
|
||||
comicCover = result.cover;
|
||||
islong = result.isLong;
|
||||
if (result.volumes.isEmpty && !result.isHide) {
|
||||
refreshV1();
|
||||
} else {
|
||||
for (var volume in result.volumes) {
|
||||
volume.sortType.value = 1;
|
||||
volume.sort();
|
||||
}
|
||||
volumes.value = result.volumes;
|
||||
}
|
||||
} catch (e) {
|
||||
pageError.value = true;
|
||||
errorMsg.value = e.toString();
|
||||
} finally {
|
||||
pageLoadding.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void selectItem(ComicDetailChapterItem item) {
|
||||
//禁止下载VIP章节
|
||||
if (item.isVip) {
|
||||
SmartDialog.showToast("请使用动漫之家官方APP下载VIP章节");
|
||||
return;
|
||||
}
|
||||
if (chapterIds.contains(item.chapterId)) {
|
||||
chapterIds.remove(item.chapterId);
|
||||
} else {
|
||||
chapterIds.add(item.chapterId);
|
||||
}
|
||||
}
|
||||
|
||||
void selectAll() {
|
||||
for (var volume in volumes) {
|
||||
for (var chapter in volume.chapters) {
|
||||
if (chapter.isVip) {
|
||||
continue;
|
||||
}
|
||||
var id = "${comicId}_${chapter.chapterId}";
|
||||
if (!ComicDownloadService.instance.downloadIds.contains(id)) {
|
||||
chapterIds.add(chapter.chapterId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cleanAll() {
|
||||
chapterIds.clear();
|
||||
}
|
||||
|
||||
void toDownloadManage() {
|
||||
AppNavigator.toComicDownloadManage(1);
|
||||
}
|
||||
|
||||
void startDownload() {
|
||||
if (chapterIds.isEmpty) {
|
||||
SmartDialog.showToast("请选择需要下载的章节");
|
||||
return;
|
||||
}
|
||||
for (var id in chapterIds) {
|
||||
//搜索章节
|
||||
ComicDetailVolume? volume;
|
||||
ComicDetailChapterItem? chapter;
|
||||
for (var item in volumes) {
|
||||
var chapterItem =
|
||||
item.chapters.firstWhereOrNull((y) => y.chapterId == id);
|
||||
if (chapterItem != null) {
|
||||
volume = item;
|
||||
chapter = chapterItem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (volume == null || chapter == null) {
|
||||
continue;
|
||||
}
|
||||
ComicDownloadService.instance.addTask(
|
||||
comicId: comicId,
|
||||
chapterId: chapter.chapterId,
|
||||
chapterSort: chapter.chapterOrder,
|
||||
volumeName: volume.title,
|
||||
comicTitle: comicTitle,
|
||||
comicCover: comicCover,
|
||||
chapterName: chapter.chapterTitle,
|
||||
isVip: chapter.isVip,
|
||||
isLongComic: islong,
|
||||
);
|
||||
}
|
||||
chapterIds.clear();
|
||||
SmartDialog.showToast("已添加到下载列表,下载过程中请保持APP在前台运行");
|
||||
}
|
||||
}
|
||||
259
lib/modules/comic/select_chapter/comic_select_chapter_page.dart
Normal file
259
lib/modules/comic/select_chapter/comic_select_chapter_page.dart
Normal file
@@ -0,0 +1,259 @@
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
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/comic/select_chapter/comic_select_chapter_controller.dart';
|
||||
import 'package:flutter_dmzj/services/comic_download_service.dart';
|
||||
import 'package:flutter_dmzj/widgets/status/app_error_widget.dart';
|
||||
import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class ComicSelectChapterPage extends StatelessWidget {
|
||||
final int comicId;
|
||||
final ComicSelectChapterController controller;
|
||||
ComicSelectChapterPage(this.comicId, {super.key})
|
||||
: controller = Get.put(
|
||||
ComicSelectChapterController(comicId),
|
||||
tag: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("选择下载章节"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: controller.toDownloadManage,
|
||||
child: const Text("下载管理"),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
EasyRefresh(
|
||||
header: const MaterialHeader(),
|
||||
onRefresh: controller.loadDetail,
|
||||
child: _buildVolumes(),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: !controller.pageLoadding.value,
|
||||
child: const AppLoaddingWidget(),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: !controller.pageError.value,
|
||||
child: AppErrorWidget(
|
||||
errorMsg: controller.errorMsg.value,
|
||||
onRefresh: () => controller.loadDetail(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
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.cleanAll,
|
||||
icon: const Icon(
|
||||
Remix.checkbox_blank_line,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text("取消选中"),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: const TextStyle(fontSize: 14),
|
||||
),
|
||||
onPressed: controller.startDownload,
|
||||
icon: const Icon(
|
||||
Remix.download_line,
|
||||
size: 20,
|
||||
),
|
||||
label:
|
||||
Obx(() => Text("下载选中(${controller.chapterIds.length})")),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVolumes() {
|
||||
return Obx(
|
||||
() => ListView.builder(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
itemCount: controller.volumes.length,
|
||||
itemBuilder: (_, i) {
|
||||
var item = controller.volumes[i];
|
||||
return _buildChapters(item);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
() => Stack(
|
||||
children: [
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: controller.chapterIds
|
||||
.contains(chapter.chapterId)
|
||||
? Get.theme.colorScheme.primary
|
||||
: Get.textTheme.bodyMedium!.color,
|
||||
side: controller.chapterIds
|
||||
.contains(chapter.chapterId)
|
||||
? BorderSide(color: Get.theme.colorScheme.primary)
|
||||
: null,
|
||||
textStyle: const TextStyle(fontSize: 14),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: const Size.fromHeight(40),
|
||||
),
|
||||
onPressed: ComicDownloadService.instance.downloadIds
|
||||
.contains("${comicId}_${chapter.chapterId}")
|
||||
? null
|
||||
: () => controller.selectItem(chapter),
|
||||
child: Text(
|
||||
item.chapters[i].chapterTitle,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -2,
|
||||
top: 0,
|
||||
child: Offstage(
|
||||
offstage: !item.chapters[i].isVip,
|
||||
child: Image.asset(
|
||||
"assets/images/vip_chapter.png",
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
crossAxisCount: count,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter_dmzj/app/app_constant.dart';
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/app/utils.dart';
|
||||
import 'package:flutter_dmzj/models/comic/special_detail_model.dart';
|
||||
import 'package:flutter_dmzj/requests/comic_request.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/services/user_service.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class SpecialDetailController extends BaseController {
|
||||
final int id;
|
||||
SpecialDetailController(this.id);
|
||||
|
||||
final ComicRequest request = ComicRequest();
|
||||
|
||||
Rx<ComicSpecialDetailModel?> detail = Rx<ComicSpecialDetailModel?>(null);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
loadData();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void loadData() async {
|
||||
try {
|
||||
pageLoadding.value = true;
|
||||
pageError.value = false;
|
||||
var result = await request.specialDetail(id: id);
|
||||
detail.value = result;
|
||||
} catch (e) {
|
||||
handleError(e, showPageError: true);
|
||||
} finally {
|
||||
pageLoadding.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void subscribeAll() {
|
||||
if (detail.value == null) {
|
||||
return;
|
||||
}
|
||||
UserService.instance.addSubscribe(
|
||||
detail.value!.comics.map((e) => e.id).toList(),
|
||||
AppConstant.kTypeComic,
|
||||
);
|
||||
}
|
||||
|
||||
void share() {
|
||||
if (detail.value == null) {
|
||||
return;
|
||||
}
|
||||
Utils.share(
|
||||
"http://m.idmzj.com/zhuanti/${detail.value!.pageUrl}",
|
||||
content: detail.value?.title ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
void comment() {
|
||||
if (detail.value == null) {
|
||||
return;
|
||||
}
|
||||
AppNavigator.toComment(objId: id, type: AppConstant.kTypeSpecial);
|
||||
}
|
||||
}
|
||||
221
lib/modules/comic/special_detail/special_detail_page.dart
Normal file
221
lib/modules/comic/special_detail/special_detail_page.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_constant.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/models/comic/special_detail_model.dart';
|
||||
import 'package:flutter_dmzj/modules/comic/special_detail/special_detail_controller.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/services/user_service.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 SpecialDetailPage extends StatelessWidget {
|
||||
final int id;
|
||||
final SpecialDetailController controller;
|
||||
SpecialDetailPage(this.id, {super.key})
|
||||
: controller = Get.put(
|
||||
SpecialDetailController(id),
|
||||
tag: "$id",
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Obx(
|
||||
() => Text(controller.detail.value?.title ?? "专题"),
|
||||
),
|
||||
),
|
||||
body: Obx(
|
||||
() => Stack(
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: controller.detail.value == null,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: (controller.detail.value?.comics.length ?? 0) + 1,
|
||||
separatorBuilder: (context, i) => Divider(
|
||||
endIndent: 12,
|
||||
indent: 12,
|
||||
color: Colors.grey.withOpacity(.2),
|
||||
height: 1,
|
||||
),
|
||||
itemBuilder: (_, i) {
|
||||
if (i == 0) {
|
||||
return buildHeader();
|
||||
}
|
||||
var item = controller.detail.value!.comics[i - 1];
|
||||
return buildItem(item);
|
||||
},
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: !controller.pageLoadding.value,
|
||||
child: const AppLoaddingWidget(),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: !controller.pageError.value,
|
||||
child: AppErrorWidget(
|
||||
errorMsg: controller.errorMsg.value,
|
||||
onRefresh: () => controller.loadData(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: const TextStyle(fontSize: 14),
|
||||
),
|
||||
onPressed: controller.subscribeAll,
|
||||
icon: const Icon(
|
||||
Remix.heart_line,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text("订阅全部"),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: const TextStyle(fontSize: 14),
|
||||
),
|
||||
onPressed: controller.comment,
|
||||
icon: const Icon(
|
||||
Remix.chat_2_line,
|
||||
size: 20,
|
||||
),
|
||||
label: Text(
|
||||
"评论(${controller.detail.value?.commentAmount ?? 0})"),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: const TextStyle(fontSize: 14),
|
||||
),
|
||||
onPressed: controller.share,
|
||||
icon: const Icon(
|
||||
Remix.share_box_line,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text("分享"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildItem(ComicSpecialComicModel item) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
AppNavigator.toComicDetail(item.id);
|
||||
},
|
||||
child: Container(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
NetImage(
|
||||
item.cover,
|
||||
width: 80,
|
||||
height: 110,
|
||||
borderRadius: 4,
|
||||
),
|
||||
AppStyle.hGap12,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
AppStyle.vGap4,
|
||||
Text(item.recommendBrief,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
AppStyle.vGap4,
|
||||
Text(item.recommendReason,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Obx(
|
||||
() => UserService.instance.subscribedComicIds.contains(item.id)
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.favorite),
|
||||
onPressed: () {
|
||||
UserService.instance.cancelSubscribe(
|
||||
[item.id],
|
||||
AppConstant.kTypeComic,
|
||||
);
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.favorite_border),
|
||||
onPressed: () {
|
||||
UserService.instance.addSubscribe(
|
||||
[item.id],
|
||||
AppConstant.kTypeComic,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildHeader() {
|
||||
if (controller.detail.value == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
var detail = controller.detail.value!;
|
||||
return Padding(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 500,
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 710 / 354,
|
||||
child: NetImage(
|
||||
detail.mobileHeaderPic,
|
||||
borderRadius: 8,
|
||||
width: 710,
|
||||
height: 354,
|
||||
),
|
||||
),
|
||||
),
|
||||
AppStyle.vGap12,
|
||||
Text(
|
||||
detail.description,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user