♻️ Better image loading animation and more commonly used blurhash

This commit is contained in:
2026-01-02 18:32:37 +08:00
parent f1f5113b01
commit 78c1a284a5
44 changed files with 2043 additions and 2185 deletions

View File

@@ -74,7 +74,7 @@ class AccountScreen extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (user.value?.profile.background?.id != null)
if (user.value?.profile.background != null)
Stack(
clipBehavior: Clip.none,
children: [
@@ -112,7 +112,7 @@ class AccountScreen extends HookConsumerWidget {
Builder(
builder: (context) {
final hasBackground =
user.value?.profile.background?.id != null;
user.value?.profile.background != null;
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: hasBackground ? 0 : 16,

View File

@@ -74,14 +74,10 @@ class UpdateProfileScreen extends HookConsumerWidget {
submitting.value = true;
try {
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(data: result, type: UniversalFileType.image),
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
@@ -188,8 +184,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
if (usernameColorType.value == 'gradient') ...{
if (usernameColorDirection.text.isNotEmpty)
'direction': usernameColorDirection.text,
'colors':
usernameColorColors.value.where((c) => c.isNotEmpty).toList(),
'colors': usernameColorColors.value
.where((c) => c.isNotEmpty)
.toList(),
},
};
@@ -206,18 +203,16 @@ class UpdateProfileScreen extends HookConsumerWidget {
'time_zone': timeZoneController.text,
'birthday': birthday.value?.toUtc().toIso8601String(),
'username_color': usernameColorData,
'links':
links.value
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
.toList(),
'links': links.value
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
.toList(),
},
);
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser();
links.value =
links.value
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
.toList();
links.value = links.value
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
.toList();
} catch (err) {
showErrorAlert(err);
} finally {
@@ -244,13 +239,12 @@ class UpdateProfileScreen extends HookConsumerWidget {
GestureDetector(
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child:
user.value!.profile.background?.id != null
? CloudImageWidget(
fileId: user.value!.profile.background!.id,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
child: user.value!.profile.background != null
? CloudImageWidget(
file: user.value!.profile.background,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
updateProfilePicture('background');
@@ -261,7 +255,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: user.value!.profile.picture?.id,
file: user.value!.profile.picture,
radius: 40,
),
onTap: () {
@@ -291,14 +285,14 @@ class UpdateProfileScreen extends HookConsumerWidget {
),
controller: usernameController,
readOnly: true,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
TextFormField(
decoration: InputDecoration(labelText: 'nickname'.tr()),
controller: nicknameController,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
DropdownButtonFormField2<String>(
decoration: InputDecoration(
@@ -385,9 +379,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
labelText: 'firstName'.tr(),
),
controller: firstNameController,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
Expanded(
@@ -396,9 +389,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
labelText: 'middleName'.tr(),
),
controller: middleNameController,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
Expanded(
@@ -407,9 +399,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
labelText: 'lastName'.tr(),
),
controller: lastNameController,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
],
@@ -423,8 +414,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
maxLines: null,
minLines: 3,
controller: bioController,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
Row(
spacing: 16,
@@ -445,33 +436,34 @@ class UpdateProfileScreen extends HookConsumerWidget {
onSelected: (String selection) {
genderController.text = selection;
},
fieldViewBuilder: (
context,
controller,
focusNode,
onFieldSubmitted,
) {
// Initialize the controller with the current value
if (controller.text.isEmpty &&
genderController.text.isNotEmpty) {
controller.text = genderController.text;
}
fieldViewBuilder:
(
context,
controller,
focusNode,
onFieldSubmitted,
) {
// Initialize the controller with the current value
if (controller.text.isEmpty &&
genderController.text.isNotEmpty) {
controller.text = genderController.text;
}
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'gender'.tr(),
),
onChanged: (value) {
genderController.text = value;
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'gender'.tr(),
),
onChanged: (value) {
genderController.text = value;
},
onTapOutside: (_) => FocusManager
.instance
.primaryFocus
?.unfocus(),
);
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
);
},
),
),
Expanded(
@@ -480,9 +472,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
labelText: 'pronouns'.tr(),
),
controller: pronounsController,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
],
@@ -496,9 +487,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
labelText: 'location'.tr(),
),
controller: locationController,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
Expanded(
@@ -507,8 +497,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
if (textEditingValue.text.isEmpty) {
return const Iterable<String>.empty();
}
final lowercaseQuery =
textEditingValue.text.toLowerCase();
final lowercaseQuery = textEditingValue.text
.toLowerCase();
return getAvailableTz().where((tz) {
return tz.toLowerCase().contains(lowercaseQuery);
});
@@ -516,46 +506,49 @@ class UpdateProfileScreen extends HookConsumerWidget {
onSelected: (String selection) {
timeZoneController.text = selection;
},
fieldViewBuilder: (
context,
controller,
focusNode,
onFieldSubmitted,
) {
// Sync the controller with timeZoneController when the widget is built
if (controller.text != timeZoneController.text) {
controller.text = timeZoneController.text;
}
fieldViewBuilder:
(
context,
controller,
focusNode,
onFieldSubmitted,
) {
// Sync the controller with timeZoneController when the widget is built
if (controller.text !=
timeZoneController.text) {
controller.text = timeZoneController.text;
}
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'timeZone'.tr(),
suffix: InkWell(
child: const Icon(
Symbols.my_location,
size: 18,
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'timeZone'.tr(),
suffix: InkWell(
child: const Icon(
Symbols.my_location,
size: 18,
),
onTap: () async {
try {
showLoadingModal(context);
final machineTz =
await getMachineTz();
controller.text = machineTz;
timeZoneController.text = machineTz;
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
}
},
),
),
onTap: () async {
try {
showLoadingModal(context);
final machineTz = await getMachineTz();
controller.text = machineTz;
timeZoneController.text = machineTz;
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
}
onChanged: (value) {
timeZoneController.text = value;
},
),
),
onChanged: (value) {
timeZoneController.text = value;
);
},
);
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
@@ -569,21 +562,21 @@ class UpdateProfileScreen extends HookConsumerWidget {
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: options.length,
itemBuilder: (
BuildContext context,
int index,
) {
final option = options.elementAt(index);
return ListTile(
title: Text(
option,
overflow: TextOverflow.ellipsis,
),
onTap: () {
onSelected(option);
itemBuilder:
(BuildContext context, int index) {
final option = options.elementAt(
index,
);
return ListTile(
title: Text(
option,
overflow: TextOverflow.ellipsis,
),
onTap: () {
onSelected(option);
},
);
},
);
},
),
),
),
@@ -644,10 +637,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHighest,
color: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Column(
@@ -664,25 +656,23 @@ class UpdateProfileScreen extends HookConsumerWidget {
type: usernameColorType.value,
value:
usernameColorType.value == 'plain' &&
usernameColorValue
.text
.isNotEmpty
? usernameColorValue.text
: null,
usernameColorValue.text.isNotEmpty
? usernameColorValue.text
: null,
direction:
usernameColorType.value ==
'gradient' &&
usernameColorDirection
.text
.isNotEmpty
? usernameColorDirection.text
: null,
'gradient' &&
usernameColorDirection
.text
.isNotEmpty
? usernameColorDirection.text
: null,
colors:
usernameColorType.value == 'gradient'
? usernameColorColors.value
.where((c) => c.isNotEmpty)
.toList()
: null,
? usernameColorColors.value
.where((c) => c.isNotEmpty)
.toList()
: null,
),
),
);
@@ -724,10 +714,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
? Symbols.check_circle
: Symbols.error,
size: 16,
color:
canUseColor
? Colors.green
: Colors.red,
color: canUseColor
? Colors.green
: Colors.red,
),
const Gap(4),
Text(
@@ -736,10 +725,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
: 'upgradeRequired'.tr(),
style: TextStyle(
fontSize: 12,
color:
canUseColor
? Colors.green
: Colors.red,
color: canUseColor
? Colors.green
: Colors.red,
),
),
],
@@ -792,34 +780,35 @@ class UpdateProfileScreen extends HookConsumerWidget {
onSelected: (String selection) {
usernameColorValue.text = selection;
},
fieldViewBuilder: (
context,
controller,
focusNode,
onFieldSubmitted,
) {
// Initialize the controller with the current value
if (controller.text.isEmpty &&
usernameColorValue.text.isNotEmpty) {
controller.text = usernameColorValue.text;
}
fieldViewBuilder:
(
context,
controller,
focusNode,
onFieldSubmitted,
) {
// Initialize the controller with the current value
if (controller.text.isEmpty &&
usernameColorValue.text.isNotEmpty) {
controller.text = usernameColorValue.text;
}
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'colorValue'.tr(),
hintText: 'e.g. red or #ff6600',
),
onChanged: (value) {
usernameColorValue.text = value;
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'colorValue'.tr(),
hintText: 'e.g. red or #ff6600',
),
onChanged: (value) {
usernameColorValue.text = value;
},
onTapOutside: (_) => FocusManager
.instance
.primaryFocus
?.unfocus(),
);
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
);
},
),
if (usernameColorType.value == 'gradient') ...[
DropdownButtonFormField2<String>(
@@ -862,10 +851,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
child: Text('gradientDirectionToTopLeft'.tr()),
),
],
value:
usernameColorDirection.text.isNotEmpty
? usernameColorDirection.text
: 'to right',
value: usernameColorDirection.text.isNotEmpty
? usernameColorDirection.text
: 'to right',
onChanged: (value) {
usernameColorDirection.text = value ?? 'to right';
},
@@ -911,21 +899,19 @@ class UpdateProfileScreen extends HookConsumerWidget {
onChanged: (value) {
usernameColorColors.value[i] = value;
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
onTapOutside: (_) => FocusManager
.instance
.primaryFocus
?.unfocus(),
),
),
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
usernameColorColors.value =
usernameColorColors.value
.whereIndexed(
(idx, _) => idx != i,
)
.toList();
usernameColorColors
.value = usernameColorColors.value
.whereIndexed((idx, _) => idx != i)
.toList();
},
),
],
@@ -968,10 +954,10 @@ class UpdateProfileScreen extends HookConsumerWidget {
name: value,
);
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
onTapOutside: (_) => FocusManager
.instance
.primaryFocus
?.unfocus(),
),
),
const Gap(8),
@@ -987,19 +973,18 @@ class UpdateProfileScreen extends HookConsumerWidget {
url: value,
);
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
onTapOutside: (_) => FocusManager
.instance
.primaryFocus
?.unfocus(),
),
),
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
links.value =
links.value
.whereIndexed((idx, _) => idx != i)
.toList();
links.value = links.value
.whereIndexed((idx, _) => idx != i)
.toList();
},
),
],

View File

