From ea90364566d01f72b701c4f701142aba993cf05c Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 21 May 2025 00:04:36 +0800 Subject: [PATCH] :iphone: Responsive for desktop --- assets/i18n/en-US.json | 5 +- lib/route.dart | 46 ++-- lib/route.gr.dart | 65 +++-- lib/screens/account/me/event_calendar.dart | 246 +++++++++--------- lib/screens/auth/tabs.dart | 49 ++-- lib/screens/creators/hub.dart | 123 +++++++-- .../{account/me => creators}/publishers.dart | 119 --------- .../me => creators}/publishers.g.dart | 0 lib/screens/explore.dart | 154 +++++++---- lib/screens/posts/compose.dart | 22 +- lib/screens/posts/detail.dart | 83 ++++-- lib/services/responsive.dart | 6 +- lib/services/tour.dart | 2 +- lib/services/tour.g.dart | 2 +- lib/widgets/content/paging_helper_ext.dart | 236 +++++++++++++++++ lib/widgets/post/post_item.dart | 3 + lib/widgets/post/post_list.dart | 3 +- lib/widgets/post/post_quick_reply.dart | 2 +- lib/widgets/post/post_replies.dart | 16 +- lib/widgets/post/publishers_modal.dart | 2 +- lib/widgets/response.dart | 14 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 23 files changed, 761 insertions(+), 440 deletions(-) rename lib/screens/{account/me => creators}/publishers.dart (69%) rename lib/screens/{account/me => creators}/publishers.g.dart (100%) create mode 100644 lib/widgets/content/paging_helper_ext.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index f278769..b392cec 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -42,7 +42,7 @@ "update": "Update", "edit": "Edit", "delete": "Delete", - "deletePublisher": "Delete Publisher {}", + "deletePublisher": "Delete Publisher", "deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.", "somethingWentWrong": "Something went wrong...", "deletePost": "Delete Post", @@ -260,5 +260,6 @@ "walletCreate": "Create a Wallet", "settingsServerUrl": "Server URL", "settingsApplied": "The settings has been applied.", - "notifications": "Notifications" + "notifications": "Notifications", + "posts": "Posts" } diff --git a/lib/route.dart b/lib/route.dart index 5430d95..0426ec4 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -37,6 +37,30 @@ class AppRouter extends RootStackRouter { AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'), ], ), + AutoRoute( + page: CreatorHubShellRoute.page, + path: '/creators', + children: [ + AutoRoute(page: CreatorHubRoute.page, path: ''), + AutoRoute(page: StickersRoute.page, path: ':name/stickers'), + AutoRoute(page: NewStickerPacksRoute.page, path: ':name/stickers/new'), + AutoRoute( + page: EditStickerPacksRoute.page, + path: ':name/stickers/:packId/edit', + ), + AutoRoute( + page: StickerPackDetailRoute.page, + path: ':name/stickers/:packId', + ), + AutoRoute(page: NewStickersRoute.page, path: ':name/stickers/new'), + AutoRoute( + page: EditStickersRoute.page, + path: ':name/stickers/:id/edit', + ), + AutoRoute(page: NewPublisherRoute.page, path: 'new'), + AutoRoute(page: EditPublisherRoute.page, path: ':name/edit'), + ], + ), AutoRoute(page: LoginRoute.page, path: '/auth/login'), AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'), AutoRoute(page: SettingsRoute.page, path: '/settings'), @@ -46,27 +70,5 @@ class AppRouter extends RootStackRouter { AutoRoute(page: NewRealmRoute.page, path: '/realms/new'), AutoRoute(page: RealmDetailRoute.page, path: '/realms/:slug'), AutoRoute(page: EditRealmRoute.page, path: '/realms/:slug/edit'), - AutoRoute(page: CreatorHubRoute.page, path: '/creators'), - AutoRoute(page: StickersRoute.page, path: '/creators/:name/stickers'), - AutoRoute( - page: NewStickerPacksRoute.page, - path: '/creators/:name/stickers/new', - ), - AutoRoute( - page: EditStickerPacksRoute.page, - path: '/creators/:name/stickers/:packId/edit', - ), - AutoRoute( - page: StickerPackDetailRoute.page, - path: '/creators/:name/stickers/:packId', - ), - AutoRoute( - page: NewStickersRoute.page, - path: '/creators/:name/stickers/new', - ), - AutoRoute( - page: EditStickersRoute.page, - path: '/creators/:name/stickers/:id/edit', - ), ]; } diff --git a/lib/route.gr.dart b/lib/route.gr.dart index 8015773..a50848b 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -16,7 +16,6 @@ import 'package:island/models/post.dart' as _i29; import 'package:island/route.dart' as _i30; import 'package:island/screens/account.dart' as _i2; import 'package:island/screens/account/me/event_calendar.dart' as _i15; -import 'package:island/screens/account/me/publishers.dart' as _i9; import 'package:island/screens/account/me/settings.dart' as _i3; import 'package:island/screens/account/me/update.dart' as _i24; import 'package:island/screens/account/profile.dart' as _i1; @@ -28,6 +27,7 @@ import 'package:island/screens/chat/chat.dart' as _i5; import 'package:island/screens/chat/room.dart' as _i6; import 'package:island/screens/chat/room_detail.dart' as _i4; import 'package:island/screens/creators/hub.dart' as _i8; +import 'package:island/screens/creators/publishers.dart' as _i9; import 'package:island/screens/creators/stickers/pack_detail.dart' as _i12; import 'package:island/screens/creators/stickers/stickers.dart' as _i11; import 'package:island/screens/explore.dart' as _i13; @@ -308,16 +308,55 @@ class CreateAccountRoute extends _i26.PageRouteInfo { /// generated route for /// [_i8.CreatorHubScreen] -class CreatorHubRoute extends _i26.PageRouteInfo { - const CreatorHubRoute({List<_i26.PageRouteInfo>? children}) - : super(CreatorHubRoute.name, initialChildren: children); +class CreatorHubRoute extends _i26.PageRouteInfo { + CreatorHubRoute({ + _i27.Key? key, + bool isAside = false, + List<_i26.PageRouteInfo>? children, + }) : super( + CreatorHubRoute.name, + args: CreatorHubRouteArgs(key: key, isAside: isAside), + initialChildren: children, + ); static const String name = 'CreatorHubRoute'; static _i26.PageInfo page = _i26.PageInfo( name, builder: (data) { - return const _i8.CreatorHubScreen(); + final args = data.argsAs( + orElse: () => const CreatorHubRouteArgs(), + ); + return _i8.CreatorHubScreen(key: args.key, isAside: args.isAside); + }, + ); +} + +class CreatorHubRouteArgs { + const CreatorHubRouteArgs({this.key, this.isAside = false}); + + final _i27.Key? key; + + final bool isAside; + + @override + String toString() { + return 'CreatorHubRouteArgs{key: $key, isAside: $isAside}'; + } +} + +/// generated route for +/// [_i8.CreatorHubShellScreen] +class CreatorHubShellRoute extends _i26.PageRouteInfo { + const CreatorHubShellRoute({List<_i26.PageRouteInfo>? children}) + : super(CreatorHubShellRoute.name, initialChildren: children); + + static const String name = 'CreatorHubShellRoute'; + + static _i26.PageInfo page = _i26.PageInfo( + name, + builder: (data) { + return const _i8.CreatorHubShellScreen(); }, ); } @@ -591,22 +630,6 @@ class LoginRoute extends _i26.PageRouteInfo { ); } -/// generated route for -/// [_i9.ManagedPublisherScreen] -class ManagedPublisherRoute extends _i26.PageRouteInfo { - const ManagedPublisherRoute({List<_i26.PageRouteInfo>? children}) - : super(ManagedPublisherRoute.name, initialChildren: children); - - static const String name = 'ManagedPublisherRoute'; - - static _i26.PageInfo page = _i26.PageInfo( - name, - builder: (data) { - return const _i9.ManagedPublisherScreen(); - }, - ); -} - /// generated route for /// [_i15.MyselfEventCalendarScreen] class MyselfEventCalendarRoute extends _i26.PageRouteInfo { diff --git a/lib/screens/account/me/event_calendar.dart b/lib/screens/account/me/event_calendar.dart index 93d4150..f06fe39 100644 --- a/lib/screens/account/me/event_calendar.dart +++ b/lib/screens/account/me/event_calendar.dart @@ -64,133 +64,135 @@ class MyselfEventCalendarScreen extends HookConsumerWidget { leading: const PageBackButton(), title: Text('eventCalander').tr(), ), - body: Column( - children: [ - TableCalendar( - locale: EasyLocalization.of(context)!.locale.toString(), - firstDay: DateTime.now().add(Duration(days: -3650)), - lastDay: DateTime.now().add(Duration(days: 3650)), - focusedDay: DateTime.utc( - selectedYear.value, - selectedMonth.value, - DateTime.now().day, - ), - calendarFormat: CalendarFormat.month, - selectedDayPredicate: (day) { - return isSameDay(selectedDay.value, day); - }, - onDaySelected: (value, _) { - selectedDay.value = value; - }, - onPageChanged: (focusedDay) { - selectedMonth.value = focusedDay.month; - selectedYear.value = focusedDay.year; - }, - eventLoader: (day) { - return events.value - ?.where((e) => isSameDay(e.date, day)) - .expand((e) => [...e.statuses, e.checkInResult]) - .where((e) => e != null) - .toList() ?? - []; - }, - calendarBuilders: CalendarBuilders( - dowBuilder: (context, day) { - final text = DateFormat.EEEEE().format(day); - return Center(child: Text(text)); + body: SingleChildScrollView( + child: Column( + children: [ + TableCalendar( + locale: EasyLocalization.of(context)!.locale.toString(), + firstDay: DateTime.now().add(Duration(days: -3650)), + lastDay: DateTime.now().add(Duration(days: 3650)), + focusedDay: DateTime.utc( + selectedYear.value, + selectedMonth.value, + DateTime.now().day, + ), + calendarFormat: CalendarFormat.month, + selectedDayPredicate: (day) { + return isSameDay(selectedDay.value, day); }, - markerBuilder: (context, day, events) { - var checkInResult = - events.whereType().firstOrNull; - if (checkInResult != null) { - return Positioned( - top: 32, - child: Text( - ['大凶', '凶', '中平', '吉', '大吉'][checkInResult.level], - style: TextStyle( - fontSize: 9, - color: - isSameDay(selectedDay.value, day) - ? Theme.of( - context, - ).colorScheme.onPrimaryContainer - : isSameDay(DateTime.now(), day) - ? Theme.of( - context, - ).colorScheme.onSecondaryContainer - : Theme.of(context).colorScheme.onSurface, + onDaySelected: (value, _) { + selectedDay.value = value; + }, + onPageChanged: (focusedDay) { + selectedMonth.value = focusedDay.month; + selectedYear.value = focusedDay.year; + }, + eventLoader: (day) { + return events.value + ?.where((e) => isSameDay(e.date, day)) + .expand((e) => [...e.statuses, e.checkInResult]) + .where((e) => e != null) + .toList() ?? + []; + }, + calendarBuilders: CalendarBuilders( + dowBuilder: (context, day) { + final text = DateFormat.EEEEE().format(day); + return Center(child: Text(text)); + }, + markerBuilder: (context, day, events) { + var checkInResult = + events.whereType().firstOrNull; + if (checkInResult != null) { + return Positioned( + top: 32, + child: Text( + ['大凶', '凶', '中平', '吉', '大吉'][checkInResult.level], + style: TextStyle( + fontSize: 9, + color: + isSameDay(selectedDay.value, day) + ? Theme.of( + context, + ).colorScheme.onPrimaryContainer + : isSameDay(DateTime.now(), day) + ? Theme.of( + context, + ).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.onSurface, + ), ), - ), - ); - } - return null; - }, + ); + } + return null; + }, + ), ), - ), - const Divider(height: 1).padding(top: 8), - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: Builder( - builder: (context) { - final event = - events.value - ?.where((e) => isSameDay(e.date, selectedDay.value)) - .firstOrNull; - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text(DateFormat.EEEE().format(selectedDay.value)) - .fontSize(16) - .bold() - .textColor( - Theme.of(context).colorScheme.onSecondaryContainer, - ), - Text(DateFormat.yMd().format(selectedDay.value)) - .fontSize(12) - .textColor( - Theme.of(context).colorScheme.onSecondaryContainer, - ), - const Gap(16), - if (event?.checkInResult != null) - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'checkInResultLevel${event!.checkInResult!.level}', - ).tr().fontSize(16).bold(), - for (final tip in event.checkInResult!.tips) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, - children: [ - Icon( - Symbols.circle, - size: 12, - fill: 1, - ).padding(top: 4, right: 4), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text(tip.title).bold(), - Text(tip.content), - ], + const Divider(height: 1).padding(top: 8), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Builder( + builder: (context) { + final event = + events.value + ?.where((e) => isSameDay(e.date, selectedDay.value)) + .firstOrNull; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(DateFormat.EEEE().format(selectedDay.value)) + .fontSize(16) + .bold() + .textColor( + Theme.of(context).colorScheme.onSecondaryContainer, + ), + Text(DateFormat.yMd().format(selectedDay.value)) + .fontSize(12) + .textColor( + Theme.of(context).colorScheme.onSecondaryContainer, + ), + const Gap(16), + if (event?.checkInResult != null) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'checkInResultLevel${event!.checkInResult!.level}', + ).tr().fontSize(16).bold(), + for (final tip in event.checkInResult!.tips) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Icon( + Symbols.circle, + size: 12, + fill: 1, + ).padding(top: 4, right: 4), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text(tip.title).bold(), + Text(tip.content), + ], + ), ), - ), - ], - ).padding(top: 8), - ], - ), - if (event?.checkInResult == null && - (event?.statuses.isEmpty ?? true)) - Text('eventCalanderEmpty').tr(), - ], - ).padding(vertical: 24, horizontal: 24); - }, + ], + ).padding(top: 8), + ], + ), + if (event?.checkInResult == null && + (event?.statuses.isEmpty ?? true)) + Text('eventCalanderEmpty').tr(), + ], + ).padding(vertical: 24, horizontal: 24); + }, + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/screens/auth/tabs.dart b/lib/screens/auth/tabs.dart index a3b7cf7..fd24645 100644 --- a/lib/screens/auth/tabs.dart +++ b/lib/screens/auth/tabs.dart @@ -106,38 +106,25 @@ class TabsNavigationWidget extends HookConsumerWidget { Column( children: [ Gap(MediaQuery.of(context).padding.top + 8), - if (useExpandableLayout) - Expanded( - child: NavigationDrawer( - backgroundColor: Colors.transparent, - children: [ - for (final destination in destinations) - NavigationDrawerDestination( - label: Text(destination.label), - icon: destination.icon, - ), - ], - ), - ) - else - Expanded( - child: NavigationRail( - selectedIndex: activeIndex, - onDestinationSelected: (index) { - router.replace(routes[index]); - }, - labelType: NavigationRailLabelType.all, - destinations: - destinations - .map( - (d) => NavigationRailDestination( - icon: d.icon, - label: Text(d.label), - ), - ) - .toList(), - ), + Expanded( + child: NavigationRail( + extended: useExpandableLayout, + selectedIndex: activeIndex, + onDestinationSelected: (index) { + router.replace(routes[index]); + }, + // labelType: NavigationRailLabelType.all, + destinations: + destinations + .map( + (d) => NavigationRailDestination( + icon: d.icon, + label: Text(d.label), + ), + ) + .toList(), ), + ), Gap(MediaQuery.of(context).padding.bottom + 8), ], ), diff --git a/lib/screens/creators/hub.dart b/lib/screens/creators/hub.dart index defe4ab..c026464 100644 --- a/lib/screens/creators/hub.dart +++ b/lib/screens/creators/hub.dart @@ -8,7 +8,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; import 'package:island/route.gr.dart'; -import 'package:island/screens/account/me/publishers.dart'; +import 'package:island/screens/creators/publishers.dart'; +import 'package:island/services/responsive.dart'; +import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -25,17 +27,69 @@ Future publisherStats(Ref ref, String? uname) async { return SnPublisherStats.fromJson(resp.data); } +@RoutePage() +class CreatorHubShellScreen extends StatelessWidget { + const CreatorHubShellScreen({super.key}); + + @override + Widget build(BuildContext context) { + final isWide = isWideScreen(context); + if (isWide) { + return Row( + children: [ + SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)), + const VerticalDivider(width: 1), + Expanded(child: AutoRouter()), + ], + ); + } + return AutoRouter(); + } +} + @RoutePage() class CreatorHubScreen extends HookConsumerWidget { - const CreatorHubScreen({super.key}); + final bool isAside; + const CreatorHubScreen({super.key, this.isAside = false}); @override Widget build(BuildContext context, WidgetRef ref) { + final isWide = isWideScreen(context); + if (isWide && !isAside) { + return Container(color: Theme.of(context).colorScheme.surface); + } + final publishers = ref.watch(publishersManagedProvider); final currentPublisher = useState( publishers.value?.firstOrNull, ); + void updatePublisher() { + context.router + .push(EditPublisherRoute(name: currentPublisher.value!.name)) + .then((value) async { + if (value == null) return; + final data = await ref.refresh(publishersManagedProvider.future); + currentPublisher.value = + data + .where((e) => e.id == currentPublisher.value!.id) + .firstOrNull; + }); + } + + void deletePublisher() { + showConfirmAlert('deletePublisherHint'.tr(), 'deletePublisher'.tr()).then( + (confirm) { + if (confirm) { + final client = ref.watch(apiClientProvider); + client.delete('/publishers/${currentPublisher.value!.name}'); + ref.invalidate(publishersManagedProvider); + currentPublisher.value = null; + } + }, + ); + } + final List> publishersMenu = publishers.when( data: (data) => @@ -184,23 +238,56 @@ class CreatorHubScreen extends HookConsumerWidget { _PublisherStatsWidget( stats: stats, ).padding(vertical: 12, horizontal: 12), - if (currentPublisher.value != null) - ListTile( - minTileHeight: 48, - title: Text('stickers').tr(), - trailing: Icon(Symbols.chevron_right), - leading: const Icon(Symbols.sticky_note), - contentPadding: EdgeInsets.symmetric( - horizontal: 24, - ), - onTap: () { - context.router.push( - StickersRoute( - pubName: currentPublisher.value!.name, - ), - ); - }, + ListTile( + minTileHeight: 48, + title: Text('stickers').tr(), + trailing: Icon(Symbols.chevron_right), + leading: const Icon(Symbols.ar_stickers), + contentPadding: EdgeInsets.symmetric( + horizontal: 24, ), + onTap: () { + context.router.push( + StickersRoute( + pubName: currentPublisher.value!.name, + ), + ); + }, + ), + ListTile( + minTileHeight: 48, + title: Text('posts').tr(), + trailing: Icon(Symbols.chevron_right), + leading: const Icon(Symbols.sticky_note_2), + contentPadding: EdgeInsets.symmetric( + horizontal: 24, + ), + ), + Divider(height: 1).padding(vertical: 8), + ListTile( + minTileHeight: 48, + title: Text('editPublisher').tr(), + trailing: Icon(Symbols.chevron_right), + leading: const Icon(Symbols.edit), + contentPadding: EdgeInsets.symmetric( + horizontal: 24, + ), + onTap: () { + updatePublisher(); + }, + ), + ListTile( + minTileHeight: 48, + title: Text('deletePublisher').tr(), + trailing: Icon(Symbols.chevron_right), + leading: const Icon(Symbols.delete), + contentPadding: EdgeInsets.symmetric( + horizontal: 24, + ), + onTap: () { + deletePublisher(); + }, + ), ], ), ), diff --git a/lib/screens/account/me/publishers.dart b/lib/screens/creators/publishers.dart similarity index 69% rename from lib/screens/account/me/publishers.dart rename to lib/screens/creators/publishers.dart index 40d2ac9..47a8a4f 100644 --- a/lib/screens/account/me/publishers.dart +++ b/lib/screens/creators/publishers.dart @@ -12,7 +12,6 @@ import 'package:island/models/realm.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; -import 'package:island/route.gr.dart'; import 'package:island/screens/realm/realms.dart'; import 'package:island/services/file.dart'; import 'package:island/widgets/alert.dart'; @@ -35,124 +34,6 @@ Future> publishersManaged(Ref ref) async { .toList(); } -@RoutePage() -class ManagedPublisherScreen extends HookConsumerWidget { - const ManagedPublisherScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final publishers = ref.watch(publishersManagedProvider); - - return AppScaffold( - appBar: AppBar( - title: Text('publishers').tr(), - leading: const PageBackButton(), - ), - body: RefreshIndicator( - child: publishers.when( - data: - (value) => Column( - children: [ - ListTile( - leading: const Icon(Symbols.add), - title: Text('createPublisher').tr(), - subtitle: Text('createPublisherHint').tr(), - trailing: const Icon(Symbols.chevron_right), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - onTap: () { - context.router.push(NewPublisherRoute()).then((value) { - if (value != null) { - ref.invalidate(publishersManagedProvider); - } - }); - }, - ), - const Divider(height: 1), - Expanded( - child: ListView.builder( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom, - ), - itemCount: value.length, - itemBuilder: (context, item) { - return ListTile( - leading: ProfilePictureWidget( - fileId: value[item].pictureId, - ), - title: Text(value[item].nick), - subtitle: Text('@${value[item].name}'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - padding: EdgeInsets.zero, - visualDensity: VisualDensity.compact, - icon: Icon(Symbols.delete), - onPressed: () { - showConfirmAlert( - 'deletePublisherHint'.tr(), - 'deletePublisher'.tr( - args: ['@${value[item].name}'], - ), - ).then((confirm) { - if (confirm) { - final client = ref.watch( - apiClientProvider, - ); - client.delete( - '/publishers/${value[item].name}', - ); - ref.invalidate(publishersManagedProvider); - } - }); - }, - ), - IconButton( - padding: EdgeInsets.zero, - visualDensity: VisualDensity.compact, - icon: Icon(Symbols.edit), - onPressed: () { - context.router - .push( - EditPublisherRoute( - name: value[item].name, - ), - ) - .then((value) { - if (value != null) { - ref.invalidate( - publishersManagedProvider, - ); - } - }); - }, - ), - ], - ), - contentPadding: EdgeInsets.only(left: 16, right: 14), - ); - }, - ), - ), - ], - ), - loading: () => const Center(child: CircularProgressIndicator()), - error: - (e, _) => GestureDetector( - child: Center( - child: Text('Error: $e', textAlign: TextAlign.center), - ), - onTap: () { - ref.invalidate(publishersManagedProvider); - }, - ), - ), - onRefresh: () => ref.refresh(publishersManagedProvider.future), - ), - ); - } -} - @riverpod Future publisher(Ref ref, String? identifier) async { if (identifier == null) return null; diff --git a/lib/screens/account/me/publishers.g.dart b/lib/screens/creators/publishers.g.dart similarity index 100% rename from lib/screens/account/me/publishers.g.dart rename to lib/screens/creators/publishers.g.dart diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index bc15bfa..233d906 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -1,11 +1,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/activity.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/route.gr.dart'; +import 'package:island/services/responsive.dart'; import 'package:island/widgets/account/status.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/models/post.dart'; @@ -25,9 +26,10 @@ class ExploreScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final user = ref.watch(userInfoProvider); final activitiesNotifier = ref.watch(activityListNotifierProvider.notifier); + final isWide = isWideScreen(context); + return TourTriggerWidget( child: AppScaffold( appBar: AppBar(title: const Text('explore').tr()), @@ -50,53 +52,40 @@ class ExploreScreen extends ConsumerWidget { futureRefreshable: activityListNotifierProvider.future, notifierRefreshable: activityListNotifierProvider.notifier, contentBuilder: - (data, widgetCount, endItemView) => CustomScrollView( - slivers: [ - if (user.hasValue) - SliverToBoxAdapter(child: CheckInWidget()), - SliverList.builder( - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - - final item = data.items[index]; - if (item.data == null) return const SizedBox.shrink(); - Widget itemWidget; - - switch (item.type) { - case 'posts.new': - itemWidget = PostItem( - item: SnPost.fromJson(item.data), - onRefresh: (_) { - activitiesNotifier.forceRefresh(); - }, - onUpdate: (post) { - activitiesNotifier.updateOne( - index, - item.copyWith(data: post.toJson()), - ); - }, - ); - break; - case 'accounts.check-in': - itemWidget = CheckInActivityWidget(item: item); - break; - case 'accounts.status': - itemWidget = StatusActivityWidget(item: item); - break; - default: - itemWidget = const Placeholder(); - } - - return Column( - children: [itemWidget, const Divider(height: 1)], - ); - }, + (data, widgetCount, endItemView) => Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: kWideScreenWidth - 160, ), - SliverGap(MediaQuery.of(context).padding.bottom + 16), - ], + child: + isWide + ? Card( + elevation: 8, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + color: Theme.of(context) + .colorScheme + .surfaceContainerLow + .withOpacity(0.8), + child: _ActivityListView( + data: data, + widgetCount: widgetCount, + endItemView: endItemView, + activitiesNotifier: activitiesNotifier, + ), + ) + : _ActivityListView( + data: data, + widgetCount: widgetCount, + endItemView: endItemView, + activitiesNotifier: activitiesNotifier, + ), + ), ), ), ), @@ -105,6 +94,75 @@ class ExploreScreen extends ConsumerWidget { } } +class _ActivityListView extends HookConsumerWidget { + final CursorPagingData data; + final int widgetCount; + final Widget endItemView; + final ActivityListNotifier activitiesNotifier; + + const _ActivityListView({ + required this.data, + required this.widgetCount, + required this.endItemView, + required this.activitiesNotifier, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(userInfoProvider); + + return CustomScrollView( + slivers: [ + if (user.hasValue) SliverToBoxAdapter(child: CheckInWidget()), + SliverList.builder( + itemCount: widgetCount, + itemBuilder: (context, index) { + if (index == widgetCount - 1) { + return endItemView; + } + + final item = data.items[index]; + if (item.data == null) { + return const SizedBox.shrink(); + } + Widget itemWidget; + + switch (item.type) { + case 'posts.new': + itemWidget = PostItem( + backgroundColor: + isWideScreen(context) ? Colors.transparent : null, + item: SnPost.fromJson(item.data), + onRefresh: (_) { + activitiesNotifier.forceRefresh(); + }, + onUpdate: (post) { + activitiesNotifier.updateOne( + index, + item.copyWith(data: post.toJson()), + ); + }, + ); + break; + case 'accounts.check-in': + itemWidget = CheckInActivityWidget(item: item); + break; + case 'accounts.status': + itemWidget = StatusActivityWidget(item: item); + break; + default: + itemWidget = const Placeholder(); + } + + return Column(children: [itemWidget, const Divider(height: 1)]); + }, + ), + SliverGap(MediaQuery.of(context).padding.bottom + 16), + ], + ); + } +} + @riverpod class ActivityListNotifier extends _$ActivityListNotifier with CursorPagingNotifierMixin { diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart index bee9b24..0d76321 100644 --- a/lib/screens/posts/compose.dart +++ b/lib/screens/posts/compose.dart @@ -14,14 +14,13 @@ import 'package:island/models/file.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; -import 'package:island/screens/account/me/publishers.dart'; +import 'package:island/screens/creators/publishers.dart'; import 'package:island/screens/posts/detail.dart'; import 'package:island/services/file.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/post/publishers_modal.dart'; -import 'package:markdown_editor_plus/widgets/markdown_auto_preview.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -291,15 +290,14 @@ class PostComposeScreen extends HookConsumerWidget { (_) => FocusManager.instance.primaryFocus?.unfocus(), ), - const Gap(12), - TapRegion( - child: MarkdownAutoPreview( - controller: contentController, - emojiConvert: true, + const Gap(8), + TextField( + controller: contentController, + style: TextStyle(fontSize: 14), + decoration: InputDecoration( + border: InputBorder.none, hintText: 'postPlaceholder'.tr(), - decoration: InputDecoration( - border: InputBorder.none, - ), + isDense: true, ), onTapOutside: (_) => @@ -343,7 +341,7 @@ class PostComposeScreen extends HookConsumerWidget { ).padding(horizontal: 16), ), Material( - elevation: 2, + elevation: 4, child: Row( children: [ IconButton( @@ -358,7 +356,7 @@ class PostComposeScreen extends HookConsumerWidget { ), ], ).padding( - bottom: MediaQuery.of(context).padding.bottom, + bottom: MediaQuery.of(context).padding.bottom + 16, horizontal: 16, top: 8, ), diff --git a/lib/screens/posts/detail.dart b/lib/screens/posts/detail.dart index cd0dd8f..1058de8 100644 --- a/lib/screens/posts/detail.dart +++ b/lib/screens/posts/detail.dart @@ -4,6 +4,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; +import 'package:island/services/responsive.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_quick_reply.dart'; @@ -29,36 +30,68 @@ class PostDetailScreen extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final post = ref.watch(postProvider(id)); + final isWide = isWideScreen(context); + return AppScaffold( appBar: AppBar(title: const Text('Post')), body: post.when( - data: - (post) => Stack( - fit: StackFit.expand, - children: [ - Column( - children: [ - PostItem(item: post!, isOpenable: false), - const Divider(height: 1), - Expanded(child: PostRepliesList(postId: id)), - Gap(MediaQuery.of(context).padding.bottom), - ], - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Material( - elevation: 2, - child: PostQuickReply(parent: post).padding( - bottom: MediaQuery.of(context).padding.bottom, - top: 16, - horizontal: 16, - ), + data: (post) { + final content = Stack( + fit: StackFit.expand, + children: [ + Column( + children: [ + PostItem( + item: post!, + isOpenable: false, + backgroundColor: isWide ? Colors.transparent : null, + ), + const Divider(height: 1), + Expanded(child: PostRepliesList(postId: id)), + Gap(MediaQuery.of(context).padding.bottom), + ], + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Material( + elevation: 2, + color: Colors.transparent, + child: PostQuickReply(parent: post).padding( + bottom: MediaQuery.of(context).padding.bottom + 16, + top: 16, + horizontal: 16, ), ), - ], - ), + ), + ], + ); + + return isWide + ? Center( + child: Card( + elevation: 8, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + color: Theme.of( + context, + ).colorScheme.surfaceContainerLow.withOpacity(0.8), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: kWideScreenWidth - 160, + ), + child: content, + ), + ), + ) + : content; + }, loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Text('Error: $e'), ), diff --git a/lib/services/responsive.dart b/lib/services/responsive.dart index ab3728b..12fbb2f 100644 --- a/lib/services/responsive.dart +++ b/lib/services/responsive.dart @@ -1,8 +1,8 @@ import 'package:flutter/widgets.dart'; -const kWideScreenWidth = 768; -const kWiderScreenWidth = 1024; -const kWidescreenWidth = 1280; +const kWideScreenWidth = 768.0; +const kWiderScreenWidth = 1024.0; +const kWidescreenWidth = 1280.0; bool isWideScreen(BuildContext context) { return MediaQuery.of(context).size.width > kWideScreenWidth; diff --git a/lib/services/tour.dart b/lib/services/tour.dart index 47f2b25..4f34f2f 100644 --- a/lib/services/tour.dart +++ b/lib/services/tour.dart @@ -53,7 +53,7 @@ class TourStatusNotifier extends _$TourStatusNotifier { } Future showTour(String tourId) async { - if (!isTourShown(tourId) || true) { + if (!isTourShown(tourId)) { final newState = {...state, tourId: true}; await _saveState(newState); return kAllTours.firstWhere((e) => e.id == tourId).widget; diff --git a/lib/services/tour.g.dart b/lib/services/tour.g.dart index e99b714..3fa869d 100644 --- a/lib/services/tour.g.dart +++ b/lib/services/tour.g.dart @@ -7,7 +7,7 @@ part of 'tour.dart'; // ************************************************************************** String _$tourStatusNotifierHash() => - r'040aac2d7cf6d14e539c1b04cf311421ee133ed3'; + r'ee712e1f8010311df8f24838814ab5c451f9e593'; /// See also [TourStatusNotifier]. @ProviderFor(TourStatusNotifier) diff --git a/lib/widgets/content/paging_helper_ext.dart b/lib/widgets/content/paging_helper_ext.dart new file mode 100644 index 0000000..2fb44dd --- /dev/null +++ b/lib/widgets/content/paging_helper_ext.dart @@ -0,0 +1,236 @@ +// ignore_for_file: implementation_imports, invalid_use_of_internal_member + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_paging_utils/src/paging_data.dart'; +import 'package:riverpod_paging_utils/src/paging_helper_view_theme.dart'; +import 'package:riverpod_paging_utils/src/paging_notifier_mixin.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +/// A generic widget for pagination. +/// +/// Main features: +/// 1. Displays the widget created by [contentBuilder] when data is available. +/// 2. Shows a CircularProgressIndicator while loading the first page. +/// 3. Displays an error widget when there is an error on the first page. +/// 4. Shows error messages using a SnackBar. +/// 5. Loads the next page when the last item is displayed. +/// 6. Supports pull-to-refresh functionality. +/// +/// You can customize the appearance of the loading view, error view, and endItemView using [PagingHelperViewTheme]. +final class PagingHelperSliverView, I> + extends ConsumerWidget { + const PagingHelperSliverView({ + required this.provider, + required this.futureRefreshable, + required this.notifierRefreshable, + required this.contentBuilder, + this.showSecondPageError = true, + super.key, + }); + + final ProviderListenable> provider; + final Refreshable> futureRefreshable; + final Refreshable> notifierRefreshable; + + /// Specifies a function that returns a widget to display when data is available. + /// endItemView is a widget to detect when the last displayed item is visible. + /// If endItemView is non-null, it is displayed at the end of the list. + final Widget Function(D data, int widgetCount, Widget endItemView) + contentBuilder; + + final bool showSecondPageError; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context).extension(); + + final loadingBuilder = + theme?.loadingViewBuilder ?? + (context) => SliverFillRemaining( + child: const Center(child: CircularProgressIndicator()), + ); + final errorBuilder = + theme?.errorViewBuilder ?? + (context, e, st, onPressed) => SliverFillRemaining( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: onPressed, + icon: const Icon(Icons.refresh), + ), + Text(e.toString()), + ], + ), + ), + ); + + return ref + .watch(provider) + .whenIgnorableError( + data: ( + data, { + required hasError, + required isLoading, + required error, + }) { + final content = contentBuilder( + data, + // Add 1 to the length to include the endItemView + data.items.length + 1, + switch ((data.hasMore, hasError, isLoading)) { + // Display a widget to detect when the last element is reached + // if there are more pages and no errors + (true, false, _) => _EndVDLoadingItemView( + onScrollEnd: + () async => ref.read(notifierRefreshable).loadNext(), + ), + (true, true, false) when showSecondPageError => + _EndErrorItemView( + error: error, + onRetryButtonPressed: + () async => ref.read(notifierRefreshable).loadNext(), + ), + (true, true, true) => const _EndLoadingItemView(), + _ => const SizedBox.shrink(), + }, + ); + + return content; + }, + // Loading state for the first page + loading: () => loadingBuilder(context), + // Error state for the first page + error: + (e, st) => errorBuilder( + context, + e, + st, + () => ref.read(notifierRefreshable).forceRefresh(), + ), + // Prioritize data for errors on the second page and beyond + skipErrorOnHasValue: true, + ); + } +} + +final class _EndLoadingItemView extends StatelessWidget { + const _EndLoadingItemView(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).extension(); + final childBuilder = + theme?.endLoadingViewBuilder ?? + (context) => const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: CircularProgressIndicator(), + ), + ); + + return childBuilder(context); + } +} + +final class _EndVDLoadingItemView extends StatelessWidget { + const _EndVDLoadingItemView({required this.onScrollEnd}); + final VoidCallback onScrollEnd; + + @override + Widget build(BuildContext context) { + return VisibilityDetector( + key: key ?? const Key('EndItem'), + onVisibilityChanged: (info) { + if (info.visibleFraction > 0.1) { + onScrollEnd(); + } + }, + child: const _EndLoadingItemView(), + ); + } +} + +final class _EndErrorItemView extends StatelessWidget { + const _EndErrorItemView({ + required this.error, + required this.onRetryButtonPressed, + }); + final Object? error; + final VoidCallback onRetryButtonPressed; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).extension(); + final childBuilder = + theme?.endErrorViewBuilder ?? + (context, e, onPressed) => Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + IconButton( + onPressed: onPressed, + icon: const Icon(Icons.refresh), + ), + Text(error.toString()), + ], + ), + ), + ); + + return childBuilder(context, error, onRetryButtonPressed); + } +} + +extension _AsyncValueX on AsyncValue { + /// Extends the [when] method to handle async data states more effectively, + /// especially when maintaining data integrity despite errors. + /// + /// Use `skipErrorOnHasValue` to retain and display existing data + /// even if subsequent fetch attempts result in errors, + /// ideal for maintaining a seamless user experience. + R whenIgnorableError({ + required R Function( + T data, { + required bool hasError, + required bool isLoading, + required Object? error, + }) + data, + required R Function(Object error, StackTrace stackTrace) error, + required R Function() loading, + bool skipLoadingOnReload = false, + bool skipLoadingOnRefresh = true, + bool skipError = false, + bool skipErrorOnHasValue = false, + }) { + if (skipErrorOnHasValue) { + if (hasValue && hasError) { + return data( + requireValue, + hasError: true, + isLoading: isLoading, + error: this.error, + ); + } + } + + return when( + skipLoadingOnReload: skipLoadingOnReload, + skipLoadingOnRefresh: skipLoadingOnRefresh, + skipError: skipError, + data: + (d) => data( + d, + hasError: hasError, + isLoading: isLoading, + error: this.error, + ), + error: error, + loading: loading, + ); + } +} diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 4db2aed..244c7eb 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -17,6 +17,7 @@ import 'package:styled_widget/styled_widget.dart'; import 'package:super_context_menu/super_context_menu.dart'; class PostItem extends HookConsumerWidget { + final Color? backgroundColor; final SnPost item; final EdgeInsets? padding; final bool isOpenable; @@ -25,6 +26,7 @@ class PostItem extends HookConsumerWidget { const PostItem({ super.key, required this.item, + this.backgroundColor, this.padding, this.isOpenable = true, this.onRefresh, @@ -96,6 +98,7 @@ class PostItem extends HookConsumerWidget { ); }, child: Material( + color: backgroundColor, child: Padding( padding: renderingPadding, child: Column( diff --git a/lib/widgets/post/post_list.dart b/lib/widgets/post/post_list.dart index f8f6a35..9ad4897 100644 --- a/lib/widgets/post/post_list.dart +++ b/lib/widgets/post/post_list.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; +import 'package:island/widgets/content/paging_helper_ext.dart'; import 'package:island/widgets/post/post_item.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; @@ -53,7 +54,7 @@ class SliverPostList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return PagingHelperView( + return PagingHelperSliverView( provider: postListNotifierProvider, futureRefreshable: postListNotifierProvider.future, notifierRefreshable: postListNotifierProvider.notifier, diff --git a/lib/widgets/post/post_quick_reply.dart b/lib/widgets/post/post_quick_reply.dart index e05c82c..7a41988 100644 --- a/lib/widgets/post/post_quick_reply.dart +++ b/lib/widgets/post/post_quick_reply.dart @@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; -import 'package:island/screens/account/me/publishers.dart'; +import 'package:island/screens/creators/publishers.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/post/publishers_modal.dart'; diff --git a/lib/widgets/post/post_replies.dart b/lib/widgets/post/post_replies.dart index ad7ef0b..f0b4c36 100644 --- a/lib/widgets/post/post_replies.dart +++ b/lib/widgets/post/post_replies.dart @@ -3,7 +3,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; +import 'package:island/services/responsive.dart'; import 'package:island/widgets/post/post_item.dart'; +import 'package:island/widgets/response.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; @@ -14,6 +16,7 @@ class PostRepliesList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final postAsync = ref.watch(postRepliesProvider(postId)); + final isWide = isWideScreen(context); return RefreshIndicator( onRefresh: @@ -37,7 +40,10 @@ class PostRepliesList extends HookConsumerWidget { onFetchData: controller.fetchMore, itemBuilder: (context, index) { final post = controller.posts[index]; - return PostItem(item: post); + return PostItem( + item: post, + backgroundColor: isWide ? Colors.transparent : null, + ); }, separatorBuilder: (_, __) => const Divider(height: 1), emptyBuilder: (context) { @@ -55,11 +61,9 @@ class PostRepliesList extends HookConsumerWidget { ), loading: () => const Center(child: CircularProgressIndicator()), error: - (e, _) => GestureDetector( - child: Center( - child: Text('Error: $e', textAlign: TextAlign.center), - ), - onTap: () { + (e, _) => ResponseErrorWidget( + error: e, + onRetry: () { ref.invalidate(postRepliesProvider(postId)); }, ), diff --git a/lib/widgets/post/publishers_modal.dart b/lib/widgets/post/publishers_modal.dart index 692553e..85e029e 100644 --- a/lib/widgets/post/publishers_modal.dart +++ b/lib/widgets/post/publishers_modal.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/route.gr.dart'; -import 'package:island/screens/account/me/publishers.dart'; +import 'package:island/screens/creators/publishers.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:styled_widget/styled_widget.dart'; diff --git a/lib/widgets/response.dart b/lib/widgets/response.dart index 36263aa..9790e2b 100644 --- a/lib/widgets/response.dart +++ b/lib/widgets/response.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:styled_widget/styled_widget.dart'; class ResponseErrorWidget extends StatelessWidget { final dynamic error; @@ -19,11 +20,14 @@ class ResponseErrorWidget extends StatelessWidget { children: [ const Icon(Symbols.error_outline, size: 48), const Gap(4), - Text( - error.toString(), - textAlign: TextAlign.center, - style: const TextStyle(color: Color(0xFF757575)), - ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: Text( + error.toString(), + textAlign: TextAlign.center, + style: const TextStyle(color: Color(0xFF757575)), + ), + ).center(), const Gap(8), TextButton(onPressed: onRetry, child: const Text('retry').tr()), ], diff --git a/pubspec.lock b/pubspec.lock index 546e154..55e4d34 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1972,7 +1972,7 @@ packages: source: hosted version: "0.9.0" visibility_detector: - dependency: transitive + dependency: "direct main" description: name: visibility_detector sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 diff --git a/pubspec.yaml b/pubspec.yaml index 57549c7..35a218b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -96,6 +96,7 @@ dependencies: crypto: ^3.0.6 avatar_stack: ^3.0.0 markdown_widget: ^2.3.2+8 + visibility_detector: ^0.4.0+2 dev_dependencies: flutter_test: