📱 Responsive for desktop

This commit is contained in:
LittleSheep 2025-05-21 00:04:36 +08:00
parent 1f2a5c107d
commit ea90364566
23 changed files with 761 additions and 440 deletions

View File

@ -42,7 +42,7 @@
"update": "Update", "update": "Update",
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "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.", "deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.",
"somethingWentWrong": "Something went wrong...", "somethingWentWrong": "Something went wrong...",
"deletePost": "Delete Post", "deletePost": "Delete Post",
@ -260,5 +260,6 @@
"walletCreate": "Create a Wallet", "walletCreate": "Create a Wallet",
"settingsServerUrl": "Server URL", "settingsServerUrl": "Server URL",
"settingsApplied": "The settings has been applied.", "settingsApplied": "The settings has been applied.",
"notifications": "Notifications" "notifications": "Notifications",
"posts": "Posts"
} }

View File

@ -37,6 +37,30 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'), 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: LoginRoute.page, path: '/auth/login'),
AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'), AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'),
AutoRoute(page: SettingsRoute.page, path: '/settings'), AutoRoute(page: SettingsRoute.page, path: '/settings'),
@ -46,27 +70,5 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: NewRealmRoute.page, path: '/realms/new'), AutoRoute(page: NewRealmRoute.page, path: '/realms/new'),
AutoRoute(page: RealmDetailRoute.page, path: '/realms/:slug'), AutoRoute(page: RealmDetailRoute.page, path: '/realms/:slug'),
AutoRoute(page: EditRealmRoute.page, path: '/realms/:slug/edit'), 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',
),
]; ];
} }

View File

