From c4b50a3fb1566363b9447547b08597693d0a4536 Mon Sep 17 00:00:00 2001 From: wgh19 Date: Tue, 14 May 2024 16:43:41 +0800 Subject: [PATCH] improve mobile ui --- lib/components/message.dart | 10 +- lib/components/page_route.dart | 5 +- lib/pages/bookmarks.dart | 33 +++--- lib/pages/following_artworks.dart | 36 +++--- lib/pages/illust_page.dart | 172 ++++++++++++++++++++--------- lib/pages/main_page.dart | 23 ++-- lib/pages/ranking.dart | 3 +- lib/pages/recommendation_page.dart | 38 ++++--- lib/pages/search_page.dart | 2 + lib/pages/user_info_page.dart | 1 + 10 files changed, 212 insertions(+), 111 deletions(-) diff --git a/lib/components/message.dart b/lib/components/message.dart index 1166ca5..f642c2d 100644 --- a/lib/components/message.dart +++ b/lib/components/message.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:pixes/components/md.dart'; void showToast(BuildContext context, {required String message, IconData? icon}) { var newEntry = OverlayEntry( @@ -27,7 +28,7 @@ class ToastOverlay extends StatelessWidget { child: Align( alignment: Alignment.bottomCenter, child: PhysicalModel( - color: Colors.white, + color: ColorScheme.of(context).surface, borderRadius: BorderRadius.circular(4), elevation: 1, child: Container( @@ -42,8 +43,11 @@ class ToastOverlay extends StatelessWidget { ), Text( message, - style: const TextStyle( - fontSize: 16, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: ColorScheme.of(context).onSurface + ), maxLines: 3, ), ], diff --git a/lib/components/page_route.dart b/lib/components/page_route.dart index 5a6bee5..919f647 100644 --- a/lib/components/page_route.dart +++ b/lib/components/page_route.dart @@ -307,7 +307,7 @@ class SideBarRoute extends PopupRoute { final Widget child; @override - Color? get barrierColor => const Color.fromARGB(64, 205, 205, 205); + Color? get barrierColor => Colors.transparent; @override bool get barrierDismissible => true; @@ -331,7 +331,8 @@ class SideBarRoute extends PopupRoute { color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.98), borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), bottomLeft: Radius.circular(4)) ), - constraints: const BoxConstraints(maxWidth: _kSideBarWidth), + constraints: BoxConstraints(maxWidth: min(_kSideBarWidth, + MediaQuery.of(context).size.width)), width: double.infinity, child: child, ), diff --git a/lib/pages/bookmarks.dart b/lib/pages/bookmarks.dart index 0289964..ba0dda0 100644 --- a/lib/pages/bookmarks.dart +++ b/lib/pages/bookmarks.dart @@ -31,20 +31,26 @@ class _BookMarkedArtworkPageState extends State{ } Widget buildTab() { - return SegmentedButton( - options: [ - SegmentedButtonOption("public", "Public".tl), - SegmentedButtonOption("private", "Private".tl), + return Row( + children: [ + Text("Following".tl, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20)), + const Spacer(), + SegmentedButton( + options: [ + SegmentedButtonOption("public", "Public".tl), + SegmentedButtonOption("private", "Private".tl), + ], + onPressed: (key) { + if(key != restrict) { + setState(() { + restrict = key; + }); + } + }, + value: restrict, + ) ], - onPressed: (key) { - if(key != restrict) { - setState(() { - restrict = key; - }); - } - }, - value: restrict, - ).padding(const EdgeInsets.symmetric(vertical: 8, horizontal: 8)); + ).paddingHorizontal(16).paddingVertical(4); } } @@ -62,6 +68,7 @@ class _OneBookmarkedPageState extends MultiPageLoadingState<_OneBookmarkedPage, Widget buildContent(BuildContext context, final List data) { return LayoutBuilder(builder: (context, constrains){ return MasonryGridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8), gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 240, ), diff --git a/lib/pages/following_artworks.dart b/lib/pages/following_artworks.dart index 615368b..36882f5 100644 --- a/lib/pages/following_artworks.dart +++ b/lib/pages/following_artworks.dart @@ -31,21 +31,28 @@ class _FollowingArtworksPageState extends State { } Widget buildTab() { - return SegmentedButton( - options: [ - SegmentedButtonOption("all", "All".tl), - SegmentedButtonOption("public", "Public".tl), - SegmentedButtonOption("private", "Private".tl), + return Row( + children: [ + Text("Following".tl, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),), + const Spacer(), + SegmentedButton( + options: [ + SegmentedButtonOption("all", "All".tl), + SegmentedButtonOption("public", "Public".tl), + SegmentedButtonOption("private", "Private".tl), + ], + onPressed: (key) { + if(key != restrict) { + setState(() { + restrict = key; + }); + } + }, + value: restrict, + ) ], - onPressed: (key) { - if(key != restrict) { - setState(() { - restrict = key; - }); - } - }, - value: restrict, - ).padding(const EdgeInsets.symmetric(vertical: 8, horizontal: 8)); + ).paddingHorizontal(16).paddingBottom(4); } } @@ -63,6 +70,7 @@ class _OneFollowingPageState extends MultiPageLoadingState<_OneFollowingPage, Il Widget buildContent(BuildContext context, final List data) { return LayoutBuilder(builder: (context, constrains){ return MasonryGridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8), gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 240, ), diff --git a/lib/pages/illust_page.dart b/lib/pages/illust_page.dart index 42a2ba4..38773fc 100644 --- a/lib/pages/illust_page.dart +++ b/lib/pages/illust_page.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show Icons; import 'package:pixes/components/animated_image.dart'; import 'package:pixes/components/loading.dart'; @@ -11,6 +12,7 @@ import 'package:pixes/foundation/image_provider.dart'; import 'package:pixes/network/download.dart'; import 'package:pixes/network/network.dart'; import 'package:pixes/pages/image_page.dart'; +import 'package:pixes/pages/search_page.dart'; import 'package:pixes/pages/user_info_page.dart'; import 'package:pixes/utils/translation.dart'; @@ -65,6 +67,7 @@ class _IllustPageState extends State { Widget buildBody(double width, double height) { return ListView.builder( itemCount: widget.illust.images.length + 2, + padding: EdgeInsets.zero, itemBuilder: (context, index) { return buildImage(width, height, index); }); @@ -155,9 +158,7 @@ class _BottomBar extends StatefulWidget { State<_BottomBar> createState() => _BottomBarState(); } -class _BottomBarState extends State<_BottomBar> { - double? top; - +class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin{ double pageHeight = 0; double widgetHeight = 48; @@ -166,36 +167,77 @@ class _BottomBarState extends State<_BottomBar> { double _width = 0; + late VerticalDragGestureRecognizer _recognizer; + + late final AnimationController animationController; + + double get minValue => pageHeight - widgetHeight; + double get maxValue => pageHeight - _kBottomBarHeight; + @override void initState() { _width = widget.width; pageHeight = widget.height; - top = pageHeight - _kBottomBarHeight; Future.delayed(const Duration(milliseconds: 200), () { final box = key.currentContext?.findRenderObject() as RenderBox?; widgetHeight = (box?.size.height) ?? 0; }); + _recognizer = VerticalDragGestureRecognizer() + ..onStart = _handlePointerDown + ..onUpdate = _handlePointerMove + ..onEnd = _handlePointerUp + ..onCancel = _handlePointerCancel; + animationController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 180), + value: 1 + ); super.initState(); } + void _handlePointerDown(DragStartDetails details) {} + void _handlePointerMove(DragUpdateDetails details) { + var offset = details.primaryDelta ?? 0; + final minValue = pageHeight - widgetHeight; + final maxValue = pageHeight - _kBottomBarHeight; + var top = animationController.value * (maxValue - minValue) + minValue; + top = (top + offset).clamp(minValue, maxValue); + animationController.value = (top - minValue) / (maxValue - minValue); + } + void _handlePointerUp(DragEndDetails details) { + var speed = details.primaryVelocity ?? 0; + const minShouldTransitionSpeed = 1000; + if(speed > minShouldTransitionSpeed) { + animationController.forward(); + } else if(speed < minShouldTransitionSpeed) { + animationController.reverse(); + } else { + _handlePointerCancel(); + } + } + void _handlePointerCancel() { + if(animationController.value >= 0.5 ) { + animationController.forward(); + } else { + animationController.reverse(); + } + } + @override void didUpdateWidget(covariant _BottomBar oldWidget) { if (widget.height != pageHeight) { setState(() { pageHeight = widget.height; - top = pageHeight - _kBottomBarHeight; }); } + _recognizer.dispose(); if(_width != widget.width) { _width = widget.width; Future.microtask(() { final box = key.currentContext?.findRenderObject() as RenderBox?; var oldHeight = widgetHeight; widgetHeight = (box?.size.height) ?? 0; - if(oldHeight != widgetHeight && top != pageHeight - _kBottomBarHeight) { - setState(() { - top = pageHeight - widgetHeight; - }); + if(oldHeight != widgetHeight) { + setState(() {}); } }); } @@ -204,32 +246,44 @@ class _BottomBarState extends State<_BottomBar> { @override Widget build(BuildContext context) { - return AnimatedPositioned( - top: top, - left: 0, - right: 0, - duration: const Duration(milliseconds: 180), - curve: Curves.ease, - child: Card( - borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), - backgroundColor: - FluentTheme.of(context).micaBackgroundColor.withOpacity(0.96), - padding: const EdgeInsets.symmetric(horizontal: 8), - child: SizedBox( - width: double.infinity, - key: key, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - buildTop(), - buildStats(), - buildTags(), - SelectableText("${"Artwork ID".tl}: ${widget.illust.id}\n${"Artist ID".tl}: ${widget.illust.author.id}", style: TextStyle(color: ColorScheme.of(context).outline),).paddingLeft(4), - const SizedBox(height: 8,) - ], - ), - ), + return AnimatedBuilder( + animation: CurvedAnimation( + parent: animationController, + curve: Curves.ease, + reverseCurve: Curves.ease, ), + builder: (context, child) { + return Positioned( + top: minValue + (maxValue - minValue) * animationController.value, + left: 0, + right: 0, + child: Listener( + onPointerDown: (event) { + _recognizer.addPointer(event); + }, + child: Card( + borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), + backgroundColor: + FluentTheme.of(context).micaBackgroundColor.withOpacity(0.96), + padding: const EdgeInsets.symmetric(horizontal: 8), + child: SizedBox( + width: double.infinity, + key: key, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildTop(), + buildStats(), + buildTags(), + SelectableText("${"Artwork ID".tl}: ${widget.illust.id}\n${"Artist ID".tl}: ${widget.illust.author.id}", style: TextStyle(color: ColorScheme.of(context).outline),).paddingLeft(4), + const SizedBox(height: 8,) + ], + ), + ), + ), + ), + ); + }, ); } @@ -243,21 +297,17 @@ class _BottomBarState extends State<_BottomBar> { buildAuthor(), ...buildActions(constrains.maxWidth), const Spacer(), - if (top == pageHeight - _kBottomBarHeight) + if (animationController.value == 1) IconButton( icon: const Icon(FluentIcons.up), onPressed: () { - setState(() { - top = pageHeight - widgetHeight; - }); + animationController.reverse(); }) else IconButton( icon: const Icon(FluentIcons.down), onPressed: () { - setState(() { - top = pageHeight - _kBottomBarHeight; - }); + animationController.forward(); }) ], ); @@ -287,14 +337,15 @@ class _BottomBarState extends State<_BottomBar> { }); } + final bool showUserName = MediaQuery.of(context).size.width > 640; + return Card( margin: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 8), borderRadius: BorderRadius.circular(8), - backgroundColor: FluentTheme.of(context).cardColor.withOpacity(0.72), child: SizedBox( height: double.infinity, - width: 246, + width: showUserName ? 246 : 128, child: Row( children: [ SizedBox( @@ -325,12 +376,13 @@ class _BottomBarState extends State<_BottomBar> { const SizedBox( width: 8, ), - Expanded( - child: Text( - widget.illust.author.name, - maxLines: 2, + if(showUserName) + Expanded( + child: Text( + widget.illust.author.name, + maxLines: 2, + ), ), - ), if(isFollowing) Button(onPressed: follow, child: const SizedBox( width: 42, @@ -343,11 +395,14 @@ class _BottomBarState extends State<_BottomBar> { ), )) else if (!widget.illust.author.isFollowed) - Button(onPressed: follow, child: Text("Follow".tl)) + Button(onPressed: follow, child: Text("Follow".tl).fixWidth(56)) else Button( onPressed: follow, - child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).error),), + child: Text( + "Unfollow".tl, + style: TextStyle(color: ColorScheme.of(context).error), + ).fixWidth(56), ), ], ), @@ -558,9 +613,17 @@ class _BottomBarState extends State<_BottomBar> { if(e.translatedName != null && e.name != e.translatedName) { text += "/${e.translatedName}"; } - return Card( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), - child: Text(text, style: const TextStyle(fontSize: 13),), + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + context.to(() => SearchResultPage(e.name)); + }, + child: Card( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), + child: Text(text, style: const TextStyle(fontSize: 13),), + ), + ), ); }).toList(), ), @@ -601,10 +664,11 @@ class _CommentsPageState extends MultiPageLoadingState<_CommentsPage, Comment> { Widget buildBody(BuildContext context, List data) { return ListView.builder( + padding: EdgeInsets.zero, itemCount: data.length + 2, itemBuilder: (context, index) { if(index == 0) { - return Text("Comments".tl, style: const TextStyle(fontSize: 20)).paddingVertical(8).paddingHorizontal(12); + return Text("Comments".tl, style: const TextStyle(fontSize: 20)).paddingVertical(16).paddingHorizontal(12); } else if(index == data.length + 1) { return const SizedBox(height: 64,); } diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index f93cf46..e9b70a9 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -23,7 +23,7 @@ import "package:window_manager/window_manager.dart"; import "../components/page_route.dart"; import "downloading_page.dart"; -const _kAppBarHeight = 36.0; +double get _appBarHeight => App.isDesktop ? 36.0 : 48.0; class MainPage extends StatefulWidget { const MainPage({super.key}); @@ -143,11 +143,14 @@ class _MainPageState extends State with WindowListener { ), ], ), - paneBodyBuilder: (pane, child) => Navigator( - key: navigatorKey, - onGenerateRoute: (settings) => AppPageRoute( - builder: (context) => const RecommendationPage()), - )), + paneBodyBuilder: (pane, child) => NavigatorPopHandler( + key: const Key("navigator"), + onPop: () => navigatorKey.currentState?.pop(), + child: Navigator( + key: navigatorKey, + onGenerateRoute: (settings) => AppPageRoute( + builder: (context) => const RecommendationPage()), + ))), )); } @@ -176,7 +179,7 @@ class _MainPageState extends State with WindowListener { BuildContext context, GlobalKey navigatorKey) { return NavigationAppBar( automaticallyImplyLeading: false, - height: _kAppBarHeight, + height: _appBarHeight, title: () { if (!App.isDesktop) { return const Align( @@ -198,9 +201,9 @@ class _MainPageState extends State with WindowListener { ); }(), leading: _BackButton(navigatorKey), - actions: WindowButtons( + actions: App.isDesktop ? WindowButtons( key: ValueKey(windowButtonKey), - ), + ) : null, ); } } @@ -298,7 +301,7 @@ class WindowButtons extends StatelessWidget { return SizedBox( width: 138, - height: _kAppBarHeight, + height: _appBarHeight, child: Row( children: [ WindowButton( diff --git a/lib/pages/ranking.dart b/lib/pages/ranking.dart index 9d86567..6c2b7b8 100644 --- a/lib/pages/ranking.dart +++ b/lib/pages/ranking.dart @@ -67,7 +67,7 @@ class _RankingPageState extends State { ) ], ) - ).padding(const EdgeInsets.symmetric(vertical: 8, horizontal: 12)); + ).padding(const EdgeInsets.symmetric(vertical: 8, horizontal: 16)); } } @@ -85,6 +85,7 @@ class _OneRankingPageState extends MultiPageLoadingState<_OneRankingPage, Illust Widget buildContent(BuildContext context, final List data) { return LayoutBuilder(builder: (context, constrains){ return MasonryGridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8), gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 240, ), diff --git a/lib/pages/recommendation_page.dart b/lib/pages/recommendation_page.dart index 4535d5a..1e70158 100644 --- a/lib/pages/recommendation_page.dart +++ b/lib/pages/recommendation_page.dart @@ -35,20 +35,29 @@ class _RecommendationPageState extends State { } Widget buildTab() { - return SegmentedButton( - options: [ - SegmentedButtonOption(0, "Artworks".tl), - SegmentedButtonOption(1, "Users".tl), - ], - onPressed: (key) { - if(key != type) { - setState(() { - type = key; - }); - } - }, - value: type, - ).padding(const EdgeInsets.symmetric(vertical: 8, horizontal: 8)); + return SizedBox( + child: Row( + children: [ + Text("Explore".tl, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),), + const Spacer(), + SegmentedButton( + options: [ + SegmentedButtonOption(0, "Artworks".tl), + SegmentedButtonOption(1, "Users".tl), + ], + onPressed: (key) { + if(key != type) { + setState(() { + type = key; + }); + } + }, + value: type, + ) + ], + ).paddingHorizontal(16).paddingBottom(4), + ); } } @@ -65,6 +74,7 @@ class _RecommendationArtworksPageState extends MultiPageLoadingState<_Recommenda Widget buildContent(BuildContext context, final List data) { return LayoutBuilder(builder: (context, constrains){ return MasonryGridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8), gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 240, ), diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index 05ff0b0..750c47e 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -58,6 +58,7 @@ class _SearchPageState extends State { @override Widget build(BuildContext context) { return ScaffoldPage( + padding: EdgeInsets.zero, content: Column( children: [ buildSearchBar(), @@ -171,6 +172,7 @@ class _TrendingTagsViewState extends LoadingState<_TrendingTagsView, List data) { return MasonryGridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8.0), gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 240, ), diff --git a/lib/pages/user_info_page.dart b/lib/pages/user_info_page.dart index 264fb6d..90b4577 100644 --- a/lib/pages/user_info_page.dart +++ b/lib/pages/user_info_page.dart @@ -77,6 +77,7 @@ class _UserInfoPageState extends LoadingState { image: CachedImageProvider(data!.avatar), width: 64, height: 64, + fit: BoxFit.cover, ), ),), const SizedBox(height: 8),