mirror of
https://github.com/venera-app/venera.git
synced 2025-12-15 22:51:15 +00:00
Add a page to view cover
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:ui';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:shimmer_animation/shimmer_animation.dart';
|
import 'package:shimmer_animation/shimmer_animation.dart';
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@@ -22,6 +25,7 @@ import 'package:venera/pages/favorites/favorites_page.dart';
|
|||||||
import 'package:venera/pages/reader/reader.dart';
|
import 'package:venera/pages/reader/reader.dart';
|
||||||
import 'package:venera/utils/app_links.dart';
|
import 'package:venera/utils/app_links.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
|
import 'package:venera/utils/file_type.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
@@ -39,6 +43,8 @@ part 'comments_preview.dart';
|
|||||||
|
|
||||||
part 'actions.dart';
|
part 'actions.dart';
|
||||||
|
|
||||||
|
part 'cover_viewer.dart';
|
||||||
|
|
||||||
class ComicPage extends StatefulWidget {
|
class ComicPage extends StatefulWidget {
|
||||||
const ComicPage({
|
const ComicPage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -296,31 +302,35 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Hero(
|
GestureDetector(
|
||||||
tag: "cover${widget.heroID}",
|
onTap: () => _viewCover(context),
|
||||||
child: Container(
|
onLongPress: () => _saveCover(context),
|
||||||
decoration: BoxDecoration(
|
child: Hero(
|
||||||
color: context.colorScheme.primaryContainer,
|
tag: "cover${widget.heroID}",
|
||||||
borderRadius: BorderRadius.circular(8),
|
child: Container(
|
||||||
boxShadow: [
|
decoration: BoxDecoration(
|
||||||
BoxShadow(
|
color: context.colorScheme.primaryContainer,
|
||||||
color: context.colorScheme.outlineVariant,
|
borderRadius: BorderRadius.circular(8),
|
||||||
blurRadius: 1,
|
boxShadow: [
|
||||||
offset: const Offset(0, 1),
|
BoxShadow(
|
||||||
),
|
color: context.colorScheme.outlineVariant,
|
||||||
],
|
blurRadius: 1,
|
||||||
),
|
offset: const Offset(0, 1),
|
||||||
height: 144,
|
),
|
||||||
width: 144 * 0.72,
|
],
|
||||||
clipBehavior: Clip.antiAlias,
|
),
|
||||||
child: AnimatedImage(
|
height: 144,
|
||||||
image: CachedImageProvider(
|
width: 144 * 0.72,
|
||||||
widget.cover ?? comic.cover,
|
clipBehavior: Clip.antiAlias,
|
||||||
sourceKey: comic.sourceKey,
|
child: AnimatedImage(
|
||||||
cid: comic.id,
|
image: CachedImageProvider(
|
||||||
|
widget.cover ?? comic.cover,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
cid: comic.id,
|
||||||
|
),
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
),
|
),
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -723,6 +733,54 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
}
|
}
|
||||||
return _CommentsPart(comments: comic.comments!, showMore: showComments);
|
return _CommentsPart(comments: comic.comments!, showMore: showComments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _viewCover(BuildContext context) {
|
||||||
|
final imageProvider = CachedImageProvider(
|
||||||
|
widget.cover ?? comic.cover,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
cid: comic.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.to(
|
||||||
|
() => _CoverViewer(
|
||||||
|
imageProvider: imageProvider,
|
||||||
|
title: comic.title,
|
||||||
|
heroTag: "cover${widget.heroID}",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _saveCover(BuildContext context) async {
|
||||||
|
try {
|
||||||
|
final imageProvider = CachedImageProvider(
|
||||||
|
widget.cover ?? comic.cover,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
cid: comic.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
final imageStream = imageProvider.resolve(const ImageConfiguration());
|
||||||
|
final completer = Completer<Uint8List>();
|
||||||
|
|
||||||
|
imageStream.addListener(
|
||||||
|
ImageStreamListener((ImageInfo info, bool _) async {
|
||||||
|
final byteData = await info.image.toByteData(
|
||||||
|
format: ImageByteFormat.png,
|
||||||
|
);
|
||||||
|
if (byteData != null) {
|
||||||
|
completer.complete(byteData.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = await completer.future;
|
||||||
|
final fileType = detectFileType(data);
|
||||||
|
await saveFile(filename: "cover${fileType.ext}", data: data);
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
context.showMessage(message: "Error".tl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ActionButton extends StatelessWidget {
|
class _ActionButton extends StatelessWidget {
|
||||||
|
|||||||
140
lib/pages/comic_details_page/cover_viewer.dart
Normal file
140
lib/pages/comic_details_page/cover_viewer.dart
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
part of 'comic_page.dart';
|
||||||
|
|
||||||
|
class _CoverViewer extends StatefulWidget {
|
||||||
|
const _CoverViewer({
|
||||||
|
required this.imageProvider,
|
||||||
|
required this.title,
|
||||||
|
required this.heroTag,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ImageProvider imageProvider;
|
||||||
|
final String title;
|
||||||
|
final String heroTag;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CoverViewer> createState() => _CoverViewerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CoverViewerState extends State<_CoverViewer> {
|
||||||
|
bool isAppBarShow = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopScope(
|
||||||
|
canPop: true,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: context.colorScheme.surface,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: PhotoView(
|
||||||
|
imageProvider: widget.imageProvider,
|
||||||
|
minScale: PhotoViewComputedScale.contained * 1.0,
|
||||||
|
maxScale: PhotoViewComputedScale.covered * 3.0,
|
||||||
|
backgroundDecoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surface,
|
||||||
|
),
|
||||||
|
loadingBuilder: (context, event) => Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
height: 24.0,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: event == null || event.expectedTotalBytes == null
|
||||||
|
? null
|
||||||
|
: event.cumulativeBytesLoaded /
|
||||||
|
event.expectedTotalBytes!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTapUp: (context, details, controllerValue) {
|
||||||
|
setState(() {
|
||||||
|
isAppBarShow = !isAppBarShow;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(tag: widget.heroTag),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedPositioned(
|
||||||
|
top: isAppBarShow ? 0 : -(context.padding.top + 52),
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
child: _buildAppBar(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAppBar() {
|
||||||
|
return Material(
|
||||||
|
color: context.colorScheme.surface.toOpacity(0.72),
|
||||||
|
child: BlurEffect(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
height: 52,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.title,
|
||||||
|
style: const TextStyle(fontSize: 18),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.save_alt),
|
||||||
|
onPressed: _saveCover,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingTop(context.padding.top),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _saveCover() async {
|
||||||
|
try {
|
||||||
|
final imageStream = widget.imageProvider.resolve(
|
||||||
|
const ImageConfiguration(),
|
||||||
|
);
|
||||||
|
final completer = Completer<Uint8List>();
|
||||||
|
|
||||||
|
imageStream.addListener(
|
||||||
|
ImageStreamListener((ImageInfo info, bool _) async {
|
||||||
|
final byteData = await info.image.toByteData(
|
||||||
|
format: ImageByteFormat.png,
|
||||||
|
);
|
||||||
|
if (byteData != null) {
|
||||||
|
completer.complete(byteData.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = await completer.future;
|
||||||
|
final fileType = detectFileType(data);
|
||||||
|
await saveFile(filename: "cover_${widget.title}${fileType.ext}", data: data);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
context.showMessage(message: "Error".tl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user