@ -16,7 +16,6 @@ import 'package:island/models/post.dart' as _i29;
import 'package:island/route.dart' as _i30; import 'package:island/route.dart' as _i30;
import 'package:island/screens/account.dart' as _i2; 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/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/settings.dart' as _i3;
import 'package:island/screens/account/me/update.dart' as _i24; import 'package:island/screens/account/me/update.dart' as _i24;
import 'package:island/screens/account/profile.dart' as _i1; 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.dart' as _i6;
import 'package:island/screens/chat/room_detail.dart' as _i4; import 'package:island/screens/chat/room_detail.dart' as _i4;
import 'package:island/screens/creators/hub.dart' as _i8; 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/pack_detail.dart' as _i12;
import 'package:island/screens/creators/stickers/stickers.dart' as _i11; import 'package:island/screens/creators/stickers/stickers.dart' as _i11;
import 'package:island/screens/explore.dart' as _i13; import 'package:island/screens/explore.dart' as _i13;
@ -308,16 +308,55 @@ class CreateAccountRoute extends _i26.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i8.CreatorHubScreen] /// [_i8.CreatorHubScreen]
class CreatorHubRoute extends _i26.PageRouteInfo<void> { class CreatorHubRoute extends _i26.PageRouteInfo<CreatorHubRouteArgs> {
const CreatorHubRoute({List<_i26.PageRouteInfo>? children}) CreatorHubRoute({
: super(CreatorHubRoute.name, initialChildren: children); _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 const String name = 'CreatorHubRoute';
static _i26.PageInfo page = _i26.PageInfo( static _i26.PageInfo page = _i26.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i8.CreatorHubScreen(); final args = data.argsAs<CreatorHubRouteArgs>(
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<void> {
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<void> {
); );
} }
/// generated route for
/// [_i9.ManagedPublisherScreen]
class ManagedPublisherRoute extends _i26.PageRouteInfo<void> {
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 /// generated route for
/// [_i15.MyselfEventCalendarScreen] /// [_i15.MyselfEventCalendarScreen]
class MyselfEventCalendarRoute extends _i26.PageRouteInfo<void> { class MyselfEventCalendarRoute extends _i26.PageRouteInfo<void> {

View File

@ -64,133 +64,135 @@ class MyselfEventCalendarScreen extends HookConsumerWidget {
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('eventCalander').tr(), title: Text('eventCalander').tr(),
), ),
body: Column( body: SingleChildScrollView(
children: [ child: Column(
TableCalendar( children: [
locale: EasyLocalization.of(context)!.locale.toString(), TableCalendar(
firstDay: DateTime.now().add(Duration(days: -3650)), locale: EasyLocalization.of(context)!.locale.toString(),
lastDay: DateTime.now().add(Duration(days: 3650)), firstDay: DateTime.now().add(Duration(days: -3650)),
focusedDay: DateTime.utc( lastDay: DateTime.now().add(Duration(days: 3650)),
selectedYear.value, focusedDay: DateTime.utc(
selectedMonth.value, selectedYear.value,
DateTime.now().day, selectedMonth.value,
), DateTime.now().day,
calendarFormat: CalendarFormat.month, ),
selectedDayPredicate: (day) { calendarFormat: CalendarFormat.month,
return isSameDay(selectedDay.value, day); 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));
}, },
markerBuilder: (context, day, events) { onDaySelected: (value, _) {
var checkInResult = selectedDay.value = value;
events.whereType<SnCheckInResult>().firstOrNull; },
if (checkInResult != null) { onPageChanged: (focusedDay) {
return Positioned( selectedMonth.value = focusedDay.month;
top: 32, selectedYear.value = focusedDay.year;
child: Text( },
['大凶', '', '中平', '', '大吉'][checkInResult.level], eventLoader: (day) {
style: TextStyle( return events.value
fontSize: 9, ?.where((e) => isSameDay(e.date, day))
color: .expand((e) => [...e.statuses, e.checkInResult])
isSameDay(selectedDay.value, day) .where((e) => e != null)
? Theme.of( .toList() ??
context, [];
).colorScheme.onPrimaryContainer },
: isSameDay(DateTime.now(), day) calendarBuilders: CalendarBuilders(
? Theme.of( dowBuilder: (context, day) {
context, final text = DateFormat.EEEEE().format(day);
).colorScheme.onSecondaryContainer return Center(child: Text(text));
: Theme.of(context).colorScheme.onSurface, },
markerBuilder: (context, day, events) {
var checkInResult =
events.whereType<SnCheckInResult>().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),
const Divider(height: 1).padding(top: 8), AnimatedSwitcher(
AnimatedSwitcher( duration: const Duration(milliseconds: 300),
duration: const Duration(milliseconds: 300), child: Builder(
child: Builder( builder: (context) {
builder: (context) { final event =
final event = events.value
events.value ?.where((e) => isSameDay(e.date, selectedDay.value))
?.where((e) => isSameDay(e.date, selectedDay.value)) .firstOrNull;
.firstOrNull; return Column(
return Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ Text(DateFormat.EEEE().format(selectedDay.value))
Text(DateFormat.EEEE().format(selectedDay.value)) .fontSize(16)
.fontSize(16) .bold()
.bold() .textColor(
.textColor( Theme.of(context).colorScheme.onSecondaryContainer,
Theme.of(context).colorScheme.onSecondaryContainer, ),
), Text(DateFormat.yMd().format(selectedDay.value))
Text(DateFormat.yMd().format(selectedDay.value)) .fontSize(12)
.fontSize(12) .textColor(
.textColor( Theme.of(context).colorScheme.onSecondaryContainer,
Theme.of(context).colorScheme.onSecondaryContainer, ),
), const Gap(16),
const Gap(16), if (event?.checkInResult != null)
if (event?.checkInResult != null) Column(
Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ Text(
Text( 'checkInResultLevel${event!.checkInResult!.level}',
'checkInResultLevel${event!.checkInResult!.level}', ).tr().fontSize(16).bold(),
).tr().fontSize(16).bold(), for (final tip in event.checkInResult!.tips)
for (final tip in event.checkInResult!.tips) Row(
Row( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, spacing: 8,
spacing: 8, children: [
children: [ Icon(
Icon( Symbols.circle,
Symbols.circle, size: 12,
size: 12, fill: 1,
fill: 1, ).padding(top: 4, right: 4),
).padding(top: 4, right: 4), Expanded(
Expanded( child: Column(
child: Column( crossAxisAlignment:
crossAxisAlignment: CrossAxisAlignment.start,
CrossAxisAlignment.start, children: [
children: [ Text(tip.title).bold(),
Text(tip.title).bold(), Text(tip.content),
Text(tip.content), ],
], ),
), ),
), ],
], ).padding(top: 8),
).padding(top: 8), ],
], ),
), if (event?.checkInResult == null &&
if (event?.checkInResult == null && (event?.statuses.isEmpty ?? true))
(event?.statuses.isEmpty ?? true)) Text('eventCalanderEmpty').tr(),
Text('eventCalanderEmpty').tr(), ],
], ).padding(vertical: 24, horizontal: 24);
).padding(vertical: 24, horizontal: 24); },
}, ),
), ),
), ],
], ),
), ),
); );
} }

View File

@ -106,38 +106,25 @@ class TabsNavigationWidget extends HookConsumerWidget {
Column( Column(
children: [ children: [
Gap(MediaQuery.of(context).padding.top + 8), Gap(MediaQuery.of(context).padding.top + 8),
if (useExpandableLayout) Expanded(
Expanded( child: NavigationRail(
child: NavigationDrawer( extended: useExpandableLayout,
backgroundColor: Colors.transparent, selectedIndex: activeIndex,
children: [ onDestinationSelected: (index) {
for (final destination in destinations) router.replace(routes[index]);
NavigationDrawerDestination( },
label: Text(destination.label), // labelType: NavigationRailLabelType.all,
icon: destination.icon, destinations:
), destinations
], .map(
), (d) => NavigationRailDestination(
) icon: d.icon,
else label: Text(d.label),
Expanded( ),
child: NavigationRail( )
selectedIndex: activeIndex, .toList(),
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), Gap(MediaQuery.of(context).padding.bottom + 8),
], ],
), ),

View File

@ -8,7 +8,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/route.gr.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/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@ -25,17 +27,69 @@ Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async {
return SnPublisherStats.fromJson(resp.data); 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() @RoutePage()
class CreatorHubScreen extends HookConsumerWidget { class CreatorHubScreen extends HookConsumerWidget {
const CreatorHubScreen({super.key}); final bool isAside;
const CreatorHubScreen({super.key, this.isAside = false});
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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 publishers = ref.watch(publishersManagedProvider);
final currentPublisher = useState<SnPublisher?>( final currentPublisher = useState<SnPublisher?>(
publishers.value?.firstOrNull, 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<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when( final List<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when(
data: data:
(data) => (data) =>
@ -184,23 +238,56 @@ class CreatorHubScreen extends HookConsumerWidget {
_PublisherStatsWidget( _PublisherStatsWidget(
stats: stats, stats: stats,
).padding(vertical: 12, horizontal: 12), ).padding(vertical: 12, horizontal: 12),
if (currentPublisher.value != null) ListTile(
ListTile( minTileHeight: 48,
minTileHeight: 48, title: Text('stickers').tr(),
title: Text('stickers').tr(), trailing: Icon(Symbols.chevron_right),
trailing: Icon(Symbols.chevron_right), leading: const Icon(Symbols.ar_stickers),
leading: const Icon(Symbols.sticky_note), contentPadding: EdgeInsets.symmetric(
contentPadding: EdgeInsets.symmetric( horizontal: 24,
horizontal: 24,
),
onTap: () {
context.router.push(
StickersRoute(
pubName: currentPublisher.value!.name,
),
);
},
), ),
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();
},
),
], ],
), ),
), ),

View File

@ -12,7 +12,6 @@ import 'package:island/models/realm.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/realm/realms.dart'; import 'package:island/screens/realm/realms.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
@ -35,124 +34,6 @@ Future<List<SnPublisher>> publishersManaged(Ref ref) async {
.toList(); .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 @riverpod
Future<SnPublisher?> publisher(Ref ref, String? identifier) async { Future<SnPublisher?> publisher(Ref ref, String? identifier) async {
if (identifier == null) return null; if (identifier == null) return null;

View File

@ -1,11 +1,12 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart'; import 'package:island/models/activity.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart'; import 'package:island/route.gr.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/status.dart'; import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
@ -25,9 +26,10 @@ class ExploreScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userInfoProvider);
final activitiesNotifier = ref.watch(activityListNotifierProvider.notifier); final activitiesNotifier = ref.watch(activityListNotifierProvider.notifier);
final isWide = isWideScreen(context);
return TourTriggerWidget( return TourTriggerWidget(
child: AppScaffold( child: AppScaffold(
appBar: AppBar(title: const Text('explore').tr()), appBar: AppBar(title: const Text('explore').tr()),
@ -50,53 +52,40 @@ class ExploreScreen extends ConsumerWidget {
futureRefreshable: activityListNotifierProvider.future, futureRefreshable: activityListNotifierProvider.future,
notifierRefreshable: activityListNotifierProvider.notifier, notifierRefreshable: activityListNotifierProvider.notifier,
contentBuilder: contentBuilder:
(data, widgetCount, endItemView) => CustomScrollView( (data, widgetCount, endItemView) => Center(
slivers: [ child: ConstrainedBox(
if (user.hasValue) constraints: const BoxConstraints(
SliverToBoxAdapter(child: CheckInWidget()), maxWidth: kWideScreenWidth - 160,
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)],
);
},
), ),
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<SnActivity> 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 @riverpod
class ActivityListNotifier extends _$ActivityListNotifier class ActivityListNotifier extends _$ActivityListNotifier
with CursorPagingNotifierMixin<SnActivity> { with CursorPagingNotifierMixin<SnActivity> {

View File

@ -14,14 +14,13 @@ import 'package:island/models/file.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.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/screens/posts/detail.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/publishers_modal.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:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@ -291,15 +290,14 @@ class PostComposeScreen extends HookConsumerWidget {
(_) => (_) =>
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), const Gap(8),
TapRegion( TextField(
child: MarkdownAutoPreview( controller: contentController,
controller: contentController, style: TextStyle(fontSize: 14),
emojiConvert: true, decoration: InputDecoration(
border: InputBorder.none,
hintText: 'postPlaceholder'.tr(), hintText: 'postPlaceholder'.tr(),
decoration: InputDecoration( isDense: true,
border: InputBorder.none,
),
), ),
onTapOutside: onTapOutside:
(_) => (_) =>
@ -343,7 +341,7 @@ class PostComposeScreen extends HookConsumerWidget {
).padding(horizontal: 16), ).padding(horizontal: 16),
), ),
Material( Material(
elevation: 2, elevation: 4,
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
@ -358,7 +356,7 @@ class PostComposeScreen extends HookConsumerWidget {
), ),
], ],
).padding( ).padding(
bottom: MediaQuery.of(context).padding.bottom, bottom: MediaQuery.of(context).padding.bottom + 16,
horizontal: 16, horizontal: 16,
top: 8, top: 8,
), ),

View File

@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_quick_reply.dart'; import 'package:island/widgets/post/post_quick_reply.dart';
@ -29,36 +30,68 @@ class PostDetailScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final post = ref.watch(postProvider(id)); final post = ref.watch(postProvider(id));
final isWide = isWideScreen(context);
return AppScaffold( return AppScaffold(
appBar: AppBar(title: const Text('Post')), appBar: AppBar(title: const Text('Post')),
body: post.when( body: post.when(
data: data: (post) {
(post) => Stack( final content = Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
Column( Column(
children: [ children: [
PostItem(item: post!, isOpenable: false), PostItem(
const Divider(height: 1), item: post!,
Expanded(child: PostRepliesList(postId: id)), isOpenable: false,
Gap(MediaQuery.of(context).padding.bottom), backgroundColor: isWide ? Colors.transparent : null,
], ),
), const Divider(height: 1),
Positioned( Expanded(child: PostRepliesList(postId: id)),
bottom: 0, Gap(MediaQuery.of(context).padding.bottom),
left: 0, ],
right: 0, ),
child: Material( Positioned(
elevation: 2, bottom: 0,
child: PostQuickReply(parent: post).padding( left: 0,
bottom: MediaQuery.of(context).padding.bottom, right: 0,
top: 16, child: Material(
horizontal: 16, 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()), loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Text('Error: $e'), error: (e, _) => Text('Error: $e'),
), ),

View File

@ -1,8 +1,8 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
const kWideScreenWidth = 768; const kWideScreenWidth = 768.0;
const kWiderScreenWidth = 1024; const kWiderScreenWidth = 1024.0;
const kWidescreenWidth = 1280; const kWidescreenWidth = 1280.0;
bool isWideScreen(BuildContext context) { bool isWideScreen(BuildContext context) {
return MediaQuery.of(context).size.width > kWideScreenWidth; return MediaQuery.of(context).size.width > kWideScreenWidth;

View File

@ -53,7 +53,7 @@ class TourStatusNotifier extends _$TourStatusNotifier {
} }
Future<Widget?> showTour(String tourId) async { Future<Widget?> showTour(String tourId) async {
if (!isTourShown(tourId) || true) { if (!isTourShown(tourId)) {
final newState = {...state, tourId: true}; final newState = {...state, tourId: true};
await _saveState(newState); await _saveState(newState);
return kAllTours.firstWhere((e) => e.id == tourId).widget; return kAllTours.firstWhere((e) => e.id == tourId).widget;

View File

@ -7,7 +7,7 @@ part of 'tour.dart';
// ************************************************************************** // **************************************************************************
String _$tourStatusNotifierHash() => String _$tourStatusNotifierHash() =>
r'040aac2d7cf6d14e539c1b04cf311421ee133ed3'; r'ee712e1f8010311df8f24838814ab5c451f9e593';
/// See also [TourStatusNotifier]. /// See also [TourStatusNotifier].
@ProviderFor(TourStatusNotifier) @ProviderFor(TourStatusNotifier)

View File

@ -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<D extends PagingData<I>, I>
extends ConsumerWidget {
const PagingHelperSliverView({
required this.provider,
required this.futureRefreshable,
required this.notifierRefreshable,
required this.contentBuilder,
this.showSecondPageError = true,
super.key,
});
final ProviderListenable<AsyncValue<D>> provider;
final Refreshable<Future<D>> futureRefreshable;
final Refreshable<PagingNotifierMixin<D, I>> 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<PagingHelperViewTheme>();
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<PagingHelperViewTheme>();
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<PagingHelperViewTheme>();
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<T> on AsyncValue<T> {
/// 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<R>({
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,
);
}
}

View File

@ -17,6 +17,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:super_context_menu/super_context_menu.dart'; import 'package:super_context_menu/super_context_menu.dart';
class PostItem extends HookConsumerWidget { class PostItem extends HookConsumerWidget {
final Color? backgroundColor;
final SnPost item; final SnPost item;
final EdgeInsets? padding; final EdgeInsets? padding;
final bool isOpenable; final bool isOpenable;
@ -25,6 +26,7 @@ class PostItem extends HookConsumerWidget {
const PostItem({ const PostItem({
super.key, super.key,
required this.item, required this.item,
this.backgroundColor,
this.padding, this.padding,
this.isOpenable = true, this.isOpenable = true,
this.onRefresh, this.onRefresh,
@ -96,6 +98,7 @@ class PostItem extends HookConsumerWidget {
); );
}, },
child: Material( child: Material(
color: backgroundColor,
child: Padding( child: Padding(
padding: renderingPadding, padding: renderingPadding,
child: Column( child: Column(

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/network.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:island/widgets/post/post_item.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
@ -53,7 +54,7 @@ class SliverPostList extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperView( return PagingHelperSliverView(
provider: postListNotifierProvider, provider: postListNotifierProvider,
futureRefreshable: postListNotifierProvider.future, futureRefreshable: postListNotifierProvider.future,
notifierRefreshable: postListNotifierProvider.notifier, notifierRefreshable: postListNotifierProvider.notifier,

View File

@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/network.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/alert.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/publishers_modal.dart'; import 'package:island/widgets/post/publishers_modal.dart';

View File

@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/network.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/post/post_item.dart';
import 'package:island/widgets/response.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -14,6 +16,7 @@ class PostRepliesList extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final postAsync = ref.watch(postRepliesProvider(postId)); final postAsync = ref.watch(postRepliesProvider(postId));
final isWide = isWideScreen(context);
return RefreshIndicator( return RefreshIndicator(
onRefresh: onRefresh:
@ -37,7 +40,10 @@ class PostRepliesList extends HookConsumerWidget {
onFetchData: controller.fetchMore, onFetchData: controller.fetchMore,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final post = controller.posts[index]; final post = controller.posts[index];
return PostItem(item: post); return PostItem(
item: post,
backgroundColor: isWide ? Colors.transparent : null,
);
}, },
separatorBuilder: (_, __) => const Divider(height: 1), separatorBuilder: (_, __) => const Divider(height: 1),
emptyBuilder: (context) { emptyBuilder: (context) {
@ -55,11 +61,9 @@ class PostRepliesList extends HookConsumerWidget {
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error:
(e, _) => GestureDetector( (e, _) => ResponseErrorWidget(
child: Center( error: e,
child: Text('Error: $e', textAlign: TextAlign.center), onRetry: () {
),
onTap: () {
ref.invalidate(postRepliesProvider(postId)); ref.invalidate(postRepliesProvider(postId));
}, },
), ),

View File

@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/route.gr.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:island/widgets/content/cloud_files.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';

View File

@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class ResponseErrorWidget extends StatelessWidget { class ResponseErrorWidget extends StatelessWidget {
final dynamic error; final dynamic error;
@ -19,11 +20,14 @@ class ResponseErrorWidget extends StatelessWidget {
children: [ children: [
const Icon(Symbols.error_outline, size: 48), const Icon(Symbols.error_outline, size: 48),
const Gap(4), const Gap(4),
Text( ConstrainedBox(
error.toString(), constraints: const BoxConstraints(maxWidth: 320),
textAlign: TextAlign.center, child: Text(
style: const TextStyle(color: Color(0xFF757575)), error.toString(),
), textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
),
).center(),
const Gap(8), const Gap(8),
TextButton(onPressed: onRetry, child: const Text('retry').tr()), TextButton(onPressed: onRetry, child: const Text('retry').tr()),
], ],

View File

@ -1972,7 +1972,7 @@ packages:
source: hosted source: hosted
version: "0.9.0" version: "0.9.0"
visibility_detector: visibility_detector:
dependency: transitive dependency: "direct main"
description: description:
name: visibility_detector name: visibility_detector
sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420

View File

@ -96,6 +96,7 @@ dependencies:
crypto: ^3.0.6 crypto: ^3.0.6
avatar_stack: ^3.0.0 avatar_stack: ^3.0.0
markdown_widget: ^2.3.2+8 markdown_widget: ^2.3.2+8
visibility_detector: ^0.4.0+2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: