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,370 @@
import 'package:flutter/material.dart';
import 'package:flutter_dmzj/app/app_style.dart';
import 'package:flutter_dmzj/services/local_storage_service.dart';
import 'package:get/get.dart';
class AppSettingsService extends GetxController {
static AppSettingsService get instance => Get.find<AppSettingsService>();
var themeMode = 0.obs;
var firstRun = false;
@override
void onInit() {
themeMode.value = LocalStorageService.instance
.getValue(LocalStorageService.kThemeMode, 0);
firstRun = LocalStorageService.instance
.getValue(LocalStorageService.kFirstRun, true);
//漫画
comicReaderDirection.value = LocalStorageService.instance
.getValue(LocalStorageService.kComicReaderDirection, 0);
comicReaderFullScreen.value = LocalStorageService.instance
.getValue(LocalStorageService.kComicReaderFullScreen, true);
comicReaderShowStatus.value = LocalStorageService.instance
.getValue(LocalStorageService.kComicReaderShowStatus, true);
comicReaderShowStatus.value = LocalStorageService.instance
.getValue(LocalStorageService.kComicReaderShowStatus, true);
comicReaderShowViewPoint.value = LocalStorageService.instance
.getValue(LocalStorageService.kComicReaderShowViewPoint, true);
comicReaderLeftHandMode.value = LocalStorageService.instance
.getValue(LocalStorageService.kComicReaderLeftHandMode, false);
comicReaderHD.value = LocalStorageService.instance
.getValue(LocalStorageService.kComicReaderHD, false);
comicReaderPageAnimation.value = LocalStorageService.instance
.getValue(LocalStorageService.kComicReaderPageAnimation, true);
comicReaderOldViewPoint.value = LocalStorageService.instance
.getValue(LocalStorageService.kComicReaderOldViewPoint, false);
//小说
novelReaderDirection.value = LocalStorageService.instance
.getValue(LocalStorageService.kNovelReaderDirection, 0);
novelReaderFontSize.value = LocalStorageService.instance
.getValue(LocalStorageService.kNovelReaderFontSize, 16);
novelReaderLineSpacing.value = LocalStorageService.instance
.getValue(LocalStorageService.kNovelReaderLineSpacing, 1.5);
novelReaderTheme.value = LocalStorageService.instance
.getValue(LocalStorageService.kNovelReaderTheme, 0);
novelReaderFullScreen.value = LocalStorageService.instance
.getValue(LocalStorageService.kNovelReaderFullScreen, true);
novelReaderShowStatus.value = LocalStorageService.instance
.getValue(LocalStorageService.kNovelReaderShowStatus, true);
novelReaderLeftHandMode.value = LocalStorageService.instance
.getValue(LocalStorageService.kNovelReaderLeftHandMode, false);
novelReaderPageAnimation.value = LocalStorageService.instance
.getValue(LocalStorageService.kNovelReaderPageAnimation, true);
//下载
downloadAllowCellular.value = LocalStorageService.instance
.getValue(LocalStorageService.kDownloadAllowCellular, true);
downloadComicTaskCount.value = LocalStorageService.instance
.getValue(LocalStorageService.kDownloadComicTaskCount, 5);
downloadNovelTaskCount.value = LocalStorageService.instance
.getValue(LocalStorageService.kDownloadNovelTaskCount, 5);
//搜索API
comicSearchUseWebApi.value = LocalStorageService.instance
.getValue(LocalStorageService.kComicSearchUseWebApi, false);
//字体大小
useSystemFontSize.value = LocalStorageService.instance
.getValue(LocalStorageService.kUseSystemFontSize, false);
//新闻字体
newsFontSize.value = LocalStorageService.instance
.getValue(LocalStorageService.kNewsFontSize, 15);
//自动添加神隐漫画至收藏夹
collectHideComic.value = LocalStorageService.instance
.getValue(LocalStorageService.kCollectHideComic, false);
//代理地址
proxyAddress.value = LocalStorageService.instance
.getValue(LocalStorageService.kProxyAddress, "");
//MD动态取色
useDynamicColor.value = LocalStorageService.instance
.getValue(LocalStorageService.kUseDynamicColor, true);
super.onInit();
}
void changeTheme() {
Get.dialog(
SimpleDialog(
title: const Text("设置主题"),
children: [
RadioListTile<int>(
title: const Text("跟随系统"),
value: 0,
groupValue: themeMode.value,
onChanged: (e) {
Get.back();
setTheme(e ?? 0);
},
),
RadioListTile<int>(
title: const Text("浅色模式"),
value: 1,
groupValue: themeMode.value,
onChanged: (e) {
Get.back();
setTheme(e ?? 1);
},
),
RadioListTile<int>(
title: const Text("深色模式"),
value: 2,
groupValue: themeMode.value,
onChanged: (e) {
Get.back();
setTheme(e ?? 2);
},
),
],
),
);
}
void setTheme(int i) {
themeMode.value = i;
var mode = ThemeMode.values[i];
LocalStorageService.instance.setValue(LocalStorageService.kThemeMode, i);
Get.changeThemeMode(mode);
}
/// 漫画阅读方向
/// * [0] 左右
/// * [1] 上下
/// * [2] 右左
var comicReaderDirection = 0.obs;
void setComicReaderDirection(int direction) {
if (comicReaderDirection.value == direction) {
return;
}
comicReaderDirection.value = direction;
LocalStorageService.instance
.setValue(LocalStorageService.kComicReaderDirection, direction);
}
/// 漫画全屏阅读
RxBool comicReaderFullScreen = true.obs;
void setComicReaderFullScreen(bool value) {
comicReaderFullScreen.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kComicReaderFullScreen, value);
}
/// 漫画阅读显示状态信息
RxBool comicReaderShowStatus = true.obs;
void setComicReaderShowStatus(bool value) {
comicReaderShowStatus.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kComicReaderShowStatus, value);
}
/// 漫画阅读尾页显示观点/吐槽
RxBool comicReaderShowViewPoint = true.obs;
void setComicReaderShowViewPoint(bool value) {
comicReaderShowViewPoint.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kComicReaderShowViewPoint, value);
}
/// 启用旧板吐槽
RxBool comicReaderOldViewPoint = false.obs;
void setComicReaderOldViewPoint(bool value) {
comicReaderOldViewPoint.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kComicReaderOldViewPoint, value);
}
/// 小说阅读方向
/// * [0] 左右
/// * [1] 上下
/// * [2] 右左
var novelReaderDirection = 0.obs;
void setNovelReaderDirection(int direction) {
if (novelReaderDirection.value == direction) {
return;
}
novelReaderDirection.value = direction;
LocalStorageService.instance
.setValue(LocalStorageService.kNovelReaderDirection, direction);
}
/// 小说字体
var novelReaderFontSize = 16.obs;
void setNovelReaderFontSize(int size) {
if (size < 5) {
size = 5;
}
//应该没人需要这么大的字体吧...
if (size > 56) {
size = 56;
}
novelReaderFontSize.value = size;
LocalStorageService.instance
.setValue(LocalStorageService.kNovelReaderFontSize, size);
}
/// 小说行距
var novelReaderLineSpacing = 1.5.obs;
void setNovelReaderLineSpacing(double spacing) {
if (spacing < 1) {
spacing = 1;
}
//应该没人需要这么大的字体吧...
if (spacing > 5) {
spacing = 5;
}
novelReaderLineSpacing.value = spacing;
LocalStorageService.instance
.setValue(LocalStorageService.kNovelReaderLineSpacing, spacing);
}
/// 小说阅读主题
var novelReaderTheme = 0.obs;
void setNovelReaderTheme(int theme) {
novelReaderTheme.value = theme;
LocalStorageService.instance
.setValue(LocalStorageService.kNovelReaderTheme, theme);
}
/// 漫画全屏阅读
RxBool novelReaderFullScreen = true.obs;
void setNovelReaderFullScreen(bool value) {
novelReaderFullScreen.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kNovelReaderFullScreen, value);
}
/// 漫画阅读显示状态信息
RxBool novelReaderShowStatus = true.obs;
void setNovelReaderShowStatus(bool value) {
novelReaderShowStatus.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kNovelReaderShowStatus, value);
}
/// 下载是否允许使用流量
RxBool downloadAllowCellular = true.obs;
void setDownloadAllowCellular(bool value) {
downloadAllowCellular.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kDownloadAllowCellular, value);
}
/// 下载漫画最大任务数
var downloadComicTaskCount = 5.obs;
void setDownloadComicTaskCount(int task) {
downloadComicTaskCount.value = task;
LocalStorageService.instance
.setValue(LocalStorageService.kDownloadComicTaskCount, task);
}
/// 下载漫画最大任务数
var downloadNovelTaskCount = 5.obs;
void setDownloadNovelTaskCount(int task) {
downloadNovelTaskCount.value = task;
LocalStorageService.instance
.setValue(LocalStorageService.kDownloadNovelTaskCount, task);
}
/// 漫画搜索使用Web接口
var comicSearchUseWebApi = false.obs;
void setComicSearchUseWebApi(bool e) {
comicSearchUseWebApi.value = e;
LocalStorageService.instance
.setValue(LocalStorageService.kComicSearchUseWebApi, e);
}
/// 显示字体大小跟随系统
var useSystemFontSize = false.obs;
void setUseSystemFontSize(bool e) {
useSystemFontSize.value = e;
LocalStorageService.instance
.setValue(LocalStorageService.kUseSystemFontSize, e);
}
/// 漫画阅读左手模式
RxBool comicReaderLeftHandMode = false.obs;
void setComicReaderLeftHandMode(bool value) {
comicReaderLeftHandMode.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kComicReaderLeftHandMode, value);
}
/// 小说阅读左手模式
RxBool novelReaderLeftHandMode = false.obs;
void setNovelReaderLeftHandMode(bool value) {
novelReaderLeftHandMode.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kNovelReaderLeftHandMode, value);
}
/// 漫画阅读优先加载高清图
RxBool comicReaderHD = false.obs;
void setComicReaderHD(bool value) {
comicReaderHD.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kComicReaderHD, value);
}
/// 漫画阅读翻页动画
RxBool comicReaderPageAnimation = true.obs;
void setComicReaderPageAnimation(bool value) {
comicReaderPageAnimation.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kComicReaderPageAnimation, value);
}
/// 小说阅读翻页动画
RxBool novelReaderPageAnimation = true.obs;
void setNovelReaderPageAnimation(bool value) {
novelReaderPageAnimation.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kNovelReaderPageAnimation, value);
}
/// 下载漫画最大任务数
var newsFontSize = 15.obs;
void setNewsFontSize(int size) {
newsFontSize.value = size;
LocalStorageService.instance
.setValue(LocalStorageService.kNewsFontSize, size);
}
/// 自动添加神隐漫画至收藏夹
RxBool collectHideComic = false.obs;
void setCollectHideComic(bool value) {
collectHideComic.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kCollectHideComic, value);
}
/// 代理地址
var proxyAddress = "".obs;
void setProxyAddress(String address) {
proxyAddress.value = address;
LocalStorageService.instance
.setValue(LocalStorageService.kProxyAddress, address);
}
void setNoFirstRun() {
LocalStorageService.instance.setValue(LocalStorageService.kFirstRun, false);
}
// 动态颜色方案缓存由DynamicColorBuilder提供
ColorScheme? _lightDynamic;
ColorScheme? _darkDynamic;
void storeDynamicColorSchemes(ColorScheme? light, ColorScheme? dark) {
_lightDynamic = light;
_darkDynamic = dark;
}
/// 是否使用MD动态取色
RxBool useDynamicColor = true.obs;
void setUseDynamicColor(bool value) {
useDynamicColor.value = value;
LocalStorageService.instance
.setValue(LocalStorageService.kUseDynamicColor, value);
final lightScheme = value ? _lightDynamic : null;
final darkScheme = value ? _darkDynamic : null;
// 同时更新亮色和暗色主题
Get.rootController.theme = AppStyle.getLightTheme(colorScheme: lightScheme);
Get.rootController.darkTheme =
AppStyle.getDarkTheme(colorScheme: darkScheme);
Get.rootController.update();
}
}

View File

@@ -0,0 +1,407 @@
import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_dmzj/models/comic/detail_info.dart';
import 'package:flutter_dmzj/models/db/comic_download_info.dart';
import 'package:flutter_dmzj/models/db/download_status.dart';
import 'package:flutter_dmzj/services/app_settings_service.dart';
import 'package:flutter_dmzj/services/download_task/comic_downloader.dart';
import 'package:get/get.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
// ignore: depend_on_referenced_packages
import 'package:collection/collection.dart';
// ignore: depend_on_referenced_packages
import 'package:path/path.dart' as p;
/// 漫画下载管理
// TODO 整理代码
class ComicDownloadService extends GetxService {
static ComicDownloadService get instance => Get.find<ComicDownloadService>();
AppSettingsService settings = AppSettingsService.instance;
late Box<ComicDownloadInfo> box;
String savePath = "";
/// 连接信息监听
StreamSubscription<ConnectivityResult>? connectivitySubscription;
/// 当前连接类型
ConnectivityResult? connectivityType;
/// 当前正在下载的数量
var currentNum = 0;
Future init() async {
var dir = await getApplicationSupportDirectory();
box = await Hive.openBox(
"ZaiComicDownload",
path: dir.path,
);
savePath = await getSavePath();
//监听网络状态
initConnectivity();
//更新ID
updateAllIds();
updateDownlaoded();
}
/// 初始化连接状态
void initConnectivity() async {
try {
var connectivity = Connectivity();
connectivitySubscription = connectivity.onConnectivityChanged
.listen((ConnectivityResult result) {
networkChanged(result);
});
connectivityType = await connectivity.checkConnectivity();
initTasks();
} catch (e) {
Log.logPrint(e);
initTasks();
}
}
/// 网络变更
void networkChanged(ConnectivityResult type) {
if (connectivityType != type && type == ConnectivityResult.mobile) {
//切换至流量
switchCellular();
} else if (connectivityType != type && type == ConnectivityResult.none) {
//网络断开
switchNoNetwork();
} else {
switchToWiFi();
}
connectivityType = type;
}
/// 切换至流量
void switchCellular() {
if (settings.downloadAllowCellular.value) {
//允许使用流量,当成WiFi处理
switchToWiFi();
return;
}
//把任务状态改为pauseCellular
for (var item in taskQueues) {
if (item.status == DownloadStatus.wait ||
item.status == DownloadStatus.loadding ||
item.status == DownloadStatus.downloading ||
item.status == DownloadStatus.waitNetwork) {
item.stopTask();
item.updateStatus(DownloadStatus.pauseCellular, updateTask: false);
}
}
updateQueue();
}
/// 无网络
void switchNoNetwork() {
//把任务状态改为pauseCellular
for (var item in taskQueues) {
if (item.status == DownloadStatus.wait ||
item.status == DownloadStatus.loadding ||
item.status == DownloadStatus.downloading ||
item.status == DownloadStatus.pauseCellular) {
item.stopTask();
item.updateStatus(DownloadStatus.waitNetwork, updateTask: false);
}
}
updateQueue();
}
void switchToWiFi() {
for (var item in taskQueues) {
if (item.status == DownloadStatus.pauseCellular ||
item.status == DownloadStatus.waitNetwork) {
item.updateStatus(DownloadStatus.wait, updateTask: false);
}
}
updateQueue();
}
/// 任务列表
RxList<ComicDownloader> taskQueues = RxList<ComicDownloader>();
/// 已下载完成的
RxList<ComicDownloadedItem> downloaded = RxList<ComicDownloadedItem>();
/// 已下载、下载中的ID
RxSet<String> downloadIds = RxSet<String>();
/// 开始下载任务
void initTasks() async {
var tasks = getDownloadingTask();
for (var item in tasks) {
//任务已被取消
if (item.status == DownloadStatus.cancel) {
box.delete(item.taskId);
continue;
}
//无网络
if (connectivityType == ConnectivityResult.none) {
if (item.status != DownloadStatus.pause) {
item.status = DownloadStatus.waitNetwork;
}
} else if (connectivityType == ConnectivityResult.mobile) {
//不允许使用数据下载
if (!settings.downloadAllowCellular.value) {
if (item.status != DownloadStatus.pause) {
item.status = DownloadStatus.pauseCellular;
}
}
} else {
//只要不是手动暂停的,全部改为等待,添加到下载队列
if (item.status != DownloadStatus.pause) {
item.status = DownloadStatus.wait;
}
}
taskQueues.add(
ComicDownloader(item, onUpdateTask: onUpdateTask),
);
}
updateQueue();
}
/// 更新队列
void updateQueue() {
//如果下载中任务数小于设定值,添加一个任务
//如果任务取消或完成,移除队列
for (var task in List<ComicDownloader>.from(taskQueues)) {
//下载完成或取消,移除队列
if (task.status == DownloadStatus.complete ||
task.status == DownloadStatus.cancel) {
taskQueues.remove(task);
updateDownlaoded();
continue;
}
}
var taskNum = settings.downloadComicTaskCount.value;
var count = taskQueues
.where((x) =>
x.status == DownloadStatus.downloading ||
x.status == DownloadStatus.loadding)
.length;
currentNum = count;
if (taskNum == 0) {
var ls = taskQueues.where((x) => x.status == DownloadStatus.wait);
for (var item in ls) {
item.start();
}
} else {
if (count < taskNum) {
var ls = taskQueues
.where((x) => x.status == DownloadStatus.wait)
.take(taskNum - count);
for (var item in ls) {
item.start();
}
}
}
updateAllIds();
}
void updateAllIds() {
downloadIds.clear();
downloadIds.addAll(box.keys.map((e) => e.toString()));
}
///读取未完成的任务
List<ComicDownloadInfo> getDownloadingTask() {
return box.values
.toList()
.where((x) => x.status != DownloadStatus.complete)
.toList();
}
/// 更新下载完成
void updateDownlaoded() {
var downlaodedList = box.values
.toList()
.where((x) => x.status == DownloadStatus.complete)
.toList();
var comicMap = groupBy(downlaodedList, (ComicDownloadInfo x) => x.comicId);
List<ComicDownloadedItem> comicList = [];
for (var comicId in comicMap.keys) {
var items = comicMap[comicId]!;
var comicName = items.first.comicName;
var comicCover = items.first.comicCover;
var isLongComic = items.first.isLongComic;
List<ComicDetailVolume> volumes = [];
var volumeMap = groupBy(items, (ComicDownloadInfo x) => x.volumeName);
for (var volumeName in volumeMap.keys) {
var chapters = volumeMap[volumeName]!
.map(
(e) => ComicDetailChapterItem(
chapterId: e.chapterId,
chapterTitle: e.chapterName,
updateTime: 0,
fileSize: 0,
chapterOrder: e.chapterSort,
),
)
.toList();
volumes.add(
ComicDetailVolume(
title: volumeName,
chapters: RxList<ComicDetailChapterItem>(chapters),
),
);
}
for (var item in volumes) {
item.sortType.value = 1;
item.sort();
}
comicList.add(
ComicDownloadedItem(
comicName: comicName,
comicCover: comicCover,
comicId: comicId,
chapterCount: items.length,
volumes: volumes,
isLongComic: isLongComic,
),
);
}
downloaded.value = comicList;
}
/// 继续
void resumeAll() {
//更新状态至等待
for (var task in taskQueues) {
if (task.status == DownloadStatus.pause) {
task.stopTask();
task.updateStatus(DownloadStatus.wait, updateTask: false);
}
}
updateQueue();
}
/// 暂停
void pauseAll() {
for (var task in taskQueues) {
if (task.status != DownloadStatus.pause &&
task.status != DownloadStatus.error &&
task.status != DownloadStatus.errorLoad) {
task.stopTask();
task.updateStatus(DownloadStatus.pause, updateTask: false);
}
}
updateQueue();
}
/// 取消任务
void cancelTask(ComicDownloader task) {
// 移除列表
// 移除数据库
// 取消任务
// 删除文件
}
/// 添加一个任务
void addTask({
required int comicId,
required int chapterId,
required String chapterName,
required int chapterSort,
required String volumeName,
required String comicTitle,
required String comicCover,
required bool isVip,
required bool isLongComic,
}) async {
var taskId = "${comicId}_$chapterId";
if (box.containsKey(taskId)) {
return;
}
var info = ComicDownloadInfo(
addTime: DateTime.now(),
chapterId: chapterId,
chapterSort: chapterSort,
comicCover: comicCover,
comicId: comicId,
comicName: comicTitle,
files: [],
index: 0,
savePath: p.join(savePath, taskId),
status: DownloadStatus.wait,
taskId: taskId,
total: 0,
volumeName: volumeName,
chapterName: chapterName,
urls: [],
isVip: isVip,
isLongComic: isLongComic,
);
await box.put(
taskId,
info,
);
taskQueues.add(ComicDownloader(info, onUpdateTask: onUpdateTask));
updateQueue();
}
void onUpdateTask() {
updateQueue();
}
/// 读取保存目录
Future<String> getSavePath() async {
var dir = await getApplicationSupportDirectory();
var comicDir = Directory(p.join(dir.path, "comic"));
if (!await comicDir.exists()) {
comicDir = await comicDir.create(recursive: true);
}
return comicDir.path;
}
///删除
void delete(ComicDownloadInfo info) async {
try {
var dir = Directory(p.join(savePath, info.taskId));
await dir.delete(recursive: true);
} catch (e) {
Log.logPrint(e);
} finally {
await box.delete(info.taskId);
updateDownlaoded();
}
updateAllIds();
}
///删除
void deleteChapter(int comicId, int chapterId) async {
var info = box.get("${comicId}_$chapterId");
if (info != null) {
delete(info);
}
}
}
class ComicDownloadedItem {
final String comicName;
final int comicId;
final String comicCover;
final List<ComicDetailVolume> volumes;
final int chapterCount;
final bool isLongComic;
ComicDownloadedItem({
required this.comicName,
required this.comicCover,
required this.comicId,
required this.chapterCount,
required this.volumes,
required this.isLongComic,
});
}

View File

@@ -0,0 +1,211 @@
import 'package:flutter_dmzj/app/app_constant.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_dmzj/models/db/comic_history.dart';
import 'package:flutter_dmzj/models/db/local_favorite.dart';
import 'package:flutter_dmzj/models/db/novel_history.dart';
import 'package:flutter_dmzj/models/user/comic_history_model.dart';
import 'package:flutter_dmzj/models/user/novel_history_model.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
class DBService extends GetxService {
static DBService get instance => Get.find<DBService>();
late Box newsLikeBox;
late Box<ComicHistory> comicHistoryBox;
late Box<NovelHistory> novelHistoryBox;
late Box<LocalFavorite> localFavoriteBox;
Future init() async {
if (kIsWeb) {
newsLikeBox = await Hive.openBox("ZaiNewsLike");
comicHistoryBox = await Hive.openBox("ZaiComicHistory");
novelHistoryBox = await Hive.openBox("ZaiNovelHistory");
localFavoriteBox = await Hive.openBox("ZaiLocalFavorite");
} else {
var dir = await getApplicationSupportDirectory();
newsLikeBox = await Hive.openBox("ZaiNewsLike", path: dir.path);
comicHistoryBox = await Hive.openBox("ZaiComicHistory", path: dir.path);
novelHistoryBox = await Hive.openBox("ZaiNovelHistory", path: dir.path);
localFavoriteBox =
await Hive.openBox("ZaiLocalFavorite", path: dir.path);
}
}
Future putComicHistory(ComicHistory history) async {
await comicHistoryBox.put(history.comicId, history);
}
Future updateComicHistory(ComicHistory history) async {
var historyItem = getComicHistory(history.comicId);
if (historyItem != null) {
historyItem.chapterId = history.chapterId;
historyItem.chapterName = history.chapterName;
historyItem.page = history.page;
historyItem.updateTime = history.updateTime;
await putComicHistory(historyItem);
} else {
await putComicHistory(history);
}
}
ComicHistory? getComicHistory(int comicId) {
return comicHistoryBox.get(comicId);
}
List<ComicHistory> getComicHistoryList() {
var ls = comicHistoryBox.values.where((x) => x.chapterId != 0).toList();
ls.sort((a, b) => b.updateTime.compareTo(a.updateTime));
return ls;
}
/// 同步远程的漫画记录
void syncRemoteComicHistory(List<UserComicHistoryModel> items) {
try {
for (var item in items) {
var remoteTime =
DateTime.fromMillisecondsSinceEpoch((item.viewingTime ?? 0) * 1000);
//本地是否存在记录
var local = comicHistoryBox.get(item.comicId);
if (local != null && local.chapterId != 0) {
//与本地记录时间做比对,如果较新则覆盖,否则直接跳过处理
if ((local.updateTime.millisecondsSinceEpoch ~/ 1000) <
remoteTime.millisecondsSinceEpoch) {
putComicHistory(
ComicHistory(
comicId: item.comicId,
chapterId: item.chapterId ?? 0,
comicName: item.comicName,
comicCover: item.cover,
chapterName: item.chapterName ?? "-",
updateTime: remoteTime,
page: item.record ?? 0,
),
);
}
} else {
//不存在,直接添加一条
putComicHistory(
ComicHistory(
comicId: item.comicId,
chapterId: item.chapterId ?? 0,
comicName: item.comicName,
comicCover: item.cover,
chapterName: item.chapterName ?? "-",
updateTime: remoteTime,
page: item.record ?? 0,
),
);
}
}
} catch (e) {
Log.logPrint(e);
}
}
Future putNovelHistory(NovelHistory history) async {
await novelHistoryBox.put(history.novelId, history);
}
Future updateNovelHistory(NovelHistory history) async {
var historyItem = getNovelHistory(history.novelId);
if (historyItem != null) {
historyItem.chapterId = history.chapterId;
historyItem.chapterName = history.chapterName;
historyItem.total = history.total;
historyItem.index = history.index;
historyItem.volumeId = history.volumeId;
historyItem.volumeName = history.volumeName;
historyItem.updateTime = history.updateTime;
await putNovelHistory(historyItem);
} else {
await putNovelHistory(history);
}
}
NovelHistory? getNovelHistory(int novelId) {
return novelHistoryBox.get(novelId);
}
List<NovelHistory> getNovelHistoryList() {
var ls = novelHistoryBox.values.where((x) => x.chapterId != 0).toList();
ls.sort((a, b) => b.updateTime.compareTo(a.updateTime));
return ls;
}
/// 同步远程的小说记录
void syncRemoteNovelHistory(List<UserNovelHistoryModel> items) {
try {
for (var item in items) {
var remoteTime =
DateTime.fromMillisecondsSinceEpoch((item.viewingTime ?? 0) * 1000);
//本地是否存在记录
var local = novelHistoryBox.get(item.lnovelId);
if (local != null && local.chapterId != 0) {
//与本地记录时间做比对,如果较新则覆盖,否则直接跳过处理
if ((local.updateTime.millisecondsSinceEpoch ~/ 1000) <
remoteTime.millisecondsSinceEpoch) {
putNovelHistory(
NovelHistory(
novelId: item.lnovelId,
chapterId: item.chapterId ?? 0,
novelName: item.novelName,
novelCover: item.cover,
chapterName: item.chapterName ?? "-",
updateTime: remoteTime,
index: item.record ?? 0,
volumeId: item.volumeId ?? 0,
volumeName: item.volumeName ?? "",
total: item.totalNum ?? 0,
),
);
}
} else {
//不存在,直接添加一条
putNovelHistory(
NovelHistory(
novelId: item.lnovelId,
chapterId: item.chapterId ?? 0,
novelName: item.novelName,
novelCover: item.cover,
chapterName: item.chapterName ?? "-",
updateTime: remoteTime,
index: item.record ?? 0,
volumeId: item.volumeId ?? 0,
volumeName: item.volumeName ?? "",
total: item.totalNum ?? 0,
),
);
}
}
} catch (e) {
Log.logPrint(e);
}
}
bool hasComicFavorited({required int comicId}) {
var id = "${AppConstant.kTypeComic}_$comicId";
return localFavoriteBox.containsKey(id);
}
void putComicFavorite(
{required String title, required String cover, required int comicId}) {
var id = "${AppConstant.kTypeComic}_$comicId";
localFavoriteBox.put(
id,
LocalFavorite(
id: id,
cover: cover,
objId: comicId,
title: title,
type: AppConstant.kTypeComic,
updateTime: DateTime.now(),
),
);
}
void removeComicFavorite({required int comicId}) {
var id = "${AppConstant.kTypeComic}_$comicId";
localFavoriteBox.delete(id);
}
}

View File

@@ -0,0 +1,193 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter_dmzj/app/dialog_utils.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_dmzj/models/db/comic_download_info.dart';
import 'package:flutter_dmzj/models/db/download_status.dart';
import 'package:flutter_dmzj/requests/comic_request.dart';
import 'package:flutter_dmzj/services/app_settings_service.dart';
import 'package:flutter_dmzj/services/comic_download_service.dart';
import 'package:get/get.dart';
// ignore: depend_on_referenced_packages
import 'package:path/path.dart' as p;
class ComicDownloader {
late Rx<ComicDownloadInfo> info;
final Function() onUpdateTask;
ComicDownloader(ComicDownloadInfo item, {required this.onUpdateTask}) {
info = Rx<ComicDownloadInfo>(item);
}
final ComicRequest request = ComicRequest();
DownloadStatus get status => info.value.status;
CancelToken? cancelToken;
Dio dio = Dio(BaseOptions(
headers: {
'Referer': "http://www.zaimanhua.com/",
},
));
void start() {
_getPageUrls();
}
void retry() {
_getPageUrls();
}
void pause() {
stopTask();
updateStatus(DownloadStatus.pause);
}
void cancel() async {
var result = await DialogUtils.showAlertDialog("确定要取消此任务吗?", title: "取消任务");
if (!result) {
return;
}
cancelToken?.cancel();
cancelToken = null;
await _delete();
}
void resume() {
_startDownload();
}
void stopTask() {
cancelToken?.cancel();
cancelToken = null;
}
void _getPageUrls() async {
try {
if (info.value.urls.isNotEmpty) {
_startDownload();
return;
}
updateStatus(DownloadStatus.loadding);
var detail = await request.chapterDetail(
comicId: info.value.comicId,
chapterId: info.value.chapterId,
useHD: AppSettingsService.instance.comicReaderHD.value,
);
if (detail.pageUrls.isEmpty) {
updateStatus(DownloadStatus.errorLoad);
return;
}
info.update((val) {
val!.urls = detail.pageUrls;
val.total = detail.pageUrls.length;
});
await _saveInfo();
_startDownload();
} catch (e) {
updateStatus(DownloadStatus.errorLoad);
}
}
int retryTime = 0;
void _startDownload() async {
updateStatus(DownloadStatus.downloading);
for (var i = info.value.index; i < info.value.total; i++) {
try {
if (status != DownloadStatus.downloading) {
break;
}
var url = info.value.urls[i];
retryTime = 0;
await _downloadImage(url, i);
} catch (e) {
break;
}
}
if (status == DownloadStatus.downloading &&
(info.value.index == info.value.total - 1)) {
updateStatus(DownloadStatus.complete);
}
}
Future _downloadImage(String url, int index) async {
try {
//检查本地是否有缓存,有缓存直接复制本地的
Uint8List bytes;
var localFile = await getCachedImageFile(url);
if (localFile != null) {
bytes = await localFile.readAsBytes();
} else {
cancelToken = CancelToken();
var result = await dio.get(
url,
options: Options(
responseType: ResponseType.bytes,
),
cancelToken: cancelToken,
);
bytes = result.data;
}
var baseName = Uri.parse(url).path;
var fileName = await _saveImage(bytes, index, p.extension(baseName));
info.update((val) {
val!.index = index;
val.files.add(fileName);
});
await _saveInfo();
} catch (e) {
Log.logPrint(e);
if (e is DioException) {
if (e.type == DioExceptionType.cancel) rethrow;
if (status == DownloadStatus.waitNetwork ||
status == DownloadStatus.pauseCellular) rethrow;
if (retryTime < 3) {
retryTime++;
await Future.delayed(const Duration(seconds: 1));
return await _downloadImage(url, index);
}
}
updateStatus(DownloadStatus.error);
rethrow;
}
}
Future<String> _saveImage(
Uint8List bytes, int index, String extension) async {
var dir = info.value.savePath;
var fileName = "${(index + 1).toString().padLeft(3, "0")}$extension";
var file = File(p.join(dir, fileName));
if (!await file.exists()) {
file = await file.create(recursive: true);
}
await file.writeAsBytes(bytes);
return fileName;
}
void updateStatus(DownloadStatus e, {bool updateTask = true}) async {
info.update((val) {
val!.status = e;
});
if (updateTask) {
onUpdateTask();
}
await _saveInfo();
}
/// 保存信息
Future _saveInfo() async {
await ComicDownloadService.instance.box.put(info.value.taskId, info.value);
}
Future _delete() async {
try {
var dir = Directory(info.value.savePath);
await dir.delete(recursive: true);
} finally {
ComicDownloadService.instance.downloadIds.remove(info.value.taskId);
await ComicDownloadService.instance.box.delete(info.value.taskId);
updateStatus(DownloadStatus.cancel);
}
}
}

View File

@@ -0,0 +1,229 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter_dmzj/app/dialog_utils.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_dmzj/models/db/novel_download_info.dart';
import 'package:flutter_dmzj/models/db/download_status.dart';
import 'package:flutter_dmzj/requests/novel_request.dart';
import 'package:flutter_dmzj/services/novel_download_service.dart';
import 'package:get/get.dart';
// ignore: depend_on_referenced_packages
import 'package:path/path.dart' as p;
class NovelDownloader {
late Rx<NovelDownloadInfo> info;
final Function() onUpdateTask;
NovelDownloader(NovelDownloadInfo item, {required this.onUpdateTask}) {
info = Rx<NovelDownloadInfo>(item);
}
final NovelRequest request = NovelRequest();
DownloadStatus get status => info.value.status;
CancelToken? cancelToken;
Dio dio = Dio(BaseOptions(
headers: {
'Referer': "http://www.zaimanhua.com/",
},
));
void start() {
_startDownload();
}
void retry() {
_startDownload();
}
void pause() {
stopTask();
updateStatus(DownloadStatus.pause);
}
void cancel() async {
var result = await DialogUtils.showAlertDialog("确定要取消此任务吗?", title: "取消任务");
if (!result) {
return;
}
cancelToken?.cancel();
cancelToken = null;
await _delete();
}
void resume() {
_startDownload();
}
void stopTask() {
cancelToken?.cancel();
cancelToken = null;
}
int retryTime = 0;
void _startDownload() async {
updateStatus(DownloadStatus.downloading);
retryTime = 0;
await _downloadContent();
int i = 0;
for (var url in info.value.imageUrls) {
try {
if (status != DownloadStatus.downloading) {
break;
}
retryTime = 0;
await _downloadImage(url, i);
i++;
} catch (e) {
break;
}
}
if (status == DownloadStatus.downloading) {
updateStatus(DownloadStatus.complete);
}
}
Future _downloadContent() async {
try {
cancelToken = CancelToken();
var content = await request.novelContent(
volumeId: info.value.volumeID,
chapterId: info.value.chapterId,
cancel: cancelToken,
cache: false,
);
var fileName = await _saveContent(content);
var subStr =
content.substring(0, content.length < 200 ? content.length : 200);
//检查是否是插画
if (subStr.contains(RegExp('<img.*?>'))) {
List<String> imgs = [];
for (var item in RegExp(r'<img.*?src=[' '""](.*?)[' '""].*?>')
.allMatches(content)) {
var src = item.group(1);
if (src != null && src.isNotEmpty) {
imgs.add(src);
}
}
info.update((val) {
val!.fileName = fileName;
val.imageUrls = imgs;
val.isImage = true;
});
} else {
info.update((val) {
val!.fileName = fileName;
val.isImage = false;
});
}
await _saveInfo();
} catch (e) {
Log.logPrint(e);
if (e is DioException) {
if (e.type == DioExceptionType.cancel) rethrow;
if (status == DownloadStatus.waitNetwork ||
status == DownloadStatus.pauseCellular) rethrow;
if (retryTime < 3) {
retryTime++;
await Future.delayed(const Duration(seconds: 1));
return await _downloadContent();
}
}
updateStatus(DownloadStatus.error);
rethrow;
}
}
Future _downloadImage(String url, int index) async {
try {
//检查本地是否有缓存,有缓存直接复制本地的
Uint8List bytes;
var localFile = await getCachedImageFile(url);
if (localFile != null) {
bytes = await localFile.readAsBytes();
} else {
cancelToken = CancelToken();
var result = await dio.get(
url,
options: Options(
responseType: ResponseType.bytes,
),
cancelToken: cancelToken,
);
bytes = result.data;
}
var baseName = Uri.parse(url).path;
var fileName = await _saveImage(bytes, index, p.extension(baseName));
info.update((val) {
val!.imageFiles.add(fileName);
});
await _saveInfo();
} catch (e) {
Log.logPrint(e);
if (e is DioException) {
if (e.type == DioExceptionType.cancel) rethrow;
if (status == DownloadStatus.waitNetwork ||
status == DownloadStatus.pauseCellular) rethrow;
if (retryTime < 3) {
retryTime++;
await Future.delayed(const Duration(seconds: 1));
return await _downloadImage(url, index);
}
}
updateStatus(DownloadStatus.error);
rethrow;
}
}
Future<String> _saveContent(String content) async {
var dir = info.value.savePath;
var fileName = "${info.value.taskId}.txt";
var file = File(p.join(dir, fileName));
if (!await file.exists()) {
file = await file.create(recursive: true);
}
await file.writeAsString(content);
return fileName;
}
Future<String> _saveImage(
Uint8List bytes, int index, String extension) async {
var dir = info.value.savePath;
var fileName = "${(index + 1).toString().padLeft(3, "0")}$extension";
var file = File(p.join(dir, fileName));
if (!await file.exists()) {
file = await file.create(recursive: true);
}
await file.writeAsBytes(bytes);
return fileName;
}
void updateStatus(DownloadStatus e, {bool updateTask = true}) async {
info.update((val) {
val!.status = e;
});
if (updateTask) {
onUpdateTask();
}
await _saveInfo();
}
/// 保存信息
Future _saveInfo() async {
await NovelDownloadService.instance.box.put(info.value.taskId, info.value);
}
Future _delete() async {
try {
var dir = Directory(info.value.savePath);
await dir.delete(recursive: true);
} finally {
await NovelDownloadService.instance.box.delete(info.value.taskId);
updateStatus(DownloadStatus.cancel);
}
}
}

View File

@@ -0,0 +1,201 @@
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
// ignore: depend_on_referenced_packages
import 'package:path/path.dart' as p;
class LocalStorageService extends GetxService {
static LocalStorageService get instance => Get.find<LocalStorageService>();
static bool kDebug = false;
/// 显示模式
/// * [0] 跟随系统
/// * [1] 浅色模式
/// * [2] 深色模式
static const String kThemeMode = "ThemeMode";
/// 首次运行
static const String kFirstRun = "FirstRun";
/// 用户登录信息
/// * 类型LoginResultModel
static const String kUserAuthInfo = "UserAuthInfo";
/// 漫画阅读方向
static const String kComicReaderDirection = "ComicReaderDirection";
/// 漫画全屏阅读
static const String kComicReaderFullScreen = "ComicReaderFullScreen";
/// 漫画阅读显示状态信息
static const String kComicReaderShowStatus = "ComicReaderShowStatus";
/// 漫画阅读尾页显示观点/吐槽
static const String kComicReaderShowViewPoint = "ComicReaderShowViewPoint";
/// 启用旧版吐槽
static const String kComicReaderOldViewPoint = "ComicReaderOldViewPoint";
/// 小说阅读方向
static const String kNovelReaderDirection = "NovelReaderDirection";
/// 小说字体大小
static const String kNovelReaderFontSize = "NovelReaderFontSize";
/// 小说行距
static const String kNovelReaderLineSpacing = "NovelReaderLineSpacing";
/// 小说阅读主题
static const String kNovelReaderTheme = "NovelReaderTheme";
/// 小说阅读显示状态信息
static const String kNovelReaderShowStatus = "NovelReaderShowStatus";
/// 小说全屏阅读
static const String kNovelReaderFullScreen = "NovelReaderFullScreen";
/// 下载是否允许使用流量
static const String kDownloadAllowCellular = "DownloadAllowCellular";
/// 下载小说最大任务数
static const String kDownloadNovelTaskCount = "DownloadNovelTaskCount";
/// 下载漫画最大任务数
static const String kDownloadComicTaskCount = "DownloadComicTaskCount";
/// 漫画搜索使用Web接口
static const String kComicSearchUseWebApi = "ComicSearchUseWebApi";
/// 显示字体大小跟随系统
static const String kUseSystemFontSize = "UseSystemFontSize";
/// 漫画-左手模式
static const String kComicReaderLeftHandMode = "ComicReaderLeftHandMode";
/// 小说-左手模式
static const String kNovelReaderLeftHandMode = "NovelReaderLeftHandMode";
/// 漫画阅读优先加载高清图
static const String kComicReaderHD = "ComicReaderHD";
/// 漫画阅读-翻页动画
static const String kComicReaderPageAnimation = "ComicReaderPageAnimation";
/// 小说阅读-翻页动画
static const String kNovelReaderPageAnimation = "NovelReaderPageAnimation";
/// 新闻字体大小
static const String kNewsFontSize = "NewsFontSize";
/// 自动添加神隐漫画至收藏夹
static const String kCollectHideComic = "CollectHideComic";
/// 代理地址
static const String kProxyAddress = "ProxyAddress";
/// 是否使用MD动态取色
static const String kUseDynamicColor = "UseDynamicColor";
late Box settingsBox;
Future init() async {
if (kIsWeb) {
settingsBox = await Hive.openBox("LocalStorage");
} else {
var dir = await getApplicationSupportDirectory();
settingsBox = await Hive.openBox(
"LocalStorage",
path: dir.path,
);
}
}
T getValue<T>(dynamic key, T defaultValue) {
var value = settingsBox.get(key, defaultValue: defaultValue) as T;
Log.d("Get LocalStorage$key\r\n$value");
return value;
}
Future setValue<T>(dynamic key, T value) async {
Log.d("Set LocalStorage$key\r\n$value");
return await settingsBox.put(key, value);
}
Future removeValue<T>(dynamic key) async {
Log.d("Remove LocalStorage$key");
return await settingsBox.delete(key);
}
bool get isFirst => getValue("First", true);
void setNoFirst() {
setValue("First", false);
}
Future<Directory> getNovelCacheDirectory() async {
var dir = await getApplicationSupportDirectory();
var novelDir = Directory(p.join(dir.path, "novel_cache"));
if (!await novelDir.exists()) {
novelDir = await novelDir.create();
}
return novelDir;
}
Future saveNovelContent({
required int volumeId,
required int chapterId,
required String content,
}) async {
try {
var novelDir = await getNovelCacheDirectory();
var fileName = p.join(novelDir.path, "${volumeId}_$chapterId.txt");
var file = File(fileName);
await file.writeAsString(content);
} catch (e) {
Log.logPrint(e);
}
}
Future<String?> getNovelContent(
{required int volumeId, required int chapterId}) async {
try {
var novelDir = await getNovelCacheDirectory();
var fileName = p.join(novelDir.path, "${volumeId}_$chapterId.txt");
var file = File(fileName);
if (await file.exists()) {
var content = await file.readAsString();
return content;
}
return null;
} catch (e) {
Log.logPrint(e);
return null;
}
}
Future<int> getNovelCacheSize() async {
var novelDir = await getNovelCacheDirectory();
var size = 0;
await for (var item in novelDir.list()) {
size += item.statSync().size;
}
return size;
}
Future<bool> cleanNovelCacheSize() async {
try {
var novelDir = await getNovelCacheDirectory();
await novelDir.delete(recursive: true);
return true;
} catch (e) {
Log.logPrint(e);
return false;
}
}
}

View File

@@ -0,0 +1,401 @@
import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_dmzj/models/db/novel_download_info.dart';
import 'package:flutter_dmzj/models/db/download_status.dart';
import 'package:flutter_dmzj/models/novel/novel_detail_model.dart';
import 'package:flutter_dmzj/services/app_settings_service.dart';
import 'package:flutter_dmzj/services/download_task/novel_downloader.dart';
import 'package:get/get.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
// ignore: depend_on_referenced_packages
import 'package:collection/collection.dart';
// ignore: depend_on_referenced_packages
import 'package:path/path.dart' as p;
/// 小说下载管理
// TODO 整理代码
class NovelDownloadService extends GetxService {
static NovelDownloadService get instance => Get.find<NovelDownloadService>();
AppSettingsService settings = AppSettingsService.instance;
late Box<NovelDownloadInfo> box;
String savePath = "";
/// 连接信息监听
StreamSubscription<ConnectivityResult>? connectivitySubscription;
/// 当前连接类型
ConnectivityResult? connectivityType;
/// 当前正在下载的数量
var currentNum = 0;
Future init() async {
var dir = await getApplicationSupportDirectory();
box = await Hive.openBox(
"NovelDownload",
path: dir.path,
);
savePath = await getSavePath();
//监听网络状态
initConnectivity();
//更新ID
updateAllIds();
updateDownlaoded();
}
/// 初始化连接状态
void initConnectivity() async {
try {
var connectivity = Connectivity();
connectivitySubscription = connectivity.onConnectivityChanged
.listen((ConnectivityResult result) {
networkChanged(result);
});
connectivityType = await connectivity.checkConnectivity();
initTasks();
} catch (e) {
Log.logPrint(e);
initTasks();
}
}
/// 网络变更
void networkChanged(ConnectivityResult type) {
if (connectivityType != type && type == ConnectivityResult.mobile) {
//切换至流量
switchCellular();
} else if (connectivityType != type && type == ConnectivityResult.none) {
//网络断开
switchNoNetwork();
} else {
switchToWiFi();
}
connectivityType = type;
}
/// 切换至流量
void switchCellular() {
if (settings.downloadAllowCellular.value) {
//允许使用流量,当成WiFi处理
switchToWiFi();
return;
}
//把任务状态改为pauseCellular
for (var item in taskQueues) {
if (item.status == DownloadStatus.wait ||
item.status == DownloadStatus.loadding ||
item.status == DownloadStatus.downloading ||
item.status == DownloadStatus.waitNetwork) {
item.stopTask();
item.updateStatus(DownloadStatus.pauseCellular, updateTask: false);
}
}
updateQueue();
}
/// 无网络
void switchNoNetwork() {
//把任务状态改为pauseCellular
for (var item in taskQueues) {
if (item.status == DownloadStatus.wait ||
item.status == DownloadStatus.loadding ||
item.status == DownloadStatus.downloading ||
item.status == DownloadStatus.pauseCellular) {
item.stopTask();
item.updateStatus(DownloadStatus.waitNetwork, updateTask: false);
}
}
updateQueue();
}
void switchToWiFi() {
for (var item in taskQueues) {
if (item.status == DownloadStatus.pauseCellular ||
item.status == DownloadStatus.waitNetwork) {
item.updateStatus(DownloadStatus.wait, updateTask: false);
}
}
updateQueue();
}
/// 任务列表
RxList<NovelDownloader> taskQueues = RxList<NovelDownloader>();
/// 已下载完成的
RxList<NovelDownloadedItem> downloaded = RxList<NovelDownloadedItem>();
/// 已下载、下载中的ID
RxSet<String> downloadIds = RxSet<String>();
/// 开始下载任务
void initTasks() async {
var tasks = getDownloadingTask();
for (var item in tasks) {
//任务已被取消
if (item.status == DownloadStatus.cancel) {
box.delete(item.taskId);
continue;
}
//无网络
if (connectivityType == ConnectivityResult.none) {
if (item.status != DownloadStatus.pause) {
item.status = DownloadStatus.waitNetwork;
}
} else if (connectivityType == ConnectivityResult.mobile) {
//不允许使用数据下载
if (!settings.downloadAllowCellular.value) {
if (item.status != DownloadStatus.pause) {
item.status = DownloadStatus.pauseCellular;
}
}
} else {
//只要不是手动暂停的,全部改为等待,添加到下载队列
if (item.status != DownloadStatus.pause) {
item.status = DownloadStatus.wait;
}
}
taskQueues.add(
NovelDownloader(item, onUpdateTask: onUpdateTask),
);
}
updateQueue();
}
/// 更新队列
void updateQueue() {
//如果下载中任务数小于设定值,添加一个任务
//如果任务取消或完成,移除队列
for (var task in List<NovelDownloader>.from(taskQueues)) {
//下载完成或取消,移除队列
if (task.status == DownloadStatus.complete ||
task.status == DownloadStatus.cancel) {
taskQueues.remove(task);
updateDownlaoded();
continue;
}
}
var taskNum = settings.downloadNovelTaskCount.value;
var count = taskQueues
.where((x) =>
x.status == DownloadStatus.downloading ||
x.status == DownloadStatus.loadding)
.length;
currentNum = count;
if (taskNum == 0) {
var ls = taskQueues.where((x) => x.status == DownloadStatus.wait);
for (var item in ls) {
item.start();
}
} else {
if (count < taskNum) {
var ls = taskQueues
.where((x) => x.status == DownloadStatus.wait)
.take(taskNum - count)
.toList();
for (var item in ls) {
item.start();
}
}
}
updateAllIds();
}
void updateAllIds() {
downloadIds.clear();
downloadIds.addAll(box.keys.map((e) => e.toString()));
}
///读取未完成的任务
List<NovelDownloadInfo> getDownloadingTask() {
return box.values
.toList()
.where((x) => x.status != DownloadStatus.complete)
.toList();
}
/// 更新下载完成
void updateDownlaoded() {
var downlaodedList = box.values
.toList()
.where((x) => x.status == DownloadStatus.complete)
.toList();
var novelMap = groupBy(downlaodedList, (NovelDownloadInfo x) => x.novelId);
List<NovelDownloadedItem> novelList = [];
for (var novelId in novelMap.keys) {
var items = novelMap[novelId]!;
var novelName = items.first.novelName;
var novelCover = items.first.novelCover;
List<NovelDetailVolume> volumes = [];
var volumeMap = groupBy(items, (NovelDownloadInfo x) => x.volumeID);
for (var volumeID in volumeMap.keys) {
var chapters = volumeMap[volumeID]!
.map(
(e) => NovelDetailChapter(
chapterId: e.chapterId,
chapterName: e.chapterName,
volumeId: e.volumeID,
volumeName: e.volumeName,
volumeOrder: e.volumeOrder,
chapterOrder: e.chapterSort,
),
)
.toList();
chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder));
volumes.add(
NovelDetailVolume(
volumeName: chapters.first.volumeName,
volumeId: chapters.first.volumeId,
volumeOrder: chapters.first.volumeOrder,
chapters: chapters,
),
);
}
volumes.sort((a, b) => a.volumeOrder.compareTo(b.volumeOrder));
novelList.add(
NovelDownloadedItem(
novelName: novelName,
novelCover: novelCover,
novelId: novelId,
chapterCount: items.length,
volumes: volumes,
),
);
}
downloaded.value = novelList;
}
/// 继续
void resumeAll() {
//更新状态至等待
for (var task in taskQueues) {
if (task.status == DownloadStatus.pause) {
task.stopTask();
task.updateStatus(DownloadStatus.wait, updateTask: false);
}
}
updateQueue();
}
/// 暂停
void pauseAll() {
for (var task in taskQueues) {
if (task.status != DownloadStatus.pause &&
task.status != DownloadStatus.error &&
task.status != DownloadStatus.errorLoad) {
task.stopTask();
task.updateStatus(DownloadStatus.pause, updateTask: false);
}
}
updateQueue();
}
/// 添加一个任务
void addTask({
required int novelId,
required int chapterId,
required String chapterName,
required int chapterSort,
required int volumeId,
required int volumeOrder,
required String volumeName,
required String novelTitle,
required String novelCover,
required bool isVip,
}) async {
var taskId = "${novelId}_${volumeId}_$chapterId";
if (box.containsKey(taskId)) {
return;
}
var info = NovelDownloadInfo(
addTime: DateTime.now(),
chapterId: chapterId,
chapterSort: chapterSort,
novelCover: novelCover,
novelId: novelId,
novelName: novelTitle,
savePath: p.join(savePath, taskId),
status: DownloadStatus.wait,
taskId: taskId,
volumeName: volumeName,
chapterName: chapterName,
isVip: isVip,
progress: 0,
fileName: '',
imageFiles: [],
isImage: false,
volumeID: volumeId,
volumeOrder: volumeOrder,
imageUrls: [],
);
await box.put(
taskId,
info,
);
taskQueues.add(NovelDownloader(info, onUpdateTask: onUpdateTask));
updateQueue();
}
void onUpdateTask() {
updateQueue();
}
/// 读取保存目录
Future<String> getSavePath() async {
var dir = await getApplicationSupportDirectory();
var novelDir = Directory(p.join(dir.path, "novel"));
if (!await novelDir.exists()) {
novelDir = await novelDir.create(recursive: true);
}
return novelDir.path;
}
///删除
void delete(NovelDownloadInfo info) async {
try {
var dir = Directory(p.join(savePath, info.taskId));
await dir.delete(recursive: true);
} catch (e) {
Log.logPrint(e);
} finally {
await box.delete(info.taskId);
updateDownlaoded();
}
updateAllIds();
}
///删除
void deleteChapter(int novelId, int volumeId, int chapterId) async {
var info = box.get("${novelId}_${volumeId}_$chapterId");
if (info != null) {
delete(info);
}
}
}
class NovelDownloadedItem {
final String novelName;
final int novelId;
final String novelCover;
final List<NovelDetailVolume> volumes;
final int chapterCount;
NovelDownloadedItem({
required this.novelName,
required this.novelCover,
required this.novelId,
required this.chapterCount,
required this.volumes,
});
}

View File

@@ -0,0 +1,333 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter_dmzj/app/app_constant.dart';
import 'package:flutter_dmzj/app/event_bus.dart';
import 'package:flutter_dmzj/app/log.dart';
import 'package:flutter_dmzj/app/utils.dart';
import 'package:flutter_dmzj/models/db/comic_history.dart';
import 'package:flutter_dmzj/models/db/novel_history.dart';
import 'package:flutter_dmzj/models/user/login_result_model.dart';
import 'package:flutter_dmzj/models/user/user_profile_model.dart';
import 'package:flutter_dmzj/modules/user/login/user_login_dialog.dart';
import 'package:flutter_dmzj/requests/user_request.dart';
import 'package:flutter_dmzj/services/db_service.dart';
import 'package:flutter_dmzj/services/local_storage_service.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
class UserService extends GetxService {
static StreamController loginedStreamController =
StreamController.broadcast();
static StreamController logoutStreamController = StreamController.broadcast();
///登录事件流
static Stream get loginedStream => loginedStreamController.stream;
///退出登录事件流
static Stream get logoutStream => logoutStreamController.stream;
static UserService get instance => Get.find<UserService>();
final LocalStorageService storage = Get.find<LocalStorageService>();
final request = UserRequest();
LoginResultModel? userAuthInfo;
Rx<UserProfileModel?> userProfile = Rx<UserProfileModel?>(null);
String get dmzjToken => userAuthInfo?.token ?? '';
String get userId => userAuthInfo?.uid.toString() ?? '';
String get nickname => userAuthInfo?.nickname ?? '';
bool get isVip => (userProfile.value?.userfeeinfo?.isVip ?? false);
String get sign => (userProfile.value?.description ?? "").isEmpty
? "无个性签名"
: userProfile.value?.description ?? "";
String get vipInfo =>
"会员有效期至${Utils.dateFormat.format(userProfile.value?.userfeeinfo?.expiresTime ?? DateTime.now())}";
/// 是否已经绑定手机号
var bindTel = true.obs;
/// 是否已经设置密码
var setPwd = true.obs;
/// 是否已经登录
var logined = false.obs;
/// 已经订阅的漫画ID
var subscribedComicIds = RxSet<int>();
/// 已经订阅的小说ID
var subscribedNovelIds = RxSet<int>();
void init() {
var value = storage.getValue(LocalStorageService.kUserAuthInfo, '');
if (value.isEmpty) {
return;
}
LoginResultModel info = LoginResultModel.fromJson(json.decode(value));
userAuthInfo = info;
logined.value = true;
if (logined.value) {
//syncRemoteHistory();
}
}
/// 设置登录信息
void setAuthInfo(LoginResultModel info) {
userAuthInfo = info;
storage.setValue(LocalStorageService.kUserAuthInfo, info.toString());
logined.value = true;
UserService.loginedStreamController.add(true);
//refreshProfile();
syncRemoteHistory();
}
void logout() {
storage.removeValue(LocalStorageService.kUserAuthInfo);
userProfile.value = null;
logined.value = false;
UserService.logoutStreamController.add(true);
}
Future<bool> login() async {
if (logined.value) {
return true;
}
var result = await Get.dialog(UserLoginDialog());
return (result != null && result == true);
}
/// 刷新个人资料
Future refreshProfile() async {
try {
if (!logined.value) {
return;
}
userProfile.value = await request.userProfile();
//updateCookie();
updateBindStatus();
} catch (e) {
Log.logPrint(e);
}
}
/// 更新一下用户的历史记录
void syncRemoteHistory() {
if (!logined.value) {
return;
}
syncRemoteComicHistory();
syncRemoteNovelHistory();
}
void syncRemoteComicHistory() async {
try {
await request.comicHistory();
} catch (e) {
Log.logPrint(e);
}
}
void syncRemoteNovelHistory() async {
try {
await request.novelHistory();
} catch (e) {
Log.logPrint(e);
}
}
/// 更新绑定状态
void updateBindStatus() async {
try {
if (!logined.value) {
return;
}
var result = await request.isBindTelPwd();
bindTel.value = result.isBindTel == 1;
setPwd.value = result.isBindTel == 1;
} catch (e) {
Log.logPrint(e);
}
}
/// 添加订阅
Future<bool> addSubscribe(List<int> ids, int type) async {
try {
if (!await login()) {
return false;
}
await request.addSubscribe(
ids: ids,
type: type,
);
if (type == AppConstant.kTypeComic) {
subscribedComicIds.addAll(ids);
} else if (type == AppConstant.kTypeNovel) {
subscribedNovelIds.addAll(ids);
}
SmartDialog.showToast("订阅成功");
return true;
} catch (e) {
SmartDialog.showToast(e.toString());
return false;
}
}
/// 取消订阅
Future<bool> cancelSubscribe(List<int> ids, int type) async {
try {
if (!await login()) {
return false;
}
await request.removeSubscribe(
ids: ids,
type: type,
);
if (type == AppConstant.kTypeComic) {
subscribedComicIds.removeAll(ids);
} else if (type == AppConstant.kTypeNovel) {
subscribedNovelIds.removeAll(ids);
}
SmartDialog.showToast("已取消订阅");
return true;
} catch (e) {
SmartDialog.showToast(e.toString());
return false;
}
}
/// 更新漫画记录
Future updateComicHistory({
required int comicId,
required int chapterId,
required int page,
required String comicName,
required String comicCover,
required String chapterName,
}) async {
try {
var time = DateTime.now();
await DBService.instance.updateComicHistory(
ComicHistory(
comicId: comicId,
chapterId: chapterId,
comicName: comicName,
comicCover: comicCover,
chapterName: chapterName,
updateTime: time,
page: page,
),
);
EventBus.instance.emit(EventBus.kUpdatedComicHistory, comicId);
if (!logined.value) {
return;
}
// await request.uploadComicHistory(
// comicId: comicId,
// chapterId: chapterId,
// page: page,
// time: time,
// );
} catch (e) {
Log.logPrint(e);
}
}
/// 更新漫画记录
Future updateNovelHistory({
required int novelId,
required int chapterId,
required int index,
required int total,
required String novelName,
required String novelCover,
required String chapterName,
required int volumeId,
required String volumeName,
}) async {
try {
var time = DateTime.now();
await DBService.instance.updateNovelHistory(
NovelHistory(
novelId: novelId,
chapterId: chapterId,
volumeName: volumeName,
volumeId: volumeId,
chapterName: chapterName,
updateTime: time,
index: index,
total: total,
novelCover: novelCover,
novelName: novelName,
),
);
EventBus.instance.emit(EventBus.kUpdatedNovelHistory, novelId);
if (!logined.value) {
return;
}
await request.uploadNovelHistory(
novelId: novelId,
volumeId: volumeId,
chapterId: chapterId,
page: 1,
total: total,
time: time,
);
} catch (e) {
Log.logPrint(e);
}
}
void updateCookie() async {
if (Platform.isAndroid || Platform.isIOS) {
final WebViewCookieManager cookieManager = WebViewCookieManager();
for (var item in getCookies()) {
await cookieManager.setCookie(item);
}
}
}
List<WebViewCookie> getCookies() {
var cookie = userProfile.value?.cookieVal ?? "";
if (cookie.isEmpty) {
return [];
}
List<WebViewCookie> cookies = [];
cookie.split(";").forEach((element) {
List<String> keyValue = element.split("=");
if (keyValue.length == 2) {
cookies.add(
WebViewCookie(
domain: ".dmzj.com",
value: keyValue[1],
name: keyValue[0],
),
);
cookies.add(
WebViewCookie(
domain: ".idmzj.com",
value: keyValue[1],
name: keyValue[0],
),
);
cookies.add(
WebViewCookie(
domain: ".muwai.com",
value: keyValue[1],
name: keyValue[0],
),
);
}
});
return cookies;
}
}