import 'package:auto_route/auto_route.dart'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/sticker.dart'; import 'package:island/pods/network.dart'; import 'package:island/route.gr.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; part 'stickers.g.dart'; @RoutePage() class StickersScreen extends HookConsumerWidget { final String pubName; const StickersScreen({super.key, @PathParam("name") required this.pubName}); @override Widget build(BuildContext context, WidgetRef ref) { return AppScaffold( appBar: AppBar( title: const Text('stickers').tr(), actions: [ IconButton( onPressed: () { context.router.push(NewStickerPacksRoute(pubName: pubName)).then(( value, ) { if (value != null) { ref.invalidate(stickerPacksNotifierProvider(pubName)); } }); }, icon: const Icon(Symbols.add_circle), ), const Gap(8), ], ), body: SliverStickerPacksList(pubName: pubName), ); } } class SliverStickerPacksList extends HookConsumerWidget { final String pubName; const SliverStickerPacksList({super.key, required this.pubName}); @override Widget build(BuildContext context, WidgetRef ref) { return PagingHelperView( provider: stickerPacksNotifierProvider(pubName), futureRefreshable: stickerPacksNotifierProvider(pubName).future, notifierRefreshable: stickerPacksNotifierProvider(pubName).notifier, contentBuilder: (data, widgetCount, endItemView) => ListView.builder( padding: EdgeInsets.zero, itemCount: widgetCount, itemBuilder: (context, index) { if (index == widgetCount - 1) { return endItemView; } final sticker = data.items[index]; return ListTile( title: Text(sticker.name), subtitle: Text(sticker.description), trailing: const Icon(Symbols.chevron_right), onTap: () { context.router.push( StickerPackDetailRoute(pubName: pubName, id: sticker.id), ); }, ); }, ), ); } } @riverpod class StickerPacksNotifier extends _$StickerPacksNotifier with CursorPagingNotifierMixin { static const int _pageSize = 20; @override Future> build(String pubName) { return fetch(cursor: null); } @override Future> fetch({ required String? cursor, }) async { final client = ref.read(apiClientProvider); final offset = cursor == null ? 0 : int.parse(cursor); try { final response = await client.get( '/stickers', queryParameters: { 'offset': offset, 'take': _pageSize, 'pubName': pubName, }, ); final total = int.parse(response.headers.value('X-Total') ?? '0'); final List data = response.data; final stickers = data.map((e) => SnStickerPack.fromJson(e)).toList(); final hasMore = offset + stickers.length < total; final nextCursor = hasMore ? (offset + stickers.length).toString() : null; return CursorPagingData( items: stickers, hasMore: hasMore, nextCursor: nextCursor, ); } catch (err) { rethrow; } } } @riverpod Future stickerPack(Ref ref, String? packId) async { if (packId == null) return null; final apiClient = ref.watch(apiClientProvider); final resp = await apiClient.get('/stickers/$packId'); return SnStickerPack.fromJson(resp.data); } @RoutePage() class NewStickerPacksScreen extends HookConsumerWidget { final String pubName; const NewStickerPacksScreen({ super.key, @PathParam("name") required this.pubName, }); @override Widget build(BuildContext context, WidgetRef ref) { return EditStickerPacksScreen(pubName: pubName); } } @RoutePage() class EditStickerPacksScreen extends HookConsumerWidget { final String pubName; final String? packId; const EditStickerPacksScreen({ super.key, @PathParam("name") required this.pubName, @PathParam("packId") this.packId, }); @override Widget build(BuildContext context, WidgetRef ref) { final formKey = useMemoized(() => GlobalKey(), []); final initialPack = ref.watch(stickerPackProvider(packId)); final nameController = useTextEditingController(); final descriptionController = useTextEditingController(); final prefixController = useTextEditingController(); useEffect(() { if (initialPack.value != null) { nameController.text = initialPack.value!.name; descriptionController.text = initialPack.value!.description; prefixController.text = initialPack.value!.prefix; } return null; }, [initialPack]); final submitting = useState(false); Future submit() async { if (!(formKey.currentState?.validate() ?? false)) return; try { submitting.value = true; final apiClient = ref.watch(apiClientProvider); final resp = await apiClient.request( '/stickers', data: { 'name': nameController.text, 'description': descriptionController.text, 'prefix': prefixController.text, }, options: Options( method: packId == null ? 'POST' : 'PATCH', headers: {'X-Pub': pubName}, ), ); if (!context.mounted) return; context.router.maybePop(SnStickerPack.fromJson(resp.data)); } catch (err) { showErrorAlert(err); } finally { submitting.value = false; } } return AppScaffold( appBar: AppBar( title: Text(packId == null ? 'createStickerPack' : 'editStickerPack').tr(), ), body: Column( children: [ Form( key: formKey, child: Column( spacing: 8, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextFormField( controller: nameController, decoration: InputDecoration( labelText: 'name'.tr(), border: const UnderlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { return 'fieldCannotBeEmpty'.tr(); } return null; }, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), TextFormField( controller: descriptionController, decoration: InputDecoration( labelText: 'description'.tr(), border: const UnderlineInputBorder(), ), minLines: 3, maxLines: null, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), TextFormField( controller: prefixController, decoration: InputDecoration( labelText: 'stickerPackPrefix'.tr(), border: const UnderlineInputBorder(), helperText: 'deleteStickerHint'.tr(), ), validator: (value) { if (value == null || value.isEmpty) { return 'fieldCannotBeEmpty'.tr(); } return null; }, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), ], ), ), const Gap(12), Align( alignment: Alignment.centerRight, child: TextButton.icon( onPressed: submitting.value ? null : submit, icon: const Icon(Symbols.save), label: Text(packId == null ? 'create'.tr() : 'saveChanges'.tr()), ), ), ], ).padding(horizontal: 24, vertical: 16), ); } }