♻️ 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

@@ -43,7 +43,6 @@ import 'package:island/screens/chat/search_messages.dart';
import 'package:island/screens/creators/hub.dart'; import 'package:island/screens/creators/hub.dart';
import 'package:island/screens/creators/posts/post_manage_list.dart'; import 'package:island/screens/creators/posts/post_manage_list.dart';
import 'package:island/screens/creators/stickers/stickers.dart'; import 'package:island/screens/creators/stickers/stickers.dart';
import 'package:island/screens/creators/stickers/pack_detail.dart';
import 'package:island/screens/stickers/sticker_marketplace.dart'; import 'package:island/screens/stickers/sticker_marketplace.dart';
import 'package:island/screens/stickers/pack_detail.dart'; import 'package:island/screens/stickers/pack_detail.dart';
import 'package:island/screens/discovery/feeds/feed_marketplace.dart'; import 'package:island/screens/discovery/feeds/feed_marketplace.dart';
@@ -232,49 +231,6 @@ final routerProvider = Provider<GoRouter>((ref) {
return StickersScreen(pubName: name); return StickersScreen(pubName: name);
}, },
), ),
GoRoute(
name: 'creatorStickerPackNew',
path: ':name/stickers/new',
builder: (context, state) {
final name = state.pathParameters['name']!;
return NewStickerPacksScreen(pubName: name);
},
),
GoRoute(
name: 'creatorStickerPackEdit',
path: ':name/stickers/:packId/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!;
return EditStickerPacksScreen(pubName: name, packId: packId);
},
),
GoRoute(
name: 'creatorStickerPackDetail',
path: ':name/stickers/:packId',
builder: (context, state) {
final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!;
return StickerPackDetailScreen(pubName: name, id: packId);
},
),
GoRoute(
name: 'creatorStickerNew',
path: ':name/stickers/:packId/new',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
return NewStickersScreen(packId: packId);
},
),
GoRoute(
name: 'creatorStickerEdit',
path: ':name/stickers/:packId/:id/edit',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
final id = state.pathParameters['id']!;
return EditStickersScreen(id: id, packId: packId);
},
),
GoRoute( GoRoute(
name: 'creatorNew', name: 'creatorNew',
path: 'new', path: 'new',

View File

@@ -8,13 +8,14 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/models/sticker.dart'; import 'package:island/models/sticker.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/creators/stickers/stickers.dart'; import 'package:island/screens/creators/stickers/stickers.dart';
import 'package:island/widgets/alert.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_file_picker.dart';
import 'package:island/widgets/content/cloud_files.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:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@@ -33,13 +34,13 @@ Future<List<SnSticker>> stickerPackContent(Ref ref, String packId) async {
.toList(); .toList();
} }
class StickerPackDetailScreen extends HookConsumerWidget { class StickerPackDetailContent extends HookConsumerWidget {
final String id; final String id;
final String pubName; final String pubName;
const StickerPackDetailScreen({ const StickerPackDetailContent({
super.key, super.key,
required this.pubName,
required this.id, required this.id,
required this.pubName,
}); });
@override @override
@@ -67,194 +68,170 @@ class StickerPackDetailScreen extends HookConsumerWidget {
} }
} }
return AppScaffold( return pack.when(
isNoBackground: false, data:
appBar: AppBar( (pack) => Column(
title: Text(pack.value?.name ?? 'loading'.tr()), crossAxisAlignment: CrossAxisAlignment.start,
actions: [ children: [
IconButton( Column(
icon: const Icon(Symbols.add_circle), crossAxisAlignment: CrossAxisAlignment.stretch,
onPressed: () { children: [
context Text(pack!.description),
.pushNamed( Row(
'creatorStickerNew', spacing: 4,
pathParameters: {'name': pubName, 'packId': id}, children: [
) const Icon(Symbols.folder, size: 16),
.then((value) { Text(
if (value != null) { '${packContent.value?.length ?? 0}/24',
ref.invalidate(stickerPackContentProvider(id)); style: GoogleFonts.robotoMono(),
} ),
}); ],
}, ).opacity(0.85),
), Row(
_StickerPackActionMenu( spacing: 4,
pubName: pubName, children: [
packId: id, const Icon(Symbols.sell, size: 16),
iconShadow: Shadow(), Text(pack.prefix, style: GoogleFonts.robotoMono()),
), ],
const Gap(8), ).opacity(0.85),
], Row(
), spacing: 4,
body: pack.when( children: [
data: const Icon(Symbols.tag, size: 16),
(pack) => Column( Flexible(
crossAxisAlignment: CrossAxisAlignment.start, child: SelectableText(
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(
pack.id, pack.id,
maxLines: 1,
style: GoogleFonts.robotoMono(), style: GoogleFonts.robotoMono(),
), ),
], ),
).opacity(0.85), ],
], ).opacity(0.85),
).padding(horizontal: 24, vertical: 24), ],
const Divider(height: 1), ).padding(horizontal: 24, vertical: 24),
Expanded( const Divider(height: 1),
child: packContent.when( Expanded(
data: child: packContent.when(
(stickers) => RefreshIndicator( data:
onRefresh: (stickers) => RefreshIndicator(
() => ref.refresh( onRefresh:
stickerPackContentProvider(id).future, () => ref.refresh(
), stickerPackContentProvider(id).future,
child: GridView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 20,
), ),
gridDelegate: child: GridView.builder(
const SliverGridDelegateWithMaxCrossAxisExtent( padding: const EdgeInsets.symmetric(
maxCrossAxisExtent: 48, horizontal: 24,
mainAxisSpacing: 8, vertical: 20,
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,
),
),
),
);
},
), ),
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, _) => error:
Text( (err, _) =>
'Error: $err', Text(
).textAlignment(TextAlign.center).center(), 'Error: $err',
loading: () => const CircularProgressIndicator().center(), ).textAlignment(TextAlign.center).center(),
), loading: () => const CircularProgressIndicator().center(),
), ),
], ),
), ],
error: ),
(err, _) => error:
Text('Error: $err').textAlignment(TextAlign.center).center(), (err, _) =>
loading: () => const CircularProgressIndicator().center(), Text('Error: $err').textAlignment(TextAlign.center).center(),
), loading: () => const CircularProgressIndicator().center(),
); );
} }
} }
class _StickerPackActionMenu extends HookConsumerWidget { class StickerPackActionMenu extends HookConsumerWidget {
final String pubName; final String pubName;
final String packId; final String packId;
final Shadow iconShadow; final Shadow iconShadow;
const _StickerPackActionMenu({ const StickerPackActionMenu({
super.key,
required this.pubName, required this.pubName,
required this.packId, required this.packId,
required this.iconShadow, required this.iconShadow,
@@ -268,7 +245,22 @@ class _StickerPackActionMenu extends HookConsumerWidget {
(context) => [ (context) => [
PopupMenuItem( PopupMenuItem(
onTap: () { 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( child: Row(
children: [ children: [
@@ -311,42 +303,10 @@ class _StickerPackActionMenu extends HookConsumerWidget {
} }
} }
@freezed class StickerForm extends HookConsumerWidget {
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 {
final String packId; final String packId;
final String? id; final String? id;
const EditStickersScreen({super.key, required this.packId, required this.id}); const StickerForm({super.key, required this.packId, this.id});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -396,88 +356,102 @@ class EditStickersScreen extends HookConsumerWidget {
} }
} }
return AppScaffold( return Column(
isNoBackground: false, crossAxisAlignment: CrossAxisAlignment.start,
appBar: AppBar( children: [
title: Text(id == null ? 'createSticker' : 'editSticker').tr(), Row(
), spacing: 8,
body: Column( crossAxisAlignment: CrossAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ SizedBox(
SizedBox( height: 80,
height: 96, width: 80,
width: 96, child: ClipRRect(
child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(8)),
borderRadius: BorderRadius.all(Radius.circular(8)), child: Container(
child: Container( decoration: BoxDecoration(
decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainer,
color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadius.all(Radius.circular(8)),
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!),
), ),
), ),
), IconButton.filledTonal(
const Gap(16), onPressed: () {
Form( showModalBottomSheet(
key: formKey, context: context,
child: Column( builder:
spacing: 8, (context) => CloudFilePicker(
crossAxisAlignment: CrossAxisAlignment.stretch, allowedTypes: {UniversalFileType.image},
children: [ ),
TextFormField( ).then((value) {
controller: imageController, if (value == null) return;
decoration: InputDecoration( image.value = value[0].id;
labelText: 'stickerImage'.tr(), imageController.text = image.value!;
border: const UnderlineInputBorder(), });
suffix: InkWell( },
onTap: () { icon: const Icon(Symbols.cloud_upload),
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(),
),
],
), ),
],
),
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( const Gap(12),
alignment: Alignment.centerRight, Align(
child: TextButton.icon( alignment: Alignment.centerRight,
onPressed: submitting.value ? null : submit, child: TextButton.icon(
icon: const Icon(Symbols.save), onPressed: submitting.value ? null : submit,
label: Text(id == null ? 'create' : 'saveChanges').tr(), 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);
}

View File

@@ -1,14 +1,16 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/sticker.dart'; import 'package:island/models/sticker.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/screens/creators/stickers/pack_detail.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@@ -22,30 +24,50 @@ class StickersScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final content = SliverStickerPacksList(pubName: pubName);
return AppScaffold( return AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: AppBar( appBar: AppBar(
title: const Text('stickers').tr(), title: const Text('stickers').tr(),
actions: [ actions: [const Gap(8)],
IconButton(
onPressed: () {
context
.pushNamed(
'creatorStickerPackNew',
pathParameters: {'name': pubName},
)
.then((value) {
if (value != null) {
ref.invalidate(stickerPacksNotifierProvider(pubName));
}
});
},
icon: const Icon(Symbols.add_circle),
),
const Gap(8),
],
), ),
body: SliverStickerPacksList(pubName: pubName), 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(stickerPacksNotifierProvider(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,
); );
} }
} }
@@ -71,13 +93,52 @@ class SliverStickerPacksList extends HookConsumerWidget {
final sticker = data.items[index]; final sticker = data.items[index];
return ListTile( return ListTile(
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
title: Text(sticker.name), title: Text(sticker.name),
subtitle: Text(sticker.description), subtitle: Text(sticker.description),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
context.pushNamed( showModalBottomSheet(
'creatorStickerPackDetail', context: context,
pathParameters: {'name': pubName, 'packId': sticker.id}, 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,
),
),
); );
}, },
); );
@@ -136,20 +197,10 @@ Future<SnStickerPack?> stickerPack(Ref ref, String? packId) async {
return SnStickerPack.fromJson(resp.data); return SnStickerPack.fromJson(resp.data);
} }
class NewStickerPacksScreen extends HookConsumerWidget { class StickerPackForm extends HookConsumerWidget {
final String pubName;
const NewStickerPacksScreen({super.key, required this.pubName});
@override
Widget build(BuildContext context, WidgetRef ref) {
return EditStickerPacksScreen(pubName: pubName);
}
}
class EditStickerPacksScreen extends HookConsumerWidget {
final String pubName; final String pubName;
final String? packId; final String? packId;
const EditStickerPacksScreen({super.key, required this.pubName, this.packId}); const StickerPackForm({super.key, required this.pubName, this.packId});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -188,7 +239,7 @@ class EditStickerPacksScreen extends HookConsumerWidget {
options: Options(method: packId == null ? 'POST' : 'PATCH'), options: Options(method: packId == null ? 'POST' : 'PATCH'),
); );
if (!context.mounted) return; if (!context.mounted) return;
context.pop(SnStickerPack.fromJson(resp.data)); Navigator.of(context).pop(SnStickerPack.fromJson(resp.data));
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
} finally { } finally {
@@ -196,77 +247,76 @@ class EditStickerPacksScreen extends HookConsumerWidget {
} }
} }
return AppScaffold( return Column(
isNoBackground: false, children: [
appBar: AppBar( Form(
title: key: formKey,
Text(packId == null ? 'createStickerPack' : 'editStickerPack').tr(), child: Column(
), crossAxisAlignment: CrossAxisAlignment.stretch,
body: Column( spacing: 16,
children: [ children: [
Form( TextFormField(
key: formKey, controller: nameController,
child: Column( decoration: InputDecoration(
spacing: 8, labelText: 'name'.tr(),
crossAxisAlignment: CrossAxisAlignment.stretch, border: const OutlineInputBorder(
children: [ borderRadius: BorderRadius.all(Radius.circular(12)),
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( validator: (value) {
controller: descriptionController, if (value == null || value.isEmpty) {
decoration: InputDecoration( return 'fieldCannotBeEmpty'.tr();
labelText: 'description'.tr(), }
border: const UnderlineInputBorder(), return null;
alignLabelWithHint: true, },
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
), ),
minLines: 3, alignLabelWithHint: true,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextFormField( minLines: 3,
controller: prefixController, maxLines: null,
decoration: InputDecoration( onTapOutside:
labelText: 'stickerPackPrefix'.tr(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
border: const UnderlineInputBorder(), ),
helperText: 'deleteStickerHint'.tr(), TextFormField(
controller: prefixController,
decoration: InputDecoration(
labelText: 'stickerPackPrefix'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
), ),
validator: (value) { helperText: 'stickerPackPrefixHint'.tr(),
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
], validator: (value) {
), if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
), ),
const Gap(12), ),
Align( const Gap(12),
alignment: Alignment.centerRight, Align(
child: TextButton.icon( alignment: Alignment.centerRight,
onPressed: submitting.value ? null : submit, child: TextButton.icon(
icon: const Icon(Symbols.save), onPressed: submitting.value ? null : submit,
label: Text(packId == null ? 'create'.tr() : 'saveChanges'.tr()), icon: const Icon(Symbols.save),
), label: Text(packId == null ? 'create'.tr() : 'saveChanges'.tr()),
), ),
], ),
).padding(horizontal: 24, vertical: 16), ],
); ).padding(horizontal: 24, vertical: 16);
} }
} }

View File

@@ -15,7 +15,16 @@ import 'package:styled_widget/styled_widget.dart';
class CloudFilePicker extends HookConsumerWidget { class CloudFilePicker extends HookConsumerWidget {
final bool allowMultiple; final bool allowMultiple;
const CloudFilePicker({super.key, this.allowMultiple = false}); final Set<UniversalFileType> allowedTypes;
const CloudFilePicker({
super.key,
this.allowMultiple = false,
this.allowedTypes = const {
UniversalFileType.image,
UniversalFileType.video,
UniversalFileType.file,
},
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -71,7 +80,7 @@ class CloudFilePicker extends HookConsumerWidget {
void pickFile() async { void pickFile() async {
showLoadingModal(context); showLoadingModal(context);
final result = await FilePickerIO().pickFiles( final result = await FilePicker.platform.pickFiles(
allowMultiple: allowMultiple, allowMultiple: allowMultiple,
); );
if (result == null) { if (result == null) {
@@ -80,9 +89,13 @@ class CloudFilePicker extends HookConsumerWidget {
} }
final newFiles = final newFiles =
result.files result.files.map((e) {
.map((e) => UniversalFile(data: e, type: UniversalFileType.file)) final xfile =
.toList(); e.bytes != null
? XFile.fromData(e.bytes!, name: e.name)
: XFile(e.path!);
return UniversalFile(data: xfile, type: UniversalFileType.file);
}).toList();
if (!allowMultiple) { if (!allowMultiple) {
files.value = newFiles; files.value = newFiles;
@@ -99,23 +112,23 @@ class CloudFilePicker extends HookConsumerWidget {
void pickImage() async { void pickImage() async {
showLoadingModal(context); showLoadingModal(context);
final result = final result = await FilePicker.platform.pickFiles(
allowMultiple allowMultiple: allowMultiple,
? await ref.read(imagePickerProvider).pickMultiImage() type: FileType.image,
: [ );
await ref if (result == null || result.files.isEmpty) {
.read(imagePickerProvider)
.pickImage(source: ImageSource.gallery),
];
if (result.isEmpty) {
if (context.mounted) hideLoadingModal(context); if (context.mounted) hideLoadingModal(context);
return; return;
} }
final newFiles = final newFiles =
result result.files.map((e) {
.map((e) => UniversalFile(data: e, type: UniversalFileType.image)) final xfile =
.toList(); e.bytes != null
? XFile.fromData(e.bytes!, name: e.name)
: XFile(e.path!);
return UniversalFile(data: xfile, type: UniversalFileType.image);
}).toList();
if (!allowMultiple) { if (!allowMultiple) {
files.value = newFiles; files.value = newFiles;
@@ -132,21 +145,26 @@ class CloudFilePicker extends HookConsumerWidget {
void pickVideo() async { void pickVideo() async {
showLoadingModal(context); showLoadingModal(context);
final result = await ref final result = await FilePicker.platform.pickFiles(
.read(imagePickerProvider) allowMultiple: allowMultiple,
.pickVideo(source: ImageSource.gallery); type: FileType.video,
if (result == null) { );
if (result == null || result.files.isEmpty) {
if (context.mounted) hideLoadingModal(context); if (context.mounted) hideLoadingModal(context);
return; return;
} }
final newFile = UniversalFile( final newFiles =
data: result, result.files.map((e) {
type: UniversalFileType.video, final xfile =
); e.bytes != null
? XFile.fromData(e.bytes!, name: e.name)
: XFile(e.path!);
return UniversalFile(data: xfile, type: UniversalFileType.video);
}).toList();
if (!allowMultiple) { if (!allowMultiple) {
files.value = [newFile]; files.value = newFiles;
if (context.mounted) { if (context.mounted) {
hideLoadingModal(context); hideLoadingModal(context);
startUpload(); startUpload();
@@ -154,7 +172,7 @@ class CloudFilePicker extends HookConsumerWidget {
return; return;
} }
files.value = [...files.value, newFile]; files.value = [...files.value, ...newFiles];
if (context.mounted) hideLoadingModal(context); if (context.mounted) hideLoadingModal(context);
} }
@@ -252,30 +270,33 @@ class CloudFilePicker extends HookConsumerWidget {
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: Column( child: Column(
children: [ children: [
ListTile( if (allowedTypes.contains(UniversalFileType.image))
shape: RoundedRectangleBorder( ListTile(
borderRadius: BorderRadius.circular(8), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
leading: const Icon(Symbols.photo),
title: Text('addPhoto'.tr()),
onTap: () => pickImage(),
), ),
leading: const Icon(Symbols.photo), if (allowedTypes.contains(UniversalFileType.video))
title: Text('addPhoto'.tr()), ListTile(
onTap: () => pickImage(), shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(8),
ListTile( ),
shape: RoundedRectangleBorder( leading: const Icon(Symbols.video_call),
borderRadius: BorderRadius.circular(8), title: Text('addVideo'.tr()),
onTap: () => pickVideo(),
), ),
leading: const Icon(Symbols.video_call), if (allowedTypes.contains(UniversalFileType.file))
title: Text('addVideo'.tr()), ListTile(
onTap: () => pickVideo(), shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(8),
ListTile( ),
shape: RoundedRectangleBorder( leading: const Icon(Symbols.draft),
borderRadius: BorderRadius.circular(8), title: Text('addFile'.tr()),
onTap: () => pickFile(),
), ),
leading: const Icon(Symbols.draft),
title: Text('addFile'.tr()),
onTap: () => pickFile(),
),
], ],
), ),
), ),

View File

@@ -30,6 +30,8 @@ class SheetScaffold extends StatelessWidget {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
letterSpacing: -0.5, letterSpacing: -0.5,
), ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
); );
return Container( return Container(
@@ -43,7 +45,7 @@ class SheetScaffold extends StatelessWidget {
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12), padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
child: Row( child: Row(
children: [ children: [
titleWidget, Expanded(child: titleWidget),
const Spacer(), const Spacer(),
...actions, ...actions,
IconButton( IconButton(