@@ -57,7 +57,7 @@ class _AccountBasicInfo extends StatelessWidget {
return Card(
child: Builder(
builder: (context) {
final hasBackground = data.profile.background?.id != null;
final hasBackground = data.profile.background != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -962,7 +962,7 @@ class AccountProfileScreen extends HookConsumerWidget {
flexibleSpace: Stack(
children: [
Positioned.fill(
child: data.profile.background?.id != null
child: data.profile.background != null
? CloudImageWidget(
file: data.profile.background,
)

View File

@@ -113,7 +113,7 @@ class RelationshipListTile extends StatelessWidget {
contentPadding: const EdgeInsets.only(left: 16, right: 12),
leading: AccountPfcGestureDetector(
uname: account.name,
child: ProfilePictureWidget(fileId: account.profile.picture?.id),
child: ProfilePictureWidget(file: account.profile.picture),
),
title: Row(
spacing: 6,

View File

@@ -178,7 +178,7 @@ class EditChatScreen extends HookConsumerWidget {
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
file: picture.value,
radius: 40,
fallbackIcon: Symbols.group,
),

View File

@@ -98,15 +98,15 @@ class PublicRoomPreview extends HookConsumerWidget {
SizedBox(
height: 26,
width: 26,
child: (room.type == 1 && room.picture?.id == null)
child: (room.type == 1 && room.picture == null)
? SplitAvatarWidget(
filesId: room.members!
.map((e) => e.account.profile.picture?.id)
files: room.members!
.map((e) => e.account.profile.picture)
.toList(),
)
: room.picture?.id != null
: room.picture != null
? ProfilePictureWidget(
fileId: room.picture?.id,
file: room.picture,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
@@ -131,15 +131,15 @@ class PublicRoomPreview extends HookConsumerWidget {
SizedBox(
height: 26,
width: 26,
child: (room.type == 1 && room.picture?.id == null)
child: (room.type == 1 && room.picture == null)
? SplitAvatarWidget(
filesId: room.members!
.map((e) => e.account.profile.picture?.id)
files: room.members!
.map((e) => e.account.profile.picture)
.toList(),
)
: room.picture?.id != null
: room.picture != null
? ProfilePictureWidget(
fileId: room.picture?.id,
file: room.picture,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(

View File

@@ -427,15 +427,15 @@ class ChatRoomScreen extends HookConsumerWidget {
child: SizedBox(
height: 26,
width: 26,
child: (room!.type == 1 && room.picture?.id == null)
child: (room!.type == 1 && room.picture == null)
? SplitAvatarWidget(
filesId: getValidMembers(
files: getValidMembers(
room.members!,
).map((e) => e.account.profile.picture?.id).toList(),
).map((e) => e.account.profile.picture).toList(),
)
: room.picture?.id != null
: room.picture != null
? ProfilePictureWidget(
fileId: room.picture?.id,
file: room.picture,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
@@ -473,15 +473,15 @@ class ChatRoomScreen extends HookConsumerWidget {
child: SizedBox(
height: 28,
width: 28,
child: (room!.type == 1 && room.picture?.id == null)
child: (room!.type == 1 && room.picture == null)
? SplitAvatarWidget(
filesId: getValidMembers(
files: getValidMembers(
room.members!,
).map((e) => e.account.profile.picture?.id).toList(),
).map((e) => e.account.profile.picture).toList(),
)
: room.picture?.id != null
: room.picture != null
? ProfilePictureWidget(
fileId: room.picture?.id,
file: room.picture,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(

View File

@@ -279,9 +279,8 @@ class ChatDetailScreen extends HookConsumerWidget {
leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar(
background:
(currentRoom!.type == 1 &&
currentRoom.background?.id != null)
? CloudImageWidget(fileId: currentRoom.background!.id)
(currentRoom!.type == 1 && currentRoom.background != null)
? CloudImageWidget(file: currentRoom.background!)
: (currentRoom.type == 1 &&
currentRoom.members!.length == 1 &&
currentRoom
@@ -293,17 +292,16 @@ class ChatDetailScreen extends HookConsumerWidget {
?.id !=
null)
? CloudImageWidget(
fileId: currentRoom
file: currentRoom
.members!
.first
.account
.profile
.background!
.id,
.background!,
)
: currentRoom.background?.id != null
: currentRoom.background != null
? CloudImageWidget(
fileId: currentRoom.background!.id,
file: currentRoom.background!,
fit: BoxFit.cover,
)
: Container(
@@ -702,7 +700,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
leading: AccountPfcGestureDetector(
uname: member.account.name,
child: ProfilePictureWidget(
fileId: member.account.profile.picture?.id,
file: member.account.profile.picture,
),
),
title: Row(

View File

@@ -155,7 +155,7 @@ class PublisherSelector extends StatelessWidget {
if (isReadOnly || currentPublisher == null) {
return ProfilePictureWidget(
radius: 16,
fileId: currentPublisher?.picture?.id,
file: currentPublisher?.picture,
).center().padding(right: 8);
}
@@ -179,7 +179,7 @@ class PublisherSelector extends StatelessWidget {
.map(
(e) => ProfilePictureWidget(
radius: 16,
fileId: e.value?.picture?.id,
file: e.value?.picture,
).center().padding(right: 8),
)
.toList();
@@ -355,10 +355,7 @@ class CreatorHubScreen extends HookConsumerWidget {
value: item,
child: ListTile(
minTileHeight: 48,
leading: ProfilePictureWidget(
radius: 16,
fileId: item.picture?.id,
),
leading: ProfilePictureWidget(radius: 16, file: item.picture),
title: Text(item.nick),
subtitle: Text('@${item.name}'),
trailing: currentPublisher.value?.id == item.id
@@ -889,7 +886,7 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
file: member.account!.profile.picture,
),
title: Row(
spacing: 6,
@@ -1137,7 +1134,7 @@ class _PublisherInviteSheet extends HookConsumerWidget {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.publisher!.picture?.id,
file: invite.publisher!.picture,
fallbackIcon: Symbols.group,
),
title: Text(invite.publisher!.nick),

View File

@@ -69,157 +69,141 @@ class StickerPackDetailContent extends HookConsumerWidget {
}
return pack.when(
data:
(pack) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
data: (pack) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
Text(pack!.description),
Row(
spacing: 4,
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),
const Icon(Symbols.folder, size: 16),
Text(
'${packContent.value?.length ?? 0}/24',
style: GoogleFonts.robotoMono(),
),
],
).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,
),
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);
},
),
],
).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,
),
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}:',
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
child: Container(
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.surfaceContainer,
borderRadius: BorderRadius.all(
Radius.circular(8),
),
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,
),
),
child: CloudImageWidget(
fileId: sticker.image.id,
fit: BoxFit.contain,
),
),
),
);
},
).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(
file: sticker.image,
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(),
],
),
error: (err, _) =>
Text('Error: $err').textAlignment(TextAlign.center).center(),
loading: () => const CircularProgressIndicator().center(),
);
}
@@ -241,65 +225,60 @@ class StickerPackActionMenu extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return PopupMenuButton(
icon: Icon(Icons.more_vert, shadows: [iconShadow]),
itemBuilder:
(context) => [
PopupMenuItem(
onTap: () {
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: [
Icon(
Icons.edit,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
const Gap(12),
const Text('editStickerPack').tr(),
],
itemBuilder: (context) => [
PopupMenuItem(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => SheetScaffold(
titleText: 'editStickerPack'.tr(),
child: StickerPackForm(pubName: pubName, packId: packId),
),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.delete, color: Colors.red),
const Gap(12),
const Text(
'deleteStickerPack',
style: TextStyle(color: Colors.red),
).tr(),
],
).then((value) {
if (value != null) {
ref.invalidate(stickerPackProvider(packId));
}
});
},
child: Row(
children: [
Icon(
Icons.edit,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
onTap: () {
showConfirmAlert(
'deleteStickerPackHint'.tr(),
'deleteStickerPack'.tr(),
isDanger: true,
).then((confirm) {
if (confirm) {
final client = ref.watch(apiClientProvider);
client.delete('/sphere/stickers/$packId');
ref.invalidate(stickerPacksProvider);
if (context.mounted) context.pop(true);
}
});
},
),
],
const Gap(12),
const Text('editStickerPack').tr(),
],
),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.delete, color: Colors.red),
const Gap(12),
const Text(
'deleteStickerPack',
style: TextStyle(color: Colors.red),
).tr(),
],
),
onTap: () {
showConfirmAlert(
'deleteStickerPackHint'.tr(),
'deleteStickerPack'.tr(),
isDanger: true,
).then((confirm) {
if (confirm) {
final client = ref.watch(apiClientProvider);
client.delete('/sphere/stickers/$packId');
ref.invalidate(stickerPacksProvider);
if (context.mounted) context.pop(true);
}
});
},
),
],
);
}
}
@@ -372,10 +351,9 @@ class StickerForm extends HookConsumerWidget {
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!),
),
),
),
@@ -383,10 +361,8 @@ class StickerForm extends HookConsumerWidget {
onPressed: () {
showModalBottomSheet(
context: context,
builder:
(context) => CloudFilePicker(
allowedTypes: {UniversalFileType.image},
),
builder: (context) =>
CloudFilePicker(allowedTypes: {UniversalFileType.image}),
).then((value) {
if (value == null) return;
image.value = value[0].id;
@@ -412,8 +388,8 @@ class StickerForm extends HookConsumerWidget {
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
],
),

View File

@@ -146,7 +146,7 @@ class _AppOverview extends StatelessWidget {
left: 20,
bottom: -32,
child: ProfilePictureWidget(
fileId: app.picture?.id,
file: app.picture,
radius: 40,
fallbackIcon: Symbols.apps,
),

View File

@@ -153,7 +153,7 @@ class CustomAppsScreen extends HookConsumerWidget {
ListTile(
title: Text(app.name),
leading: ProfilePictureWidget(
fileId: app.picture?.id,
file: app.picture,
fallbackIcon: Symbols.apps,
),
subtitle: Text(

View File

@@ -143,7 +143,7 @@ class _BotOverview extends StatelessWidget {
left: 20,
bottom: -32,
child: ProfilePictureWidget(
fileId: bot.account.profile.picture?.id,
file: bot.account.profile.picture,
radius: 40,
fallbackIcon: Symbols.smart_toy,
),

View File

@@ -51,10 +51,9 @@ class EditAppScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final app =
isNew
? null
: ref.watch(customAppProvider(publisherName, projectId, id!));
final app = isNew
? null
: ref.watch(customAppProvider(publisherName, projectId, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
@@ -139,14 +138,10 @@ class EditAppScreen extends HookConsumerWidget {
submitting.value = true;
try {
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(data: result, type: UniversalFileType.image),
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
@@ -169,41 +164,40 @@ class EditAppScreen extends HookConsumerWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'addScope'.tr(),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: scopeController,
decoration: InputDecoration(
labelText: 'scopeName'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
builder: (context) => SheetScaffold(
titleText: 'addScope'.tr(),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: scopeController,
decoration: InputDecoration(
labelText: 'scopeName'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
const SizedBox(height: 20),
FilledButton.tonalIcon(
onPressed: () {
if (scopeController.text.isNotEmpty) {
allowedScopes.value = [
...allowedScopes.value,
scopeController.text,
];
Navigator.pop(context);
}
},
icon: const Icon(Symbols.add),
label: Text('add').tr(),
),
],
),
),
),
const SizedBox(height: 20),
FilledButton.tonalIcon(
onPressed: () {
if (scopeController.text.isNotEmpty) {
allowedScopes.value = [
...allowedScopes.value,
scopeController.text,
];
Navigator.pop(context);
}
},
icon: const Icon(Symbols.add),
label: Text('add').tr(),
),
],
),
),
),
);
}
@@ -212,57 +206,56 @@ class EditAppScreen extends HookConsumerWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'addRedirectUri'.tr(),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: uriController,
decoration: InputDecoration(
labelText: 'redirectUri'.tr(),
hintText: 'https://example.com/auth/callback',
helperText: 'redirectUriHint'.tr(),
helperMaxLines: 3,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
keyboardType: TextInputType.url,
validator: (value) {
if (value == null || value.isEmpty) {
return 'uriRequired'.tr();
}
final uri = Uri.tryParse(value);
if (uri == null || !uri.hasAbsolutePath) {
return 'invalidUri'.tr();
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
builder: (context) => SheetScaffold(
titleText: 'addRedirectUri'.tr(),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: uriController,
decoration: InputDecoration(
labelText: 'redirectUri'.tr(),
hintText: 'https://example.com/auth/callback',
helperText: 'redirectUriHint'.tr(),
helperMaxLines: 3,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
const SizedBox(height: 20),
FilledButton.tonalIcon(
onPressed: () {
if (uriController.text.isNotEmpty) {
redirectUris.value = [
...redirectUris.value,
uriController.text,
];
Navigator.pop(context);
}
},
icon: const Icon(Symbols.add),
label: Text('add').tr(),
),
],
),
keyboardType: TextInputType.url,
validator: (value) {
if (value == null || value.isEmpty) {
return 'uriRequired'.tr();
}
final uri = Uri.tryParse(value);
if (uri == null || !uri.hasAbsolutePath) {
return 'invalidUri'.tr();
}
return null;
},
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
const SizedBox(height: 20),
FilledButton.tonalIcon(
onPressed: () {
if (uriController.text.isNotEmpty) {
redirectUris.value = [
...redirectUris.value,
uriController.text,
];
Navigator.pop(context);
}
},
icon: const Icon(Symbols.add),
label: Text('add').tr(),
),
],
),
),
),
);
}
@@ -275,31 +268,28 @@ class EditAppScreen extends HookConsumerWidget {
'picture_id': picture.value?.id,
'background_id': background.value?.id,
'links': {
'home_page':
homePageController.text.isNotEmpty
? homePageController.text
: null,
'privacy_policy':
privacyPolicyController.text.isNotEmpty
? privacyPolicyController.text
: null,
'terms_of_service':
termsController.text.isNotEmpty ? termsController.text : null,
'home_page': homePageController.text.isNotEmpty
? homePageController.text
: null,
'privacy_policy': privacyPolicyController.text.isNotEmpty
? privacyPolicyController.text
: null,
'terms_of_service': termsController.text.isNotEmpty
? termsController.text
: null,
},
'oauth_config':
oauthEnabled.value
? {
'redirect_uris': redirectUris.value,
'post_logout_redirect_uris':
postLogoutUris.value.isNotEmpty
? postLogoutUris.value
: null,
'allowed_scopes': allowedScopes.value,
'allowed_grant_types': allowedGrantTypes.value,
'require_pkce': requirePkce.value,
'allow_offline_access': allowOfflineAccess.value,
}
: null,
'oauth_config': oauthEnabled.value
? {
'redirect_uris': redirectUris.value,
'post_logout_redirect_uris': postLogoutUris.value.isNotEmpty
? postLogoutUris.value
: null,
'allowed_scopes': allowedScopes.value,
'allowed_grant_types': allowedGrantTypes.value,
'require_pkce': requirePkce.value,
'allow_offline_access': allowOfflineAccess.value,
}
: null,
};
try {
showLoadingModal(context);
@@ -326,287 +316,269 @@ class EditAppScreen extends HookConsumerWidget {
}
}
final bodyContent =
app == null && !isNew
? const Center(child: CircularProgressIndicator())
: app?.hasError == true && !isNew
? ResponseErrorWidget(
error: app!.error,
onRetry:
() => ref.invalidate(
customAppProvider(publisherName, projectId, id!),
),
)
: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
final bodyContent = app == null && !isNew
? const Center(child: CircularProgressIndicator())
: app?.hasError == true && !isNew
? ResponseErrorWidget(
error: app!.error,
onRetry: () => ref.invalidate(
customAppProvider(publisherName, projectId, id!),
),
)
: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color: Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child: background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
file: picture.value,
radius: 40,
fallbackIcon: Symbols.apps,
),
onTap: () {
setPicture('background');
setPicture('picture');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
radius: 40,
fallbackIcon: Symbols.apps,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(
labelText: 'name'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
maxLines: 3,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
ExpansionPanelList(
expansionCallback: (index, isExpanded) {
switch (index) {
case 0:
enableLinks.value = isExpanded;
break;
case 1:
oauthEnabled.value = isExpanded;
break;
}
},
children: [
ExpansionPanel(
headerBuilder:
(context, isExpanded) =>
ListTile(title: Text('appLinks').tr()),
body: Column(
spacing: 16,
children: [
TextFormField(
controller: homePageController,
decoration: InputDecoration(
labelText: 'homePageUrl'.tr(),
hintText: 'https://example.com',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
keyboardType: TextInputType.url,
),
TextFormField(
controller: privacyPolicyController,
decoration: InputDecoration(
labelText: 'privacyPolicyUrl'.tr(),
hintText: 'https://example.com/privacy',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
keyboardType: TextInputType.url,
),
TextFormField(
controller: termsController,
decoration: InputDecoration(
labelText: 'termsOfServiceUrl'.tr(),
hintText: 'https://example.com/terms',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
keyboardType: TextInputType.url,
),
],
).padding(horizontal: 16, bottom: 24),
isExpanded: enableLinks.value,
),
ExpansionPanel(
headerBuilder:
(context, isExpanded) =>
ListTile(title: Text('oauthConfig').tr()),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('redirectUris'.tr()),
Card(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: Column(
children: [
...redirectUris.value.map(
(uri) => ListTile(
title: Text(uri),
trailing: IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
redirectUris.value =
redirectUris.value
.where((u) => u != uri)
.toList();
},
),
),
),
if (redirectUris.value.isNotEmpty)
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.add),
title: Text('addRedirectUri'.tr()),
onTap: showAddRedirectUriDialog,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
8,
),
),
),
],
),
),
const SizedBox(height: 16),
Text('allowedScopes'.tr()),
Card(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: Column(
children: [
...allowedScopes.value.map(
(scope) => ListTile(
title: Text(scope),
trailing: IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
allowedScopes.value =
allowedScopes.value
.where(
(s) => s != scope,
)
.toList();
},
),
),
),
if (allowedScopes.value.isNotEmpty)
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.add),
title: Text('add').tr(),
onTap: showAddScopeDialog,
),
],
),
),
const SizedBox(height: 16),
SwitchListTile(
title: Text('requirePkce'.tr()),
value: requirePkce.value,
onChanged:
(value) => requirePkce.value = value,
),
SwitchListTile(
title: Text('allowOfflineAccess'.tr()),
value: allowOfflineAccess.value,
onChanged:
(value) =>
allowOfflineAccess.value = value,
),
],
).padding(horizontal: 16, bottom: 24),
isExpanded: oauthEnabled.value,
),
],
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : performAction,
label: Text('saveChanges'.tr()),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
],
),
);
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(
labelText: 'name'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
maxLines: 3,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
ExpansionPanelList(
expansionCallback: (index, isExpanded) {
switch (index) {
case 0:
enableLinks.value = isExpanded;
break;
case 1:
oauthEnabled.value = isExpanded;
break;
}
},
children: [
ExpansionPanel(
headerBuilder: (context, isExpanded) =>
ListTile(title: Text('appLinks').tr()),
body: Column(
spacing: 16,
children: [
TextFormField(
controller: homePageController,
decoration: InputDecoration(
labelText: 'homePageUrl'.tr(),
hintText: 'https://example.com',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
keyboardType: TextInputType.url,
),
TextFormField(
controller: privacyPolicyController,
decoration: InputDecoration(
labelText: 'privacyPolicyUrl'.tr(),
hintText: 'https://example.com/privacy',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
keyboardType: TextInputType.url,
),
TextFormField(
controller: termsController,
decoration: InputDecoration(
labelText: 'termsOfServiceUrl'.tr(),
hintText: 'https://example.com/terms',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
keyboardType: TextInputType.url,
),
],
).padding(horizontal: 16, bottom: 24),
isExpanded: enableLinks.value,
),
ExpansionPanel(
headerBuilder: (context, isExpanded) =>
ListTile(title: Text('oauthConfig').tr()),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('redirectUris'.tr()),
Card(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: Column(
children: [
...redirectUris.value.map(
(uri) => ListTile(
title: Text(uri),
trailing: IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
redirectUris.value = redirectUris
.value
.where((u) => u != uri)
.toList();
},
),
),
),
if (redirectUris.value.isNotEmpty)
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.add),
title: Text('addRedirectUri'.tr()),
onTap: showAddRedirectUriDialog,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
8,
),
),
),
],
),
),
const SizedBox(height: 16),
Text('allowedScopes'.tr()),
Card(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: Column(
children: [
...allowedScopes.value.map(
(scope) => ListTile(
title: Text(scope),
trailing: IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
allowedScopes.value =
allowedScopes.value
.where((s) => s != scope)
.toList();
},
),
),
),
if (allowedScopes.value.isNotEmpty)
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.add),
title: Text('add').tr(),
onTap: showAddScopeDialog,
),
],
),
),
const SizedBox(height: 16),
SwitchListTile(
title: Text('requirePkce'.tr()),
value: requirePkce.value,
onChanged: (value) =>
requirePkce.value = value,
),
SwitchListTile(
title: Text('allowOfflineAccess'.tr()),
value: allowOfflineAccess.value,
onChanged: (value) =>
allowOfflineAccess.value = value,
),
],
).padding(horizontal: 16, bottom: 24),
isExpanded: oauthEnabled.value,
),
],
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : performAction,
label: Text('saveChanges'.tr()),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
);
if (isModal) {
return bodyContent;

View File

@@ -50,8 +50,9 @@ class EditBotScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final botData =
isNew ? null : ref.watch(botProvider(publisherName, projectId, id!));
final botData = isNew
? null
: ref.watch(botProvider(publisherName, projectId, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
final submitting = useState(false);
@@ -125,14 +126,10 @@ class EditBotScreen extends HookConsumerWidget {
submitting.value = true;
try {
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(data: result, type: UniversalFileType.image),
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
@@ -193,284 +190,267 @@ class EditBotScreen extends HookConsumerWidget {
}
}
final bodyContent =
botData == null && !isNew
? const Center(child: CircularProgressIndicator())
: botData?.hasError == true && !isNew
? ResponseErrorWidget(
error: botData!.error,
onRetry:
() => ref.invalidate(
botProvider(publisherName, projectId, id!),
),
)
: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
final bodyContent = botData == null && !isNew
? const Center(child: CircularProgressIndicator())
: botData?.hasError == true && !isNew
? ResponseErrorWidget(
error: botData!.error,
onRetry: () =>
ref.invalidate(botProvider(publisherName, projectId, id!)),
)
: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color: Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child: background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
file: picture.value,
radius: 40,
fallbackIcon: Symbols.smart_toy,
),
onTap: () {
setPicture('background');
setPicture('picture');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
radius: 40,
fallbackIcon: Symbols.smart_toy,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(
labelText: 'name'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
const SizedBox(height: 16),
TextFormField(
controller: nickController,
decoration: InputDecoration(
labelText: 'nickname'.tr(),
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
const SizedBox(height: 16),
TextFormField(
controller: bioController,
decoration: InputDecoration(
labelText: 'bio'.tr(),
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
maxLines: 3,
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: firstNameController,
decoration: InputDecoration(
labelText: 'firstName'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: middleNameController,
decoration: InputDecoration(
labelText: 'middleName'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: lastNameController,
decoration: InputDecoration(
labelText: 'lastName'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
],
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: genderController,
decoration: InputDecoration(
labelText: 'gender'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: pronounsController,
decoration: InputDecoration(
labelText: 'pronouns'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
],
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: locationController,
decoration: InputDecoration(
labelText: 'location'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: timeZoneController,
decoration: InputDecoration(
labelText: 'timeZone'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
],
),
const SizedBox(height: 16),
GestureDetector(
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: birthday.value ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
if (date != null) {
birthday.value = date;
}
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'birthday'.tr(),
style: TextStyle(
color: Theme.of(context).hintColor,
),
),
Text(
birthday.value != null
? DateFormat.yMMMd().format(
birthday.value!,
)
: 'Select a date'.tr(),
),
],
),
),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : performAction,
label: Text('saveChanges').tr(),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
],
),
);
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(
labelText: 'name'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
),
const SizedBox(height: 16),
TextFormField(
controller: nickController,
decoration: InputDecoration(
labelText: 'nickname'.tr(),
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
),
const SizedBox(height: 16),
TextFormField(
controller: bioController,
decoration: InputDecoration(
labelText: 'bio'.tr(),
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
maxLines: 3,
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: firstNameController,
decoration: InputDecoration(
labelText: 'firstName'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: middleNameController,
decoration: InputDecoration(
labelText: 'middleName'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: lastNameController,
decoration: InputDecoration(
labelText: 'lastName'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
],
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: genderController,
decoration: InputDecoration(
labelText: 'gender'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: pronounsController,
decoration: InputDecoration(
labelText: 'pronouns'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
],
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: locationController,
decoration: InputDecoration(
labelText: 'location'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: timeZoneController,
decoration: InputDecoration(
labelText: 'timeZone'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
],
),
const SizedBox(height: 16),
GestureDetector(
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: birthday.value ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
if (date != null) {
birthday.value = date;
}
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'birthday'.tr(),
style: TextStyle(
color: Theme.of(context).hintColor,
),
),
Text(
birthday.value != null
? DateFormat.yMMMd().format(birthday.value!)
: 'Select a date'.tr(),
),
],
),
),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : performAction,
label: Text('saveChanges').tr(),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
);
if (isModal) {
return bodyContent;

View File

@@ -329,7 +329,7 @@ class DeveloperSelector extends HookConsumerWidget {
minTileHeight: 48,
leading: ProfilePictureWidget(
radius: 16,
fileId: item.publisher?.picture?.id,
file: item.publisher?.picture,
),
title: Text(item.publisher!.nick),
subtitle: Text('@${item.publisher!.name}'),
@@ -348,7 +348,7 @@ class DeveloperSelector extends HookConsumerWidget {
if (isReadOnly || currentDeveloper == null) {
return ProfilePictureWidget(
radius: 16,
fileId: currentDeveloper?.publisher?.picture?.id,
file: currentDeveloper?.publisher?.picture,
).center().padding(right: 8);
}
@@ -373,7 +373,7 @@ class DeveloperSelector extends HookConsumerWidget {
...developersMenu.map(
(e) => ProfilePictureWidget(
radius: 16,
fileId: e.value?.publisher?.picture?.id,
file: e.value?.publisher?.picture,
).center().padding(right: 8),
),
];
@@ -928,7 +928,7 @@ class _DeveloperEnrollmentSheet extends HookConsumerWidget {
final publisher = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: publisher.picture?.id,
file: publisher.picture,
fallbackIcon: Symbols.group,
),
title: Text(publisher.nick),

View File

@@ -37,7 +37,7 @@ class SkeletonNotificationTile extends StatelessWidget {
isThreeLine: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: fakePfp != null
? ProfilePictureWidget(fileId: fakePfp, radius: 20)
? ProfilePictureWidget(file: null, radius: 20)
: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Icon(

View File

@@ -34,16 +34,14 @@ class ArticleEditScreen extends HookConsumerWidget {
final post = ref.watch(postProvider(id));
return post.when(
data: (post) => ArticleComposeScreen(originalPost: post),
loading:
() => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: const Center(child: CircularProgressIndicator()),
),
error:
(e, _) => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Text('Error: $e', textAlign: TextAlign.center),
),
loading: () => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: const Center(child: CircularProgressIndicator()),
),
error: (e, _) => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Text('Error: $e', textAlign: TextAlign.center),
),
);
}
}
@@ -127,8 +125,8 @@ class ArticleComposeScreen extends HookConsumerWidget {
final mostRecentDraft = drafts.values.reduce(
(a, b) =>
(a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0))
? a
: b,
? a
: b,
);
// Only load if the draft has meaningful content
@@ -191,12 +189,11 @@ class ArticleComposeScreen extends HookConsumerWidget {
MarkdownTextContent(
content: contentValue.text,
textStyle: theme.textTheme.bodyMedium,
attachments:
state.attachments.value
.where((e) => e.isOnCloud)
.map((e) => e.data)
.cast<SnCloudFile>()
.toList(),
attachments: state.attachments.value
.where((e) => e.isOnCloud)
.map((e) => e.data)
.cast<SnCloudFile>()
.toList(),
),
],
);
@@ -290,22 +287,21 @@ class ArticleComposeScreen extends HookConsumerWidget {
onExpansionChanged: (expanded) {
isAttachmentsExpanded.value = expanded;
},
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer,
collapsedBackgroundColor: Theme.of(
context,
).colorScheme.surfaceContainer,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('attachments').tr(),
Text(
'articleAttachmentHint'.tr(),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
color:
Theme.of(
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
),
],
),
@@ -336,13 +332,12 @@ class ArticleComposeScreen extends HookConsumerWidget {
>(
context: context,
isScrollControlled: true,
builder:
(context) =>
AttachmentUploaderSheet(
ref: ref,
state: state,
index: idx,
),
builder: (context) =>
AttachmentUploaderSheet(
ref: ref,
state: state,
index: idx,
),
);
if (config != null) {
await ComposeLogic.uploadAttachment(
@@ -353,21 +348,20 @@ class ArticleComposeScreen extends HookConsumerWidget {
);
}
},
onUpdate:
(value) =>
ComposeLogic.updateAttachment(
state,
value,
idx,
),
onDelete:
() => ComposeLogic.deleteAttachment(
onUpdate: (value) =>
ComposeLogic.updateAttachment(
state,
value,
idx,
),
onDelete: () =>
ComposeLogic.deleteAttachment(
ref,
state,
idx,
),
onInsert:
() => ComposeLogic.insertAttachment(
onInsert: () =>
ComposeLogic.insertAttachment(
ref,
state,
idx,
@@ -413,12 +407,11 @@ class ArticleComposeScreen extends HookConsumerWidget {
const SizedBox.shrink(),
IconButton(
icon: ProfilePictureWidget(
fileId: state.currentPublisher.value?.picture?.id,
file: state.currentPublisher.value?.picture,
radius: 12,
fallbackIcon:
state.currentPublisher.value == null
? Symbols.question_mark
: null,
fallbackIcon: state.currentPublisher.value == null
? Symbols.question_mark
: null,
),
onPressed: () {
showModalBottomSheet(
@@ -448,30 +441,26 @@ class ArticleComposeScreen extends HookConsumerWidget {
valueListenable: state.submitting,
builder: (context, submitting, _) {
return IconButton(
onPressed:
submitting
? null
: () => ComposeLogic.performAction(
ref,
state,
context,
originalPost: originalPost,
),
icon:
submitting
? SizedBox(
width: 28,
height: 28,
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
).center()
: Icon(
originalPost != null
? Symbols.edit
: Symbols.upload,
onPressed: submitting
? null
: () => ComposeLogic.performAction(
ref,
state,
context,
originalPost: originalPost,
),
icon: submitting
? SizedBox(
width: 28,
height: 28,
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
).center()
: Icon(
originalPost != null ? Symbols.edit : Symbols.upload,
),
);
},
),
@@ -483,23 +472,22 @@ class ArticleComposeScreen extends HookConsumerWidget {
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child:
isWideScreen(context)
? Row(
spacing: 16,
children: [
Expanded(
flex: showPreview.value ? 1 : 2,
child: buildEditorPane(),
),
if (showPreview.value) const VerticalDivider(),
if (showPreview.value)
Expanded(child: buildPreviewPane()),
],
)
: showPreview.value
? buildPreviewPane()
: buildEditorPane(),
child: isWideScreen(context)
? Row(
spacing: 16,
children: [
Expanded(
flex: showPreview.value ? 1 : 2,
child: buildEditorPane(),
),
if (showPreview.value) const VerticalDivider(),
if (showPreview.value)
Expanded(child: buildPreviewPane()),
],
)
: showPreview.value
? buildPreviewPane()
: buildEditorPane(),
),
),

View File

@@ -52,7 +52,7 @@ class _PublisherBasisWidget extends StatelessWidget {
return Card(
child: Builder(
builder: (context) {
final hasBackground = data.background?.id != null;
final hasBackground = data.background != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -598,7 +598,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
flexibleSpace: Stack(
children: [
Positioned.fill(
child: data.background?.id != null
child: data.background != null
? CloudImageWidget(file: data.background)
: Container(
color: Theme.of(

View File

@@ -183,8 +183,8 @@ class RealmDetailScreen extends HookConsumerWidget {
flexibleSpace: Stack(
children: [
Positioned.fill(
child: realm!.background?.id != null
? CloudImageWidget(fileId: realm.background!.id)
child: realm!.background != null
? CloudImageWidget(file: realm.background!)
: Container(
color: Theme.of(
context,
@@ -281,8 +281,8 @@ class RealmDetailScreen extends HookConsumerWidget {
flexibleSpace: Stack(
children: [
Positioned.fill(
child: realm!.background?.id != null
? CloudImageWidget(fileId: realm.background!.id)
child: realm!.background != null
? CloudImageWidget(file: realm.background!)
: Container(
color: Theme.of(
context,
@@ -604,7 +604,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
leading: AccountPfcGestureDetector(
uname: member.account!.name,
child: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
file: member.account!.profile.picture,
),
),
title: Row(

View File

@@ -90,14 +90,10 @@ class EditRealmScreen extends HookConsumerWidget {
showLoadingModal(context);
submitting.value = true;
try {
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(data: result, type: UniversalFileType.image),
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
@@ -162,13 +158,12 @@ class EditRealmScreen extends HookConsumerWidget {
GestureDetector(
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
child: background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
@@ -179,7 +174,7 @@ class EditRealmScreen extends HookConsumerWidget {
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
file: picture.value,
radius: 40,
fallbackIcon: Symbols.group,
),
@@ -202,15 +197,15 @@ class EditRealmScreen extends HookConsumerWidget {
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
@@ -221,8 +216,8 @@ class EditRealmScreen extends HookConsumerWidget {
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
Card(

View File

@@ -213,7 +213,7 @@ class _RealmInviteSheet extends HookConsumerWidget {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.realm!.picture?.id,
file: invite.realm!.picture,
fallbackIcon: Symbols.group,
),
title: Text(invite.realm!.name),

View File

@@ -163,7 +163,7 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
child: AspectRatio(
aspectRatio: 1,
child: CloudImageWidget(
fileId: sticker.image.id,
file: sticker.image,
fit: BoxFit.contain,
),
),

View File

@@ -24,193 +24,181 @@ class AccountNameplate extends HookConsumerWidget {
final user = ref.watch(accountProvider(name));
return Container(
decoration:
isOutlined
? BoxDecoration(
border: Border.all(
width: 1 / MediaQuery.of(context).devicePixelRatio,
color: Theme.of(context).dividerColor,
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
)
: null,
decoration: isOutlined
? BoxDecoration(
border: Border.all(
width: 1 / MediaQuery.of(context).devicePixelRatio,
color: Theme.of(context).dividerColor,
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
)
: null,
margin: padding,
child: Card(
margin: EdgeInsets.zero,
elevation: 0,
color: Colors.transparent,
child: user.when(
data:
(account) =>
account.profile.background != null
? AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
children: [
// Background image
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CloudFileWidget(
item: account.profile.background!,
fit: BoxFit.cover,
),
),
data: (account) => account.profile.background != null
? AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
children: [
// Background image
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CloudFileWidget(
item: account.profile.background!,
fit: BoxFit.cover,
),
),
),
// Gradient overlay for text readability
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.black.withOpacity(0.1),
Colors.transparent,
],
),
// Gradient overlay for text readability
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.black.withOpacity(0.1),
Colors.transparent,
],
),
),
),
),
),
// Content positioned at the bottom
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
children: [
// Profile picture (equivalent to leading)
ProfilePictureWidget(
file: account.profile.picture,
),
),
// Content positioned at the bottom
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
const SizedBox(width: 16),
// Text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Profile picture (equivalent to leading)
ProfilePictureWidget(
fileId: account.profile.picture?.id,
),
const SizedBox(width: 16),
// Text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
AccountName(
account: account,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'@${account.name}',
).textColor(Colors.white70),
],
AccountName(
account: account,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'@${account.name}',
).textColor(Colors.white70),
],
),
),
),
],
],
),
),
)
: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
decoration:
isOutlined
? BoxDecoration(
border: Border.all(
color:
Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(12),
)
: null,
child: Row(
),
],
),
)
: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
decoration: isOutlined
? BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(12),
)
: null,
child: Row(
children: [
// Profile picture (equivalent to leading)
ProfilePictureWidget(file: account.profile.picture),
const SizedBox(width: 16),
// Text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Profile picture (equivalent to leading)
ProfilePictureWidget(
fileId: account.profile.picture?.id,
),
const SizedBox(width: 16),
// Text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
AccountName(
account: account,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
Text('@${account.name}'),
],
),
AccountName(
account: account,
style: TextStyle(fontWeight: FontWeight.bold),
),
Text('@${account.name}'),
],
),
),
loading:
() => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
],
),
),
child: Row(
children: [
// Loading indicator (equivalent to leading)
const CircularProgressIndicator(),
const SizedBox(width: 16),
// Loading text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('loading').bold().tr(),
const SizedBox(height: 4),
const Text('...'),
],
),
),
],
loading: () => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
children: [
// Loading indicator (equivalent to leading)
const CircularProgressIndicator(),
const SizedBox(width: 16),
// Loading text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('loading').bold().tr(),
const SizedBox(height: 4),
const Text('...'),
],
),
),
),
error:
(error, stackTrace) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
],
),
),
error: (error, stackTrace) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
children: [
// Error icon (equivalent to leading)
const Icon(Symbols.error),
const SizedBox(width: 16),
// Error text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('somethingWentWrong').tr().bold(),
const SizedBox(height: 4),
Text(error.toString()),
],
),
),
child: Row(
children: [
// Error icon (equivalent to leading)
const Icon(Symbols.error),
const SizedBox(width: 16),
// Error text content (equivalent to title and subtitle)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('somethingWentWrong').tr().bold(),
const SizedBox(height: 4),
Text(error.toString()),
],
),
),
],
),
),
],
),
),
),
),
);

View File

@@ -62,8 +62,8 @@ class AccountPickerSheet extends HookConsumerWidget {
),
),
autofocus: true,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
Expanded(
@@ -74,23 +74,22 @@ class AccountPickerSheet extends HookConsumerWidget {
);
return searchResult.when(
data:
(accounts) => ListView.builder(
itemCount: accounts.length,
itemBuilder: (context, index) {
final account = accounts[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: account.profile.picture?.id,
),
title: Text(account.nick),
subtitle: Text('@${account.name}'),
onTap: () => Navigator.of(context).pop(account),
);
},
),
loading:
() => const Center(child: CircularProgressIndicator()),
data: (accounts) => ListView.builder(
itemCount: accounts.length,
itemBuilder: (context, index) {
final account = accounts[index];
return ListTile(
leading: ProfilePictureWidget(
file: account.profile.picture,
),
title: Text(account.nick),
subtitle: Text('@${account.name}'),
onTap: () => Navigator.of(context).pop(account),
);
},
),
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
);
},

View File

@@ -133,10 +133,9 @@ class _ExpandedSection extends StatelessWidget {
},
child: Card(
margin: EdgeInsets.zero,
color:
Theme.of(
context,
).colorScheme.surfaceContainer,
color: Theme.of(
context,
).colorScheme.surfaceContainer,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -144,8 +143,9 @@ class _ExpandedSection extends StatelessWidget {
const Gap(4),
Text(
'Poll',
style:
Theme.of(context).textTheme.bodySmall,
style: Theme.of(
context,
).textTheme.bodySmall,
),
],
),
@@ -160,8 +160,8 @@ class _ExpandedSection extends StatelessWidget {
await showModalBottomSheet<SnWalletFund>(
context: context,
isScrollControlled: true,
builder:
(context) => const ComposeFundSheet(),
builder: (context) =>
const ComposeFundSheet(),
);
if (fund != null) {
onFundSelected(fund);
@@ -169,10 +169,9 @@ class _ExpandedSection extends StatelessWidget {
},
child: Card(
margin: EdgeInsets.zero,
color:
Theme.of(
context,
).colorScheme.surfaceContainer,
color: Theme.of(
context,
).colorScheme.surfaceContainer,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -180,8 +179,9 @@ class _ExpandedSection extends StatelessWidget {
const Gap(4),
Text(
'fund'.tr(),
style:
Theme.of(context).textTheme.bodySmall,
style: Theme.of(
context,
).textTheme.bodySmall,
),
],
),
@@ -192,11 +192,8 @@ class _ExpandedSection extends StatelessWidget {
),
StickerPickerEmbedded(
height: kInputDrawerExpandedHeight,
onPick:
(placeholder) => _insertPlaceholder(
messageController,
placeholder,
),
onPick: (placeholder) =>
_insertPlaceholder(messageController, placeholder),
),
],
),
@@ -373,15 +370,16 @@ class ChatInput extends HookConsumerWidget {
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -0.3),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
),
),
position:
Tween<Offset>(
begin: const Offset(0, -0.3),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
),
),
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1.0,
@@ -389,41 +387,40 @@ class ChatInput extends HookConsumerWidget {
),
);
},
child:
chatSubscribe.isNotEmpty
? Container(
key: const ValueKey('typing-indicator'),
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
child: Row(
children: [
const Icon(
Symbols.more_horiz,
size: 16,
).padding(horizontal: 8),
const Gap(8),
Expanded(
child: Text(
'typingHint'.plural(
chatSubscribe.length,
args: [
chatSubscribe
.map((x) => x.nick ?? x.account.nick)
.join(', '),
],
),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
)
: const SizedBox.shrink(
key: ValueKey('typing-indicator-none'),
child: chatSubscribe.isNotEmpty
? Container(
key: const ValueKey('typing-indicator'),
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
child: Row(
children: [
const Icon(
Symbols.more_horiz,
size: 16,
).padding(horizontal: 8),
const Gap(8),
Expanded(
child: Text(
'typingHint'.plural(
chatSubscribe.length,
args: [
chatSubscribe
.map((x) => x.nick ?? x.account.nick)
.join(', '),
],
),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
)
: const SizedBox.shrink(
key: ValueKey('typing-indicator-none'),
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
@@ -445,41 +442,36 @@ class ChatInput extends HookConsumerWidget {
),
);
},
child:
attachments.isNotEmpty
? SizedBox(
key: ValueKey('attachments-${attachments.length}'),
height: 180,
child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 12),
scrollDirection: Axis.horizontal,
itemCount: attachments.length,
itemBuilder: (context, idx) {
return SizedBox(
width: 180,
child: AttachmentPreview(
isCompact: true,
item: attachments[idx],
progress:
attachmentProgress['chat-upload']?[idx],
onRequestUpload:
() => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx),
onUpdate: (value) {
attachments[idx] = value;
onAttachmentsChanged(attachments);
},
onMove:
(delta) => onMoveAttachment(idx, delta),
),
);
},
separatorBuilder: (_, _) => const Gap(8),
),
).padding(vertical: 12)
: const SizedBox.shrink(
key: ValueKey('no-attachments'),
child: attachments.isNotEmpty
? SizedBox(
key: ValueKey('attachments-${attachments.length}'),
height: 180,
child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 12),
scrollDirection: Axis.horizontal,
itemCount: attachments.length,
itemBuilder: (context, idx) {
return SizedBox(
width: 180,
child: AttachmentPreview(
isCompact: true,
item: attachments[idx],
progress:
attachmentProgress['chat-upload']?[idx],
onRequestUpload: () => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx),
onUpdate: (value) {
attachments[idx] = value;
onAttachmentsChanged(attachments);
},
onMove: (delta) => onMoveAttachment(idx, delta),
),
);
},
separatorBuilder: (_, _) => const Gap(8),
),
).padding(vertical: 12)
: const SizedBox.shrink(key: ValueKey('no-attachments')),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
@@ -501,66 +493,62 @@ class ChatInput extends HookConsumerWidget {
),
);
},
child:
selectedPoll != null
? Container(
key: const ValueKey('selected-poll'),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Row(
children: [
Icon(
Symbols.how_to_vote,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
selectedPoll!.title ?? 'Poll',
style: Theme.of(context).textTheme.bodySmall!
.copyWith(fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: () => onPollSelected(null),
tooltip: 'clear'.tr(),
),
),
],
),
)
: const SizedBox.shrink(
key: ValueKey('no-selected-poll'),
child: selectedPoll != null
? Container(
key: const ValueKey('selected-poll'),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Row(
children: [
Icon(
Symbols.how_to_vote,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
selectedPoll!.title ?? 'Poll',
style: Theme.of(context).textTheme.bodySmall!
.copyWith(fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: () => onPollSelected(null),
tooltip: 'clear'.tr(),
),
),
],
),
)
: const SizedBox.shrink(key: ValueKey('no-selected-poll')),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
@@ -582,93 +570,88 @@ class ChatInput extends HookConsumerWidget {
),
);
},
child:
selectedFund != null
? Container(
key: const ValueKey('selected-fund'),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
child: selectedFund != null
? Container(
key: const ValueKey('selected-fund'),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Row(
children: [
Icon(
Symbols.currency_exchange,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Row(
children: [
Icon(
Symbols.currency_exchange,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}',
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w500,
const Gap(8),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}',
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (selectedFund!.message != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
selectedFund!.message!,
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
fontSize: 10,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (selectedFund!.message != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
selectedFund!.message!,
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
fontSize: 10,
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: () => onFundSelected(null),
tooltip: 'clear'.tr(),
),
),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: () => onFundSelected(null),
tooltip: 'clear'.tr(),
),
],
),
)
: const SizedBox.shrink(
key: ValueKey('no-selected-fund'),
),
],
),
)
: const SizedBox.shrink(key: ValueKey('no-selected-fund')),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
@@ -692,59 +675,57 @@ class ChatInput extends HookConsumerWidget {
},
child:
(messageReplyingTo != null ||
messageForwardingTo != null ||
messageEditingTo != null)
? Container(
key: ValueKey(
messageReplyingTo?.id ??
messageForwardingTo?.id ??
messageEditingTo?.id ??
'action',
messageForwardingTo != null ||
messageEditingTo != null)
? Container(
key: ValueKey(
messageReplyingTo?.id ??
messageForwardingTo?.id ??
messageEditingTo?.id ??
'action',
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
messageReplyingTo != null
? Symbols.reply
: messageForwardingTo != null
? Symbols.forward
: Symbols.edit,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
messageReplyingTo != null
? Symbols.reply
: messageForwardingTo != null
? Symbols.forward
: Symbols.edit,
size: 18,
color:
Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
messageReplyingTo != null
? 'chatReplyingTo'.tr(
? 'chatReplyingTo'.tr(
args: [
messageReplyingTo
?.sender
@@ -753,60 +734,57 @@ class ChatInput extends HookConsumerWidget {
'unknown'.tr(),
],
)
: messageForwardingTo != null
? 'chatForwarding'.tr()
: 'chatEditing'.tr(),
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: onClear,
tooltip: 'clear'.tr(),
),
),
],
),
if (messageReplyingTo != null ||
messageForwardingTo != null ||
messageEditingTo != null)
Padding(
padding: const EdgeInsets.only(
top: 6,
left: 26,
),
child: Text(
(messageReplyingTo ??
messageForwardingTo ??
messageEditingTo)
?.content ??
'chatNoContent'.tr(),
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 2,
: messageForwardingTo != null
? 'chatForwarding'.tr()
: 'chatEditing'.tr(),
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
)
: const SizedBox.shrink(key: ValueKey('no-action')),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: onClear,
tooltip: 'clear'.tr(),
),
),
],
),
if (messageReplyingTo != null ||
messageForwardingTo != null ||
messageEditingTo != null)
Padding(
padding: const EdgeInsets.only(
top: 6,
left: 26,
),
child: Text(
(messageReplyingTo ??
messageForwardingTo ??
messageEditingTo)
?.content ??
'chatNoContent'.tr(),
style: Theme.of(context).textTheme.bodySmall!
.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
)
: const SizedBox.shrink(key: ValueKey('no-action')),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -815,25 +793,19 @@ class ChatInput extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip:
isExpanded.value ? 'collapse'.tr() : 'more'.tr(),
tooltip: isExpanded.value
? 'collapse'.tr()
: 'more'.tr(),
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder:
(child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child:
isExpanded.value
? const Icon(
Symbols.close,
key: ValueKey('close'),
)
: const Icon(
Symbols.add,
key: ValueKey('add'),
),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: isExpanded.value
? const Icon(
Symbols.close,
key: ValueKey('close'),
)
: const Icon(Symbols.add, key: ValueKey('add')),
),
onPressed: () {
isExpanded.value = !isExpanded.value;
@@ -885,46 +857,43 @@ class ChatInput extends HookConsumerWidget {
hintMaxLines: 1,
hintText:
(chatRoom.type == 1 && chatRoom.name == null)
? 'chatDirectMessageHint'.tr(
args: [
getValidMembers(
chatRoom.members!,
).map((e) => e.account.nick).join(', '),
],
)
: 'chatMessageHint'.tr(
args: [chatRoom.name!],
),
? 'chatDirectMessageHint'.tr(
args: [
getValidMembers(
chatRoom.members!,
).map((e) => e.account.nick).join(', '),
],
)
: 'chatMessageHint'.tr(args: [chatRoom.name!]),
border: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
counterText:
messageController.text.length > 1024
? '${messageController.text.length}/4096'
: null,
counterText: messageController.text.length > 1024
? '${messageController.text.length}/4096'
: null,
),
maxLines: 5,
minLines: 1,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
textInputAction:
settings.enterToSend
? TextInputAction.send
: null,
onSubmitted:
settings.enterToSend ? (_) => send() : null,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
textInputAction: settings.enterToSend
? TextInputAction.send
: null,
onSubmitted: settings.enterToSend
? (_) => send()
: null,
);
},
suggestionsCallback: (pattern) async {
// Only trigger on @ or :
final atIndex = pattern.lastIndexOf('@');
final colonIndex = pattern.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
final triggerIndex = atIndex > colonIndex
? atIndex
: colonIndex;
if (triggerIndex == -1) return [];
final chopped = pattern.substring(triggerIndex);
if (chopped.contains(' ')) return [];
@@ -986,9 +955,7 @@ class ChatInput extends HookConsumerWidget {
child: SizedBox(
width: 28,
height: 28,
child: CloudImageWidget(
fileId: sticker.image.id,
),
child: CloudImageWidget(file: sticker.image),
),
);
break;
@@ -1005,8 +972,9 @@ class ChatInput extends HookConsumerWidget {
final text = messageController.text;
final atIndex = text.lastIndexOf('@');
final colonIndex = text.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
final triggerIndex = atIndex > colonIndex
? atIndex
: colonIndex;
if (triggerIndex == -1) return;
final newText = text.replaceRange(
triggerIndex,
@@ -1053,16 +1021,15 @@ class ChatInput extends HookConsumerWidget {
),
);
},
child:
isExpanded.value
? _ExpandedSection(
messageController: messageController,
selectedPoll: selectedPoll,
onPollSelected: onPollSelected,
selectedFund: selectedFund,
onFundSelected: onFundSelected,
)
: const SizedBox.shrink(key: ValueKey('collapsed')),
child: isExpanded.value
? _ExpandedSection(
messageController: messageController,
selectedPoll: selectedPoll,
onPollSelected: onPollSelected,
selectedFund: selectedFund,
onFundSelected: onFundSelected,
)
: const SizedBox.shrink(key: ValueKey('collapsed')),
),
],
),

View File

@@ -31,7 +31,7 @@ class MessageListTile extends StatelessWidget {
radius: 20,
backgroundColor: Colors.transparent,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
file: sender.account.profile.picture,
radius: 20,
),
),

View File

@@ -24,12 +24,11 @@ class MessageSenderInfo extends StatelessWidget {
@override
Widget build(BuildContext context) {
final timestamp =
DateTime.now().difference(createdAt).inDays > 365
? DateFormat('yyyy/MM/dd HH:mm').format(createdAt.toLocal())
: DateTime.now().difference(createdAt).inDays > 0
? DateFormat('MM/dd HH:mm').format(createdAt.toLocal())
: DateFormat('HH:mm').format(createdAt.toLocal());
final timestamp = DateTime.now().difference(createdAt).inDays > 365
? DateFormat('yyyy/MM/dd HH:mm').format(createdAt.toLocal())
: DateTime.now().difference(createdAt).inDays > 0
? DateFormat('MM/dd HH:mm').format(createdAt.toLocal())
: DateFormat('HH:mm').format(createdAt.toLocal());
if (isCompact) {
return Row(
@@ -41,7 +40,7 @@ class MessageSenderInfo extends StatelessWidget {
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
file: sender.account.profile.picture,
radius: 14,
),
),
@@ -69,7 +68,7 @@ class MessageSenderInfo extends StatelessWidget {
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
file: sender.account.profile.picture,
radius: 14,
),
),
@@ -106,7 +105,7 @@ class MessageSenderInfo extends StatelessWidget {
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
file: sender.account.profile.picture,
radius: 16,
),
),

View File

@@ -99,15 +99,15 @@ class PublicRoomPreview extends HookConsumerWidget {
SizedBox(
height: 26,
width: 26,
child: (room.type == 1 && room.picture?.id == null)
child: (room.type == 1 && room.picture == null)
? SplitAvatarWidget(
filesId: room.members!
.map((e) => e.account.profile.picture?.id)
files: room.members!
.map((e) => e.account.profile.picture)
.toList(),
)
: room.picture?.id != null
: room.picture != null
? ProfilePictureWidget(
fileId: room.picture?.id,
file: room.picture,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
@@ -132,15 +132,15 @@ class PublicRoomPreview extends HookConsumerWidget {
SizedBox(
height: 26,
width: 26,
child: (room.type == 1 && room.picture?.id == null)
child: (room.type == 1 && room.picture == null)
? SplitAvatarWidget(
filesId: room.members!
.map((e) => e.account.profile.picture?.id)
files: room.members!
.map((e) => e.account.profile.picture)
.toList(),
)
: room.picture?.id != null
: room.picture != null
? ProfilePictureWidget(
fileId: room.picture?.id,
file: room.picture,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(

View File

@@ -22,15 +22,13 @@ class ChatRoomAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final avatarChild = (isDirect && room.picture?.id == null)
final avatarChild = (isDirect && room.picture == null)
? SplitAvatarWidget(
filesId: validMembers
.map((e) => e.account.profile.picture?.id)
.toList(),
files: validMembers.map((e) => e.account.profile.picture).toList(),
)
: room.picture?.id == null
: room.picture == null
? CircleAvatar(child: Text((room.name ?? 'DM')[0].toUpperCase()))
: ProfilePictureWidget(fileId: room.picture?.id);
: ProfilePictureWidget(file: room.picture);
final badgeChild = Badge(
isLabelVisible: summary.when(

View File

@@ -262,10 +262,7 @@ class CheckInActivityWidget extends StatelessWidget {
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
fileId: result.account!.profile.picture?.id,
radius: 12,
),
ProfilePictureWidget(file: result.account!.profile.picture, radius: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,

View File

@@ -577,6 +577,9 @@ class ProfilePictureWidget extends ConsumerWidget {
final serverUrl = ref.watch(serverUrlProvider);
final String? id = file?.id ?? fileId;
final meta = file?.fileMeta is Map ? (file!.fileMeta as Map) : const {};
final blurHash = meta['blur'] as String?;
final fallback = Icon(
fallbackIcon ?? Symbols.account_circle,
size: radius,
@@ -590,6 +593,7 @@ class ProfilePictureWidget extends ConsumerWidget {
placeholder: fallback,
content: () => UniversalImage(
uri: '$serverUrl/drive/files/$id',
blurHash: blurHash,
fit: BoxFit.cover,
),
);
@@ -625,14 +629,14 @@ class ProfilePictureWidget extends ConsumerWidget {
}
class SplitAvatarWidget extends ConsumerWidget {
final List<String?> filesId;
final List<SnCloudFile?> files;
final double radius;
final IconData fallbackIcon;
final Color? fallbackColor;
const SplitAvatarWidget({
super.key,
required this.filesId,
required this.files,
this.radius = 20,
this.fallbackIcon = Symbols.account_circle,
this.fallbackColor,
@@ -640,17 +644,17 @@ class SplitAvatarWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
if (filesId.isEmpty) {
if (files.isEmpty) {
return ProfilePictureWidget(
fileId: null,
file: null,
radius: radius,
fallbackIcon: fallbackIcon,
fallbackColor: fallbackColor,
);
}
if (filesId.length == 1) {
if (files.length == 1) {
return ProfilePictureWidget(
fileId: filesId[0],
file: files[0],
radius: radius,
fallbackIcon: fallbackIcon,
fallbackColor: fallbackColor,
@@ -665,32 +669,32 @@ class SplitAvatarWidget extends ConsumerWidget {
color: Theme.of(context).colorScheme.primaryContainer,
child: Stack(
children: [
if (filesId.length == 2)
if (files.length == 2)
Row(
children: [
Expanded(
child: _buildQuadrant(context, filesId[0], ref, radius),
child: _buildQuadrant(context, files[0], ref, radius),
),
Expanded(
child: _buildQuadrant(context, filesId[1], ref, radius),
child: _buildQuadrant(context, files[1], ref, radius),
),
],
)
else if (filesId.length == 3)
else if (files.length == 3)
Row(
children: [
Column(
children: [
Expanded(
child: _buildQuadrant(context, filesId[0], ref, radius),
child: _buildQuadrant(context, files[0], ref, radius),
),
Expanded(
child: _buildQuadrant(context, filesId[1], ref, radius),
child: _buildQuadrant(context, files[1], ref, radius),
),
],
),
Expanded(
child: _buildQuadrant(context, filesId[2], ref, radius),
child: _buildQuadrant(context, files[2], ref, radius),
),
],
)
@@ -701,20 +705,10 @@ class SplitAvatarWidget extends ConsumerWidget {
child: Row(
children: [
Expanded(
child: _buildQuadrant(
context,
filesId[0],
ref,
radius,
),
child: _buildQuadrant(context, files[0], ref, radius),
),
Expanded(
child: _buildQuadrant(
context,
filesId[1],
ref,
radius,
),
child: _buildQuadrant(context, files[1], ref, radius),
),
],
),
@@ -723,22 +717,17 @@ class SplitAvatarWidget extends ConsumerWidget {
child: Row(
children: [
Expanded(
child: _buildQuadrant(
context,
filesId[2],
ref,
radius,
),
child: _buildQuadrant(context, files[2], ref, radius),
),
Expanded(
child: filesId.length > 4
child: files.length > 4
? Container(
color: Theme.of(
context,
).colorScheme.primaryContainer,
child: Center(
child: Text(
'+${filesId.length - 3}',
'+${files.length - 3}',
style: TextStyle(
fontSize: radius * 0.4,
color: Theme.of(
@@ -748,12 +737,7 @@ class SplitAvatarWidget extends ConsumerWidget {
),
),
)
: _buildQuadrant(
context,
filesId[3],
ref,
radius,
),
: _buildQuadrant(context, files[3], ref, radius),
),
],
),
@@ -768,11 +752,11 @@ class SplitAvatarWidget extends ConsumerWidget {
Widget _buildQuadrant(
BuildContext context,
String? fileId,
SnCloudFile? file,
WidgetRef ref,
double radius,
) {
if (fileId == null) {
if (file == null) {
return Container(
width: radius,
height: radius,
@@ -787,7 +771,7 @@ class SplitAvatarWidget extends ConsumerWidget {
}
final serverUrl = ref.watch(serverUrlProvider);
final uri = '$serverUrl/drive/files/$fileId';
final uri = '$serverUrl/drive/files/${file.id}';
return SizedBox(
width: radius,

View File

@@ -1,9 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/flutter_svg.dart';
class UniversalImage extends StatelessWidget {
class UniversalImage extends HookWidget {
final String uri;
final String? blurHash;
final BoxFit fit;
@@ -27,6 +28,7 @@ class UniversalImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final loaded = useState(false);
final isSvgImage = isSvg || uri.toLowerCase().endsWith('.svg');
if (isSvgImage) {
@@ -35,9 +37,8 @@ class UniversalImage extends StatelessWidget {
fit: fit,
width: width,
height: height,
placeholderBuilder:
(BuildContext context) =>
Center(child: CircularProgressIndicator()),
placeholderBuilder: (BuildContext context) =>
Center(child: CircularProgressIndicator()),
);
}
@@ -46,8 +47,9 @@ class UniversalImage extends StatelessWidget {
if (width != null && height != null && !noCacheOptimization) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
cacheWidth = width != null ? (width! * devicePixelRatio).round() : null;
cacheHeight =
height != null ? (height! * devicePixelRatio).round() : null;
cacheHeight = height != null
? (height! * devicePixelRatio).round()
: null;
}
return SizedBox(
@@ -66,21 +68,72 @@ class UniversalImage extends StatelessWidget {
memCacheWidth: cacheWidth,
progressIndicatorBuilder: (context, url, progress) {
return Center(
child: CircularProgressIndicator(value: progress.progress),
child: AnimatedCircularProgressIndicator(
value: progress.progress,
color: Colors.white.withOpacity(0.5),
),
);
},
errorWidget:
(context, url, error) =>
useFallbackImage
? Image.asset(
'assets/images/media-offline.jpg',
fit: BoxFit.cover,
key: Key('image-broke-$uri'),
)
: SizedBox.shrink(),
imageBuilder: (context, imageProvider) {
Future(() => loaded.value = true);
return AnimatedOpacity(
opacity: loaded.value ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: Image(
image: imageProvider,
fit: fit,
width: width,
height: height,
),
);
},
errorWidget: (context, url, error) => useFallbackImage
? Image.asset(
'assets/images/media-offline.jpg',
fit: BoxFit.cover,
key: Key('image-broke-$uri'),
)
: SizedBox.shrink(),
),
],
),
);
}
}
class AnimatedCircularProgressIndicator extends HookWidget {
final double? value;
final Color? color;
final double strokeWidth;
final Duration duration;
const AnimatedCircularProgressIndicator({
super.key,
this.value,
this.color,
this.strokeWidth = 4.0,
this.duration = const Duration(milliseconds: 200),
});
@override
Widget build(BuildContext context) {
final animationController = useAnimationController(duration: duration);
final animation = useAnimation(
Tween<double>(begin: 0.0, end: value ?? 0.0).animate(
CurvedAnimation(parent: animationController, curve: Curves.linear),
),
);
useEffect(() {
animationController.animateTo(value ?? 0.0);
return null;
}, [value]);
return CircularProgressIndicator(
value: animation,
color: color,
strokeWidth: strokeWidth,
backgroundColor: Colors.transparent,
);
}
}

View File

@@ -41,12 +41,11 @@ class ComposeFormFields extends HookConsumerWidget {
GestureDetector(
onTap: onPublisherTap,
child: ProfilePictureWidget(
fileId: state.currentPublisher.value?.picture?.id,
file: state.currentPublisher.value?.picture,
radius: 20,
fallbackIcon:
state.currentPublisher.value == null
? Icons.question_mark
: null,
fallbackIcon: state.currentPublisher.value == null
? Icons.question_mark
: null,
),
),
@@ -98,8 +97,8 @@ class ComposeFormFields extends HookConsumerWidget {
),
),
style: theme.textTheme.titleMedium,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
// Description field
@@ -115,8 +114,8 @@ class ComposeFormFields extends HookConsumerWidget {
style: theme.textTheme.bodyMedium,
minLines: 1,
maxLines: 3,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
// Content field
@@ -138,16 +137,17 @@ class ComposeFormFields extends HookConsumerWidget {
),
),
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
);
},
suggestionsCallback: (pattern) async {
// Only trigger on @ or :
final atIndex = pattern.lastIndexOf('@');
final colonIndex = pattern.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
final triggerIndex = atIndex > colonIndex
? atIndex
: colonIndex;
if (triggerIndex == -1) return [];
final chopped = pattern.substring(triggerIndex);
if (chopped.contains(' ')) return [];
@@ -202,7 +202,7 @@ class ComposeFormFields extends HookConsumerWidget {
child: SizedBox(
width: 28,
height: 28,
child: CloudImageWidget(fileId: sticker.image.id),
child: CloudImageWidget(file: sticker.image),
),
);
break;
@@ -219,8 +219,9 @@ class ComposeFormFields extends HookConsumerWidget {
final text = state.contentController.text;
final atIndex = text.lastIndexOf('@');
final colonIndex = text.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
final triggerIndex = atIndex > colonIndex
? atIndex
: colonIndex;
if (triggerIndex == -1) return;
final newText = text.replaceRange(
triggerIndex,
@@ -281,8 +282,8 @@ class ArticleComposeFormFields extends StatelessWidget {
),
),
style: theme.textTheme.titleMedium,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
// Description field
@@ -297,8 +298,8 @@ class ArticleComposeFormFields extends StatelessWidget {
style: theme.textTheme.bodyMedium,
minLines: 1,
maxLines: 3,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
// Content field (expanded)
@@ -317,8 +318,8 @@ class ArticleComposeFormFields extends StatelessWidget {
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
],

View File

@@ -135,10 +135,7 @@ class CompactReferencePost extends StatelessWidget {
Widget _buildProfilePicture(BuildContext context) {
// Handle publisher case
if (post.publisher != null) {
return ProfilePictureWidget(
fileId: post.publisher!.picture?.id,
radius: 16,
);
return ProfilePictureWidget(file: post.publisher!.picture, radius: 16);
}
// Handle actor case
if (post.actor != null) {
@@ -169,7 +166,7 @@ class CompactReferencePost extends StatelessWidget {
}
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: 16);
return ProfilePictureWidget(file: null, radius: 16);
}
String _getDisplayName() {

View File

@@ -412,7 +412,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
child: Row(
children: [
ProfilePictureWidget(
fileId: currentRealm.picture?.id,
file: currentRealm.picture,
fallbackIcon: Symbols.workspaces,
radius: 16,
),
@@ -428,7 +428,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
child: Row(
children: [
ProfilePictureWidget(
fileId: realm.picture?.id,
file: realm.picture,
fallbackIcon: Symbols.workspaces,
radius: 16,
),
@@ -454,7 +454,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
)
else
ProfilePictureWidget(
fileId: currentRealm.picture?.id,
file: currentRealm.picture,
fallbackIcon: Symbols.workspaces,
radius: 16,
),

View File

@@ -54,7 +54,7 @@ class PostAwardSheet extends HookConsumerWidget {
}
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: radius);
return ProfilePictureWidget(file: null, radius: radius);
}
String _getPublisherName() {

View File

@@ -73,96 +73,94 @@ class PostQuickReply extends HookConsumerWidget {
const kInputChipHeight = 54.0;
return publishers.when(
data:
(data) => Material(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(28),
child: Container(
constraints: BoxConstraints(minHeight: kInputChipHeight),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
child: ProfilePictureWidget(
fileId: currentPublisher.value?.picture?.id,
radius: (kInputChipHeight * 0.5) - 6,
data: (data) => Material(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(28),
child: Container(
constraints: BoxConstraints(minHeight: kInputChipHeight),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
child: ProfilePictureWidget(
file: currentPublisher.value?.picture,
radius: (kInputChipHeight * 0.5) - 6,
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => PublisherModal(),
).then((value) {
if (value is SnPublisher) {
currentPublisher.value = value;
}
});
},
).padding(right: 12),
Expanded(
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: 'postReplyPlaceholder'.tr(),
border: InputBorder.none,
isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 14,
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => PublisherModal(),
).then((value) {
if (value is SnPublisher) {
currentPublisher.value = value;
}
});
},
).padding(right: 12),
Expanded(
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: 'postReplyPlaceholder'.tr(),
border: InputBorder.none,
isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 14,
),
visualDensity: VisualDensity.compact,
),
style: TextStyle(fontSize: 14),
minLines: 1,
maxLines: 5,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
const Gap(8),
IconButton(
onPressed: () async {
onLaunch?.call();
final value = await PostComposeSheet.show(
context,
initialState: PostComposeInitialState(
content: contentController.text,
replyingTo: parent,
),
);
if (value != null) onPosted?.call();
},
icon: const Icon(Symbols.launch, size: 20),
visualDensity: VisualDensity.compact,
constraints: BoxConstraints(
maxHeight: kInputChipHeight - 6,
minHeight: kInputChipHeight - 6,
),
),
IconButton(
icon:
submitting.value
? SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(strokeWidth: 3),
)
: Icon(Symbols.send, size: 20),
color: Theme.of(context).colorScheme.primary,
onPressed: submitting.value ? null : performAction,
visualDensity: VisualDensity.compact,
constraints: BoxConstraints(
maxHeight: kInputChipHeight - 6,
minHeight: kInputChipHeight - 6,
),
),
],
style: TextStyle(fontSize: 14),
minLines: 1,
maxLines: 5,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
),
const Gap(8),
IconButton(
onPressed: () async {
onLaunch?.call();
final value = await PostComposeSheet.show(
context,
initialState: PostComposeInitialState(
content: contentController.text,
replyingTo: parent,
),
);
if (value != null) onPosted?.call();
},
icon: const Icon(Symbols.launch, size: 20),
visualDensity: VisualDensity.compact,
constraints: BoxConstraints(
maxHeight: kInputChipHeight - 6,
minHeight: kInputChipHeight - 6,
),
),
IconButton(
icon: submitting.value
? SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(strokeWidth: 3),
)
: Icon(Symbols.send, size: 20),
color: Theme.of(context).colorScheme.primary,
onPressed: submitting.value ? null : performAction,
visualDensity: VisualDensity.compact,
constraints: BoxConstraints(
maxHeight: kInputChipHeight - 6,
minHeight: kInputChipHeight - 6,
),
),
],
),
),
),
loading: () => const SizedBox.shrink(),
error: (e, _) => const SizedBox.shrink(),
);

View File

@@ -148,7 +148,7 @@ class PostReplyPreview extends HookConsumerWidget {
return ActorPictureWidget(actor: post.actor!, radius: radius);
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: radius);
return ProfilePictureWidget(file: null, radius: radius);
}
@override
@@ -448,7 +448,7 @@ class ReferencedPostWidget extends StatelessWidget {
// Handle publisher case
if (post.publisher != null) {
return ProfilePictureWidget(
fileId: post.publisher!.picture?.id,
file: post.publisher!.picture,
radius: radius,
);
}
@@ -457,7 +457,7 @@ class ReferencedPostWidget extends StatelessWidget {
return ActorPictureWidget(actor: post.actor!, radius: radius);
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: radius);
return ProfilePictureWidget(file: null, radius: radius);
}
String _getDisplayName(SnPost post) {
@@ -701,7 +701,7 @@ class PostHeader extends HookConsumerWidget {
return ActorPictureWidget(actor: post.actor!, radius: radius);
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: radius);
return ProfilePictureWidget(file: null, radius: radius);
}
String _getDisplayName(SnPost post) {

View File

@@ -21,63 +21,57 @@ class PublisherModal extends HookConsumerWidget {
children: [
Expanded(
child: publishers.when(
data:
(value) =>
value.isEmpty
? ConstrainedBox(
constraints: BoxConstraints(maxWidth: 280),
child:
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'publishersEmpty',
textAlign: TextAlign.center,
).tr().fontSize(17).bold(),
Text(
'publishersEmptyDescription',
textAlign: TextAlign.center,
).tr(),
const Gap(12),
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) =>
const NewPublisherScreen(),
).then((value) {
if (value != null) {
ref.invalidate(
publishersManagedProvider,
);
}
});
},
child: Text('createPublisher').tr(),
),
],
).center(),
)
: SingleChildScrollView(
child: Column(
children: [
for (final publisher in value)
ListTile(
leading: ProfilePictureWidget(
fileId: publisher.picture?.id,
),
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
onTap: () {
Navigator.pop(context, publisher);
},
),
],
),
data: (value) => value.isEmpty
? ConstrainedBox(
constraints: BoxConstraints(maxWidth: 280),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'publishersEmpty',
textAlign: TextAlign.center,
).tr().fontSize(17).bold(),
Text(
'publishersEmptyDescription',
textAlign: TextAlign.center,
).tr(),
const Gap(12),
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) =>
const NewPublisherScreen(),
).then((value) {
if (value != null) {
ref.invalidate(publishersManagedProvider);
}
});
},
child: Text('createPublisher').tr(),
),
],
).center(),
)
: SingleChildScrollView(
child: Column(
children: [
for (final publisher in value)
ListTile(
leading: ProfilePictureWidget(
file: publisher.picture,
),
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
onTap: () {
Navigator.pop(context, publisher);
},
),
],
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Text('Error: $e'),
),

View File

@@ -30,17 +30,16 @@ class RealmListTile extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child:
realm.background == null
? const SizedBox.shrink()
: CloudImageWidget(file: realm.background),
child: realm.background == null
? const SizedBox.shrink()
: CloudImageWidget(file: realm.background),
),
),
Positioned(
bottom: -30,
left: 18,
child: ProfilePictureWidget(
fileId: realm.picture?.id,
file: realm.picture,
fallbackIcon: Symbols.group,
radius: 24,
),

View File

@@ -49,7 +49,7 @@ class RealmSelectionDropdown extends StatelessWidget {
child: Row(
children: [
ProfilePictureWidget(
fileId: realm.picture?.id,
file: realm.picture,
fallbackIcon: Symbols.workspaces,
radius: 16,
),

View File

@@ -776,19 +776,19 @@ class _ChatRoomOption extends HookConsumerWidget {
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: (isDirect && room.picture?.id == null)
child: (isDirect && room.picture == null)
? SplitAvatarWidget(
filesId: validMembers
.map((e) => e.account.profile.picture?.id)
files: validMembers
.map((e) => e.account.profile.picture)
.toList(),
radius: 16,
)
: room.picture?.id == null
: room.picture == null
? CircleAvatar(
radius: 16,
child: Text(room.name![0].toUpperCase()),
)
: ProfilePictureWidget(fileId: room.picture?.id, radius: 16),
: ProfilePictureWidget(file: room.picture, radius: 16),
),
const SizedBox(height: 4),
// Chat room name

View File

@@ -75,31 +75,29 @@ class StickerPicker extends HookConsumerWidget {
},
);
},
loading:
() => const SizedBox(
width: 320,
height: 320,
child: Center(child: CircularProgressIndicator()),
),
error:
(err, _) => SizedBox(
width: 360,
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.error, size: 28),
const Gap(8),
Text('Error: $err', textAlign: TextAlign.center),
const Gap(12),
FilledButton.icon(
onPressed: () => ref.invalidate(myStickerPacksProvider),
icon: const Icon(Symbols.refresh),
label: Text('retry').tr(),
),
],
).padding(all: 16),
),
loading: () => const SizedBox(
width: 320,
height: 320,
child: Center(child: CircularProgressIndicator()),
),
error: (err, _) => SizedBox(
width: 360,
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.error, size: 28),
const Gap(8),
Text('Error: $err', textAlign: TextAlign.center),
const Gap(12),
FilledButton.icon(
onPressed: () => ref.invalidate(myStickerPacksProvider),
icon: const Icon(Symbols.refresh),
label: Text('retry').tr(),
),
],
).padding(all: 16),
),
),
),
);
@@ -263,7 +261,7 @@ class _StickersGrid extends StatelessWidget {
child: AspectRatio(
aspectRatio: 1,
child: CloudImageWidget(
fileId: sticker.image.id,
file: sticker.image,
fit: BoxFit.contain,
),
),
@@ -310,31 +308,29 @@ class StickerPickerEmbedded extends HookConsumerWidget {
},
);
},
loading:
() => SizedBox(
width: 320,
height: height ?? 320,
child: const Center(child: CircularProgressIndicator()),
),
error:
(err, _) => SizedBox(
width: 360,
height: height ?? 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.error, size: 28),
const Gap(8),
Text('Error: $err', textAlign: TextAlign.center),
const Gap(12),
FilledButton.icon(
onPressed: () => ref.invalidate(myStickerPacksProvider),
icon: const Icon(Symbols.refresh),
label: Text('retry').tr(),
),
],
).padding(all: 16),
),
loading: () => SizedBox(
width: 320,
height: height ?? 320,
child: const Center(child: CircularProgressIndicator()),
),
error: (err, _) => SizedBox(
width: 360,
height: height ?? 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.error, size: 28),
const Gap(8),
Text('Error: $err', textAlign: TextAlign.center),
const Gap(12),
FilledButton.icon(
onPressed: () => ref.invalidate(myStickerPacksProvider),
icon: const Icon(Symbols.refresh),
label: Text('retry').tr(),
),
],
).padding(all: 16),
),
);
}
}
@@ -386,18 +382,16 @@ class _EmbeddedPackSwitcherState extends State<_EmbeddedPackSwitcher> {
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color:
selected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainer,
color: selected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(Radius.circular(8)),
border:
selected
? Border.all(
color: Theme.of(context).colorScheme.primary,
width: 4,
)
: null,
border: selected
? Border.all(
color: Theme.of(context).colorScheme.primary,
width: 4,
)
: null,
),
margin: const EdgeInsets.only(right: 8),
child: InkWell(
@@ -413,11 +407,11 @@ class _EmbeddedPackSwitcherState extends State<_EmbeddedPackSwitcher> {
builder: (context, value, _) {
return packs[i].icon != null
? CloudImageWidget(
file: packs[i].icon!,
).clipRRect(all: value)
file: packs[i].icon!,
).clipRRect(all: value)
: CloudImageWidget(
file: packs[i].stickers.firstOrNull?.image,
).clipRRect(all: value);
file: packs[i].stickers.firstOrNull?.image,
).clipRRect(all: value);
},
),
),
@@ -458,18 +452,17 @@ Future<void> showStickerPickerPopover(
offset: offset,
alignment: alignment ?? Alignment.topLeft,
dimBackground: true,
builder:
(ctx) => SizedBox(
width: math.min(480, MediaQuery.of(context).size.width * 0.9),
height: 480,
child: ProviderScope(
child: StickerPicker(
onPick: (ph) {
onPick(ph);
Navigator.of(ctx).maybePop();
},
),
),
builder: (ctx) => SizedBox(
width: math.min(480, MediaQuery.of(context).size.width * 0.9),
height: 480,
child: ProviderScope(
child: StickerPicker(
onPick: (ph) {
onPick(ph);
Navigator.of(ctx).maybePop();
},
),
),
),
);
}