From 399b9abaeee41434ed858547a47d97965fd08dfa Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 15 Jan 2025 18:24:38 +0800 Subject: [PATCH] Improve UI --- lib/components/comic.dart | 170 ++++++++++++++++------- lib/components/gesture.dart | 47 ++++--- lib/components/navigation_bar.dart | 56 ++++---- lib/foundation/app_page_route.dart | 22 --- lib/pages/aggregated_search_page.dart | 39 +----- lib/pages/comic_page.dart | 185 +++++++++++++++++++++++--- lib/pages/home_page.dart | 54 +------- pubspec.lock | 10 +- pubspec.yaml | 2 +- 9 files changed, 358 insertions(+), 227 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 71799f7..9ffe801 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -1,5 +1,27 @@ 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 { const ComicTile( {super.key, @@ -27,8 +49,14 @@ class ComicTile extends StatelessWidget { onTap!(); return; } - App.mainNavigatorKey?.currentContext - ?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); + App.mainNavigatorKey?.currentContext?.to( + () => ComicPage( + id: comic.id, + sourceKey: comic.sourceKey, + cover: comic.cover, + title: comic.title, + ), + ); } void _onLongPressed(context) { @@ -61,8 +89,14 @@ class ComicTile extends StatelessWidget { icon: Icons.chrome_reader_mode_outlined, text: 'Details'.tl, onClick: () { - App.mainNavigatorKey?.currentContext - ?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); + App.mainNavigatorKey?.currentContext?.to( + () => ComicPage( + id: comic.id, + sourceKey: comic.sourceKey, + cover: comic.cover, + title: comic.title, + ), + ); }, ), MenuEntry( @@ -161,23 +195,9 @@ class ComicTile extends StatelessWidget { } Widget buildImage(BuildContext context) { - ImageProvider image; - if (comic is LocalComic) { - image = LocalComicImageProvider(comic as LocalComic); - } 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, - ); + var image = _findImageProvider(comic); + if (image == null) { + return const SizedBox(); } return AnimatedImage( image: image, @@ -199,15 +219,25 @@ class ComicTile extends StatelessWidget { padding: const EdgeInsets.fromLTRB(16, 8, 24, 8), child: Row( children: [ - Container( - width: height * 0.68, - height: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), + Hero( + tag: "cover${comic.id}${comic.sourceKey}", + child: Container( + width: height * 0.68, + height: double.infinity, + 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( size: const Size(16, 5), @@ -248,20 +278,23 @@ class ComicTile extends StatelessWidget { child: Stack( children: [ Positioned.fill( - child: Container( - decoration: BoxDecoration( - color: context.colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.toOpacity(0.2), - blurRadius: 2, - offset: const Offset(0, 2), - ), - ], + child: Hero( + tag: "cover${comic.id}${comic.sourceKey}", + child: Container( + decoration: BoxDecoration( + color: context.colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + 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( @@ -1400,7 +1433,7 @@ class _RatingWidgetState extends State { } if (full < widget.count) { children.add(ClipRect( - clipper: SMClipper(rating: star() * widget.size), + clipper: _SMClipper(rating: star() * widget.size), child: Icon( Icons.star, size: widget.size, @@ -1449,10 +1482,10 @@ class _RatingWidgetState extends State { } } -class SMClipper extends CustomClipper { +class _SMClipper extends CustomClipper { final double rating; - SMClipper({required this.rating}); + _SMClipper({required this.rating}); @override Rect getClip(Size size) { @@ -1460,7 +1493,52 @@ class SMClipper extends CustomClipper { } @override - bool shouldReclip(SMClipper oldClipper) { + bool shouldReclip(_SMClipper oldClipper) { 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, + ), + ); + } +} diff --git a/lib/components/gesture.dart b/lib/components/gesture.dart index 10bc71e..44fd07c 100644 --- a/lib/components/gesture.dart +++ b/lib/components/gesture.dart @@ -41,39 +41,44 @@ class AnimatedTapRegion extends StatefulWidget { } class _AnimatedTapRegionState extends State { - bool isScaled = false; - bool isHovered = false; @override Widget build(BuildContext context) { return MouseRegion( onEnter: (_) { - isHovered = true; - if (!isScaled) { - Future.delayed(const Duration(milliseconds: 100), () { - if (isHovered) { - setState(() => isScaled = true); - } - }); - } + setState(() { + isHovered = true; + }); }, onExit: (_) { - isHovered = false; - if(isScaled) { - setState(() => isScaled = false); - } + setState(() { + isHovered = false; + }); }, child: GestureDetector( onTap: widget.onTap, - child: ClipRRect( - borderRadius: BorderRadius.circular(widget.borderRadius), - clipBehavior: Clip.antiAlias, - child: AnimatedScale( - duration: _fastAnimationDuration, - scale: isScaled ? 1.1 : 1, - child: widget.child, + child: AnimatedContainer( + duration: _fastAnimationDuration, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.borderRadius), + boxShadow: isHovered + ? [ + 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, ), ), ); diff --git a/lib/components/navigation_bar.dart b/lib/components/navigation_bar.dart index 09dc06e..791436f 100644 --- a/lib/components/navigation_bar.dart +++ b/lib/components/navigation_bar.dart @@ -200,15 +200,17 @@ class NaviPaneState extends State } Widget buildMainView() { - return Navigator( - observers: [widget.observer], - key: widget.navigatorKey, - onGenerateRoute: (settings) => AppPageRoute( - preventRebuild: false, - isRootRoute: true, - builder: (context) { - return _NaviMainView(state: this); - }, + return HeroControllerScope( + controller: MaterialApp.createMaterialHeroController(), + child: Navigator( + observers: [widget.observer], + key: widget.navigatorKey, + onGenerateRoute: (settings) => AppPageRoute( + preventRebuild: false, + builder: (context) { + return _NaviMainView(state: this); + }, + ), ), ); } @@ -362,16 +364,14 @@ class _SideNaviWidget extends StatelessWidget { color: enabled ? colorScheme.primaryContainer : null, borderRadius: BorderRadius.circular(12), ), - child: showTitle ? Row( - children: [ - icon, - const SizedBox(width: 12), - Text(entry.label) - ], - ) : Align( - alignment: Alignment.centerLeft, - child: icon, - ), + child: showTitle + ? Row( + children: [icon, const SizedBox(width: 12), Text(entry.label)], + ) + : Align( + alignment: Alignment.centerLeft, + child: icon, + ), ), ).paddingVertical(4); } @@ -395,16 +395,14 @@ class _PaneActionWidget extends StatelessWidget { duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 12), height: 38, - child: showTitle ? Row( - children: [ - icon, - const SizedBox(width: 12), - Text(entry.label) - ], - ) : Align( - alignment: Alignment.centerLeft, - child: icon, - ), + child: showTitle + ? Row( + children: [icon, const SizedBox(width: 12), Text(entry.label)], + ) + : Align( + alignment: Alignment.centerLeft, + child: icon, + ), ), ).paddingVertical(4); } diff --git a/lib/foundation/app_page_route.dart b/lib/foundation/app_page_route.dart index 39d3bd6..e273a03 100644 --- a/lib/foundation/app_page_route.dart +++ b/lib/foundation/app_page_route.dart @@ -19,7 +19,6 @@ class AppPageRoute extends PageRoute with _AppRouteTransitionMixin{ super.barrierDismissible = false, this.enableIOSGesture = true, this.preventRebuild = true, - this.isRootRoute = false, }) { assert(opaque); } @@ -50,9 +49,6 @@ class AppPageRoute extends PageRoute with _AppRouteTransitionMixin{ @override final bool preventRebuild; - - @override - final bool isRootRoute; } mixin _AppRouteTransitionMixin on PageRoute { @@ -79,8 +75,6 @@ mixin _AppRouteTransitionMixin on PageRoute { bool get preventRebuild; - bool get isRootRoute; - Widget? _child; @override @@ -121,22 +115,6 @@ mixin _AppRouteTransitionMixin on PageRoute { @override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { - if(isRootRoute) { - return FadeTransition( - opacity: Tween(begin: 0, end: 1.0).animate(CurvedAnimation( - parent: animation, - curve: Curves.ease - )), - child: FadeTransition( - opacity: Tween(begin: 1.0, end: 0).animate(CurvedAnimation( - parent: secondaryAnimation, - curve: Curves.ease - )), - child: child, - ), - ); - } - return SlidePageTransitionBuilder().buildTransitions( this, context, diff --git a/lib/pages/aggregated_search_page.dart b/lib/pages/aggregated_search_page.dart index 16007cd..06093a4 100644 --- a/lib/pages/aggregated_search_page.dart +++ b/lib/pages/aggregated_search_page.dart @@ -1,14 +1,11 @@ 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/foundation/app.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/utils/translations.dart"; -import "comic_page.dart"; - class AggregatedSearchPage extends StatefulWidget { const AggregatedSearchPage({super.key, required this.keyword}); @@ -73,9 +70,9 @@ class _SliverSearchResultState extends State<_SliverSearchResult> with AutomaticKeepAliveClientMixin { 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; @@ -123,28 +120,9 @@ class _SliverSearchResultState extends State<_SliverSearchResult> } Widget buildComic(Comic c) { - return AnimatedTapRegion( - borderRadius: 8, - onTap: () { - 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); + return SimpleComicTile(comic: c) + .paddingLeft(_kLeftPadding) + .paddingBottom(2); } @override @@ -169,10 +147,7 @@ class _SliverSearchResultState extends State<_SliverSearchResult> SizedBox( height: _kComicHeight, width: double.infinity, - child: Shimmer.fromColors( - baseColor: context.colorScheme.surfaceContainerLow, - highlightColor: context.colorScheme.surfaceContainer, - direction: ShimmerDirection.ltr, + child: Shimmer( child: LayoutBuilder(builder: (context, constrains) { var itemWidth = _comicWidth + _kLeftPadding; var items = (constrains.maxWidth / itemWidth).ceil(); diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index ed84ff4..761f635 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:shimmer_animation/shimmer_animation.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; @@ -26,12 +27,22 @@ import 'dart:math' as math; import 'comments_page.dart'; 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 sourceKey; + final String? cover; + + final String? title; + @override State createState() => _ComicPageState(); } @@ -55,13 +66,11 @@ class _ComicPageState extends LoadingState @override Widget buildLoading() { - return Column( - children: [ - const Appbar(title: Text("")), - Expanded( - child: super.buildLoading(), - ), - ], + return _ComicPageLoadingPlaceHolder( + cover: widget.cover, + title: widget.title, + sourceKey: widget.sourceKey, + cid: widget.id, ); } @@ -201,21 +210,32 @@ class _ComicPageState extends LoadingState crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(width: 16), - Container( - decoration: BoxDecoration( - color: context.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(8), - ), - height: 144, - width: 144 * 0.72, - clipBehavior: Clip.antiAlias, - child: AnimatedImage( - image: CachedImageProvider( - comic.cover, - sourceKey: comic.sourceKey, + Hero( + tag: "cover${comic.id}${comic.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: 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), @@ -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, + ), + ); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 67cb9a3..fee7a82 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -6,8 +6,6 @@ import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/favorites.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/log.dart'; import 'package:venera/pages/accounts_page.dart'; @@ -267,8 +265,8 @@ class _HistoryState extends State<_History> { scrollDirection: Axis.horizontal, itemCount: history.length, itemBuilder: (context, index) { - return AnimatedTapRegion( - borderRadius: 8, + return SimpleComicTile( + comic: history[index], onTap: () { context.to( () => ComicPage( @@ -277,25 +275,7 @@ class _HistoryState extends State<_History> { ), ); }, - child: Container( - 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).paddingVertical(2); }, ), ).paddingHorizontal(8).paddingBottom(16), @@ -388,32 +368,8 @@ class _LocalState extends State<_Local> { scrollDirection: Axis.horizontal, itemCount: local.length, itemBuilder: (context, index) { - return AnimatedTapRegion( - onTap: () { - 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); + return SimpleComicTile(comic: local[index]) + .paddingHorizontal(8); }, ), ).paddingHorizontal(8), diff --git a/pubspec.lock b/pubspec.lock index 89f5900..915d1df 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -867,14 +867,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.2" - shimmer: + shimmer_animation: dependency: "direct main" description: - name: shimmer - sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + name: shimmer_animation + sha256: "9357080b7dd892aae837d569e1fbbcbe7f9a02ca994e558561d90e35e92f1101" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.2.2" sky_engine: dependency: transitive description: flutter @@ -1147,4 +1147,4 @@ packages: version: "0.0.6" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.1" \ No newline at end of file + flutter: ">=3.27.1" diff --git a/pubspec.yaml b/pubspec.yaml index 3c67428..163bfa2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,7 +69,7 @@ dependencies: ref: 7637b8b67d0a831f3cd7e702b8173e300880d32e pdf: ^3.11.1 dynamic_color: ^1.7.0 - shimmer: ^3.0.0 + shimmer_animation: ^2.1.0 flutter_memory_info: ^0.0.1 syntax_highlight: ^0.4.0 text_scroll: ^0.2.0