diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 1cc5923e..b8cc83dd 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -74,7 +74,7 @@ class AccountScreen extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (user.value?.profile.background?.id != null) + if (user.value?.profile.background != null) Stack( clipBehavior: Clip.none, children: [ @@ -112,7 +112,7 @@ class AccountScreen extends HookConsumerWidget { Builder( builder: (context) { final hasBackground = - user.value?.profile.background?.id != null; + user.value?.profile.background != null; return Row( crossAxisAlignment: CrossAxisAlignment.center, spacing: hasBackground ? 0 : 16, diff --git a/lib/screens/account/me/profile_update.dart b/lib/screens/account/me/profile_update.dart index d70940ed..1e104923 100644 --- a/lib/screens/account/me/profile_update.dart +++ b/lib/screens/account/me/profile_update.dart @@ -74,14 +74,10 @@ class UpdateProfileScreen extends HookConsumerWidget { submitting.value = true; try { - final cloudFile = - await FileUploader.createCloudFile( - ref: ref, - fileData: UniversalFile( - data: result, - type: UniversalFileType.image, - ), - ).future; + final cloudFile = await FileUploader.createCloudFile( + ref: ref, + fileData: UniversalFile(data: result, type: UniversalFileType.image), + ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); } @@ -188,8 +184,9 @@ class UpdateProfileScreen extends HookConsumerWidget { if (usernameColorType.value == 'gradient') ...{ if (usernameColorDirection.text.isNotEmpty) 'direction': usernameColorDirection.text, - 'colors': - usernameColorColors.value.where((c) => c.isNotEmpty).toList(), + 'colors': usernameColorColors.value + .where((c) => c.isNotEmpty) + .toList(), }, }; @@ -206,18 +203,16 @@ class UpdateProfileScreen extends HookConsumerWidget { 'time_zone': timeZoneController.text, 'birthday': birthday.value?.toUtc().toIso8601String(), 'username_color': usernameColorData, - 'links': - links.value - .where((e) => e.name.isNotEmpty && e.url.isNotEmpty) - .toList(), + 'links': links.value + .where((e) => e.name.isNotEmpty && e.url.isNotEmpty) + .toList(), }, ); final userNotifier = ref.read(userInfoProvider.notifier); userNotifier.fetchUser(); - links.value = - links.value - .where((e) => e.name.isNotEmpty && e.url.isNotEmpty) - .toList(); + links.value = links.value + .where((e) => e.name.isNotEmpty && e.url.isNotEmpty) + .toList(); } catch (err) { showErrorAlert(err); } finally { @@ -244,13 +239,12 @@ class UpdateProfileScreen extends HookConsumerWidget { GestureDetector( child: Container( color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: - user.value!.profile.background?.id != null - ? CloudImageWidget( - fileId: user.value!.profile.background!.id, - fit: BoxFit.cover, - ) - : const SizedBox.shrink(), + child: user.value!.profile.background != null + ? CloudImageWidget( + file: user.value!.profile.background, + fit: BoxFit.cover, + ) + : const SizedBox.shrink(), ), onTap: () { updateProfilePicture('background'); @@ -261,7 +255,7 @@ class UpdateProfileScreen extends HookConsumerWidget { bottom: -32, child: GestureDetector( child: ProfilePictureWidget( - fileId: user.value!.profile.picture?.id, + file: user.value!.profile.picture, radius: 40, ), onTap: () { @@ -291,14 +285,14 @@ class UpdateProfileScreen extends HookConsumerWidget { ), controller: usernameController, readOnly: true, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), TextFormField( decoration: InputDecoration(labelText: 'nickname'.tr()), controller: nicknameController, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), DropdownButtonFormField2( decoration: InputDecoration( @@ -385,9 +379,8 @@ class UpdateProfileScreen extends HookConsumerWidget { labelText: 'firstName'.tr(), ), controller: firstNameController, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), Expanded( @@ -396,9 +389,8 @@ class UpdateProfileScreen extends HookConsumerWidget { labelText: 'middleName'.tr(), ), controller: middleNameController, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), Expanded( @@ -407,9 +399,8 @@ class UpdateProfileScreen extends HookConsumerWidget { labelText: 'lastName'.tr(), ), controller: lastNameController, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), ], @@ -423,8 +414,8 @@ class UpdateProfileScreen extends HookConsumerWidget { maxLines: null, minLines: 3, controller: bioController, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), Row( spacing: 16, @@ -445,33 +436,34 @@ class UpdateProfileScreen extends HookConsumerWidget { onSelected: (String selection) { genderController.text = selection; }, - fieldViewBuilder: ( - context, - controller, - focusNode, - onFieldSubmitted, - ) { - // Initialize the controller with the current value - if (controller.text.isEmpty && - genderController.text.isNotEmpty) { - controller.text = genderController.text; - } + fieldViewBuilder: + ( + context, + controller, + focusNode, + onFieldSubmitted, + ) { + // Initialize the controller with the current value + if (controller.text.isEmpty && + genderController.text.isNotEmpty) { + controller.text = genderController.text; + } - return TextFormField( - controller: controller, - focusNode: focusNode, - decoration: InputDecoration( - labelText: 'gender'.tr(), - ), - onChanged: (value) { - genderController.text = value; + return TextFormField( + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + labelText: 'gender'.tr(), + ), + onChanged: (value) { + genderController.text = value; + }, + onTapOutside: (_) => FocusManager + .instance + .primaryFocus + ?.unfocus(), + ); }, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus - ?.unfocus(), - ); - }, ), ), Expanded( @@ -480,9 +472,8 @@ class UpdateProfileScreen extends HookConsumerWidget { labelText: 'pronouns'.tr(), ), controller: pronounsController, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), ], @@ -496,9 +487,8 @@ class UpdateProfileScreen extends HookConsumerWidget { labelText: 'location'.tr(), ), controller: locationController, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), Expanded( @@ -507,8 +497,8 @@ class UpdateProfileScreen extends HookConsumerWidget { if (textEditingValue.text.isEmpty) { return const Iterable.empty(); } - final lowercaseQuery = - textEditingValue.text.toLowerCase(); + final lowercaseQuery = textEditingValue.text + .toLowerCase(); return getAvailableTz().where((tz) { return tz.toLowerCase().contains(lowercaseQuery); }); @@ -516,46 +506,49 @@ class UpdateProfileScreen extends HookConsumerWidget { onSelected: (String selection) { timeZoneController.text = selection; }, - fieldViewBuilder: ( - context, - controller, - focusNode, - onFieldSubmitted, - ) { - // Sync the controller with timeZoneController when the widget is built - if (controller.text != timeZoneController.text) { - controller.text = timeZoneController.text; - } + fieldViewBuilder: + ( + context, + controller, + focusNode, + onFieldSubmitted, + ) { + // Sync the controller with timeZoneController when the widget is built + if (controller.text != + timeZoneController.text) { + controller.text = timeZoneController.text; + } - return TextFormField( - controller: controller, - focusNode: focusNode, - decoration: InputDecoration( - labelText: 'timeZone'.tr(), - suffix: InkWell( - child: const Icon( - Symbols.my_location, - size: 18, + return TextFormField( + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + labelText: 'timeZone'.tr(), + suffix: InkWell( + child: const Icon( + Symbols.my_location, + size: 18, + ), + onTap: () async { + try { + showLoadingModal(context); + final machineTz = + await getMachineTz(); + controller.text = machineTz; + timeZoneController.text = machineTz; + } finally { + if (context.mounted) { + hideLoadingModal(context); + } + } + }, + ), ), - onTap: () async { - try { - showLoadingModal(context); - final machineTz = await getMachineTz(); - controller.text = machineTz; - timeZoneController.text = machineTz; - } finally { - if (context.mounted) { - hideLoadingModal(context); - } - } + onChanged: (value) { + timeZoneController.text = value; }, - ), - ), - onChanged: (value) { - timeZoneController.text = value; + ); }, - ); - }, optionsViewBuilder: (context, onSelected, options) { return Align( alignment: Alignment.topLeft, @@ -569,21 +562,21 @@ class UpdateProfileScreen extends HookConsumerWidget { child: ListView.builder( padding: const EdgeInsets.all(8.0), itemCount: options.length, - itemBuilder: ( - BuildContext context, - int index, - ) { - final option = options.elementAt(index); - return ListTile( - title: Text( - option, - overflow: TextOverflow.ellipsis, - ), - onTap: () { - onSelected(option); + itemBuilder: + (BuildContext context, int index) { + final option = options.elementAt( + index, + ); + return ListTile( + title: Text( + option, + overflow: TextOverflow.ellipsis, + ), + onTap: () { + onSelected(option); + }, + ); }, - ); - }, ), ), ), @@ -644,10 +637,9 @@ class UpdateProfileScreen extends HookConsumerWidget { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: - Theme.of( - context, - ).colorScheme.surfaceContainerHighest, + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Column( @@ -664,25 +656,23 @@ class UpdateProfileScreen extends HookConsumerWidget { type: usernameColorType.value, value: usernameColorType.value == 'plain' && - usernameColorValue - .text - .isNotEmpty - ? usernameColorValue.text - : null, + usernameColorValue.text.isNotEmpty + ? usernameColorValue.text + : null, direction: usernameColorType.value == - 'gradient' && - usernameColorDirection - .text - .isNotEmpty - ? usernameColorDirection.text - : null, + 'gradient' && + usernameColorDirection + .text + .isNotEmpty + ? usernameColorDirection.text + : null, colors: usernameColorType.value == 'gradient' - ? usernameColorColors.value - .where((c) => c.isNotEmpty) - .toList() - : null, + ? usernameColorColors.value + .where((c) => c.isNotEmpty) + .toList() + : null, ), ), ); @@ -724,10 +714,9 @@ class UpdateProfileScreen extends HookConsumerWidget { ? Symbols.check_circle : Symbols.error, size: 16, - color: - canUseColor - ? Colors.green - : Colors.red, + color: canUseColor + ? Colors.green + : Colors.red, ), const Gap(4), Text( @@ -736,10 +725,9 @@ class UpdateProfileScreen extends HookConsumerWidget { : 'upgradeRequired'.tr(), style: TextStyle( fontSize: 12, - color: - canUseColor - ? Colors.green - : Colors.red, + color: canUseColor + ? Colors.green + : Colors.red, ), ), ], @@ -792,34 +780,35 @@ class UpdateProfileScreen extends HookConsumerWidget { onSelected: (String selection) { usernameColorValue.text = selection; }, - fieldViewBuilder: ( - context, - controller, - focusNode, - onFieldSubmitted, - ) { - // Initialize the controller with the current value - if (controller.text.isEmpty && - usernameColorValue.text.isNotEmpty) { - controller.text = usernameColorValue.text; - } + fieldViewBuilder: + ( + context, + controller, + focusNode, + onFieldSubmitted, + ) { + // Initialize the controller with the current value + if (controller.text.isEmpty && + usernameColorValue.text.isNotEmpty) { + controller.text = usernameColorValue.text; + } - return TextFormField( - controller: controller, - focusNode: focusNode, - decoration: InputDecoration( - labelText: 'colorValue'.tr(), - hintText: 'e.g. red or #ff6600', - ), - onChanged: (value) { - usernameColorValue.text = value; + return TextFormField( + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + labelText: 'colorValue'.tr(), + hintText: 'e.g. red or #ff6600', + ), + onChanged: (value) { + usernameColorValue.text = value; + }, + onTapOutside: (_) => FocusManager + .instance + .primaryFocus + ?.unfocus(), + ); }, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus - ?.unfocus(), - ); - }, ), if (usernameColorType.value == 'gradient') ...[ DropdownButtonFormField2( @@ -862,10 +851,9 @@ class UpdateProfileScreen extends HookConsumerWidget { child: Text('gradientDirectionToTopLeft'.tr()), ), ], - value: - usernameColorDirection.text.isNotEmpty - ? usernameColorDirection.text - : 'to right', + value: usernameColorDirection.text.isNotEmpty + ? usernameColorDirection.text + : 'to right', onChanged: (value) { usernameColorDirection.text = value ?? 'to right'; }, @@ -911,21 +899,19 @@ class UpdateProfileScreen extends HookConsumerWidget { onChanged: (value) { usernameColorColors.value[i] = value; }, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus - ?.unfocus(), + onTapOutside: (_) => FocusManager + .instance + .primaryFocus + ?.unfocus(), ), ), IconButton( icon: const Icon(Symbols.delete), onPressed: () { - usernameColorColors.value = - usernameColorColors.value - .whereIndexed( - (idx, _) => idx != i, - ) - .toList(); + usernameColorColors + .value = usernameColorColors.value + .whereIndexed((idx, _) => idx != i) + .toList(); }, ), ], @@ -968,10 +954,10 @@ class UpdateProfileScreen extends HookConsumerWidget { name: value, ); }, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus - ?.unfocus(), + onTapOutside: (_) => FocusManager + .instance + .primaryFocus + ?.unfocus(), ), ), const Gap(8), @@ -987,19 +973,18 @@ class UpdateProfileScreen extends HookConsumerWidget { url: value, ); }, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus - ?.unfocus(), + onTapOutside: (_) => FocusManager + .instance + .primaryFocus + ?.unfocus(), ), ), IconButton( icon: const Icon(Symbols.delete), onPressed: () { - links.value = - links.value - .whereIndexed((idx, _) => idx != i) - .toList(); + links.value = links.value + .whereIndexed((idx, _) => idx != i) + .toList(); }, ), ], diff --git a/lib/screens/account/profile.dart b/lib/screens/account/profile.dart index 3c070300..e7fded52 100644 --- a/lib/screens/account/profile.dart +++ b/lib/screens/account/profile.dart @@ -57,7 +57,7 @@ class _AccountBasicInfo extends StatelessWidget { return Card( child: Builder( builder: (context) { - final hasBackground = data.profile.background?.id != null; + final hasBackground = data.profile.background != null; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -962,7 +962,7 @@ class AccountProfileScreen extends HookConsumerWidget { flexibleSpace: Stack( children: [ Positioned.fill( - child: data.profile.background?.id != null + child: data.profile.background != null ? CloudImageWidget( file: data.profile.background, ) diff --git a/lib/screens/account/relationship.dart b/lib/screens/account/relationship.dart index 72ea3910..36ce629d 100644 --- a/lib/screens/account/relationship.dart +++ b/lib/screens/account/relationship.dart @@ -113,7 +113,7 @@ class RelationshipListTile extends StatelessWidget { contentPadding: const EdgeInsets.only(left: 16, right: 12), leading: AccountPfcGestureDetector( uname: account.name, - child: ProfilePictureWidget(fileId: account.profile.picture?.id), + child: ProfilePictureWidget(file: account.profile.picture), ), title: Row( spacing: 6, diff --git a/lib/screens/chat/chat_form.dart b/lib/screens/chat/chat_form.dart index d7163895..690f7b08 100644 --- a/lib/screens/chat/chat_form.dart +++ b/lib/screens/chat/chat_form.dart @@ -178,7 +178,7 @@ class EditChatScreen extends HookConsumerWidget { bottom: -32, child: GestureDetector( child: ProfilePictureWidget( - fileId: picture.value?.id, + file: picture.value, radius: 40, fallbackIcon: Symbols.group, ), diff --git a/lib/screens/chat/public_room_preview.dart b/lib/screens/chat/public_room_preview.dart index 40f17e8c..13010783 100644 --- a/lib/screens/chat/public_room_preview.dart +++ b/lib/screens/chat/public_room_preview.dart @@ -98,15 +98,15 @@ class PublicRoomPreview extends HookConsumerWidget { SizedBox( height: 26, width: 26, - child: (room.type == 1 && room.picture?.id == null) + child: (room.type == 1 && room.picture == null) ? SplitAvatarWidget( - filesId: room.members! - .map((e) => e.account.profile.picture?.id) + files: room.members! + .map((e) => e.account.profile.picture) .toList(), ) - : room.picture?.id != null + : room.picture != null ? ProfilePictureWidget( - fileId: room.picture?.id, + file: room.picture, fallbackIcon: Symbols.chat, ) : CircleAvatar( @@ -131,15 +131,15 @@ class PublicRoomPreview extends HookConsumerWidget { SizedBox( height: 26, width: 26, - child: (room.type == 1 && room.picture?.id == null) + child: (room.type == 1 && room.picture == null) ? SplitAvatarWidget( - filesId: room.members! - .map((e) => e.account.profile.picture?.id) + files: room.members! + .map((e) => e.account.profile.picture) .toList(), ) - : room.picture?.id != null + : room.picture != null ? ProfilePictureWidget( - fileId: room.picture?.id, + file: room.picture, fallbackIcon: Symbols.chat, ) : CircleAvatar( diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 243ea783..6d0d3686 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -427,15 +427,15 @@ class ChatRoomScreen extends HookConsumerWidget { child: SizedBox( height: 26, width: 26, - child: (room!.type == 1 && room.picture?.id == null) + child: (room!.type == 1 && room.picture == null) ? SplitAvatarWidget( - filesId: getValidMembers( + files: getValidMembers( room.members!, - ).map((e) => e.account.profile.picture?.id).toList(), + ).map((e) => e.account.profile.picture).toList(), ) - : room.picture?.id != null + : room.picture != null ? ProfilePictureWidget( - fileId: room.picture?.id, + file: room.picture, fallbackIcon: Symbols.chat, ) : CircleAvatar( @@ -473,15 +473,15 @@ class ChatRoomScreen extends HookConsumerWidget { child: SizedBox( height: 28, width: 28, - child: (room!.type == 1 && room.picture?.id == null) + child: (room!.type == 1 && room.picture == null) ? SplitAvatarWidget( - filesId: getValidMembers( + files: getValidMembers( room.members!, - ).map((e) => e.account.profile.picture?.id).toList(), + ).map((e) => e.account.profile.picture).toList(), ) - : room.picture?.id != null + : room.picture != null ? ProfilePictureWidget( - fileId: room.picture?.id, + file: room.picture, fallbackIcon: Symbols.chat, ) : CircleAvatar( diff --git a/lib/screens/chat/room_detail.dart b/lib/screens/chat/room_detail.dart index 0a851437..fbfca430 100644 --- a/lib/screens/chat/room_detail.dart +++ b/lib/screens/chat/room_detail.dart @@ -279,9 +279,8 @@ class ChatDetailScreen extends HookConsumerWidget { leading: PageBackButton(shadows: [iconShadow]), flexibleSpace: FlexibleSpaceBar( background: - (currentRoom!.type == 1 && - currentRoom.background?.id != null) - ? CloudImageWidget(fileId: currentRoom.background!.id) + (currentRoom!.type == 1 && currentRoom.background != null) + ? CloudImageWidget(file: currentRoom.background!) : (currentRoom.type == 1 && currentRoom.members!.length == 1 && currentRoom @@ -293,17 +292,16 @@ class ChatDetailScreen extends HookConsumerWidget { ?.id != null) ? CloudImageWidget( - fileId: currentRoom + file: currentRoom .members! .first .account .profile - .background! - .id, + .background!, ) - : currentRoom.background?.id != null + : currentRoom.background != null ? CloudImageWidget( - fileId: currentRoom.background!.id, + file: currentRoom.background!, fit: BoxFit.cover, ) : Container( @@ -702,7 +700,7 @@ class _ChatMemberListSheet extends HookConsumerWidget { leading: AccountPfcGestureDetector( uname: member.account.name, child: ProfilePictureWidget( - fileId: member.account.profile.picture?.id, + file: member.account.profile.picture, ), ), title: Row( diff --git a/lib/screens/creators/hub.dart b/lib/screens/creators/hub.dart index 52a010dd..6ffcd527 100644 --- a/lib/screens/creators/hub.dart +++ b/lib/screens/creators/hub.dart @@ -155,7 +155,7 @@ class PublisherSelector extends StatelessWidget { if (isReadOnly || currentPublisher == null) { return ProfilePictureWidget( radius: 16, - fileId: currentPublisher?.picture?.id, + file: currentPublisher?.picture, ).center().padding(right: 8); } @@ -179,7 +179,7 @@ class PublisherSelector extends StatelessWidget { .map( (e) => ProfilePictureWidget( radius: 16, - fileId: e.value?.picture?.id, + file: e.value?.picture, ).center().padding(right: 8), ) .toList(); @@ -355,10 +355,7 @@ class CreatorHubScreen extends HookConsumerWidget { value: item, child: ListTile( minTileHeight: 48, - leading: ProfilePictureWidget( - radius: 16, - fileId: item.picture?.id, - ), + leading: ProfilePictureWidget(radius: 16, file: item.picture), title: Text(item.nick), subtitle: Text('@${item.name}'), trailing: currentPublisher.value?.id == item.id @@ -889,7 +886,7 @@ class _PublisherMemberListSheet extends HookConsumerWidget { return ListTile( contentPadding: EdgeInsets.only(left: 16, right: 12), leading: ProfilePictureWidget( - fileId: member.account!.profile.picture?.id, + file: member.account!.profile.picture, ), title: Row( spacing: 6, @@ -1137,7 +1134,7 @@ class _PublisherInviteSheet extends HookConsumerWidget { final invite = items[index]; return ListTile( leading: ProfilePictureWidget( - fileId: invite.publisher!.picture?.id, + file: invite.publisher!.picture, fallbackIcon: Symbols.group, ), title: Text(invite.publisher!.nick), diff --git a/lib/screens/creators/stickers/pack_detail.dart b/lib/screens/creators/stickers/pack_detail.dart index d2206b75..6990b571 100644 --- a/lib/screens/creators/stickers/pack_detail.dart +++ b/lib/screens/creators/stickers/pack_detail.dart @@ -69,157 +69,141 @@ class StickerPackDetailContent extends HookConsumerWidget { } return pack.when( - data: - (pack) => Column( - crossAxisAlignment: CrossAxisAlignment.start, + data: (pack) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + Text(pack!.description), + Row( + spacing: 4, children: [ - Text(pack!.description), - Row( - spacing: 4, - children: [ - const Icon(Symbols.folder, size: 16), - Text( - '${packContent.value?.length ?? 0}/24', - style: GoogleFonts.robotoMono(), - ), - ], - ).opacity(0.85), - Row( - spacing: 4, - children: [ - const Icon(Symbols.sell, size: 16), - Text(pack.prefix, style: GoogleFonts.robotoMono()), - ], - ).opacity(0.85), - Row( - spacing: 4, - children: [ - const Icon(Symbols.tag, size: 16), - Flexible( - child: SelectableText( - pack.id, - maxLines: 1, - style: GoogleFonts.robotoMono(), - ), - ), - ], - ).opacity(0.85), + const Icon(Symbols.folder, size: 16), + Text( + '${packContent.value?.length ?? 0}/24', + style: GoogleFonts.robotoMono(), + ), ], - ).padding(horizontal: 24, vertical: 24), - const Divider(height: 1), - Expanded( - child: packContent.when( - data: - (stickers) => RefreshIndicator( - onRefresh: - () => ref.refresh( - stickerPackContentProvider(id).future, - ), - child: GridView.builder( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 20, - ), - gridDelegate: - const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 80, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - ), - itemCount: stickers.length, - itemBuilder: (context, index) { - final sticker = stickers[index]; - return ContextMenuWidget( - menuProvider: (_) { - return Menu( - children: [ - MenuAction( - title: 'stickerCopyPlaceholder'.tr(), - image: MenuImage.icon(Symbols.copy_all), - callback: () { - Clipboard.setData( - ClipboardData( - text: - ':${pack.prefix}+${sticker.slug}:', - ), - ); - }, - ), - MenuSeparator(), - MenuAction( - title: 'edit'.tr(), - image: MenuImage.icon(Symbols.edit), - callback: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: - (context) => SheetScaffold( - titleText: 'editSticker'.tr(), - child: StickerForm( - packId: id, - id: sticker.id, - ), - ), - ).then((value) { - if (value != null) { - ref.invalidate( - stickerPackContentProvider(id), - ); - } - }); - }, - ), - MenuAction( - title: 'delete'.tr(), - image: MenuImage.icon(Symbols.delete), - callback: () { - deleteSticker(sticker); - }, - ), - ], + ).opacity(0.85), + Row( + spacing: 4, + children: [ + const Icon(Symbols.sell, size: 16), + Text(pack.prefix, style: GoogleFonts.robotoMono()), + ], + ).opacity(0.85), + Row( + spacing: 4, + children: [ + const Icon(Symbols.tag, size: 16), + Flexible( + child: SelectableText( + pack.id, + maxLines: 1, + style: GoogleFonts.robotoMono(), + ), + ), + ], + ).opacity(0.85), + ], + ).padding(horizontal: 24, vertical: 24), + const Divider(height: 1), + Expanded( + child: packContent.when( + data: (stickers) => RefreshIndicator( + onRefresh: () => + ref.refresh(stickerPackContentProvider(id).future), + child: GridView.builder( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 20, + ), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 80, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + ), + itemCount: stickers.length, + itemBuilder: (context, index) { + final sticker = stickers[index]; + return ContextMenuWidget( + menuProvider: (_) { + return Menu( + children: [ + MenuAction( + title: 'stickerCopyPlaceholder'.tr(), + image: MenuImage.icon(Symbols.copy_all), + callback: () { + Clipboard.setData( + ClipboardData( + text: ':${pack.prefix}+${sticker.slug}:', + ), ); }, - child: ClipRRect( - borderRadius: BorderRadius.all( - Radius.circular(8), - ), - child: Container( - decoration: BoxDecoration( - color: - Theme.of( - context, - ).colorScheme.surfaceContainer, - borderRadius: BorderRadius.all( - Radius.circular(8), + ), + MenuSeparator(), + MenuAction( + title: 'edit'.tr(), + image: MenuImage.icon(Symbols.edit), + callback: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => SheetScaffold( + titleText: 'editSticker'.tr(), + child: StickerForm( + packId: id, + id: sticker.id, ), ), - child: CloudImageWidget( - fileId: sticker.image.id, - fit: BoxFit.contain, - ), - ), - ), - ); - }, + ).then((value) { + if (value != null) { + ref.invalidate( + stickerPackContentProvider(id), + ); + } + }); + }, + ), + MenuAction( + title: 'delete'.tr(), + image: MenuImage.icon(Symbols.delete), + callback: () { + deleteSticker(sticker); + }, + ), + ], + ); + }, + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(8)), + child: Container( + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainer, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: CloudImageWidget( + file: sticker.image, + fit: BoxFit.contain, + ), ), ), - error: - (err, _) => - Text( - 'Error: $err', - ).textAlignment(TextAlign.center).center(), - loading: () => const CircularProgressIndicator().center(), + ); + }, ), ), - ], + error: (err, _) => + Text('Error: $err').textAlignment(TextAlign.center).center(), + loading: () => const CircularProgressIndicator().center(), + ), ), - error: - (err, _) => - Text('Error: $err').textAlignment(TextAlign.center).center(), + ], + ), + error: (err, _) => + Text('Error: $err').textAlignment(TextAlign.center).center(), loading: () => const CircularProgressIndicator().center(), ); } @@ -241,65 +225,60 @@ class StickerPackActionMenu extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return PopupMenuButton( icon: Icon(Icons.more_vert, shadows: [iconShadow]), - itemBuilder: - (context) => [ - PopupMenuItem( - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: - (context) => SheetScaffold( - titleText: 'editStickerPack'.tr(), - child: StickerPackForm( - pubName: pubName, - packId: packId, - ), - ), - ).then((value) { - if (value != null) { - ref.invalidate(stickerPackProvider(packId)); - } - }); - }, - child: Row( - children: [ - Icon( - Icons.edit, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - const Gap(12), - const Text('editStickerPack').tr(), - ], + itemBuilder: (context) => [ + PopupMenuItem( + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => SheetScaffold( + titleText: 'editStickerPack'.tr(), + child: StickerPackForm(pubName: pubName, packId: packId), ), - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.delete, color: Colors.red), - const Gap(12), - const Text( - 'deleteStickerPack', - style: TextStyle(color: Colors.red), - ).tr(), - ], + ).then((value) { + if (value != null) { + ref.invalidate(stickerPackProvider(packId)); + } + }); + }, + child: Row( + children: [ + Icon( + Icons.edit, + color: Theme.of(context).colorScheme.onSecondaryContainer, ), - onTap: () { - showConfirmAlert( - 'deleteStickerPackHint'.tr(), - 'deleteStickerPack'.tr(), - isDanger: true, - ).then((confirm) { - if (confirm) { - final client = ref.watch(apiClientProvider); - client.delete('/sphere/stickers/$packId'); - ref.invalidate(stickerPacksProvider); - if (context.mounted) context.pop(true); - } - }); - }, - ), - ], + const Gap(12), + const Text('editStickerPack').tr(), + ], + ), + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.delete, color: Colors.red), + const Gap(12), + const Text( + 'deleteStickerPack', + style: TextStyle(color: Colors.red), + ).tr(), + ], + ), + onTap: () { + showConfirmAlert( + 'deleteStickerPackHint'.tr(), + 'deleteStickerPack'.tr(), + isDanger: true, + ).then((confirm) { + if (confirm) { + final client = ref.watch(apiClientProvider); + client.delete('/sphere/stickers/$packId'); + ref.invalidate(stickerPacksProvider); + if (context.mounted) context.pop(true); + } + }); + }, + ), + ], ); } } @@ -372,10 +351,9 @@ class StickerForm extends HookConsumerWidget { color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadius.all(Radius.circular(8)), ), - child: - (image.value?.isEmpty ?? true) - ? const SizedBox.shrink() - : CloudImageWidget(fileId: image.value!), + child: (image.value?.isEmpty ?? true) + ? const SizedBox.shrink() + : CloudImageWidget(fileId: image.value!), ), ), ), @@ -383,10 +361,8 @@ class StickerForm extends HookConsumerWidget { onPressed: () { showModalBottomSheet( context: context, - builder: - (context) => CloudFilePicker( - allowedTypes: {UniversalFileType.image}, - ), + builder: (context) => + CloudFilePicker(allowedTypes: {UniversalFileType.image}), ).then((value) { if (value == null) return; image.value = value[0].id; @@ -412,8 +388,8 @@ class StickerForm extends HookConsumerWidget { borderRadius: BorderRadius.all(Radius.circular(12)), ), ), - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ], ), diff --git a/lib/screens/developers/app_detail.dart b/lib/screens/developers/app_detail.dart index c395c85f..861b909f 100644 --- a/lib/screens/developers/app_detail.dart +++ b/lib/screens/developers/app_detail.dart @@ -146,7 +146,7 @@ class _AppOverview extends StatelessWidget { left: 20, bottom: -32, child: ProfilePictureWidget( - fileId: app.picture?.id, + file: app.picture, radius: 40, fallbackIcon: Symbols.apps, ), diff --git a/lib/screens/developers/apps.dart b/lib/screens/developers/apps.dart index 36bfeffc..358e50ad 100644 --- a/lib/screens/developers/apps.dart +++ b/lib/screens/developers/apps.dart @@ -153,7 +153,7 @@ class CustomAppsScreen extends HookConsumerWidget { ListTile( title: Text(app.name), leading: ProfilePictureWidget( - fileId: app.picture?.id, + file: app.picture, fallbackIcon: Symbols.apps, ), subtitle: Text( diff --git a/lib/screens/developers/bot_detail.dart b/lib/screens/developers/bot_detail.dart index c05d161e..f4ed91ab 100644 --- a/lib/screens/developers/bot_detail.dart +++ b/lib/screens/developers/bot_detail.dart @@ -143,7 +143,7 @@ class _BotOverview extends StatelessWidget { left: 20, bottom: -32, child: ProfilePictureWidget( - fileId: bot.account.profile.picture?.id, + file: bot.account.profile.picture, radius: 40, fallbackIcon: Symbols.smart_toy, ), diff --git a/lib/screens/developers/edit_app.dart b/lib/screens/developers/edit_app.dart index da7d7caa..d2ade277 100644 --- a/lib/screens/developers/edit_app.dart +++ b/lib/screens/developers/edit_app.dart @@ -51,10 +51,9 @@ class EditAppScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final isNew = id == null; - final app = - isNew - ? null - : ref.watch(customAppProvider(publisherName, projectId, id!)); + final app = isNew + ? null + : ref.watch(customAppProvider(publisherName, projectId, id!)); final formKey = useMemoized(() => GlobalKey()); @@ -139,14 +138,10 @@ class EditAppScreen extends HookConsumerWidget { submitting.value = true; try { - final cloudFile = - await FileUploader.createCloudFile( - ref: ref, - fileData: UniversalFile( - data: result, - type: UniversalFileType.image, - ), - ).future; + final cloudFile = await FileUploader.createCloudFile( + ref: ref, + fileData: UniversalFile(data: result, type: UniversalFileType.image), + ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); } @@ -169,41 +164,40 @@ class EditAppScreen extends HookConsumerWidget { showModalBottomSheet( context: context, isScrollControlled: true, - builder: - (context) => SheetScaffold( - titleText: 'addScope'.tr(), - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextFormField( - controller: scopeController, - decoration: InputDecoration( - labelText: 'scopeName'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - ), + builder: (context) => SheetScaffold( + titleText: 'addScope'.tr(), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + controller: scopeController, + decoration: InputDecoration( + labelText: 'scopeName'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), ), - const SizedBox(height: 20), - FilledButton.tonalIcon( - onPressed: () { - if (scopeController.text.isNotEmpty) { - allowedScopes.value = [ - ...allowedScopes.value, - scopeController.text, - ]; - Navigator.pop(context); - } - }, - icon: const Icon(Symbols.add), - label: Text('add').tr(), - ), - ], + ), ), - ), + const SizedBox(height: 20), + FilledButton.tonalIcon( + onPressed: () { + if (scopeController.text.isNotEmpty) { + allowedScopes.value = [ + ...allowedScopes.value, + scopeController.text, + ]; + Navigator.pop(context); + } + }, + icon: const Icon(Symbols.add), + label: Text('add').tr(), + ), + ], ), + ), + ), ); } @@ -212,57 +206,56 @@ class EditAppScreen extends HookConsumerWidget { showModalBottomSheet( context: context, isScrollControlled: true, - builder: - (context) => SheetScaffold( - titleText: 'addRedirectUri'.tr(), - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextFormField( - controller: uriController, - decoration: InputDecoration( - labelText: 'redirectUri'.tr(), - hintText: 'https://example.com/auth/callback', - helperText: 'redirectUriHint'.tr(), - helperMaxLines: 3, - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - ), - keyboardType: TextInputType.url, - validator: (value) { - if (value == null || value.isEmpty) { - return 'uriRequired'.tr(); - } - final uri = Uri.tryParse(value); - if (uri == null || !uri.hasAbsolutePath) { - return 'invalidUri'.tr(); - } - return null; - }, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + builder: (context) => SheetScaffold( + titleText: 'addRedirectUri'.tr(), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + controller: uriController, + decoration: InputDecoration( + labelText: 'redirectUri'.tr(), + hintText: 'https://example.com/auth/callback', + helperText: 'redirectUriHint'.tr(), + helperMaxLines: 3, + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), ), - const SizedBox(height: 20), - FilledButton.tonalIcon( - onPressed: () { - if (uriController.text.isNotEmpty) { - redirectUris.value = [ - ...redirectUris.value, - uriController.text, - ]; - Navigator.pop(context); - } - }, - icon: const Icon(Symbols.add), - label: Text('add').tr(), - ), - ], + ), + keyboardType: TextInputType.url, + validator: (value) { + if (value == null || value.isEmpty) { + return 'uriRequired'.tr(); + } + final uri = Uri.tryParse(value); + if (uri == null || !uri.hasAbsolutePath) { + return 'invalidUri'.tr(); + } + return null; + }, + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), - ), + const SizedBox(height: 20), + FilledButton.tonalIcon( + onPressed: () { + if (uriController.text.isNotEmpty) { + redirectUris.value = [ + ...redirectUris.value, + uriController.text, + ]; + Navigator.pop(context); + } + }, + icon: const Icon(Symbols.add), + label: Text('add').tr(), + ), + ], ), + ), + ), ); } @@ -275,31 +268,28 @@ class EditAppScreen extends HookConsumerWidget { 'picture_id': picture.value?.id, 'background_id': background.value?.id, 'links': { - 'home_page': - homePageController.text.isNotEmpty - ? homePageController.text - : null, - 'privacy_policy': - privacyPolicyController.text.isNotEmpty - ? privacyPolicyController.text - : null, - 'terms_of_service': - termsController.text.isNotEmpty ? termsController.text : null, + 'home_page': homePageController.text.isNotEmpty + ? homePageController.text + : null, + 'privacy_policy': privacyPolicyController.text.isNotEmpty + ? privacyPolicyController.text + : null, + 'terms_of_service': termsController.text.isNotEmpty + ? termsController.text + : null, }, - 'oauth_config': - oauthEnabled.value - ? { - 'redirect_uris': redirectUris.value, - 'post_logout_redirect_uris': - postLogoutUris.value.isNotEmpty - ? postLogoutUris.value - : null, - 'allowed_scopes': allowedScopes.value, - 'allowed_grant_types': allowedGrantTypes.value, - 'require_pkce': requirePkce.value, - 'allow_offline_access': allowOfflineAccess.value, - } - : null, + 'oauth_config': oauthEnabled.value + ? { + 'redirect_uris': redirectUris.value, + 'post_logout_redirect_uris': postLogoutUris.value.isNotEmpty + ? postLogoutUris.value + : null, + 'allowed_scopes': allowedScopes.value, + 'allowed_grant_types': allowedGrantTypes.value, + 'require_pkce': requirePkce.value, + 'allow_offline_access': allowOfflineAccess.value, + } + : null, }; try { showLoadingModal(context); @@ -326,287 +316,269 @@ class EditAppScreen extends HookConsumerWidget { } } - final bodyContent = - app == null && !isNew - ? const Center(child: CircularProgressIndicator()) - : app?.hasError == true && !isNew - ? ResponseErrorWidget( - error: app!.error, - onRetry: - () => ref.invalidate( - customAppProvider(publisherName, projectId, id!), - ), - ) - : SingleChildScrollView( - child: Column( - children: [ - AspectRatio( - aspectRatio: 16 / 7, - child: Stack( - clipBehavior: Clip.none, - fit: StackFit.expand, - children: [ - GestureDetector( - child: Container( - color: - Theme.of( - context, - ).colorScheme.surfaceContainerHigh, - child: - background.value != null - ? CloudFileWidget( - item: background.value!, - fit: BoxFit.cover, - ) - : const SizedBox.shrink(), + final bodyContent = app == null && !isNew + ? const Center(child: CircularProgressIndicator()) + : app?.hasError == true && !isNew + ? ResponseErrorWidget( + error: app!.error, + onRetry: () => ref.invalidate( + customAppProvider(publisherName, projectId, id!), + ), + ) + : SingleChildScrollView( + child: Column( + children: [ + AspectRatio( + aspectRatio: 16 / 7, + child: Stack( + clipBehavior: Clip.none, + fit: StackFit.expand, + children: [ + GestureDetector( + child: Container( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + child: background.value != null + ? CloudFileWidget( + item: background.value!, + fit: BoxFit.cover, + ) + : const SizedBox.shrink(), + ), + onTap: () { + setPicture('background'); + }, + ), + Positioned( + left: 20, + bottom: -32, + child: GestureDetector( + child: ProfilePictureWidget( + file: picture.value, + radius: 40, + fallbackIcon: Symbols.apps, ), onTap: () { - setPicture('background'); + setPicture('picture'); }, ), - Positioned( - left: 20, - bottom: -32, - child: GestureDetector( - child: ProfilePictureWidget( - fileId: picture.value?.id, - radius: 40, - fallbackIcon: Symbols.apps, - ), - onTap: () { - setPicture('picture'); - }, - ), - ), - ], - ), - ).padding(bottom: 32), - Form( - key: formKey, - child: Column( - children: [ - TextFormField( - controller: nameController, - decoration: InputDecoration( - labelText: 'name'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), - ), - const SizedBox(height: 16), - TextFormField( - controller: slugController, - decoration: InputDecoration( - labelText: 'slug'.tr(), - helperText: 'slugHint'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), - ), - const SizedBox(height: 16), - TextFormField( - controller: descriptionController, - decoration: InputDecoration( - labelText: 'description'.tr(), - alignLabelWithHint: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - maxLines: 3, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), - ), - const SizedBox(height: 16), - ExpansionPanelList( - expansionCallback: (index, isExpanded) { - switch (index) { - case 0: - enableLinks.value = isExpanded; - break; - case 1: - oauthEnabled.value = isExpanded; - break; - } - }, - children: [ - ExpansionPanel( - headerBuilder: - (context, isExpanded) => - ListTile(title: Text('appLinks').tr()), - body: Column( - spacing: 16, - children: [ - TextFormField( - controller: homePageController, - decoration: InputDecoration( - labelText: 'homePageUrl'.tr(), - hintText: 'https://example.com', - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - keyboardType: TextInputType.url, - ), - TextFormField( - controller: privacyPolicyController, - decoration: InputDecoration( - labelText: 'privacyPolicyUrl'.tr(), - hintText: 'https://example.com/privacy', - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - keyboardType: TextInputType.url, - ), - TextFormField( - controller: termsController, - decoration: InputDecoration( - labelText: 'termsOfServiceUrl'.tr(), - hintText: 'https://example.com/terms', - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - keyboardType: TextInputType.url, - ), - ], - ).padding(horizontal: 16, bottom: 24), - isExpanded: enableLinks.value, - ), - ExpansionPanel( - headerBuilder: - (context, isExpanded) => - ListTile(title: Text('oauthConfig').tr()), - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('redirectUris'.tr()), - Card( - margin: const EdgeInsets.symmetric( - vertical: 8, - ), - child: Column( - children: [ - ...redirectUris.value.map( - (uri) => ListTile( - title: Text(uri), - trailing: IconButton( - icon: const Icon(Symbols.delete), - onPressed: () { - redirectUris.value = - redirectUris.value - .where((u) => u != uri) - .toList(); - }, - ), - ), - ), - if (redirectUris.value.isNotEmpty) - const Divider(height: 1), - ListTile( - leading: const Icon(Symbols.add), - title: Text('addRedirectUri'.tr()), - onTap: showAddRedirectUriDialog, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 8, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - Text('allowedScopes'.tr()), - Card( - margin: const EdgeInsets.symmetric( - vertical: 8, - ), - child: Column( - children: [ - ...allowedScopes.value.map( - (scope) => ListTile( - title: Text(scope), - trailing: IconButton( - icon: const Icon(Symbols.delete), - onPressed: () { - allowedScopes.value = - allowedScopes.value - .where( - (s) => s != scope, - ) - .toList(); - }, - ), - ), - ), - if (allowedScopes.value.isNotEmpty) - const Divider(height: 1), - ListTile( - leading: const Icon(Symbols.add), - title: Text('add').tr(), - onTap: showAddScopeDialog, - ), - ], - ), - ), - const SizedBox(height: 16), - SwitchListTile( - title: Text('requirePkce'.tr()), - value: requirePkce.value, - onChanged: - (value) => requirePkce.value = value, - ), - SwitchListTile( - title: Text('allowOfflineAccess'.tr()), - value: allowOfflineAccess.value, - onChanged: - (value) => - allowOfflineAccess.value = value, - ), - ], - ).padding(horizontal: 16, bottom: 24), - isExpanded: oauthEnabled.value, - ), - ], - ), - const SizedBox(height: 16), - Align( - alignment: Alignment.centerRight, - child: TextButton.icon( - onPressed: submitting.value ? null : performAction, - label: Text('saveChanges'.tr()), - icon: const Icon(Symbols.save), - ), - ), - ], - ).padding(all: 24), + ), + ], ), - ], - ), - ); + ).padding(bottom: 32), + Form( + key: formKey, + child: Column( + children: [ + TextFormField( + controller: nameController, + decoration: InputDecoration( + labelText: 'name'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 16), + TextFormField( + controller: slugController, + decoration: InputDecoration( + labelText: 'slug'.tr(), + helperText: 'slugHint'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 16), + TextFormField( + controller: descriptionController, + decoration: InputDecoration( + labelText: 'description'.tr(), + alignLabelWithHint: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + maxLines: 3, + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 16), + ExpansionPanelList( + expansionCallback: (index, isExpanded) { + switch (index) { + case 0: + enableLinks.value = isExpanded; + break; + case 1: + oauthEnabled.value = isExpanded; + break; + } + }, + children: [ + ExpansionPanel( + headerBuilder: (context, isExpanded) => + ListTile(title: Text('appLinks').tr()), + body: Column( + spacing: 16, + children: [ + TextFormField( + controller: homePageController, + decoration: InputDecoration( + labelText: 'homePageUrl'.tr(), + hintText: 'https://example.com', + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + ), + keyboardType: TextInputType.url, + ), + TextFormField( + controller: privacyPolicyController, + decoration: InputDecoration( + labelText: 'privacyPolicyUrl'.tr(), + hintText: 'https://example.com/privacy', + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + ), + keyboardType: TextInputType.url, + ), + TextFormField( + controller: termsController, + decoration: InputDecoration( + labelText: 'termsOfServiceUrl'.tr(), + hintText: 'https://example.com/terms', + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + ), + keyboardType: TextInputType.url, + ), + ], + ).padding(horizontal: 16, bottom: 24), + isExpanded: enableLinks.value, + ), + ExpansionPanel( + headerBuilder: (context, isExpanded) => + ListTile(title: Text('oauthConfig').tr()), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('redirectUris'.tr()), + Card( + margin: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Column( + children: [ + ...redirectUris.value.map( + (uri) => ListTile( + title: Text(uri), + trailing: IconButton( + icon: const Icon(Symbols.delete), + onPressed: () { + redirectUris.value = redirectUris + .value + .where((u) => u != uri) + .toList(); + }, + ), + ), + ), + if (redirectUris.value.isNotEmpty) + const Divider(height: 1), + ListTile( + leading: const Icon(Symbols.add), + title: Text('addRedirectUri'.tr()), + onTap: showAddRedirectUriDialog, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 8, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Text('allowedScopes'.tr()), + Card( + margin: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Column( + children: [ + ...allowedScopes.value.map( + (scope) => ListTile( + title: Text(scope), + trailing: IconButton( + icon: const Icon(Symbols.delete), + onPressed: () { + allowedScopes.value = + allowedScopes.value + .where((s) => s != scope) + .toList(); + }, + ), + ), + ), + if (allowedScopes.value.isNotEmpty) + const Divider(height: 1), + ListTile( + leading: const Icon(Symbols.add), + title: Text('add').tr(), + onTap: showAddScopeDialog, + ), + ], + ), + ), + const SizedBox(height: 16), + SwitchListTile( + title: Text('requirePkce'.tr()), + value: requirePkce.value, + onChanged: (value) => + requirePkce.value = value, + ), + SwitchListTile( + title: Text('allowOfflineAccess'.tr()), + value: allowOfflineAccess.value, + onChanged: (value) => + allowOfflineAccess.value = value, + ), + ], + ).padding(horizontal: 16, bottom: 24), + isExpanded: oauthEnabled.value, + ), + ], + ), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + onPressed: submitting.value ? null : performAction, + label: Text('saveChanges'.tr()), + icon: const Icon(Symbols.save), + ), + ), + ], + ).padding(all: 24), + ), + ], + ), + ); if (isModal) { return bodyContent; diff --git a/lib/screens/developers/edit_bot.dart b/lib/screens/developers/edit_bot.dart index 23f47749..074eea10 100644 --- a/lib/screens/developers/edit_bot.dart +++ b/lib/screens/developers/edit_bot.dart @@ -50,8 +50,9 @@ class EditBotScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final isNew = id == null; - final botData = - isNew ? null : ref.watch(botProvider(publisherName, projectId, id!)); + final botData = isNew + ? null + : ref.watch(botProvider(publisherName, projectId, id!)); final formKey = useMemoized(() => GlobalKey()); final submitting = useState(false); @@ -125,14 +126,10 @@ class EditBotScreen extends HookConsumerWidget { submitting.value = true; try { - final cloudFile = - await FileUploader.createCloudFile( - ref: ref, - fileData: UniversalFile( - data: result, - type: UniversalFileType.image, - ), - ).future; + final cloudFile = await FileUploader.createCloudFile( + ref: ref, + fileData: UniversalFile(data: result, type: UniversalFileType.image), + ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); } @@ -193,284 +190,267 @@ class EditBotScreen extends HookConsumerWidget { } } - final bodyContent = - botData == null && !isNew - ? const Center(child: CircularProgressIndicator()) - : botData?.hasError == true && !isNew - ? ResponseErrorWidget( - error: botData!.error, - onRetry: - () => ref.invalidate( - botProvider(publisherName, projectId, id!), - ), - ) - : SingleChildScrollView( - child: Column( - children: [ - AspectRatio( - aspectRatio: 16 / 7, - child: Stack( - clipBehavior: Clip.none, - fit: StackFit.expand, - children: [ - GestureDetector( - child: Container( - color: - Theme.of( - context, - ).colorScheme.surfaceContainerHigh, - child: - background.value != null - ? CloudFileWidget( - item: background.value!, - fit: BoxFit.cover, - ) - : const SizedBox.shrink(), + final bodyContent = botData == null && !isNew + ? const Center(child: CircularProgressIndicator()) + : botData?.hasError == true && !isNew + ? ResponseErrorWidget( + error: botData!.error, + onRetry: () => + ref.invalidate(botProvider(publisherName, projectId, id!)), + ) + : SingleChildScrollView( + child: Column( + children: [ + AspectRatio( + aspectRatio: 16 / 7, + child: Stack( + clipBehavior: Clip.none, + fit: StackFit.expand, + children: [ + GestureDetector( + child: Container( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + child: background.value != null + ? CloudFileWidget( + item: background.value!, + fit: BoxFit.cover, + ) + : const SizedBox.shrink(), + ), + onTap: () { + setPicture('background'); + }, + ), + Positioned( + left: 20, + bottom: -32, + child: GestureDetector( + child: ProfilePictureWidget( + file: picture.value, + radius: 40, + fallbackIcon: Symbols.smart_toy, ), onTap: () { - setPicture('background'); + setPicture('picture'); }, ), - Positioned( - left: 20, - bottom: -32, - child: GestureDetector( - child: ProfilePictureWidget( - fileId: picture.value?.id, - radius: 40, - fallbackIcon: Symbols.smart_toy, - ), - onTap: () { - setPicture('picture'); - }, - ), - ), - ], - ), - ).padding(bottom: 32), - Form( - key: formKey, - child: Column( - children: [ - TextFormField( - controller: nameController, - decoration: InputDecoration( - labelText: 'name'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - ), - const SizedBox(height: 16), - TextFormField( - controller: nickController, - decoration: InputDecoration( - labelText: 'nickname'.tr(), - alignLabelWithHint: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - ), - const SizedBox(height: 16), - TextFormField( - controller: slugController, - decoration: InputDecoration( - labelText: 'slug'.tr(), - helperText: 'slugHint'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - ), - const SizedBox(height: 16), - TextFormField( - controller: bioController, - decoration: InputDecoration( - labelText: 'bio'.tr(), - alignLabelWithHint: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - maxLines: 3, - ), - const SizedBox(height: 16), - Row( - spacing: 16, - children: [ - Expanded( - child: TextFormField( - controller: firstNameController, - decoration: InputDecoration( - labelText: 'firstName'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - ), - ), - Expanded( - child: TextFormField( - controller: middleNameController, - decoration: InputDecoration( - labelText: 'middleName'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - ), - ), - Expanded( - child: TextFormField( - controller: lastNameController, - decoration: InputDecoration( - labelText: 'lastName'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - spacing: 16, - children: [ - Expanded( - child: TextFormField( - controller: genderController, - decoration: InputDecoration( - labelText: 'gender'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - ), - ), - Expanded( - child: TextFormField( - controller: pronounsController, - decoration: InputDecoration( - labelText: 'pronouns'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - spacing: 16, - children: [ - Expanded( - child: TextFormField( - controller: locationController, - decoration: InputDecoration( - labelText: 'location'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - ), - ), - Expanded( - child: TextFormField( - controller: timeZoneController, - decoration: InputDecoration( - labelText: 'timeZone'.tr(), - border: OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - ), - ), - ), - ], - ), - const SizedBox(height: 16), - GestureDetector( - onTap: () async { - final date = await showDatePicker( - context: context, - initialDate: birthday.value ?? DateTime.now(), - firstDate: DateTime(1900), - lastDate: DateTime.now(), - ); - if (date != null) { - birthday.value = date; - } - }, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).dividerColor, - width: 1, - ), - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'birthday'.tr(), - style: TextStyle( - color: Theme.of(context).hintColor, - ), - ), - Text( - birthday.value != null - ? DateFormat.yMMMd().format( - birthday.value!, - ) - : 'Select a date'.tr(), - ), - ], - ), - ), - ), - const SizedBox(height: 16), - Align( - alignment: Alignment.centerRight, - child: TextButton.icon( - onPressed: submitting.value ? null : performAction, - label: Text('saveChanges').tr(), - icon: const Icon(Symbols.save), - ), - ), - ], - ).padding(all: 24), + ), + ], ), - ], - ), - ); + ).padding(bottom: 32), + Form( + key: formKey, + child: Column( + children: [ + TextFormField( + controller: nameController, + decoration: InputDecoration( + labelText: 'name'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + ), + const SizedBox(height: 16), + TextFormField( + controller: nickController, + decoration: InputDecoration( + labelText: 'nickname'.tr(), + alignLabelWithHint: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + ), + const SizedBox(height: 16), + TextFormField( + controller: slugController, + decoration: InputDecoration( + labelText: 'slug'.tr(), + helperText: 'slugHint'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + ), + const SizedBox(height: 16), + TextFormField( + controller: bioController, + decoration: InputDecoration( + labelText: 'bio'.tr(), + alignLabelWithHint: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + maxLines: 3, + ), + const SizedBox(height: 16), + Row( + spacing: 16, + children: [ + Expanded( + child: TextFormField( + controller: firstNameController, + decoration: InputDecoration( + labelText: 'firstName'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + ), + ), + ), + Expanded( + child: TextFormField( + controller: middleNameController, + decoration: InputDecoration( + labelText: 'middleName'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + ), + ), + ), + Expanded( + child: TextFormField( + controller: lastNameController, + decoration: InputDecoration( + labelText: 'lastName'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + spacing: 16, + children: [ + Expanded( + child: TextFormField( + controller: genderController, + decoration: InputDecoration( + labelText: 'gender'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + ), + ), + ), + Expanded( + child: TextFormField( + controller: pronounsController, + decoration: InputDecoration( + labelText: 'pronouns'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + spacing: 16, + children: [ + Expanded( + child: TextFormField( + controller: locationController, + decoration: InputDecoration( + labelText: 'location'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + ), + ), + ), + Expanded( + child: TextFormField( + controller: timeZoneController, + decoration: InputDecoration( + labelText: 'timeZone'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + GestureDetector( + onTap: () async { + final date = await showDatePicker( + context: context, + initialDate: birthday.value ?? DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime.now(), + ); + if (date != null) { + birthday.value = date; + } + }, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).dividerColor, + width: 1, + ), + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'birthday'.tr(), + style: TextStyle( + color: Theme.of(context).hintColor, + ), + ), + Text( + birthday.value != null + ? DateFormat.yMMMd().format(birthday.value!) + : 'Select a date'.tr(), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + onPressed: submitting.value ? null : performAction, + label: Text('saveChanges').tr(), + icon: const Icon(Symbols.save), + ), + ), + ], + ).padding(all: 24), + ), + ], + ), + ); if (isModal) { return bodyContent; diff --git a/lib/screens/developers/hub.dart b/lib/screens/developers/hub.dart index cbc50721..e1403c5b 100644 --- a/lib/screens/developers/hub.dart +++ b/lib/screens/developers/hub.dart @@ -329,7 +329,7 @@ class DeveloperSelector extends HookConsumerWidget { minTileHeight: 48, leading: ProfilePictureWidget( radius: 16, - fileId: item.publisher?.picture?.id, + file: item.publisher?.picture, ), title: Text(item.publisher!.nick), subtitle: Text('@${item.publisher!.name}'), @@ -348,7 +348,7 @@ class DeveloperSelector extends HookConsumerWidget { if (isReadOnly || currentDeveloper == null) { return ProfilePictureWidget( radius: 16, - fileId: currentDeveloper?.publisher?.picture?.id, + file: currentDeveloper?.publisher?.picture, ).center().padding(right: 8); } @@ -373,7 +373,7 @@ class DeveloperSelector extends HookConsumerWidget { ...developersMenu.map( (e) => ProfilePictureWidget( radius: 16, - fileId: e.value?.publisher?.picture?.id, + file: e.value?.publisher?.picture, ).center().padding(right: 8), ), ]; @@ -928,7 +928,7 @@ class _DeveloperEnrollmentSheet extends HookConsumerWidget { final publisher = items[index]; return ListTile( leading: ProfilePictureWidget( - fileId: publisher.picture?.id, + file: publisher.picture, fallbackIcon: Symbols.group, ), title: Text(publisher.nick), diff --git a/lib/screens/notification.dart b/lib/screens/notification.dart index da5eb878..485bc78d 100644 --- a/lib/screens/notification.dart +++ b/lib/screens/notification.dart @@ -37,7 +37,7 @@ class SkeletonNotificationTile extends StatelessWidget { isThreeLine: true, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), leading: fakePfp != null - ? ProfilePictureWidget(fileId: fakePfp, radius: 20) + ? ProfilePictureWidget(file: null, radius: 20) : CircleAvatar( backgroundColor: Theme.of(context).colorScheme.primaryContainer, child: Icon( diff --git a/lib/screens/posts/compose_article.dart b/lib/screens/posts/compose_article.dart index 281c9c2e..25a07299 100644 --- a/lib/screens/posts/compose_article.dart +++ b/lib/screens/posts/compose_article.dart @@ -34,16 +34,14 @@ class ArticleEditScreen extends HookConsumerWidget { final post = ref.watch(postProvider(id)); return post.when( data: (post) => ArticleComposeScreen(originalPost: post), - loading: - () => AppScaffold( - appBar: AppBar(leading: const PageBackButton()), - body: const Center(child: CircularProgressIndicator()), - ), - error: - (e, _) => AppScaffold( - appBar: AppBar(leading: const PageBackButton()), - body: Text('Error: $e', textAlign: TextAlign.center), - ), + loading: () => AppScaffold( + appBar: AppBar(leading: const PageBackButton()), + body: const Center(child: CircularProgressIndicator()), + ), + error: (e, _) => AppScaffold( + appBar: AppBar(leading: const PageBackButton()), + body: Text('Error: $e', textAlign: TextAlign.center), + ), ); } } @@ -127,8 +125,8 @@ class ArticleComposeScreen extends HookConsumerWidget { final mostRecentDraft = drafts.values.reduce( (a, b) => (a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0)) - ? a - : b, + ? a + : b, ); // Only load if the draft has meaningful content @@ -191,12 +189,11 @@ class ArticleComposeScreen extends HookConsumerWidget { MarkdownTextContent( content: contentValue.text, textStyle: theme.textTheme.bodyMedium, - attachments: - state.attachments.value - .where((e) => e.isOnCloud) - .map((e) => e.data) - .cast() - .toList(), + attachments: state.attachments.value + .where((e) => e.isOnCloud) + .map((e) => e.data) + .cast() + .toList(), ), ], ); @@ -290,22 +287,21 @@ class ArticleComposeScreen extends HookConsumerWidget { onExpansionChanged: (expanded) { isAttachmentsExpanded.value = expanded; }, - collapsedBackgroundColor: - Theme.of(context).colorScheme.surfaceContainer, + collapsedBackgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainer, title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('attachments').tr(), Text( 'articleAttachmentHint'.tr(), - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith( - color: - Theme.of( + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( context, ).colorScheme.onSurfaceVariant, - ), + ), ), ], ), @@ -336,13 +332,12 @@ class ArticleComposeScreen extends HookConsumerWidget { >( context: context, isScrollControlled: true, - builder: - (context) => - AttachmentUploaderSheet( - ref: ref, - state: state, - index: idx, - ), + builder: (context) => + AttachmentUploaderSheet( + ref: ref, + state: state, + index: idx, + ), ); if (config != null) { await ComposeLogic.uploadAttachment( @@ -353,21 +348,20 @@ class ArticleComposeScreen extends HookConsumerWidget { ); } }, - onUpdate: - (value) => - ComposeLogic.updateAttachment( - state, - value, - idx, - ), - onDelete: - () => ComposeLogic.deleteAttachment( + onUpdate: (value) => + ComposeLogic.updateAttachment( + state, + value, + idx, + ), + onDelete: () => + ComposeLogic.deleteAttachment( ref, state, idx, ), - onInsert: - () => ComposeLogic.insertAttachment( + onInsert: () => + ComposeLogic.insertAttachment( ref, state, idx, @@ -413,12 +407,11 @@ class ArticleComposeScreen extends HookConsumerWidget { const SizedBox.shrink(), IconButton( icon: ProfilePictureWidget( - fileId: state.currentPublisher.value?.picture?.id, + file: state.currentPublisher.value?.picture, radius: 12, - fallbackIcon: - state.currentPublisher.value == null - ? Symbols.question_mark - : null, + fallbackIcon: state.currentPublisher.value == null + ? Symbols.question_mark + : null, ), onPressed: () { showModalBottomSheet( @@ -448,30 +441,26 @@ class ArticleComposeScreen extends HookConsumerWidget { valueListenable: state.submitting, builder: (context, submitting, _) { return IconButton( - onPressed: - submitting - ? null - : () => ComposeLogic.performAction( - ref, - state, - context, - originalPost: originalPost, - ), - icon: - submitting - ? SizedBox( - width: 28, - height: 28, - child: const CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2.5, - ), - ).center() - : Icon( - originalPost != null - ? Symbols.edit - : Symbols.upload, + onPressed: submitting + ? null + : () => ComposeLogic.performAction( + ref, + state, + context, + originalPost: originalPost, + ), + icon: submitting + ? SizedBox( + width: 28, + height: 28, + child: const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2.5, ), + ).center() + : Icon( + originalPost != null ? Symbols.edit : Symbols.upload, + ), ); }, ), @@ -483,23 +472,22 @@ class ArticleComposeScreen extends HookConsumerWidget { Expanded( child: Padding( padding: const EdgeInsets.only(left: 16, right: 16), - child: - isWideScreen(context) - ? Row( - spacing: 16, - children: [ - Expanded( - flex: showPreview.value ? 1 : 2, - child: buildEditorPane(), - ), - if (showPreview.value) const VerticalDivider(), - if (showPreview.value) - Expanded(child: buildPreviewPane()), - ], - ) - : showPreview.value - ? buildPreviewPane() - : buildEditorPane(), + child: isWideScreen(context) + ? Row( + spacing: 16, + children: [ + Expanded( + flex: showPreview.value ? 1 : 2, + child: buildEditorPane(), + ), + if (showPreview.value) const VerticalDivider(), + if (showPreview.value) + Expanded(child: buildPreviewPane()), + ], + ) + : showPreview.value + ? buildPreviewPane() + : buildEditorPane(), ), ), diff --git a/lib/screens/posts/publisher_profile.dart b/lib/screens/posts/publisher_profile.dart index 2a155e76..6fb84bbe 100644 --- a/lib/screens/posts/publisher_profile.dart +++ b/lib/screens/posts/publisher_profile.dart @@ -52,7 +52,7 @@ class _PublisherBasisWidget extends StatelessWidget { return Card( child: Builder( builder: (context) { - final hasBackground = data.background?.id != null; + final hasBackground = data.background != null; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -598,7 +598,7 @@ class PublisherProfileScreen extends HookConsumerWidget { flexibleSpace: Stack( children: [ Positioned.fill( - child: data.background?.id != null + child: data.background != null ? CloudImageWidget(file: data.background) : Container( color: Theme.of( diff --git a/lib/screens/realm/realm_detail.dart b/lib/screens/realm/realm_detail.dart index 225db61c..0f59da7b 100644 --- a/lib/screens/realm/realm_detail.dart +++ b/lib/screens/realm/realm_detail.dart @@ -183,8 +183,8 @@ class RealmDetailScreen extends HookConsumerWidget { flexibleSpace: Stack( children: [ Positioned.fill( - child: realm!.background?.id != null - ? CloudImageWidget(fileId: realm.background!.id) + child: realm!.background != null + ? CloudImageWidget(file: realm.background!) : Container( color: Theme.of( context, @@ -281,8 +281,8 @@ class RealmDetailScreen extends HookConsumerWidget { flexibleSpace: Stack( children: [ Positioned.fill( - child: realm!.background?.id != null - ? CloudImageWidget(fileId: realm.background!.id) + child: realm!.background != null + ? CloudImageWidget(file: realm.background!) : Container( color: Theme.of( context, @@ -604,7 +604,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { leading: AccountPfcGestureDetector( uname: member.account!.name, child: ProfilePictureWidget( - fileId: member.account!.profile.picture?.id, + file: member.account!.profile.picture, ), ), title: Row( diff --git a/lib/screens/realm/realm_form.dart b/lib/screens/realm/realm_form.dart index 07d75846..dc8a6410 100644 --- a/lib/screens/realm/realm_form.dart +++ b/lib/screens/realm/realm_form.dart @@ -90,14 +90,10 @@ class EditRealmScreen extends HookConsumerWidget { showLoadingModal(context); submitting.value = true; try { - final cloudFile = - await FileUploader.createCloudFile( - ref: ref, - fileData: UniversalFile( - data: result, - type: UniversalFileType.image, - ), - ).future; + final cloudFile = await FileUploader.createCloudFile( + ref: ref, + fileData: UniversalFile(data: result, type: UniversalFileType.image), + ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); } @@ -162,13 +158,12 @@ class EditRealmScreen extends HookConsumerWidget { GestureDetector( child: Container( color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: - background.value != null - ? CloudFileWidget( - item: background.value!, - fit: BoxFit.cover, - ) - : const SizedBox.shrink(), + child: background.value != null + ? CloudFileWidget( + item: background.value!, + fit: BoxFit.cover, + ) + : const SizedBox.shrink(), ), onTap: () { setPicture('background'); @@ -179,7 +174,7 @@ class EditRealmScreen extends HookConsumerWidget { bottom: -32, child: GestureDetector( child: ProfilePictureWidget( - fileId: picture.value?.id, + file: picture.value, radius: 40, fallbackIcon: Symbols.group, ), @@ -202,15 +197,15 @@ class EditRealmScreen extends HookConsumerWidget { labelText: 'slug'.tr(), helperText: 'slugHint'.tr(), ), - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const SizedBox(height: 16), TextFormField( controller: nameController, decoration: InputDecoration(labelText: 'name'.tr()), - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const SizedBox(height: 16), TextFormField( @@ -221,8 +216,8 @@ class EditRealmScreen extends HookConsumerWidget { ), minLines: 3, maxLines: null, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const SizedBox(height: 16), Card( diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index 2063fffb..c9ab4d10 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -213,7 +213,7 @@ class _RealmInviteSheet extends HookConsumerWidget { final invite = items[index]; return ListTile( leading: ProfilePictureWidget( - fileId: invite.realm!.picture?.id, + file: invite.realm!.picture, fallbackIcon: Symbols.group, ), title: Text(invite.realm!.name), diff --git a/lib/screens/stickers/pack_detail.dart b/lib/screens/stickers/pack_detail.dart index 1a53903f..bee5055a 100644 --- a/lib/screens/stickers/pack_detail.dart +++ b/lib/screens/stickers/pack_detail.dart @@ -163,7 +163,7 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget { child: AspectRatio( aspectRatio: 1, child: CloudImageWidget( - fileId: sticker.image.id, + file: sticker.image, fit: BoxFit.contain, ), ), diff --git a/lib/widgets/account/account_nameplate.dart b/lib/widgets/account/account_nameplate.dart index 93582b64..0c940be1 100644 --- a/lib/widgets/account/account_nameplate.dart +++ b/lib/widgets/account/account_nameplate.dart @@ -24,193 +24,181 @@ class AccountNameplate extends HookConsumerWidget { final user = ref.watch(accountProvider(name)); return Container( - decoration: - isOutlined - ? BoxDecoration( - border: Border.all( - width: 1 / MediaQuery.of(context).devicePixelRatio, - color: Theme.of(context).dividerColor, - ), - borderRadius: const BorderRadius.all(Radius.circular(8)), - ) - : null, + decoration: isOutlined + ? BoxDecoration( + border: Border.all( + width: 1 / MediaQuery.of(context).devicePixelRatio, + color: Theme.of(context).dividerColor, + ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ) + : null, margin: padding, child: Card( margin: EdgeInsets.zero, elevation: 0, color: Colors.transparent, child: user.when( - data: - (account) => - account.profile.background != null - ? AspectRatio( - aspectRatio: 16 / 9, - child: Stack( - children: [ - // Background image - Positioned.fill( - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: CloudFileWidget( - item: account.profile.background!, - fit: BoxFit.cover, - ), - ), + data: (account) => account.profile.background != null + ? AspectRatio( + aspectRatio: 16 / 9, + child: Stack( + children: [ + // Background image + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CloudFileWidget( + item: account.profile.background!, + fit: BoxFit.cover, + ), + ), + ), + // Gradient overlay for text readability + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withOpacity(0.8), + Colors.black.withOpacity(0.1), + Colors.transparent, + ], ), - // Gradient overlay for text readability - Positioned.fill( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Colors.black.withOpacity(0.8), - Colors.black.withOpacity(0.1), - Colors.transparent, - ], - ), - ), + ), + ), + ), + // Content positioned at the bottom + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Row( + children: [ + // Profile picture (equivalent to leading) + ProfilePictureWidget( + file: account.profile.picture, ), - ), - // Content positioned at the bottom - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Row( + const SizedBox(width: 16), + // Text content (equivalent to title and subtitle) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - // Profile picture (equivalent to leading) - ProfilePictureWidget( - fileId: account.profile.picture?.id, - ), - const SizedBox(width: 16), - // Text content (equivalent to title and subtitle) - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - AccountName( - account: account, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - Text( - '@${account.name}', - ).textColor(Colors.white70), - ], + AccountName( + account: account, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, ), ), + Text( + '@${account.name}', + ).textColor(Colors.white70), ], ), ), - ), - ], + ], + ), ), - ) - : Container( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - decoration: - isOutlined - ? BoxDecoration( - border: Border.all( - color: - Theme.of(context).colorScheme.outline, - ), - borderRadius: BorderRadius.circular(12), - ) - : null, - child: Row( + ), + ], + ), + ) + : Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + decoration: isOutlined + ? BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: BorderRadius.circular(12), + ) + : null, + child: Row( + children: [ + // Profile picture (equivalent to leading) + ProfilePictureWidget(file: account.profile.picture), + const SizedBox(width: 16), + // Text content (equivalent to title and subtitle) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - // Profile picture (equivalent to leading) - ProfilePictureWidget( - fileId: account.profile.picture?.id, - ), - const SizedBox(width: 16), - // Text content (equivalent to title and subtitle) - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - AccountName( - account: account, - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - Text('@${account.name}'), - ], - ), + AccountName( + account: account, + style: TextStyle(fontWeight: FontWeight.bold), ), + Text('@${account.name}'), ], ), ), - loading: - () => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, + ], + ), ), - child: Row( - children: [ - // Loading indicator (equivalent to leading) - const CircularProgressIndicator(), - const SizedBox(width: 16), - // Loading text content (equivalent to title and subtitle) - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('loading').bold().tr(), - const SizedBox(height: 4), - const Text('...'), - ], - ), - ), - ], + loading: () => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Row( + children: [ + // Loading indicator (equivalent to leading) + const CircularProgressIndicator(), + const SizedBox(width: 16), + // Loading text content (equivalent to title and subtitle) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('loading').bold().tr(), + const SizedBox(height: 4), + const Text('...'), + ], + ), ), - ), - error: - (error, stackTrace) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, + ], + ), + ), + error: (error, stackTrace) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Row( + children: [ + // Error icon (equivalent to leading) + const Icon(Symbols.error), + const SizedBox(width: 16), + // Error text content (equivalent to title and subtitle) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text('somethingWentWrong').tr().bold(), + const SizedBox(height: 4), + Text(error.toString()), + ], + ), ), - child: Row( - children: [ - // Error icon (equivalent to leading) - const Icon(Symbols.error), - const SizedBox(width: 16), - // Error text content (equivalent to title and subtitle) - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text('somethingWentWrong').tr().bold(), - const SizedBox(height: 4), - Text(error.toString()), - ], - ), - ), - ], - ), - ), + ], + ), + ), ), ), ); diff --git a/lib/widgets/account/account_picker.dart b/lib/widgets/account/account_picker.dart index 396d3553..7e9cc8ab 100644 --- a/lib/widgets/account/account_picker.dart +++ b/lib/widgets/account/account_picker.dart @@ -62,8 +62,8 @@ class AccountPickerSheet extends HookConsumerWidget { ), ), autofocus: true, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), Expanded( @@ -74,23 +74,22 @@ class AccountPickerSheet extends HookConsumerWidget { ); return searchResult.when( - data: - (accounts) => ListView.builder( - itemCount: accounts.length, - itemBuilder: (context, index) { - final account = accounts[index]; - return ListTile( - leading: ProfilePictureWidget( - fileId: account.profile.picture?.id, - ), - title: Text(account.nick), - subtitle: Text('@${account.name}'), - onTap: () => Navigator.of(context).pop(account), - ); - }, - ), - loading: - () => const Center(child: CircularProgressIndicator()), + data: (accounts) => ListView.builder( + itemCount: accounts.length, + itemBuilder: (context, index) { + final account = accounts[index]; + return ListTile( + leading: ProfilePictureWidget( + file: account.profile.picture, + ), + title: Text(account.nick), + subtitle: Text('@${account.name}'), + onTap: () => Navigator.of(context).pop(account), + ); + }, + ), + loading: () => + const Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), ); }, diff --git a/lib/widgets/chat/chat_input.dart b/lib/widgets/chat/chat_input.dart index bb75a8f8..92eb8a38 100644 --- a/lib/widgets/chat/chat_input.dart +++ b/lib/widgets/chat/chat_input.dart @@ -133,10 +133,9 @@ class _ExpandedSection extends StatelessWidget { }, child: Card( margin: EdgeInsets.zero, - color: - Theme.of( - context, - ).colorScheme.surfaceContainer, + color: Theme.of( + context, + ).colorScheme.surfaceContainer, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -144,8 +143,9 @@ class _ExpandedSection extends StatelessWidget { const Gap(4), Text( 'Poll', - style: - Theme.of(context).textTheme.bodySmall, + style: Theme.of( + context, + ).textTheme.bodySmall, ), ], ), @@ -160,8 +160,8 @@ class _ExpandedSection extends StatelessWidget { await showModalBottomSheet( context: context, isScrollControlled: true, - builder: - (context) => const ComposeFundSheet(), + builder: (context) => + const ComposeFundSheet(), ); if (fund != null) { onFundSelected(fund); @@ -169,10 +169,9 @@ class _ExpandedSection extends StatelessWidget { }, child: Card( margin: EdgeInsets.zero, - color: - Theme.of( - context, - ).colorScheme.surfaceContainer, + color: Theme.of( + context, + ).colorScheme.surfaceContainer, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -180,8 +179,9 @@ class _ExpandedSection extends StatelessWidget { const Gap(4), Text( 'fund'.tr(), - style: - Theme.of(context).textTheme.bodySmall, + style: Theme.of( + context, + ).textTheme.bodySmall, ), ], ), @@ -192,11 +192,8 @@ class _ExpandedSection extends StatelessWidget { ), StickerPickerEmbedded( height: kInputDrawerExpandedHeight, - onPick: - (placeholder) => _insertPlaceholder( - messageController, - placeholder, - ), + onPick: (placeholder) => + _insertPlaceholder(messageController, placeholder), ), ], ), @@ -373,15 +370,16 @@ class ChatInput extends HookConsumerWidget { switchOutCurve: Curves.fastEaseInToSlowEaseOut, transitionBuilder: (Widget child, Animation animation) { return SlideTransition( - position: Tween( - begin: const Offset(0, -0.3), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: animation, - curve: Curves.easeOutCubic, - ), - ), + position: + Tween( + begin: const Offset(0, -0.3), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + ), + ), child: SizeTransition( sizeFactor: animation, axisAlignment: -1.0, @@ -389,41 +387,40 @@ class ChatInput extends HookConsumerWidget { ), ); }, - child: - chatSubscribe.isNotEmpty - ? Container( - key: const ValueKey('typing-indicator'), - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - child: Row( - children: [ - const Icon( - Symbols.more_horiz, - size: 16, - ).padding(horizontal: 8), - const Gap(8), - Expanded( - child: Text( - 'typingHint'.plural( - chatSubscribe.length, - args: [ - chatSubscribe - .map((x) => x.nick ?? x.account.nick) - .join(', '), - ], - ), - style: Theme.of(context).textTheme.bodySmall, - ), - ), - ], - ), - ) - : const SizedBox.shrink( - key: ValueKey('typing-indicator-none'), + child: chatSubscribe.isNotEmpty + ? Container( + key: const ValueKey('typing-indicator'), + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, ), + child: Row( + children: [ + const Icon( + Symbols.more_horiz, + size: 16, + ).padding(horizontal: 8), + const Gap(8), + Expanded( + child: Text( + 'typingHint'.plural( + chatSubscribe.length, + args: [ + chatSubscribe + .map((x) => x.nick ?? x.account.nick) + .join(', '), + ], + ), + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ], + ), + ) + : const SizedBox.shrink( + key: ValueKey('typing-indicator-none'), + ), ), AnimatedSwitcher( duration: const Duration(milliseconds: 250), @@ -445,41 +442,36 @@ class ChatInput extends HookConsumerWidget { ), ); }, - child: - attachments.isNotEmpty - ? SizedBox( - key: ValueKey('attachments-${attachments.length}'), - height: 180, - child: ListView.separated( - padding: EdgeInsets.symmetric(horizontal: 12), - scrollDirection: Axis.horizontal, - itemCount: attachments.length, - itemBuilder: (context, idx) { - return SizedBox( - width: 180, - child: AttachmentPreview( - isCompact: true, - item: attachments[idx], - progress: - attachmentProgress['chat-upload']?[idx], - onRequestUpload: - () => onUploadAttachment(idx), - onDelete: () => onDeleteAttachment(idx), - onUpdate: (value) { - attachments[idx] = value; - onAttachmentsChanged(attachments); - }, - onMove: - (delta) => onMoveAttachment(idx, delta), - ), - ); - }, - separatorBuilder: (_, _) => const Gap(8), - ), - ).padding(vertical: 12) - : const SizedBox.shrink( - key: ValueKey('no-attachments'), + child: attachments.isNotEmpty + ? SizedBox( + key: ValueKey('attachments-${attachments.length}'), + height: 180, + child: ListView.separated( + padding: EdgeInsets.symmetric(horizontal: 12), + scrollDirection: Axis.horizontal, + itemCount: attachments.length, + itemBuilder: (context, idx) { + return SizedBox( + width: 180, + child: AttachmentPreview( + isCompact: true, + item: attachments[idx], + progress: + attachmentProgress['chat-upload']?[idx], + onRequestUpload: () => onUploadAttachment(idx), + onDelete: () => onDeleteAttachment(idx), + onUpdate: (value) { + attachments[idx] = value; + onAttachmentsChanged(attachments); + }, + onMove: (delta) => onMoveAttachment(idx, delta), + ), + ); + }, + separatorBuilder: (_, _) => const Gap(8), ), + ).padding(vertical: 12) + : const SizedBox.shrink(key: ValueKey('no-attachments')), ), AnimatedSwitcher( duration: const Duration(milliseconds: 200), @@ -501,66 +493,62 @@ class ChatInput extends HookConsumerWidget { ), ); }, - child: - selectedPoll != null - ? Container( - key: const ValueKey('selected-poll'), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - color: - Theme.of( - context, - ).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(24), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.outline.withOpacity(0.2), - width: 1, - ), - ), - margin: const EdgeInsets.only( - left: 8, - right: 8, - top: 8, - bottom: 8, - ), - child: Row( - children: [ - Icon( - Symbols.how_to_vote, - size: 18, - color: Theme.of(context).colorScheme.primary, - ), - const Gap(8), - Expanded( - child: Text( - selectedPoll!.title ?? 'Poll', - style: Theme.of(context).textTheme.bodySmall! - .copyWith(fontWeight: FontWeight.w500), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - SizedBox( - width: 24, - height: 24, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.close, size: 18), - onPressed: () => onPollSelected(null), - tooltip: 'clear'.tr(), - ), - ), - ], - ), - ) - : const SizedBox.shrink( - key: ValueKey('no-selected-poll'), + child: selectedPoll != null + ? Container( + key: const ValueKey('selected-poll'), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + width: 1, + ), + ), + margin: const EdgeInsets.only( + left: 8, + right: 8, + top: 8, + bottom: 8, + ), + child: Row( + children: [ + Icon( + Symbols.how_to_vote, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Expanded( + child: Text( + selectedPoll!.title ?? 'Poll', + style: Theme.of(context).textTheme.bodySmall! + .copyWith(fontWeight: FontWeight.w500), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + SizedBox( + width: 24, + height: 24, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.close, size: 18), + onPressed: () => onPollSelected(null), + tooltip: 'clear'.tr(), + ), + ), + ], + ), + ) + : const SizedBox.shrink(key: ValueKey('no-selected-poll')), ), AnimatedSwitcher( duration: const Duration(milliseconds: 200), @@ -582,93 +570,88 @@ class ChatInput extends HookConsumerWidget { ), ); }, - child: - selectedFund != null - ? Container( - key: const ValueKey('selected-fund'), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + child: selectedFund != null + ? Container( + key: const ValueKey('selected-fund'), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + width: 1, ), - decoration: BoxDecoration( - color: - Theme.of( - context, - ).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(24), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.outline.withOpacity(0.2), - width: 1, + ), + margin: const EdgeInsets.only( + left: 8, + right: 8, + top: 8, + bottom: 8, + ), + child: Row( + children: [ + Icon( + Symbols.currency_exchange, + size: 18, + color: Theme.of(context).colorScheme.primary, ), - ), - margin: const EdgeInsets.only( - left: 8, - right: 8, - top: 8, - bottom: 8, - ), - child: Row( - children: [ - Icon( - Symbols.currency_exchange, - size: 18, - color: Theme.of(context).colorScheme.primary, - ), - const Gap(8), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}', - style: Theme.of( - context, - ).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w500, + const Gap(8), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(fontWeight: FontWeight.w500), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (selectedFund!.message != null) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + selectedFund!.message!, + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontSize: 10, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - if (selectedFund!.message != null) - Padding( - padding: const EdgeInsets.only(top: 2), - child: Text( - selectedFund!.message!, - style: Theme.of( - context, - ).textTheme.bodySmall!.copyWith( - fontSize: 10, - color: - Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), + ], ), - SizedBox( - width: 24, - height: 24, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.close, size: 18), - onPressed: () => onFundSelected(null), - tooltip: 'clear'.tr(), - ), + ), + SizedBox( + width: 24, + height: 24, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.close, size: 18), + onPressed: () => onFundSelected(null), + tooltip: 'clear'.tr(), ), - ], - ), - ) - : const SizedBox.shrink( - key: ValueKey('no-selected-fund'), + ), + ], ), + ) + : const SizedBox.shrink(key: ValueKey('no-selected-fund')), ), AnimatedSwitcher( duration: const Duration(milliseconds: 200), @@ -692,59 +675,57 @@ class ChatInput extends HookConsumerWidget { }, child: (messageReplyingTo != null || - messageForwardingTo != null || - messageEditingTo != null) - ? Container( - key: ValueKey( - messageReplyingTo?.id ?? - messageForwardingTo?.id ?? - messageEditingTo?.id ?? - 'action', + messageForwardingTo != null || + messageEditingTo != null) + ? Container( + key: ValueKey( + messageReplyingTo?.id ?? + messageForwardingTo?.id ?? + messageEditingTo?.id ?? + 'action', + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + width: 1, ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - color: - Theme.of( - context, - ).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(24), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.outline.withOpacity(0.2), - width: 1, - ), - ), - margin: const EdgeInsets.only( - left: 8, - right: 8, - top: 8, - bottom: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( + ), + margin: const EdgeInsets.only( + left: 8, + right: 8, + top: 8, + bottom: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + messageReplyingTo != null + ? Symbols.reply + : messageForwardingTo != null + ? Symbols.forward + : Symbols.edit, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Expanded( + child: Text( messageReplyingTo != null - ? Symbols.reply - : messageForwardingTo != null - ? Symbols.forward - : Symbols.edit, - size: 18, - color: - Theme.of(context).colorScheme.primary, - ), - const Gap(8), - Expanded( - child: Text( - messageReplyingTo != null - ? 'chatReplyingTo'.tr( + ? 'chatReplyingTo'.tr( args: [ messageReplyingTo ?.sender @@ -753,60 +734,57 @@ class ChatInput extends HookConsumerWidget { 'unknown'.tr(), ], ) - : messageForwardingTo != null - ? 'chatForwarding'.tr() - : 'chatEditing'.tr(), - style: Theme.of( - context, - ).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w500, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - SizedBox( - width: 24, - height: 24, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.close, size: 18), - onPressed: onClear, - tooltip: 'clear'.tr(), - ), - ), - ], - ), - if (messageReplyingTo != null || - messageForwardingTo != null || - messageEditingTo != null) - Padding( - padding: const EdgeInsets.only( - top: 6, - left: 26, - ), - child: Text( - (messageReplyingTo ?? - messageForwardingTo ?? - messageEditingTo) - ?.content ?? - 'chatNoContent'.tr(), - style: Theme.of( - context, - ).textTheme.bodySmall!.copyWith( - color: - Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - maxLines: 2, + : messageForwardingTo != null + ? 'chatForwarding'.tr() + : 'chatEditing'.tr(), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(fontWeight: FontWeight.w500), + maxLines: 1, overflow: TextOverflow.ellipsis, ), ), - ], - ), - ) - : const SizedBox.shrink(key: ValueKey('no-action')), + SizedBox( + width: 24, + height: 24, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.close, size: 18), + onPressed: onClear, + tooltip: 'clear'.tr(), + ), + ), + ], + ), + if (messageReplyingTo != null || + messageForwardingTo != null || + messageEditingTo != null) + Padding( + padding: const EdgeInsets.only( + top: 6, + left: 26, + ), + child: Text( + (messageReplyingTo ?? + messageForwardingTo ?? + messageEditingTo) + ?.content ?? + 'chatNoContent'.tr(), + style: Theme.of(context).textTheme.bodySmall! + .copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ) + : const SizedBox.shrink(key: ValueKey('no-action')), ), Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -815,25 +793,19 @@ class ChatInput extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ IconButton( - tooltip: - isExpanded.value ? 'collapse'.tr() : 'more'.tr(), + tooltip: isExpanded.value + ? 'collapse'.tr() + : 'more'.tr(), icon: AnimatedSwitcher( duration: const Duration(milliseconds: 200), - transitionBuilder: - (child, animation) => FadeTransition( - opacity: animation, - child: child, - ), - child: - isExpanded.value - ? const Icon( - Symbols.close, - key: ValueKey('close'), - ) - : const Icon( - Symbols.add, - key: ValueKey('add'), - ), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: isExpanded.value + ? const Icon( + Symbols.close, + key: ValueKey('close'), + ) + : const Icon(Symbols.add, key: ValueKey('add')), ), onPressed: () { isExpanded.value = !isExpanded.value; @@ -885,46 +857,43 @@ class ChatInput extends HookConsumerWidget { hintMaxLines: 1, hintText: (chatRoom.type == 1 && chatRoom.name == null) - ? 'chatDirectMessageHint'.tr( - args: [ - getValidMembers( - chatRoom.members!, - ).map((e) => e.account.nick).join(', '), - ], - ) - : 'chatMessageHint'.tr( - args: [chatRoom.name!], - ), + ? 'chatDirectMessageHint'.tr( + args: [ + getValidMembers( + chatRoom.members!, + ).map((e) => e.account.nick).join(', '), + ], + ) + : 'chatMessageHint'.tr(args: [chatRoom.name!]), border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 12, ), - counterText: - messageController.text.length > 1024 - ? '${messageController.text.length}/4096' - : null, + counterText: messageController.text.length > 1024 + ? '${messageController.text.length}/4096' + : null, ), maxLines: 5, minLines: 1, - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), - textInputAction: - settings.enterToSend - ? TextInputAction.send - : null, - onSubmitted: - settings.enterToSend ? (_) => send() : null, + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + textInputAction: settings.enterToSend + ? TextInputAction.send + : null, + onSubmitted: settings.enterToSend + ? (_) => send() + : null, ); }, suggestionsCallback: (pattern) async { // Only trigger on @ or : final atIndex = pattern.lastIndexOf('@'); final colonIndex = pattern.lastIndexOf(':'); - final triggerIndex = - atIndex > colonIndex ? atIndex : colonIndex; + final triggerIndex = atIndex > colonIndex + ? atIndex + : colonIndex; if (triggerIndex == -1) return []; final chopped = pattern.substring(triggerIndex); if (chopped.contains(' ')) return []; @@ -986,9 +955,7 @@ class ChatInput extends HookConsumerWidget { child: SizedBox( width: 28, height: 28, - child: CloudImageWidget( - fileId: sticker.image.id, - ), + child: CloudImageWidget(file: sticker.image), ), ); break; @@ -1005,8 +972,9 @@ class ChatInput extends HookConsumerWidget { final text = messageController.text; final atIndex = text.lastIndexOf('@'); final colonIndex = text.lastIndexOf(':'); - final triggerIndex = - atIndex > colonIndex ? atIndex : colonIndex; + final triggerIndex = atIndex > colonIndex + ? atIndex + : colonIndex; if (triggerIndex == -1) return; final newText = text.replaceRange( triggerIndex, @@ -1053,16 +1021,15 @@ class ChatInput extends HookConsumerWidget { ), ); }, - child: - isExpanded.value - ? _ExpandedSection( - messageController: messageController, - selectedPoll: selectedPoll, - onPollSelected: onPollSelected, - selectedFund: selectedFund, - onFundSelected: onFundSelected, - ) - : const SizedBox.shrink(key: ValueKey('collapsed')), + child: isExpanded.value + ? _ExpandedSection( + messageController: messageController, + selectedPoll: selectedPoll, + onPollSelected: onPollSelected, + selectedFund: selectedFund, + onFundSelected: onFundSelected, + ) + : const SizedBox.shrink(key: ValueKey('collapsed')), ), ], ), diff --git a/lib/widgets/chat/message_list_tile.dart b/lib/widgets/chat/message_list_tile.dart index 419f16b8..a59ab7df 100644 --- a/lib/widgets/chat/message_list_tile.dart +++ b/lib/widgets/chat/message_list_tile.dart @@ -31,7 +31,7 @@ class MessageListTile extends StatelessWidget { radius: 20, backgroundColor: Colors.transparent, child: ProfilePictureWidget( - fileId: sender.account.profile.picture?.id, + file: sender.account.profile.picture, radius: 20, ), ), diff --git a/lib/widgets/chat/message_sender_info.dart b/lib/widgets/chat/message_sender_info.dart index 1479ce12..f6cf0f60 100644 --- a/lib/widgets/chat/message_sender_info.dart +++ b/lib/widgets/chat/message_sender_info.dart @@ -24,12 +24,11 @@ class MessageSenderInfo extends StatelessWidget { @override Widget build(BuildContext context) { - final timestamp = - DateTime.now().difference(createdAt).inDays > 365 - ? DateFormat('yyyy/MM/dd HH:mm').format(createdAt.toLocal()) - : DateTime.now().difference(createdAt).inDays > 0 - ? DateFormat('MM/dd HH:mm').format(createdAt.toLocal()) - : DateFormat('HH:mm').format(createdAt.toLocal()); + final timestamp = DateTime.now().difference(createdAt).inDays > 365 + ? DateFormat('yyyy/MM/dd HH:mm').format(createdAt.toLocal()) + : DateTime.now().difference(createdAt).inDays > 0 + ? DateFormat('MM/dd HH:mm').format(createdAt.toLocal()) + : DateFormat('HH:mm').format(createdAt.toLocal()); if (isCompact) { return Row( @@ -41,7 +40,7 @@ class MessageSenderInfo extends StatelessWidget { AccountPfcGestureDetector( uname: sender.account.name, child: ProfilePictureWidget( - fileId: sender.account.profile.picture?.id, + file: sender.account.profile.picture, radius: 14, ), ), @@ -69,7 +68,7 @@ class MessageSenderInfo extends StatelessWidget { AccountPfcGestureDetector( uname: sender.account.name, child: ProfilePictureWidget( - fileId: sender.account.profile.picture?.id, + file: sender.account.profile.picture, radius: 14, ), ), @@ -106,7 +105,7 @@ class MessageSenderInfo extends StatelessWidget { AccountPfcGestureDetector( uname: sender.account.name, child: ProfilePictureWidget( - fileId: sender.account.profile.picture?.id, + file: sender.account.profile.picture, radius: 16, ), ), diff --git a/lib/widgets/chat/public_room_preview.dart b/lib/widgets/chat/public_room_preview.dart index c57cfcad..bbc36f68 100644 --- a/lib/widgets/chat/public_room_preview.dart +++ b/lib/widgets/chat/public_room_preview.dart @@ -99,15 +99,15 @@ class PublicRoomPreview extends HookConsumerWidget { SizedBox( height: 26, width: 26, - child: (room.type == 1 && room.picture?.id == null) + child: (room.type == 1 && room.picture == null) ? SplitAvatarWidget( - filesId: room.members! - .map((e) => e.account.profile.picture?.id) + files: room.members! + .map((e) => e.account.profile.picture) .toList(), ) - : room.picture?.id != null + : room.picture != null ? ProfilePictureWidget( - fileId: room.picture?.id, + file: room.picture, fallbackIcon: Symbols.chat, ) : CircleAvatar( @@ -132,15 +132,15 @@ class PublicRoomPreview extends HookConsumerWidget { SizedBox( height: 26, width: 26, - child: (room.type == 1 && room.picture?.id == null) + child: (room.type == 1 && room.picture == null) ? SplitAvatarWidget( - filesId: room.members! - .map((e) => e.account.profile.picture?.id) + files: room.members! + .map((e) => e.account.profile.picture) .toList(), ) - : room.picture?.id != null + : room.picture != null ? ProfilePictureWidget( - fileId: room.picture?.id, + file: room.picture, fallbackIcon: Symbols.chat, ) : CircleAvatar( diff --git a/lib/widgets/chat_room_widgets.dart b/lib/widgets/chat_room_widgets.dart index 58f890c8..694d195a 100644 --- a/lib/widgets/chat_room_widgets.dart +++ b/lib/widgets/chat_room_widgets.dart @@ -22,15 +22,13 @@ class ChatRoomAvatar extends StatelessWidget { @override Widget build(BuildContext context) { - final avatarChild = (isDirect && room.picture?.id == null) + final avatarChild = (isDirect && room.picture == null) ? SplitAvatarWidget( - filesId: validMembers - .map((e) => e.account.profile.picture?.id) - .toList(), + files: validMembers.map((e) => e.account.profile.picture).toList(), ) - : room.picture?.id == null + : room.picture == null ? CircleAvatar(child: Text((room.name ?? 'DM')[0].toUpperCase())) - : ProfilePictureWidget(fileId: room.picture?.id); + : ProfilePictureWidget(file: room.picture); final badgeChild = Badge( isLabelVisible: summary.when( diff --git a/lib/widgets/check_in.dart b/lib/widgets/check_in.dart index 5ef15839..60d202e4 100644 --- a/lib/widgets/check_in.dart +++ b/lib/widgets/check_in.dart @@ -262,10 +262,7 @@ class CheckInActivityWidget extends StatelessWidget { spacing: 12, crossAxisAlignment: CrossAxisAlignment.start, children: [ - ProfilePictureWidget( - fileId: result.account!.profile.picture?.id, - radius: 12, - ), + ProfilePictureWidget(file: result.account!.profile.picture, radius: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/widgets/content/cloud_files.dart b/lib/widgets/content/cloud_files.dart index cab4e02a..7aaf485f 100644 --- a/lib/widgets/content/cloud_files.dart +++ b/lib/widgets/content/cloud_files.dart @@ -577,6 +577,9 @@ class ProfilePictureWidget extends ConsumerWidget { final serverUrl = ref.watch(serverUrlProvider); final String? id = file?.id ?? fileId; + final meta = file?.fileMeta is Map ? (file!.fileMeta as Map) : const {}; + final blurHash = meta['blur'] as String?; + final fallback = Icon( fallbackIcon ?? Symbols.account_circle, size: radius, @@ -590,6 +593,7 @@ class ProfilePictureWidget extends ConsumerWidget { placeholder: fallback, content: () => UniversalImage( uri: '$serverUrl/drive/files/$id', + blurHash: blurHash, fit: BoxFit.cover, ), ); @@ -625,14 +629,14 @@ class ProfilePictureWidget extends ConsumerWidget { } class SplitAvatarWidget extends ConsumerWidget { - final List filesId; + final List files; final double radius; final IconData fallbackIcon; final Color? fallbackColor; const SplitAvatarWidget({ super.key, - required this.filesId, + required this.files, this.radius = 20, this.fallbackIcon = Symbols.account_circle, this.fallbackColor, @@ -640,17 +644,17 @@ class SplitAvatarWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - if (filesId.isEmpty) { + if (files.isEmpty) { return ProfilePictureWidget( - fileId: null, + file: null, radius: radius, fallbackIcon: fallbackIcon, fallbackColor: fallbackColor, ); } - if (filesId.length == 1) { + if (files.length == 1) { return ProfilePictureWidget( - fileId: filesId[0], + file: files[0], radius: radius, fallbackIcon: fallbackIcon, fallbackColor: fallbackColor, @@ -665,32 +669,32 @@ class SplitAvatarWidget extends ConsumerWidget { color: Theme.of(context).colorScheme.primaryContainer, child: Stack( children: [ - if (filesId.length == 2) + if (files.length == 2) Row( children: [ Expanded( - child: _buildQuadrant(context, filesId[0], ref, radius), + child: _buildQuadrant(context, files[0], ref, radius), ), Expanded( - child: _buildQuadrant(context, filesId[1], ref, radius), + child: _buildQuadrant(context, files[1], ref, radius), ), ], ) - else if (filesId.length == 3) + else if (files.length == 3) Row( children: [ Column( children: [ Expanded( - child: _buildQuadrant(context, filesId[0], ref, radius), + child: _buildQuadrant(context, files[0], ref, radius), ), Expanded( - child: _buildQuadrant(context, filesId[1], ref, radius), + child: _buildQuadrant(context, files[1], ref, radius), ), ], ), Expanded( - child: _buildQuadrant(context, filesId[2], ref, radius), + child: _buildQuadrant(context, files[2], ref, radius), ), ], ) @@ -701,20 +705,10 @@ class SplitAvatarWidget extends ConsumerWidget { child: Row( children: [ Expanded( - child: _buildQuadrant( - context, - filesId[0], - ref, - radius, - ), + child: _buildQuadrant(context, files[0], ref, radius), ), Expanded( - child: _buildQuadrant( - context, - filesId[1], - ref, - radius, - ), + child: _buildQuadrant(context, files[1], ref, radius), ), ], ), @@ -723,22 +717,17 @@ class SplitAvatarWidget extends ConsumerWidget { child: Row( children: [ Expanded( - child: _buildQuadrant( - context, - filesId[2], - ref, - radius, - ), + child: _buildQuadrant(context, files[2], ref, radius), ), Expanded( - child: filesId.length > 4 + child: files.length > 4 ? Container( color: Theme.of( context, ).colorScheme.primaryContainer, child: Center( child: Text( - '+${filesId.length - 3}', + '+${files.length - 3}', style: TextStyle( fontSize: radius * 0.4, color: Theme.of( @@ -748,12 +737,7 @@ class SplitAvatarWidget extends ConsumerWidget { ), ), ) - : _buildQuadrant( - context, - filesId[3], - ref, - radius, - ), + : _buildQuadrant(context, files[3], ref, radius), ), ], ), @@ -768,11 +752,11 @@ class SplitAvatarWidget extends ConsumerWidget { Widget _buildQuadrant( BuildContext context, - String? fileId, + SnCloudFile? file, WidgetRef ref, double radius, ) { - if (fileId == null) { + if (file == null) { return Container( width: radius, height: radius, @@ -787,7 +771,7 @@ class SplitAvatarWidget extends ConsumerWidget { } final serverUrl = ref.watch(serverUrlProvider); - final uri = '$serverUrl/drive/files/$fileId'; + final uri = '$serverUrl/drive/files/${file.id}'; return SizedBox( width: radius, diff --git a/lib/widgets/content/image.dart b/lib/widgets/content/image.dart index e8fd70f6..d7698405 100644 --- a/lib/widgets/content/image.dart +++ b/lib/widgets/content/image.dart @@ -1,9 +1,10 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg/flutter_svg.dart'; -class UniversalImage extends StatelessWidget { +class UniversalImage extends HookWidget { final String uri; final String? blurHash; final BoxFit fit; @@ -27,6 +28,7 @@ class UniversalImage extends StatelessWidget { @override Widget build(BuildContext context) { + final loaded = useState(false); final isSvgImage = isSvg || uri.toLowerCase().endsWith('.svg'); if (isSvgImage) { @@ -35,9 +37,8 @@ class UniversalImage extends StatelessWidget { fit: fit, width: width, height: height, - placeholderBuilder: - (BuildContext context) => - Center(child: CircularProgressIndicator()), + placeholderBuilder: (BuildContext context) => + Center(child: CircularProgressIndicator()), ); } @@ -46,8 +47,9 @@ class UniversalImage extends StatelessWidget { if (width != null && height != null && !noCacheOptimization) { final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; cacheWidth = width != null ? (width! * devicePixelRatio).round() : null; - cacheHeight = - height != null ? (height! * devicePixelRatio).round() : null; + cacheHeight = height != null + ? (height! * devicePixelRatio).round() + : null; } return SizedBox( @@ -66,21 +68,72 @@ class UniversalImage extends StatelessWidget { memCacheWidth: cacheWidth, progressIndicatorBuilder: (context, url, progress) { return Center( - child: CircularProgressIndicator(value: progress.progress), + child: AnimatedCircularProgressIndicator( + value: progress.progress, + color: Colors.white.withOpacity(0.5), + ), ); }, - errorWidget: - (context, url, error) => - useFallbackImage - ? Image.asset( - 'assets/images/media-offline.jpg', - fit: BoxFit.cover, - key: Key('image-broke-$uri'), - ) - : SizedBox.shrink(), + imageBuilder: (context, imageProvider) { + Future(() => loaded.value = true); + return AnimatedOpacity( + opacity: loaded.value ? 1.0 : 0.0, + duration: const Duration(milliseconds: 300), + child: Image( + image: imageProvider, + fit: fit, + width: width, + height: height, + ), + ); + }, + errorWidget: (context, url, error) => useFallbackImage + ? Image.asset( + 'assets/images/media-offline.jpg', + fit: BoxFit.cover, + key: Key('image-broke-$uri'), + ) + : SizedBox.shrink(), ), ], ), ); } } + +class AnimatedCircularProgressIndicator extends HookWidget { + final double? value; + final Color? color; + final double strokeWidth; + final Duration duration; + + const AnimatedCircularProgressIndicator({ + super.key, + this.value, + this.color, + this.strokeWidth = 4.0, + this.duration = const Duration(milliseconds: 200), + }); + + @override + Widget build(BuildContext context) { + final animationController = useAnimationController(duration: duration); + final animation = useAnimation( + Tween(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, + ); + } +} diff --git a/lib/widgets/post/compose_form_fields.dart b/lib/widgets/post/compose_form_fields.dart index df443c87..6d081b1f 100644 --- a/lib/widgets/post/compose_form_fields.dart +++ b/lib/widgets/post/compose_form_fields.dart @@ -41,12 +41,11 @@ class ComposeFormFields extends HookConsumerWidget { GestureDetector( onTap: onPublisherTap, child: ProfilePictureWidget( - fileId: state.currentPublisher.value?.picture?.id, + file: state.currentPublisher.value?.picture, radius: 20, - fallbackIcon: - state.currentPublisher.value == null - ? Icons.question_mark - : null, + fallbackIcon: state.currentPublisher.value == null + ? Icons.question_mark + : null, ), ), @@ -98,8 +97,8 @@ class ComposeFormFields extends HookConsumerWidget { ), ), style: theme.textTheme.titleMedium, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), // Description field @@ -115,8 +114,8 @@ class ComposeFormFields extends HookConsumerWidget { style: theme.textTheme.bodyMedium, minLines: 1, maxLines: 3, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), // Content field @@ -138,16 +137,17 @@ class ComposeFormFields extends HookConsumerWidget { ), ), maxLines: null, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ); }, suggestionsCallback: (pattern) async { // Only trigger on @ or : final atIndex = pattern.lastIndexOf('@'); final colonIndex = pattern.lastIndexOf(':'); - final triggerIndex = - atIndex > colonIndex ? atIndex : colonIndex; + final triggerIndex = atIndex > colonIndex + ? atIndex + : colonIndex; if (triggerIndex == -1) return []; final chopped = pattern.substring(triggerIndex); if (chopped.contains(' ')) return []; @@ -202,7 +202,7 @@ class ComposeFormFields extends HookConsumerWidget { child: SizedBox( width: 28, height: 28, - child: CloudImageWidget(fileId: sticker.image.id), + child: CloudImageWidget(file: sticker.image), ), ); break; @@ -219,8 +219,9 @@ class ComposeFormFields extends HookConsumerWidget { final text = state.contentController.text; final atIndex = text.lastIndexOf('@'); final colonIndex = text.lastIndexOf(':'); - final triggerIndex = - atIndex > colonIndex ? atIndex : colonIndex; + final triggerIndex = atIndex > colonIndex + ? atIndex + : colonIndex; if (triggerIndex == -1) return; final newText = text.replaceRange( triggerIndex, @@ -281,8 +282,8 @@ class ArticleComposeFormFields extends StatelessWidget { ), ), style: theme.textTheme.titleMedium, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), // Description field @@ -297,8 +298,8 @@ class ArticleComposeFormFields extends StatelessWidget { style: theme.textTheme.bodyMedium, minLines: 1, maxLines: 3, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), // Content field (expanded) @@ -317,8 +318,8 @@ class ArticleComposeFormFields extends StatelessWidget { maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), ], diff --git a/lib/widgets/post/compose_info_banner.dart b/lib/widgets/post/compose_info_banner.dart index 90890b27..bad1011b 100644 --- a/lib/widgets/post/compose_info_banner.dart +++ b/lib/widgets/post/compose_info_banner.dart @@ -135,10 +135,7 @@ class CompactReferencePost extends StatelessWidget { Widget _buildProfilePicture(BuildContext context) { // Handle publisher case if (post.publisher != null) { - return ProfilePictureWidget( - fileId: post.publisher!.picture?.id, - radius: 16, - ); + return ProfilePictureWidget(file: post.publisher!.picture, radius: 16); } // Handle actor case if (post.actor != null) { @@ -169,7 +166,7 @@ class CompactReferencePost extends StatelessWidget { } } // Fallback - return ProfilePictureWidget(fileId: null, radius: 16); + return ProfilePictureWidget(file: null, radius: 16); } String _getDisplayName() { diff --git a/lib/widgets/post/compose_settings_sheet.dart b/lib/widgets/post/compose_settings_sheet.dart index 10c5e9b9..244a6728 100644 --- a/lib/widgets/post/compose_settings_sheet.dart +++ b/lib/widgets/post/compose_settings_sheet.dart @@ -412,7 +412,7 @@ class ComposeSettingsSheet extends HookConsumerWidget { child: Row( children: [ ProfilePictureWidget( - fileId: currentRealm.picture?.id, + file: currentRealm.picture, fallbackIcon: Symbols.workspaces, radius: 16, ), @@ -428,7 +428,7 @@ class ComposeSettingsSheet extends HookConsumerWidget { child: Row( children: [ ProfilePictureWidget( - fileId: realm.picture?.id, + file: realm.picture, fallbackIcon: Symbols.workspaces, radius: 16, ), @@ -454,7 +454,7 @@ class ComposeSettingsSheet extends HookConsumerWidget { ) else ProfilePictureWidget( - fileId: currentRealm.picture?.id, + file: currentRealm.picture, fallbackIcon: Symbols.workspaces, radius: 16, ), diff --git a/lib/widgets/post/post_award_sheet.dart b/lib/widgets/post/post_award_sheet.dart index 6c628631..98279c1e 100644 --- a/lib/widgets/post/post_award_sheet.dart +++ b/lib/widgets/post/post_award_sheet.dart @@ -54,7 +54,7 @@ class PostAwardSheet extends HookConsumerWidget { } } // Fallback - return ProfilePictureWidget(fileId: null, radius: radius); + return ProfilePictureWidget(file: null, radius: radius); } String _getPublisherName() { diff --git a/lib/widgets/post/post_quick_reply.dart b/lib/widgets/post/post_quick_reply.dart index 221569e4..b2127a5b 100644 --- a/lib/widgets/post/post_quick_reply.dart +++ b/lib/widgets/post/post_quick_reply.dart @@ -73,96 +73,94 @@ class PostQuickReply extends HookConsumerWidget { const kInputChipHeight = 54.0; return publishers.when( - data: - (data) => Material( - elevation: 2, - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(28), - child: Container( - constraints: BoxConstraints(minHeight: kInputChipHeight), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - child: ProfilePictureWidget( - fileId: currentPublisher.value?.picture?.id, - radius: (kInputChipHeight * 0.5) - 6, + data: (data) => Material( + elevation: 2, + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(28), + child: Container( + constraints: BoxConstraints(minHeight: kInputChipHeight), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + child: ProfilePictureWidget( + file: currentPublisher.value?.picture, + radius: (kInputChipHeight * 0.5) - 6, + ), + onTap: () { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) => PublisherModal(), + ).then((value) { + if (value is SnPublisher) { + currentPublisher.value = value; + } + }); + }, + ).padding(right: 12), + Expanded( + child: TextField( + controller: contentController, + decoration: InputDecoration( + hintText: 'postReplyPlaceholder'.tr(), + border: InputBorder.none, + isDense: true, + isCollapsed: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 14, ), - onTap: () { - showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (context) => PublisherModal(), - ).then((value) { - if (value is SnPublisher) { - currentPublisher.value = value; - } - }); - }, - ).padding(right: 12), - Expanded( - child: TextField( - controller: contentController, - decoration: InputDecoration( - hintText: 'postReplyPlaceholder'.tr(), - border: InputBorder.none, - isDense: true, - isCollapsed: true, - contentPadding: EdgeInsets.symmetric( - horizontal: 12, - vertical: 14, - ), - visualDensity: VisualDensity.compact, - ), - style: TextStyle(fontSize: 14), - minLines: 1, - maxLines: 5, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - ), - const Gap(8), - IconButton( - onPressed: () async { - onLaunch?.call(); - final value = await PostComposeSheet.show( - context, - initialState: PostComposeInitialState( - content: contentController.text, - replyingTo: parent, - ), - ); - if (value != null) onPosted?.call(); - }, - icon: const Icon(Symbols.launch, size: 20), visualDensity: VisualDensity.compact, - constraints: BoxConstraints( - maxHeight: kInputChipHeight - 6, - minHeight: kInputChipHeight - 6, - ), ), - IconButton( - icon: - submitting.value - ? SizedBox( - width: 28, - height: 28, - child: CircularProgressIndicator(strokeWidth: 3), - ) - : Icon(Symbols.send, size: 20), - color: Theme.of(context).colorScheme.primary, - onPressed: submitting.value ? null : performAction, - visualDensity: VisualDensity.compact, - constraints: BoxConstraints( - maxHeight: kInputChipHeight - 6, - minHeight: kInputChipHeight - 6, - ), - ), - ], + style: TextStyle(fontSize: 14), + minLines: 1, + maxLines: 5, + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), ), - ), + const Gap(8), + IconButton( + onPressed: () async { + onLaunch?.call(); + final value = await PostComposeSheet.show( + context, + initialState: PostComposeInitialState( + content: contentController.text, + replyingTo: parent, + ), + ); + if (value != null) onPosted?.call(); + }, + icon: const Icon(Symbols.launch, size: 20), + visualDensity: VisualDensity.compact, + constraints: BoxConstraints( + maxHeight: kInputChipHeight - 6, + minHeight: kInputChipHeight - 6, + ), + ), + IconButton( + icon: submitting.value + ? SizedBox( + width: 28, + height: 28, + child: CircularProgressIndicator(strokeWidth: 3), + ) + : Icon(Symbols.send, size: 20), + color: Theme.of(context).colorScheme.primary, + onPressed: submitting.value ? null : performAction, + visualDensity: VisualDensity.compact, + constraints: BoxConstraints( + maxHeight: kInputChipHeight - 6, + minHeight: kInputChipHeight - 6, + ), + ), + ], ), + ), + ), loading: () => const SizedBox.shrink(), error: (e, _) => const SizedBox.shrink(), ); diff --git a/lib/widgets/post/post_shared.dart b/lib/widgets/post/post_shared.dart index e119350e..80beb263 100644 --- a/lib/widgets/post/post_shared.dart +++ b/lib/widgets/post/post_shared.dart @@ -148,7 +148,7 @@ class PostReplyPreview extends HookConsumerWidget { return ActorPictureWidget(actor: post.actor!, radius: radius); } // Fallback - return ProfilePictureWidget(fileId: null, radius: radius); + return ProfilePictureWidget(file: null, radius: radius); } @override @@ -448,7 +448,7 @@ class ReferencedPostWidget extends StatelessWidget { // Handle publisher case if (post.publisher != null) { return ProfilePictureWidget( - fileId: post.publisher!.picture?.id, + file: post.publisher!.picture, radius: radius, ); } @@ -457,7 +457,7 @@ class ReferencedPostWidget extends StatelessWidget { return ActorPictureWidget(actor: post.actor!, radius: radius); } // Fallback - return ProfilePictureWidget(fileId: null, radius: radius); + return ProfilePictureWidget(file: null, radius: radius); } String _getDisplayName(SnPost post) { @@ -701,7 +701,7 @@ class PostHeader extends HookConsumerWidget { return ActorPictureWidget(actor: post.actor!, radius: radius); } // Fallback - return ProfilePictureWidget(fileId: null, radius: radius); + return ProfilePictureWidget(file: null, radius: radius); } String _getDisplayName(SnPost post) { diff --git a/lib/widgets/post/publishers_modal.dart b/lib/widgets/post/publishers_modal.dart index 2560d9d9..0ff03f5c 100644 --- a/lib/widgets/post/publishers_modal.dart +++ b/lib/widgets/post/publishers_modal.dart @@ -21,63 +21,57 @@ class PublisherModal extends HookConsumerWidget { children: [ Expanded( child: publishers.when( - data: - (value) => - value.isEmpty - ? ConstrainedBox( - constraints: BoxConstraints(maxWidth: 280), - child: - Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'publishersEmpty', - textAlign: TextAlign.center, - ).tr().fontSize(17).bold(), - Text( - 'publishersEmptyDescription', - textAlign: TextAlign.center, - ).tr(), - const Gap(12), - ElevatedButton( - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: - (context) => - const NewPublisherScreen(), - ).then((value) { - if (value != null) { - ref.invalidate( - publishersManagedProvider, - ); - } - }); - }, - child: Text('createPublisher').tr(), - ), - ], - ).center(), - ) - : SingleChildScrollView( - child: Column( - children: [ - for (final publisher in value) - ListTile( - leading: ProfilePictureWidget( - fileId: publisher.picture?.id, - ), - title: Text(publisher.nick), - subtitle: Text('@${publisher.name}'), - onTap: () { - Navigator.pop(context, publisher); - }, - ), - ], - ), + data: (value) => value.isEmpty + ? ConstrainedBox( + constraints: BoxConstraints(maxWidth: 280), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'publishersEmpty', + textAlign: TextAlign.center, + ).tr().fontSize(17).bold(), + Text( + 'publishersEmptyDescription', + textAlign: TextAlign.center, + ).tr(), + const Gap(12), + ElevatedButton( + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => + const NewPublisherScreen(), + ).then((value) { + if (value != null) { + ref.invalidate(publishersManagedProvider); + } + }); + }, + child: Text('createPublisher').tr(), ), + ], + ).center(), + ) + : SingleChildScrollView( + child: Column( + children: [ + for (final publisher in value) + ListTile( + leading: ProfilePictureWidget( + file: publisher.picture, + ), + title: Text(publisher.nick), + subtitle: Text('@${publisher.name}'), + onTap: () { + Navigator.pop(context, publisher); + }, + ), + ], + ), + ), loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Text('Error: $e'), ), diff --git a/lib/widgets/realm/realm_list_tile.dart b/lib/widgets/realm/realm_list_tile.dart index 94e9c3fd..c723a78e 100644 --- a/lib/widgets/realm/realm_list_tile.dart +++ b/lib/widgets/realm/realm_list_tile.dart @@ -30,17 +30,16 @@ class RealmListTile extends StatelessWidget { borderRadius: const BorderRadius.all(Radius.circular(8)), child: Container( color: Theme.of(context).colorScheme.surfaceContainer, - child: - realm.background == null - ? const SizedBox.shrink() - : CloudImageWidget(file: realm.background), + child: realm.background == null + ? const SizedBox.shrink() + : CloudImageWidget(file: realm.background), ), ), Positioned( bottom: -30, left: 18, child: ProfilePictureWidget( - fileId: realm.picture?.id, + file: realm.picture, fallbackIcon: Symbols.group, radius: 24, ), diff --git a/lib/widgets/realm/realm_selection_dropdown.dart b/lib/widgets/realm/realm_selection_dropdown.dart index bd7a9b97..d8099028 100644 --- a/lib/widgets/realm/realm_selection_dropdown.dart +++ b/lib/widgets/realm/realm_selection_dropdown.dart @@ -49,7 +49,7 @@ class RealmSelectionDropdown extends StatelessWidget { child: Row( children: [ ProfilePictureWidget( - fileId: realm.picture?.id, + file: realm.picture, fallbackIcon: Symbols.workspaces, radius: 16, ), diff --git a/lib/widgets/share/share_sheet.dart b/lib/widgets/share/share_sheet.dart index dd98aca3..e206a7a9 100644 --- a/lib/widgets/share/share_sheet.dart +++ b/lib/widgets/share/share_sheet.dart @@ -776,19 +776,19 @@ class _ChatRoomOption extends HookConsumerWidget { color: Theme.of(context).colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(16), ), - child: (isDirect && room.picture?.id == null) + child: (isDirect && room.picture == null) ? SplitAvatarWidget( - filesId: validMembers - .map((e) => e.account.profile.picture?.id) + files: validMembers + .map((e) => e.account.profile.picture) .toList(), radius: 16, ) - : room.picture?.id == null + : room.picture == null ? CircleAvatar( radius: 16, child: Text(room.name![0].toUpperCase()), ) - : ProfilePictureWidget(fileId: room.picture?.id, radius: 16), + : ProfilePictureWidget(file: room.picture, radius: 16), ), const SizedBox(height: 4), // Chat room name diff --git a/lib/widgets/stickers/sticker_picker.dart b/lib/widgets/stickers/sticker_picker.dart index d7db8e27..5b8616ca 100644 --- a/lib/widgets/stickers/sticker_picker.dart +++ b/lib/widgets/stickers/sticker_picker.dart @@ -75,31 +75,29 @@ class StickerPicker extends HookConsumerWidget { }, ); }, - loading: - () => const SizedBox( - width: 320, - height: 320, - child: Center(child: CircularProgressIndicator()), - ), - error: - (err, _) => SizedBox( - width: 360, - height: 200, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Symbols.error, size: 28), - const Gap(8), - Text('Error: $err', textAlign: TextAlign.center), - const Gap(12), - FilledButton.icon( - onPressed: () => ref.invalidate(myStickerPacksProvider), - icon: const Icon(Symbols.refresh), - label: Text('retry').tr(), - ), - ], - ).padding(all: 16), - ), + loading: () => const SizedBox( + width: 320, + height: 320, + child: Center(child: CircularProgressIndicator()), + ), + error: (err, _) => SizedBox( + width: 360, + height: 200, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Symbols.error, size: 28), + const Gap(8), + Text('Error: $err', textAlign: TextAlign.center), + const Gap(12), + FilledButton.icon( + onPressed: () => ref.invalidate(myStickerPacksProvider), + icon: const Icon(Symbols.refresh), + label: Text('retry').tr(), + ), + ], + ).padding(all: 16), + ), ), ), ); @@ -263,7 +261,7 @@ class _StickersGrid extends StatelessWidget { child: AspectRatio( aspectRatio: 1, child: CloudImageWidget( - fileId: sticker.image.id, + file: sticker.image, fit: BoxFit.contain, ), ), @@ -310,31 +308,29 @@ class StickerPickerEmbedded extends HookConsumerWidget { }, ); }, - loading: - () => SizedBox( - width: 320, - height: height ?? 320, - child: const Center(child: CircularProgressIndicator()), - ), - error: - (err, _) => SizedBox( - width: 360, - height: height ?? 200, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Symbols.error, size: 28), - const Gap(8), - Text('Error: $err', textAlign: TextAlign.center), - const Gap(12), - FilledButton.icon( - onPressed: () => ref.invalidate(myStickerPacksProvider), - icon: const Icon(Symbols.refresh), - label: Text('retry').tr(), - ), - ], - ).padding(all: 16), - ), + loading: () => SizedBox( + width: 320, + height: height ?? 320, + child: const Center(child: CircularProgressIndicator()), + ), + error: (err, _) => SizedBox( + width: 360, + height: height ?? 200, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Symbols.error, size: 28), + const Gap(8), + Text('Error: $err', textAlign: TextAlign.center), + const Gap(12), + FilledButton.icon( + onPressed: () => ref.invalidate(myStickerPacksProvider), + icon: const Icon(Symbols.refresh), + label: Text('retry').tr(), + ), + ], + ).padding(all: 16), + ), ); } } @@ -386,18 +382,16 @@ class _EmbeddedPackSwitcherState extends State<_EmbeddedPackSwitcher> { duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, decoration: BoxDecoration( - color: - selected - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surfaceContainer, + color: selected + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceContainer, borderRadius: const BorderRadius.all(Radius.circular(8)), - border: - selected - ? Border.all( - color: Theme.of(context).colorScheme.primary, - width: 4, - ) - : null, + border: selected + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 4, + ) + : null, ), margin: const EdgeInsets.only(right: 8), child: InkWell( @@ -413,11 +407,11 @@ class _EmbeddedPackSwitcherState extends State<_EmbeddedPackSwitcher> { builder: (context, value, _) { return packs[i].icon != null ? CloudImageWidget( - file: packs[i].icon!, - ).clipRRect(all: value) + file: packs[i].icon!, + ).clipRRect(all: value) : CloudImageWidget( - file: packs[i].stickers.firstOrNull?.image, - ).clipRRect(all: value); + file: packs[i].stickers.firstOrNull?.image, + ).clipRRect(all: value); }, ), ), @@ -458,18 +452,17 @@ Future showStickerPickerPopover( offset: offset, alignment: alignment ?? Alignment.topLeft, dimBackground: true, - builder: - (ctx) => SizedBox( - width: math.min(480, MediaQuery.of(context).size.width * 0.9), - height: 480, - child: ProviderScope( - child: StickerPicker( - onPick: (ph) { - onPick(ph); - Navigator.of(ctx).maybePop(); - }, - ), - ), + builder: (ctx) => SizedBox( + width: math.min(480, MediaQuery.of(context).size.width * 0.9), + height: 480, + child: ProviderScope( + child: StickerPicker( + onPick: (ph) { + onPick(ph); + Navigator.of(ctx).maybePop(); + }, ), + ), + ), ); }