This commit is contained in:
2026-03-07 17:24:59 +08:00
parent 4418ebecac
commit b0ec8ab4bd
417 changed files with 42546 additions and 2 deletions

View File

@@ -0,0 +1,670 @@
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_color.dart';
import 'package:flutter_dmzj/app/app_constant.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/app/dialog_utils.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_dmzj/modules/novel/reader/novel_horizontal_reader.dart';
import 'package:flutter_dmzj/modules/novel/reader/novel_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:remixicon/remixicon.dart';
class NovelReaderPage extends GetView<NovelReaderController> {
const NovelReaderPage({Key? key}) : super(key: key);
Color get color =>
AppColor.novelThemes[controller.settings.novelReaderTheme.value]!.last;
@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: Obx(
() => Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: AppColor
.novelThemes[controller.settings.novelReaderTheme.value]!.first,
body: Stack(
children: [
Obx(
() => Offstage(
offstage: controller.content.value.isEmpty,
child: GestureDetector(
onTap: () {
controller.setShowControls();
},
child: controller.isPicture.value
? buildPicture(context)
: (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.loadContent(),
),
),
),
buildBottomStatus(),
//顶部
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: Text(
controller.chapters[controller.chapterIndex.value]
.chapterName,
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: NovelHorizontalReader(
controller.content.value,
controller: controller.pageController,
reverse: controller.direction.value == ReaderDirection.kRightToLeft,
style: TextStyle(
fontSize: controller.settings.novelReaderFontSize.value.toDouble(),
height: controller.settings.novelReaderLineSpacing.value,
color: AppColor
.novelThemes[controller.settings.novelReaderTheme.value]!.last,
),
padding: AppStyle.edgeInsetsA12.copyWith(
top: AppStyle.statusBarHeight + 12,
bottom: (controller.settings.novelReaderShowStatus.value ? 24 : 12),
),
onPageChanged: (i, m) {
controller.currentIndex.value = i;
controller.maxPage.value = m;
},
),
);
}
Widget buildVertical(BuildContext context) {
return SizedBox(
height: double.infinity,
child: Padding(
padding: EdgeInsets.only(
top: AppStyle.statusBarHeight,
),
child: Padding(
padding: AppStyle.edgeInsetsA12.copyWith(
bottom:
(controller.settings.novelReaderShowStatus.value ? 32 : 12),
),
child: 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: SingleChildScrollView(
controller: controller.scrollController,
child: Text(
controller.content.value,
textAlign: TextAlign.justify,
style: TextStyle(
fontSize:
controller.settings.novelReaderFontSize.value.toDouble(),
height: controller.settings.novelReaderLineSpacing.value,
color: AppColor
.novelThemes[controller.settings.novelReaderTheme.value]!
.last,
),
),
),
),
),
),
);
}
Widget buildPicture(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
top: AppStyle.statusBarHeight,
),
child: EasyRefresh(
header: MaterialHeader2(
triggerOffset: 80,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: AppStyle.radius24,
),
padding: AppStyle.edgeInsetsA12,
child: Icon(
controller.direction.value != ReaderDirection.kUpToDown
? Icons.arrow_circle_left
: 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(
controller.direction.value != ReaderDirection.kUpToDown
? Icons.arrow_circle_right
: Icons.arrow_circle_down,
color: Theme.of(context).colorScheme.primary,
),
),
),
refreshOnStart: false,
onRefresh: () async {
controller.forwardChapter();
},
onLoad: () async {
controller.nextChapter();
},
child: controller.direction.value != ReaderDirection.kUpToDown
? PageView.builder(
controller: controller.pageController,
itemCount: controller.pictures.length,
reverse:
controller.direction.value == ReaderDirection.kRightToLeft,
onPageChanged: (e) {
controller.currentIndex.value = e;
controller.maxPage.value = controller.pictures.length;
},
itemBuilder: (_, i) {
return Padding(
padding: EdgeInsets.only(
bottom: (controller.settings.novelReaderShowStatus.value
? 24
: 12),
),
child: GestureDetector(
onDoubleTap: () {
DialogUtils.showImageViewer(
i, controller.pictures.toList());
},
child: controller.isLocal
? LocalImage(
controller.pictures[i],
fit: BoxFit.contain,
)
: NetImage(
controller.pictures[i],
fit: BoxFit.contain,
progress: true,
),
),
);
})
: ListView.separated(
controller: controller.scrollController,
itemCount: controller.pictures.length,
padding: EdgeInsets.zero,
separatorBuilder: (_, i) => AppStyle.vGap4,
itemBuilder: (_, i) {
return GestureDetector(
onDoubleTap: () {
DialogUtils.showImageViewer(
i, controller.pictures.toList());
},
child: controller.isLocal
? LocalImage(
controller.pictures[i],
fit: BoxFit.fitWidth,
)
: NetImage(
controller.pictures[i],
fit: BoxFit.fitWidth,
progress: true,
),
);
}),
),
);
}
Widget buildSilderBar() {
if (controller.direction.value == ReaderDirection.kUpToDown) {
return Obx(
() {
var value = controller.progress.value;
var max = 1.0;
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.scrollController.jumpTo(
controller.scrollController.position.maxScrollExtent *
e,
);
},
),
),
],
),
);
},
);
}
return Obx(
() {
var value = controller.currentIndex.value + 1.0;
var max = controller.maxPage.value;
if (value > max) {
return const SizedBox(
height: 48,
);
}
return SizedBox(
height: 48,
child: Row(
children: [
Expanded(
child: Slider(
value: value,
max: max.toDouble(),
onChanged: (e) {
controller.jumpToPage((e - 1).toInt());
},
),
),
],
),
);
},
);
}
Widget buildBottomStatus() {
return Positioned(
right: 8,
left: 8,
bottom: 4,
child: Obx(
() => Offstage(
offstage: !controller.settings.novelReaderShowStatus.value,
child: Container(
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
padding: AppStyle.edgeInsetsA12.copyWith(top: 4, bottom: 4),
child: Obx(
() => Row(
children: [
buildConnectivity(),
buildBattery(),
const Expanded(child: SizedBox()),
controller.direction.value != ReaderDirection.kUpToDown
? Text(
"${controller.currentIndex.value + 1} / ${controller.maxPage.value}",
style: const TextStyle(
fontSize: 12,
height: 1.0,
color: Colors.white,
fontWeight: FontWeight.bold,
),
)
: Text(
"${(controller.progress.value * 100).toStringAsFixed(0)}%",
style: const TextStyle(
fontSize: 12,
height: 1.0,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
),
);
}
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_rounded;
// } 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: 12, color: color.withOpacity(.6)),
Text(
"电量 $battery%",
style: const TextStyle(
fontSize: 12,
height: 1.0,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
AppStyle.hGap8,
],
),
);
}
}