231 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			231 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter/services.dart';
 | |
| import 'package:gap/gap.dart';
 | |
| import 'package:google_fonts/google_fonts.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:island/models/sticker.dart';
 | |
| import 'package:island/pods/network.dart';
 | |
| import 'package:island/screens/creators/stickers/stickers.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';
 | |
| import 'package:riverpod_annotation/riverpod_annotation.dart';
 | |
| import 'package:styled_widget/styled_widget.dart';
 | |
| 
 | |
| part 'pack_detail.g.dart'; // generated by riverpod_annotation build_runner
 | |
| 
 | |
| /// Marketplace version of sticker pack detail page (no publisher dependency).
 | |
| /// Shows all stickers in the pack and provides a button to add the sticker.
 | |
| /// API interactions are intentionally left blank per request.
 | |
| @riverpod
 | |
| Future<List<SnSticker>> marketplaceStickerPackContent(
 | |
|   Ref ref, {
 | |
|   required String packId,
 | |
| }) async {
 | |
|   final apiClient = ref.watch(apiClientProvider);
 | |
|   final resp = await apiClient.get('/sphere/stickers/$packId/content');
 | |
|   return (resp.data as List).map((e) => SnSticker.fromJson(e)).toList();
 | |
| }
 | |
| 
 | |
| @riverpod
 | |
| Future<bool> marketplaceStickerPackOwnership(
 | |
|   Ref ref, {
 | |
|   required String packId,
 | |
| }) async {
 | |
|   final api = ref.watch(apiClientProvider);
 | |
|   try {
 | |
|     await api.get('/sphere/stickers/$packId/own');
 | |
|     // If not 404, consider owned
 | |
|     return true;
 | |
|   } on Object catch (e) {
 | |
|     // Dio error handling agnostic: treat 404 as not-owned, rethrow others
 | |
|     final msg = e.toString();
 | |
|     if (msg.contains('404')) return false;
 | |
|     rethrow;
 | |
|   }
 | |
| }
 | |
| 
 | |
| class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
 | |
|   final String id;
 | |
|   const MarketplaceStickerPackDetailScreen({super.key, required this.id});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     // Pack metadata provider exists globally in creators file; reuse it.
 | |
|     final pack = ref.watch(stickerPackProvider(id));
 | |
|     final packContent = ref.watch(
 | |
|       marketplaceStickerPackContentProvider(packId: id),
 | |
|     );
 | |
|     final owned = ref.watch(
 | |
|       marketplaceStickerPackOwnershipProvider(packId: id),
 | |
|     );
 | |
| 
 | |
|     // Add entire pack to user's collection
 | |
|     Future<void> addPackToMyCollection() async {
 | |
|       final apiClient = ref.watch(apiClientProvider);
 | |
|       await apiClient.post('/sphere/stickers/$id/own');
 | |
|       HapticFeedback.selectionClick();
 | |
|       ref.invalidate(marketplaceStickerPackOwnershipProvider(packId: id));
 | |
|       if (!context.mounted) return;
 | |
|       showSnackBar('stickerPackAdded'.tr());
 | |
|     }
 | |
| 
 | |
|     // Remove ownership of the pack
 | |
|     Future<void> removePackFromMyCollection() async {
 | |
|       final apiClient = ref.watch(apiClientProvider);
 | |
|       await apiClient.delete('/sphere/stickers/$id/own');
 | |
|       HapticFeedback.selectionClick();
 | |
|       ref.invalidate(marketplaceStickerPackOwnershipProvider(packId: id));
 | |
|       if (!context.mounted) return;
 | |
|       showSnackBar('stickerPackRemoved'.tr());
 | |
|     }
 | |
| 
 | |
|     return AppScaffold(
 | |
|       appBar: AppBar(title: Text(pack.value?.name ?? 'loading'.tr())),
 | |
|       body: pack.when(
 | |
|         data: (p) {
 | |
|           return Column(
 | |
|             crossAxisAlignment: CrossAxisAlignment.stretch,
 | |
|             children: [
 | |
|               // Pack meta
 | |
|               Column(
 | |
|                 crossAxisAlignment: CrossAxisAlignment.stretch,
 | |
|                 children: [
 | |
|                   Text(p?.description ?? ''),
 | |
|                   Row(
 | |
|                     spacing: 4,
 | |
|                     children: [
 | |
|                       const Icon(Symbols.folder, size: 16),
 | |
|                       Text(
 | |
|                         '${packContent.value?.length ?? 0}/24',
 | |
|                         style: GoogleFonts.robotoMono(),
 | |
|                       ),
 | |
|                     ],
 | |
|                   ).opacity(0.85),
 | |
|                   Row(
 | |
|                     spacing: 4,
 | |
|                     children: [
 | |
|                       const Icon(Symbols.sell, size: 16),
 | |
|                       Text(p?.prefix ?? '', style: GoogleFonts.robotoMono()),
 | |
|                     ],
 | |
|                   ).opacity(0.85),
 | |
|                   Row(
 | |
|                     spacing: 4,
 | |
|                     children: [
 | |
|                       const Icon(Symbols.tag, size: 16),
 | |
|                       SelectableText(
 | |
|                         p?.id ?? id,
 | |
|                         style: GoogleFonts.robotoMono(),
 | |
|                       ),
 | |
|                     ],
 | |
|                   ).opacity(0.85),
 | |
|                 ],
 | |
|               ).padding(horizontal: 24, vertical: 24),
 | |
|               const Divider(height: 1),
 | |
|               // Stickers grid
 | |
|               Expanded(
 | |
|                 child: packContent.when(
 | |
|                   data:
 | |
|                       (stickers) => RefreshIndicator(
 | |
|                         onRefresh:
 | |
|                             () => ref.refresh(
 | |
|                               marketplaceStickerPackContentProvider(
 | |
|                                 packId: id,
 | |
|                               ).future,
 | |
|                             ),
 | |
|                         child: GridView.builder(
 | |
|                           padding: const EdgeInsets.symmetric(
 | |
|                             horizontal: 24,
 | |
|                             vertical: 20,
 | |
|                           ),
 | |
|                           gridDelegate:
 | |
|                               const SliverGridDelegateWithMaxCrossAxisExtent(
 | |
|                                 maxCrossAxisExtent: 96,
 | |
|                                 mainAxisSpacing: 12,
 | |
|                                 crossAxisSpacing: 12,
 | |
|                               ),
 | |
|                           itemCount: stickers.length,
 | |
|                           itemBuilder: (context, index) {
 | |
|                             final sticker = stickers[index];
 | |
|                             return Tooltip(
 | |
|                               message: ':${p?.prefix ?? ''}${sticker.slug}:',
 | |
|                               child: ClipRRect(
 | |
|                                 borderRadius: const BorderRadius.all(
 | |
|                                   Radius.circular(8),
 | |
|                                 ),
 | |
|                                 child: Container(
 | |
|                                   decoration: BoxDecoration(
 | |
|                                     color:
 | |
|                                         Theme.of(
 | |
|                                           context,
 | |
|                                         ).colorScheme.surfaceContainer,
 | |
|                                     borderRadius: const BorderRadius.all(
 | |
|                                       Radius.circular(8),
 | |
|                                     ),
 | |
|                                   ),
 | |
|                                   child: AspectRatio(
 | |
|                                     aspectRatio: 1,
 | |
|                                     child: CloudImageWidget(
 | |
|                                       fileId: sticker.image.id,
 | |
|                                       fit: BoxFit.contain,
 | |
|                                     ),
 | |
|                                   ),
 | |
|                                 ),
 | |
|                               ),
 | |
|                             );
 | |
|                           },
 | |
|                         ),
 | |
|                       ),
 | |
|                   error:
 | |
|                       (err, _) =>
 | |
|                           Text(
 | |
|                             'Error: $err',
 | |
|                           ).textAlignment(TextAlign.center).center(),
 | |
|                   loading: () => const CircularProgressIndicator().center(),
 | |
|                 ),
 | |
|               ),
 | |
|               Padding(
 | |
|                 padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
 | |
|                 child: owned.when(
 | |
|                   data:
 | |
|                       (isOwned) => FilledButton.icon(
 | |
|                         onPressed:
 | |
|                             isOwned
 | |
|                                 ? removePackFromMyCollection
 | |
|                                 : addPackToMyCollection,
 | |
|                         icon: Icon(
 | |
|                           isOwned ? Symbols.remove_circle : Symbols.add_circle,
 | |
|                         ),
 | |
|                         label: Text(
 | |
|                           isOwned ? 'removePack'.tr() : 'addPack'.tr(),
 | |
|                         ),
 | |
|                       ),
 | |
|                   loading:
 | |
|                       () => const SizedBox(
 | |
|                         height: 32,
 | |
|                         width: 32,
 | |
|                         child: CircularProgressIndicator(strokeWidth: 2),
 | |
|                       ),
 | |
|                   error:
 | |
|                       (_, _) => OutlinedButton.icon(
 | |
|                         onPressed: addPackToMyCollection,
 | |
|                         icon: const Icon(Symbols.add_circle),
 | |
|                         label: Text('addPack').tr(),
 | |
|                       ),
 | |
|                 ),
 | |
|               ),
 | |
|               Gap(MediaQuery.of(context).padding.bottom + 16),
 | |
|             ],
 | |
|           );
 | |
|         },
 | |
|         error:
 | |
|             (err, _) =>
 | |
|                 Text('Error: $err').textAlignment(TextAlign.center).center(),
 | |
|         loading: () => const CircularProgressIndicator().center(),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |