671 lines
23 KiB
Dart
671 lines
23 KiB
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_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,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|