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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -69,157 +69,141 @@ class StickerPackDetailContent extends HookConsumerWidget {
} }
return pack.when( return pack.when(
data: data: (pack) => Column(
(pack) => Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Column( Text(pack!.description),
crossAxisAlignment: CrossAxisAlignment.stretch, Row(
spacing: 4,
children: [ children: [
Text(pack!.description), const Icon(Symbols.folder, size: 16),
Row( Text(
spacing: 4, '${packContent.value?.length ?? 0}/24',
children: [ style: GoogleFonts.robotoMono(),
const Icon(Symbols.folder, size: 16), ),
Text(
'${packContent.value?.length ?? 0}/24',
style: GoogleFonts.robotoMono(),
),
],
).opacity(0.85),
Row(
spacing: 4,
children: [
const Icon(Symbols.sell, size: 16),
Text(pack.prefix, style: GoogleFonts.robotoMono()),
],
).opacity(0.85),
Row(
spacing: 4,
children: [
const Icon(Symbols.tag, size: 16),
Flexible(
child: SelectableText(
pack.id,
maxLines: 1,
style: GoogleFonts.robotoMono(),
),
),
],
).opacity(0.85),
], ],
).padding(horizontal: 24, vertical: 24), ).opacity(0.85),
const Divider(height: 1), Row(
Expanded( spacing: 4,
child: packContent.when( children: [
data: const Icon(Symbols.sell, size: 16),
(stickers) => RefreshIndicator( Text(pack.prefix, style: GoogleFonts.robotoMono()),
onRefresh: ],
() => ref.refresh( ).opacity(0.85),
stickerPackContentProvider(id).future, Row(
), spacing: 4,
child: GridView.builder( children: [
padding: const EdgeInsets.symmetric( const Icon(Symbols.tag, size: 16),
horizontal: 24, Flexible(
vertical: 20, child: SelectableText(
), pack.id,
gridDelegate: maxLines: 1,
const SliverGridDelegateWithMaxCrossAxisExtent( style: GoogleFonts.robotoMono(),
maxCrossAxisExtent: 80, ),
mainAxisSpacing: 8, ),
crossAxisSpacing: 8, ],
), ).opacity(0.85),
itemCount: stickers.length, ],
itemBuilder: (context, index) { ).padding(horizontal: 24, vertical: 24),
final sticker = stickers[index]; const Divider(height: 1),
return ContextMenuWidget( Expanded(
menuProvider: (_) { child: packContent.when(
return Menu( data: (stickers) => RefreshIndicator(
children: [ onRefresh: () =>
MenuAction( ref.refresh(stickerPackContentProvider(id).future),
title: 'stickerCopyPlaceholder'.tr(), child: GridView.builder(
image: MenuImage.icon(Symbols.copy_all), padding: const EdgeInsets.symmetric(
callback: () { horizontal: 24,
Clipboard.setData( vertical: 20,
ClipboardData( ),
text: gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
':${pack.prefix}+${sticker.slug}:', maxCrossAxisExtent: 80,
), mainAxisSpacing: 8,
); crossAxisSpacing: 8,
}, ),
), itemCount: stickers.length,
MenuSeparator(), itemBuilder: (context, index) {
MenuAction( final sticker = stickers[index];
title: 'edit'.tr(), return ContextMenuWidget(
image: MenuImage.icon(Symbols.edit), menuProvider: (_) {
callback: () { return Menu(
showModalBottomSheet( children: [
context: context, MenuAction(
isScrollControlled: true, title: 'stickerCopyPlaceholder'.tr(),
builder: image: MenuImage.icon(Symbols.copy_all),
(context) => SheetScaffold( callback: () {
titleText: 'editSticker'.tr(), Clipboard.setData(
child: StickerForm( ClipboardData(
packId: id, text: ':${pack.prefix}+${sticker.slug}:',
id: sticker.id, ),
),
),
).then((value) {
if (value != null) {
ref.invalidate(
stickerPackContentProvider(id),
);
}
});
},
),
MenuAction(
title: 'delete'.tr(),
image: MenuImage.icon(Symbols.delete),
callback: () {
deleteSticker(sticker);
},
),
],
); );
}, },
child: ClipRRect( ),
borderRadius: BorderRadius.all( MenuSeparator(),
Radius.circular(8), MenuAction(
), title: 'edit'.tr(),
child: Container( image: MenuImage.icon(Symbols.edit),
decoration: BoxDecoration( callback: () {
color: showModalBottomSheet(
Theme.of( context: context,
context, isScrollControlled: true,
).colorScheme.surfaceContainer, builder: (context) => SheetScaffold(
borderRadius: BorderRadius.all( titleText: 'editSticker'.tr(),
Radius.circular(8), child: StickerForm(
packId: id,
id: sticker.id,
), ),
), ),
child: CloudImageWidget( ).then((value) {
fileId: sticker.image.id, if (value != null) {
fit: BoxFit.contain, 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(), loading: () => const CircularProgressIndicator().center(),
); );
} }
@@ -241,65 +225,60 @@ class StickerPackActionMenu extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return PopupMenuButton( return PopupMenuButton(
icon: Icon(Icons.more_vert, shadows: [iconShadow]), icon: Icon(Icons.more_vert, shadows: [iconShadow]),
itemBuilder: itemBuilder: (context) => [
(context) => [ PopupMenuItem(
PopupMenuItem( onTap: () {
onTap: () { showModalBottomSheet(
showModalBottomSheet( context: context,
context: context, isScrollControlled: true,
isScrollControlled: true, builder: (context) => SheetScaffold(
builder: titleText: 'editStickerPack'.tr(),
(context) => SheetScaffold( child: StickerPackForm(pubName: pubName, packId: packId),
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(),
],
), ),
), ).then((value) {
PopupMenuItem( if (value != null) {
child: Row( ref.invalidate(stickerPackProvider(packId));
children: [ }
const Icon(Icons.delete, color: Colors.red), });
const Gap(12), },
const Text( child: Row(
'deleteStickerPack', children: [
style: TextStyle(color: Colors.red), Icon(
).tr(), Icons.edit,
], color: Theme.of(context).colorScheme.onSecondaryContainer,
), ),
onTap: () { const Gap(12),
showConfirmAlert( const Text('editStickerPack').tr(),
'deleteStickerPackHint'.tr(), ],
'deleteStickerPack'.tr(), ),
isDanger: true, ),
).then((confirm) { PopupMenuItem(
if (confirm) { child: Row(
final client = ref.watch(apiClientProvider); children: [
client.delete('/sphere/stickers/$packId'); const Icon(Icons.delete, color: Colors.red),
ref.invalidate(stickerPacksProvider); const Gap(12),
if (context.mounted) context.pop(true); 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, color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.all(Radius.circular(8)), borderRadius: BorderRadius.all(Radius.circular(8)),
), ),
child: child: (image.value?.isEmpty ?? true)
(image.value?.isEmpty ?? true) ? const SizedBox.shrink()
? const SizedBox.shrink() : CloudImageWidget(fileId: image.value!),
: CloudImageWidget(fileId: image.value!),
), ),
), ),
), ),
@@ -383,10 +361,8 @@ class StickerForm extends HookConsumerWidget {
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: builder: (context) =>
(context) => CloudFilePicker( CloudFilePicker(allowedTypes: {UniversalFileType.image}),
allowedTypes: {UniversalFileType.image},
),
).then((value) { ).then((value) {
if (value == null) return; if (value == null) return;
image.value = value[0].id; image.value = value[0].id;
@@ -412,8 +388,8 @@ class StickerForm extends HookConsumerWidget {
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
), ),
), ),
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
], ],
), ),

View File

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

View File

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

View File

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

View File

@@ -51,10 +51,9 @@ class EditAppScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null; final isNew = id == null;
final app = final app = isNew
isNew ? null
? null : ref.watch(customAppProvider(publisherName, projectId, id!));
: ref.watch(customAppProvider(publisherName, projectId, id!));
final formKey = useMemoized(() => GlobalKey<FormState>()); final formKey = useMemoized(() => GlobalKey<FormState>());
@@ -139,14 +138,10 @@ class EditAppScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final cloudFile = final cloudFile = await FileUploader.createCloudFile(
await FileUploader.createCloudFile( ref: ref,
ref: ref, fileData: UniversalFile(data: result, type: UniversalFileType.image),
fileData: UniversalFile( ).future;
data: result,
type: UniversalFileType.image,
),
).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');
} }
@@ -169,41 +164,40 @@ class EditAppScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( titleText: 'addScope'.tr(),
titleText: 'addScope'.tr(), child: Padding(
child: Padding( padding: const EdgeInsets.all(20),
padding: const EdgeInsets.all(20), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ TextFormField(
TextFormField( controller: scopeController,
controller: scopeController, decoration: InputDecoration(
decoration: InputDecoration( labelText: 'scopeName'.tr(),
labelText: 'scopeName'.tr(), border: OutlineInputBorder(
border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)),
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( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( titleText: 'addRedirectUri'.tr(),
titleText: 'addRedirectUri'.tr(), child: Padding(
child: Padding( padding: const EdgeInsets.all(20),
padding: const EdgeInsets.all(20), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ TextFormField(
TextFormField( controller: uriController,
controller: uriController, decoration: InputDecoration(
decoration: InputDecoration( labelText: 'redirectUri'.tr(),
labelText: 'redirectUri'.tr(), hintText: 'https://example.com/auth/callback',
hintText: 'https://example.com/auth/callback', helperText: 'redirectUriHint'.tr(),
helperText: 'redirectUriHint'.tr(), helperMaxLines: 3,
helperMaxLines: 3, border: OutlineInputBorder(
border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)),
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(),
), ),
const SizedBox(height: 20), ),
FilledButton.tonalIcon( keyboardType: TextInputType.url,
onPressed: () { validator: (value) {
if (uriController.text.isNotEmpty) { if (value == null || value.isEmpty) {
redirectUris.value = [ return 'uriRequired'.tr();
...redirectUris.value, }
uriController.text, final uri = Uri.tryParse(value);
]; if (uri == null || !uri.hasAbsolutePath) {
Navigator.pop(context); return 'invalidUri'.tr();
} }
}, return null;
icon: const Icon(Symbols.add), },
label: Text('add').tr(), 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, 'picture_id': picture.value?.id,
'background_id': background.value?.id, 'background_id': background.value?.id,
'links': { 'links': {
'home_page': 'home_page': homePageController.text.isNotEmpty
homePageController.text.isNotEmpty ? homePageController.text
? homePageController.text : null,
: null, 'privacy_policy': privacyPolicyController.text.isNotEmpty
'privacy_policy': ? privacyPolicyController.text
privacyPolicyController.text.isNotEmpty : null,
? privacyPolicyController.text 'terms_of_service': termsController.text.isNotEmpty
: null, ? termsController.text
'terms_of_service': : null,
termsController.text.isNotEmpty ? termsController.text : null,
}, },
'oauth_config': 'oauth_config': oauthEnabled.value
oauthEnabled.value ? {
? { 'redirect_uris': redirectUris.value,
'redirect_uris': redirectUris.value, 'post_logout_redirect_uris': postLogoutUris.value.isNotEmpty
'post_logout_redirect_uris': ? postLogoutUris.value
postLogoutUris.value.isNotEmpty : null,
? postLogoutUris.value 'allowed_scopes': allowedScopes.value,
: null, 'allowed_grant_types': allowedGrantTypes.value,
'allowed_scopes': allowedScopes.value, 'require_pkce': requirePkce.value,
'allowed_grant_types': allowedGrantTypes.value, 'allow_offline_access': allowOfflineAccess.value,
'require_pkce': requirePkce.value, }
'allow_offline_access': allowOfflineAccess.value, : null,
}
: null,
}; };
try { try {
showLoadingModal(context); showLoadingModal(context);
@@ -326,287 +316,269 @@ class EditAppScreen extends HookConsumerWidget {
} }
} }
final bodyContent = final bodyContent = app == null && !isNew
app == null && !isNew ? const Center(child: CircularProgressIndicator())
? const Center(child: CircularProgressIndicator()) : app?.hasError == true && !isNew
: app?.hasError == true && !isNew ? ResponseErrorWidget(
? ResponseErrorWidget( error: app!.error,
error: app!.error, onRetry: () => ref.invalidate(
onRetry: customAppProvider(publisherName, projectId, id!),
() => ref.invalidate( ),
customAppProvider(publisherName, projectId, id!), )
), : SingleChildScrollView(
) child: Column(
: SingleChildScrollView( children: [
child: Column( AspectRatio(
children: [ aspectRatio: 16 / 7,
AspectRatio( child: Stack(
aspectRatio: 16 / 7, clipBehavior: Clip.none,
child: Stack( fit: StackFit.expand,
clipBehavior: Clip.none, children: [
fit: StackFit.expand, GestureDetector(
children: [ child: Container(
GestureDetector( color: Theme.of(
child: Container( context,
color: ).colorScheme.surfaceContainerHigh,
Theme.of( child: background.value != null
context, ? CloudFileWidget(
).colorScheme.surfaceContainerHigh, item: background.value!,
child: fit: BoxFit.cover,
background.value != null )
? CloudFileWidget( : const SizedBox.shrink(),
item: background.value!, ),
fit: BoxFit.cover, onTap: () {
) setPicture('background');
: const SizedBox.shrink(), },
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
file: picture.value,
radius: 40,
fallbackIcon: Symbols.apps,
), ),
onTap: () { 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) { if (isModal) {
return bodyContent; return bodyContent;

View File

@@ -50,8 +50,9 @@ class EditBotScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null; final isNew = id == null;
final botData = final botData = isNew
isNew ? null : ref.watch(botProvider(publisherName, projectId, id!)); ? null
: ref.watch(botProvider(publisherName, projectId, id!));
final formKey = useMemoized(() => GlobalKey<FormState>()); final formKey = useMemoized(() => GlobalKey<FormState>());
final submitting = useState(false); final submitting = useState(false);
@@ -125,14 +126,10 @@ class EditBotScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final cloudFile = final cloudFile = await FileUploader.createCloudFile(
await FileUploader.createCloudFile( ref: ref,
ref: ref, fileData: UniversalFile(data: result, type: UniversalFileType.image),
fileData: UniversalFile( ).future;
data: result,
type: UniversalFileType.image,
),
).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');
} }
@@ -193,284 +190,267 @@ class EditBotScreen extends HookConsumerWidget {
} }
} }
final bodyContent = final bodyContent = botData == null && !isNew
botData == null && !isNew ? const Center(child: CircularProgressIndicator())
? const Center(child: CircularProgressIndicator()) : botData?.hasError == true && !isNew
: botData?.hasError == true && !isNew ? ResponseErrorWidget(
? ResponseErrorWidget( error: botData!.error,
error: botData!.error, onRetry: () =>
onRetry: ref.invalidate(botProvider(publisherName, projectId, id!)),
() => ref.invalidate( )
botProvider(publisherName, projectId, id!), : SingleChildScrollView(
), child: Column(
) children: [
: SingleChildScrollView( AspectRatio(
child: Column( aspectRatio: 16 / 7,
children: [ child: Stack(
AspectRatio( clipBehavior: Clip.none,
aspectRatio: 16 / 7, fit: StackFit.expand,
child: Stack( children: [
clipBehavior: Clip.none, GestureDetector(
fit: StackFit.expand, child: Container(
children: [ color: Theme.of(
GestureDetector( context,
child: Container( ).colorScheme.surfaceContainerHigh,
color: child: background.value != null
Theme.of( ? CloudFileWidget(
context, item: background.value!,
).colorScheme.surfaceContainerHigh, fit: BoxFit.cover,
child: )
background.value != null : const SizedBox.shrink(),
? CloudFileWidget( ),
item: background.value!, onTap: () {
fit: BoxFit.cover, setPicture('background');
) },
: const SizedBox.shrink(), ),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
file: picture.value,
radius: 40,
fallbackIcon: Symbols.smart_toy,
), ),
onTap: () { 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) { if (isModal) {
return bodyContent; return bodyContent;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,193 +24,181 @@ class AccountNameplate extends HookConsumerWidget {
final user = ref.watch(accountProvider(name)); final user = ref.watch(accountProvider(name));
return Container( return Container(
decoration: decoration: isOutlined
isOutlined ? BoxDecoration(
? BoxDecoration( border: Border.all(
border: Border.all( width: 1 / MediaQuery.of(context).devicePixelRatio,
width: 1 / MediaQuery.of(context).devicePixelRatio, color: Theme.of(context).dividerColor,
color: Theme.of(context).dividerColor, ),
), borderRadius: const BorderRadius.all(Radius.circular(8)),
borderRadius: const BorderRadius.all(Radius.circular(8)), )
) : null,
: null,
margin: padding, margin: padding,
child: Card( child: Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
elevation: 0, elevation: 0,
color: Colors.transparent, color: Colors.transparent,
child: user.when( child: user.when(
data: data: (account) => account.profile.background != null
(account) => ? AspectRatio(
account.profile.background != null aspectRatio: 16 / 9,
? AspectRatio( child: Stack(
aspectRatio: 16 / 9, children: [
child: Stack( // Background image
children: [ Positioned.fill(
// Background image child: ClipRRect(
Positioned.fill( borderRadius: BorderRadius.circular(8),
child: ClipRRect( child: CloudFileWidget(
borderRadius: BorderRadius.circular(8), item: account.profile.background!,
child: CloudFileWidget( fit: BoxFit.cover,
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( // Content positioned at the bottom
borderRadius: BorderRadius.circular(8), Positioned(
gradient: LinearGradient( left: 0,
begin: Alignment.bottomCenter, right: 0,
end: Alignment.topCenter, bottom: 0,
colors: [ child: Padding(
Colors.black.withOpacity(0.8), padding: const EdgeInsets.symmetric(
Colors.black.withOpacity(0.1), horizontal: 16.0,
Colors.transparent, vertical: 8.0,
], ),
), child: Row(
), children: [
// Profile picture (equivalent to leading)
ProfilePictureWidget(
file: account.profile.picture,
), ),
), const SizedBox(width: 16),
// Content positioned at the bottom // Text content (equivalent to title and subtitle)
Positioned( Expanded(
left: 0, child: Column(
right: 0, crossAxisAlignment: CrossAxisAlignment.start,
bottom: 0, mainAxisSize: MainAxisSize.min,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
children: [ children: [
// Profile picture (equivalent to leading) AccountName(
ProfilePictureWidget( account: account,
fileId: account.profile.picture?.id, style: TextStyle(
), fontWeight: FontWeight.bold,
const SizedBox(width: 16), color: Colors.white,
// 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),
],
), ),
), ),
Text(
'@${account.name}',
).textColor(Colors.white70),
], ],
), ),
), ),
), ],
], ),
), ),
) ),
: Container( ],
padding: const EdgeInsets.symmetric( ),
horizontal: 16.0, )
vertical: 8.0, : Container(
), padding: const EdgeInsets.symmetric(
decoration: horizontal: 16.0,
isOutlined vertical: 8.0,
? BoxDecoration( ),
border: Border.all( decoration: isOutlined
color: ? BoxDecoration(
Theme.of(context).colorScheme.outline, border: Border.all(
), color: Theme.of(context).colorScheme.outline,
borderRadius: BorderRadius.circular(12), ),
) borderRadius: BorderRadius.circular(12),
: null, )
child: Row( : 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: [ children: [
// Profile picture (equivalent to leading) AccountName(
ProfilePictureWidget( account: account,
fileId: account.profile.picture?.id, style: TextStyle(fontWeight: FontWeight.bold),
),
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}'),
],
),
), ),
Text('@${account.name}'),
], ],
), ),
), ),
loading: ],
() => Padding( ),
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
), ),
child: Row( loading: () => Padding(
children: [ padding: const EdgeInsets.symmetric(
// Loading indicator (equivalent to leading) horizontal: 16.0,
const CircularProgressIndicator(), vertical: 8.0,
const SizedBox(width: 16), ),
// Loading text content (equivalent to title and subtitle) child: Row(
Expanded( children: [
child: Column( // Loading indicator (equivalent to leading)
crossAxisAlignment: CrossAxisAlignment.start, const CircularProgressIndicator(),
mainAxisSize: MainAxisSize.min, const SizedBox(width: 16),
children: [ // Loading text content (equivalent to title and subtitle)
const Text('loading').bold().tr(), Expanded(
const SizedBox(height: 4), child: Column(
const Text('...'), 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( error: (error, stackTrace) => Padding(
horizontal: 16.0, padding: const EdgeInsets.symmetric(
vertical: 8.0, 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, autofocus: true,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
Expanded( Expanded(
@@ -74,23 +74,22 @@ class AccountPickerSheet extends HookConsumerWidget {
); );
return searchResult.when( return searchResult.when(
data: data: (accounts) => ListView.builder(
(accounts) => ListView.builder( itemCount: accounts.length,
itemCount: accounts.length, itemBuilder: (context, index) {
itemBuilder: (context, index) { final account = accounts[index];
final account = accounts[index]; return ListTile(
return ListTile( leading: ProfilePictureWidget(
leading: ProfilePictureWidget( file: account.profile.picture,
fileId: account.profile.picture?.id, ),
), title: Text(account.nick),
title: Text(account.nick), subtitle: Text('@${account.name}'),
subtitle: Text('@${account.name}'), onTap: () => Navigator.of(context).pop(account),
onTap: () => Navigator.of(context).pop(account), );
); },
}, ),
), loading: () =>
loading: const Center(child: CircularProgressIndicator()),
() => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')), error: (error, stack) => Center(child: Text('Error: $error')),
); );
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
class UniversalImage extends StatelessWidget { class UniversalImage extends HookWidget {
final String uri; final String uri;
final String? blurHash; final String? blurHash;
final BoxFit fit; final BoxFit fit;
@@ -27,6 +28,7 @@ class UniversalImage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loaded = useState(false);
final isSvgImage = isSvg || uri.toLowerCase().endsWith('.svg'); final isSvgImage = isSvg || uri.toLowerCase().endsWith('.svg');
if (isSvgImage) { if (isSvgImage) {
@@ -35,9 +37,8 @@ class UniversalImage extends StatelessWidget {
fit: fit, fit: fit,
width: width, width: width,
height: height, height: height,
placeholderBuilder: placeholderBuilder: (BuildContext context) =>
(BuildContext context) => Center(child: CircularProgressIndicator()),
Center(child: CircularProgressIndicator()),
); );
} }
@@ -46,8 +47,9 @@ class UniversalImage extends StatelessWidget {
if (width != null && height != null && !noCacheOptimization) { if (width != null && height != null && !noCacheOptimization) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
cacheWidth = width != null ? (width! * devicePixelRatio).round() : null; cacheWidth = width != null ? (width! * devicePixelRatio).round() : null;
cacheHeight = cacheHeight = height != null
height != null ? (height! * devicePixelRatio).round() : null; ? (height! * devicePixelRatio).round()
: null;
} }
return SizedBox( return SizedBox(
@@ -66,21 +68,72 @@ class UniversalImage extends StatelessWidget {
memCacheWidth: cacheWidth, memCacheWidth: cacheWidth,
progressIndicatorBuilder: (context, url, progress) { progressIndicatorBuilder: (context, url, progress) {
return Center( return Center(
child: CircularProgressIndicator(value: progress.progress), child: AnimatedCircularProgressIndicator(
value: progress.progress,
color: Colors.white.withOpacity(0.5),
),
); );
}, },
errorWidget: imageBuilder: (context, imageProvider) {
(context, url, error) => Future(() => loaded.value = true);
useFallbackImage return AnimatedOpacity(
? Image.asset( opacity: loaded.value ? 1.0 : 0.0,
'assets/images/media-offline.jpg', duration: const Duration(milliseconds: 300),
fit: BoxFit.cover, child: Image(
key: Key('image-broke-$uri'), image: imageProvider,
) fit: fit,
: SizedBox.shrink(), 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( GestureDetector(
onTap: onPublisherTap, onTap: onPublisherTap,
child: ProfilePictureWidget( child: ProfilePictureWidget(
fileId: state.currentPublisher.value?.picture?.id, file: state.currentPublisher.value?.picture,
radius: 20, radius: 20,
fallbackIcon: fallbackIcon: state.currentPublisher.value == null
state.currentPublisher.value == null ? Icons.question_mark
? Icons.question_mark : null,
: null,
), ),
), ),
@@ -98,8 +97,8 @@ class ComposeFormFields extends HookConsumerWidget {
), ),
), ),
style: theme.textTheme.titleMedium, style: theme.textTheme.titleMedium,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
// Description field // Description field
@@ -115,8 +114,8 @@ class ComposeFormFields extends HookConsumerWidget {
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
minLines: 1, minLines: 1,
maxLines: 3, maxLines: 3,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
// Content field // Content field
@@ -138,16 +137,17 @@ class ComposeFormFields extends HookConsumerWidget {
), ),
), ),
maxLines: null, maxLines: null,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
); );
}, },
suggestionsCallback: (pattern) async { suggestionsCallback: (pattern) async {
// Only trigger on @ or : // Only trigger on @ or :
final atIndex = pattern.lastIndexOf('@'); final atIndex = pattern.lastIndexOf('@');
final colonIndex = pattern.lastIndexOf(':'); final colonIndex = pattern.lastIndexOf(':');
final triggerIndex = final triggerIndex = atIndex > colonIndex
atIndex > colonIndex ? atIndex : colonIndex; ? atIndex
: colonIndex;
if (triggerIndex == -1) return []; if (triggerIndex == -1) return [];
final chopped = pattern.substring(triggerIndex); final chopped = pattern.substring(triggerIndex);
if (chopped.contains(' ')) return []; if (chopped.contains(' ')) return [];
@@ -202,7 +202,7 @@ class ComposeFormFields extends HookConsumerWidget {
child: SizedBox( child: SizedBox(
width: 28, width: 28,
height: 28, height: 28,
child: CloudImageWidget(fileId: sticker.image.id), child: CloudImageWidget(file: sticker.image),
), ),
); );
break; break;
@@ -219,8 +219,9 @@ class ComposeFormFields extends HookConsumerWidget {
final text = state.contentController.text; final text = state.contentController.text;
final atIndex = text.lastIndexOf('@'); final atIndex = text.lastIndexOf('@');
final colonIndex = text.lastIndexOf(':'); final colonIndex = text.lastIndexOf(':');
final triggerIndex = final triggerIndex = atIndex > colonIndex
atIndex > colonIndex ? atIndex : colonIndex; ? atIndex
: colonIndex;
if (triggerIndex == -1) return; if (triggerIndex == -1) return;
final newText = text.replaceRange( final newText = text.replaceRange(
triggerIndex, triggerIndex,
@@ -281,8 +282,8 @@ class ArticleComposeFormFields extends StatelessWidget {
), ),
), ),
style: theme.textTheme.titleMedium, style: theme.textTheme.titleMedium,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
// Description field // Description field
@@ -297,8 +298,8 @@ class ArticleComposeFormFields extends StatelessWidget {
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
minLines: 1, minLines: 1,
maxLines: 3, maxLines: 3,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
// Content field (expanded) // Content field (expanded)
@@ -317,8 +318,8 @@ class ArticleComposeFormFields extends StatelessWidget {
maxLines: null, maxLines: null,
expands: true, expands: true,
textAlignVertical: TextAlignVertical.top, textAlignVertical: TextAlignVertical.top,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
], ],

View File

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

View File

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

View File

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

View File

@@ -73,96 +73,94 @@ class PostQuickReply extends HookConsumerWidget {
const kInputChipHeight = 54.0; const kInputChipHeight = 54.0;
return publishers.when( return publishers.when(
data: data: (data) => Material(
(data) => Material( elevation: 2,
elevation: 2, color: Theme.of(context).colorScheme.surfaceContainerHighest,
color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(28),
borderRadius: BorderRadius.circular(28), child: Container(
child: Container( constraints: BoxConstraints(minHeight: kInputChipHeight),
constraints: BoxConstraints(minHeight: kInputChipHeight), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Row(
child: Row( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ GestureDetector(
GestureDetector( child: ProfilePictureWidget(
child: ProfilePictureWidget( file: currentPublisher.value?.picture,
fileId: currentPublisher.value?.picture?.id, radius: (kInputChipHeight * 0.5) - 6,
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, visualDensity: VisualDensity.compact,
constraints: BoxConstraints(
maxHeight: kInputChipHeight - 6,
minHeight: kInputChipHeight - 6,
),
), ),
IconButton( style: TextStyle(fontSize: 14),
icon: minLines: 1,
submitting.value maxLines: 5,
? SizedBox( onTapOutside: (_) =>
width: 28, FocusManager.instance.primaryFocus?.unfocus(),
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,
),
),
],
), ),
), 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(), loading: () => const SizedBox.shrink(),
error: (e, _) => 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); return ActorPictureWidget(actor: post.actor!, radius: radius);
} }
// Fallback // Fallback
return ProfilePictureWidget(fileId: null, radius: radius); return ProfilePictureWidget(file: null, radius: radius);
} }
@override @override
@@ -448,7 +448,7 @@ class ReferencedPostWidget extends StatelessWidget {
// Handle publisher case // Handle publisher case
if (post.publisher != null) { if (post.publisher != null) {
return ProfilePictureWidget( return ProfilePictureWidget(
fileId: post.publisher!.picture?.id, file: post.publisher!.picture,
radius: radius, radius: radius,
); );
} }
@@ -457,7 +457,7 @@ class ReferencedPostWidget extends StatelessWidget {
return ActorPictureWidget(actor: post.actor!, radius: radius); return ActorPictureWidget(actor: post.actor!, radius: radius);
} }
// Fallback // Fallback
return ProfilePictureWidget(fileId: null, radius: radius); return ProfilePictureWidget(file: null, radius: radius);
} }
String _getDisplayName(SnPost post) { String _getDisplayName(SnPost post) {
@@ -701,7 +701,7 @@ class PostHeader extends HookConsumerWidget {
return ActorPictureWidget(actor: post.actor!, radius: radius); return ActorPictureWidget(actor: post.actor!, radius: radius);
} }
// Fallback // Fallback
return ProfilePictureWidget(fileId: null, radius: radius); return ProfilePictureWidget(file: null, radius: radius);
} }
String _getDisplayName(SnPost post) { String _getDisplayName(SnPost post) {

View File

@@ -21,63 +21,57 @@ class PublisherModal extends HookConsumerWidget {
children: [ children: [
Expanded( Expanded(
child: publishers.when( child: publishers.when(
data: data: (value) => value.isEmpty
(value) => ? ConstrainedBox(
value.isEmpty constraints: BoxConstraints(maxWidth: 280),
? ConstrainedBox( child: Column(
constraints: BoxConstraints(maxWidth: 280), crossAxisAlignment: CrossAxisAlignment.center,
child: mainAxisAlignment: MainAxisAlignment.center,
Column( children: [
crossAxisAlignment: CrossAxisAlignment.center, Text(
mainAxisAlignment: MainAxisAlignment.center, 'publishersEmpty',
children: [ textAlign: TextAlign.center,
Text( ).tr().fontSize(17).bold(),
'publishersEmpty', Text(
textAlign: TextAlign.center, 'publishersEmptyDescription',
).tr().fontSize(17).bold(), textAlign: TextAlign.center,
Text( ).tr(),
'publishersEmptyDescription', const Gap(12),
textAlign: TextAlign.center, ElevatedButton(
).tr(), onPressed: () {
const Gap(12), showModalBottomSheet(
ElevatedButton( context: context,
onPressed: () { isScrollControlled: true,
showModalBottomSheet( builder: (context) =>
context: context, const NewPublisherScreen(),
isScrollControlled: true, ).then((value) {
builder: if (value != null) {
(context) => ref.invalidate(publishersManagedProvider);
const NewPublisherScreen(), }
).then((value) { });
if (value != null) { },
ref.invalidate( child: Text('createPublisher').tr(),
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);
},
),
],
),
), ),
],
).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()), loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Text('Error: $e'), error: (e, _) => Text('Error: $e'),
), ),

View File

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

View File

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

View File

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

View File

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