mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
download & view local comics
This commit is contained in:
@@ -7,9 +7,12 @@ import 'package:venera/foundation/consts.dart';
|
||||
import 'package:venera/foundation/favorites.dart';
|
||||
import 'package:venera/foundation/history.dart';
|
||||
import 'package:venera/foundation/image_provider/cached_image.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/res.dart';
|
||||
import 'package:venera/network/download.dart';
|
||||
import 'package:venera/pages/favorites/favorites_page.dart';
|
||||
import 'package:venera/pages/reader/reader.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
@@ -32,6 +35,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
|
||||
var scrollController = ScrollController();
|
||||
|
||||
bool isDownloaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
scrollController.addListener(onScroll);
|
||||
@@ -92,9 +97,16 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
}
|
||||
|
||||
@override
|
||||
onDataLoaded() {
|
||||
Future<void> onDataLoaded() async {
|
||||
isLiked = comic.isLiked ?? false;
|
||||
isFavorite = comic.isFavorite ?? false;
|
||||
if (comic.chapters == null) {
|
||||
isDownloaded = await LocalManager().isDownloaded(
|
||||
comic.id,
|
||||
comic.comicType,
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Iterable<Widget> buildTitle() sync* {
|
||||
@@ -173,7 +185,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
onPressed: read,
|
||||
iconColor: context.useTextColor(Colors.orange),
|
||||
),
|
||||
if (!isMobile)
|
||||
if (!isMobile && !isDownloaded)
|
||||
_ActionButton(
|
||||
icon: const Icon(Icons.download),
|
||||
text: 'Download'.tl,
|
||||
@@ -219,7 +231,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.tonal(
|
||||
onPressed: () {},
|
||||
onPressed: download,
|
||||
child: Text("Download".tl),
|
||||
),
|
||||
),
|
||||
@@ -335,7 +347,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
children: [
|
||||
buildTag(text: e.key, isTitle: true),
|
||||
for (var tag in e.value)
|
||||
buildTag(text: tag, onTap: () => onTagTap(tag, e.key)),
|
||||
buildTag(text: tag, onTap: () => onTapTag(tag, e.key)),
|
||||
],
|
||||
),
|
||||
if (comic.uploader != null)
|
||||
@@ -455,7 +467,9 @@ abstract mixin class _ComicPageActions {
|
||||
);
|
||||
}
|
||||
|
||||
void share() {}
|
||||
void share() {
|
||||
Share.shareText(comic.title);
|
||||
}
|
||||
|
||||
/// read the comic
|
||||
///
|
||||
@@ -482,9 +496,57 @@ abstract mixin class _ComicPageActions {
|
||||
read(ep, page);
|
||||
}
|
||||
|
||||
void download() {}
|
||||
void download() async {
|
||||
if (LocalManager().isDownloading(comic.id, comic.comicType)) {
|
||||
App.rootContext.showMessage(message: "The comic is downloading".tl);
|
||||
return;
|
||||
}
|
||||
if (comic.chapters == null &&
|
||||
await LocalManager().isDownloaded(comic.id, comic.comicType, 0)) {
|
||||
App.rootContext.showMessage(message: "The comic is downloaded".tl);
|
||||
return;
|
||||
}
|
||||
if (comic.chapters == null) {
|
||||
LocalManager().addTask(ImagesDownloadTask(
|
||||
source: comicSource,
|
||||
comicId: comic.id,
|
||||
comic: comic,
|
||||
));
|
||||
} else {
|
||||
List<int>? selected;
|
||||
var downloaded = <int>[];
|
||||
var localComic = LocalManager().find(comic.id, comic.comicType);
|
||||
if (localComic != null) {
|
||||
for (int i = 0; i < comic.chapters!.length; i++) {
|
||||
if (localComic.downloadedChapters
|
||||
.contains(comic.chapters!.keys.elementAt(i))) {
|
||||
downloaded.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
await showSideBar(
|
||||
App.rootContext,
|
||||
_SelectDownloadChapter(
|
||||
comic.chapters!.values.toList(),
|
||||
(v) => selected = v,
|
||||
downloaded,
|
||||
),
|
||||
);
|
||||
if (selected == null) return;
|
||||
LocalManager().addTask(ImagesDownloadTask(
|
||||
source: comicSource,
|
||||
comicId: comic.id,
|
||||
comic: comic,
|
||||
chapters: selected!.map((i) {
|
||||
return comic.chapters!.keys.elementAt(i);
|
||||
}).toList(),
|
||||
));
|
||||
}
|
||||
App.rootContext.showMessage(message: "Download started".tl);
|
||||
update();
|
||||
}
|
||||
|
||||
void onTagTap(String tag, String namespace) {}
|
||||
void onTapTag(String tag, String namespace) {}
|
||||
|
||||
void showMoreActions() {}
|
||||
|
||||
@@ -1137,3 +1199,95 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectDownloadChapter extends StatefulWidget {
|
||||
const _SelectDownloadChapter(this.eps, this.finishSelect, this.downloadedEps);
|
||||
|
||||
final List<String> eps;
|
||||
final void Function(List<int>) finishSelect;
|
||||
final List<int> downloadedEps;
|
||||
|
||||
@override
|
||||
State<_SelectDownloadChapter> createState() => _SelectDownloadChapterState();
|
||||
}
|
||||
|
||||
class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
|
||||
List<int> selected = [];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: Appbar(title: Text("Download".tl), backgroundColor: context.colorScheme.surfaceContainerLow,),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: widget.eps.length,
|
||||
itemBuilder: (context, i) {
|
||||
return CheckboxListTile(
|
||||
title: Text(widget.eps[i]),
|
||||
value: selected.contains(i) ||
|
||||
widget.downloadedEps.contains(i),
|
||||
onChanged: widget.downloadedEps.contains(i)
|
||||
? null
|
||||
: (v) {
|
||||
setState(() {
|
||||
if (selected.contains(i)) {
|
||||
selected.remove(i);
|
||||
} else {
|
||||
selected.add(i);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: context.colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
var res = <int>[];
|
||||
for (int i = 0; i < widget.eps.length; i++) {
|
||||
if (!widget.downloadedEps.contains(i)) {
|
||||
res.add(i);
|
||||
}
|
||||
}
|
||||
widget.finishSelect(res);
|
||||
context.pop();
|
||||
},
|
||||
child: Text("Download All".tl),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
widget.finishSelect(selected);
|
||||
context.pop();
|
||||
},
|
||||
child: Text("Download Selected".tl),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).padding.bottom + 4),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
218
lib/pages/downloading_page.dart
Normal file
218
lib/pages/downloading_page.dart
Normal file
@@ -0,0 +1,218 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:venera/components/components.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/network/download.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
||||
class DownloadingPage extends StatefulWidget {
|
||||
const DownloadingPage({super.key});
|
||||
|
||||
@override
|
||||
State<DownloadingPage> createState() => _DownloadingPageState();
|
||||
}
|
||||
|
||||
class _DownloadingPageState extends State<DownloadingPage> {
|
||||
@override
|
||||
void initState() {
|
||||
LocalManager().addListener(update);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
LocalManager().removeListener(update);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void update() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopUpWidgetScaffold(
|
||||
title: "",
|
||||
body: ListView.builder(
|
||||
itemCount: LocalManager().downloadingTasks.length + 1,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
if (i == 0) {
|
||||
return buildTop();
|
||||
}
|
||||
i--;
|
||||
|
||||
return _DownloadTaskTile(
|
||||
task: LocalManager().downloadingTasks[i],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTop() {
|
||||
int speed = 0;
|
||||
if (LocalManager().downloadingTasks.isNotEmpty) {
|
||||
speed = LocalManager().downloadingTasks.first.speed;
|
||||
}
|
||||
var first = LocalManager().downloadingTasks.firstOrNull;
|
||||
return Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: context.colorScheme.outlineVariant,
|
||||
width: 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (first?.isPaused == true)
|
||||
Text(
|
||||
"Paused".tl,
|
||||
style: ts.s18.bold,
|
||||
)
|
||||
else if (first?.isError == true)
|
||||
Text(
|
||||
"Error".tl,
|
||||
style: ts.s18.bold,
|
||||
)
|
||||
else
|
||||
Text(
|
||||
"${bytesToReadableString(speed)}/s",
|
||||
style: ts.s18.bold,
|
||||
),
|
||||
const Spacer(),
|
||||
if (first?.isPaused == true || first?.isError == true)
|
||||
OutlinedButton(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.play_arrow, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
Text("Start".tl),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
first!.resume();
|
||||
},
|
||||
)
|
||||
else if (first != null)
|
||||
OutlinedButton(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.pause, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
Text("Pause".tl),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
first.pause();
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingHorizontal(16),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DownloadTaskTile extends StatefulWidget {
|
||||
const _DownloadTaskTile({required this.task});
|
||||
|
||||
final DownloadTask task;
|
||||
|
||||
@override
|
||||
State<_DownloadTaskTile> createState() => _DownloadTaskTileState();
|
||||
}
|
||||
|
||||
class _DownloadTaskTileState extends State<_DownloadTaskTile> {
|
||||
@override
|
||||
void initState() {
|
||||
widget.task.addListener(update);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.task.removeListener(update);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void update() {
|
||||
context.findAncestorStateOfType<_DownloadingPageState>()?.update();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 136,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 82,
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: context.colorScheme.primaryContainer,
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: widget.task.cover == null
|
||||
? null
|
||||
: Image.file(
|
||||
File(widget.task.cover!),
|
||||
filterQuality: FilterQuality.medium,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.task.title,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const Spacer(),
|
||||
MenuButton(
|
||||
entries: [
|
||||
MenuEntry(
|
||||
icon: Icons.close,
|
||||
text: "Cancel".tl,
|
||||
onClick: () {
|
||||
widget.task.cancel();
|
||||
},
|
||||
),
|
||||
MenuEntry(
|
||||
icon: Icons.vertical_align_top,
|
||||
text: "Move To First".tl,
|
||||
onClick: () {
|
||||
LocalManager().moveToFirst(widget.task);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
if (!widget.task.isPaused || widget.task.isError)
|
||||
Text(
|
||||
widget.task.message,
|
||||
style: ts.s12,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
LinearProgressIndicator(
|
||||
value: widget.task.progress,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -12,11 +12,14 @@ import 'package:venera/foundation/log.dart';
|
||||
import 'package:venera/pages/accounts_page.dart';
|
||||
import 'package:venera/pages/comic_page.dart';
|
||||
import 'package:venera/pages/comic_source_page.dart';
|
||||
import 'package:venera/pages/downloading_page.dart';
|
||||
import 'package:venera/pages/history_page.dart';
|
||||
import 'package:venera/pages/search_page.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
||||
import 'local_comics_page.dart';
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@@ -163,8 +166,8 @@ class _HistoryState extends State<_History> {
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
width: 96,
|
||||
height: 128,
|
||||
width: 92,
|
||||
height: 114,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -242,7 +245,9 @@ class _LocalState extends State<_Local> {
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
context.to(() => const LocalComicsPage());
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -277,12 +282,12 @@ class _LocalState extends State<_Local> {
|
||||
itemBuilder: (context, index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
// TODO: view local comic
|
||||
local[index].read();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
width: 96,
|
||||
height: 128,
|
||||
width: 92,
|
||||
height: 114,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -307,6 +312,24 @@ class _LocalState extends State<_Local> {
|
||||
).paddingHorizontal(8),
|
||||
Row(
|
||||
children: [
|
||||
if (LocalManager().downloadingTasks.isNotEmpty)
|
||||
Button.outlined(
|
||||
child: Row(
|
||||
children: [
|
||||
if(LocalManager().downloadingTasks.first.isPaused)
|
||||
const Icon(Icons.pause_circle_outline, size: 18)
|
||||
else
|
||||
const _AnimatedDownloadingIcon(),
|
||||
const SizedBox(width: 8),
|
||||
Text("@a Tasks".tlParams({
|
||||
'a': LocalManager().downloadingTasks.length,
|
||||
})),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
showPopUpWidget(context, const DownloadingPage());
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
Button.filled(
|
||||
onPressed: import,
|
||||
@@ -601,6 +624,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
||||
chapters: Map.fromIterables(chapters, chapters),
|
||||
cover: coverPath,
|
||||
comicType: ComicType.local,
|
||||
downloadedChapters: chapters,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
@@ -810,3 +834,62 @@ class _AccountsWidgetState extends State<_AccountsWidget> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedDownloadingIcon extends StatefulWidget {
|
||||
const _AnimatedDownloadingIcon();
|
||||
|
||||
@override
|
||||
State<_AnimatedDownloadingIcon> createState() =>
|
||||
__AnimatedDownloadingIconState();
|
||||
}
|
||||
|
||||
class __AnimatedDownloadingIconState extends State<_AnimatedDownloadingIcon>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
lowerBound: -1,
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 18 * _controller.value),
|
||||
child: Icon(
|
||||
Icons.arrow_downward,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
66
lib/pages/local_comics_page.dart
Normal file
66
lib/pages/local_comics_page.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:venera/components/components.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/pages/downloading_page.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
||||
class LocalComicsPage extends StatefulWidget {
|
||||
const LocalComicsPage({super.key});
|
||||
|
||||
@override
|
||||
State<LocalComicsPage> createState() => _LocalComicsPageState();
|
||||
}
|
||||
|
||||
class _LocalComicsPageState extends State<LocalComicsPage> {
|
||||
late List<LocalComic> comics;
|
||||
|
||||
void update() {
|
||||
setState(() {
|
||||
comics = LocalManager().getComics();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
comics = LocalManager().getComics();
|
||||
LocalManager().addListener(update);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
LocalManager().removeListener(update);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SmoothCustomScrollView(
|
||||
slivers: [
|
||||
SliverAppbar(
|
||||
title: Text("Local".tl),
|
||||
actions: [
|
||||
Tooltip(
|
||||
message: "Downloading".tl,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: () {
|
||||
showPopUpWidget(context, const DownloadingPage());
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
SliverGridComics(
|
||||
comics: comics,
|
||||
onTap: (c) {
|
||||
(c as LocalComic).read();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -510,6 +510,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
}
|
||||
});
|
||||
}
|
||||
if(event is KeyUpEvent) {
|
||||
return;
|
||||
}
|
||||
bool? forward;
|
||||
if (reader.mode == ReaderMode.continuousLeftToRight &&
|
||||
event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
|
Reference in New Issue
Block a user