📱 Responsive for desktop
This commit is contained in:
parent
1f2a5c107d
commit
ea90364566
@ -42,7 +42,7 @@
|
||||
"update": "Update",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"deletePublisher": "Delete Publisher {}",
|
||||
"deletePublisher": "Delete Publisher",
|
||||
"deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.",
|
||||
"somethingWentWrong": "Something went wrong...",
|
||||
"deletePost": "Delete Post",
|
||||
@ -260,5 +260,6 @@
|
||||
"walletCreate": "Create a Wallet",
|
||||
"settingsServerUrl": "Server URL",
|
||||
"settingsApplied": "The settings has been applied.",
|
||||
"notifications": "Notifications"
|
||||
"notifications": "Notifications",
|
||||
"posts": "Posts"
|
||||
}
|
||||
|
@ -37,6 +37,30 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'),
|
||||
],
|
||||
),
|
||||
AutoRoute(
|
||||
page: CreatorHubShellRoute.page,
|
||||
path: '/creators',
|
||||
children: [
|
||||
AutoRoute(page: CreatorHubRoute.page, path: ''),
|
||||
AutoRoute(page: StickersRoute.page, path: ':name/stickers'),
|
||||
AutoRoute(page: NewStickerPacksRoute.page, path: ':name/stickers/new'),
|
||||
AutoRoute(
|
||||
page: EditStickerPacksRoute.page,
|
||||
path: ':name/stickers/:packId/edit',
|
||||
),
|
||||
AutoRoute(
|
||||
page: StickerPackDetailRoute.page,
|
||||
path: ':name/stickers/:packId',
|
||||
),
|
||||
AutoRoute(page: NewStickersRoute.page, path: ':name/stickers/new'),
|
||||
AutoRoute(
|
||||
page: EditStickersRoute.page,
|
||||
path: ':name/stickers/:id/edit',
|
||||
),
|
||||
AutoRoute(page: NewPublisherRoute.page, path: 'new'),
|
||||
AutoRoute(page: EditPublisherRoute.page, path: ':name/edit'),
|
||||
],
|
||||
),
|
||||
AutoRoute(page: LoginRoute.page, path: '/auth/login'),
|
||||
AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'),
|
||||
AutoRoute(page: SettingsRoute.page, path: '/settings'),
|
||||
@ -46,27 +70,5 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: NewRealmRoute.page, path: '/realms/new'),
|
||||
AutoRoute(page: RealmDetailRoute.page, path: '/realms/:slug'),
|
||||
AutoRoute(page: EditRealmRoute.page, path: '/realms/:slug/edit'),
|
||||
AutoRoute(page: CreatorHubRoute.page, path: '/creators'),
|
||||
AutoRoute(page: StickersRoute.page, path: '/creators/:name/stickers'),
|
||||
AutoRoute(
|
||||
page: NewStickerPacksRoute.page,
|
||||
path: '/creators/:name/stickers/new',
|
||||
),
|
||||
AutoRoute(
|
||||
page: EditStickerPacksRoute.page,
|
||||
path: '/creators/:name/stickers/:packId/edit',
|
||||
),
|
||||
AutoRoute(
|
||||
page: StickerPackDetailRoute.page,
|
||||
path: '/creators/:name/stickers/:packId',
|
||||
),
|
||||
AutoRoute(
|
||||
page: NewStickersRoute.page,
|
||||
path: '/creators/:name/stickers/new',
|
||||
),
|
||||
AutoRoute(
|
||||
page: EditStickersRoute.page,
|
||||
path: '/creators/:name/stickers/:id/edit',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import 'package:island/models/post.dart' as _i29;
|
||||
import 'package:island/route.dart' as _i30;
|
||||
import 'package:island/screens/account.dart' as _i2;
|
||||
import 'package:island/screens/account/me/event_calendar.dart' as _i15;
|
||||
import 'package:island/screens/account/me/publishers.dart' as _i9;
|
||||
import 'package:island/screens/account/me/settings.dart' as _i3;
|
||||
import 'package:island/screens/account/me/update.dart' as _i24;
|
||||
import 'package:island/screens/account/profile.dart' as _i1;
|
||||
@ -28,6 +27,7 @@ import 'package:island/screens/chat/chat.dart' as _i5;
|
||||
import 'package:island/screens/chat/room.dart' as _i6;
|
||||
import 'package:island/screens/chat/room_detail.dart' as _i4;
|
||||
import 'package:island/screens/creators/hub.dart' as _i8;
|
||||
import 'package:island/screens/creators/publishers.dart' as _i9;
|
||||
import 'package:island/screens/creators/stickers/pack_detail.dart' as _i12;
|
||||
import 'package:island/screens/creators/stickers/stickers.dart' as _i11;
|
||||
import 'package:island/screens/explore.dart' as _i13;
|
||||
@ -308,16 +308,55 @@ class CreateAccountRoute extends _i26.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i8.CreatorHubScreen]
|
||||
class CreatorHubRoute extends _i26.PageRouteInfo<void> {
|
||||
const CreatorHubRoute({List<_i26.PageRouteInfo>? children})
|
||||
: super(CreatorHubRoute.name, initialChildren: children);
|
||||
class CreatorHubRoute extends _i26.PageRouteInfo<CreatorHubRouteArgs> {
|
||||
CreatorHubRoute({
|
||||
_i27.Key? key,
|
||||
bool isAside = false,
|
||||
List<_i26.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
CreatorHubRoute.name,
|
||||
args: CreatorHubRouteArgs(key: key, isAside: isAside),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'CreatorHubRoute';
|
||||
|
||||
static _i26.PageInfo page = _i26.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i8.CreatorHubScreen();
|
||||
final args = data.argsAs<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
|
||||
/// [_i15.MyselfEventCalendarScreen]
|
||||
class MyselfEventCalendarRoute extends _i26.PageRouteInfo<void> {
|
||||
|
@ -64,7 +64,8 @@ class MyselfEventCalendarScreen extends HookConsumerWidget {
|
||||
leading: const PageBackButton(),
|
||||
title: Text('eventCalander').tr(),
|
||||
),
|
||||
body: Column(
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
TableCalendar(
|
||||
locale: EasyLocalization.of(context)!.locale.toString(),
|
||||
@ -192,6 +193,7 @@ class MyselfEventCalendarScreen extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -106,27 +106,14 @@ class TabsNavigationWidget extends HookConsumerWidget {
|
||||
Column(
|
||||
children: [
|
||||
Gap(MediaQuery.of(context).padding.top + 8),
|
||||
if (useExpandableLayout)
|
||||
Expanded(
|
||||
child: NavigationDrawer(
|
||||
backgroundColor: Colors.transparent,
|
||||
children: [
|
||||
for (final destination in destinations)
|
||||
NavigationDrawerDestination(
|
||||
label: Text(destination.label),
|
||||
icon: destination.icon,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: NavigationRail(
|
||||
extended: useExpandableLayout,
|
||||
selectedIndex: activeIndex,
|
||||
onDestinationSelected: (index) {
|
||||
router.replace(routes[index]);
|
||||
},
|
||||
labelType: NavigationRailLabelType.all,
|
||||
// labelType: NavigationRailLabelType.all,
|
||||
destinations:
|
||||
destinations
|
||||
.map(
|
||||
|
@ -8,7 +8,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/screens/account/me/publishers.dart';
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@ -25,17 +27,69 @@ Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async {
|
||||
return SnPublisherStats.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class CreatorHubShellScreen extends StatelessWidget {
|
||||
const CreatorHubShellScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isWide = isWideScreen(context);
|
||||
if (isWide) {
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)),
|
||||
const VerticalDivider(width: 1),
|
||||
Expanded(child: AutoRouter()),
|
||||
],
|
||||
);
|
||||
}
|
||||
return AutoRouter();
|
||||
}
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class CreatorHubScreen extends HookConsumerWidget {
|
||||
const CreatorHubScreen({super.key});
|
||||
final bool isAside;
|
||||
const CreatorHubScreen({super.key, this.isAside = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isWide = isWideScreen(context);
|
||||
if (isWide && !isAside) {
|
||||
return Container(color: Theme.of(context).colorScheme.surface);
|
||||
}
|
||||
|
||||
final publishers = ref.watch(publishersManagedProvider);
|
||||
final currentPublisher = useState<SnPublisher?>(
|
||||
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(
|
||||
data:
|
||||
(data) =>
|
||||
@ -184,12 +238,11 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
_PublisherStatsWidget(
|
||||
stats: stats,
|
||||
).padding(vertical: 12, horizontal: 12),
|
||||
if (currentPublisher.value != null)
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text('stickers').tr(),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.sticky_note),
|
||||
leading: const Icon(Symbols.ar_stickers),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
@ -201,6 +254,40 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
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/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/screens/realm/realms.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
@ -35,124 +34,6 @@ Future<List<SnPublisher>> publishersManaged(Ref ref) async {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class ManagedPublisherScreen extends HookConsumerWidget {
|
||||
const ManagedPublisherScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final publishers = ref.watch(publishersManagedProvider);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('publishers').tr(),
|
||||
leading: const PageBackButton(),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
child: publishers.when(
|
||||
data:
|
||||
(value) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('createPublisher').tr(),
|
||||
subtitle: Text('createPublisherHint').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
context.router.push(NewPublisherRoute()).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(publishersManagedProvider);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
itemCount: value.length,
|
||||
itemBuilder: (context, item) {
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
fileId: value[item].pictureId,
|
||||
),
|
||||
title: Text(value[item].nick),
|
||||
subtitle: Text('@${value[item].name}'),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
showConfirmAlert(
|
||||
'deletePublisherHint'.tr(),
|
||||
'deletePublisher'.tr(
|
||||
args: ['@${value[item].name}'],
|
||||
),
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.watch(
|
||||
apiClientProvider,
|
||||
);
|
||||
client.delete(
|
||||
'/publishers/${value[item].name}',
|
||||
);
|
||||
ref.invalidate(publishersManagedProvider);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: Icon(Symbols.edit),
|
||||
onPressed: () {
|
||||
context.router
|
||||
.push(
|
||||
EditPublisherRoute(
|
||||
name: value[item].name,
|
||||
),
|
||||
)
|
||||
.then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(
|
||||
publishersManagedProvider,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
contentPadding: EdgeInsets.only(left: 16, right: 14),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(e, _) => GestureDetector(
|
||||
child: Center(
|
||||
child: Text('Error: $e', textAlign: TextAlign.center),
|
||||
),
|
||||
onTap: () {
|
||||
ref.invalidate(publishersManagedProvider);
|
||||
},
|
||||
),
|
||||
),
|
||||
onRefresh: () => ref.refresh(publishersManagedProvider.future),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnPublisher?> publisher(Ref ref, String? identifier) async {
|
||||
if (identifier == null) return null;
|
@ -1,11 +1,12 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/activity.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/account/status.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
@ -25,9 +26,10 @@ class ExploreScreen extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final user = ref.watch(userInfoProvider);
|
||||
final activitiesNotifier = ref.watch(activityListNotifierProvider.notifier);
|
||||
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
return TourTriggerWidget(
|
||||
child: AppScaffold(
|
||||
appBar: AppBar(title: const Text('explore').tr()),
|
||||
@ -50,10 +52,68 @@ class ExploreScreen extends ConsumerWidget {
|
||||
futureRefreshable: activityListNotifierProvider.future,
|
||||
notifierRefreshable: activityListNotifierProvider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => CustomScrollView(
|
||||
(data, widgetCount, endItemView) => Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: kWideScreenWidth - 160,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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()),
|
||||
if (user.hasValue) SliverToBoxAdapter(child: CheckInWidget()),
|
||||
SliverList.builder(
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
@ -62,12 +122,16 @@ class ExploreScreen extends ConsumerWidget {
|
||||
}
|
||||
|
||||
final item = data.items[index];
|
||||
if (item.data == null) return const SizedBox.shrink();
|
||||
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();
|
||||
@ -90,17 +154,11 @@ class ExploreScreen extends ConsumerWidget {
|
||||
itemWidget = const Placeholder();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [itemWidget, const Divider(height: 1)],
|
||||
);
|
||||
return Column(children: [itemWidget, const Divider(height: 1)]);
|
||||
},
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,14 +14,13 @@ import 'package:island/models/file.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/account/me/publishers.dart';
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/screens/posts/detail.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/post/publishers_modal.dart';
|
||||
import 'package:markdown_editor_plus/widgets/markdown_auto_preview.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@ -291,15 +290,14 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TapRegion(
|
||||
child: MarkdownAutoPreview(
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: contentController,
|
||||
emojiConvert: true,
|
||||
hintText: 'postPlaceholder'.tr(),
|
||||
style: TextStyle(fontSize: 14),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
),
|
||||
hintText: 'postPlaceholder'.tr(),
|
||||
isDense: true,
|
||||
),
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
@ -343,7 +341,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
).padding(horizontal: 16),
|
||||
),
|
||||
Material(
|
||||
elevation: 2,
|
||||
elevation: 4,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
@ -358,7 +356,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
).padding(
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
horizontal: 16,
|
||||
top: 8,
|
||||
),
|
||||
|
@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/widgets/post/post_quick_reply.dart';
|
||||
@ -29,16 +30,22 @@ class PostDetailScreen extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final post = ref.watch(postProvider(id));
|
||||
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: const Text('Post')),
|
||||
body: post.when(
|
||||
data:
|
||||
(post) => Stack(
|
||||
data: (post) {
|
||||
final content = Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
PostItem(item: post!, isOpenable: false),
|
||||
PostItem(
|
||||
item: post!,
|
||||
isOpenable: false,
|
||||
backgroundColor: isWide ? Colors.transparent : null,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(child: PostRepliesList(postId: id)),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
@ -50,15 +57,41 @@ class PostDetailScreen extends HookConsumerWidget {
|
||||
right: 0,
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
color: Colors.transparent,
|
||||
child: PostQuickReply(parent: post).padding(
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
top: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return isWide
|
||||
? Center(
|
||||
child: Card(
|
||||
elevation: 8,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerLow.withOpacity(0.8),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: kWideScreenWidth - 160,
|
||||
),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
)
|
||||
: content;
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Text('Error: $e'),
|
||||
),
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
const kWideScreenWidth = 768;
|
||||
const kWiderScreenWidth = 1024;
|
||||
const kWidescreenWidth = 1280;
|
||||
const kWideScreenWidth = 768.0;
|
||||
const kWiderScreenWidth = 1024.0;
|
||||
const kWidescreenWidth = 1280.0;
|
||||
|
||||
bool isWideScreen(BuildContext context) {
|
||||
return MediaQuery.of(context).size.width > kWideScreenWidth;
|
||||
|
@ -53,7 +53,7 @@ class TourStatusNotifier extends _$TourStatusNotifier {
|
||||
}
|
||||
|
||||
Future<Widget?> showTour(String tourId) async {
|
||||
if (!isTourShown(tourId) || true) {
|
||||
if (!isTourShown(tourId)) {
|
||||
final newState = {...state, tourId: true};
|
||||
await _saveState(newState);
|
||||
return kAllTours.firstWhere((e) => e.id == tourId).widget;
|
||||
|
@ -7,7 +7,7 @@ part of 'tour.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$tourStatusNotifierHash() =>
|
||||
r'040aac2d7cf6d14e539c1b04cf311421ee133ed3';
|
||||
r'ee712e1f8010311df8f24838814ab5c451f9e593';
|
||||
|
||||
/// See also [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';
|
||||
|
||||
class PostItem extends HookConsumerWidget {
|
||||
final Color? backgroundColor;
|
||||
final SnPost item;
|
||||
final EdgeInsets? padding;
|
||||
final bool isOpenable;
|
||||
@ -25,6 +26,7 @@ class PostItem extends HookConsumerWidget {
|
||||
const PostItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
this.isOpenable = true,
|
||||
this.onRefresh,
|
||||
@ -96,6 +98,7 @@ class PostItem extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
child: Material(
|
||||
color: backgroundColor,
|
||||
child: Padding(
|
||||
padding: renderingPadding,
|
||||
child: Column(
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/content/paging_helper_ext.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
@ -53,7 +54,7 @@ class SliverPostList extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return PagingHelperView(
|
||||
return PagingHelperSliverView(
|
||||
provider: postListNotifierProvider,
|
||||
futureRefreshable: postListNotifierProvider.future,
|
||||
notifierRefreshable: postListNotifierProvider.notifier,
|
||||
|
@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/account/me/publishers.dart';
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/post/publishers_modal.dart';
|
||||
|
@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
@ -14,6 +16,7 @@ class PostRepliesList extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final postAsync = ref.watch(postRepliesProvider(postId));
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh:
|
||||
@ -37,7 +40,10 @@ class PostRepliesList extends HookConsumerWidget {
|
||||
onFetchData: controller.fetchMore,
|
||||
itemBuilder: (context, index) {
|
||||
final post = controller.posts[index];
|
||||
return PostItem(item: post);
|
||||
return PostItem(
|
||||
item: post,
|
||||
backgroundColor: isWide ? Colors.transparent : null,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
emptyBuilder: (context) {
|
||||
@ -55,11 +61,9 @@ class PostRepliesList extends HookConsumerWidget {
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(e, _) => GestureDetector(
|
||||
child: Center(
|
||||
child: Text('Error: $e', textAlign: TextAlign.center),
|
||||
),
|
||||
onTap: () {
|
||||
(e, _) => ResponseErrorWidget(
|
||||
error: e,
|
||||
onRetry: () {
|
||||
ref.invalidate(postRepliesProvider(postId));
|
||||
},
|
||||
),
|
||||
|
@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/screens/account/me/publishers.dart';
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
|
@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class ResponseErrorWidget extends StatelessWidget {
|
||||
final dynamic error;
|
||||
@ -19,11 +20,14 @@ class ResponseErrorWidget extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Symbols.error_outline, size: 48),
|
||||
const Gap(4),
|
||||
Text(
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 320),
|
||||
child: Text(
|
||||
error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Color(0xFF757575)),
|
||||
),
|
||||
).center(),
|
||||
const Gap(8),
|
||||
TextButton(onPressed: onRetry, child: const Text('retry').tr()),
|
||||
],
|
||||
|
@ -1972,7 +1972,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.9.0"
|
||||
visibility_detector:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: visibility_detector
|
||||
sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420
|
||||
|
@ -96,6 +96,7 @@ dependencies:
|
||||
crypto: ^3.0.6
|
||||
avatar_stack: ^3.0.0
|
||||
markdown_widget: ^2.3.2+8
|
||||
visibility_detector: ^0.4.0+2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
x
Reference in New Issue
Block a user