add chapter download module

This commit is contained in:
kodjodevf 2023-04-14 17:50:03 +01:00
parent 054fa0642c
commit 35b01b51c3
23 changed files with 1021 additions and 100 deletions

View file

@ -47,7 +47,7 @@ android {
applicationId "com.kodjodevf.mangayomi"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 21
minSdkVersion 24
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View file

@ -1,6 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.kodjodevf.mangayomi">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:label="mangayomi"
android:name="${applicationName}"

View file

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.8.0'
repositories {
google()
mavenCentral()

View file

@ -10,6 +10,7 @@ import 'package:mangayomi/models/manga_history.dart';
import 'package:mangayomi/models/model_manga.dart';
import 'package:mangayomi/router/router.dart';
import 'package:mangayomi/source/source_model.dart';
import 'package:mangayomi/views/manga/download/download_model.dart';
import 'package:mangayomi/views/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/views/more/settings/appearance/providers/blend_level_state_provider.dart';
import 'views/more/settings/appearance/providers/flex_scheme_color_state_provider.dart';
@ -35,10 +36,12 @@ void main() async {
Hive.registerAdapter(SourceModelAdapter());
Hive.registerAdapter(ReaderModeAdapter());
Hive.registerAdapter(TypeSourceAdapter());
Hive.registerAdapter(DownloadModelAdapter());
await Hive.openBox<ModelManga>(HiveConstant.hiveBoxManga);
await Hive.openBox<MangaHistoryModel>(HiveConstant.hiveBoxMangaHistory);
await Hive.openBox<ReaderMode>(HiveConstant.hiveBoxReaderMode);
await Hive.openBox<SourceModel>(HiveConstant.hiveBoxMangaSource);
await Hive.openBox<DownloadModel>(HiveConstant.hiveBoxDownloads);
await Hive.openBox(HiveConstant.hiveBoxMangaInfo);
await Hive.openBox(HiveConstant.hiveBoxMangaFilter);
await Hive.openBox(HiveConstant.hiveBoxAppSettings);

View file

@ -4,6 +4,7 @@ import 'package:mangayomi/utils/constant.dart';
import 'package:mangayomi/models/manga_history.dart';
import 'package:mangayomi/models/model_manga.dart';
import 'package:mangayomi/source/source_model.dart';
import 'package:mangayomi/views/manga/download/download_model.dart';
import 'package:mangayomi/views/manga/reader/providers/reader_controller_provider.dart';
final hiveBoxManga = Provider<Box<ModelManga>>((ref) {
@ -27,6 +28,10 @@ final hiveBoxMangaFilterProvider = Provider<Box>((ref) {
final hiveBoxMangaSourceProvider = Provider<Box<SourceModel>>((ref) {
return Hive.box<SourceModel>(HiveConstant.hiveBoxMangaSource);
});
final hiveBoxMangaDownloads = Provider<Box<DownloadModel>>((ref) {
return Hive.box<DownloadModel>(HiveConstant.hiveBoxDownloads);
});
final hiveBoxSettings = Provider<Box>((ref) {
return Hive.box(HiveConstant.hiveBoxAppSettings);
});

View file

@ -0,0 +1,49 @@
// ignore_for_file: depend_on_referenced_packages
import 'dart:io';
import 'package:mangayomi/models/model_manga.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
class StorageProvider {
Future<bool> requestPermission() async {
Permission permission = Permission.manageExternalStorage;
if (Platform.isAndroid || Platform.isIOS) {
if (await permission.isGranted) {
return true;
} else {
final result = await permission.request();
if (result == PermissionStatus.granted) {
return true;
} else {
return false;
}
}
}
return true;
}
Future<Directory?> getDirectory() async {
Directory? directory;
if (Platform.isAndroid) {
directory = Directory("/storage/emulated/0/Mangayomi/");
} else {
final dir = await getApplicationDocumentsDirectory();
directory = Directory("${dir.path}/Mangayomi");
}
return directory;
}
Future<Directory?> getMangaChapterDirectory(
ModelManga modelManga, index) async {
final dir = await getDirectory();
return Directory(
"${dir!.path}/downloads/${modelManga.source} (${modelManga.lang!.toUpperCase()})/${modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/${modelManga.chapterTitle![index].replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/");
}
Future<Directory?> getMangaMainDirectory(ModelManga modelManga, index) async {
final dir = await getDirectory();
return Directory(
"${dir!.path}/downloads/${modelManga.source} (${modelManga.lang!.toUpperCase()})/${modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/");
}
}

View file

@ -8,11 +8,11 @@ import 'package:html/dom.dart' as dom;
import 'package:mangayomi/models/comick/chapter_page_comick.dart';
import 'package:mangayomi/models/model_manga.dart';
import 'package:mangayomi/providers/hive_provider.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/services/get_popular_manga.dart';
import 'package:mangayomi/services/http_res_to_dom_html.dart';
import 'package:mangayomi/source/source_model.dart';
import 'package:mangayomi/views/more/settings/providers/incognito_mode_state_provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_js/flutter_js.dart';
part 'get_manga_chapter_url.g.dart';
@ -36,15 +36,7 @@ Future<GetMangaChapterUrlModel> getMangaChapterUrl(
"${modelManga.source}/${modelManga.name}/${modelManga.chapterTitle![index]}-pageurl",
defaultValue: []);
final incognitoMode = ref.watch(incognitoModeStateProvider);
Directory? pathh;
if (Platform.isAndroid || Platform.isIOS) {
pathh = await getExternalStorageDirectory();
} else {
pathh = await getApplicationDocumentsDirectory();
}
path = Directory(
"${pathh!.path}/${modelManga.source}/${modelManga.name}/${modelManga.chapterTitle![index]}/");
path = await StorageProvider().getMangaChapterDirectory(modelManga, index);
if (hiveUrl.isNotEmpty) {
urll = hiveUrl;

View file

@ -6,7 +6,7 @@ part of 'search_manga.dart';
// RiverpodGenerator
// **************************************************************************
String _$searchMangaHash() => r'e84374580686773aa67deb76ab91c00e2e6fab8b';
String _$searchMangaHash() => r'6cb4c0eaa232a0c2b54a2c8f4841d3acfffacd40';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -5,7 +5,7 @@ class HiveConstant {
static String get hiveBoxMangaSource => "_manga_box_source_";
static String get hiveBoxMangaFilter => "_manga_box_filter_";
static String get hiveBoxAppSettings => "_app_box_settings_";
static String get hiveBoxDownloads => "_manga_box_downloads_";
static String get hiveBoxReaderSettings => "_reader_box_settings_";
static String get hiveBoxReaderMode =>
"_readerMode_box_settings_";
static String get hiveBoxReaderMode => "_readerMode_box_settings_";
}

View file

@ -1,4 +1,4 @@
Map<String, String>? headers(String source) {
Map<String, String> headers(String source) {
return source == 'mangakawaii'
? {
'Referer': 'https://www.mangakawaii.io/',

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/source/source_model.dart';
import 'package:mangayomi/views/browse/extension/extension_screen.dart';
import 'package:mangayomi/views/browse/migrate_screen.dart';
@ -22,16 +23,22 @@ class _BrowseScreenState extends State<BrowseScreen>
_tabBarController = TabController(length: 3, vsync: this);
_tabBarController.animateTo(0);
_tabBarController.addListener(() {
_chekPermission();
setState(() {
_textEditingController.clear();
_entriesFilter = [];
_isSearch = false;
});
});
super.initState();
}
List<SourceModel> entries = [];
List<SourceModel> entriesFilter = [];
_chekPermission() async {
await StorageProvider().requestPermission();
}
List<SourceModel> _entries = [];
List<SourceModel> _entriesFilter = [];
final _textEditingController = TextEditingController();
bool _isSearch = false;
@override
@ -52,7 +59,7 @@ class _BrowseScreenState extends State<BrowseScreen>
? SeachFormTextField(
onChanged: (value) {
setState(() {
entriesFilter = entries
_entriesFilter = _entries
.where((element) => element.sourceName
.toLowerCase()
.contains(value.toLowerCase()))
@ -67,7 +74,7 @@ class _BrowseScreenState extends State<BrowseScreen>
_isSearch = false;
});
_textEditingController.clear();
entriesFilter = entries;
_entriesFilter = _entries;
},
controller: _textEditingController,
)
@ -122,9 +129,9 @@ class _BrowseScreenState extends State<BrowseScreen>
const SourcesScreen(),
ExtensionScreen(
entriesData: (val) {
entries = val as List<SourceModel>;
_entries = val as List<SourceModel>;
},
entriesFilter: entriesFilter,
entriesFilter: _entriesFilter,
),
const MigrateScreen()
]),

View file

@ -3,13 +3,13 @@ import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mangayomi/models/manga_reader.dart';
import 'package:mangayomi/models/model_manga.dart';
import 'package:mangayomi/utils/cached_network.dart';
import 'package:mangayomi/utils/media_query.dart';
import 'package:mangayomi/views/manga/detail/providers/state_providers.dart';
import 'package:mangayomi/views/manga/detail/readmore.dart';
import 'package:mangayomi/views/manga/download/download_page_widget.dart';
class MangaDetailView extends ConsumerStatefulWidget {
final Function(bool) isExtended;
@ -169,13 +169,12 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
modelManga: widget.modelManga!,
index: reverse ? reverseIndex : finalIndex);
},
trailing: const Icon(
FontAwesomeIcons.circleDown,
size: 20,
),
trailing: ref.watch(ChapterPageDownloadsProvider(
index: reverse ? reverseIndex : finalIndex,
modelManga: widget.modelManga!)),
subtitle: Text(
chapterDate[finalIndex],
style: const TextStyle(fontSize: 12),
style: const TextStyle(fontSize: 11),
),
title: Text(
chapterTitle[finalIndex],

View file

@ -0,0 +1,32 @@
import 'package:hive/hive.dart';
import 'package:mangayomi/models/model_manga.dart';
part 'download_model.g.dart';
@HiveType(typeId: 6)
class DownloadModel {
@HiveField(0)
final ModelManga modelManga;
@HiveField(1)
final int index;
@HiveField(2)
final int succeeded;
@HiveField(3)
final int failed;
@HiveField(4)
final int total;
@HiveField(6)
final bool isDownload;
@HiveField(7)
final List taskIds;
@HiveField(8)
final bool isStartDownload;
DownloadModel(
{required this.modelManga,
required this.succeeded,
required this.failed,
required this.index,
required this.total,
required this.isDownload,
required this.taskIds,
required this.isStartDownload});
}

View file

@ -0,0 +1,62 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'download_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class DownloadModelAdapter extends TypeAdapter<DownloadModel> {
@override
final int typeId = 6;
@override
DownloadModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return DownloadModel(
modelManga: fields[0] as ModelManga,
succeeded: fields[2] as int,
failed: fields[3] as int,
index: fields[1] as int,
total: fields[4] as int,
isDownload: fields[6] as bool,
taskIds: (fields[7] as List).cast<dynamic>(),
isStartDownload: fields[8] as bool,
);
}
@override
void write(BinaryWriter writer, DownloadModel obj) {
writer
..writeByte(8)
..writeByte(0)
..write(obj.modelManga)
..writeByte(1)
..write(obj.index)
..writeByte(2)
..write(obj.succeeded)
..writeByte(3)
..write(obj.failed)
..writeByte(4)
..write(obj.total)
..writeByte(6)
..write(obj.isDownload)
..writeByte(7)
..write(obj.taskIds)
..writeByte(8)
..write(obj.isStartDownload);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DownloadModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View file

@ -0,0 +1,489 @@
// ignore_for_file: implementation_imports, depend_on_referenced_packages
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:mangayomi/models/model_manga.dart';
import 'package:mangayomi/providers/hive_provider.dart';
import 'package:mangayomi/services/get_manga_chapter_url.dart';
import 'package:mangayomi/utils/constant.dart';
import 'package:mangayomi/utils/headers.dart';
import 'package:mangayomi/views/manga/download/download_model.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'download_page_widget.g.dart';
@riverpod
class ChapterPageDownloads extends _$ChapterPageDownloads {
@override
Widget build({required ModelManga modelManga, required int index}) {
return ChapterPageDownload(
index: index,
modelManga: modelManga,
);
}
// ...
}
class ChapterPageDownload extends ConsumerStatefulWidget {
final ModelManga modelManga;
final int index;
const ChapterPageDownload(
{super.key, required this.modelManga, required this.index});
@override
ConsumerState createState() => _ChapterPageDownloadState();
}
class _ChapterPageDownloadState extends ConsumerState<ChapterPageDownload>
with AutomaticKeepAliveClientMixin<ChapterPageDownload> {
List _urll = [];
List<DownloadTask> tasks = [];
final StorageProvider _storageProvider = StorageProvider();
_startDownload() async {
await _storageProvider.requestPermission();
Directory? path;
bool isOk = false;
final path1 = await _storageProvider.getDirectory();
final finalPath =
"downloads/${widget.modelManga.source} (${widget.modelManga.lang!.toUpperCase()})/${widget.modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/${widget.modelManga.chapterTitle![widget.index].replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}";
path = Directory(
"${path1!.path}downloads/${widget.modelManga.source} (${widget.modelManga.lang!.toUpperCase()})/${widget.modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/${widget.modelManga.chapterTitle![widget.index].replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/");
ref
.read(getMangaChapterUrlProvider(
modelManga: widget.modelManga,
index: widget.index,
).future)
.then((value) {
if (value.urll.isNotEmpty) {
if (mounted) {
setState(() {
_urll = value.urll;
isOk = true;
});
}
}
});
await Future.doWhile(() async {
await Future.delayed(const Duration(seconds: 1));
if (isOk == true) {
return false;
}
return true;
});
if (_urll.isNotEmpty) {
for (var index = 0; index < _urll.length; index++) {
final path2 = Directory("${path1.path}downloads/");
final path4 = Directory(
"${path2.path}${widget.modelManga.source} (${widget.modelManga.lang!.toUpperCase()})/");
final path3 = Directory(
"${path2.path}${widget.modelManga.source} (${widget.modelManga.lang!.toUpperCase()})/${widget.modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/");
final path5 = Directory(
"${path2.path}${widget.modelManga.source} (${widget.modelManga.lang!.toUpperCase()})/${widget.modelManga.name!.replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}/${widget.modelManga.chapterTitle![widget.index].replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_')}");
if (!(await path1.exists())) {
path1.create();
}
if (!(await path2.exists())) {
path2.create();
}
if (!(await path4.exists())) {
path4.create();
}
if (!(await path3.exists())) {
path3.create();
}
if (!(await path5.exists())) {
path5.create();
}
if ((await path.exists())) {
if (await File("${path.path}" "${index + 1}.jpg").exists()) {
} else {
tasks.add(DownloadTask(
taskId: _urll[index],
headers: headers(widget.modelManga.source!),
url: _urll[index],
filename: "${index + 1}.jpg",
baseDirectory:
Platform.isWindows || Platform.isMacOS || Platform.isLinux
? BaseDirectory.applicationDocuments
: BaseDirectory.temporary,
directory:
Platform.isWindows || Platform.isMacOS || Platform.isLinux
? 'Mangayomi/$finalPath'
: finalPath,
updates: Updates.statusAndProgress,
allowPause: true,
));
}
} else {
path.create();
if (await File("${path.path}" "${index + 1}.jpg").exists()) {
} else {
tasks.add(DownloadTask(
taskId: _urll[index],
headers: headers(widget.modelManga.source!),
url: _urll[index],
filename: "${index + 1}.jpg",
baseDirectory:
Platform.isWindows || Platform.isMacOS || Platform.isLinux
? BaseDirectory.applicationDocuments
: BaseDirectory.temporary,
directory:
Platform.isWindows || Platform.isMacOS || Platform.isLinux
? 'Mangayomi/$finalPath'
: finalPath,
updates: Updates.statusAndProgress,
allowPause: true,
));
}
}
}
if (tasks.isEmpty && _urll.isNotEmpty) {
final model = DownloadModel(
modelManga: widget.modelManga,
succeeded: 0,
failed: 0,
index: widget.index,
total: 0,
isDownload: true,
taskIds: _urll,
isStartDownload: false);
ref
.watch(hiveBoxMangaDownloads)
.put(widget.modelManga.chapterTitle![widget.index], model);
} else {
await FileDownloader().downloadBatch(
tasks,
batchProgressCallback: (succeeded, failed) {
final model = DownloadModel(
modelManga: widget.modelManga,
succeeded: succeeded,
failed: failed,
index: widget.index,
total: tasks.length,
isDownload: (succeeded == tasks.length) ? true : false,
taskIds: _urll,
isStartDownload: true);
Hive.box<DownloadModel>(HiveConstant.hiveBoxDownloads)
.put(widget.modelManga.chapterTitle![widget.index], model);
},
taskProgressCallback: (task, progress) async {
if (progress == 1.0) {
final downloadTask = DownloadTask(
creationTime: task.creationTime,
taskId: task.taskId,
headers: task.headers,
url: task.url,
filename: task.filename,
baseDirectory: task.baseDirectory,
directory: task.directory,
updates: task.updates,
allowPause: task.allowPause,
);
if (Platform.isAndroid || Platform.isIOS) {
await FileDownloader().moveToSharedStorage(
downloadTask, SharedStorage.external,
directory: finalPath);
}
}
},
);
}
}
}
_deleteFile(List pageUrl) async {
final path = await _storageProvider.getMangaChapterDirectory(
widget.modelManga, widget.index);
try {
path!.deleteSync(recursive: true);
ref.watch(hiveBoxMangaDownloads).delete(
widget.modelManga.chapterTitle![widget.index],
);
} catch (e) {
ref.watch(hiveBoxMangaDownloads).delete(
widget.modelManga.chapterTitle![widget.index],
);
}
}
bool _isStarted = false;
@override
Widget build(BuildContext context) {
super.build(context);
return SizedBox(
height: 41,
width: 35,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: ValueListenableBuilder<Box<DownloadModel>>(
valueListenable: ref.watch(hiveBoxMangaDownloads).listenable(),
builder: (context, val, child) {
final entries = val.values
.where((element) =>
element.modelManga.chapterTitle![element.index] ==
widget.modelManga.chapterTitle![widget.index])
.toList();
if (entries.isNotEmpty) {
return entries.first.isDownload
? PopupMenuButton(
child: Icon(
size: 25,
Icons.check_circle,
color:
Theme.of(context).iconTheme.color!.withOpacity(0.7),
),
onSelected: (value) {
if (value.toString() == 'Delete') {
setState(() {
_isStarted = false;
});
_deleteFile(entries.first.taskIds);
}
},
itemBuilder: (context) => [
const PopupMenuItem(value: 'Send', child: Text("Send")),
const PopupMenuItem(
value: 'Delete', child: Text('Delete')),
],
)
: entries.first.isStartDownload &&
entries.first.succeeded == 0
? SizedBox(
height: 41,
width: 35,
child: PopupMenuButton(
child: _downloadWidget(context, false),
onSelected: (value) {
if (value.toString() == 'Cancel') {
setState(() {
_isStarted = false;
});
List<String> taskIds = [];
for (var id in entries.first.taskIds) {
taskIds.add(id);
}
FileDownloader()
.cancelTasksWithIds(taskIds)
.then((value) async {
await Future.delayed(
const Duration(seconds: 2));
ref.watch(hiveBoxMangaDownloads).delete(
widget.modelManga
.chapterTitle![widget.index],
);
});
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'Cancel', child: Text("Cancel")),
],
))
: entries.first.succeeded != 0
? SizedBox(
height: 41,
width: 35,
child: PopupMenuButton(
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: Icon(
Icons.arrow_downward_sharp,
color: Theme.of(context)
.iconTheme
.color!
.withOpacity(0.7),
)),
Align(
alignment: Alignment.center,
child: TweenAnimationBuilder<double>(
duration:
const Duration(milliseconds: 250),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 0,
end: (entries.first.succeeded /
entries.first.total),
),
builder: (context, value, _) =>
SizedBox(
height: 2,
width: 2,
child: CircularProgressIndicator(
strokeWidth: 19,
value: value,
color: Theme.of(context)
.iconTheme
.color!
.withOpacity(0.7),
),
),
),
),
Align(
alignment: Alignment.center,
child: Icon(
Icons.arrow_downward_sharp,
color: Theme.of(context)
.scaffoldBackgroundColor,
)),
],
),
onSelected: (value) {
if (value.toString() == 'Cancel') {
setState(() {
_isStarted = false;
});
List<String> taskIds = [];
for (var id in entries.first.taskIds) {
taskIds.add(id);
}
FileDownloader()
.cancelTasksWithIds(taskIds)
.then((value) async {
await Future.delayed(
const Duration(seconds: 2));
ref.watch(hiveBoxMangaDownloads).delete(
widget.modelManga
.chapterTitle![widget.index],
);
});
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'Cancel', child: Text("Cancel")),
],
))
: entries.first.succeeded == 0
? IconButton(
onPressed: () {
// _startDownload();
setState(() {
_isStarted = true;
});
},
icon: Icon(
FontAwesomeIcons.circleDown,
color: Theme.of(context)
.iconTheme
.color!
.withOpacity(0.7),
size: 25,
))
: SizedBox(
height: 50,
width: 50,
child: PopupMenuButton(
child: const Icon(
Icons.error_outline_outlined,
color: Colors.red,
size: 25,
),
onSelected: (value) {
if (value.toString() == 'Retry') {
ref.watch(hiveBoxMangaDownloads).delete(
widget.modelManga
.chapterTitle![widget.index],
);
_startDownload();
setState(() {
_isStarted = true;
});
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'Retry', child: Text("Retry")),
],
));
}
return _isStarted
? SizedBox(
height: 50,
width: 50,
child: PopupMenuButton(
child: _downloadWidget(context, true),
onSelected: (value) {
if (value.toString() == 'Cancel') {
setState(() {
_isStarted = false;
});
List<String> taskIds = [];
for (var id in _urll) {
taskIds.add(id);
}
FileDownloader()
.cancelTasksWithIds(taskIds)
.then((value) async {
await Future.delayed(const Duration(seconds: 2));
ref.watch(hiveBoxMangaDownloads).delete(
widget.modelManga.chapterTitle![widget.index],
);
});
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'Cancel', child: Text("Cancel")),
],
))
: IconButton(
splashRadius: 5,
iconSize: 17,
onPressed: () {
_startDownload();
setState(() {
_isStarted = true;
});
},
icon: _downloadWidget(context, false),
);
},
),
),
);
}
@override
bool get wantKeepAlive => true;
}
Widget _downloadWidget(BuildContext context, bool isLoading) {
return Stack(
children: [
Align(
alignment: Alignment.center,
child: Icon(
size: 18,
Icons.arrow_downward_sharp,
color: Theme.of(context).iconTheme.color!.withOpacity(0.7),
)),
Align(
alignment: Alignment.center,
child: SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
value: isLoading ? null : 1,
color: Theme.of(context).iconTheme.color!.withOpacity(0.7),
strokeWidth: 2,
),
),
),
],
);
}

View file

@ -0,0 +1,140 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'download_page_widget.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$chapterPageDownloadsHash() =>
r'0b3eaf9a3ca4786287616a87e5de62af24259b68';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$ChapterPageDownloads
extends BuildlessAutoDisposeNotifier<Widget> {
late final ModelManga modelManga;
late final int index;
Widget build({
required ModelManga modelManga,
required int index,
});
}
/// See also [ChapterPageDownloads].
@ProviderFor(ChapterPageDownloads)
const chapterPageDownloadsProvider = ChapterPageDownloadsFamily();
/// See also [ChapterPageDownloads].
class ChapterPageDownloadsFamily extends Family<Widget> {
/// See also [ChapterPageDownloads].
const ChapterPageDownloadsFamily();
/// See also [ChapterPageDownloads].
ChapterPageDownloadsProvider call({
required ModelManga modelManga,
required int index,
}) {
return ChapterPageDownloadsProvider(
modelManga: modelManga,
index: index,
);
}
@override
ChapterPageDownloadsProvider getProviderOverride(
covariant ChapterPageDownloadsProvider provider,
) {
return call(
modelManga: provider.modelManga,
index: provider.index,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'chapterPageDownloadsProvider';
}
/// See also [ChapterPageDownloads].
class ChapterPageDownloadsProvider
extends AutoDisposeNotifierProviderImpl<ChapterPageDownloads, Widget> {
/// See also [ChapterPageDownloads].
ChapterPageDownloadsProvider({
required this.modelManga,
required this.index,
}) : super.internal(
() => ChapterPageDownloads()
..modelManga = modelManga
..index = index,
from: chapterPageDownloadsProvider,
name: r'chapterPageDownloadsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$chapterPageDownloadsHash,
dependencies: ChapterPageDownloadsFamily._dependencies,
allTransitiveDependencies:
ChapterPageDownloadsFamily._allTransitiveDependencies,
);
final ModelManga modelManga;
final int index;
@override
bool operator ==(Object other) {
return other is ChapterPageDownloadsProvider &&
other.modelManga == modelManga &&
other.index == index;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, modelManga.hashCode);
hash = _SystemHash.combine(hash, index.hashCode);
return _SystemHash.finish(hash);
}
@override
Widget runNotifierBuild(
covariant ChapterPageDownloads notifier,
) {
return notifier.build(
modelManga: modelManga,
index: index,
);
}
}
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions

View file

@ -36,21 +36,53 @@ class ImageViewHorizontal extends StatefulWidget {
typedef DoubleClickAnimationListener = void Function();
class _ImageViewHorizontalState extends State<ImageViewHorizontal> {
@override
void initState() {
_localCheck();
super.initState();
}
_localCheck() async {
if (await File("${widget.path.path}" "${widget.index + 1}.jpg").exists()) {
if (mounted) {
setState(() {
_isLocale = true;
});
}
} else {
if (mounted) {
setState(() {
_isLocale = false;
});
}
}
}
bool _isLocale = false;
@override
Widget build(BuildContext context) {
return ExtendedImage.network(
widget.url,
cache: true,
clearMemoryCacheWhenDispose: true,
enableMemoryCache: false,
cacheMaxAge: const Duration(days: 7),
headers: headers(widget.source),
mode: ExtendedImageMode.gesture,
initGestureConfigHandler: widget.initGestureConfigHandler,
onDoubleTap: widget.onDoubleTap,
handleLoadingProgress: true,
loadStateChanged: widget.loadStateChanged,
);
return _isLocale
? ExtendedImage.file(
File("${widget.path.path}" "${widget.index + 1}.jpg"),
clearMemoryCacheWhenDispose: true,
enableMemoryCache: false,
mode: ExtendedImageMode.gesture,
initGestureConfigHandler: widget.initGestureConfigHandler,
onDoubleTap: widget.onDoubleTap,
loadStateChanged: widget.loadStateChanged,
)
: ExtendedImage.network(
widget.url,
cache: true,
clearMemoryCacheWhenDispose: true,
enableMemoryCache: false,
cacheMaxAge: const Duration(days: 7),
headers: headers(widget.source),
mode: ExtendedImageMode.gesture,
initGestureConfigHandler: widget.initGestureConfigHandler,
onDoubleTap: widget.onDoubleTap,
handleLoadingProgress: true,
loadStateChanged: widget.loadStateChanged,
);
}
}

View file

@ -33,6 +33,29 @@ class ImageViewVertical extends ConsumerStatefulWidget {
class _ImageViewVerticalState extends ConsumerState<ImageViewVertical>
with AutomaticKeepAliveClientMixin<ImageViewVertical> {
@override
void initState() {
_localCheck();
super.initState();
}
_localCheck() async {
if (await File("${widget.path.path}" "${widget.index + 1}.jpg").exists()) {
if (mounted) {
setState(() {
_isLocale = true;
});
}
} else {
if (mounted) {
setState(() {
_isLocale = false;
});
}
}
}
bool _isLocale = false;
@override
Widget build(BuildContext context) {
super.build(context);
return Container(
@ -44,44 +67,57 @@ class _ImageViewVerticalState extends ConsumerState<ImageViewVertical>
SizedBox(
height: MediaQuery.of(context).padding.top,
),
ExtendedImage.network(widget.url,
headers: headers(widget.source),
handleLoadingProgress: true,
fit: BoxFit.contain,
cacheMaxAge: const Duration(days: 7),
clearMemoryCacheWhenDispose: true,
enableMemoryCache: false,
loadStateChanged: (ExtendedImageState state) {
if (state.extendedImageLoadState == LoadState.loading) {
final ImageChunkEvent? loadingProgress = state.loadingProgress;
final double? progress =
loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null;
return SizedBox(
height: mediaHeight(context, 0.5),
child: Center(
child: CircularProgressIndicator(
value: progress,
),
),
);
}
if (state.extendedImageLoadState == LoadState.failed) {
return Center(
child: ElevatedButton(
onPressed: () {
state.reLoadImage();
},
child: const Icon(
Icons.replay_outlined,
size: 30,
)),
);
}
return null;
}),
_isLocale
? ExtendedImage.file(
fit: BoxFit.contain,
clearMemoryCacheWhenDispose: true,
enableMemoryCache: false,
File('${widget.path.path}${widget.index + 1}.jpg'))
: ExtendedImage.network(widget.url,
headers: headers(widget.source),
handleLoadingProgress: true,
fit: BoxFit.contain,
cacheMaxAge: const Duration(days: 7),
clearMemoryCacheWhenDispose: true,
enableMemoryCache: false,
loadStateChanged: (ExtendedImageState state) {
if (state.extendedImageLoadState == LoadState.loading) {
final ImageChunkEvent? loadingProgress =
state.loadingProgress;
final double? progress =
loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null;
return Container(
color: Colors.black,
height: mediaHeight(context, 0.8),
child: Center(
child: CircularProgressIndicator(
value: progress,
),
),
);
}
if (state.extendedImageLoadState == LoadState.failed) {
return Container(
color: Colors.black,
height: mediaHeight(context, 0.8),
child: Column(
children: [
ElevatedButton(
onPressed: () {
state.reLoadImage();
},
child: const Icon(
Icons.replay_outlined,
size: 30,
)),
],
));
}
return null;
}),
if (widget.index + 1 == widget.length)
Column(
children: [

View file

@ -169,20 +169,20 @@ class _MangaChapterPageGalleryState
widget.readerController.setPageIndex(index);
}
void _onAddButtonTapped(int ok, bool isPrev, {bool isSlide = false}) {
void _onAddButtonTapped(int index, bool isPrev, {bool isSlide = false}) {
if (isPrev) {
if (_selectedValue == ReaderMode.verticalContinuous ||
_selectedValue == ReaderMode.webtoon) {
if (ok != -1) {
if (index != -1) {
_itemScrollController.scrollTo(
curve: Curves.ease,
index: ok,
index: index,
duration: Duration(milliseconds: isSlide ? 2 : 150));
}
} else {
if (ok != -1) {
if (index != -1) {
if (_extendedController.hasClients) {
_extendedController.animateToPage(ok.toInt(),
_extendedController.animateToPage(index,
duration: Duration(milliseconds: isSlide ? 2 : 150),
curve: Curves.ease);
}
@ -191,16 +191,16 @@ class _MangaChapterPageGalleryState
} else {
if (_selectedValue == ReaderMode.verticalContinuous ||
_selectedValue == ReaderMode.webtoon) {
if (widget.readerController.getPageLength(widget.url) != ok) {
if (widget.readerController.getPageLength(widget.url) != index) {
_itemScrollController.scrollTo(
curve: Curves.ease,
index: ok,
index: index,
duration: Duration(milliseconds: isSlide ? 2 : 150));
}
} else {
if (widget.readerController.getPageLength(widget.url) != ok) {
if (widget.readerController.getPageLength(widget.url) != index) {
if (_extendedController.hasClients) {
_extendedController.animateToPage(ok.toInt(),
_extendedController.animateToPage(index.toInt(),
duration: Duration(milliseconds: isSlide ? 2 : 150),
curve: Curves.ease);
}
@ -923,7 +923,9 @@ class _MangaChapterPageGalleryState
reverse: _isReversHorizontal,
physics: const ClampingScrollPhysics(),
canScrollPage: (GestureDetails? gestureDetails) {
return !(gestureDetails!.totalScale! > 1.0);
return gestureDetails != null
? !(gestureDetails.totalScale! > 1.0)
: true;
},
itemBuilder: (BuildContext context, int index) {
return ImageViewHorizontal(
@ -981,16 +983,21 @@ class _MangaChapterPageGalleryState
}
if (state.extendedImageLoadState ==
LoadState.failed) {
return Center(
child: ElevatedButton(
onPressed: () {
state.reLoadImage();
},
child: const Icon(
Icons.replay_outlined,
size: 30,
)),
);
return Container(
color: Colors.black,
height: mediaHeight(context, 0.8),
child: Column(
children: [
ElevatedButton(
onPressed: () {
state.reLoadImage();
},
child: const Icon(
Icons.replay_outlined,
size: 30,
)),
],
));
}
return Container();
},

View file

@ -41,6 +41,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.10.0"
background_downloader:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: "7b778222546d53fcd41a84f6aee90b87c2593930"
url: "https://github.com/kodjodevf/background_downloader.git"
source: git
version: "5.4.5"
boolean_selector:
dependency: transitive
description:
@ -544,6 +553,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.1"
localstore:
dependency: transitive
description:
name: localstore
sha256: "42a0afb7696cfab1b4bd7d08355b4ee01f975fd364553b28d51496eccaf11cce"
url: "https://pub.dev"
source: hosted
version: "1.3.5"
logging:
dependency: transitive
description:
@ -688,6 +705,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8"
url: "https://pub.dev"
source: hosted
version: "10.2.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2"
url: "https://pub.dev"
source: hosted
version: "10.2.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85
url: "https://pub.dev"
source: hosted
version: "9.0.8"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84"
url: "https://pub.dev"
source: hosted
version: "3.9.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b
url: "https://pub.dev"
source: hosted
version: "0.1.2"
photo_view:
dependency: "direct main"
description:

View file

@ -55,6 +55,10 @@ dependencies:
# draggable_menu: ^0.2.0
url_launcher: ^6.1.10
package_info_plus: ^3.0.2
background_downloader:
git:
url: https://github.com/kodjodevf/background_downloader.git
permission_handler: ^10.2.0
# The following adds the Cupertino Icons font to your application.

View file

@ -7,11 +7,14 @@
#include "generated_plugin_registrant.h"
#include <flutter_js/flutter_js_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterJsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterJsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View file

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_js
permission_handler_windows
url_launcher_windows
)