parent
2041b81141
commit
4b329b4c6f
8 changed files with 1115 additions and 119 deletions
|
|
@ -271,28 +271,30 @@ class const Composer(
|
|||
onPressed: () => context.showAdaptiveSheet(
|
||||
(context) => EmojiPicker(
|
||||
roomId: roomId,
|
||||
onSelection: (value, {shortcode}) {
|
||||
Navigator.of(context).pop();
|
||||
if (shortcode == null) {
|
||||
controller.text += value;
|
||||
} else {
|
||||
final text = "${controller.text}:";
|
||||
controller.value = .new(
|
||||
text: text,
|
||||
selection: .collapsed(
|
||||
offset: text.length,
|
||||
),
|
||||
);
|
||||
controller.addTag(
|
||||
id: ImagePack.emojiMarkdown(
|
||||
shortcode,
|
||||
value,
|
||||
),
|
||||
name: shortcode,
|
||||
);
|
||||
}
|
||||
node?.requestFocus();
|
||||
},
|
||||
onSelection:
|
||||
(value, {shortcode, description}) {
|
||||
Navigator.of(context).pop();
|
||||
if (shortcode == null) {
|
||||
controller.text += value;
|
||||
} else {
|
||||
final text = "${controller.text}:";
|
||||
controller.value = .new(
|
||||
text: text,
|
||||
selection: .collapsed(
|
||||
offset: text.length,
|
||||
),
|
||||
);
|
||||
controller.addTag(
|
||||
id: ImagePack.emojiMarkdown(
|
||||
shortcode,
|
||||
value,
|
||||
description: description,
|
||||
),
|
||||
name: shortcode,
|
||||
);
|
||||
}
|
||||
node?.requestFocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
icon: Icon(Icons.emoji_emotions),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
|
|||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/controllers/all_emoji.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/models/content/image_pack.dart";
|
||||
import "package:nexus/models/image_pack.dart";
|
||||
|
||||
class const EmojiOverlay(
|
||||
|
|
@ -39,6 +40,10 @@ class const EmojiOverlay(
|
|||
? ImagePack.emojiMarkdown(
|
||||
emoji.aliases.first,
|
||||
emoji.value,
|
||||
description: switch (emoji.data) {
|
||||
PackImage(:final body) => body,
|
||||
_ => null,
|
||||
},
|
||||
)
|
||||
: emoji.value,
|
||||
name: emoji.aliases.first,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,12 @@ import "package:nexus/models/content/image_pack.dart";
|
|||
class const EmojiPicker({
|
||||
required final String roomId,
|
||||
|
||||
required final FutureOr<void> Function(String value, {String? shortcode})
|
||||
/// Has the [shortcode] and [description] of images from packs.
|
||||
required final FutureOr<void> Function(
|
||||
String value, {
|
||||
String? shortcode,
|
||||
String? description,
|
||||
})
|
||||
onSelection,
|
||||
final bool allowFreeText = false,
|
||||
super.key,
|
||||
|
|
@ -41,11 +46,14 @@ class const EmojiPicker({
|
|||
|
||||
return upstream.EmojiPicker(
|
||||
onSelection: (emoji) async {
|
||||
final image = switch (emoji.data) {
|
||||
final PackImage image => image,
|
||||
_ => null,
|
||||
};
|
||||
await onSelection(
|
||||
emoji.value,
|
||||
shortcode: emoji.data is PackImage
|
||||
? emoji.aliases.first
|
||||
: null,
|
||||
shortcode: image == null ? null : emoji.aliases.first,
|
||||
description: image?.body,
|
||||
);
|
||||
|
||||
await ref
|
||||
|
|
|
|||
14
lib/widgets/image_pack_avatar.dart
Normal file
14
lib/widgets/image_pack_avatar.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/widgets/mxc_emoji.dart";
|
||||
|
||||
class const ImagePackAvatar(final Uri? icon, {super.key})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox.square(
|
||||
dimension: 40,
|
||||
child: switch (icon) {
|
||||
final icon? => MxcEmoji(icon),
|
||||
null => Icon(Icons.emoji_emotions),
|
||||
},
|
||||
);
|
||||
}
|
||||
118
lib/widgets/settings/enable_image_packs_sheet.dart
Normal file
118
lib/widgets/settings/enable_image_packs_sheet.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import "package:collection/collection.dart";
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/controllers/room_image_packs.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/models/account_data.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
import "package:nexus/widgets/image_pack_avatar.dart";
|
||||
import "package:nexus/widgets/sheet_layout.dart";
|
||||
|
||||
/// Chooses a room, then which of its image packs to use in every room.
|
||||
class const EnableImagePacksSheet({
|
||||
required final ImagePackRooms enabled,
|
||||
required final void Function(ImagePackRooms enabled) onSave,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final chosen = useState(enabled);
|
||||
final roomId = useState<String?>(null);
|
||||
final rooms = ref.watch(
|
||||
RoomsController.provider.select(
|
||||
(rooms) => rooms.values
|
||||
.map((room) => room.metadata)
|
||||
.nonNulls
|
||||
.map(
|
||||
(metadata) => (
|
||||
id: metadata.id,
|
||||
name: metadata.name ?? metadata.id,
|
||||
avatar: metadata.avatar,
|
||||
),
|
||||
)
|
||||
.sortedBy((room) => room.name.toLowerCase())
|
||||
.toIList(),
|
||||
),
|
||||
);
|
||||
|
||||
return SheetLayout(
|
||||
title: Text("Add Image Packs"),
|
||||
actions: [
|
||||
TextButton(onPressed: Navigator.of(context).pop, child: Text("Cancel")),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onSave(chosen.value);
|
||||
},
|
||||
child: Text("Save"),
|
||||
),
|
||||
],
|
||||
children: [
|
||||
Text("Packs you add can be used in every room."),
|
||||
DropdownMenu(
|
||||
expandedInsets: .zero,
|
||||
menuHeight: 300,
|
||||
enableFilter: true,
|
||||
requestFocusOnTap: true,
|
||||
label: Text("Room"),
|
||||
onSelected: (id) => roomId.value = id,
|
||||
dropdownMenuEntries: rooms
|
||||
.map(
|
||||
(room) => DropdownMenuEntry(
|
||||
value: room.id,
|
||||
label: room.name,
|
||||
leadingIcon: AvatarOrHash(
|
||||
room.avatar,
|
||||
room.name,
|
||||
fallback: Icon(Icons.numbers),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: Durations.medium2,
|
||||
curve: Curves.easeInOut,
|
||||
alignment: .topCenter,
|
||||
child: switch (roomId.value) {
|
||||
final roomId? =>
|
||||
ref
|
||||
.watch(RoomImagePacksController.provider(roomId))
|
||||
.betterWhen(
|
||||
data: (packs) => packs.isEmpty
|
||||
? Text("This room has no image packs")
|
||||
: Column(
|
||||
mainAxisSize: .min,
|
||||
children: packs.entries
|
||||
.map(
|
||||
(entry) => CheckboxListTile(
|
||||
secondary: ImagePackAvatar(
|
||||
entry.value.icon,
|
||||
),
|
||||
title: Text(entry.value.name),
|
||||
subtitle: Text(entry.value.imageCountLabel),
|
||||
value: chosen.value.contains(
|
||||
roomId,
|
||||
entry.key,
|
||||
),
|
||||
onChanged: (value) =>
|
||||
chosen.value = chosen.value.withPack(
|
||||
roomId,
|
||||
entry.key,
|
||||
enabled: value ?? false,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
null => const SizedBox(width: .infinity),
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
250
lib/widgets/settings/image_pack_sheet.dart
Normal file
250
lib/widgets/settings/image_pack_sheet.dart
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
|
||||
import "package:nexus/helpers/file_dialog_helper.dart";
|
||||
import "package:nexus/helpers/hooks/saving.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/models/content/image_pack.dart";
|
||||
import "package:nexus/models/content/message.dart";
|
||||
import "package:nexus/widgets/image_pack_avatar.dart";
|
||||
import "package:nexus/widgets/mxc_emoji.dart";
|
||||
import "package:nexus/widgets/settings/image_usage_chips.dart";
|
||||
import "package:nexus/widgets/settings/pack_image_sheet.dart";
|
||||
import "package:nexus/widgets/sheet_layout.dart";
|
||||
|
||||
/// Edits an image pack, or only shows it if [onSave] is null.
|
||||
class const ImagePackSheet({
|
||||
required final String title,
|
||||
required final ImagePackContent content,
|
||||
required final void Function(ImagePackContent content)? onSave,
|
||||
final VoidCallback? onDeleted,
|
||||
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final editable = onSave != null;
|
||||
final nameController = useTextEditingController(
|
||||
text: content.pack?.displayName,
|
||||
);
|
||||
final name = useValueListenable(nameController).text.trim();
|
||||
final avatar = useState(content.pack?.avatarUrl);
|
||||
final usage = useState(content.pack?.usage ?? const ISet.empty());
|
||||
final images = useState(content.images);
|
||||
final uploading = Saving.use();
|
||||
final packUsage = usage.value.isEmpty
|
||||
? ISet(ImageUsage.values)
|
||||
: usage.value;
|
||||
final valid = !uploading.active && name.isNotEmpty;
|
||||
|
||||
Future<void> upload(
|
||||
void Function(String filename, PackImage image) onUploaded,
|
||||
) async {
|
||||
final file = await ref
|
||||
.read(FileDialogHelper.provider)
|
||||
.pickFile(images: true);
|
||||
if (file == null || !context.mounted) return;
|
||||
|
||||
await uploading.run(() async {
|
||||
final content = await ref
|
||||
.read(ClientController.provider.notifier)
|
||||
.uploadMedia(.new(path: file.path, encrypt: false));
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (content case ImageMessageContent(:final url, :final info)) {
|
||||
onUploaded(file.name, PackImage(url: url, info: info));
|
||||
} else {
|
||||
throw Exception("The file isn't an image");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void editImage(String shortcode, PackImage image, {bool isNew = false}) =>
|
||||
context.showAdaptiveSheet(
|
||||
(_) => PackImageSheet(
|
||||
title: isNew
|
||||
? "Add Image"
|
||||
: editable
|
||||
? "Edit Image"
|
||||
: ":$shortcode:",
|
||||
shortcode: shortcode,
|
||||
image: image,
|
||||
taken: isNew
|
||||
? images.value.keys.toISet()
|
||||
: images.value.keys.toISet().remove(shortcode),
|
||||
packUsage: packUsage,
|
||||
onSave: editable
|
||||
? (newShortcode, newImage) => images.value = isNew
|
||||
? images.value.add(newShortcode, newImage)
|
||||
// Replaced in place, to keep the images' order
|
||||
: .fromEntries(
|
||||
images.value.entries.map(
|
||||
(entry) => entry.key == shortcode
|
||||
? MapEntry(newShortcode, newImage)
|
||||
: entry,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onRemoved: editable && !isNew
|
||||
? () => images.value = images.value.remove(shortcode)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> delete() async {
|
||||
final confirmed = await context.showAdaptiveSheet<bool>(
|
||||
(context) => SheetLayout(
|
||||
title: Text("Delete Pack"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: Text("Delete"),
|
||||
),
|
||||
],
|
||||
children: [
|
||||
Text(
|
||||
"Delete this pack? Its images will no longer be available here, or to anyone using it in other rooms.",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
Navigator.of(context).pop();
|
||||
onDeleted?.call();
|
||||
}
|
||||
|
||||
void save() {
|
||||
Navigator.of(context).pop();
|
||||
onSave!(
|
||||
content.copyWith(
|
||||
images: images.value,
|
||||
pack: (content.pack ?? const PackInfo()).copyWith(
|
||||
displayName: name,
|
||||
avatarUrl: avatar.value,
|
||||
usage: usage.value,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return SheetLayout(
|
||||
title: Text(title),
|
||||
actions: [
|
||||
if (onDeleted != null)
|
||||
TextButton(
|
||||
onPressed: uploading.active ? null : delete,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: Text("Delete"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: Text(editable ? "Cancel" : "Close"),
|
||||
),
|
||||
if (editable)
|
||||
TextButton(onPressed: valid ? save : null, child: Text("Save")),
|
||||
],
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
enabled: editable,
|
||||
autofocus: true,
|
||||
decoration: .new(labelText: "Name"),
|
||||
),
|
||||
ListTile(
|
||||
enabled: editable && !uploading.active,
|
||||
leading: Icon(Icons.image),
|
||||
title: Text("Avatar"),
|
||||
subtitle: Text("The pack's first image if not set."),
|
||||
trailing: Row(
|
||||
mainAxisSize: .min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
ImagePackAvatar(
|
||||
avatar.value ?? images.value.values.firstOrNull?.url,
|
||||
),
|
||||
if (avatar.value != null && editable)
|
||||
IconButton(
|
||||
tooltip: "Remove avatar",
|
||||
icon: Icon(Icons.delete),
|
||||
onPressed: uploading.active
|
||||
? null
|
||||
: () => avatar.value = null,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () =>
|
||||
upload((_, image) => avatar.value = image.url).onError(showError),
|
||||
),
|
||||
Text("Default usage", style: textTheme.titleSmall),
|
||||
ImageUsageChips(
|
||||
value: packUsage,
|
||||
onChanged: editable
|
||||
? (value) =>
|
||||
usage.value = value.length == ImageUsage.values.length
|
||||
? const .empty()
|
||||
: value
|
||||
: null,
|
||||
),
|
||||
Text("Images", style: textTheme.titleSmall),
|
||||
if (images.value.isEmpty && !editable) Text("No images"),
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
...images.value.entries.map(
|
||||
(entry) => Tooltip(
|
||||
message: ":${entry.key}:",
|
||||
child: InkWell(
|
||||
borderRadius: .circular(8),
|
||||
onTap: () => editImage(entry.key, entry.value),
|
||||
child: Padding(
|
||||
padding: .all(4),
|
||||
child: SizedBox.square(
|
||||
dimension: 56,
|
||||
child: MxcEmoji(entry.value.url, label: entry.key),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (editable)
|
||||
SizedBox.square(
|
||||
dimension: 64,
|
||||
child: uploading.active
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: IconButton(
|
||||
tooltip: "Add image",
|
||||
icon: Icon(Icons.add_photo_alternate),
|
||||
onPressed: () => upload(
|
||||
(filename, image) => editImage(
|
||||
filename
|
||||
.split(".")
|
||||
.first
|
||||
.replaceAll(RegExp(r"[^\w-]"), ""),
|
||||
image,
|
||||
isNew: true,
|
||||
),
|
||||
).onError(showError),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib/widgets/settings/image_usage_chips.dart
Normal file
38
lib/widgets/settings/image_usage_chips.dart
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/models/content/image_pack.dart";
|
||||
|
||||
/// Chooses what images are used as, keeping at least one usage chosen.
|
||||
class const ImageUsageChips({
|
||||
required final ISet<ImageUsage> value,
|
||||
|
||||
/// Disables the chips if null.
|
||||
required final void Function(ISet<ImageUsage> value)? onChanged,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
static String nameOf(ImageUsage usage) => switch (usage) {
|
||||
.emoticon => "Emoticons",
|
||||
.sticker => "Stickers",
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: ImageUsage.values
|
||||
.map(
|
||||
(usage) => FilterChip(
|
||||
label: Text(nameOf(usage)),
|
||||
selected: value.contains(usage),
|
||||
onSelected:
|
||||
onChanged == null ||
|
||||
(value.length == 1 && value.contains(usage))
|
||||
? null
|
||||
: (selected) => onChanged!(
|
||||
selected ? value.add(usage) : value.remove(usage),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
134
lib/widgets/settings/pack_image_sheet.dart
Normal file
134
lib/widgets/settings/pack_image_sheet.dart
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/models/content/image_pack.dart";
|
||||
import "package:nexus/widgets/mxc_emoji.dart";
|
||||
import "package:nexus/widgets/settings/image_usage_chips.dart";
|
||||
import "package:nexus/widgets/sheet_layout.dart";
|
||||
|
||||
/// Edits an image in a pack, or only shows it if [onSave] is null.
|
||||
class const PackImageSheet({
|
||||
required final String title,
|
||||
required final String shortcode,
|
||||
required final PackImage image,
|
||||
|
||||
/// The other images' shortcodes, which this one can't also have.
|
||||
required final ISet<String> taken,
|
||||
|
||||
/// The pack's default usage, which images have unless they set their own.
|
||||
required final ISet<ImageUsage> packUsage,
|
||||
required final void Function(String shortcode, PackImage image)? onSave,
|
||||
final VoidCallback? onRemoved,
|
||||
super.key,
|
||||
}) extends HookWidget {
|
||||
static final shortcodePattern = RegExp(r"^[\w-]+$");
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shortcodeController = useTextEditingController(text: shortcode);
|
||||
final bodyController = useTextEditingController(text: image.body);
|
||||
final usage = useState(image.usage);
|
||||
final newShortcode = useValueListenable(shortcodeController).text.trim();
|
||||
final error = taken.contains(newShortcode)
|
||||
? "Another image has this shortcode"
|
||||
: newShortcode.isNotEmpty && !shortcodePattern.hasMatch(newShortcode)
|
||||
? "Only letters, numbers, - and _ are allowed"
|
||||
: null;
|
||||
final valid = newShortcode.isNotEmpty && error == null;
|
||||
|
||||
void save() {
|
||||
final body = bodyController.text.trim();
|
||||
Navigator.of(context).pop();
|
||||
onSave!(
|
||||
newShortcode,
|
||||
image.copyWith(body: body.isEmpty ? null : body, usage: usage.value),
|
||||
);
|
||||
}
|
||||
|
||||
return SheetLayout(
|
||||
title: Text(title),
|
||||
actions: [
|
||||
if (onRemoved case final onRemoved?)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onRemoved();
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: Text("Remove"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: Text(onSave == null ? "Close" : "Cancel"),
|
||||
),
|
||||
if (onSave != null)
|
||||
TextButton(onPressed: valid ? save : null, child: Text("Save")),
|
||||
],
|
||||
children: [
|
||||
Center(
|
||||
child: SizedBox.square(
|
||||
dimension: 128,
|
||||
child: MxcEmoji(image.url, label: shortcode),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: shortcodeController,
|
||||
enabled: onSave != null,
|
||||
onSubmitted: valid ? (_) => save() : null,
|
||||
decoration: .new(
|
||||
labelText: "Shortcode",
|
||||
prefixText: ":",
|
||||
suffixText: ":",
|
||||
errorText: error,
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: bodyController,
|
||||
enabled: onSave != null,
|
||||
onSubmitted: valid ? (_) => save() : null,
|
||||
decoration: .new(
|
||||
labelText: "Description",
|
||||
hintText: "Read by screen readers",
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .stretch,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: Text("Custom usage"),
|
||||
subtitle: Text(
|
||||
usage.value.isEmpty
|
||||
? "Same as the pack's default (${packUsage.map(ImageUsageChips.nameOf).join(", ")})"
|
||||
: "Used as chosen below, instead of the pack's default",
|
||||
),
|
||||
value: usage.value.isNotEmpty,
|
||||
onChanged: onSave == null
|
||||
? null
|
||||
: (custom) =>
|
||||
usage.value = custom ? packUsage : const .empty(),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: Durations.medium2,
|
||||
curve: Curves.easeInOut,
|
||||
alignment: .topCenter,
|
||||
child: usage.value.isEmpty
|
||||
? const SizedBox(width: .infinity)
|
||||
: Padding(
|
||||
padding: .only(top: 8),
|
||||
child: ImageUsageChips(
|
||||
value: usage.value,
|
||||
onChanged: onSave == null
|
||||
? null
|
||||
: (value) => usage.value = value,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue