♻️ Refactored cloud file picker, stickers

This commit is contained in:
2025-10-12 02:21:39 +08:00
parent c660a419e2
commit bbb07d574a
5 changed files with 485 additions and 482 deletions

View File

@@ -8,13 +8,14 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.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/screens/creators/stickers/stickers.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:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -33,13 +34,13 @@ Future<List<SnSticker>> stickerPackContent(Ref ref, String packId) async {
.toList();
}
class StickerPackDetailScreen extends HookConsumerWidget {
class StickerPackDetailContent extends HookConsumerWidget {
final String id;
final String pubName;
const StickerPackDetailScreen({
const StickerPackDetailContent({
super.key,
required this.pubName,
required this.id,
required this.pubName,
});
@override
@@ -67,194 +68,170 @@ class StickerPackDetailScreen extends HookConsumerWidget {
}
}
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: Text(pack.value?.name ?? 'loading'.tr()),
actions: [
IconButton(
icon: const Icon(Symbols.add_circle),
onPressed: () {
context
.pushNamed(
'creatorStickerNew',
pathParameters: {'name': pubName, 'packId': id},
)
.then((value) {
if (value != null) {
ref.invalidate(stickerPackContentProvider(id));
}
});
},
),
_StickerPackActionMenu(
pubName: pubName,
packId: id,
iconShadow: Shadow(),
),
const Gap(8),
],
),
body: pack.when(
data:
(pack) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(pack!.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(pack.prefix, style: GoogleFonts.robotoMono()),
],
).opacity(0.85),
Row(
spacing: 4,
children: [
const Icon(Symbols.tag, size: 16),
SelectableText(
return pack.when(
data:
(pack) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(pack!.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(pack.prefix, style: GoogleFonts.robotoMono()),
],
).opacity(0.85),
Row(
spacing: 4,
children: [
const Icon(Symbols.tag, size: 16),
Flexible(
child: SelectableText(
pack.id,
maxLines: 1,
style: GoogleFonts.robotoMono(),
),
],
).opacity(0.85),
],
).padding(horizontal: 24, vertical: 24),
const Divider(height: 1),
Expanded(
child: packContent.when(
data:
(stickers) => RefreshIndicator(
onRefresh:
() => ref.refresh(
stickerPackContentProvider(id).future,
),
child: GridView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 20,
),
],
).opacity(0.85),
],
).padding(horizontal: 24, vertical: 24),
const Divider(height: 1),
Expanded(
child: packContent.when(
data:
(stickers) => RefreshIndicator(
onRefresh:
() => ref.refresh(
stickerPackContentProvider(id).future,
),
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 48,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: stickers.length,
itemBuilder: (context, index) {
final sticker = stickers[index];
return ContextMenuWidget(
menuProvider: (_) {
return Menu(
children: [
MenuAction(
title: 'stickerCopyPlaceholder'.tr(),
image: MenuImage.icon(Symbols.copy_all),
callback: () {
Clipboard.setData(
ClipboardData(
text:
':${pack.prefix}${sticker.slug}:',
),
);
},
),
MenuSeparator(),
MenuAction(
title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit),
callback: () {
context
.pushNamed(
'creatorStickerEdit',
pathParameters: {
'name': pubName,
'packId': id,
'id': sticker.id,
},
)
.then((value) {
if (value != null) {
ref.invalidate(
stickerPackContentProvider(
id,
),
);
}
});
},
),
MenuAction(
title: 'delete'.tr(),
image: MenuImage.icon(Symbols.delete),
callback: () {
deleteSticker(sticker);
},
),
],
);
},
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: CloudImageWidget(
fileId: sticker.imageId,
),
),
),
);
},
child: GridView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 20,
),
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 80,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: stickers.length,
itemBuilder: (context, index) {
final sticker = stickers[index];
return ContextMenuWidget(
menuProvider: (_) {
return Menu(
children: [
MenuAction(
title: 'stickerCopyPlaceholder'.tr(),
image: MenuImage.icon(Symbols.copy_all),
callback: () {
Clipboard.setData(
ClipboardData(
text:
':${pack.prefix}+${sticker.slug}:',
),
);
},
),
MenuSeparator(),
MenuAction(
title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit),
callback: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'editSticker'.tr(),
child: StickerForm(
packId: id,
id: sticker.id,
),
),
).then((value) {
if (value != null) {
ref.invalidate(
stickerPackContentProvider(id),
);
}
});
},
),
MenuAction(
title: 'delete'.tr(),
image: MenuImage.icon(Symbols.delete),
callback: () {
deleteSticker(sticker);
},
),
],
);
},
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: CloudImageWidget(
fileId: sticker.imageId,
fit: BoxFit.contain,
),
),
),
);
},
),
error:
(err, _) =>
Text(
'Error: $err',
).textAlignment(TextAlign.center).center(),
loading: () => const CircularProgressIndicator().center(),
),
),
error:
(err, _) =>
Text(
'Error: $err',
).textAlignment(TextAlign.center).center(),
loading: () => const CircularProgressIndicator().center(),
),
],
),
error:
(err, _) =>
Text('Error: $err').textAlignment(TextAlign.center).center(),
loading: () => const CircularProgressIndicator().center(),
),
),
],
),
error:
(err, _) =>
Text('Error: $err').textAlignment(TextAlign.center).center(),
loading: () => const CircularProgressIndicator().center(),
);
}
}
class _StickerPackActionMenu extends HookConsumerWidget {
class StickerPackActionMenu extends HookConsumerWidget {
final String pubName;
final String packId;
final Shadow iconShadow;
const _StickerPackActionMenu({
const StickerPackActionMenu({
super.key,
required this.pubName,
required this.packId,
required this.iconShadow,
@@ -268,7 +245,22 @@ class _StickerPackActionMenu extends HookConsumerWidget {
(context) => [
PopupMenuItem(
onTap: () {
context.push('/creators/$pubName/stickers/$packId/edit');
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'editStickerPack'.tr(),
child: StickerPackForm(
pubName: pubName,
packId: packId,
),
),
).then((value) {
if (value != null) {
ref.invalidate(stickerPackProvider(packId));
}
});
},
child: Row(
children: [
@@ -311,42 +303,10 @@ class _StickerPackActionMenu extends HookConsumerWidget {
}
}
@freezed
sealed class StickerWithPackQuery with _$StickerWithPackQuery {
const factory StickerWithPackQuery({
required String packId,
required String id,
}) = _StickerWithPackQuery;
}
@riverpod
Future<SnSticker?> stickerPackSticker(
Ref ref,
StickerWithPackQuery? query,
) async {
if (query == null) return null;
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get(
'/sphere/stickers/${query.packId}/content/${query.id}',
);
if (resp.data == null) return null;
return SnSticker.fromJson(resp.data);
}
class NewStickersScreen extends StatelessWidget {
final String packId;
const NewStickersScreen({super.key, required this.packId});
@override
Widget build(BuildContext context) {
return EditStickersScreen(packId: packId, id: null);
}
}
class EditStickersScreen extends HookConsumerWidget {
class StickerForm extends HookConsumerWidget {
final String packId;
final String? id;
const EditStickersScreen({super.key, required this.packId, required this.id});
const StickerForm({super.key, required this.packId, this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -396,88 +356,102 @@ class EditStickersScreen extends HookConsumerWidget {
}
}
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: Text(id == null ? 'createSticker' : 'editSticker').tr(),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 96,
width: 96,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(8)),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.all(Radius.circular(8)),
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
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:
(image.value?.isEmpty ?? true)
? const SizedBox.shrink()
: CloudImageWidget(fileId: image.value!),
),
child:
(image.value?.isEmpty ?? true)
? const SizedBox.shrink()
: CloudImageWidget(fileId: image.value!),
),
),
),
const Gap(16),
Form(
key: formKey,
child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: imageController,
decoration: InputDecoration(
labelText: 'stickerImage'.tr(),
border: const UnderlineInputBorder(),
suffix: InkWell(
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => CloudFilePicker(),
).then((value) {
if (value == null) return;
image.value = value[0].id;
imageController.text = image.value!;
});
},
borderRadius: BorderRadius.all(Radius.circular(8)),
child: const Icon(
Symbols.cloud_upload,
).padding(horizontal: 4),
),
),
readOnly: true,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'stickerSlug'.tr(),
helperText: 'stickerSlugHint'.tr(),
border: const UnderlineInputBorder(),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
IconButton.filledTonal(
onPressed: () {
showModalBottomSheet(
context: context,
builder:
(context) => CloudFilePicker(
allowedTypes: {UniversalFileType.image},
),
).then((value) {
if (value == null) return;
image.value = value[0].id;
imageController.text = image.value!;
});
},
icon: const Icon(Symbols.cloud_upload),
),
],
),
const Gap(16),
Form(
key: formKey,
child: Column(
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'stickerSlug'.tr(),
helperText: 'stickerSlugHint'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
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(id == null ? 'create' : 'saveChanges').tr(),
),
),
const Gap(12),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : submit,
icon: const Icon(Symbols.save),
label: Text(id == null ? 'create' : 'saveChanges').tr(),
),
],
).padding(horizontal: 24, vertical: 24),
);
),
],
).padding(horizontal: 24, vertical: 16);
}
}
@freezed
sealed class StickerWithPackQuery with _$StickerWithPackQuery {
const factory StickerWithPackQuery({
required String packId,
required String id,
}) = _StickerWithPackQuery;
}
@riverpod
Future<SnSticker?> stickerPackSticker(
Ref ref,
StickerWithPackQuery? query,
) async {
if (query == null) return null;
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get(
'/sphere/stickers/${query.packId}/content/${query.id}',
);
if (resp.data == null) return null;
return SnSticker.fromJson(resp.data);
}