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/file.dart'; import 'package:island/models/sticker.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/paging.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_file_picker.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:island/screens/creators/stickers/pack_detail.dart'; import 'package:island/widgets/paging/pagination_list.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; part 'stickers.g.dart'; class StickersScreen extends HookConsumerWidget { final String pubName; const StickersScreen({super.key, required this.pubName}); @override Widget build(BuildContext context, WidgetRef ref) { final content = SliverStickerPacksList(pubName: pubName); return AppScaffold( isNoBackground: false, appBar: AppBar( title: const Text('stickers').tr(), actions: [const Gap(8)], ), floatingActionButton: FloatingActionButton( onPressed: () { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => SheetScaffold( titleText: 'createStickerPack'.tr(), child: StickerPackForm(pubName: pubName), ), ).then((value) { if (value != null) { ref.invalidate(stickerPacksProvider(pubName)); } }); }, child: const Icon(Symbols.add), ), body: isWideScreen(context) ? Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: 640), child: Card( shape: RoundedRectangleBorder( borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), topRight: Radius.circular(8), ), ), margin: const EdgeInsets.only(top: 16), child: content, ), ), ) : content, ); } } class SliverStickerPacksList extends HookConsumerWidget { final String pubName; const SliverStickerPacksList({super.key, required this.pubName}); @override Widget build(BuildContext context, WidgetRef ref) { return PaginationList( provider: stickerPacksProvider(pubName), notifier: stickerPacksProvider(pubName).notifier, itemBuilder: (context, index, sticker) { return ListTile( shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(8)), ), title: Text(sticker.name), subtitle: Text(sticker.description), trailing: const Icon(Symbols.chevron_right), onTap: () { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => SheetScaffold( titleText: sticker.name, actions: [ IconButton( icon: const Icon(Symbols.add_circle), onPressed: () { final id = sticker.id; showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => SheetScaffold( titleText: 'createSticker'.tr(), child: StickerForm(packId: id), ), ).then((value) { if (value != null) { ref.invalidate(stickerPackContentProvider(id)); } }); }, ), StickerPackActionMenu( pubName: pubName, packId: sticker.id, iconShadow: Shadow(), ), ], child: StickerPackDetailContent( id: sticker.id, pubName: pubName, ), ), ); }, ); }, ); } } final stickerPacksProvider = AsyncNotifierProvider.family.autoDispose( StickerPacksNotifier.new, ); class StickerPacksNotifier extends AsyncNotifier> with AsyncPaginationController { static const int pageSize = 20; final String arg; StickerPacksNotifier(this.arg); @override Future> fetch() async { final client = ref.read(apiClientProvider); try { final response = await client.get( '/sphere/stickers', queryParameters: { 'offset': fetchedCount.toString(), 'take': pageSize, 'pub': arg, }, ); totalCount = int.parse(response.headers.value('X-Total') ?? '0'); final stickers = response.data .map((e) => SnStickerPack.fromJson(e)) .cast() .toList(); return stickers; } 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('/sphere/stickers/$packId'); return SnStickerPack.fromJson(resp.data); } class StickerPackForm extends HookConsumerWidget { final String pubName; final String? packId; const StickerPackForm({super.key, required this.pubName, this.packId}); @override Widget build(BuildContext context, WidgetRef ref) { final formKey = useMemoized(() => GlobalKey(), []); final initialPack = ref.watch(stickerPackProvider(packId)); final icon = useState( packId == null ? '' : initialPack.value?.icon?.id, ); 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( packId == null ? '/sphere/stickers' : '/sphere/stickers/$packId', data: { 'name': nameController.text, 'description': descriptionController.text, 'prefix': prefixController.text, 'icon_id': icon.value, }, queryParameters: {'pub': pubName}, options: Options(method: packId == null ? 'POST' : 'PATCH'), ); if (!context.mounted) return; Navigator.of(context).pop(SnStickerPack.fromJson(resp.data)); } catch (err) { showErrorAlert(err); } finally { submitting.value = false; } } return Column( children: [ Form( key: formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, spacing: 16, children: [ Row( spacing: 8, crossAxisAlignment: CrossAxisAlignment.end, children: [ SizedBox( height: 80, width: 80, child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(8)), child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadius.all(Radius.circular(8)), ), child: (icon.value?.isEmpty ?? true) ? const SizedBox.shrink() : CloudImageWidget(fileId: icon.value!), ), ), ), IconButton.filledTonal( onPressed: () { showModalBottomSheet( context: context, builder: (context) => CloudFilePicker( allowedTypes: {UniversalFileType.image}, ), ).then((value) { if (value == null) return; icon.value = value[0].id; }); }, icon: const Icon(Symbols.cloud_upload), ), ], ), TextFormField( controller: nameController, decoration: InputDecoration( labelText: 'name'.tr(), border: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), ), 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 OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), alignLabelWithHint: true, ), minLines: 3, maxLines: null, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), TextFormField( controller: prefixController, decoration: InputDecoration( labelText: 'stickerPackPrefix'.tr(), border: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), helperText: 'stickerPackPrefixHint'.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); } }