mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Improve UI
This commit is contained in:
@@ -1,5 +1,27 @@
|
|||||||
part of 'components.dart';
|
part of 'components.dart';
|
||||||
|
|
||||||
|
ImageProvider? _findImageProvider(Comic comic) {
|
||||||
|
ImageProvider image;
|
||||||
|
if (comic is LocalComic) {
|
||||||
|
image = LocalComicImageProvider(comic);
|
||||||
|
} else if (comic is History) {
|
||||||
|
image = HistoryImageProvider(comic);
|
||||||
|
} else if (comic.sourceKey == 'local') {
|
||||||
|
var localComic = LocalManager().find(comic.id, ComicType.local);
|
||||||
|
if (localComic == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
image = FileImage(localComic.coverFile);
|
||||||
|
} else {
|
||||||
|
image = CachedImageProvider(
|
||||||
|
comic.cover,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
cid: comic.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
class ComicTile extends StatelessWidget {
|
class ComicTile extends StatelessWidget {
|
||||||
const ComicTile(
|
const ComicTile(
|
||||||
{super.key,
|
{super.key,
|
||||||
@@ -27,8 +49,14 @@ class ComicTile extends StatelessWidget {
|
|||||||
onTap!();
|
onTap!();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
App.mainNavigatorKey?.currentContext
|
App.mainNavigatorKey?.currentContext?.to(
|
||||||
?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey));
|
() => ComicPage(
|
||||||
|
id: comic.id,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
cover: comic.cover,
|
||||||
|
title: comic.title,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLongPressed(context) {
|
void _onLongPressed(context) {
|
||||||
@@ -61,8 +89,14 @@ class ComicTile extends StatelessWidget {
|
|||||||
icon: Icons.chrome_reader_mode_outlined,
|
icon: Icons.chrome_reader_mode_outlined,
|
||||||
text: 'Details'.tl,
|
text: 'Details'.tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
App.mainNavigatorKey?.currentContext
|
App.mainNavigatorKey?.currentContext?.to(
|
||||||
?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey));
|
() => ComicPage(
|
||||||
|
id: comic.id,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
cover: comic.cover,
|
||||||
|
title: comic.title,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
@@ -161,23 +195,9 @@ class ComicTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildImage(BuildContext context) {
|
Widget buildImage(BuildContext context) {
|
||||||
ImageProvider image;
|
var image = _findImageProvider(comic);
|
||||||
if (comic is LocalComic) {
|
if (image == null) {
|
||||||
image = LocalComicImageProvider(comic as LocalComic);
|
return const SizedBox();
|
||||||
} else if (comic is History) {
|
|
||||||
image = HistoryImageProvider(comic as History);
|
|
||||||
} else if (comic.sourceKey == 'local') {
|
|
||||||
var localComic = LocalManager().find(comic.id, ComicType.local);
|
|
||||||
if (localComic == null) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
image = FileImage(localComic.coverFile);
|
|
||||||
} else {
|
|
||||||
image = CachedImageProvider(
|
|
||||||
comic.cover,
|
|
||||||
sourceKey: comic.sourceKey,
|
|
||||||
cid: comic.id,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return AnimatedImage(
|
return AnimatedImage(
|
||||||
image: image,
|
image: image,
|
||||||
@@ -199,15 +219,25 @@ class ComicTile extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.fromLTRB(16, 8, 24, 8),
|
padding: const EdgeInsets.fromLTRB(16, 8, 24, 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Hero(
|
||||||
width: height * 0.68,
|
tag: "cover${comic.id}${comic.sourceKey}",
|
||||||
height: double.infinity,
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
width: height * 0.68,
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
height: double.infinity,
|
||||||
borderRadius: BorderRadius.circular(8),
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
blurRadius: 1,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: buildImage(context),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: buildImage(context),
|
|
||||||
),
|
),
|
||||||
SizedBox.fromSize(
|
SizedBox.fromSize(
|
||||||
size: const Size(16, 5),
|
size: const Size(16, 5),
|
||||||
@@ -248,20 +278,23 @@ class ComicTile extends StatelessWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Hero(
|
||||||
decoration: BoxDecoration(
|
tag: "cover${comic.id}${comic.sourceKey}",
|
||||||
color: context.colorScheme.secondaryContainer,
|
child: Container(
|
||||||
borderRadius: BorderRadius.circular(8),
|
decoration: BoxDecoration(
|
||||||
boxShadow: [
|
color: context.colorScheme.secondaryContainer,
|
||||||
BoxShadow(
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: Colors.black.toOpacity(0.2),
|
boxShadow: [
|
||||||
blurRadius: 2,
|
BoxShadow(
|
||||||
offset: const Offset(0, 2),
|
color: Colors.black.toOpacity(0.2),
|
||||||
),
|
blurRadius: 2,
|
||||||
],
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: buildImage(context),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: buildImage(context),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
@@ -1400,7 +1433,7 @@ class _RatingWidgetState extends State<RatingWidget> {
|
|||||||
}
|
}
|
||||||
if (full < widget.count) {
|
if (full < widget.count) {
|
||||||
children.add(ClipRect(
|
children.add(ClipRect(
|
||||||
clipper: SMClipper(rating: star() * widget.size),
|
clipper: _SMClipper(rating: star() * widget.size),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.star,
|
Icons.star,
|
||||||
size: widget.size,
|
size: widget.size,
|
||||||
@@ -1449,10 +1482,10 @@ class _RatingWidgetState extends State<RatingWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SMClipper extends CustomClipper<Rect> {
|
class _SMClipper extends CustomClipper<Rect> {
|
||||||
final double rating;
|
final double rating;
|
||||||
|
|
||||||
SMClipper({required this.rating});
|
_SMClipper({required this.rating});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rect getClip(Size size) {
|
Rect getClip(Size size) {
|
||||||
@@ -1460,7 +1493,52 @@ class SMClipper extends CustomClipper<Rect> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldReclip(SMClipper oldClipper) {
|
bool shouldReclip(_SMClipper oldClipper) {
|
||||||
return rating != oldClipper.rating;
|
return rating != oldClipper.rating;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SimpleComicTile extends StatelessWidget {
|
||||||
|
const SimpleComicTile({super.key, required this.comic, this.onTap});
|
||||||
|
|
||||||
|
final Comic comic;
|
||||||
|
|
||||||
|
final void Function()? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var image = _findImageProvider(comic);
|
||||||
|
|
||||||
|
var child = image == null
|
||||||
|
? const SizedBox()
|
||||||
|
: AnimatedImage(
|
||||||
|
image: image,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
);
|
||||||
|
|
||||||
|
return AnimatedTapRegion(
|
||||||
|
borderRadius: 8,
|
||||||
|
onTap: onTap ?? () {
|
||||||
|
context.to(
|
||||||
|
() => ComicPage(
|
||||||
|
id: comic.id,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 92,
|
||||||
|
height: 114,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -41,39 +41,44 @@ class AnimatedTapRegion extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AnimatedTapRegionState extends State<AnimatedTapRegion> {
|
class _AnimatedTapRegionState extends State<AnimatedTapRegion> {
|
||||||
bool isScaled = false;
|
|
||||||
|
|
||||||
bool isHovered = false;
|
bool isHovered = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
onEnter: (_) {
|
onEnter: (_) {
|
||||||
isHovered = true;
|
setState(() {
|
||||||
if (!isScaled) {
|
isHovered = true;
|
||||||
Future.delayed(const Duration(milliseconds: 100), () {
|
});
|
||||||
if (isHovered) {
|
|
||||||
setState(() => isScaled = true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onExit: (_) {
|
onExit: (_) {
|
||||||
isHovered = false;
|
setState(() {
|
||||||
if(isScaled) {
|
isHovered = false;
|
||||||
setState(() => isScaled = false);
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: widget.onTap,
|
onTap: widget.onTap,
|
||||||
child: ClipRRect(
|
child: AnimatedContainer(
|
||||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
duration: _fastAnimationDuration,
|
||||||
clipBehavior: Clip.antiAlias,
|
decoration: BoxDecoration(
|
||||||
child: AnimatedScale(
|
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||||
duration: _fastAnimationDuration,
|
boxShadow: isHovered
|
||||||
scale: isScaled ? 1.1 : 1,
|
? [
|
||||||
child: widget.child,
|
BoxShadow(
|
||||||
|
color: context.colorScheme.outline,
|
||||||
|
blurRadius: 2,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
BoxShadow(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
blurRadius: 1,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
child: widget.child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -200,15 +200,17 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMainView() {
|
Widget buildMainView() {
|
||||||
return Navigator(
|
return HeroControllerScope(
|
||||||
observers: [widget.observer],
|
controller: MaterialApp.createMaterialHeroController(),
|
||||||
key: widget.navigatorKey,
|
child: Navigator(
|
||||||
onGenerateRoute: (settings) => AppPageRoute(
|
observers: [widget.observer],
|
||||||
preventRebuild: false,
|
key: widget.navigatorKey,
|
||||||
isRootRoute: true,
|
onGenerateRoute: (settings) => AppPageRoute(
|
||||||
builder: (context) {
|
preventRebuild: false,
|
||||||
return _NaviMainView(state: this);
|
builder: (context) {
|
||||||
},
|
return _NaviMainView(state: this);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -362,16 +364,14 @@ class _SideNaviWidget extends StatelessWidget {
|
|||||||
color: enabled ? colorScheme.primaryContainer : null,
|
color: enabled ? colorScheme.primaryContainer : null,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: showTitle ? Row(
|
child: showTitle
|
||||||
children: [
|
? Row(
|
||||||
icon,
|
children: [icon, const SizedBox(width: 12), Text(entry.label)],
|
||||||
const SizedBox(width: 12),
|
)
|
||||||
Text(entry.label)
|
: Align(
|
||||||
],
|
alignment: Alignment.centerLeft,
|
||||||
) : Align(
|
child: icon,
|
||||||
alignment: Alignment.centerLeft,
|
),
|
||||||
child: icon,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
).paddingVertical(4);
|
).paddingVertical(4);
|
||||||
}
|
}
|
||||||
@@ -395,16 +395,14 @@ class _PaneActionWidget extends StatelessWidget {
|
|||||||
duration: const Duration(milliseconds: 180),
|
duration: const Duration(milliseconds: 180),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
height: 38,
|
height: 38,
|
||||||
child: showTitle ? Row(
|
child: showTitle
|
||||||
children: [
|
? Row(
|
||||||
icon,
|
children: [icon, const SizedBox(width: 12), Text(entry.label)],
|
||||||
const SizedBox(width: 12),
|
)
|
||||||
Text(entry.label)
|
: Align(
|
||||||
],
|
alignment: Alignment.centerLeft,
|
||||||
) : Align(
|
child: icon,
|
||||||
alignment: Alignment.centerLeft,
|
),
|
||||||
child: icon,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
).paddingVertical(4);
|
).paddingVertical(4);
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,6 @@ class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin{
|
|||||||
super.barrierDismissible = false,
|
super.barrierDismissible = false,
|
||||||
this.enableIOSGesture = true,
|
this.enableIOSGesture = true,
|
||||||
this.preventRebuild = true,
|
this.preventRebuild = true,
|
||||||
this.isRootRoute = false,
|
|
||||||
}) {
|
}) {
|
||||||
assert(opaque);
|
assert(opaque);
|
||||||
}
|
}
|
||||||
@@ -50,9 +49,6 @@ class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin{
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
final bool preventRebuild;
|
final bool preventRebuild;
|
||||||
|
|
||||||
@override
|
|
||||||
final bool isRootRoute;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
|
mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
|
||||||
@@ -79,8 +75,6 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
|
|||||||
|
|
||||||
bool get preventRebuild;
|
bool get preventRebuild;
|
||||||
|
|
||||||
bool get isRootRoute;
|
|
||||||
|
|
||||||
Widget? _child;
|
Widget? _child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -121,22 +115,6 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||||
if(isRootRoute) {
|
|
||||||
return FadeTransition(
|
|
||||||
opacity: Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
|
|
||||||
parent: animation,
|
|
||||||
curve: Curves.ease
|
|
||||||
)),
|
|
||||||
child: FadeTransition(
|
|
||||||
opacity: Tween<double>(begin: 1.0, end: 0).animate(CurvedAnimation(
|
|
||||||
parent: secondaryAnimation,
|
|
||||||
curve: Curves.ease
|
|
||||||
)),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SlidePageTransitionBuilder().buildTransitions(
|
return SlidePageTransitionBuilder().buildTransitions(
|
||||||
this,
|
this,
|
||||||
context,
|
context,
|
||||||
|
@@ -1,14 +1,11 @@
|
|||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:shimmer/shimmer.dart";
|
import 'package:shimmer_animation/shimmer_animation.dart';
|
||||||
import "package:venera/components/components.dart";
|
import "package:venera/components/components.dart";
|
||||||
import "package:venera/foundation/app.dart";
|
import "package:venera/foundation/app.dart";
|
||||||
import "package:venera/foundation/comic_source/comic_source.dart";
|
import "package:venera/foundation/comic_source/comic_source.dart";
|
||||||
import "package:venera/foundation/image_provider/cached_image.dart";
|
|
||||||
import "package:venera/pages/search_result_page.dart";
|
import "package:venera/pages/search_result_page.dart";
|
||||||
import "package:venera/utils/translations.dart";
|
import "package:venera/utils/translations.dart";
|
||||||
|
|
||||||
import "comic_page.dart";
|
|
||||||
|
|
||||||
class AggregatedSearchPage extends StatefulWidget {
|
class AggregatedSearchPage extends StatefulWidget {
|
||||||
const AggregatedSearchPage({super.key, required this.keyword});
|
const AggregatedSearchPage({super.key, required this.keyword});
|
||||||
|
|
||||||
@@ -73,9 +70,9 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
|||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
|
|
||||||
static const _kComicHeight = 144.0;
|
static const _kComicHeight = 132.0;
|
||||||
|
|
||||||
get _comicWidth => _kComicHeight * 0.72;
|
get _comicWidth => _kComicHeight * 0.7;
|
||||||
|
|
||||||
static const _kLeftPadding = 16.0;
|
static const _kLeftPadding = 16.0;
|
||||||
|
|
||||||
@@ -123,28 +120,9 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildComic(Comic c) {
|
Widget buildComic(Comic c) {
|
||||||
return AnimatedTapRegion(
|
return SimpleComicTile(comic: c)
|
||||||
borderRadius: 8,
|
.paddingLeft(_kLeftPadding)
|
||||||
onTap: () {
|
.paddingBottom(2);
|
||||||
context.to(() => ComicPage(
|
|
||||||
id: c.id,
|
|
||||||
sourceKey: c.sourceKey,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
height: _kComicHeight,
|
|
||||||
width: _comicWidth,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.colorScheme.surfaceContainerLow,
|
|
||||||
),
|
|
||||||
child: AnimatedImage(
|
|
||||||
width: _comicWidth,
|
|
||||||
height: _kComicHeight,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
image: CachedImageProvider(c.cover),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).paddingLeft(_kLeftPadding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -169,10 +147,7 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
height: _kComicHeight,
|
height: _kComicHeight,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Shimmer.fromColors(
|
child: Shimmer(
|
||||||
baseColor: context.colorScheme.surfaceContainerLow,
|
|
||||||
highlightColor: context.colorScheme.surfaceContainer,
|
|
||||||
direction: ShimmerDirection.ltr,
|
|
||||||
child: LayoutBuilder(builder: (context, constrains) {
|
child: LayoutBuilder(builder: (context, constrains) {
|
||||||
var itemWidth = _comicWidth + _kLeftPadding;
|
var itemWidth = _comicWidth + _kLeftPadding;
|
||||||
var items = (constrains.maxWidth / itemWidth).ceil();
|
var items = (constrains.maxWidth / itemWidth).ceil();
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.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';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
@@ -26,12 +27,22 @@ import 'dart:math' as math;
|
|||||||
import 'comments_page.dart';
|
import 'comments_page.dart';
|
||||||
|
|
||||||
class ComicPage extends StatefulWidget {
|
class ComicPage extends StatefulWidget {
|
||||||
const ComicPage({super.key, required this.id, required this.sourceKey});
|
const ComicPage({
|
||||||
|
super.key,
|
||||||
|
required this.id,
|
||||||
|
required this.sourceKey,
|
||||||
|
this.cover,
|
||||||
|
this.title,
|
||||||
|
});
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
final String sourceKey;
|
final String sourceKey;
|
||||||
|
|
||||||
|
final String? cover;
|
||||||
|
|
||||||
|
final String? title;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ComicPage> createState() => _ComicPageState();
|
State<ComicPage> createState() => _ComicPageState();
|
||||||
}
|
}
|
||||||
@@ -55,13 +66,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildLoading() {
|
Widget buildLoading() {
|
||||||
return Column(
|
return _ComicPageLoadingPlaceHolder(
|
||||||
children: [
|
cover: widget.cover,
|
||||||
const Appbar(title: Text("")),
|
title: widget.title,
|
||||||
Expanded(
|
sourceKey: widget.sourceKey,
|
||||||
child: super.buildLoading(),
|
cid: widget.id,
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,21 +210,32 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Container(
|
Hero(
|
||||||
decoration: BoxDecoration(
|
tag: "cover${comic.id}${comic.sourceKey}",
|
||||||
color: context.colorScheme.primaryContainer,
|
child: Container(
|
||||||
borderRadius: BorderRadius.circular(8),
|
decoration: BoxDecoration(
|
||||||
),
|
color: context.colorScheme.primaryContainer,
|
||||||
height: 144,
|
borderRadius: BorderRadius.circular(8),
|
||||||
width: 144 * 0.72,
|
boxShadow: [
|
||||||
clipBehavior: Clip.antiAlias,
|
BoxShadow(
|
||||||
child: AnimatedImage(
|
color: context.colorScheme.outlineVariant,
|
||||||
image: CachedImageProvider(
|
blurRadius: 1,
|
||||||
comic.cover,
|
offset: const Offset(0, 1),
|
||||||
sourceKey: comic.sourceKey,
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
height: 144,
|
||||||
|
width: 144 * 0.72,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(
|
||||||
|
widget.cover ?? comic.cover,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
cid: comic.id,
|
||||||
|
),
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
),
|
),
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
@@ -1946,3 +1966,124 @@ class _CommentWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ComicPageLoadingPlaceHolder extends StatelessWidget {
|
||||||
|
const _ComicPageLoadingPlaceHolder({
|
||||||
|
this.cover,
|
||||||
|
this.title,
|
||||||
|
required this.sourceKey,
|
||||||
|
required this.cid,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? cover;
|
||||||
|
|
||||||
|
final String? title;
|
||||||
|
|
||||||
|
final String sourceKey;
|
||||||
|
|
||||||
|
final String cid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget buildContainer(double? width, double? height,
|
||||||
|
{Color? color, double? radius}) {
|
||||||
|
return Container(
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color ?? context.colorScheme.surfaceContainerLow,
|
||||||
|
borderRadius: BorderRadius.circular(radius ?? 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Shimmer(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Appbar(title: Text(""), backgroundColor: context.colorScheme.surface),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
buildImage(context),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (title != null)
|
||||||
|
Text(title ?? "", style: ts.s18)
|
||||||
|
else
|
||||||
|
buildContainer(200, 25),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
buildContainer(80, 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (context.width < changePoint)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: buildContainer(null, 36, radius: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: buildContainer(null, 36, radius: 18),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.4,
|
||||||
|
).fixHeight(24).fixWidth(24),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildImage(BuildContext context) {
|
||||||
|
Widget child;
|
||||||
|
if (cover != null) {
|
||||||
|
child = AnimatedImage(
|
||||||
|
image: CachedImageProvider(
|
||||||
|
cover!,
|
||||||
|
sourceKey: sourceKey,
|
||||||
|
cid: cid,
|
||||||
|
),
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
child = const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Hero(
|
||||||
|
tag: "cover$cid$sourceKey",
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
blurRadius: 1,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
height: 144,
|
||||||
|
width: 144 * 0.72,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -6,8 +6,6 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
|
|||||||
import 'package:venera/foundation/consts.dart';
|
import 'package:venera/foundation/consts.dart';
|
||||||
import 'package:venera/foundation/favorites.dart';
|
import 'package:venera/foundation/favorites.dart';
|
||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/image_provider/history_image_provider.dart';
|
|
||||||
import 'package:venera/foundation/image_provider/local_comic_image.dart';
|
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/pages/accounts_page.dart';
|
import 'package:venera/pages/accounts_page.dart';
|
||||||
@@ -267,8 +265,8 @@ class _HistoryState extends State<_History> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: history.length,
|
itemCount: history.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return AnimatedTapRegion(
|
return SimpleComicTile(
|
||||||
borderRadius: 8,
|
comic: history[index],
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.to(
|
context.to(
|
||||||
() => ComicPage(
|
() => ComicPage(
|
||||||
@@ -277,25 +275,7 @@ class _HistoryState extends State<_History> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
).paddingHorizontal(8).paddingVertical(2);
|
||||||
width: 92,
|
|
||||||
height: 114,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.secondaryContainer,
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: AnimatedImage(
|
|
||||||
image: HistoryImageProvider(history[index]),
|
|
||||||
width: 96,
|
|
||||||
height: 128,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
filterQuality: FilterQuality.medium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).paddingHorizontal(8);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).paddingHorizontal(8).paddingBottom(16),
|
).paddingHorizontal(8).paddingBottom(16),
|
||||||
@@ -388,32 +368,8 @@ class _LocalState extends State<_Local> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: local.length,
|
itemCount: local.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return AnimatedTapRegion(
|
return SimpleComicTile(comic: local[index])
|
||||||
onTap: () {
|
.paddingHorizontal(8);
|
||||||
local[index].read();
|
|
||||||
},
|
|
||||||
borderRadius: 8,
|
|
||||||
child: Container(
|
|
||||||
width: 92,
|
|
||||||
height: 114,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.secondaryContainer,
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: AnimatedImage(
|
|
||||||
image: LocalComicImageProvider(
|
|
||||||
local[index],
|
|
||||||
),
|
|
||||||
width: 96,
|
|
||||||
height: 128,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
filterQuality: FilterQuality.medium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).paddingHorizontal(8);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).paddingHorizontal(8),
|
).paddingHorizontal(8),
|
||||||
|
10
pubspec.lock
10
pubspec.lock
@@ -867,14 +867,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "5.0.2"
|
||||||
shimmer:
|
shimmer_animation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shimmer
|
name: shimmer_animation
|
||||||
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
|
sha256: "9357080b7dd892aae837d569e1fbbcbe7f9a02ca994e558561d90e35e92f1101"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "2.2.2"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -1147,4 +1147,4 @@ packages:
|
|||||||
version: "0.0.6"
|
version: "0.0.6"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.0 <4.0.0"
|
dart: ">=3.6.0 <4.0.0"
|
||||||
flutter: ">=3.27.1"
|
flutter: ">=3.27.1"
|
||||||
|
@@ -69,7 +69,7 @@ dependencies:
|
|||||||
ref: 7637b8b67d0a831f3cd7e702b8173e300880d32e
|
ref: 7637b8b67d0a831f3cd7e702b8173e300880d32e
|
||||||
pdf: ^3.11.1
|
pdf: ^3.11.1
|
||||||
dynamic_color: ^1.7.0
|
dynamic_color: ^1.7.0
|
||||||
shimmer: ^3.0.0
|
shimmer_animation: ^2.1.0
|
||||||
flutter_memory_info: ^0.0.1
|
flutter_memory_info: ^0.0.1
|
||||||
syntax_highlight: ^0.4.0
|
syntax_highlight: ^0.4.0
|
||||||
text_scroll: ^0.2.0
|
text_scroll: ^0.2.0
|
||||||
|
Reference in New Issue
Block a user