📱 Responsive for desktop
This commit is contained in:
parent
1f2a5c107d
commit
ea90364566
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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',
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -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> {
|
||||||
|
@ -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);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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;
|
@ -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> {
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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'),
|
||||||
),
|
),
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
236
lib/widgets/content/paging_helper_ext.dart
Normal file
236
lib/widgets/content/paging_helper_ext.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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';
|
||||||
|
@ -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));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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()),
|
||||||
],
|
],
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user