v1.0.1
This commit is contained in:
370
lib/services/app_settings_service.dart
Normal file
370
lib/services/app_settings_service.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
407
lib/services/comic_download_service.dart
Normal file
407
lib/services/comic_download_service.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
211
lib/services/db_service.dart
Normal file
211
lib/services/db_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
193
lib/services/download_task/comic_downloader.dart
Normal file
193
lib/services/download_task/comic_downloader.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
229
lib/services/download_task/novel_downloader.dart
Normal file
229
lib/services/download_task/novel_downloader.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
201
lib/services/local_storage_service.dart
Normal file
201
lib/services/local_storage_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
401
lib/services/novel_download_service.dart
Normal file
401
lib/services/novel_download_service.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
333
lib/services/user_service.dart
Normal file
333
lib/services/user_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user